Files
dukang/server/dukang-api/src/integrations/wecom/wecom-bot-ai.service.ts
T
2026-08-04 21:38:49 +08:00

150 lines
6.6 KiB
TypeScript

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 <BUG|REQUIREMENT|OPTIMIZATION> <描述>',
'- dev_plan_task_update_status <任务编号> <TODO|DEVELOPED|RELEASED>',
'- dev_plan_version_create <版本号>',
'- dev_plan_version_update_status <版本号> <PENDING|IN_PROGRESS|TESTING|RELEASED>',
'- 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<string | null> {
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(', ')}`;
}
}