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 { isNotEmptyString } from '../utils/is'
|
||||
import type { ApiModel, ChatContext, ChatGPTUnofficialProxyAPIOptions, ModelConfig } from '../types'
|
||||
import type { RequestOptions } from './types'
|
||||
|
||||
dotenv.config()
|
||||
|
||||
const ErrorCodeMessage: Record<string, string> = {
|
||||
401: '[OpenAI] 提供错误的API密钥 | Incorrect API key provided',
|
||||
|
@ -19,13 +22,11 @@ const ErrorCodeMessage: Record<string, string> = {
|
|||
500: '[OpenAI] 服务器繁忙,请稍后再试 | Internal Server Error',
|
||||
}
|
||||
|
||||
dotenv.config()
|
||||
|
||||
const timeoutMs: number = !isNaN(+process.env.TIMEOUT_MS) ? +process.env.TIMEOUT_MS : 30 * 1000
|
||||
|
||||
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')
|
||||
|
||||
let api: ChatGPTAPI | ChatGPTUnofficialProxyAPI
|
||||
|
@ -33,7 +34,7 @@ let api: ChatGPTAPI | ChatGPTUnofficialProxyAPI
|
|||
(async () => {
|
||||
// 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 model = isNotEmptyString(OPENAI_API_MODEL) ? OPENAI_API_MODEL : 'gpt-3.5-turbo'
|
||||
|
||||
|
@ -67,17 +68,19 @@ let api: ChatGPTAPI | ChatGPTUnofficialProxyAPI
|
|||
}
|
||||
})()
|
||||
|
||||
async function chatReplyProcess(
|
||||
message: string,
|
||||
lastContext?: { conversationId?: string; parentMessageId?: string },
|
||||
process?: (chat: ChatMessage) => void,
|
||||
) {
|
||||
async function chatReplyProcess(options: RequestOptions) {
|
||||
const { message, lastContext, process, systemMessage } = options
|
||||
try {
|
||||
let options: SendMessageOptions = { timeoutMs }
|
||||
|
||||
if (lastContext) {
|
||||
if (apiModel === 'ChatGPTAPI') {
|
||||
if (isNotEmptyString(systemMessage))
|
||||
options.systemMessage = systemMessage
|
||||
}
|
||||
|
||||
if (lastContext != null) {
|
||||
if (apiModel === 'ChatGPTAPI')
|
||||
options = { parentMessageId: lastContext.parentMessageId }
|
||||
options.parentMessageId = lastContext.parentMessageId
|
||||
else
|
||||
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 type { ChatContext, ChatMessage } from './chatgpt'
|
||||
import type { RequestProps } from './types'
|
||||
import type { ChatMessage } from './chatgpt'
|
||||
import { chatConfig, chatReplyProcess, currentModel } from './chatgpt'
|
||||
import { auth } from './middleware/auth'
|
||||
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')
|
||||
|
||||
try {
|
||||
const { prompt, options = {} } = req.body as { prompt: string; options?: ChatContext }
|
||||
const { prompt, options = {}, systemMessage } = req.body as RequestProps
|
||||
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)}`)
|
||||
firstChunk = false
|
||||
},
|
||||
systemMessage,
|
||||
})
|
||||
}
|
||||
catch (error) {
|
||||
|
|
|
@ -1,5 +1,11 @@
|
|||
import type { FetchFn } from 'chatgpt'
|
||||
|
||||
export interface RequestProps {
|
||||
prompt: string
|
||||
options?: ChatContext
|
||||
systemMessage: string
|
||||
}
|
||||
|
||||
export interface ChatContext {
|
||||
conversationId?: string
|
||||
parentMessageId?: string
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import type { AxiosProgressEvent, GenericAbortSignal } from 'axios'
|
||||
import { post } from '@/utils/request'
|
||||
import { useSettingStore } from '@/store'
|
||||
|
||||
export function fetchChatAPI<T = any>(
|
||||
prompt: string,
|
||||
|
@ -26,9 +27,11 @@ export function fetchChatAPIProcess<T = any>(
|
|||
signal?: GenericAbortSignal
|
||||
onDownloadProgress?: (progressEvent: AxiosProgressEvent) => void },
|
||||
) {
|
||||
const settingStore = useSettingStore()
|
||||
|
||||
return post<T>({
|
||||
url: '/chat-process',
|
||||
data: { prompt: params.prompt, options: params.options },
|
||||
data: { prompt: params.prompt, options: params.options, systemMessage: settingStore.systemMessage },
|
||||
signal: params.signal,
|
||||
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') }}
|
||||
</NButton>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="flex items-center space-x-4"
|
||||
:class="isMobile && 'items-start'"
|
||||
|
|
|
@ -2,13 +2,11 @@
|
|||
import { computed, ref } from 'vue'
|
||||
import { NModal, NTabPane, NTabs } from 'naive-ui'
|
||||
import General from './General.vue'
|
||||
import Advanced from './Advanced.vue'
|
||||
import About from './About.vue'
|
||||
import { useAuthStore } from '@/store'
|
||||
import { SvgIcon } from '@/components/common'
|
||||
|
||||
const props = defineProps<Props>()
|
||||
|
||||
const emit = defineEmits<Emit>()
|
||||
|
||||
interface Props {
|
||||
visible: boolean
|
||||
}
|
||||
|
@ -17,6 +15,14 @@ interface Emit {
|
|||
(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 show = computed({
|
||||
|
@ -42,6 +48,15 @@ const show = computed({
|
|||
<General />
|
||||
</div>
|
||||
</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">
|
||||
<template #tab>
|
||||
<SvgIcon class="text-lg" icon="ri:list-settings-line" />
|
||||
|
|
|
@ -52,10 +52,12 @@ export default {
|
|||
setting: {
|
||||
setting: 'Setting',
|
||||
general: 'General',
|
||||
advanced: 'Advanced',
|
||||
config: 'Config',
|
||||
avatarLink: 'Avatar Link',
|
||||
name: 'Name',
|
||||
description: 'Description',
|
||||
role: 'Role',
|
||||
resetUserInfo: 'Reset UserInfo',
|
||||
chatHistory: 'ChatHistory',
|
||||
theme: 'Theme',
|
||||
|
|
|
@ -52,10 +52,12 @@ export default {
|
|||
setting: {
|
||||
setting: '设置',
|
||||
general: '总览',
|
||||
advanced: '高级',
|
||||
config: '配置',
|
||||
avatarLink: '头像链接',
|
||||
name: '名称',
|
||||
description: '描述',
|
||||
role: '角色设定',
|
||||
resetUserInfo: '重置用户信息',
|
||||
chatHistory: '聊天记录',
|
||||
theme: '主题',
|
||||
|
|
|
@ -52,10 +52,12 @@ export default {
|
|||
setting: {
|
||||
setting: '設定',
|
||||
general: '總覽',
|
||||
advanced: '高級',
|
||||
config: '設定',
|
||||
avatarLink: '頭貼連結',
|
||||
name: '名稱',
|
||||
description: '描述',
|
||||
role: '角色設定',
|
||||
resetUserInfo: '重設使用者資訊',
|
||||
chatHistory: '紀錄',
|
||||
theme: '主題',
|
||||
|
|
|
@ -2,4 +2,5 @@ export * from './app'
|
|||
export * from './chat'
|
||||
export * from './user'
|
||||
export * from './prompt'
|
||||
export * from './settings'
|
||||
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