import { Injectable, Logger } from '@nestjs/common'; import { PrismaService } from '../../common/prisma/prisma.module'; import { LlmChatClient } from '../llm/llm-chat.client'; import { KnowledgeRetrievalService } from '../llm/knowledge-retrieval.service'; import { WecomBotActionsService } from './wecom-bot-actions.service'; import { parseWecomToolLine, sanitizeWecomUserReply } from './wecom-bot-reply.util'; import type { WecomBotRuntimeConfig } from './wecom-bot.types'; import { wecomBotHasPermission } from './wecom-bot.types'; const DEFAULT_SYSTEM = [ '你是杜康好客企业内部助手。', '优先依据提供的知识库内容回答;知识库未覆盖时如实说明不确定。', '不要编造订单号、金额、权限;涉及写操作请引导用户使用指令(如「帮助」)。', '回答简洁,使用中文。', ].join(''); const TOOL_INSTRUCTION = [ '当用户需要查询业务数据时,你必须在回复的第一行输出工具指令(仅一行,用户不可见后续会过滤):', '格式:`TOOL <工具名> [参数]`', '可用工具(按权限):', '- order_read <订单号>', '- delivery_read <单号>', '- store_read <关键词> · redeem_read <核销单号或门店>', '- user_read <用户号>', '- support_tickets_open · support_ticket_read [工单号]', '- support_ticket_approve <工单号> · support_ticket_reject <工单号> <理由>', '- finance_store_bill [门店] · finance_partner_bill · finance_winery_bill · finance_logistics_bill', '- finance_payout · finance_withdrawal', '- dev_plan_tasks [状态] · dev_plan_versions [版本号]', '- dev_plan_task_create <描述>', '- dev_plan_task_update_status <任务编号> ', '- dev_plan_version_create <版本号>', '- dev_plan_version_update_status <版本号> ', '- dev_plan_version_link_tasks <版本号> <任务编号1,任务编号2>', '- server_logs [关键词] · handbook_read [关键词]', '禁止输出 JSON、api 字段、「请稍等正在检索」等占位话术。', '若仅需解释概念、无需查库,第一行写 `ANSWER` 后直接回答。', ].join('\n'); @Injectable() export class WecomBotAiService { private readonly logger = new Logger(WecomBotAiService.name); constructor( private readonly prisma: PrismaService, private readonly llm: LlmChatClient, private readonly kb: KnowledgeRetrievalService, private readonly actions: WecomBotActionsService, ) {} async replyIfConfigured( bot: WecomBotRuntimeConfig, userText: string, wecomUserId: string, ): Promise { if (!bot.aiEnabled || !bot.llmConfigId) return null; const cfg = await this.prisma.llmApiConfig.findUnique({ where: { id: BigInt(bot.llmConfigId) }, }); if (!cfg?.enabled) { return '已开启 AI,但绑定的语言模型未启用或已删除。请在 HQ「企微机器人」检查配置。'; } let kbBlock = ''; if (bot.knowledgeBaseId) { try { kbBlock = await this.kb.buildContext(BigInt(bot.knowledgeBaseId), userText); } catch (e) { this.logger.warn(`kb retrieve failed: ${String(e)}`); } } const systemParts = [ cfg.systemPrompt?.trim() || DEFAULT_SYSTEM, this.buildToolHint(bot), TOOL_INSTRUCTION, kbBlock ? `\n\n以下为知识库检索片段:\n${kbBlock}` : '', ]; try { this.logger.log( `wecom ai request bot=${bot.key} user=${wecomUserId} text=${userText.slice(0, 80)}`, ); const raw = await this.llm.chat({ baseUrl: cfg.baseUrl, apiKey: cfg.apiKey, model: cfg.modelName, temperature: cfg.temperature != null ? Number(cfg.temperature) : 0.3, maxTokens: cfg.maxTokens ?? 1024, messages: [ { role: 'system', content: systemParts.join('') }, { role: 'user', content: userText }, ], }); const tool = parseWecomToolLine(raw); if (tool) { this.logger.log( `wecom ai tool=${tool.name} bot=${bot.key} user=${wecomUserId} args=${tool.args.slice(0, 80)}`, ); try { const result = await this.actions.runTool(bot, wecomUserId, tool.name, tool.args); return result; } catch (e) { const msg = e instanceof Error ? e.message : String(e); this.logger.warn(`wecom ai tool failed ${tool.name}: ${msg}`); return `查询失败:${msg}`; } } const answerMatch = raw.match(/^ANSWER\s+([\s\S]*)/i); if (answerMatch) { return sanitizeWecomUserReply(answerMatch[1]); } return sanitizeWecomUserReply(raw); } catch (e) { const msg = e instanceof Error ? e.message : String(e); this.logger.error(`wecom ai reply failed: ${msg}`); return `AI 回复失败:${msg}`; } } private buildToolHint(bot: WecomBotRuntimeConfig): string { const tools: string[] = []; if (wecomBotHasPermission(bot, 'order.read')) tools.push('order_read'); if (wecomBotHasPermission(bot, 'delivery.read')) tools.push('delivery_read'); if (wecomBotHasPermission(bot, 'store.read')) tools.push('store_read'); if (wecomBotHasPermission(bot, 'redeem.read')) tools.push('redeem_read'); if (wecomBotHasPermission(bot, 'user.read')) tools.push('user_read'); if (wecomBotHasPermission(bot, 'support_ticket.read')) { tools.push('support_tickets_open', 'support_ticket_read'); } if (wecomBotHasPermission(bot, 'finance.store_bill.read')) tools.push('finance_store_bill'); if (wecomBotHasPermission(bot, 'finance.partner_bill.read')) tools.push('finance_partner_bill'); if (wecomBotHasPermission(bot, 'finance.winery_bill.read')) tools.push('finance_winery_bill'); if (wecomBotHasPermission(bot, 'finance.logistics_bill.read')) tools.push('finance_logistics_bill'); if (wecomBotHasPermission(bot, 'finance.payout.read')) tools.push('finance_payout'); if (wecomBotHasPermission(bot, 'finance.withdrawal.read')) tools.push('finance_withdrawal'); if (wecomBotHasPermission(bot, 'dev_plan.task.read')) tools.push('dev_plan_tasks'); if (wecomBotHasPermission(bot, 'dev_plan.version.read')) tools.push('dev_plan_versions'); if (wecomBotHasPermission(bot, 'dev_plan.version.link_tasks')) tools.push('dev_plan_version_link_tasks'); if (wecomBotHasPermission(bot, 'server_log.read')) tools.push('server_logs'); if (wecomBotHasPermission(bot, 'handbook.read')) tools.push('handbook_read'); if (!tools.length) return ''; return `\n\n当前机器人可用工具:${tools.join(', ')}`; } }