Complete admin model management

This commit is contained in:
2026-07-07 12:58:26 +08:00
parent 08c301c136
commit 55b7c5b0ea
12 changed files with 859 additions and 40 deletions

View File

@@ -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.createModel({ ...modelForm });
ElMessage.success("模型已新增");
Object.assign(modelForm, {
provider: "openai-compatible",
modelName: "",
apiUrl: "",
apiKey: "",
temperature: 0.2,
maxToken: 1024,
timeoutSecond: 30,
});
if (!validateModelForm()) 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 MessagesGemini 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="apiUrl" label="API URL" />
<el-table-column prop="baseUrl" 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="110">
<template #default="{ row }"><el-button size="small" @click="enableModel(row.id)">启用</el-button></template>
<el-table-column label="操作" width="210" 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>