文件
OmniCard/backend/src/modules/ai/service/deepseek.ts

402 行
12 KiB
TypeScript

此文件含有模棱两可的 Unicode 字符

此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。

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;
/** 多租户私有 BYOK 配置映射表 */
private tenantConfigs: Map<string | number, DeepSeekConfig> = new Map();
/** 租户 Token 累计消耗账本 */
private tenantUsage: Map<
string | number,
{ totalTokens: number; promptTokens: number; completionTokens: number; callCount: number }
> = new Map();
/** 租户 Token 配额上限表 */
private tenantQuotas: Map<string | number, number> = new Map();
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 };
}
/**
* 判断指定租户是否已配置专属私有 Key
*/
hasTenantConfig(tenantId: string | number): boolean {
return this.tenantConfigs.has(tenantId);
}
/**
* 获取指定租户有效配置(未单独配置则优雅回退至平台公共默认配置)
*/
getTenantConfig(tenantId?: string | number): DeepSeekConfig {
if (tenantId !== undefined && tenantId !== null && this.tenantConfigs.has(tenantId)) {
return { ...this.tenantConfigs.get(tenantId)! };
}
return this.getConfig();
}
/**
* 为指定企业租户更新专属定制配置 (BYOK)
*/
updateTenantConfig(tenantId: string | number, partial: Partial<DeepSeekConfig>): void {
const current = this.getTenantConfig(tenantId);
const updated: DeepSeekConfig = {
...current,
...partial,
enabled: partial.apiKey ? Boolean(partial.apiKey.trim()) : current.enabled,
};
this.tenantConfigs.set(tenantId, updated);
}
/**
* 记录指定租户的 Token 消耗
*/
recordTokenUsage(
tenantId: string | number,
tokens:
| number
| { promptTokens?: number; completionTokens?: number; totalTokens?: number }
): void {
const current = this.tenantUsage.get(tenantId) || {
totalTokens: 0,
promptTokens: 0,
completionTokens: 0,
callCount: 0,
};
let prompt = 0;
let completion = 0;
let total = 0;
if (typeof tokens === 'number') {
total = Number(tokens) || 0;
} else if (tokens && typeof tokens === 'object') {
prompt = Number(tokens.promptTokens) || 0;
completion = Number(tokens.completionTokens) || 0;
total = Number(tokens.totalTokens) || prompt + completion;
}
this.tenantUsage.set(tenantId, {
totalTokens: current.totalTokens + total,
promptTokens: current.promptTokens + prompt,
completionTokens: current.completionTokens + completion,
callCount: current.callCount + 1,
});
}
/**
* 获取指定租户的 Token 用量统计
*/
getTenantTokenUsage(tenantId: string | number): {
totalTokens: number;
promptTokens: number;
completionTokens: number;
callCount: number;
} {
return this.tenantUsage.get(tenantId) || {
totalTokens: 0,
promptTokens: 0,
completionTokens: 0,
callCount: 0,
};
}
/**
* 设置指定租户的 Token 最大额度上限
*/
setTenantTokenQuota(tenantId: string | number, maxTotalTokens: number): void {
this.tenantQuotas.set(tenantId, Math.max(0, Number(maxTotalTokens) || 0));
}
/**
* 获取指定租户的 Token 最大额度上限
*/
getTenantTokenQuota(tenantId: string | number): number {
return this.tenantQuotas.get(tenantId) || 0;
}
/**
* 断言当前租户 Token 配额充足,超限则抛出 429 阻断
*/
assertTenantTokenQuota(tenantId: string | number, estimatedTokens = 1): void {
const quota = this.tenantQuotas.get(tenantId);
if (quota === undefined) {
return;
}
const current = this.getTenantTokenUsage(tenantId).totalTokens;
if (current + estimatedTokens > quota) {
throw new CoolCommException(
`当前租户 AI Token 配额已耗尽 (${current}/${quota}),请升级套餐或补充额度`,
429
);
}
}
/**
* 动态更新配置
*/
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(tenantId?: number | string): boolean {
const cfg = this.getTenantConfig(tenantId);
return Boolean(cfg.apiKey && cfg.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('你是微官网的在线智能商务顾问。');
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');
}
/**
* 对话补全调用 (支持多租户 BYOK、Token 计量与优雅降级)
*/
async chatCompletion(
req: DeepSeekChatCompletionRequest,
fallbackReply?: string,
tenantId?: number | string
): Promise<string> {
const cfg = this.getTenantConfig(tenantId);
const apiKey = cfg.apiKey?.trim();
if (!apiKey) {
if (fallbackReply !== undefined) {
return fallbackReply;
}
throw new CoolCommException('DeepSeek API Key 未配置');
}
// 租户 Token 配额校验 (预估输入字符数 / 2)
const estimatedTokens = Math.ceil(
(req.messages || []).reduce((acc, m) => acc + (m.content || '').length, 0) / 2
);
this.assertTenantTokenQuota(tenantId, estimatedTokens);
const endpoint = `${cfg.baseUrl || 'https://api.deepseek.com'}/chat/completions`;
const payload = {
model: req.model || cfg.model || 'deepseek-chat',
messages: req.messages,
temperature: req.temperature ?? cfg.temperature ?? 0.7,
max_tokens: req.maxTokens ?? cfg.maxTokens ?? 2048,
stream: false,
};
try {
const response = await this.httpClient.post<DeepSeekChatCompletionResponse>(
endpoint,
payload,
{
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${apiKey}`,
},
}
);
// 记录实际 Token 消耗到租户账本
if (response.data?.usage?.total_tokens) {
this.recordTokenUsage(tenantId, response.data.usage.total_tokens);
}
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,
tenantId?: number | string
): Promise<DeepSeekTestConnectionResult> {
const cfg = this.getTenantConfig(tenantId);
const key = (testApiKey || cfg.apiKey || '').trim();
if (!key) {
return {
success: false,
message: 'API Key 未提供,无法测试连通性',
};
}
const startTime = Date.now();
const endpoint = `${cfg.baseUrl || 'https://api.deepseek.com'}/chat/completions`;
const model = cfg.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}`,
};
}
}
}