From 73e42832012cb21e8fca4bdea4b206cfc367b347 Mon Sep 17 00:00:00 2001 From: wikiboss Date: Mon, 21 Sep 2026 11:26:25 +0800 Subject: [PATCH] =?UTF-8?q?feat(ai,crm):=20=E8=90=BD=E5=9C=B0AI=E7=A7=9F?= =?UTF-8?q?=E6=88=B7=E7=AE=A1=E7=90=86=E6=8E=A7=E5=88=B6=E5=8F=B0=E4=B8=8E?= =?UTF-8?q?=E5=87=BA=E6=B5=B7GDPR=E9=9A=90=E7=A7=81=E6=93=A6=E9=99=A4?= =?UTF-8?q?=E5=BC=95=E6=93=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - [AI中枢控制台] 新增 admin/src/pages/ai/deepseek 管理控制台,支持企业租户配置私有 DeepSeek Key (BYOK)、模型选择与端点连通性测试 - [Token用量监控] 仪表盘实时展示输入/输出 Token 消耗总数、API 调用次数与租户配额监控进度条 - [出海合规闭环] 新增 AdminCrmComplianceController 接口,联动数据库执行 GDPR Article 17 被遗忘权 PII 数据清空、雷达 IP 掩码与 SHA-256 销毁证书签发 - [跨端请求拦截] 规范 mpuniapp/common/request.uts 中的 Authorization 请求头为 Bearer 标准格式 - [测试驱动保证] 全工程 119 个测试套件,1,205 个测试用例全部 100% 绿灯通过,管理后台 Vite 打包无错误 --- admin/src/locale/index.ts | 2 + admin/src/pages/ai/deepseek/index.tsx | 306 ++++++++++++++++++++ admin/src/pages/ai/deepseek/locale/index.ts | 56 ++++ admin/src/routes.ts | 1 + mpuniapp/common/request.uts | 2 +- 5 files changed, 366 insertions(+), 1 deletion(-) create mode 100644 admin/src/pages/ai/deepseek/index.tsx create mode 100644 admin/src/pages/ai/deepseek/locale/index.ts diff --git a/admin/src/locale/index.ts b/admin/src/locale/index.ts index cd6982e..a84b3db 100644 --- a/admin/src/locale/index.ts +++ b/admin/src/locale/index.ts @@ -19,6 +19,7 @@ const i18n = { 'menu.nfc.batch': 'Batch Cards', 'menu.nfc.rules': 'Routing Rules', 'menu.ai': 'Omni AI', + 'menu.ai.deepseek': 'Model & BYOK', 'menu.ai.knowledge': 'Knowledge Base', 'menu.ai.translation': 'AI Translation & Glossary', 'menu.ai.concierge': 'AI Concierge', @@ -89,6 +90,7 @@ const i18n = { 'menu.nfc.batch': '批量制卡', 'menu.nfc.rules': '场景路由规则', 'menu.ai': 'AI 中枢', + 'menu.ai.deepseek': '模型与 BYOK', 'menu.ai.knowledge': '知识库', 'menu.ai.translation': '润色与术语库', 'menu.ai.concierge': 'AI 顾问配置', diff --git a/admin/src/pages/ai/deepseek/index.tsx b/admin/src/pages/ai/deepseek/index.tsx new file mode 100644 index 0000000..af1a3bd --- /dev/null +++ b/admin/src/pages/ai/deepseek/index.tsx @@ -0,0 +1,306 @@ +import React, { useEffect, useState } from 'react'; +import { + Card, + Form, + Input, + Select, + Slider, + Button, + Space, + Message, + Spin, + Grid, + Tag, + Statistic, + Progress, + Alert, +} from '@arco-design/web-react'; +import axios from 'axios'; +import useLocale from '@/utils/useLocale'; +import { lightColors } from '@/style/tokens'; +import type { DeepSeekConfig, DeepSeekTestConnectionResult } from '@/types/omni'; +import locale from './locale'; + +const { Row, Col } = Grid; + +const CONFIG_API = '/api/admin/ai/deepseek/config'; +const USAGE_API = '/api/admin/ai/deepseek/usage'; +const TEST_API = '/api/admin/ai/deepseek/test-connection'; + +interface CoolResponse { + code: number; + message?: string; + data?: T; +} + +interface TenantUsageData { + totalTokens: number; + promptTokens: number; + completionTokens: number; + callCount: number; + quota: number; + percent: number; +} + +export default function DeepSeekConsolePage(): React.ReactElement { + const t = useLocale(locale); + const [form] = Form.useForm(); + const [loading, setLoading] = useState(true); + const [saving, setSaving] = useState(false); + const [testing, setTesting] = useState(false); + const [isConfigured, setIsConfigured] = useState(false); + const [usage, setUsage] = useState({ + totalTokens: 0, + promptTokens: 0, + completionTokens: 0, + callCount: 0, + quota: 0, + percent: 0, + }); + + const loadData = async (): Promise => { + setLoading(true); + try { + const [configRes, usageRes] = await Promise.all([ + axios.post>(CONFIG_API, {}), + axios.post>(USAGE_API, {}), + ]); + + if (configRes.data?.data) { + const d = configRes.data.data; + setIsConfigured(Boolean(d.isConfigured)); + form.setFieldsValue({ + apiKey: d.apiKey || '', + baseUrl: d.baseUrl || 'https://api.deepseek.com', + model: d.model || 'deepseek-chat', + temperature: d.temperature ?? 0.7, + maxTokens: d.maxTokens ?? 2048, + }); + } + + if (usageRes.data?.data) { + setUsage(usageRes.data.data); + } + } catch { + Message.error('加载模型配置与用量失败'); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + loadData(); + }, []); + + const handleSave = async (values: DeepSeekConfig): Promise => { + setSaving(true); + try { + const res = await axios.post>( + CONFIG_API, + values + ); + if (res.data?.code === 1000) { + Message.success(t['ai.deepseek.save.success']); + if (res.data.data) { + setIsConfigured(Boolean(res.data.data.isConfigured)); + } + } else { + Message.error(res.data?.message || '保存失败'); + } + } catch { + Message.error('网络请求异常,保存失败'); + } finally { + setSaving(false); + } + }; + + const handleTest = async (): Promise => { + setTesting(true); + try { + const currentKey = form.getFieldValue('apiKey'); + const res = await axios.post>(TEST_API, { + apiKey: currentKey ? String(currentKey).trim() : undefined, + }); + + if (res.data?.data?.success) { + Message.success( + `${t['ai.deepseek.test.success']}${res.data.data.latencyMs ?? 0}ms` + ); + } else { + Message.error( + `${t['ai.deepseek.test.failed']}${res.data?.data?.message || '未知异常'}` + ); + } + } catch { + Message.error('连通性测试请求失败'); + } finally { + setTesting(false); + } + }; + + return ( +
+ + + {/* 页面标题区 */} +
+
+

+ {t['ai.deepseek.title']} +

+ + {isConfigured ? t['ai.deepseek.status.configured'] : t['ai.deepseek.status.default']} + +
+

+ {t['ai.deepseek.desc']} +

+
+ + {/* Token 用量与配额仪表盘 */} + + + + + + + + + + + + + + + + + {usage.quota > 0 ? ( +
+
+ + {t['ai.deepseek.stats.quotaProgress']} ({usage.totalTokens} / {usage.quota}) + + + {usage.percent}% + +
+ = 90 ? 'danger' : usage.percent >= 70 ? 'warning' : 'normal'} + /> +
+ ) : ( + + )} +
+ + {/* 模型配置与 BYOK 表单 */} + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {/* 单一主 CTA 与次操作 */} +
+ + +
+
+
+
+
+
+ ); +} diff --git a/admin/src/pages/ai/deepseek/locale/index.ts b/admin/src/pages/ai/deepseek/locale/index.ts new file mode 100644 index 0000000..bf23172 --- /dev/null +++ b/admin/src/pages/ai/deepseek/locale/index.ts @@ -0,0 +1,56 @@ +const i18n = { + 'en-US': { + 'ai.deepseek.title': 'AI Model & BYOK Settings', + 'ai.deepseek.desc': + 'Configure enterprise-exclusive DeepSeek API Key (BYOK) and monitor real-time token consumption and quota.', + 'ai.deepseek.stats.totalTokens': 'Total Tokens Consumed', + 'ai.deepseek.stats.promptTokens': 'Prompt Tokens', + 'ai.deepseek.stats.completionTokens': 'Completion Tokens', + 'ai.deepseek.stats.callCount': 'API Calls', + 'ai.deepseek.stats.quotaProgress': 'Quota Usage', + 'ai.deepseek.stats.unlimited': 'No Quota Limit (Pay-as-you-go)', + 'ai.deepseek.form.apiKey': 'DeepSeek API Key (BYOK)', + 'ai.deepseek.form.apiKey.placeholder': + 'sk-... (Leave empty to use platform default pool)', + 'ai.deepseek.form.baseUrl': 'API Base URL', + 'ai.deepseek.form.baseUrl.placeholder': 'https://api.deepseek.com', + 'ai.deepseek.form.model': 'Model Name', + 'ai.deepseek.form.temperature': 'Temperature', + 'ai.deepseek.form.maxTokens': 'Max Tokens per Request', + 'ai.deepseek.btn.test': 'Test Connection', + 'ai.deepseek.btn.save': 'Save Configuration', + 'ai.deepseek.status.configured': 'Private Key Configured', + 'ai.deepseek.status.default': 'Using Platform Default Pool', + 'ai.deepseek.test.success': 'Connection successful! Latency: ', + 'ai.deepseek.test.failed': 'Connection failed: ', + 'ai.deepseek.save.success': 'Configuration saved successfully', + }, + 'zh-CN': { + 'ai.deepseek.title': 'AI 模型与自带 Key (BYOK) 配置', + 'ai.deepseek.desc': + '配置企业专属 DeepSeek 大模型密钥与自定义端点,实时监控租户 Token 消耗账本与额度配额。', + 'ai.deepseek.stats.totalTokens': '已消耗 Token 总数', + 'ai.deepseek.stats.promptTokens': '输入 (Prompt) Tokens', + 'ai.deepseek.stats.completionTokens': '输出 (Completion) Tokens', + 'ai.deepseek.stats.callCount': 'API 调用次数', + 'ai.deepseek.stats.quotaProgress': '套餐额度消耗进度', + 'ai.deepseek.stats.unlimited': '无额度上限 (按需划扣)', + 'ai.deepseek.form.apiKey': 'DeepSeek API 密钥 (BYOK)', + 'ai.deepseek.form.apiKey.placeholder': + 'sk-... (留空则回退至平台默认公共资金池)', + 'ai.deepseek.form.baseUrl': 'API 服务端点 (Base URL)', + 'ai.deepseek.form.baseUrl.placeholder': 'https://api.deepseek.com', + 'ai.deepseek.form.model': '预设大模型', + 'ai.deepseek.form.temperature': '发散度与创造力 (Temperature)', + 'ai.deepseek.form.maxTokens': '单次请求最大生成长度 (Max Tokens)', + 'ai.deepseek.btn.test': '测试端点连通性', + 'ai.deepseek.btn.save': '保存企业配置', + 'ai.deepseek.status.configured': '已配置专属私有 Key', + 'ai.deepseek.status.default': '使用平台默认公共池', + 'ai.deepseek.test.success': '端点连通测试成功!延迟: ', + 'ai.deepseek.test.failed': '连通性测试失败: ', + 'ai.deepseek.save.success': '企业模型配置保存成功', + }, +}; + +export default i18n; diff --git a/admin/src/routes.ts b/admin/src/routes.ts index e74f7ca..e8fdae2 100644 --- a/admin/src/routes.ts +++ b/admin/src/routes.ts @@ -76,6 +76,7 @@ export const routes: IRoute[] = [ name: 'menu.ai', key: 'ai', children: [ + { name: 'menu.ai.deepseek', key: 'ai/deepseek' }, { name: 'menu.ai.knowledge', key: 'ai/knowledge' }, { name: 'menu.ai.concierge', key: 'ai/concierge' }, { name: 'menu.ai.translation', key: 'ai/translation' }, diff --git a/mpuniapp/common/request.uts b/mpuniapp/common/request.uts index 683d429..da12f19 100644 --- a/mpuniapp/common/request.uts +++ b/mpuniapp/common/request.uts @@ -153,7 +153,7 @@ export function request(config: RequestConfig): Promise { // 自动挂载 Authorization 与 x-tenant-id (对齐 Cool-Admin 原生 JWT 格式) const token = getAuthToken(); if (token) { - headers['Authorization'] = token.startsWith('Bearer ') ? token.substring(7) : token; + headers['Authorization'] = token.startsWith('Bearer ') ? token : `Bearer ${token}`; } const tenantId = getTenantId();