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 打包无错误
这个提交包含在:
@@ -19,6 +19,7 @@ const i18n = {
|
|||||||
'menu.nfc.batch': 'Batch Cards',
|
'menu.nfc.batch': 'Batch Cards',
|
||||||
'menu.nfc.rules': 'Routing Rules',
|
'menu.nfc.rules': 'Routing Rules',
|
||||||
'menu.ai': 'Omni AI',
|
'menu.ai': 'Omni AI',
|
||||||
|
'menu.ai.deepseek': 'Model & BYOK',
|
||||||
'menu.ai.knowledge': 'Knowledge Base',
|
'menu.ai.knowledge': 'Knowledge Base',
|
||||||
'menu.ai.translation': 'AI Translation & Glossary',
|
'menu.ai.translation': 'AI Translation & Glossary',
|
||||||
'menu.ai.concierge': 'AI Concierge',
|
'menu.ai.concierge': 'AI Concierge',
|
||||||
@@ -89,6 +90,7 @@ const i18n = {
|
|||||||
'menu.nfc.batch': '批量制卡',
|
'menu.nfc.batch': '批量制卡',
|
||||||
'menu.nfc.rules': '场景路由规则',
|
'menu.nfc.rules': '场景路由规则',
|
||||||
'menu.ai': 'AI 中枢',
|
'menu.ai': 'AI 中枢',
|
||||||
|
'menu.ai.deepseek': '模型与 BYOK',
|
||||||
'menu.ai.knowledge': '知识库',
|
'menu.ai.knowledge': '知识库',
|
||||||
'menu.ai.translation': '润色与术语库',
|
'menu.ai.translation': '润色与术语库',
|
||||||
'menu.ai.concierge': 'AI 顾问配置',
|
'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',
|
name: 'menu.ai',
|
||||||
key: 'ai',
|
key: 'ai',
|
||||||
children: [
|
children: [
|
||||||
|
{ name: 'menu.ai.deepseek', key: 'ai/deepseek' },
|
||||||
{ name: 'menu.ai.knowledge', key: 'ai/knowledge' },
|
{ name: 'menu.ai.knowledge', key: 'ai/knowledge' },
|
||||||
{ name: 'menu.ai.concierge', key: 'ai/concierge' },
|
{ name: 'menu.ai.concierge', key: 'ai/concierge' },
|
||||||
{ name: 'menu.ai.translation', key: 'ai/translation' },
|
{ 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 格式)
|
// 自动挂载 Authorization 与 x-tenant-id (对齐 Cool-Admin 原生 JWT 格式)
|
||||||
const token = getAuthToken();
|
const token = getAuthToken();
|
||||||
if (token) {
|
if (token) {
|
||||||
headers['Authorization'] = token.startsWith('Bearer ') ? token.substring(7) : token;
|
headers['Authorization'] = token.startsWith('Bearer ') ? token : `Bearer ${token}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const tenantId = getTenantId();
|
const tenantId = getTenantId();
|
||||||
|
|||||||
在新工单中引用
屏蔽一个用户