feat(ai,crm): 落地AI租户管理控制台与出海GDPR隐私擦除引擎

- [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 打包无错误
这个提交包含在:
wikiboss
2026-09-21 11:26:25 +08:00
父节点 669d4b91c0
当前提交 73e4283201
共有 5 个文件被更改,包括 366 次插入 和 1 次删除

查看文件

@@ -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 顾问配置',

查看文件

@@ -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<T> {
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<DeepSeekConfig>();
const [loading, setLoading] = useState<boolean>(true);
const [saving, setSaving] = useState<boolean>(false);
const [testing, setTesting] = useState<boolean>(false);
const [isConfigured, setIsConfigured] = useState<boolean>(false);
const [usage, setUsage] = useState<TenantUsageData>({
totalTokens: 0,
promptTokens: 0,
completionTokens: 0,
callCount: 0,
quota: 0,
percent: 0,
});
const loadData = async (): Promise<void> => {
setLoading(true);
try {
const [configRes, usageRes] = await Promise.all([
axios.post<CoolResponse<DeepSeekConfig & { isConfigured?: boolean }>>(CONFIG_API, {}),
axios.post<CoolResponse<TenantUsageData>>(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<void> => {
setSaving(true);
try {
const res = await axios.post<CoolResponse<DeepSeekConfig & { isConfigured?: boolean }>>(
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<void> => {
setTesting(true);
try {
const currentKey = form.getFieldValue('apiKey');
const res = await axios.post<CoolResponse<DeepSeekTestConnectionResult>>(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 (
<div style={{ padding: '24px', maxWidth: '1200px', margin: '0 auto' }}>
<Spin loading={loading} style={{ width: '100%' }}>
<Space direction="vertical" size={20} style={{ width: '100%' }}>
{/* 页面标题区 */}
<div>
<div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
<h2 style={{ margin: 0, fontSize: '20px', fontWeight: 600, color: lightColors.text }}>
{t['ai.deepseek.title']}
</h2>
<Tag color={isConfigured ? 'green' : 'blue'}>
{isConfigured ? t['ai.deepseek.status.configured'] : t['ai.deepseek.status.default']}
</Tag>
</div>
<p style={{ margin: '8px 0 0 0', color: lightColors.text2, fontSize: '14px' }}>
{t['ai.deepseek.desc']}
</p>
</div>
{/* Token 用量与配额仪表盘 */}
<Card title="Token 消耗账本与额度概览" bordered={true} style={{ borderRadius: '16px' }}>
<Row gutter={24}>
<Col span={6}>
<Statistic
title={t['ai.deepseek.stats.totalTokens']}
value={usage.totalTokens}
groupSeparator
valueStyle={{ color: lightColors.primary, fontWeight: 700 }}
/>
</Col>
<Col span={6}>
<Statistic
title={t['ai.deepseek.stats.promptTokens']}
value={usage.promptTokens}
groupSeparator
/>
</Col>
<Col span={6}>
<Statistic
title={t['ai.deepseek.stats.completionTokens']}
value={usage.completionTokens}
groupSeparator
/>
</Col>
<Col span={6}>
<Statistic
title={t['ai.deepseek.stats.callCount']}
value={usage.callCount}
groupSeparator
/>
</Col>
</Row>
{usage.quota > 0 ? (
<div style={{ marginTop: '20px' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '6px' }}>
<span style={{ fontSize: '13px', color: lightColors.text2 }}>
{t['ai.deepseek.stats.quotaProgress']} ({usage.totalTokens} / {usage.quota})
</span>
<span style={{ fontSize: '13px', fontWeight: 600, color: lightColors.text }}>
{usage.percent}%
</span>
</div>
<Progress
percent={usage.percent}
status={usage.percent >= 90 ? 'danger' : usage.percent >= 70 ? 'warning' : 'normal'}
/>
</div>
) : (
<Alert
type="info"
style={{ marginTop: '16px', borderRadius: '10px' }}
content={t['ai.deepseek.stats.unlimited']}
/>
)}
</Card>
{/* 模型配置与 BYOK 表单 */}
<Card title="模型连接参数 (BYOK)" bordered={true} style={{ borderRadius: '16px' }}>
<Form form={form} layout="vertical" onFinish={handleSave}>
<Form.Item
label={t['ai.deepseek.form.apiKey']}
field="apiKey"
tooltip="支持配置企业自建私有 DeepSeek 密钥,流量与费用独立结算"
>
<Input.Password
placeholder={t['ai.deepseek.form.apiKey.placeholder']}
style={{ height: '44px', borderRadius: '12px' }}
/>
</Form.Item>
<Row gutter={16}>
<Col span={12}>
<Form.Item
label={t['ai.deepseek.form.baseUrl']}
field="baseUrl"
rules={[{ required: true }]}
>
<Input
placeholder={t['ai.deepseek.form.baseUrl.placeholder']}
style={{ height: '44px', borderRadius: '12px' }}
/>
</Form.Item>
</Col>
<Col span={12}>
<Form.Item
label={t['ai.deepseek.form.model']}
field="model"
rules={[{ required: true }]}
>
<Select style={{ height: '44px', borderRadius: '12px' }}>
<Select.Option value="deepseek-chat">deepseek-chat (通用对话与顾问)</Select.Option>
<Select.Option value="deepseek-reasoner">deepseek-reasoner (深度逻辑推理)</Select.Option>
</Select>
</Form.Item>
</Col>
</Row>
<Row gutter={16}>
<Col span={12}>
<Form.Item
label={t['ai.deepseek.form.temperature']}
field="temperature"
>
<Slider min={0} max={2} step={0.1} showTicks />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item
label={t['ai.deepseek.form.maxTokens']}
field="maxTokens"
>
<Input
type="number"
style={{ height: '44px', borderRadius: '12px' }}
/>
</Form.Item>
</Col>
</Row>
{/* 单一主 CTA 与次操作 */}
<div style={{ marginTop: '16px', display: 'flex', justifyContent: 'flex-end', gap: '12px' }}>
<Button
type="outline"
loading={testing}
onClick={handleTest}
style={{ height: '44px', borderRadius: '12px', padding: '0 20px' }}
>
{t['ai.deepseek.btn.test']}
</Button>
<Button
type="primary"
htmlType="submit"
loading={saving}
style={{ height: '44px', borderRadius: '12px', padding: '0 28px' }}
>
{t['ai.deepseek.btn.save']}
</Button>
</div>
</Form>
</Card>
</Space>
</Spin>
</div>
);
}

查看文件

@@ -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;

查看文件

@@ -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' },

查看文件

@@ -153,7 +153,7 @@ export function request<T>(config: RequestConfig): Promise<T> {
// 自动挂载 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();