From da313f88ed1c09a717a99a11ccc52972245955be Mon Sep 17 00:00:00 2001 From: Nelson <1475262689@qq.com> Date: Fri, 31 Jul 2026 17:25:16 +0800 Subject: [PATCH] feat: route background ai workloads by model --- .../apps/admin-web/src/App.vue | 68 +++++++- .../apps/admin-web/src/services/api.ts | 6 +- .../apps/admin-web/src/styles.css | 3 + .../apps/admin-web/src/types/api.ts | 11 ++ .../alembic/versions/0022_model_routing.py | 52 ++++++ .../apps/backend/app/api/admin_settings.py | 58 ++++++- .../apps/backend/app/models/ai_config.py | 1 + .../apps/backend/app/schemas/admin.py | 17 ++ .../backend/app/services/admin_service.py | 68 ++++++-- .../app/services/ai_request_log_service.py | 8 +- .../app/services/growth_profile_service.py | 26 ++- .../app/services/knowledge_agent_service.py | 8 +- .../app/services/model_routing_service.py | 89 ++++++++++ .../backend/app/services/model_service.py | 44 ++++- .../app/services/model_stream_service.py | 9 +- .../app/services/periodic_report_service.py | 13 +- .../services/tracked_generation_service.py | 64 ++++++++ .../backend/tests/test_ai_cost_tracking.py | 50 ++++++ .../apps/backend/tests/test_model_routing.py | 154 ++++++++++++++++++ .../backend/tests/test_periodic_reports.py | 8 +- .../deployment/DEPLOYMENT_GUIDE.md | 19 +++ .../docs/qianwen_product_todo.md | 1 + 22 files changed, 714 insertions(+), 63 deletions(-) create mode 100644 ai_knowledge_base_v2/apps/backend/alembic/versions/0022_model_routing.py create mode 100644 ai_knowledge_base_v2/apps/backend/app/services/model_routing_service.py create mode 100644 ai_knowledge_base_v2/apps/backend/app/services/tracked_generation_service.py create mode 100644 ai_knowledge_base_v2/apps/backend/tests/test_model_routing.py diff --git a/ai_knowledge_base_v2/apps/admin-web/src/App.vue b/ai_knowledge_base_v2/apps/admin-web/src/App.vue index 795fcc1..e39e234 100644 --- a/ai_knowledge_base_v2/apps/admin-web/src/App.vue +++ b/ai_knowledge_base_v2/apps/admin-web/src/App.vue @@ -305,7 +305,15 @@ async function loadDashboard() { async function refreshStorage() { storageLoading.value = true; try { storageStats.value = await api.storageStats(true); ElMessage.success("存储统计已刷新"); } finally { storageLoading.value = false; } } function formatBytes(value: unknown) { if (typeof value !== "number") return "无法统计"; const units = ["B", "KB", "MB", "GB", "TB"]; let size = value, i = 0; while (size >= 1024 && i < units.length - 1) { size /= 1024; i += 1; } return `${size.toFixed(i ? 2 : 0)} ${units[i]}`; } -function formatMoney(value?: number | null, currency = "CNY") { if (typeof value !== "number") return "-"; return `${currency || "CNY"} ${value.toFixed(6)}`; } +function formatMoney(value?: number | null, currency = "CNY") { if (typeof value !== "number") return "-"; if (currency === "MIXED") return "多币种,见明细"; return `${currency || "CNY"} ${value.toFixed(6)}`; } +function modelSceneLabel(scene: string) { + return ({ + background_report: "周期报告", + background_summary: "摘要沉淀", + knowledge_grounded: "知识问答", + general_chat: "通用对话", + } as Record)[scene] || scene || "未分类"; +} async function cleanupRetrievalLogs() { if (!cleanupBefore.value) return ElMessage.warning("请选择清理日期"); const estimate = await api.estimateRetrievalCleanup(cleanupBefore.value); await ElMessageBox.confirm(`预计清理 ${estimate.estimatedCount} 条检索日志及关联明细,操作日志不会被删除。`, "确认清理", { type: "warning" }); const result = await api.cleanupRetrievalLogs(cleanupBefore.value); ElMessage.success(`已清理 ${result.deleted} 条日志`); await loadCurrentMenu(); } async function saveRetention() { await api.saveRetrievalRetention(retentionDays.value); ElMessage.success(retentionDays.value == null ? "已设为永久保留" : `已设为保留 ${retentionDays.value} 天`); await loadCurrentMenu(); } @@ -711,12 +719,25 @@ function applyQuickProvider(provider: string) { quickModelForm.modelName = target.models[0]?.value ?? ""; } -async function enableModel(id: number) { - await api.enableModel(id); - ElMessage.success("模型已启用"); +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("确认删除该模型?删除后不可恢复。", "删除模型", { @@ -1108,6 +1129,23 @@ function formatRecordDateTime(value: string, boundary: "start" | "end") {
总 Token{{ stats?.totalToken ?? 0 }}
估算成本{{ formatMoney(stats?.estimatedCost, stats?.costCurrency || 'CNY') }}
+
+
+

模型使用与成本

按实际调用场景和模型统计;报告、摘要分流是否生效可在这里核对。

+
+ + + + + + + + + + + + +

项目存储占用

数据库、Redis 与附件分别统计;无权限或不支持时显示“无法统计”。

手动刷新
项目总量{{ formatBytes(storageStats?.totalBytes) }}
数据库{{ formatBytes(storageStats?.detail?.database?.bytes) }}
Redis{{ formatBytes(storageStats?.detail?.redis?.bytes) }}
附件/文件{{ formatBytes(storageStats?.detail?.files?.bytes) }}
近7天增长{{ formatBytes(storageStats?.growth7DaysBytes) }}
近30天增长{{ formatBytes(storageStats?.growth30DaysBytes) }}
最后统计:{{ storageStats?.createdAt || '尚未统计' }}
@@ -1322,7 +1360,7 @@ function formatRecordDateTime(value: string, boundary: "start" | "end") {