feat: 添加角色设定预留API 设定页(#768)

* add systemMessage

* perf: 优化代码和类型

* perf: 补全翻译和为以后做准备

---------

Co-authored-by: ChenZhaoYu <790348264@qq.com>
This commit is contained in:
quzard 2023-03-22 17:47:07 +08:00 committed by GitHub
parent e02ab1fbad
commit 6ecc61ac5d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 160 additions and 22 deletions

View File

@ -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 }
}

View File

@ -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
}

View File

@ -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) => {
res.write(firstChunk ? JSON.stringify(chat) : `\n${JSON.stringify(chat)}`)
firstChunk = false
await chatReplyProcess({
message: prompt,
lastContext: options,
process: (chat: ChatMessage) => {
res.write(firstChunk ? JSON.stringify(chat) : `\n${JSON.stringify(chat)}`)
firstChunk = false
},
systemMessage,
})
}
catch (error) {

View File

@ -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

View File

@ -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,
})

View File

@ -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]">&nbsp;</span>
<NButton size="small" @click="handleReset">
{{ $t('common.reset') }}
</NButton>
</div>
</div>
</div>
</template>

View File

@ -150,7 +150,6 @@ function handleImportButtonClick(): void {
{{ $t('common.save') }}
</NButton>
</div>
<div
class="flex items-center space-x-4"
:class="isMobile && 'items-start'"

View File

@ -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" />

View File

@ -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',

View File

@ -52,10 +52,12 @@ export default {
setting: {
setting: '设置',
general: '总览',
advanced: '高级',
config: '配置',
avatarLink: '头像链接',
name: '名称',
description: '描述',
role: '角色设定',
resetUserInfo: '重置用户信息',
chatHistory: '聊天记录',
theme: '主题',

View File

@ -52,10 +52,12 @@ export default {
setting: {
setting: '設定',
general: '總覽',
advanced: '高級',
config: '設定',
avatarLink: '頭貼連結',
name: '名稱',
description: '描述',
role: '角色設定',
resetUserInfo: '重設使用者資訊',
chatHistory: '紀錄',
theme: '主題',

View File

@ -2,4 +2,5 @@ export * from './app'
export * from './chat'
export * from './user'
export * from './prompt'
export * from './settings'
export * from './auth'

View File

@ -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)
}

View File

@ -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)
},
},
})