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.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' },
|
||||
|
||||
在新工单中引用
屏蔽一个用户