@@ -32,6 +32,7 @@ const promptContent = ref("");
const chatDetail = ref < ChatDetail | null > ( null ) ;
const chatDetailOpen = ref ( false ) ;
const recordTab = ref ( "chats" ) ;
const editingModelId = ref < number | null > ( null ) ;
const knowledgeForm = reactive ( {
name : "" ,
@@ -42,15 +43,115 @@ const knowledgeForm = reactive({
} ) ;
const modelForm = reactive ( {
provider : "openai-compatible " ,
provider : "openai" ,
displayName : "" ,
apiType : "openai_compatible" ,
modelName : "" ,
baseUrl : "https://api.openai.com/v1" ,
apiUrl : "" ,
apiKey : "" ,
temperature : 0.2 ,
authType : "bearer" ,
apiVersion : "" ,
temperature : 0.2 as number | null ,
topP : null as number | null ,
topK : null as number | null ,
presencePenalty : null as number | null ,
frequencyPenalty : null as number | null ,
maxToken : 1024 ,
contextWindow : null as number | null ,
streamEnabled : 1 ,
responseFormat : "" ,
extraParams : "" ,
remark : "" ,
timeoutSecond : 30 ,
} ) ;
const modelPresets = [
{
label : "OpenAI" ,
provider : "openai" ,
apiType : "openai_compatible" ,
baseUrl : "https://api.openai.com/v1" ,
apiUrl : "" ,
authType : "bearer" ,
} ,
{
label : "DeepSeek" ,
provider : "deepseek" ,
apiType : "openai_compatible" ,
baseUrl : "https://api.deepseek.com/v1" ,
apiUrl : "" ,
authType : "bearer" ,
} ,
{
label : "通义千问 DashScope" ,
provider : "qwen" ,
apiType : "openai_compatible" ,
baseUrl : "https://dashscope.aliyuncs.com/compatible-mode/v1" ,
apiUrl : "" ,
authType : "bearer" ,
} ,
{
label : "Kimi / Moonshot" ,
provider : "moonshot" ,
apiType : "openai_compatible" ,
baseUrl : "https://api.moonshot.cn/v1" ,
apiUrl : "" ,
authType : "bearer" ,
} ,
{
label : "硅基流动 SiliconFlow" ,
provider : "siliconflow" ,
apiType : "openai_compatible" ,
baseUrl : "https://api.siliconflow.cn/v1" ,
apiUrl : "" ,
authType : "bearer" ,
} ,
{
label : "火山方舟 Ark" ,
provider : "volcengine-ark" ,
apiType : "openai_compatible" ,
baseUrl : "https://ark.cn-beijing.volces.com/api/v3" ,
apiUrl : "" ,
authType : "bearer" ,
} ,
{
label : "Anthropic Claude" ,
provider : "anthropic" ,
apiType : "anthropic_messages" ,
baseUrl : "https://api.anthropic.com" ,
apiUrl : "" ,
authType : "api_key" ,
apiVersion : "2023-06-01" ,
temperature : null ,
topP : null ,
topK : null ,
} ,
{
label : "Google Gemini" ,
provider : "gemini" ,
apiType : "gemini_generate_content" ,
baseUrl : "https://generativelanguage.googleapis.com/v1beta" ,
apiUrl : "" ,
authType : "api_key" ,
temperature : null ,
topP : null ,
topK : null ,
} ,
] ;
const nullableModelFields = [
"temperature" ,
"topP" ,
"topK" ,
"presencePenalty" ,
"frequencyPenalty" ,
"contextWindow" ,
"responseFormat" ,
"extraParams" ,
"remark" ,
] as const ;
const configForm = reactive ( {
configKey : "daily_chat_limit" ,
configValue : "100" ,
@@ -143,26 +244,141 @@ async function resetPrompt() {
}
async function saveModel ( ) {
await api . cre ateModel( { ... modelForm } ) ;
ElMessage . success ( "模型已新增" ) ;
Object . assign ( modelForm , {
provider : "openai-compatible" ,
modelName : "" ,
apiUrl : "" ,
apiKey : "" ,
temperature : 0.2 ,
maxToken : 1024 ,
timeoutSecond : 30 ,
} ) ;
if ( ! valid ateModelForm ( ) ) return ;
const payload = buildModelPayload ( ) ;
if ( editingModelId . value ) {
await api . updateModel ( editingModelId . value , payload ) ;
ElMessage . success ( "模型已更新" ) ;
} else {
await api . createModel ( payload ) ;
ElMessage . success ( "模型已新增" ) ;
}
resetModelForm ( ) ;
await loadCurrentMenu ( ) ;
}
function buildModelPayload ( ) {
const payload = { ... modelForm } as Record < string , unknown > ;
nullableModelFields . forEach ( ( field ) => {
if ( payload [ field ] === undefined || payload [ field ] === "" ) {
payload [ field ] = null ;
}
} ) ;
return payload ;
}
async function enableModel ( id : number ) {
await api . enableModel ( id ) ;
ElMessage . success ( "模型已启用" ) ;
await loadCurrentMenu ( ) ;
}
async function testModel ( id : number ) {
const result = await api . testModel ( id ) ;
if ( result . ok ) {
ElMessage . success ( result . message ) ;
} else {
ElMessage . error ( result . message ) ;
}
}
function applyModelPreset ( label : string ) {
const preset = modelPresets . find ( ( item ) => item . label === label ) ;
if ( ! preset ) return ;
Object . assign (
modelForm ,
{
apiVersion : "" ,
authType : "bearer" ,
temperature : 0.2 ,
topP : null ,
topK : null ,
presencePenalty : null ,
frequencyPenalty : null ,
responseFormat : "" ,
streamEnabled : 1 ,
} ,
preset ,
) ;
}
function editModel ( row : ModelItem ) {
editingModelId . value = row . id ;
Object . assign ( modelForm , {
provider : row . provider ,
displayName : row . displayName ? ? "" ,
apiType : row . apiType ,
modelName : row . modelName ,
baseUrl : row . baseUrl ? ? "" ,
apiUrl : row . apiUrl ,
apiKey : "" ,
authType : row . authType ,
apiVersion : row . apiVersion ? ? "" ,
temperature : row . temperature ? ? null ,
topP : row . topP ? ? null ,
topK : row . topK ? ? null ,
presencePenalty : row . presencePenalty ? ? null ,
frequencyPenalty : row . frequencyPenalty ? ? null ,
maxToken : row . maxToken ? ? 1024 ,
contextWindow : row . contextWindow ? ? null ,
streamEnabled : row . streamEnabled ,
responseFormat : row . responseFormat ? ? "" ,
extraParams : row . extraParams ? ? "" ,
remark : row . remark ? ? "" ,
timeoutSecond : row . timeoutSecond ,
} ) ;
}
function resetModelForm ( ) {
editingModelId . value = null ;
Object . assign ( modelForm , {
provider : "openai" ,
displayName : "" ,
apiType : "openai_compatible" ,
modelName : "" ,
baseUrl : "https://api.openai.com/v1" ,
apiUrl : "" ,
apiKey : "" ,
authType : "bearer" ,
apiVersion : "" ,
temperature : 0.2 ,
topP : null ,
topK : null ,
presencePenalty : null ,
frequencyPenalty : null ,
maxToken : 1024 ,
contextWindow : null ,
streamEnabled : 1 ,
responseFormat : "" ,
extraParams : "" ,
remark : "" ,
timeoutSecond : 30 ,
} ) ;
}
function validateModelForm ( ) {
if ( ! modelForm . apiUrl && ! modelForm . baseUrl ) {
ElMessage . error ( "请填写 Base URL 或 API URL" ) ;
return false ;
}
if ( ! editingModelId . value && ! modelForm . apiKey ) {
ElMessage . error ( "新增模型必须填写 API Key" ) ;
return false ;
}
if ( modelForm . extraParams . trim ( ) ) {
try {
const extraParams = JSON . parse ( modelForm . extraParams ) ;
if ( ! extraParams || Array . isArray ( extraParams ) || typeof extraParams !== "object" ) {
throw new Error ( "高级 JSON 参数必须是对象" ) ;
}
} catch ( error ) {
ElMessage . error ( error instanceof Error ? error . message : "高级 JSON 参数不是合法 JSON" ) ;
return false ;
}
}
return true ;
}
async function saveConfig ( ) {
await api . saveConfig ( { ... configForm } ) ;
ElMessage . success ( "配置已保存" ) ;
@@ -299,21 +515,117 @@ function buildChatQuery() {
< / template >
< template v-if = "activeMenu === 'models'" >
< div class = "page-head" > < h2 > 模型管理 < / h2 > < p > 同一时间只启用一个模型 。 < / p > < / div >
< el-form class = "model-form" label -position = " top " :model = "modelForm" >
< el-input v-model = "modelForm.provider" placeholder="Provider" / >
< el-input v-model = "modelForm.modelName" placeholder="模型名" / >
< el-input v- model= "modelForm.apiUrl" placeholder="API URL" / >
< el-input v-model = "modelForm.apiKey" placeholder="API Key" show -password / >
< el-button type = "primary" @click ="saveModel" > 新增模型 < / el -button >
< div class = "page-head inline " >
< div > < h2 > 模型管理 < / h2 > < p > 支持 OpenAI 兼容 、 Anthropic Messages 、 Gemini generateContent 和常见第三方 LLM 。 < / p > < / div >
< el-button @click ="resetModelForm" > 清空表单 < / el -button >
< /div >
< el-form class = "model-config-panel" label -position = " top " : model= "modelForm" >
< div class = "model-config-grid" >
< el-form-item label = "供应商预设" >
< el-select placeholder = "选择预设" clearable @change ="applyModelPreset" >
< el -option v-for = "preset in modelPresets" :key="preset.label" :label="preset.label" :value="preset.label" / >
< / el-select >
< / el-form-item >
< el-form-item label = "供应商标识" >
< el-input v-model = "modelForm.provider" placeholder="openai / deepseek / qwen" / >
< / el-form-item >
< el-form-item label = "显示名称" >
< el-input v-model = "modelForm.displayName" placeholder="例如: DeepSeek V3 生产" / >
< / el-form-item >
< el-form-item label = "协议类型" >
< el-select v-model = "modelForm.apiType" >
< el -option label = "OpenAI 兼容" value = "openai_compatible" / >
< el-option label = "Anthropic Messages" value = "anthropic_messages" / >
< el-option label = "Gemini generateContent" value = "gemini_generate_content" / >
< / el-select >
< / el-form-item >
< el-form-item label = "模型名" >
< el-input v-model = "modelForm.modelName" placeholder="gpt-4.1 / deepseek-chat / claude..." / >
< / el-form-item >
< el-form-item label = "Base URL" >
< el-input v-model = "modelForm.baseUrl" placeholder="https://api.example.com/v1" / >
< / el-form-item >
< el-form-item label = "API URL( 可覆盖) " >
< el-input v-model = "modelForm.apiUrl" placeholder="留空则按协议自动拼接" / >
< / el-form-item >
< el-form-item label = "鉴权方式" >
< el-select v-model = "modelForm.authType" >
< el -option label = "Bearer Token" value = "bearer" / >
< el-option label = "x-api-key" value = "api_key" / >
< / el-select >
< / el-form-item >
< el-form-item label = "API Key" >
< el-input v-model = "modelForm.apiKey" placeholder="保存时会覆盖旧 Key" show -password / >
< / el-form-item >
< el-form-item label = "API Version" >
< el-input v-model = "modelForm.apiVersion" placeholder="Anthropic 默认 2023-06-01" / >
< / el-form-item >
< el-form-item label = "Temperature" >
< el-input-number v-model = "modelForm.temperature" :min="0" :max="2" :step="0.1" / >
< / el-form-item >
< el-form-item label = "Top P" >
< el-input-number v-model = "modelForm.topP" :min="0" :max="1" :step="0.05" / >
< / el-form-item >
< el-form-item label = "Top K" >
< el-input-number v-model = "modelForm.topK" :min="1" :max="1000" / >
< / el-form-item >
< el-form-item label = "Max Tokens" >
< el-input-number v-model = "modelForm.maxToken" :min="1" :max="100000" / >
< / el-form-item >
< el-form-item label = "上下文窗口" >
< el-input-number v-model = "modelForm.contextWindow" :min="1" :max="2000000" / >
< / el-form-item >
< el-form-item label = "Presence Penalty" >
< el-input-number v-model = "modelForm.presencePenalty" :min="-2" :max="2" :step="0.1" / >
< / el-form-item >
< el-form-item label = "Frequency Penalty" >
< el-input-number v-model = "modelForm.frequencyPenalty" :min="-2" :max="2" :step="0.1" / >
< / el-form-item >
< el-form-item label = "流式输出" >
< el-switch v-model = "modelForm.streamEnabled" :active-value="1" :inactive-value="0" / >
< / el-form-item >
< el-form-item label = "响应格式" >
< el-select v-model = "modelForm.responseFormat" clearable >
< el-option label = "默认文本" value = "" / >
< el-option label = "JSON Object" value = "json_object" / >
< / el-select >
< / el-form-item >
< el-form-item label = "超时秒数" >
< el-input-number v-model = "modelForm.timeoutSecond" :min="1" :max="300" / >
< / el-form-item >
< / div >
< el-form-item label = "高级 JSON 参数" >
< el-input
v-model = "modelForm.extraParams"
type = "textarea"
:rows = "4"
placeholder = '例如:{"stop":["###"],"seed":42}'
/ >
< / el-form-item >
< el-form-item label = "备注" >
< el-input v-model = "modelForm.remark" placeholder="用途、额度、注意事项等" / >
< / el-form-item >
< div class = "actions" >
< el-button type = "primary" @click ="saveModel" > {{ editingModelId ? " 保存模型 " : " 新增模型 " }} < / el -button >
< el-button @click ="resetModelForm" > 取消编辑 < / el -button >
< / div >
< / el-form >
< el-table :data = "models" stripe >
< el-table-column prop = "displayName" label = "显示名称" width = "170" / >
< el-table-column prop = "provider" label = "Provider" / >
< el-table-column prop = "apiType" label = "协议" width = "170" / >
< el-table-column prop = "modelName" label = "模型" / >
< el-table-column prop = "api Url" label = "API URL" / >
< el-table-column prop = "base Url" label = "Base URL" min -width = " 220 " show -overflow -tooltip / >
< el-table-column prop = "temperature" label = "温度" width = "80" / >
< el-table-column prop = "topP" label = "Top P" width = "80" / >
< el-table-column prop = "maxToken" label = "Max" width = "90" / >
< el-table-column prop = "enabled" label = "启用" width = "90" / >
< el-table-column label = "操作" width = "1 10" >
< template # default = "{ row }" > < el-button size = "small" @click ="enableModel(row.id)" > 启用 < / el -button > < / template >
< el-table-column label = "操作" width = "2 10" fixed = "right" >
< template # default = "{ row }" >
< el-button size = "small" @click ="editModel(row)" > 编辑 < / el -button >
< el-button size = "small" @click ="testModel(row.id)" > 测试 < / el -button >
< el-button size = "small" type = "primary" @click ="enableModel(row.id)" > 启用 < / el -button >
< / template >
< / el-table-column >
< / el-table >
< / template >