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

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

查看文件

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