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

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

查看文件

@@ -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');
});
});
});