Files
QuestionProject/ai_knowledge_base_v2/apps/admin-web/src/App.vue

1157 lines
37 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
import { ElMessage, ElMessageBox } from "element-plus";
import { computed, defineAsyncComponent, onMounted, reactive, ref } from "vue";
import AdminLoginView from "./components/AdminLoginView.vue";
import TableRowActions from "./components/TableRowActions.vue";
import {
systemSettingDefinitions,
type SystemSettingValue,
} from "./config/systemSettings";
import { api, clearToken, getToken, saveToken } from "./services/api";
import type {
AdminProfile,
EntitlementPlan,
ModelItem,
SystemConfigItem,
} from "./types/api";
const AgentManagementView = defineAsyncComponent(
() => import("./components/AgentManagementView.vue"),
);
const AttentionManagementView = defineAsyncComponent(
() => import("./components/AttentionManagementView.vue"),
);
const ContentGenerationConfigView = defineAsyncComponent(
() => import("./components/ContentGenerationConfigView.vue"),
);
const DashboardView = defineAsyncComponent(
() => import("./components/DashboardView.vue"),
);
const EntitlementManagementView = defineAsyncComponent(
() => import("./components/EntitlementManagementView.vue"),
);
const KnowledgeManagementView = defineAsyncComponent(
() => import("./components/KnowledgeManagementView.vue"),
);
const RecordAuditView = defineAsyncComponent(
() => import("./components/RecordAuditView.vue"),
);
const RetrievalLogView = defineAsyncComponent(
() => import("./components/RetrievalLogView.vue"),
);
const UserManagementView = defineAsyncComponent(
() => import("./components/UserManagementView.vue"),
);
const SsoIntegrationView = defineAsyncComponent(
() => import("./components/SsoIntegrationView.vue"),
);
const SystemConfigView = defineAsyncComponent(
() => import("./components/SystemConfigView.vue"),
);
const AdminManagementView = defineAsyncComponent(
() => import("./components/AdminManagementView.vue"),
);
const FeedbackManagementView = defineAsyncComponent(() => import("./components/FeedbackManagementView.vue"));
const UserBehaviorAnalysisView = defineAsyncComponent(() => import("./components/UserBehaviorAnalysisView.vue"));
const admin = ref<AdminProfile | null>(null);
const activeMenu = ref("dashboard");
const loading = ref(false);
const passwordDialogVisible = ref(false);
const passwordSaving = ref(false);
const passwordForm = reactive({ currentPassword: "", newPassword: "", confirmPassword: "" });
const can = (permission: string) => Boolean(admin.value?.isSuperAdmin || admin.value?.permissions.includes(permission));
const menuPermissions: Record<string, string> = {
dashboard: "dashboard.view", users: "users.view", entitlements: "entitlements.view",
knowledge: "knowledge.view", prompt: "prompt.view", models: "models.view",
"content-generation": "content-generation.view", configs: "configs.view", sso: "sso.view",
records: "records.view", retrievals: "retrievals.view", attention: "attention.view", admins: "admins.view",
feedback: "feedback.view",
behavior: "behavior.view",
};
const entitlementPlans = ref<EntitlementPlan[]>([]);
const models = ref<ModelItem[]>([]);
const configs = ref<SystemConfigItem[]>([]);
const savingSystemSettings = ref(false);
const systemSettingsBaseline = ref<Record<string, string>>({});
const previewKnowledgeId = ref<number | null>(null);
const editingModelId = ref<number | null>(null);
const modelForm = reactive({
provider: "openai-compatible",
displayName: "",
apiType: "openai_compatible",
modelName: "",
baseUrl: "https://api.openai.com/v1",
apiUrl: "",
apiKey: "",
authType: "bearer",
apiVersion: "",
temperature: null as number | null,
topP: null as number | null,
topK: null as number | null,
presencePenalty: null as number | null,
frequencyPenalty: null as number | null,
maxToken: 8192 as number | null,
contextWindow: null as number | null,
streamEnabled: 1,
responseFormat: "",
extraParams: "",
remark: "",
timeoutSecond: 30,
inputPricePer1k: null as number | null,
outputPricePer1k: null as number | null,
currency: "CNY",
usageScenarios: "",
allowSummary: 1,
allowReport: 1,
allowFixedInfo: 1,
allowSimpleKnowledge: 1,
allowDeepChat: 1,
});
const quickModelForm = reactive({
provider: "deepseek",
modelName: "deepseek-v4-pro",
apiKey: "",
});
const quickModelProviders = [
{
label: "DeepSeek",
provider: "deepseek",
apiType: "openai_compatible",
baseUrl: "https://api.deepseek.com",
authType: "bearer",
apiVersion: "",
temperature: null,
topP: null,
maxToken: null,
contextWindow: null,
models: [
{ label: "deepseek-v4-pro", value: "deepseek-v4-pro" },
{ label: "deepseek-v4-flash", value: "deepseek-v4-flash" },
],
},
{
label: "MiniMax",
provider: "minimax",
apiType: "openai_compatible",
baseUrl: "https://api.minimaxi.com/v1",
authType: "bearer",
apiVersion: "",
temperature: null,
topP: null,
maxToken: null,
contextWindow: null,
models: [
{ label: "MiniMax-Text-01", value: "MiniMax-Text-01" },
{ label: "MiniMax-M3", value: "MiniMax-M3" },
],
},
] as const;
const protocolPresets = [
{
label: "OpenAI 兼容协议",
provider: "openai-compatible",
apiType: "openai_compatible",
baseUrl: "https://api.openai.com/v1",
apiUrl: "",
authType: "bearer",
},
{
label: "Anthropic Messages 协议",
provider: "anthropic-compatible",
apiType: "anthropic_messages",
baseUrl: "https://api.anthropic.com",
apiUrl: "",
authType: "api_key",
apiVersion: "2023-06-01",
temperature: null,
topP: null,
topK: null,
},
];
const nullableModelFields = [
"temperature",
"topP",
"topK",
"presencePenalty",
"frequencyPenalty",
"contextWindow",
"responseFormat",
"extraParams",
"remark",
"inputPricePer1k",
"outputPricePer1k",
"usageScenarios",
] as const;
const systemSettingValues = reactive<Record<string, SystemSettingValue>>({});
systemSettingDefinitions.forEach((setting) => {
systemSettingValues[setting.key] = setting.defaultValue;
});
const systemSettingsDirty = computed(() =>
systemSettingDefinitions.some(
(setting) =>
serializeSystemSettingValue(systemSettingValues[setting.key]) !==
systemSettingsBaseline.value[setting.key],
),
);
onMounted(async () => {
if (!getToken()) return;
try {
admin.value = await api.profile();
passwordDialogVisible.value = admin.value.mustChangePassword;
if (!admin.value.mustChangePassword) await enterFirstAvailableMenu();
} catch {
clearToken();
}
});
async function login(credentials: { username: string; password: string }) {
loading.value = true;
try {
const result = await api.login(credentials.username, credentials.password);
saveToken(result.token);
admin.value = result.admin;
passwordDialogVisible.value = result.admin.mustChangePassword;
if (!result.admin.mustChangePassword) await enterFirstAvailableMenu();
} catch (error) {
ElMessage.error(error instanceof Error ? error.message : "登录失败");
} finally {
loading.value = false;
}
}
async function logout() {
try {
await api.logout();
} finally {
clearToken();
admin.value = null;
}
}
async function switchMenu(menu: string) {
if (!can(menuPermissions[menu])) return;
activeMenu.value = menu;
await loadCurrentMenu();
}
async function enterFirstAvailableMenu() {
const first = Object.keys(menuPermissions).find((menu) => can(menuPermissions[menu]));
activeMenu.value = first ?? "dashboard";
if (first) await loadCurrentMenu();
}
async function changeInitialPassword() {
if (passwordForm.newPassword.length < 8) return ElMessage.warning("新密码至少 8 位");
if (passwordForm.newPassword !== passwordForm.confirmPassword) return ElMessage.warning("两次输入的新密码不一致");
passwordSaving.value = true;
try {
await api.changeAdminPassword({ currentPassword: passwordForm.currentPassword, newPassword: passwordForm.newPassword });
if (admin.value) admin.value.mustChangePassword = false;
passwordDialogVisible.value = false;
Object.assign(passwordForm, { currentPassword: "", newPassword: "", confirmPassword: "" });
ElMessage.success("密码已修改,请使用新密码登录");
await logout();
} catch (error) { ElMessage.error(error instanceof Error ? error.message : "密码修改失败"); }
finally { passwordSaving.value = false; }
}
async function previewKnowledge(knowledgeId: number) {
previewKnowledgeId.value = knowledgeId;
await switchMenu("prompt");
}
function formatMoney(value?: number | null, currency = "CNY") {
if (typeof value !== "number") return "-";
if (currency === "MIXED") return "多币种,见明细";
return `${currency || "CNY"} ${value.toFixed(6)}`;
}
async function loadCurrentMenu() {
loading.value = true;
try {
if (activeMenu.value === "entitlements") await loadEntitlementPlans();
if (activeMenu.value === "models") models.value = await api.models();
if (activeMenu.value === "configs") {
configs.value = await api.configs();
hydrateSystemSettings();
}
} finally {
loading.value = false;
}
}
async function loadEntitlementPlans() {
entitlementPlans.value = await api.entitlementPlans(true);
}
async function saveModel() {
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;
}
const selectedQuickProvider = computed(() => {
return (
quickModelProviders.find(
(item) => item.provider === quickModelForm.provider,
) ?? quickModelProviders[0]
);
});
async function quickAddModel() {
const provider = selectedQuickProvider.value;
if (!provider) return;
if (!quickModelForm.apiKey) {
ElMessage.error("请填写 API Key");
return;
}
await api.createModel({
provider: provider.provider,
displayName: `${provider.label} ${quickModelForm.modelName}`,
apiType: provider.apiType,
modelName: quickModelForm.modelName,
baseUrl: provider.baseUrl,
apiUrl: "",
apiKey: quickModelForm.apiKey,
authType: provider.authType,
apiVersion: provider.apiVersion,
temperature: null,
topP: null,
topK: null,
presencePenalty: null,
frequencyPenalty: null,
maxToken: 8192,
contextWindow: null,
streamEnabled: 1,
responseFormat: null,
extraParams: null,
remark: "快速添加",
timeoutSecond: 30,
inputPricePer1k: null,
outputPricePer1k: null,
currency: "CNY",
usageScenarios: "正式对话",
allowSummary: 1,
allowReport: 1,
allowFixedInfo: 1,
allowSimpleKnowledge: 1,
allowDeepChat: 1,
});
ElMessage.success(`${provider.label} 模型已新增`);
quickModelForm.apiKey = "";
await loadCurrentMenu();
}
function applyQuickProvider(provider: string) {
const target = quickModelProviders.find((item) => item.provider === provider);
if (!target) return;
quickModelForm.modelName = target.models[0]?.value ?? "";
}
async function setModelAvailability(row: ModelItem, enabled: number) {
await api.setModelAvailability(row.id, enabled);
ElMessage.success(enabled === 1 ? "模型已加入可用池" : "模型已停用");
await loadCurrentMenu();
}
async function setDefaultModel(row: ModelItem) {
await api.setDefaultModel(row.id);
ElMessage.success("默认主模型已更新");
await loadCurrentMenu();
}
async function handleModelCommand(command: string, row: ModelItem) {
if (command === "enable") return setModelAvailability(row, 1);
if (command === "disable") return setModelAvailability(row, 0);
if (command === "default") return setDefaultModel(row);
return deleteModel(row.id);
}
async function deleteModel(id: number) {
try {
await ElMessageBox.confirm("确认删除该模型?删除后不可恢复。", "删除模型", {
confirmButtonText: "确认删除",
cancelButtonText: "取消",
type: "warning",
});
} catch {
return;
}
await api.deleteModel(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 = protocolPresets.find((item) => item.label === label);
if (!preset) return;
Object.assign(
modelForm,
{
apiVersion: "",
authType: "bearer",
temperature: null,
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 ?? 8192,
contextWindow: row.contextWindow ?? null,
streamEnabled: row.streamEnabled,
responseFormat: row.responseFormat ?? "",
extraParams: row.extraParams ?? "",
remark: row.remark ?? "",
timeoutSecond: row.timeoutSecond,
inputPricePer1k: row.inputPricePer1k ?? null,
outputPricePer1k: row.outputPricePer1k ?? null,
currency: row.currency || "CNY",
usageScenarios: row.usageScenarios ?? "",
allowSummary: row.allowSummary ?? 1,
allowReport: row.allowReport ?? 1,
allowFixedInfo: row.allowFixedInfo ?? 1,
allowSimpleKnowledge: row.allowSimpleKnowledge ?? 1,
allowDeepChat: row.allowDeepChat ?? 1,
});
}
function resetModelForm() {
editingModelId.value = null;
Object.assign(modelForm, {
provider: "openai-compatible",
displayName: "",
apiType: "openai_compatible",
modelName: "",
baseUrl: "https://api.openai.com/v1",
apiUrl: "",
apiKey: "",
authType: "bearer",
apiVersion: "",
temperature: null,
topP: null,
topK: null,
presencePenalty: null,
frequencyPenalty: null,
maxToken: 8192,
contextWindow: null,
streamEnabled: 1,
responseFormat: "",
extraParams: "",
remark: "",
timeoutSecond: 30,
inputPricePer1k: null,
outputPricePer1k: null,
currency: "CNY",
usageScenarios: "",
allowSummary: 1,
allowReport: 1,
allowFixedInfo: 1,
allowSimpleKnowledge: 1,
allowDeepChat: 1,
});
}
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 saveSystemSettings() {
if (savingSystemSettings.value || !systemSettingsDirty.value) return;
savingSystemSettings.value = true;
try {
await Promise.all(
systemSettingDefinitions.map((setting) =>
api.saveConfig({
configKey: setting.key,
configValue: serializeSystemSettingValue(
systemSettingValues[setting.key],
),
description: setting.description,
}),
),
);
configs.value = await api.configs();
hydrateSystemSettings();
ElMessage.success("系统配置已保存");
} catch (error) {
ElMessage.error(
error instanceof Error ? error.message : "系统配置保存失败,请重试",
);
} finally {
savingSystemSettings.value = false;
}
}
function hydrateSystemSettings() {
const configMap = new Map(
configs.value.map((item) => [item.configKey, item.configValue]),
);
systemSettingDefinitions.forEach((setting) => {
systemSettingValues[setting.key] = normalizeSystemSettingValue(
setting,
configMap.get(setting.key),
);
});
systemSettingsBaseline.value = Object.fromEntries(
systemSettingDefinitions.map((setting) => [
setting.key,
serializeSystemSettingValue(systemSettingValues[setting.key]),
]),
);
}
function updateSystemSetting(key: string, value: SystemSettingValue) {
if (systemSettingValues[key] === value) return;
systemSettingValues[key] = value;
}
function normalizeSystemSettingValue(
setting: (typeof systemSettingDefinitions)[number],
rawValue?: string,
) {
if (rawValue === undefined || rawValue === "") {
return setting.defaultValue;
}
if (setting.type === "switch") {
return ["1", "true", "yes", "on", "启用"].includes(rawValue.toLowerCase());
}
if (
setting.type === "text" ||
setting.type === "password" ||
setting.type === "select"
) {
return rawValue;
}
const parsed = Number(rawValue);
if (Number.isFinite(parsed)) {
return parsed;
}
return setting.defaultValue;
}
function serializeSystemSettingValue(value: SystemSettingValue) {
if (typeof value === "boolean") return value ? "true" : "false";
return String(value);
}
async function clearFeishuCache() {
try {
const result = await api.clearFeishuCache();
ElMessage.success(result.message || `已清空 ${result.cleared} 条缓存`);
} catch (e: any) {
ElMessage.error(e.message || "刷新缓存失败");
}
}
</script>
<template>
<AdminLoginView v-if="!admin" :loading="loading" @submit="login" />
<main v-else class="admin-shell">
<aside class="sidebar">
<div class="sidebar-title">大本营千问千答</div>
<button
v-if="can('dashboard.view')"
:class="{ active: activeMenu === 'dashboard' }"
@click="switchMenu('dashboard')"
>
数据看板
</button>
<button
v-if="can('users.view')"
:class="{ active: activeMenu === 'users' }"
@click="switchMenu('users')"
>
用户管理
</button>
<button
v-if="can('entitlements.view')"
:class="{ active: activeMenu === 'entitlements' }"
@click="switchMenu('entitlements')"
>
权益管理
</button>
<button
v-if="can('knowledge.view')"
:class="{ active: activeMenu === 'knowledge' }"
@click="switchMenu('knowledge')"
>
知识库管理
</button>
<button
v-if="can('prompt.view')"
:class="{ active: activeMenu === 'prompt' }"
@click="switchMenu('prompt')"
>
Agent 管理
</button>
<button
v-if="can('models.view')"
:class="{ active: activeMenu === 'models' }"
@click="switchMenu('models')"
>
模型管理
</button>
<button
v-if="can('content-generation.view')"
:class="{ active: activeMenu === 'content-generation' }"
@click="switchMenu('content-generation')"
>
内容生成
</button>
<button
v-if="can('configs.view')"
:class="{ active: activeMenu === 'configs' }"
@click="switchMenu('configs')"
>
系统配置
</button>
<button
v-if="can('sso.view')"
:class="{ active: activeMenu === 'sso' }"
@click="switchMenu('sso')"
>
应用接入
</button>
<button
v-if="can('records.view')"
:class="{ active: activeMenu === 'records' }"
@click="switchMenu('records')"
>
记录审计
</button>
<button
v-if="can('retrievals.view')"
:class="{ active: activeMenu === 'retrievals' }"
@click="switchMenu('retrievals')"
>
检索日志
</button>
<button
v-if="can('feedback.view')"
:class="{ active: activeMenu === 'feedback' }"
@click="switchMenu('feedback')"
>反馈管理</button>
<button
v-if="can('behavior.view')"
:class="{ active: activeMenu === 'behavior' }"
@click="switchMenu('behavior')"
>用户行为分析</button>
<button
v-if="can('attention.view')"
:class="{ active: activeMenu === 'attention' }"
@click="switchMenu('attention')"
>
人工关注
</button>
<button
v-if="admin.isSuperAdmin"
:class="{ active: activeMenu === 'admins' }"
@click="switchMenu('admins')"
>管理员与权限</button>
</aside>
<section class="workspace">
<header class="topbar">
<div>
<strong>{{ admin.name }}</strong>
<span>管理员</span>
</div>
<el-button @click="logout">退出</el-button>
</header>
<section v-loading="loading" class="content">
<DashboardView v-if="activeMenu === 'dashboard'" />
<UserManagementView v-if="activeMenu === 'users'" />
<template v-if="activeMenu === 'entitlements'">
<EntitlementManagementView
:plans="entitlementPlans"
@refreshed="loadEntitlementPlans"
/>
</template>
<template v-if="activeMenu === 'knowledge'">
<KnowledgeManagementView @preview="previewKnowledge" />
</template>
<template v-if="activeMenu === 'prompt'">
<AgentManagementView
:preview-knowledge-id="previewKnowledgeId"
:can-batch="can('prompt.batch')"
@consumed-preview="previewKnowledgeId = null"
/>
</template>
<template v-if="activeMenu === 'models'">
<div class="page-head inline">
<div>
<h2>模型管理</h2>
<p>
可同时启用多个模型但只保留一个默认主模型正式聊天始终使用主模型报告和摘要按能力标签分流并自动回退主模型
</p>
</div>
<el-button @click="resetModelForm">清空表单</el-button>
</div>
<section class="quick-model-panel">
<div class="quick-model-head">
<div>
<h3>快速添加</h3>
<p>
适用于 DeepSeek MiniMax只需要填写 API Key 并选择模型
</p>
</div>
</div>
<div class="quick-model-grid">
<el-form-item label="厂商">
<el-select
v-model="quickModelForm.provider"
@change="applyQuickProvider"
>
<el-option
v-for="provider in quickModelProviders"
:key="provider.provider"
:label="provider.label"
:value="provider.provider"
/>
</el-select>
</el-form-item>
<el-form-item label="模型">
<el-select
v-model="quickModelForm.modelName"
:key="quickModelForm.provider"
>
<el-option
v-for="model in selectedQuickProvider.models"
:key="model.value"
:label="model.label"
:value="model.value"
/>
</el-select>
</el-form-item>
<el-form-item label="API Key">
<el-input
v-model="quickModelForm.apiKey"
placeholder="填写后保存,列表中只展示掩码"
show-password
/>
</el-form-item>
<el-form-item label=" ">
<el-button
type="primary"
class="full-btn"
@click="quickAddModel"
>快速新增</el-button
>
</el-form-item>
</div>
</section>
<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 protocolPresets"
: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="custom-openai / custom-anthropic"
/>
</el-form-item>
<el-form-item label="显示名称">
<el-input
v-model="modelForm.displayName"
placeholder="例如:自建网关生产模型"
/>
</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="MiniMax 官方" value="minimax" />
</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="超时秒数">
<el-input-number
v-model="modelForm.timeoutSecond"
:min="1"
:max="300"
/>
</el-form-item>
<el-form-item label="输入单价/千Token">
<el-input-number
v-model="modelForm.inputPricePer1k"
:min="0"
:precision="6"
:step="0.001"
placeholder="不填则不估算成本"
/>
</el-form-item>
<el-form-item label="输出单价/千Token">
<el-input-number
v-model="modelForm.outputPricePer1k"
:min="0"
:precision="6"
:step="0.001"
placeholder="不填则不估算成本"
/>
</el-form-item>
<el-form-item label="币种">
<el-input
v-model="modelForm.currency"
placeholder="CNY / USD"
maxlength="10"
/>
</el-form-item>
</div>
<el-form-item label="适用场景">
<el-input
v-model="modelForm.usageScenarios"
placeholder="例如:正式对话、摘要、固定信息、报告批处理"
/>
</el-form-item>
<el-form-item label="可用能力">
<div>
<div class="entitlement-capabilities">
<el-checkbox
v-model="modelForm.allowFixedInfo"
:true-value="1"
:false-value="0"
>固定信息</el-checkbox
>
<el-checkbox
v-model="modelForm.allowSimpleKnowledge"
:true-value="1"
:false-value="0"
>简单知识</el-checkbox
>
<el-checkbox
v-model="modelForm.allowDeepChat"
:true-value="1"
:false-value="0"
>深度对话</el-checkbox
>
<el-checkbox
v-model="modelForm.allowSummary"
:true-value="1"
:false-value="0"
>摘要沉淀</el-checkbox
>
<el-checkbox
v-model="modelForm.allowReport"
:true-value="1"
:false-value="0"
>周期报告</el-checkbox
>
</div>
<small class="model-routing-help"
>默认主模型勾选某项能力时会优先承担该场景如需专用模型接管请取消主模型的对应能力并为专用模型开启该能力</small
>
</div>
</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 label="估算单价" width="180">
<template #default="{ row }"
>{{ row.currency || "CNY" }} {{ row.inputPricePer1k ?? "-" }}/{{
row.outputPricePer1k ?? "-"
}}</template
>
</el-table-column>
<el-table-column label="分流能力" min-width="220">
<template #default="{ row }">
<div class="model-capability-tags">
<el-tag v-if="row.allowDeepChat === 1" size="small"
>深度对话</el-tag
>
<el-tag v-if="row.allowFixedInfo === 1" size="small"
>固定信息</el-tag
>
<el-tag v-if="row.allowSimpleKnowledge === 1" size="small"
>简单知识</el-tag
>
<el-tag
v-if="row.allowSummary === 1"
size="small"
type="success"
>摘要</el-tag
>
<el-tag
v-if="row.allowReport === 1"
size="small"
type="warning"
>报告</el-tag
>
</div>
</template>
</el-table-column>
<el-table-column
prop="baseUrl"
label="Base URL"
min-width="220"
show-overflow-tooltip
/>
<el-table-column prop="authType" label="鉴权" width="110" />
<el-table-column label="运行状态" width="150">
<template #default="{ row }">
<el-tag :type="row.enabled === 1 ? 'success' : 'info'">{{
row.enabled === 1 ? "可用" : "停用"
}}</el-tag>
<el-tag v-if="row.isDefault === 1" class="model-default-tag"
>主模型</el-tag
>
</template>
</el-table-column>
<el-table-column
label="操作"
width="190"
fixed="right"
align="center"
>
<template #default="{ row }">
<TableRowActions
><el-button size="small" @click="editModel(row)"
>编辑</el-button
><el-button size="small" @click="testModel(row.id)"
>测试</el-button
><el-dropdown
trigger="click"
teleported
@command="
(command: string) => handleModelCommand(command, row)
"
><el-button size="small">更多</el-button
><template #dropdown
><el-dropdown-menu
><el-dropdown-item
v-if="row.enabled !== 1"
command="enable"
>加入可用池</el-dropdown-item
><el-dropdown-item
v-if="row.enabled === 1 && row.isDefault !== 1"
command="default"
>设为主模型</el-dropdown-item
><el-dropdown-item
v-if="row.enabled === 1 && row.isDefault !== 1"
command="disable"
>停用</el-dropdown-item
><el-dropdown-item
v-if="row.isDefault !== 1"
command="delete"
divided
>删除</el-dropdown-item
></el-dropdown-menu
></template
></el-dropdown
></TableRowActions
>
</template>
</el-table-column>
</el-table>
</template>
<ContentGenerationConfigView
v-if="activeMenu === 'content-generation'"
/>
<SsoIntegrationView v-if="activeMenu === 'sso'" />
<SystemConfigView
v-if="activeMenu === 'configs'"
:values="systemSettingValues"
:saving="savingSystemSettings"
:dirty="systemSettingsDirty"
@update="updateSystemSetting"
@save="saveSystemSettings"
/>
<RetrievalLogView v-if="activeMenu === 'retrievals'" />
<AttentionManagementView
v-if="activeMenu === 'attention'"
:can-config="can('attention.config')"
:can-preview="can('attention.preview')"
/>
<FeedbackManagementView
v-if="activeMenu === 'feedback'"
:can-delete="can('feedback.delete')"
:can-detail="can('feedback.detail')"
:can-export="can('feedback.export')"
/>
<UserBehaviorAnalysisView v-if="activeMenu === 'behavior'" :can-export="can('behavior.export')" />
<RecordAuditView v-if="activeMenu === 'records'" :can-export="can('records.export')" />
<AdminManagementView v-if="activeMenu === 'admins'" />
</section>
</section>
</main>
<el-dialog v-model="passwordDialogVisible" title="首次登录,请修改初始密码" width="460px" :close-on-click-modal="false" :close-on-press-escape="false" :show-close="false">
<el-alert title="为保障账号安全,修改密码后需要重新登录。" type="warning" :closable="false" show-icon />
<el-form label-position="top" class="password-form">
<el-form-item label="当前初始密码"><el-input v-model="passwordForm.currentPassword" type="password" show-password /></el-form-item>
<el-form-item label="新密码"><el-input v-model="passwordForm.newPassword" type="password" show-password placeholder="至少 8 位" /></el-form-item>
<el-form-item label="确认新密码"><el-input v-model="passwordForm.confirmPassword" type="password" show-password /></el-form-item>
</el-form>
<template #footer><el-button @click="logout">退出登录</el-button><el-button type="primary" :loading="passwordSaving" @click="changeInitialPassword">确认修改</el-button></template>
</el-dialog>
</template>