From cd01e45fbd21bcbc65d66cc3f780faae76686433 Mon Sep 17 00:00:00 2001 From: Nelson <1475262689@qq.com> Date: Tue, 7 Jul 2026 19:05:52 +0800 Subject: [PATCH] Implement admin Excel import settings and chat preview --- .../apps/admin-web/src/App.vue | 300 +++++++++++++++--- .../apps/admin-web/src/services/api.ts | 23 +- .../apps/admin-web/src/styles.css | 278 +++++++++++++++- .../apps/admin-web/src/types/api.ts | 8 + .../apps/backend/app/api/admin_users.py | 213 ++++++++++++- .../apps/backend/requirements.txt | 2 + 6 files changed, 746 insertions(+), 78 deletions(-) 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 93778e3..4d1f4b8 100644 --- a/ai_knowledge_base_v2/apps/admin-web/src/App.vue +++ b/ai_knowledge_base_v2/apps/admin-web/src/App.vue @@ -13,6 +13,7 @@ import type { DashboardStats, KnowledgeItem, ModelItem, + SystemConfigItem, UserImportResult, } from "./types/api"; @@ -26,7 +27,7 @@ const users = ref([]); const userKeyword = ref(""); const knowledge = ref([]); const models = ref([]); -const configs = ref[]>([]); +const configs = ref([]); const chats = ref([]); const aiLogs = ref([]); const operationLogs = ref[]>([]); @@ -49,7 +50,8 @@ const studentForm = reactive({ dailyChatLimit: 100, expiredAt: "", }); -const studentImportText = ref("手机号,姓名,昵称\n13800000001,张三,三三\n13800000002,李四,"); +const studentImportFile = ref(null); +const studentImportInput = ref(null); const studentImportResult = ref(null); const knowledgeForm = reactive({ @@ -166,6 +168,118 @@ const configForm = reactive({ description: "默认每日聊天次数", }); +type SystemSettingValue = string | number | boolean; +type SystemSettingType = "number" | "switch"; + +interface SystemSettingDefinition { + key: string; + label: string; + type: SystemSettingType; + defaultValue: SystemSettingValue; + min?: number; + max?: number; + description: string; +} + +interface SystemSettingSection { + title: string; + description: string; + settings: SystemSettingDefinition[]; +} + +const systemSettingSections: SystemSettingSection[] = [ + { + title: "用户与登录", + description: "控制学员准入后的默认额度和登录态有效时间。", + settings: [ + { + key: "daily_chat_limit", + label: "默认每日聊天额度", + type: "number", + defaultValue: 100, + min: 0, + max: 100000, + description: "新建或导入学员未单独填写额度时使用。", + }, + { + key: "login_token_expire_minutes", + label: "登录有效期(分钟)", + type: "number", + defaultValue: 10080, + min: 5, + max: 43200, + description: "用户端和管理端 Token 的建议有效期配置。", + }, + ], + }, + { + title: "AI 问答", + description: "控制 Agent 调用、上下文和用户端回答展示策略。", + settings: [ + { + key: "ai_timeout_seconds", + label: "AI 请求超时(秒)", + type: "number", + defaultValue: 60, + min: 5, + max: 300, + description: "真实模型请求超过该时间应判定为超时。", + }, + { + key: "chat_context_message_count", + label: "上下文消息数", + type: "number", + defaultValue: 10, + min: 0, + max: 100, + description: "组装 Prompt 时带入的历史消息数量。", + }, + { + key: "show_reference_sources", + label: "用户端展示引用来源", + type: "switch", + defaultValue: true, + description: "后台始终记录引用;此项控制用户端是否展示。", + }, + ], + }, + { + title: "外部服务", + description: "控制飞书检索和模型 mock 开关,方便开发、测试和灰度。", + settings: [ + { + key: "feishu_retry_count", + label: "飞书检索重试次数", + type: "number", + defaultValue: 2, + min: 0, + max: 10, + description: "飞书检索失败时的最大重试次数。", + }, + { + key: "use_mock_feishu", + label: "使用 mock 飞书检索", + type: "switch", + defaultValue: true, + description: "开发和演示环境可开启,生产环境应关闭。", + }, + { + key: "use_mock_model", + label: "使用 mock 大模型", + type: "switch", + defaultValue: true, + description: "开发和演示环境可开启,真实验收前应关闭。", + }, + ], + }, +]; + +const systemSettingDefinitions = systemSettingSections.flatMap((section) => section.settings); +const systemSettingValues = reactive>({}); +systemSettingDefinitions.forEach((setting) => { + systemSettingValues[setting.key] = setting.defaultValue; +}); + const chatFilters = reactive({ keyword: "", userId: undefined as number | undefined, @@ -236,7 +350,10 @@ async function loadCurrentMenu() { } } if (activeMenu.value === "models") models.value = await api.models(); - if (activeMenu.value === "configs") configs.value = await api.configs(); + if (activeMenu.value === "configs") { + configs.value = await api.configs(); + hydrateSystemSettings(); + } if (activeMenu.value === "records") { chats.value = await api.chats(buildChatQuery()); aiLogs.value = await api.aiLogs(); @@ -271,37 +388,29 @@ async function createStudent() { await loadCurrentMenu(); } +function handleStudentImportFile(event: Event) { + const input = event.target as HTMLInputElement; + studentImportFile.value = input.files?.[0] ?? null; +} + +async function downloadStudentTemplate() { + await api.downloadStudentTemplate(); +} + async function importStudents() { - const students = parseStudentImportText(studentImportText.value); - if (!students.length) { - ElMessage.error("请粘贴学员数据"); + if (!studentImportFile.value) { + ElMessage.error("请先选择 Excel 文件"); return; } - studentImportResult.value = await api.importUsers(students); + studentImportResult.value = await api.importUsersExcel(studentImportFile.value); ElMessage.success( `导入完成:新增 ${studentImportResult.value.created},更新 ${studentImportResult.value.updated},失败 ${studentImportResult.value.failed}`, ); + studentImportFile.value = null; + if (studentImportInput.value) studentImportInput.value.value = ""; await loadCurrentMenu(); } -function parseStudentImportText(text: string) { - return text - .split(/\r?\n/) - .map((line) => line.trim()) - .filter(Boolean) - .filter((line, index) => !(index === 0 && line.includes("手机号"))) - .map((line) => { - const [phone = "", name = "", nickname = ""] = line.split(/,|\t/).map((item) => item.trim()); - return { - phone, - name, - nickname: nickname || null, - status: 1, - dailyChatLimit: 100, - }; - }); -} - function resetStudentForm() { Object.assign(studentForm, { phone: "", name: "", nickname: "", status: 1, dailyChatLimit: 100, expiredAt: "" }); } @@ -591,6 +700,57 @@ async function saveConfig() { await loadCurrentMenu(); } +async function saveSystemSettings() { + await Promise.all( + systemSettingDefinitions.map((setting) => + api.saveConfig({ + configKey: setting.key, + configValue: serializeSystemSettingValue(systemSettingValues[setting.key]), + description: setting.description, + }), + ), + ); + ElMessage.success("系统配置已保存"); + configs.value = await api.configs(); + hydrateSystemSettings(); +} + +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)); + }); +} + +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()); + } + 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); +} + +function agentMessageParts(content: string) { + const matched = content.match(/([\s\S]*?)<\/think>/i); + if (!matched) return { reasoning: "", answer: content }; + const answer = content.replace(matched[0], "").trim(); + return { + reasoning: matched[1].trim(), + answer: answer || "模型未返回正式回答。", + }; +} + async function debugAgent() { if (!agentForm.modelId) { ElMessage.error("请选择调试模型"); @@ -753,15 +913,20 @@ function buildChatQuery() {

批量导入学员

- +

下载模板后填写学员信息,再上传 .xlsx 文件导入。

+
+ + {{ studentImportFile?.name || "未选择文件" }} +
- 导入学员 + 下载模板 + 导入 Excel 新增 {{ studentImportResult.created }},更新 {{ studentImportResult.updated }},失败 {{ studentImportResult.failed }} @@ -897,11 +1062,28 @@ function buildChatQuery() {
- {{ message.role === "user" ? "用户" : "Agent" }} -
{{ message.content }}
+
{{ message.role === "user" ? "你" : "AI" }}
+
+ {{ message.role === "user" ? "你" : "Agent" }} + +
{{ message.content }}
+
+
+
+
AI
+
+ Agent +
正在思考中...
+
@@ -910,7 +1092,7 @@ function buildChatQuery() { type="textarea" :rows="2" resize="none" - placeholder="输入测试问题" + placeholder="向 Agent 发送测试问题" @keydown.enter.exact.prevent="debugAgent" /> 发送 @@ -1035,13 +1217,43 @@ function buildChatQuery() {