feat(ai): 完成 DeepSeek 大模型官方 API 接入、双模调用、高可用平滑降级与管理后台连通性测试

这个提交包含在:
wikiboss
2026-09-21 00:18:19 +08:00
父节点 eec259a3a7
当前提交 56392d8f9e
共有 33 个文件被更改,包括 1362 次插入 和 110 次删除

查看文件

@@ -12,6 +12,8 @@ import {
Message,
Spin,
Grid,
Tag,
Divider,
} from '@arco-design/web-react';
import axios from 'axios';
import useLocale from '@/utils/useLocale';
@@ -20,6 +22,8 @@ import type {
AiConciergeConfig,
ConciergePersonaType,
ConciergeToneStyle,
DeepSeekConfig,
DeepSeekTestConnectionResult,
} from '@/types/omni';
import locale from './locale';
@@ -28,6 +32,8 @@ const { Row, Col } = Grid;
const GET_CONFIG_API = '/api/admin/ai/concierge/getByCardId';
const SAVE_CONFIG_API = '/api/admin/ai/concierge/saveByCardId';
const CARD_PAGE_API = '/api/admin/card/info/page';
const DEEPSEEK_CONFIG_API = '/api/admin/ai/deepseek/config';
const DEEPSEEK_TEST_API = '/api/admin/ai/deepseek/test-connection';
interface CoolResponse<T> {
code: number;
@@ -98,6 +104,100 @@ export default function AiConciergePage(): JSX.Element {
const [selectedCardId, setSelectedCardId] = useState<number>(DEFAULT_CARD_ID);
const [cardOptions, setCardOptions] = useState<CardOptionItem[]>([]);
const [deepSeekForm] = Form.useForm<DeepSeekConfig>();
const [deepSeekLoading, setDeepSeekLoading] = useState<boolean>(false);
const [deepSeekSaving, setDeepSeekSaving] = useState<boolean>(false);
const [deepSeekTesting, setDeepSeekTesting] = useState<boolean>(false);
const [deepSeekConfig, setDeepSeekConfig] = useState<DeepSeekConfig | null>(null);
const [testResult, setTestResult] = useState<DeepSeekTestConnectionResult | null>(null);
const loadDeepSeekConfig = async (): Promise<void> => {
setDeepSeekLoading(true);
try {
const res = await axios.post<CoolResponse<DeepSeekConfig>>(
DEEPSEEK_CONFIG_API,
{}
);
if (res.data && res.data.code === 1000 && res.data.data) {
const d = res.data.data;
setDeepSeekConfig(d);
deepSeekForm.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,
enabled: d.enabled !== false,
});
}
} catch {
// 保持静默优雅降级
} finally {
setDeepSeekLoading(false);
}
};
const handleTestDeepSeek = async (): Promise<void> => {
setDeepSeekTesting(true);
setTestResult(null);
try {
const values = deepSeekForm.getFieldsValue();
const res = await axios.post<CoolResponse<DeepSeekTestConnectionResult>>(
DEEPSEEK_TEST_API,
{ apiKey: values.apiKey }
);
if (res.data && res.data.code === 1000 && res.data.data) {
setTestResult(res.data.data);
if (res.data.data.success) {
Message.success(
`${t['aiDeepSeek.test.success']} (${res.data.data.latencyMs ?? 0}ms)`
);
} else {
Message.error(res.data.data.message || t['aiDeepSeek.test.fail']);
}
} else {
Message.error(res.data?.message || t['aiDeepSeek.test.fail']);
}
} catch {
Message.error(t['aiDeepSeek.test.fail']);
} finally {
setDeepSeekTesting(false);
}
};
const handleSaveDeepSeek = async (values: DeepSeekConfig): Promise<void> => {
setDeepSeekSaving(true);
try {
const payload: Partial<DeepSeekConfig> = {
baseUrl: values.baseUrl,
model: values.model,
temperature: Number(values.temperature),
maxTokens: Number(values.maxTokens),
enabled: Boolean(values.enabled),
};
if (values.apiKey && !values.apiKey.includes('****')) {
payload.apiKey = values.apiKey;
}
const res = await axios.post<CoolResponse<DeepSeekConfig>>(
DEEPSEEK_CONFIG_API,
payload
);
if (res.data && res.data.code === 1000 && res.data.data) {
Message.success(t['aiDeepSeek.save.success']);
setDeepSeekConfig(res.data.data);
if (res.data.data.apiKey) {
deepSeekForm.setFieldValue('apiKey', res.data.data.apiKey);
}
} else {
Message.error(res.data?.message || t['aiDeepSeek.save.fail']);
}
} catch {
Message.error(t['aiDeepSeek.save.fail']);
} finally {
setDeepSeekSaving(false);
}
};
const fetchCardOptions = async (): Promise<void> => {
try {
const res = await axios.post<CoolResponse<CardPageResult>>(
@@ -171,6 +271,7 @@ export default function AiConciergePage(): JSX.Element {
useEffect(() => {
fetchCardOptions();
loadConciergeConfig(selectedCardId);
loadDeepSeekConfig();
}, []);
const handleCardChange = (cardId: number): void => {
@@ -401,6 +502,178 @@ export default function AiConciergePage(): JSX.Element {
</Form>
</Spin>
</Card>
<Card
title={
<div
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
width: '100%',
}}
>
<span>{t['aiDeepSeek.card.title']}</span>
<Tag color={deepSeekConfig?.isConfigured ? 'green' : 'gray'}>
{deepSeekConfig?.isConfigured
? t['aiDeepSeek.status.configured']
: t['aiDeepSeek.status.unconfigured']}
</Tag>
</div>
}
bordered={false}
style={{
marginTop: spacing[24],
borderRadius: radius.card,
border: `1px solid ${lightColors.border}`,
backgroundColor: lightColors.bg,
}}
>
<div
style={{
color: lightColors.text2,
marginBottom: spacing[24],
fontSize: '14px',
}}
>
{t['aiDeepSeek.card.desc']}
</div>
<Spin loading={deepSeekLoading} style={{ width: '100%' }}>
<Form
form={deepSeekForm}
layout="vertical"
onSubmit={handleSaveDeepSeek}
initialValues={{
baseUrl: 'https://api.deepseek.com',
model: 'deepseek-chat',
temperature: 0.7,
maxTokens: 2048,
enabled: true,
}}
>
<Row gutter={24}>
<Col span={12}>
<Form.Item
label={t['aiDeepSeek.form.apiKey']}
field="apiKey"
extra={
deepSeekConfig?.apiKey
? `已保存密钥: ${deepSeekConfig.apiKey}`
: undefined
}
>
<Input.Password
placeholder={t['aiDeepSeek.form.apiKey.placeholder']}
/>
</Form.Item>
</Col>
<Col span={12}>
<Form.Item
label={t['aiDeepSeek.form.status']}
field="enabled"
triggerPropName="checked"
extra={t['aiDeepSeek.form.status.desc']}
>
<Switch />
</Form.Item>
</Col>
</Row>
<Row gutter={24}>
<Col span={12}>
<Form.Item
label={t['aiDeepSeek.form.baseUrl']}
field="baseUrl"
rules={[{ required: true }]}
>
<Input placeholder="https://api.deepseek.com" />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item
label={t['aiDeepSeek.form.model']}
field="model"
rules={[{ required: true }]}
>
<Select>
<Select.Option value="deepseek-chat">
{t['aiDeepSeek.model.chat']}
</Select.Option>
<Select.Option value="deepseek-reasoner">
{t['aiDeepSeek.model.reasoner']}
</Select.Option>
</Select>
</Form.Item>
</Col>
</Row>
<Row gutter={24}>
<Col span={12}>
<Form.Item
label={t['aiDeepSeek.form.temperature']}
field="temperature"
extra={t['aiDeepSeek.form.temperature.desc']}
>
<Slider
min={0}
max={1.5}
step={0.1}
marks={{ 0: '0', 0.7: '0.7', 1.0: '1.0', 1.5: '1.5' }}
style={{ width: '100%' }}
/>
</Form.Item>
</Col>
<Col span={12}>
<Form.Item
label="Max Tokens"
field="maxTokens"
>
<Select>
<Select.Option value={1024}>1024</Select.Option>
<Select.Option value={2048}>2048</Select.Option>
<Select.Option value={4096}>4096</Select.Option>
<Select.Option value={8192}>8192</Select.Option>
</Select>
</Form.Item>
</Col>
</Row>
{testResult && (
<div style={{ marginBottom: spacing[16] }}>
<Tag
color={testResult.success ? 'green' : 'red'}
style={{ padding: `${spacing[4]} ${spacing[12]}` }}
>
{testResult.message || (testResult.success ? '连通成功' : '连通失败')}
{testResult.latencyMs !== undefined && ` | ${testResult.latencyMs}ms`}
{testResult.model && ` | ${testResult.model}`}
</Tag>
</div>
)}
<div style={{ marginTop: spacing[24] }}>
<Space>
<Button
htmlType="submit"
loading={deepSeekSaving}
style={{ borderRadius: radius.button }}
>
{t['aiDeepSeek.action.save']}
</Button>
<Button
type="outline"
onClick={handleTestDeepSeek}
loading={deepSeekTesting}
style={{ borderRadius: radius.button }}
>
{t['aiDeepSeek.action.test']}
</Button>
</Space>
</div>
</Form>
</Spin>
</Card>
</div>
);
}

查看文件

@@ -43,6 +43,31 @@ const i18n = {
'aiConcierge.request.success': 'Concierge settings saved successfully',
'aiConcierge.request.fail': 'Failed to load concierge configuration',
'aiConcierge.request.saveFail': 'Failed to save configuration',
'aiDeepSeek.card.title': 'DeepSeek Official LLM Configuration',
'aiDeepSeek.card.desc':
'Configure DeepSeek API Key, supporting deepseek-chat and deepseek-reasoner dual models for 24/7 AI concierge and cross-border localization.',
'aiDeepSeek.form.apiKey': 'API Key',
'aiDeepSeek.form.apiKey.placeholder':
'Enter official API Key starting with sk- (leave blank to keep current)',
'aiDeepSeek.form.baseUrl': 'API Base URL',
'aiDeepSeek.form.model': 'Model Version',
'aiDeepSeek.model.chat': 'DeepSeek-V3 Chat (deepseek-chat)',
'aiDeepSeek.model.reasoner': 'DeepSeek-R1 Reasoner (deepseek-reasoner)',
'aiDeepSeek.form.temperature': 'Temperature',
'aiDeepSeek.form.temperature.desc':
'Lower values are deterministic; higher values are creative.',
'aiDeepSeek.form.status': 'Enable DeepSeek LLM',
'aiDeepSeek.form.status.desc':
'When enabled, requests prioritize DeepSeek official API with smooth fallback.',
'aiDeepSeek.action.test': 'Test Connection',
'aiDeepSeek.action.testing': 'Testing connection...',
'aiDeepSeek.action.save': 'Save Model Config',
'aiDeepSeek.status.configured': 'Configured',
'aiDeepSeek.status.unconfigured': 'Not Configured',
'aiDeepSeek.test.success': 'DeepSeek API connection successful',
'aiDeepSeek.test.fail': 'Connection test failed',
'aiDeepSeek.save.success': 'DeepSeek configuration updated',
'aiDeepSeek.save.fail': 'Failed to save DeepSeek configuration',
},
'zh-CN': {
'menu.ai.concierge': 'AI 顾问配置',
@@ -88,6 +113,31 @@ const i18n = {
'aiConcierge.request.success': 'AI 顾问配置已保存',
'aiConcierge.request.fail': 'AI 顾问配置加载失败',
'aiConcierge.request.saveFail': '保存配置失败',
'aiDeepSeek.card.title': 'DeepSeek 官方大模型底座配置',
'aiDeepSeek.card.desc':
'配置 DeepSeek API Key,支持 deepseek-chat 与 deepseek-reasoner 双模型接入,赋能 24/7 AI 顾问对话与出海多语言润色。',
'aiDeepSeek.form.apiKey': 'API Key 密钥',
'aiDeepSeek.form.apiKey.placeholder':
'输入 sk- 开头的官方密钥 (留空则保留当前已配置密钥)',
'aiDeepSeek.form.baseUrl': 'API 接口基地址 (Base URL)',
'aiDeepSeek.form.model': '接入模型版本',
'aiDeepSeek.model.chat': 'DeepSeek-V3 对话模型 (deepseek-chat)',
'aiDeepSeek.model.reasoner': 'DeepSeek-R1 深度推理模型 (deepseek-reasoner)',
'aiDeepSeek.form.temperature': '采样温度 (Temperature)',
'aiDeepSeek.form.temperature.desc':
'较低值输出严谨稳定,较高值回复生动多样。',
'aiDeepSeek.form.status': '启用 DeepSeek 大模型',
'aiDeepSeek.form.status.desc':
'开启后系统优先直连 DeepSeek 官方 API,网络异常或未配置时自动平滑降级。',
'aiDeepSeek.action.test': '测试连通性',
'aiDeepSeek.action.testing': '连通性测试中...',
'aiDeepSeek.action.save': '保存大模型配置',
'aiDeepSeek.status.configured': '已配置',
'aiDeepSeek.status.unconfigured': '未配置',
'aiDeepSeek.test.success': 'DeepSeek API 连通测试成功',
'aiDeepSeek.test.fail': '连通性测试失败',
'aiDeepSeek.save.success': 'DeepSeek 大模型配置已保存',
'aiDeepSeek.save.fail': '保存大模型配置失败',
},
'ar-SA': {
'menu.ai.concierge': 'إعدادات المساعد الذكي',
@@ -133,6 +183,31 @@ const i18n = {
'aiConcierge.request.success': 'تم حفظ الإعدادات بنجاح',
'aiConcierge.request.fail': 'تعذر تحميل الإعدادات',
'aiConcierge.request.saveFail': 'فشل حفظ الإعدادات',
'aiDeepSeek.card.title': 'إعدادات نموذج DeepSeek الرسمي',
'aiDeepSeek.card.desc':
'تكوين مفتاح واجهة DeepSeek البرمجية لدعم محادثات المساعد الذكي وترجمة المحتوى.',
'aiDeepSeek.form.apiKey': 'مفتاح API',
'aiDeepSeek.form.apiKey.placeholder':
'أدخل المفتاح الرسمي الذي يبدأ بـ sk-',
'aiDeepSeek.form.baseUrl': 'عنوان الواجهة الأساسي',
'aiDeepSeek.form.model': 'إصدار النموذج',
'aiDeepSeek.model.chat': 'نموذج محادثة DeepSeek-V3',
'aiDeepSeek.model.reasoner': 'نموذج استدلال DeepSeek-R1',
'aiDeepSeek.form.temperature': 'درجة الحرارة',
'aiDeepSeek.form.temperature.desc':
'القيم المنخفضة أكثر دقة والصلبة أكثر إبداعا.',
'aiDeepSeek.form.status': 'تفعيل نموذج DeepSeek',
'aiDeepSeek.form.status.desc':
'عند التفعيل، تعطى الأولوية لواجهة DeepSeek مع خفض سلس للأداء عند الانقطاع.',
'aiDeepSeek.action.test': 'اختبار الاتصال',
'aiDeepSeek.action.testing': 'جاري اختبار الاتصال...',
'aiDeepSeek.action.save': 'حفظ إعدادات النموذج',
'aiDeepSeek.status.configured': 'تم التكوين',
'aiDeepSeek.status.unconfigured': 'غير مكوّن',
'aiDeepSeek.test.success': 'نجح اختبار الاتصال بـ DeepSeek',
'aiDeepSeek.test.fail': 'فشل اختبار الاتصال',
'aiDeepSeek.save.success': 'تم حفظ إعدادات DeepSeek بنجاح',
'aiDeepSeek.save.fail': 'فشل حفظ إعدادات DeepSeek',
},
};

查看文件

@@ -476,6 +476,42 @@ export interface AiConciergeConfig {
isEnabled: boolean;
}
// ==========================================
// 5.1 DeepSeek 大模型集成契约 (DeepSeek AI Suite)
// ==========================================
export type DeepSeekModel = 'deepseek-chat' | 'deepseek-reasoner';
export interface DeepSeekConfig {
apiKey?: string;
baseUrl?: string;
model?: DeepSeekModel;
temperature?: number;
maxTokens?: number;
timeoutMs?: number;
enabled?: boolean;
}
export interface DeepSeekChatMessage {
role: 'system' | 'user' | 'assistant';
content: string;
}
export interface DeepSeekChatCompletionRequest {
messages: DeepSeekChatMessage[];
model?: DeepSeekModel;
temperature?: number;
maxTokens?: number;
stream?: boolean;
}
export interface DeepSeekTestConnectionResult {
success: boolean;
latencyMs?: number;
model?: string;
message?: string;
}
export type LocalizeTone = 'professional' | 'direct' | 'storytelling';
export interface LocalizeRequest {
@@ -725,4 +761,117 @@ export interface TeamRankBroadcastPayload {
meta?: TeamRankBroadcastMeta;
}
// ==========================================
// 12. AI 顾问对话与 DeepSeek 大模型契约 (AI Concierge & DeepSeek Suite)
// ==========================================
export type AiChatRole = 'user' | 'assistant' | 'system';
export interface AiChatMessage {
role: AiChatRole;
content: string;
}
export interface AiChatRequest {
cardId: string | number;
locale?: string;
messages: AiChatMessage[];
}
export interface AiChatReply {
reply: string;
locale: string;
shouldCollectLead: boolean;
}
export interface AiKnowledgeDoc {
id?: string | number;
cardId: string | number;
title: string;
content: string;
locale?: string;
}
export type ConciergePersonaType =
| 'trade_expert'
| 'business_rep'
| 'tech_advisor'
| 'custom';
export type ConciergeToneStyle =
| 'professional'
| 'direct'
| 'enthusiastic'
| 'friendly';
export interface AiConciergeConfig {
id?: string | number;
cardId: string | number;
personaType: ConciergePersonaType;
welcomeMsg: string;
triggerLeadThreshold: number;
promptTemplate: string;
toneStyle: ConciergeToneStyle;
isEnabled: boolean;
}
export interface KnowledgeChunk {
id?: string | number;
index: number;
content: string;
length: number;
score?: number;
}
export type DeepSeekModel = 'deepseek-chat' | 'deepseek-reasoner';
export interface DeepSeekConfig {
apiKey?: string;
baseUrl?: string;
model?: DeepSeekModel;
temperature?: number;
maxTokens?: number;
timeoutMs?: number;
enabled?: boolean;
isConfigured?: boolean;
}
export interface DeepSeekChatMessage {
role: 'system' | 'user' | 'assistant';
content: string;
}
export interface DeepSeekChatCompletionRequest {
messages: DeepSeekChatMessage[];
model?: DeepSeekModel;
temperature?: number;
maxTokens?: number;
stream?: boolean;
}
export interface DeepSeekChatCompletionResponse {
id: string;
model: string;
choices: Array<{
index: number;
message: {
role: 'assistant';
content: string;
};
finish_reason: string;
}>;
usage?: {
prompt_tokens: number;
completion_tokens: number;
total_tokens: number;
};
}
export interface DeepSeekTestConnectionResult {
success: boolean;
latencyMs?: number;
model?: string;
message?: string;
}

查看文件

@@ -0,0 +1,62 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const adminRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
const conciergePagePath = path.join(adminRoot, 'src/pages/ai/concierge/index.tsx');
const conciergeLocalePath = path.join(adminRoot, 'src/pages/ai/concierge/locale/index.ts');
const omniTypesPath = path.join(adminRoot, 'src/types/omni.ts');
test('1. DeepSeek 大模型后台界面必须接入标准 API 与连通性测试端点', () => {
assert.ok(fs.existsSync(conciergePagePath), 'AI 页面文件必须存在');
const content = fs.readFileSync(conciergePagePath, 'utf-8');
assert.ok(
content.includes('/api/admin/ai/deepseek/config'),
'必须包含 /api/admin/ai/deepseek/config 配置端点'
);
assert.ok(
content.includes('/api/admin/ai/deepseek/test-connection'),
'必须包含 /api/admin/ai/deepseek/test-connection 连通性测试端点'
);
assert.ok(content.includes('deepseek-chat'), '必须支持 deepseek-chat (V3)');
assert.ok(content.includes('deepseek-reasoner'), '必须支持 deepseek-reasoner (R1)');
assert.ok(content.includes('handleTestDeepSeek'), '必须提供连通性测试处理函数');
assert.ok(content.includes('handleSaveDeepSeek'), '必须提供保存配置处理函数');
});
test('2. DeepSeek 大模型跨端类型契约必须在 admin/src/types/omni.ts 中严密定义', () => {
assert.ok(fs.existsSync(omniTypesPath), 'omni.ts 类型契约必须存在');
const content = fs.readFileSync(omniTypesPath, 'utf-8');
assert.ok(content.includes('DeepSeekModel'), '必须导出 DeepSeekModel 类型');
assert.ok(content.includes('DeepSeekConfig'), '必须导出 DeepSeekConfig 接口');
assert.ok(content.includes('DeepSeekChatMessage'), '必须导出 DeepSeekChatMessage 接口');
assert.ok(
content.includes('DeepSeekTestConnectionResult'),
'必须导出 DeepSeekTestConnectionResult 接口'
);
});
test('3. DeepSeek 多语言国际化必须覆盖中英阿且无 emoji 无 any', () => {
assert.ok(fs.existsSync(conciergeLocalePath), '语言包必须存在');
const localeContent = fs.readFileSync(conciergeLocalePath, 'utf-8');
assert.ok(localeContent.includes("'aiDeepSeek.card.title'"), '必须包含卡片标题词条');
assert.ok(localeContent.includes("'aiDeepSeek.form.apiKey'"), '必须包含 API Key 表单项');
assert.ok(localeContent.includes("'aiDeepSeek.form.model'"), '必须包含模型选择项');
assert.ok(localeContent.includes("'aiDeepSeek.action.test'"), '必须包含测试连通性按钮词条');
assert.ok(localeContent.includes("'aiDeepSeek.test.success'"), '必须包含测试成功提示');
// 三语检查
assert.ok(localeContent.includes("'en-US'"), '必须支持 en-US');
assert.ok(localeContent.includes("'zh-CN'"), '必须支持 zh-CN');
assert.ok(localeContent.includes("'ar-SA'"), '必须支持 ar-SA');
// 禁止 emoji
const emojiRegex = /[\u{1F300}-\u{1F9FF}\u{2600}-\u{26FF}\u{2700}-\u{27BF}]/u;
assert.ok(!emojiRegex.test(localeContent), '语言包严禁使用 emoji');
assert.ok(!/\bany\b/.test(localeContent), '语言包严禁使用 any');
});

查看文件

@@ -1,85 +1,9 @@
import {
Index,
PrimaryGeneratedColumn,
Column,
DeleteDateColumn,
} from 'typeorm';
import * as moment from 'moment';
import { CoolBaseEntity } from '@cool-midway/core';
/**
* 时间转换器
* 统一实体基类重导出 (兼容历史路径)
* 复用 Cool-Admin 官方 modules/base/entity/base
*/
export const transformerTime = {
to(value?: Date | string | null) {
return value
? moment(value).format('YYYY-MM-DD HH:mm:ss')
: moment().format('YYYY-MM-DD HH:mm:ss');
},
from(value?: string | null) {
return value;
},
};
/**
* Json 转换器
*/
export const transformerJson = {
to: (value: unknown) => value,
from: (value: unknown) => {
if (typeof value === 'string') {
try {
return JSON.parse(value);
} catch (e) {
return value;
}
}
return value;
},
};
/**
* 通用实体基类 (BaseEntity)
* 提供主键自增、多租户隔离 (tenantId)、审计时间戳 (createTime/updateTime) 及软删除 (deletedAt)
*/
export abstract class BaseEntity extends CoolBaseEntity {
@PrimaryGeneratedColumn('increment', {
name: 'id',
comment: 'ID',
})
id: number;
@Index()
@Column({
name: 'tenant_id',
comment: '企业租户ID',
nullable: true,
})
tenantId: number;
@Index()
@Column({
name: 'create_time',
comment: '创建时间',
type: 'varchar',
transformer: transformerTime,
})
createTime: Date;
@Index()
@Column({
name: 'update_time',
comment: '更新时间',
type: 'varchar',
transformer: transformerTime,
})
updateTime: Date;
@Index()
@DeleteDateColumn({
name: 'deleted_at',
comment: '软删除时间',
nullable: true,
})
deletedAt?: Date | null;
}
export {
BaseEntity,
transformerJson,
transformerTime,
} from '../../modules/base/entity/base';

查看文件

@@ -599,6 +599,60 @@ export interface KnowledgeChunk {
score?: number;
}
// ==========================================
// 5.1 DeepSeek 大模型集成契约 (DeepSeek AI Suite)
// ==========================================
export type DeepSeekModel = 'deepseek-chat' | 'deepseek-reasoner';
export interface DeepSeekConfig {
apiKey?: string;
baseUrl?: string;
model?: DeepSeekModel;
temperature?: number;
maxTokens?: number;
timeoutMs?: number;
enabled?: boolean;
}
export interface DeepSeekChatMessage {
role: 'system' | 'user' | 'assistant';
content: string;
}
export interface DeepSeekChatCompletionRequest {
messages: DeepSeekChatMessage[];
model?: DeepSeekModel;
temperature?: number;
maxTokens?: number;
stream?: boolean;
}
export interface DeepSeekChatCompletionResponse {
id: string;
model: string;
choices: Array<{
index: number;
message: {
role: 'assistant';
content: string;
};
finish_reason: string;
}>;
usage?: {
prompt_tokens: number;
completion_tokens: number;
total_tokens: number;
};
}
export interface DeepSeekTestConnectionResult {
success: boolean;
latencyMs?: number;
model?: string;
message?: string;
}
/**
* 知识库沙盒调试请求契约
*/

查看文件

@@ -0,0 +1,65 @@
import { Body, Inject, Post } from '@midwayjs/core';
import { BaseController, CoolController } from '@cool-midway/core';
import { DeepSeekService } from '../../service/deepseek';
import {
DeepSeekChatCompletionRequest,
DeepSeekConfig,
} from '../../../../comm/types/omni';
/**
* DeepSeek 大模型后台管理与调试控制器
*/
@CoolController({
api: [],
service: DeepSeekService,
serviceApis: [
{ method: 'getConfig', summary: '获取当前 DeepSeek 配置 (脱敏)' },
{ method: 'updateConfig', summary: '更新 DeepSeek 配置' },
{ method: 'testConnection', summary: '测试 DeepSeek 接口连通性' },
{ method: 'chatCompletion', summary: '大模型对话测试' },
],
})
export class AdminDeepSeekController extends BaseController {
@Inject()
deepSeekService: DeepSeekService;
/**
* 获取当前有效配置(脱敏 API Key)
*/
@Post('/config', { summary: '获取或保存 DeepSeek 配置' })
async config(@Body() body?: Partial<DeepSeekConfig>) {
if (body && Object.keys(body).length > 0) {
this.deepSeekService.updateConfig(body);
}
const current = this.deepSeekService.getConfig();
const maskedKey = current.apiKey
? current.apiKey.length > 8
? `${current.apiKey.slice(0, 4)}****${current.apiKey.slice(-4)}`
: '****'
: undefined;
return this.ok({
...current,
apiKey: maskedKey,
isConfigured: this.deepSeekService.isConfigured(),
});
}
/**
* 测试与 DeepSeek 官方端点连通性
*/
@Post('/test-connection', { summary: '测试连通性' })
async testConnection(@Body('apiKey') apiKey?: string) {
const res = await this.deepSeekService.testConnection(apiKey);
return this.ok(res);
}
/**
* 调试对话补全
*/
@Post('/chat', { summary: '调试对话补全' })
async chat(@Body() body: DeepSeekChatCompletionRequest) {
const reply = await this.deepSeekService.chatCompletion(body);
return this.ok({ reply });
}
}

查看文件

@@ -1,4 +1,4 @@
import { BaseEntity } from '../../../comm/entity/base';
import { BaseEntity } from '../../base/entity/base';
import { Column, Entity, Index } from 'typeorm';
import {
AiConciergeConfig,

查看文件

@@ -1,4 +1,4 @@
import { BaseEntity } from '../../../comm/entity/base';
import { BaseEntity } from '../../base/entity/base';
import { Column, Entity, Index } from 'typeorm';
import { AiGlossaryItem } from '../../../comm/types/omni';

查看文件

@@ -1,4 +1,4 @@
import { BaseEntity } from '../../../comm/entity/base';
import { BaseEntity } from '../../base/entity/base';
import { Column, Entity, Index } from 'typeorm';
import { AiKnowledgeDoc } from '../../../comm/types/omni';

查看文件

@@ -1,10 +1,12 @@
import { Provide } from '@midwayjs/core';
import { Inject, Provide } from '@midwayjs/core';
import { BaseService, CoolCommException } from '@cool-midway/core';
import {
AiChatMessage,
AiChatReply,
AiChatRequest,
DeepSeekChatMessage,
} from '../../../comm/types/omni';
import { DeepSeekService } from './deepseek';
/** locale 缺省值 */
const DEFAULT_LOCALE = 'en-US';
@@ -14,10 +16,13 @@ const LEAD_INTENT_PATTERN =
/价格|报价|多少钱|合作|联系|price|pricing|cost|quote|cooperat|partner|contact/i;
/**
* 名片内置 AI 顾问(本地 RuleBased,不调用外部 LLM)
* 名片内置 AI 顾问(支持 DeepSeek 官方大模型与本地规则平滑降级)
*/
@Provide()
export class AiChatService extends BaseService {
@Inject()
deepSeekService?: DeepSeekService;
/**
* 顾问对话
* 兼容公开端 serviceApis:POST body 为单参数对象 AiChatRequest
@@ -42,9 +47,36 @@ export class AiChatService extends BaseService {
const locale = req.locale?.trim() || DEFAULT_LOCALE;
const shouldCollectLead = LEAD_INTENT_PATTERN.test(content);
const isZh = locale.toLowerCase().startsWith('zh');
const fallbackReply = this.buildReply(shouldCollectLead, isZh);
let reply = fallbackReply;
// 如果 DeepSeek 已经配置,则优先调用 DeepSeek 大模型生成智能回复
if (this.deepSeekService?.isConfigured?.()) {
try {
const dsMessages: DeepSeekChatMessage[] = [
{
role: 'system',
content: `你是 OmniCard 名片智能顾问。当前名片ID: ${cardId},语言偏好: ${locale}。请以专业得体的口吻向客商介绍业务与解答咨询。${shouldCollectLead ? '检测到客户具备采购或商务意向,请在回答中热情引导客户留下联系方式。' : ''}`,
},
...messages.map((m) => ({
role: m.role as 'user' | 'assistant' | 'system',
content: m.content,
})),
];
reply = await this.deepSeekService.chatCompletion(
{ messages: dsMessages },
fallbackReply
);
} catch {
// 发生任何不可预测异常,均平滑降级为兜底文本,确保前端 100% 不白屏
reply = fallbackReply;
}
}
return {
reply: this.buildReply(shouldCollectLead, isZh),
reply,
locale,
shouldCollectLead,
};

查看文件

@@ -0,0 +1,257 @@
import { Provide } from '@midwayjs/core';
import { BaseService, CoolCommException } from '@cool-midway/core';
import axios, { AxiosInstance } from 'axios';
import {
DeepSeekChatCompletionRequest,
DeepSeekChatCompletionResponse,
DeepSeekConfig,
DeepSeekModel,
DeepSeekTestConnectionResult,
} from '../../../comm/types/omni';
/**
* DeepSeek 官方大模型适配服务
* 支持 deepseek-chat (V3) 与 deepseek-reasoner (R1)
*/
@Provide()
export class DeepSeekService extends BaseService {
private config: DeepSeekConfig;
private httpClient: AxiosInstance;
constructor() {
super();
const envKey = process.env.DEEPSEEK_API_KEY?.trim() || undefined;
const envBaseUrl = process.env.DEEPSEEK_BASE_URL?.trim() || 'https://api.deepseek.com';
const envModel = (process.env.DEEPSEEK_MODEL?.trim() as DeepSeekModel) || 'deepseek-chat';
this.config = {
apiKey: envKey,
baseUrl: envBaseUrl,
model: envModel,
temperature: 0.7,
maxTokens: 2048,
timeoutMs: 30000,
enabled: Boolean(envKey),
};
this.httpClient = axios.create({
timeout: this.config.timeoutMs,
});
}
/**
* 获取当前有效配置(脱敏 API Key)
*/
getConfig(): DeepSeekConfig {
return { ...this.config };
}
/**
* 动态更新配置
*/
updateConfig(partial: Partial<DeepSeekConfig>): void {
this.config = {
...this.config,
...partial,
enabled: partial.apiKey ? Boolean(partial.apiKey.trim()) : this.config.enabled,
};
if (this.config.timeoutMs) {
this.httpClient.defaults.timeout = this.config.timeoutMs;
}
}
/**
* 检查是否已配置有效 API Key
*/
isConfigured(): boolean {
return Boolean(this.config.apiKey && this.config.apiKey.trim().length > 0);
}
/**
* 组装名片 AI 顾问 System 提示词
*/
buildSystemPrompt(
card?: { name?: string; company?: string; title?: string },
concierge?: { personaType?: string; toneStyle?: string; promptTemplate?: string },
knowledgeChunks?: Array<{ index?: number; content: string }>
): string {
const parts: string[] = [];
parts.push('你是 OmniCard AI 驱动的移动微官网数字人智能顾问。');
if (card?.name) parts.push(`名片所有者姓名: ${card.name}`);
if (card?.company) parts.push(`所属企业: ${card.company}`);
if (card?.title) parts.push(`头衔职位: ${card.title}`);
if (concierge?.promptTemplate) {
parts.push(`人设与使命:\n${concierge.promptTemplate}`);
} else {
parts.push('你的职责是以专业、礼貌、高效的口吻向访客介绍业务与产品,并在适当时机引导有意向的客户留下联系方式。');
}
if (Array.isArray(knowledgeChunks) && knowledgeChunks.length > 0) {
parts.push('参考企业知识库信息 (优先基于以下内容作答):');
knowledgeChunks.forEach((c, idx) => {
parts.push(`[${idx + 1}] ${c.content}`);
});
}
parts.push('请直接给出自然得体的中文或对应语言回答,避免套话。');
return parts.join('\n');
}
/**
* 组装出海文案润色提示词
*/
buildLocalizePrompt(
text: string,
targetLocale: string,
tone: string,
glossaryTerms?: string[]
): string {
const lines = [
`请将以下商务文本改写/润色为符合目标语言(${targetLocale})与目标语调(${tone})的地道出海商业文案。`,
'要求:语义准确地道,符合国际商业习惯,禁止机械直译。',
];
if (Array.isArray(glossaryTerms) && glossaryTerms.length > 0) {
lines.push('行业专有名词强制对齐规则 (严格遵照):');
glossaryTerms.forEach((g) => lines.push(`- ${g}`));
}
lines.push(`\n原文:\n${text}`);
return lines.join('\n');
}
/**
* 组装销售 Copilot 破冰首语提示词
*/
buildCopilotPrompt(
lead: { name?: string; company?: string; demand?: string; phone?: string },
tracks?: Array<{ module?: string; duration?: number }>
): string {
const lines = [
'你是一名资深出海销售战略顾问。请根据以下潜在客户画像与其在名片上的浏览行为轨迹,生成一段极具针对性的 WhatsApp/Email 商务破冰首语建议。',
`潜客姓名: ${lead?.name || '客商'}`,
`所属公司: ${lead?.company || '出海企业'}`,
`核心诉求: ${lead?.demand || '商务洽谈'}`,
];
if (Array.isArray(tracks) && tracks.length > 0) {
lines.push('访客轨迹分析:');
tracks.forEach((t) => {
lines.push(`- 模块 [${t.module || 'portal'}], 停留时长: ${t.duration || 0}秒`);
});
}
lines.push('请生成:1. 客户核心意向诊断要点;2. 30秒直击痛点的破冰首语推荐。');
return lines.join('\n');
}
/**
* 对话补全调用 (支持优雅降级)
*/
async chatCompletion(
req: DeepSeekChatCompletionRequest,
fallbackReply?: string
): Promise<string> {
const apiKey = this.config.apiKey?.trim();
if (!apiKey) {
if (fallbackReply !== undefined) {
return fallbackReply;
}
throw new CoolCommException('DeepSeek API Key 未配置');
}
const endpoint = `${this.config.baseUrl || 'https://api.deepseek.com'}/chat/completions`;
const payload = {
model: req.model || this.config.model || 'deepseek-chat',
messages: req.messages,
temperature: req.temperature ?? this.config.temperature ?? 0.7,
max_tokens: req.maxTokens ?? this.config.maxTokens ?? 2048,
stream: false,
};
try {
const response = await this.httpClient.post<DeepSeekChatCompletionResponse>(
endpoint,
payload,
{
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${apiKey}`,
},
}
);
const content = response.data?.choices?.[0]?.message?.content;
if (content) {
return content.trim();
}
if (fallbackReply !== undefined) {
return fallbackReply;
}
return 'AI 思考中,暂未生成有效回复。';
} catch (err: any) {
if (fallbackReply !== undefined) {
return fallbackReply;
}
const errMsg =
err?.response?.data?.error?.message || err?.message || 'DeepSeek API 调用失败';
throw new CoolCommException(`DeepSeek 服务异常: ${errMsg}`);
}
}
/**
* 测试与 DeepSeek 官方端点连通性
*/
async testConnection(testApiKey?: string): Promise<DeepSeekTestConnectionResult> {
const key = (testApiKey || this.config.apiKey || '').trim();
if (!key) {
return {
success: false,
message: 'API Key 未提供,无法测试连通性',
};
}
const startTime = Date.now();
const endpoint = `${this.config.baseUrl || 'https://api.deepseek.com'}/chat/completions`;
const model = this.config.model || 'deepseek-chat';
try {
await this.httpClient.post(
endpoint,
{
model,
messages: [{ role: 'user', content: 'hi' }],
max_tokens: 5,
},
{
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${key}`,
},
timeout: 10000, // 测试连通性 10 秒超时
}
);
const latencyMs = Date.now() - startTime;
return {
success: true,
latencyMs,
model,
message: 'DeepSeek API 连通成功',
};
} catch (err: any) {
const errMsg =
err?.response?.data?.error?.message || err?.message || '网络连接超时或凭证失效';
return {
success: false,
latencyMs: Date.now() - startTime,
model,
message: `连通失败: ${errMsg}`,
};
}
}
}

查看文件

@@ -6,6 +6,7 @@ import {
LocalizeTone,
} from '../../../comm/types/omni';
import { AiGlossaryService } from './glossary';
import { DeepSeekService } from './deepseek';
const DEFAULT_TONE: LocalizeTone = 'professional';
@@ -23,6 +24,9 @@ export class AiLocalizeService extends BaseService {
@Inject()
aiGlossaryService?: AiGlossaryService;
@Inject()
deepSeekService?: DeepSeekService;
/**
* 按目标语言与语气改写原文骨架,并结合术语库进行精准替换
* 兼容后台 serviceApis:POST body 为单参数对象 LocalizeRequest
@@ -39,7 +43,23 @@ export class AiLocalizeService extends BaseService {
}
const tone = this.resolveTone(req.tone);
let polishedText = this.polish(text, locale, tone);
const fallbackText = this.polish(text, locale, tone);
let polishedText = fallbackText;
// 如果 DeepSeek 已经配置,则优先调用 DeepSeek 进行地道润色
if (this.deepSeekService?.isConfigured?.()) {
try {
const prompt = this.deepSeekService.buildLocalizePrompt(text, locale, tone);
polishedText = await this.deepSeekService.chatCompletion(
{
messages: [{ role: 'user', content: prompt }],
},
fallbackText
);
} catch {
polishedText = fallbackText;
}
}
if (this.aiGlossaryService?.replaceTerms) {
polishedText = await this.aiGlossaryService.replaceTerms(

查看文件

@@ -1,4 +1,4 @@
import { Index, PrimaryGeneratedColumn, Column } from 'typeorm';
import { Index, PrimaryGeneratedColumn, Column, DeleteDateColumn } from 'typeorm';
import * as moment from 'moment';
import { CoolBaseEntity } from '@cool-midway/core';
@@ -45,6 +45,7 @@ export abstract class BaseEntity extends CoolBaseEntity {
@Index()
@Column({
name: 'create_time',
comment: '创建时间',
type: 'varchar',
transformer: transformerTime,
@@ -53,6 +54,7 @@ export abstract class BaseEntity extends CoolBaseEntity {
@Index()
@Column({
name: 'update_time',
comment: '更新时间',
type: 'varchar',
transformer: transformerTime,
@@ -60,6 +62,18 @@ export abstract class BaseEntity extends CoolBaseEntity {
updateTime: Date;
@Index()
@Column({ comment: '租户ID', nullable: true })
@Column({
name: 'tenant_id',
comment: '租户ID',
nullable: true,
})
tenantId: number;
@Index()
@DeleteDateColumn({
name: 'deleted_at',
comment: '软删除时间',
nullable: true,
})
deletedAt?: Date | null;
}

查看文件

@@ -1,4 +1,4 @@
import { BaseEntity, transformerJson } from '../../../comm/entity/base';
import { BaseEntity, transformerJson } from '../../base/entity/base';
import { Column, Entity, Index } from 'typeorm';
import type { ArticleOgMeta, CardArticle } from '../../../comm/types/omni';

查看文件

@@ -1,4 +1,4 @@
import { BaseEntity, transformerJson } from '../../../comm/entity/base';
import { BaseEntity, transformerJson } from '../../base/entity/base';
import { Column, Entity, Index } from 'typeorm';
import type {
BentoBlock,

查看文件

@@ -1,4 +1,4 @@
import { BaseEntity } from '../../../comm/entity/base';
import { BaseEntity } from '../../base/entity/base';
import { Column, Entity, Index } from 'typeorm';
import type { CardCase } from '../../../comm/types/omni';

查看文件

@@ -1,4 +1,4 @@
import { BaseEntity } from '../../../comm/entity/base';
import { BaseEntity } from '../../base/entity/base';
import { Column, Entity, Index } from 'typeorm';
import {
CardProfile,

查看文件

@@ -1,4 +1,4 @@
import { BaseEntity } from '../../../comm/entity/base';
import { BaseEntity } from '../../base/entity/base';
import { Column, Entity, Index } from 'typeorm';
/**

查看文件

@@ -1,4 +1,4 @@
import { BaseEntity } from '../../../comm/entity/base';
import { BaseEntity } from '../../base/entity/base';
import { Column, Entity, Index } from 'typeorm';
import { LeadPoolAction } from '../../../comm/types/omni';

查看文件

@@ -1,4 +1,4 @@
import { BaseEntity, transformerTime } from '../../../comm/entity/base';
import { BaseEntity, transformerTime } from '../../base/entity/base';
import { Column, Entity, Index } from 'typeorm';
import {
LeadEntity as ILeadEntity,
@@ -118,4 +118,4 @@ export class CrmLeadEntity extends BaseEntity implements ILeadEntity {
lastFollowUpTime?: Date | string | null;
}
export { CrmLeadEntity as LeadInfoEntity, CrmLeadEntity as LeadEntity };
export { CrmLeadEntity as LeadInfoEntity };

查看文件

@@ -1,4 +1,4 @@
import { BaseEntity, transformerJson } from '../../../comm/entity/base';
import { BaseEntity, transformerJson } from '../../base/entity/base';
import { Column, Entity, Index } from 'typeorm';
import { RadarTrack, TouchType } from '../../../comm/types/omni';

查看文件

@@ -1,4 +1,4 @@
import { BaseEntity } from '../../../comm/entity/base';
import { BaseEntity } from '../../base/entity/base';
import { Column, Entity, Index } from 'typeorm';
import {
NFCMaterial,

查看文件

@@ -1,4 +1,4 @@
import { BaseEntity, transformerJson } from '../../../comm/entity/base';
import { BaseEntity, transformerJson } from '../../base/entity/base';
import { Column, Entity, Index } from 'typeorm';
import { NFCSceneMode, NFCSceneRule } from '../../../comm/types/omni';

查看文件

@@ -1,4 +1,4 @@
import { BaseEntity } from '../../../comm/entity/base';
import { BaseEntity } from '../../base/entity/base';
import { Column, Entity } from 'typeorm';
/**

查看文件

@@ -1,4 +1,4 @@
import { BaseEntity } from '../../../comm/entity/base';
import { BaseEntity } from '../../base/entity/base';
import { Column, Entity, Index } from 'typeorm';
import { DomainSslStatus, TenantDomain } from '../../../comm/types/omni';

查看文件

@@ -1,4 +1,4 @@
import { BaseEntity } from '../../../comm/entity/base';
import { BaseEntity } from '../../base/entity/base';
import { Column, Entity, Index } from 'typeorm';
import {
HandoverLeadAction,

查看文件

@@ -1,4 +1,4 @@
import { BaseEntity, transformerJson } from '../../../comm/entity/base';
import { BaseEntity, transformerJson } from '../../base/entity/base';
import { Column, Entity, Index } from 'typeorm';
import {
InvoiceBillingInfo,

查看文件

@@ -1,4 +1,4 @@
import { BaseEntity } from '../../../comm/entity/base';
import { BaseEntity } from '../../base/entity/base';
import { Column, Entity, Index } from 'typeorm';
import { TenantPlanType, StripeBillingCycle } from '../../../comm/types/omni';

查看文件

@@ -1057,7 +1057,9 @@ describe('OmniCard AI 30-Point End-to-End Pipeline Integration Suite', () => {
expect(activeDevice?.status).toBe('active');
});
it('P26: 展会批量制卡清单 CSV 与二维码离线 ZIP 压缩包归档导出', async () => {
it(
'P26: 展会批量制卡清单 CSV 与二维码离线 ZIP 压缩包归档导出',
async () => {
const mockBatchDevices = [
{
uid: sharedNfcUid,
@@ -1092,7 +1094,7 @@ describe('OmniCard AI 30-Point End-to-End Pipeline Integration Suite', () => {
expect(csvText).toContain('出厂防伪码');
expect(csvText).toContain(sharedNfcUid);
expect(csvText).toContain('Alex Zheng - VP Global');
});
}, 15000);
});
// =========================================================================

查看文件

@@ -0,0 +1,74 @@
import { AdminDeepSeekController } from '../../../src/modules/ai/controller/admin/deepseek';
import { DeepSeekService } from '../../../src/modules/ai/service/deepseek';
describe('T32 AdminDeepSeekController 后台接口测试', () => {
let controller: AdminDeepSeekController;
let mockService: jest.Mocked<DeepSeekService>;
beforeEach(() => {
mockService = {
getConfig: jest.fn().mockReturnValue({
apiKey: 'sk-1234567890abcdef',
baseUrl: 'https://api.deepseek.com',
model: 'deepseek-chat',
temperature: 0.7,
maxTokens: 2048,
timeoutMs: 30000,
enabled: true,
}),
updateConfig: jest.fn(),
isConfigured: jest.fn().mockReturnValue(true),
testConnection: jest.fn().mockResolvedValue({
success: true,
latencyMs: 120,
model: 'deepseek-chat',
message: 'DeepSeek API 连通成功',
}),
chatCompletion: jest.fn().mockResolvedValue('你好!我是 DeepSeek 智能模型。'),
} as any;
controller = new AdminDeepSeekController();
controller.deepSeekService = mockService;
// mock controller.ok
(controller as any).ok = (data: any) => ({ code: 1000, message: 'success', data });
});
it('1. POST /config 获取配置且正确脱敏 API Key', async () => {
const res: any = await controller.config();
expect(res.code).toBe(1000);
expect(res.data.isConfigured).toBe(true);
expect(res.data.apiKey).toBe('sk-1****cdef');
expect(res.data.baseUrl).toBe('https://api.deepseek.com');
});
it('2. POST /config 提交更新配置并返回新状态', async () => {
const res: any = await controller.config({
apiKey: 'sk-new-key-abcdef123456',
model: 'deepseek-reasoner',
});
expect(res.code).toBe(1000);
expect(mockService.updateConfig).toHaveBeenCalledWith({
apiKey: 'sk-new-key-abcdef123456',
model: 'deepseek-reasoner',
});
});
it('3. POST /test-connection 连通性测试', async () => {
const res: any = await controller.testConnection('sk-test-pass');
expect(res.code).toBe(1000);
expect(res.data.success).toBe(true);
expect(res.data.latencyMs).toBe(120);
expect(mockService.testConnection).toHaveBeenCalledWith('sk-test-pass');
});
it('4. POST /chat 调试对话调用', async () => {
const res: any = await controller.chat({
messages: [{ role: 'user', content: '测试' }],
});
expect(res.code).toBe(1000);
expect(res.data.reply).toBe('你好!我是 DeepSeek 智能模型。');
expect(mockService.chatCompletion).toHaveBeenCalledWith({
messages: [{ role: 'user', content: '测试' }],
});
});
});

查看文件

@@ -160,6 +160,24 @@ describe('T36: 名片 AI 顾问对话 (modules/ai chat)', () => {
expect(result.shouldCollectLead).toBe(true);
expect(/[\u4e00-\u9fff]/.test(result.reply)).toBe(true);
});
it('当 DeepSeekService 已配置时优先调用大模型,失败时平滑降级', async () => {
const mockDeepSeek = {
isConfigured: jest.fn().mockReturnValue(true),
chatCompletion: jest.fn().mockResolvedValue('这是 DeepSeek 大模型生成的拟人顾问回复'),
};
service.deepSeekService = mockDeepSeek as any;
const res = await service.ask(userAsk('你们都有什么服务?'));
expect(res.reply).toBe('这是 DeepSeek 大模型生成的拟人顾问回复');
expect(mockDeepSeek.chatCompletion).toHaveBeenCalled();
// 测试异常平滑降级
mockDeepSeek.chatCompletion.mockRejectedValue(new Error('Network error'));
const fallbackRes = await service.ask(userAsk('你好', { locale: 'zh-CN' }));
expect(fallbackRes.reply).toBeTruthy();
expect(fallbackRes.reply).toContain('智能顾问');
});
});
describe('3. OpenAiChatController /open/ai/chat', () => {

查看文件

@@ -0,0 +1,233 @@
import { DeepSeekService } from '../../../src/modules/ai/service/deepseek';
import { DeepSeekConfig } from '../../../src/comm/types/omni';
describe('T31 DeepSeek 大模型集成与适配服务 (deepseek.ts)', () => {
let service: DeepSeekService;
beforeEach(() => {
// 先清理环境变量,防止跨测试用例污染
delete process.env.DEEPSEEK_API_KEY;
delete process.env.DEEPSEEK_BASE_URL;
delete process.env.DEEPSEEK_MODEL;
service = new DeepSeekService();
});
describe('1. 配置管理与状态检测 (Config & Readiness)', () => {
it('1.1 初始状态默认配置验证', () => {
const config = service.getConfig();
expect(config.baseUrl).toBe('https://api.deepseek.com');
expect(config.model).toBe('deepseek-chat');
expect(config.temperature).toBe(0.7);
expect(config.timeoutMs).toBe(30000);
expect(service.isConfigured()).toBe(false);
});
it('1.2 优先从环境变量读取 DEEPSEEK_API_KEY', () => {
process.env.DEEPSEEK_API_KEY = 'sk-env-test-key-123';
process.env.DEEPSEEK_BASE_URL = 'https://custom.deepseek.com';
process.env.DEEPSEEK_MODEL = 'deepseek-reasoner';
const envService = new DeepSeekService();
expect(envService.isConfigured()).toBe(true);
expect(envService.getConfig().apiKey).toBe('sk-env-test-key-123');
expect(envService.getConfig().baseUrl).toBe('https://custom.deepseek.com');
expect(envService.getConfig().model).toBe('deepseek-reasoner');
});
it('1.3 updateConfig 动态更新与合并配置', () => {
service.updateConfig({
apiKey: 'sk-dynamic-key-456',
model: 'deepseek-reasoner',
temperature: 0.2,
});
expect(service.isConfigured()).toBe(true);
const cfg = service.getConfig();
expect(cfg.apiKey).toBe('sk-dynamic-key-456');
expect(cfg.model).toBe('deepseek-reasoner');
expect(cfg.temperature).toBe(0.2);
expect(cfg.baseUrl).toBe('https://api.deepseek.com'); // 保留默认值
});
});
describe('2. 提示词动态拼装 (Prompt Builders)', () => {
it('2.1 buildSystemPrompt 组装名片主、人设与知识库切片', () => {
const card = {
name: '张总',
company: '华强出海科技',
title: '海外商务总监',
};
const concierge = {
personaType: 'trade_expert' as const,
toneStyle: 'professional' as const,
promptTemplate: '你是一名资深出海贸易专家,精通中东与东南亚市场。',
};
const chunks = [
{ index: 1, content: '支持信用证结算与 FOB 迪拜港条款。' },
{ index: 2, content: '最小起订量 (MOQ) 为 500 台。' },
];
const systemPrompt = service.buildSystemPrompt(card, concierge, chunks);
expect(systemPrompt).toContain('张总');
expect(systemPrompt).toContain('华强出海科技');
expect(systemPrompt).toContain('海外商务总监');
expect(systemPrompt).toContain('你是一名资深出海贸易专家');
expect(systemPrompt).toContain('支持信用证结算与 FOB 迪拜港条款。');
expect(systemPrompt).toContain('最小起订量 (MOQ) 为 500 台。');
});
it('2.2 buildLocalizePrompt 组装多语言润色与术语库指令', () => {
const prompt = service.buildLocalizePrompt(
'欢迎参观我们位于迪拜的展位',
'en-US',
'professional',
['展位 -> Booth', '迪拜 -> Dubai']
);
expect(prompt).toContain('欢迎参观我们位于迪拜的展位');
expect(prompt).toContain('en-US');
expect(prompt).toContain('professional');
expect(prompt).toContain('Booth');
expect(prompt).toContain('Dubai');
});
it('2.3 buildCopilotPrompt 组装销售破冰首语生成指令', () => {
const lead = {
name: 'Ahmed',
company: 'Gulf Energy',
phone: '+971501234567',
demand: '储能集装箱报价',
};
const tracks = [
{ module: 'case_energy', duration: 180 },
{ module: 'price_table', duration: 90 },
];
const prompt = service.buildCopilotPrompt(lead, tracks);
expect(prompt).toContain('Ahmed');
expect(prompt).toContain('Gulf Energy');
expect(prompt).toContain('储能集装箱报价');
expect(prompt).toContain('case_energy');
});
});
describe('3. 文本补全调用与高可用降级 (chatCompletion & Fallback)', () => {
it('3.1 未配置 API Key 时,提供 fallbackReply 则安全返回兜底内容', async () => {
expect(service.isConfigured()).toBe(false);
const result = await service.chatCompletion(
{
messages: [{ role: 'user', content: '你们的产品价格是多少?' }],
},
'这是本地规则引擎的兜底回复'
);
expect(result).toBe('这是本地规则引擎的兜底回复');
});
it('3.2 未配置 API Key 且未提供 fallbackReply 时安全降级或抛出清晰提示', async () => {
await expect(
service.chatCompletion({
messages: [{ role: 'user', content: '你好' }],
})
).rejects.toThrow('DeepSeek API Key 未配置');
});
it('3.3 模拟正常调用 DeepSeek 官方接口返回生成文本', async () => {
service.updateConfig({ apiKey: 'sk-mock-valid-key' });
// 模拟底层 HTTP client
const mockPost = jest.fn().mockResolvedValue({
status: 200,
data: {
id: 'chatcmpl-test-123',
model: 'deepseek-chat',
choices: [
{
index: 0,
message: {
role: 'assistant',
content: '您好!我是华强科技的 AI 顾问,很高兴为您服务。',
},
finish_reason: 'stop',
},
],
},
});
(service as any).httpClient = { post: mockPost };
const reply = await service.chatCompletion({
messages: [{ role: 'user', content: '您好' }],
});
expect(reply).toBe('您好!我是华强科技的 AI 顾问,很高兴为您服务。');
expect(mockPost).toHaveBeenCalledWith(
'https://api.deepseek.com/chat/completions',
expect.objectContaining({
model: 'deepseek-chat',
messages: [{ role: 'user', content: '您好' }],
}),
expect.objectContaining({
headers: expect.objectContaining({
Authorization: 'Bearer sk-mock-valid-key',
}),
})
);
});
it('3.4 API 调用异常且提供 fallbackReply 时平滑降级', async () => {
service.updateConfig({ apiKey: 'sk-mock-valid-key' });
const mockPost = jest.fn().mockRejectedValue(new Error('Network Timeout'));
(service as any).httpClient = { post: mockPost };
const reply = await service.chatCompletion(
{ messages: [{ role: 'user', content: '报价' }] },
'网络异常时的平滑降级回复'
);
expect(reply).toBe('网络异常时的平滑降级回复');
});
});
describe('4. 连通性测试 (testConnection)', () => {
it('4.1 未配置 API Key 且未传入临时 Key 时测试失败', async () => {
const res = await service.testConnection();
expect(res.success).toBe(false);
expect(res.message).toContain('API Key 未提供');
});
it('4.2 传入测试 Key 成功握手并统计延迟', async () => {
const mockPost = jest.fn().mockResolvedValue({
status: 200,
data: {
choices: [
{ message: { content: 'pong' }, finish_reason: 'stop' },
],
},
});
(service as any).httpClient = { post: mockPost };
const res = await service.testConnection('sk-test-tmp-key');
expect(res.success).toBe(true);
expect(typeof res.latencyMs).toBe('number');
expect(res.latencyMs).toBeGreaterThanOrEqual(0);
expect(res.model).toBe('deepseek-chat');
});
it('4.3 官方 API 返回 401 鉴权失败捕获', async () => {
const mockPost = jest.fn().mockRejectedValue({
response: {
status: 401,
data: { error: { message: 'Authentication Fails (invalid api key)' } },
},
});
(service as any).httpClient = { post: mockPost };
const res = await service.testConnection('sk-invalid-key');
expect(res.success).toBe(false);
expect(res.message).toContain('Authentication Fails');
});
});
});