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 = new Map(); /** 租户 Token 累计消耗账本 */ private tenantUsage: Map< string | number, { totalTokens: number; promptTokens: number; completionTokens: number; callCount: number } > = new Map(); /** 租户 Token 配额上限表 */ private tenantQuotas: Map = 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): 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): 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 { 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( 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 { 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}`, }; } } }