Complete admin model management
This commit is contained in:
@@ -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 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="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>
|
||||
|
||||
Reference in New Issue
Block a user