feat: 添加角色设定预留API 设定页(#768)
* add systemMessage * perf: 优化代码和类型 * perf: 补全翻译和为以后做准备 --------- Co-authored-by: ChenZhaoYu <790348264@qq.com>
This commit is contained in:
parent
e02ab1fbad
commit
6ecc61ac5d
|
@ -9,6 +9,9 @@ import axios from 'axios'
|
||||||
import { sendResponse } from '../utils'
|
import { sendResponse } from '../utils'
|
||||||
import { isNotEmptyString } from '../utils/is'
|
import { isNotEmptyString } from '../utils/is'
|
||||||
import type { ApiModel, ChatContext, ChatGPTUnofficialProxyAPIOptions, ModelConfig } from '../types'
|
import type { ApiModel, ChatContext, ChatGPTUnofficialProxyAPIOptions, ModelConfig } from '../types'
|
||||||
|
import type { RequestOptions } from './types'
|
||||||
|
|
||||||
|
dotenv.config()
|
||||||
|
|
||||||
const ErrorCodeMessage: Record<string, string> = {
|
const ErrorCodeMessage: Record<string, string> = {
|
||||||
401: '[OpenAI] 提供错误的API密钥 | Incorrect API key provided',
|
401: '[OpenAI] 提供错误的API密钥 | Incorrect API key provided',
|
||||||
|
@ -19,13 +22,11 @@ const ErrorCodeMessage: Record<string, string> = {
|
||||||
500: '[OpenAI] 服务器繁忙,请稍后再试 | Internal Server Error',
|
500: '[OpenAI] 服务器繁忙,请稍后再试 | Internal Server Error',
|
||||||
}
|
}
|
||||||
|
|
||||||
dotenv.config()
|
|
||||||
|
|
||||||
const timeoutMs: number = !isNaN(+process.env.TIMEOUT_MS) ? +process.env.TIMEOUT_MS : 30 * 1000
|
const timeoutMs: number = !isNaN(+process.env.TIMEOUT_MS) ? +process.env.TIMEOUT_MS : 30 * 1000
|
||||||
|
|
||||||
let apiModel: ApiModel
|
let apiModel: ApiModel
|
||||||
|
|
||||||
if (!process.env.OPENAI_API_KEY && !process.env.OPENAI_ACCESS_TOKEN)
|
if (!isNotEmptyString(process.env.OPENAI_API_KEY) && !isNotEmptyString(process.env.OPENAI_ACCESS_TOKEN))
|
||||||
throw new Error('Missing OPENAI_API_KEY or OPENAI_ACCESS_TOKEN environment variable')
|
throw new Error('Missing OPENAI_API_KEY or OPENAI_ACCESS_TOKEN environment variable')
|
||||||
|
|
||||||
let api: ChatGPTAPI | ChatGPTUnofficialProxyAPI
|
let api: ChatGPTAPI | ChatGPTUnofficialProxyAPI
|
||||||
|
@ -33,7 +34,7 @@ let api: ChatGPTAPI | ChatGPTUnofficialProxyAPI
|
||||||
(async () => {
|
(async () => {
|
||||||
// More Info: https://github.com/transitive-bullshit/chatgpt-api
|
// More Info: https://github.com/transitive-bullshit/chatgpt-api
|
||||||
|
|
||||||
if (process.env.OPENAI_API_KEY) {
|
if (isNotEmptyString(process.env.OPENAI_API_KEY)) {
|
||||||
const OPENAI_API_MODEL = process.env.OPENAI_API_MODEL
|
const OPENAI_API_MODEL = process.env.OPENAI_API_MODEL
|
||||||
const model = isNotEmptyString(OPENAI_API_MODEL) ? OPENAI_API_MODEL : 'gpt-3.5-turbo'
|
const model = isNotEmptyString(OPENAI_API_MODEL) ? OPENAI_API_MODEL : 'gpt-3.5-turbo'
|
||||||
|
|
||||||
|
@ -67,17 +68,19 @@ let api: ChatGPTAPI | ChatGPTUnofficialProxyAPI
|
||||||
}
|
}
|
||||||
})()
|
})()
|
||||||
|
|
||||||
async function chatReplyProcess(
|
async function chatReplyProcess(options: RequestOptions) {
|
||||||
message: string,
|
const { message, lastContext, process, systemMessage } = options
|
||||||
lastContext?: { conversationId?: string; parentMessageId?: string },
|
|
||||||
process?: (chat: ChatMessage) => void,
|
|
||||||
) {
|
|
||||||
try {
|
try {
|
||||||
let options: SendMessageOptions = { timeoutMs }
|
let options: SendMessageOptions = { timeoutMs }
|
||||||
|
|
||||||
if (lastContext) {
|
if (apiModel === 'ChatGPTAPI') {
|
||||||
|
if (isNotEmptyString(systemMessage))
|
||||||
|
options.systemMessage = systemMessage
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lastContext != null) {
|
||||||
if (apiModel === 'ChatGPTAPI')
|
if (apiModel === 'ChatGPTAPI')
|
||||||
options = { parentMessageId: lastContext.parentMessageId }
|
options.parentMessageId = lastContext.parentMessageId
|
||||||
else
|
else
|
||||||
options = { ...lastContext }
|
options = { ...lastContext }
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,8 @@
|
||||||
|
import type { ChatMessage } from 'chatgpt'
|
||||||
|
|
||||||
|
export interface RequestOptions {
|
||||||
|
message: string
|
||||||
|
lastContext?: { conversationId?: string; parentMessageId?: string }
|
||||||
|
process?: (chat: ChatMessage) => void
|
||||||
|
systemMessage?: string
|
||||||
|
}
|
|
@ -1,5 +1,6 @@
|
||||||
import express from 'express'
|
import express from 'express'
|
||||||
import type { ChatContext, ChatMessage } from './chatgpt'
|
import type { RequestProps } from './types'
|
||||||
|
import type { ChatMessage } from './chatgpt'
|
||||||
import { chatConfig, chatReplyProcess, currentModel } from './chatgpt'
|
import { chatConfig, chatReplyProcess, currentModel } from './chatgpt'
|
||||||
import { auth } from './middleware/auth'
|
import { auth } from './middleware/auth'
|
||||||
import { limiter } from './middleware/limiter'
|
import { limiter } from './middleware/limiter'
|
||||||
|
@ -22,11 +23,16 @@ router.post('/chat-process', [auth, limiter], async (req, res) => {
|
||||||
res.setHeader('Content-type', 'application/octet-stream')
|
res.setHeader('Content-type', 'application/octet-stream')
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { prompt, options = {} } = req.body as { prompt: string; options?: ChatContext }
|
const { prompt, options = {}, systemMessage } = req.body as RequestProps
|
||||||
let firstChunk = true
|
let firstChunk = true
|
||||||
await chatReplyProcess(prompt, options, (chat: ChatMessage) => {
|
await chatReplyProcess({
|
||||||
|
message: prompt,
|
||||||
|
lastContext: options,
|
||||||
|
process: (chat: ChatMessage) => {
|
||||||
res.write(firstChunk ? JSON.stringify(chat) : `\n${JSON.stringify(chat)}`)
|
res.write(firstChunk ? JSON.stringify(chat) : `\n${JSON.stringify(chat)}`)
|
||||||
firstChunk = false
|
firstChunk = false
|
||||||
|
},
|
||||||
|
systemMessage,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
catch (error) {
|
catch (error) {
|
||||||
|
|
|
@ -1,5 +1,11 @@
|
||||||
import type { FetchFn } from 'chatgpt'
|
import type { FetchFn } from 'chatgpt'
|
||||||
|
|
||||||
|
export interface RequestProps {
|
||||||
|
prompt: string
|
||||||
|
options?: ChatContext
|
||||||
|
systemMessage: string
|
||||||
|
}
|
||||||
|
|
||||||
export interface ChatContext {
|
export interface ChatContext {
|
||||||
conversationId?: string
|
conversationId?: string
|
||||||
parentMessageId?: string
|
parentMessageId?: string
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import type { AxiosProgressEvent, GenericAbortSignal } from 'axios'
|
import type { AxiosProgressEvent, GenericAbortSignal } from 'axios'
|
||||||
import { post } from '@/utils/request'
|
import { post } from '@/utils/request'
|
||||||
|
import { useSettingStore } from '@/store'
|
||||||
|
|
||||||
export function fetchChatAPI<T = any>(
|
export function fetchChatAPI<T = any>(
|
||||||
prompt: string,
|
prompt: string,
|
||||||
|
@ -26,9 +27,11 @@ export function fetchChatAPIProcess<T = any>(
|
||||||
signal?: GenericAbortSignal
|
signal?: GenericAbortSignal
|
||||||
onDownloadProgress?: (progressEvent: AxiosProgressEvent) => void },
|
onDownloadProgress?: (progressEvent: AxiosProgressEvent) => void },
|
||||||
) {
|
) {
|
||||||
|
const settingStore = useSettingStore()
|
||||||
|
|
||||||
return post<T>({
|
return post<T>({
|
||||||
url: '/chat-process',
|
url: '/chat-process',
|
||||||
data: { prompt: params.prompt, options: params.options },
|
data: { prompt: params.prompt, options: params.options, systemMessage: settingStore.systemMessage },
|
||||||
signal: params.signal,
|
signal: params.signal,
|
||||||
onDownloadProgress: params.onDownloadProgress,
|
onDownloadProgress: params.onDownloadProgress,
|
||||||
})
|
})
|
||||||
|
|
|
@ -0,0 +1,46 @@
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { NButton, NInput, useMessage } from 'naive-ui'
|
||||||
|
import { useSettingStore } from '@/store'
|
||||||
|
import type { SettingsState } from '@/store/modules/settings/helper'
|
||||||
|
import { t } from '@/locales'
|
||||||
|
|
||||||
|
const settingStore = useSettingStore()
|
||||||
|
|
||||||
|
const ms = useMessage()
|
||||||
|
|
||||||
|
const systemMessage = ref(settingStore.systemMessage ?? '')
|
||||||
|
|
||||||
|
function updateSettings(options: Partial<SettingsState>) {
|
||||||
|
settingStore.updateSetting(options)
|
||||||
|
ms.success(t('common.success'))
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleReset() {
|
||||||
|
settingStore.resetSetting()
|
||||||
|
ms.success(t('common.success'))
|
||||||
|
window.location.reload()
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="p-4 space-y-5 min-h-[200px]">
|
||||||
|
<div class="space-y-6">
|
||||||
|
<div class="flex items-center space-x-4">
|
||||||
|
<span class="flex-shrink-0 w-[100px]">{{ $t('setting.role') }}</span>
|
||||||
|
<div class="flex-1">
|
||||||
|
<NInput v-model:value="systemMessage" placeholder="" />
|
||||||
|
</div>
|
||||||
|
<NButton size="tiny" text type="primary" @click="updateSettings({ systemMessage })">
|
||||||
|
{{ $t('common.save') }}
|
||||||
|
</NButton>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center space-x-4">
|
||||||
|
<span class="flex-shrink-0 w-[100px]"> </span>
|
||||||
|
<NButton size="small" @click="handleReset">
|
||||||
|
{{ $t('common.reset') }}
|
||||||
|
</NButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
|
@ -150,7 +150,6 @@ function handleImportButtonClick(): void {
|
||||||
{{ $t('common.save') }}
|
{{ $t('common.save') }}
|
||||||
</NButton>
|
</NButton>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="flex items-center space-x-4"
|
class="flex items-center space-x-4"
|
||||||
:class="isMobile && 'items-start'"
|
:class="isMobile && 'items-start'"
|
||||||
|
|
|
@ -2,13 +2,11 @@
|
||||||
import { computed, ref } from 'vue'
|
import { computed, ref } from 'vue'
|
||||||
import { NModal, NTabPane, NTabs } from 'naive-ui'
|
import { NModal, NTabPane, NTabs } from 'naive-ui'
|
||||||
import General from './General.vue'
|
import General from './General.vue'
|
||||||
|
import Advanced from './Advanced.vue'
|
||||||
import About from './About.vue'
|
import About from './About.vue'
|
||||||
|
import { useAuthStore } from '@/store'
|
||||||
import { SvgIcon } from '@/components/common'
|
import { SvgIcon } from '@/components/common'
|
||||||
|
|
||||||
const props = defineProps<Props>()
|
|
||||||
|
|
||||||
const emit = defineEmits<Emit>()
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
visible: boolean
|
visible: boolean
|
||||||
}
|
}
|
||||||
|
@ -17,6 +15,14 @@ interface Emit {
|
||||||
(e: 'update:visible', visible: boolean): void
|
(e: 'update:visible', visible: boolean): void
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const props = defineProps<Props>()
|
||||||
|
|
||||||
|
const emit = defineEmits<Emit>()
|
||||||
|
|
||||||
|
const authStore = useAuthStore()
|
||||||
|
|
||||||
|
const isChatGPTAPI = computed<boolean>(() => !!authStore.isChatGPTAPI)
|
||||||
|
|
||||||
const active = ref('General')
|
const active = ref('General')
|
||||||
|
|
||||||
const show = computed({
|
const show = computed({
|
||||||
|
@ -42,6 +48,15 @@ const show = computed({
|
||||||
<General />
|
<General />
|
||||||
</div>
|
</div>
|
||||||
</NTabPane>
|
</NTabPane>
|
||||||
|
<NTabPane v-if="isChatGPTAPI" name="Advanced" tab="Advanced">
|
||||||
|
<template #tab>
|
||||||
|
<SvgIcon class="text-lg" icon="ri:equalizer-line" />
|
||||||
|
<span class="ml-2">{{ $t('setting.advanced') }}</span>
|
||||||
|
</template>
|
||||||
|
<div class="min-h-[100px]">
|
||||||
|
<Advanced />
|
||||||
|
</div>
|
||||||
|
</NTabPane>
|
||||||
<NTabPane name="Config" tab="Config">
|
<NTabPane name="Config" tab="Config">
|
||||||
<template #tab>
|
<template #tab>
|
||||||
<SvgIcon class="text-lg" icon="ri:list-settings-line" />
|
<SvgIcon class="text-lg" icon="ri:list-settings-line" />
|
||||||
|
|
|
@ -52,10 +52,12 @@ export default {
|
||||||
setting: {
|
setting: {
|
||||||
setting: 'Setting',
|
setting: 'Setting',
|
||||||
general: 'General',
|
general: 'General',
|
||||||
|
advanced: 'Advanced',
|
||||||
config: 'Config',
|
config: 'Config',
|
||||||
avatarLink: 'Avatar Link',
|
avatarLink: 'Avatar Link',
|
||||||
name: 'Name',
|
name: 'Name',
|
||||||
description: 'Description',
|
description: 'Description',
|
||||||
|
role: 'Role',
|
||||||
resetUserInfo: 'Reset UserInfo',
|
resetUserInfo: 'Reset UserInfo',
|
||||||
chatHistory: 'ChatHistory',
|
chatHistory: 'ChatHistory',
|
||||||
theme: 'Theme',
|
theme: 'Theme',
|
||||||
|
|
|
@ -52,10 +52,12 @@ export default {
|
||||||
setting: {
|
setting: {
|
||||||
setting: '设置',
|
setting: '设置',
|
||||||
general: '总览',
|
general: '总览',
|
||||||
|
advanced: '高级',
|
||||||
config: '配置',
|
config: '配置',
|
||||||
avatarLink: '头像链接',
|
avatarLink: '头像链接',
|
||||||
name: '名称',
|
name: '名称',
|
||||||
description: '描述',
|
description: '描述',
|
||||||
|
role: '角色设定',
|
||||||
resetUserInfo: '重置用户信息',
|
resetUserInfo: '重置用户信息',
|
||||||
chatHistory: '聊天记录',
|
chatHistory: '聊天记录',
|
||||||
theme: '主题',
|
theme: '主题',
|
||||||
|
|
|
@ -52,10 +52,12 @@ export default {
|
||||||
setting: {
|
setting: {
|
||||||
setting: '設定',
|
setting: '設定',
|
||||||
general: '總覽',
|
general: '總覽',
|
||||||
|
advanced: '高級',
|
||||||
config: '設定',
|
config: '設定',
|
||||||
avatarLink: '頭貼連結',
|
avatarLink: '頭貼連結',
|
||||||
name: '名稱',
|
name: '名稱',
|
||||||
description: '描述',
|
description: '描述',
|
||||||
|
role: '角色設定',
|
||||||
resetUserInfo: '重設使用者資訊',
|
resetUserInfo: '重設使用者資訊',
|
||||||
chatHistory: '紀錄',
|
chatHistory: '紀錄',
|
||||||
theme: '主題',
|
theme: '主題',
|
||||||
|
|
|
@ -2,4 +2,5 @@ export * from './app'
|
||||||
export * from './chat'
|
export * from './chat'
|
||||||
export * from './user'
|
export * from './user'
|
||||||
export * from './prompt'
|
export * from './prompt'
|
||||||
|
export * from './settings'
|
||||||
export * from './auth'
|
export * from './auth'
|
||||||
|
|
|
@ -0,0 +1,23 @@
|
||||||
|
import { ss } from '@/utils/storage'
|
||||||
|
|
||||||
|
const LOCAL_NAME = 'settingsStorage'
|
||||||
|
|
||||||
|
export interface SettingsState {
|
||||||
|
systemMessage: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export function defaultSetting(): SettingsState {
|
||||||
|
const currentDate = new Date().toISOString().split('T')[0]
|
||||||
|
return {
|
||||||
|
systemMessage: `You are ChatGPT, a large language model trained by OpenAI. Answer as concisely as possible.\nKnowledge cutoff: 2021-09-01\nCurrent date: ${currentDate}`,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getLocalState(): SettingsState {
|
||||||
|
const localSetting: SettingsState | undefined = ss.get(LOCAL_NAME)
|
||||||
|
return { ...defaultSetting(), ...localSetting }
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setLocalState(setting: SettingsState): void {
|
||||||
|
ss.set(LOCAL_NAME, setting)
|
||||||
|
}
|
|
@ -0,0 +1,22 @@
|
||||||
|
import { defineStore } from 'pinia'
|
||||||
|
import type { SettingsState } from './helper'
|
||||||
|
import { defaultSetting, getLocalState, setLocalState } from './helper'
|
||||||
|
|
||||||
|
export const useSettingStore = defineStore('setting-store', {
|
||||||
|
state: (): SettingsState => getLocalState(),
|
||||||
|
actions: {
|
||||||
|
updateSetting(settings: Partial<SettingsState>) {
|
||||||
|
this.$state = { ...this.$state, ...settings }
|
||||||
|
this.recordState()
|
||||||
|
},
|
||||||
|
|
||||||
|
resetSetting() {
|
||||||
|
this.$state = defaultSetting()
|
||||||
|
this.recordState()
|
||||||
|
},
|
||||||
|
|
||||||
|
recordState() {
|
||||||
|
setLocalState(this.$state)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
Loading…
Reference in New Issue