import { Inject, Injectable } from '@nestjs/common'; import { SUPPORT_TICKET_STATUS_LABELS, SUPPORT_TICKET_TYPE_LABELS, TICKET_TYPE_LABELS, WECOM_BOT_PERMISSION_LABELS, mapSupportTicketTypeToDevPlanTask, type AfterSaleTicketType, type SupportTicketTypeDto, type WecomBotPermission, } from '@dukang/shared-types'; import { PrismaService } from '../../common/prisma/prisma.module'; import { TicketService } from '../../modules/common/ticket.service'; import { SupportTicketService } from '../../modules/common/support-ticket.service'; import { DevPlanService } from '../../modules/dev-plan/dev-plan.service'; import { SettlementService } from '../../modules/settlement/settlement.service'; import { SMS_PROVIDER } from '../integrations.constants'; import type { ISmsProvider } from '../sms/sms.interface'; import type { WecomBotRuntimeConfig } from './wecom-bot.types'; import { wecomBotCanReview, wecomBotHasPermission } from './wecom-bot.types'; import { WecomBotSessionService } from './wecom-bot-session.service'; import { WecomBotAuditService } from './wecom-bot-audit.service'; import { searchHandbook } from './wecom-handbook'; const SMS_SCENE = 'WECOM_USER_VIEW'; const PHONE_RE = /^1\d{10}$/; const TICKET_TYPE_ALIASES: Record = { 仅退款: 'REFUND', 退款: 'REFUND', refund: 'REFUND', 破损补发: 'RESHIPMENT', 补发: 'RESHIPMENT', reshipment: 'RESHIPMENT', 破损退货: 'DAMAGE_RETURN', 退货: 'DAMAGE_RETURN', damage_return: 'DAMAGE_RETURN', 退货退款: 'RETURN_REFUND', return_refund: 'RETURN_REFUND', }; const SUPPORT_TYPE_ALIASES: Record = { bug: 'BUG', BUG: 'BUG', 缺陷: 'BUG', 建议: 'SUGGESTION', suggestion: 'SUGGESTION', 其他: 'OTHER', other: 'OTHER', }; @Injectable() export class WecomBotCapabilityService { constructor( private readonly prisma: PrismaService, private readonly ticketService: TicketService, private readonly supportTicketService: SupportTicketService, private readonly devPlan: DevPlanService, private readonly settlement: SettlementService, private readonly session: WecomBotSessionService, private readonly audit: WecomBotAuditService, @Inject(SMS_PROVIDER) private readonly sms: ISmsProvider, ) {} buildHelp(bot: WecomBotRuntimeConfig): string { const lines = [`**${bot.name}**`, '', '通用:`帮助` · `状态`', '']; if (wecomBotHasPermission(bot, 'ticket.create')) { lines.push( '**售后工单**', '`工单 <订单号> <类型> [备注]`', '类型:仅退款 / 破损补发 / 破损退货 / 退货退款', '', ); } if (wecomBotHasPermission(bot, 'user.read_sms')) { lines.push( '**查用户(需短信验证)**', '`查用户 <手机号>` → 发验证码 · `验证 <验证码>`', '', ); } if (wecomBotHasPermission(bot, 'user.read')) { lines.push('**查用户**', '`用户号 <用户号>`', ''); } if (wecomBotHasPermission(bot, 'order.read')) { lines.push('**订单**', '`查订单 <订单号>` · `订单 <订单号>`', ''); } if (wecomBotHasPermission(bot, 'delivery.read')) { lines.push('**配送**', '`快递 <订单号|运单号>`', ''); } if (wecomBotHasPermission(bot, 'store.read')) { lines.push('**门店**', '`查门店 <关键词>`', ''); } if (wecomBotHasPermission(bot, 'redeem.read')) { lines.push('**核销**', '`核销 <订单号|门店名>`', ''); } if (wecomBotHasPermission(bot, 'ticket.read')) { lines.push('**售后工单查询**', '`售后工单 <工单号>`', ''); } if (wecomBotHasPermission(bot, 'finance.store_bill.read')) { lines.push('**财务**', '`门店账单 [门店名]` · `合伙人账单` · `酒厂账单` · `物流账单`', ''); lines.push('', '`门店打款` · `门店提现`', ''); } if (wecomBotHasPermission(bot, 'support_ticket.create')) { lines.push('**技术支持提单**', '`提单 <标题> [| 详情]`', ''); } if (wecomBotHasPermission(bot, 'support_ticket.read')) { lines.push( '**技术支持工单**', '`工单 ST-001` · `未完成技术支持工单` · `开发任务` · `版本 v3.4.11`', '', ); } if ( wecomBotHasPermission(bot, 'dev_plan.task.create') || wecomBotHasPermission(bot, 'dev_plan.task.update_status') ) { const cmds: string[] = []; if (wecomBotHasPermission(bot, 'dev_plan.task.create')) cmds.push('`新建任务 BUG|需求|优化 <描述>`'); if (wecomBotHasPermission(bot, 'dev_plan.task.update_status')) cmds.push('`修改任务 <编号> TODO|DEVELOPED|RELEASED`'); lines.push('**开发任务管理**', cmds.join(' · '), ''); } if ( wecomBotHasPermission(bot, 'dev_plan.version.create') || wecomBotHasPermission(bot, 'dev_plan.version.update_status') || wecomBotHasPermission(bot, 'dev_plan.version.link_tasks') ) { const cmds: string[] = []; if (wecomBotHasPermission(bot, 'dev_plan.version.create')) cmds.push('`新建版本 <版本号>`'); if (wecomBotHasPermission(bot, 'dev_plan.version.update_status')) cmds.push('`修改版本 <版本号> <状态>`'); if (wecomBotHasPermission(bot, 'dev_plan.version.link_tasks')) cmds.push('`关联任务 <版本号> <任务1,任务2>`'); lines.push('**开发版本管理**', cmds.join(' · '), ''); } if ( wecomBotHasPermission(bot, 'support_ticket.review') || wecomBotHasPermission(bot, 'support_ticket.approve') || wecomBotHasPermission(bot, 'support_ticket.reject') ) { lines.push( '**审批(SuperAdmin 白名单)**', '`审批 驳回 <工单号> 理由:…` · `驳回 <工单号> 理由…` · `审批 通过 <工单号>`', wecomBotHasPermission(bot, 'support_ticket.approve') ? '(可审批通过)' : '', wecomBotHasPermission(bot, 'support_ticket.reject') ? '(可驳回)' : '', '', ); } if (wecomBotHasPermission(bot, 'handbook.read')) { lines.push('**手册**', '`手册 [关键词]`', ''); } if (wecomBotHasPermission(bot, 'server_log.read')) { lines.push('**日志**', '`日志 [关键词]` · `三方日志 [provider]`', ''); } if (bot.aiEnabled && bot.llmConfigId) { lines.push('**智能问答**:未匹配指令时由 AI 理解意图并查询(结果不含 JSON)', ''); } lines.push(`当前权限:${bot.permissions.map((p) => WECOM_BOT_PERMISSION_LABELS[p] || p).join('、') || '无'}`); return lines.join('\n'); } async dispatch( bot: WecomBotRuntimeConfig, wecomUserId: string, text: string, opts?: { skipNaturalFallback?: boolean }, ): Promise { const content = text.trim(); if (!content) return this.buildHelp(bot); const reviewCmd = parseReviewCommand(content); if (reviewCmd) { if (reviewCmd.decision === 'APPROVE') { return this.audit.run( { bot, wecomUserId, action: 'support_ticket.review.approve', permission: 'support_ticket.approve', inputSummary: reviewCmd.ticketNo, }, () => this.approveSupportTicket(bot, wecomUserId, reviewCmd.ticketNo), ); } return this.audit.run( { bot, wecomUserId, action: 'support_ticket.review.reject', permission: 'support_ticket.reject', inputSummary: `${reviewCmd.ticketNo} ${reviewCmd.reason}`.trim(), }, () => this.rejectSupportTicket(bot, wecomUserId, reviewCmd.ticketNo, reviewCmd.reason), ); } if ( (wecomBotHasPermission(bot, 'support_ticket.review') || wecomBotHasPermission(bot, 'support_ticket.approve') || wecomBotHasPermission(bot, 'support_ticket.reject')) && /审批/.test(content) && /(通过|驳回|approve|reject)/i.test(content) ) { return '审批指令格式:`审批 驳回 <工单号> 理由:…` 或 `驳回 <工单号> 理由…`'; } if (/^(查用户|用户)\s+/i.test(content) && !/^用户号/i.test(content)) { const phone = content.replace(/^(查用户|用户)\s+/i, '').trim(); return this.audit.run( { bot, wecomUserId, action: 'user.read_sms.start', permission: 'user.read_sms', inputSummary: maskPhone(phone) }, () => this.startUserView(bot, wecomUserId, phone), ); } if (/^(验证|verify)\s+/i.test(content)) { const code = content.replace(/^(验证|verify)\s+/i, '').trim(); return this.audit.run( { bot, wecomUserId, action: 'user.read_sms.verify', permission: 'user.read_sms' }, () => this.verifyUserView(bot, wecomUserId, code), ); } if (/^(快递|配送|物流)\s+/i.test(content)) { const q = content.replace(/^(快递|配送|物流)\s+/i, '').trim(); return this.audit.run( { bot, wecomUserId, action: 'delivery.read', permission: 'delivery.read', inputSummary: q }, () => this.lookupDelivery(bot, q), ); } if (/^(工单|创建工单)\s+/i.test(content)) { const rest = content.replace(/^(工单|创建工单)\s+/i, '').trim(); if (/^ST/i.test(rest)) { return this.audit.run( { bot, wecomUserId, action: 'support_ticket.read', permission: 'support_ticket.read', inputSummary: rest }, () => this.supportTicketDetail(bot, rest), ); } return this.audit.run( { bot, wecomUserId, action: 'ticket.create', permission: 'ticket.create', inputSummary: rest.slice(0, 120) }, () => this.createAfterSaleTicket(bot, rest, wecomUserId), ); } if (/^(提单|技术支持)\s+/i.test(content)) { const rest = content.replace(/^(提单|技术支持)\s+/i, '').trim(); return this.audit.run( { bot, wecomUserId, action: 'support_ticket.create', permission: 'support_ticket.create', inputSummary: rest.slice(0, 120) }, () => this.createSupportTicket(bot, rest, wecomUserId), ); } if (/^(进度|开发进度|技术支持工单|未完成技术支持工单)/i.test(content)) { const rest = content.replace(/^(进度|开发进度|技术支持工单|未完成技术支持工单)\s*/i, '').trim(); if (!rest && /未完成/.test(content)) { return this.audit.run( { bot, wecomUserId, action: 'support_ticket.read.open', permission: 'support_ticket.read' }, () => this.listOpenSupportTickets(bot), ); } return this.audit.run( { bot, wecomUserId, action: 'support_ticket.read', permission: 'support_ticket.read', inputSummary: rest || undefined }, () => this.supportProgress(bot, rest), ); } if (/^(手册|帮助文档|文档)/i.test(content)) { const q = content.replace(/^(手册|帮助文档|文档)\s*/i, '').trim(); return this.audit.run( { bot, wecomUserId, action: 'handbook.read', permission: 'handbook.read', inputSummary: q || undefined }, () => this.queryHandbook(bot, q), ); } if (/^(日志|错误日志|服务端日志)/i.test(content)) { const q = content.replace(/^(日志|错误日志|服务端日志)\s*/i, '').trim(); return this.audit.run( { bot, wecomUserId, action: 'server_log.read', permission: 'server_log.read', inputSummary: q || undefined }, () => this.queryServerLogs(bot, q), ); } if (/^(三方日志|第三方日志)/i.test(content)) { const q = content.replace(/^(三方日志|第三方日志)\s*/i, '').trim(); return this.audit.run( { bot, wecomUserId, action: 'server_log.read.third_party', permission: 'server_log.read', inputSummary: q || undefined }, () => this.queryThirdPartyLogs(bot, q), ); } if (/^(查订单|订单查询|订单)\s+/i.test(content)) { const orderNo = content.replace(/^(查订单|订单查询|订单)\s+/i, '').trim(); return this.audit.run( { bot, wecomUserId, action: 'order.read', permission: 'order.read', inputSummary: orderNo }, () => this.queryOrder(bot, orderNo), ); } if (/^(用户号|查用户号)\s+/i.test(content)) { const userNo = content.replace(/^(用户号|查用户号)\s+/i, '').trim(); return this.audit.run( { bot, wecomUserId, action: 'user.read', permission: 'user.read', inputSummary: userNo }, () => this.queryUserByNo(bot, userNo), ); } if (/^(查门店|门店)\s+/i.test(content)) { const q = content.replace(/^(查门店|门店)\s+/i, '').trim(); return this.audit.run( { bot, wecomUserId, action: 'store.read', permission: 'store.read', inputSummary: q }, () => this.queryStore(bot, q), ); } if (/^(核销|核销记录)\s+/i.test(content)) { const q = content.replace(/^(核销|核销记录)\s+/i, '').trim(); return this.audit.run( { bot, wecomUserId, action: 'redeem.read', permission: 'redeem.read', inputSummary: q }, () => this.queryRedeem(bot, q), ); } if (/^(售后工单)\s+/i.test(content)) { const q = content.replace(/^(售后工单)\s+/i, '').trim(); return this.audit.run( { bot, wecomUserId, action: 'ticket.read', permission: 'ticket.read', inputSummary: q }, () => this.queryAfterSaleTicket(bot, q), ); } if (/^(门店账单|账单)\s*/i.test(content)) { const q = content.replace(/^(门店账单|账单)\s*/i, '').trim(); return this.audit.run( { bot, wecomUserId, action: 'finance.store_bill.read', permission: 'finance.store_bill.read', inputSummary: q || undefined }, () => this.queryStoreBills(bot, q), ); } if (/^合伙人账单/i.test(content)) { return this.audit.run( { bot, wecomUserId, action: 'finance.partner_bill.read', permission: 'finance.partner_bill.read' }, () => this.queryPartnerBills(bot), ); } if (/^酒厂账单/i.test(content)) { return this.audit.run( { bot, wecomUserId, action: 'finance.winery_bill.read', permission: 'finance.winery_bill.read' }, () => this.queryWineryBills(bot), ); } if (/^物流账单/i.test(content)) { return this.audit.run( { bot, wecomUserId, action: 'finance.logistics_bill.read', permission: 'finance.logistics_bill.read' }, () => this.queryLogisticsBills(bot), ); } if (/^门店打款/i.test(content)) { return this.audit.run( { bot, wecomUserId, action: 'finance.payout.read', permission: 'finance.payout.read' }, () => this.queryStorePayouts(bot), ); } if (/^门店提现/i.test(content)) { return this.audit.run( { bot, wecomUserId, action: 'finance.withdrawal.read', permission: 'finance.withdrawal.read' }, () => this.queryStoreWithdrawals(bot), ); } if (/^(开发任务|任务池)/i.test(content)) { const status = content.replace(/^(开发任务|任务池)\s*/i, '').trim(); return this.audit.run( { bot, wecomUserId, action: 'dev_plan.task.read', permission: 'dev_plan.task.read', inputSummary: status || undefined }, () => this.queryDevPlanTasks(bot, status), ); } if (/^(开发版本|版本)/i.test(content)) { const versionNo = content.replace(/^(开发版本|版本池|版本)\s*/i, '').trim(); return this.audit.run( { bot, wecomUserId, action: 'dev_plan.version.read', permission: 'dev_plan.version.read', inputSummary: versionNo || undefined }, () => this.queryDevPlanVersions(bot, versionNo), ); } if (/^(新建任务|创建任务)\s+/i.test(content)) { const rest = content.replace(/^(新建任务|创建任务)\s+/i, '').trim(); return this.audit.run( { bot, wecomUserId, action: 'dev_plan.task.create', permission: 'dev_plan.task.create', inputSummary: rest.slice(0, 120) }, () => this.createDevPlanTask(bot, wecomUserId, rest), ); } if (/^(任务状态|修改任务)\s+/i.test(content)) { const rest = content.replace(/^(任务状态|修改任务)\s+/i, '').trim(); return this.audit.run( { bot, wecomUserId, action: 'dev_plan.task.update_status', permission: 'dev_plan.task.update_status', inputSummary: rest.slice(0, 120) }, () => this.updateDevPlanTaskStatus(bot, wecomUserId, rest), ); } if (/^(新建版本|创建版本)\s+/i.test(content)) { const rest = content.replace(/^(新建版本|创建版本)\s+/i, '').trim(); return this.audit.run( { bot, wecomUserId, action: 'dev_plan.version.create', permission: 'dev_plan.version.create', inputSummary: rest.slice(0, 120) }, () => this.createDevPlanVersion(bot, wecomUserId, rest), ); } if (/^(版本状态|修改版本)\s+/i.test(content)) { const rest = content.replace(/^(版本状态|修改版本)\s+/i, '').trim(); return this.audit.run( { bot, wecomUserId, action: 'dev_plan.version.update_status', permission: 'dev_plan.version.update_status', inputSummary: rest.slice(0, 120) }, () => this.updateDevPlanVersionStatus(bot, wecomUserId, rest), ); } if (/^(关联任务|版本关联|关联到版本)\s+/i.test(content)) { const rest = content.replace(/^(关联任务|版本关联|关联到版本)\s+/i, '').trim(); return this.audit.run( { bot, wecomUserId, action: 'dev_plan.version.link_tasks', permission: 'dev_plan.version.link_tasks', inputSummary: rest.slice(0, 120) }, () => this.linkTasksToVersion(bot, wecomUserId, rest), ); } const nlReply = await this.tryNaturalLanguageQuery(bot, wecomUserId, content); if (nlReply) return nlReply; if ( !opts?.skipNaturalFallback && wecomBotHasPermission(bot, 'handbook.read') && content.length >= 2 ) { const hit = searchHandbook(content, 1); if (hit.length) return formatHandbook(hit); } if (opts?.skipNaturalFallback) return null; return `未识别指令。\n\n${this.buildHelp(bot)}`; } async runTool( bot: WecomBotRuntimeConfig, wecomUserId: string, toolName: string, args: string, ): Promise { const name = toolName.toLowerCase().replace(/-/g, '_'); const ctx = { bot, wecomUserId, action: `tool:${name}`, inputSummary: args?.slice(0, 120) || undefined }; switch (name) { case 'support_tickets_open': return this.audit.run({ ...ctx, permission: 'support_ticket.read' }, () => this.listOpenSupportTickets(bot), ); case 'support_ticket_approve': return this.audit.run({ ...ctx, permission: 'support_ticket.approve' }, () => this.approveSupportTicket(bot, wecomUserId, args), ); case 'support_ticket_reject': { const [ticketNo, ...reasonParts] = (args ?? '').split(/\s+/).filter(Boolean); return this.audit.run({ ...ctx, permission: 'support_ticket.reject' }, () => this.rejectSupportTicket(bot, wecomUserId, ticketNo ?? '', reasonParts.join(' ')), ); } case 'support_ticket_read': case 'support_progress': return this.audit.run({ ...ctx, permission: 'support_ticket.read' }, () => this.supportProgress(bot, args), ); case 'order_read': case 'order_lookup': return this.audit.run({ ...ctx, permission: 'order.read' }, () => this.queryOrder(bot, args)); case 'user_read': case 'user_lookup': return this.audit.run({ ...ctx, permission: 'user.read' }, () => this.queryUserByNo(bot, args)); case 'store_read': case 'store_lookup': return this.audit.run({ ...ctx, permission: 'store.read' }, () => this.queryStore(bot, args)); case 'redeem_read': return this.audit.run({ ...ctx, permission: 'redeem.read' }, () => this.queryRedeem(bot, args)); case 'delivery_read': case 'delivery_lookup': return this.audit.run({ ...ctx, permission: 'delivery.read' }, () => this.lookupDelivery(bot, args)); case 'dev_plan_tasks': case 'dev_tasks': return this.audit.run({ ...ctx, permission: 'dev_plan.task.read' }, () => this.queryDevPlanTasks(bot, args), ); case 'dev_plan_versions': case 'dev_versions': return this.audit.run({ ...ctx, permission: 'dev_plan.version.read' }, () => this.queryDevPlanVersions(bot, args), ); case 'dev_plan_task_create': return this.audit.run({ ...ctx, permission: 'dev_plan.task.create' }, () => this.createDevPlanTask(bot, wecomUserId, args), ); case 'dev_plan_task_update_status': return this.audit.run({ ...ctx, permission: 'dev_plan.task.update_status' }, () => this.updateDevPlanTaskStatus(bot, wecomUserId, args), ); case 'dev_plan_version_create': return this.audit.run({ ...ctx, permission: 'dev_plan.version.create' }, () => this.createDevPlanVersion(bot, wecomUserId, args), ); case 'dev_plan_version_update_status': return this.audit.run({ ...ctx, permission: 'dev_plan.version.update_status' }, () => this.updateDevPlanVersionStatus(bot, wecomUserId, args), ); case 'dev_plan_version_link_tasks': return this.audit.run({ ...ctx, permission: 'dev_plan.version.link_tasks' }, () => this.linkTasksToVersion(bot, wecomUserId, args), ); case 'finance_store_bill': return this.audit.run({ ...ctx, permission: 'finance.store_bill.read' }, () => this.queryStoreBills(bot, args), ); case 'finance_partner_bill': return this.audit.run({ ...ctx, permission: 'finance.partner_bill.read' }, () => this.queryPartnerBills(bot), ); case 'finance_winery_bill': return this.audit.run({ ...ctx, permission: 'finance.winery_bill.read' }, () => this.queryWineryBills(bot), ); case 'finance_logistics_bill': return this.audit.run({ ...ctx, permission: 'finance.logistics_bill.read' }, () => this.queryLogisticsBills(bot), ); case 'finance_payout': return this.audit.run({ ...ctx, permission: 'finance.payout.read' }, () => this.queryStorePayouts(bot), ); case 'finance_withdrawal': return this.audit.run({ ...ctx, permission: 'finance.withdrawal.read' }, () => this.queryStoreWithdrawals(bot), ); case 'server_logs': return this.audit.run({ ...ctx, permission: 'server_log.read' }, () => this.queryServerLogs(bot, args), ); case 'handbook_read': return this.audit.run({ ...ctx, permission: 'handbook.read' }, () => this.queryHandbook(bot, args), ); default: throw new Error(`未知工具:${toolName}`); } } async tryNaturalLanguageQuery( bot: WecomBotRuntimeConfig, wecomUserId: string, content: string, ): Promise { if ( wecomBotHasPermission(bot, 'support_ticket.read') && /(技术支持|支持工单)/.test(content) && /工单/.test(content) && /(未完成|进行中|待处理|未关闭|所有|全部|列出|查询|查看|有哪些|现在)/.test(content) ) { return this.audit.run( { bot, wecomUserId, action: 'nl:support_tickets_open', permission: 'support_ticket.read' }, () => this.listOpenSupportTickets(bot), ); } if ( wecomBotHasPermission(bot, 'support_ticket.read') && /(开发进度|工单进度|技术支持进度)/.test(content) ) { return this.audit.run( { bot, wecomUserId, action: 'nl:support_progress', permission: 'support_ticket.read' }, () => this.supportProgress(bot, ''), ); } const orderMatch = content.match(/(?:订单|order)[号#]?\s*[::]?\s*([A-Za-z0-9-]+)/i) || content.match(/\b(DK[A-Za-z0-9-]+)\b/); if (orderMatch && wecomBotHasPermission(bot, 'order.read')) { return this.audit.run( { bot, wecomUserId, action: 'nl:order.read', permission: 'order.read', inputSummary: orderMatch[1], }, () => this.queryOrder(bot, orderMatch[1]), ); } return null; } private requirePerm(bot: WecomBotRuntimeConfig, permission: WecomBotPermission) { if (!wecomBotHasPermission(bot, permission)) { throw new Error(`当前机器人无权限:${permission}`); } } private requireReview( bot: WecomBotRuntimeConfig, wecomUserId: string, specificPermission?: WecomBotPermission, ) { // 支持细粒度权限(approve/reject),回退到通用的 review const effectivePerm = specificPermission ?? 'support_ticket.review'; const hasReview = wecomBotHasPermission(bot, 'support_ticket.review'); const hasSpecific = hasReview || wecomBotHasPermission(bot, effectivePerm); if (!hasSpecific) { throw new Error(`当前机器人无权限:${effectivePerm}`); } if (!wecomBotCanReview(bot, wecomUserId)) { const ids = bot.reviewSuperAdminWecomUserIds.filter(Boolean); throw new Error( [ '您不在 SuperAdmin 审批白名单内,无法执行审批。', `当前企微 userid:${wecomUserId}`, ids.length ? `白名单已配置 ${ids.length} 项(须为企微 userid,非姓名):${ids.join('、')}` : '白名单为空,请在 HQ「企微机器人」填写审批 userid', ].join('\n'), ); } } private async approveSupportTicket( bot: WecomBotRuntimeConfig, wecomUserId: string, ticketNo: string, ) { this.requireReview(bot, wecomUserId, 'support_ticket.approve'); if (!ticketNo) return '请提供工单号,例如:`通过 ST1234567890123`'; const ticket = await this.findSupportTicketByNo(ticketNo); if (!ticket) return `未找到工单:${ticketNo}`; const reviewer = await this.resolveReviewer(wecomUserId); const result = await this.supportTicketService.review(ticket.id, reviewer, { decision: 'APPROVE', tasks: [ { content: ticket.title, type: mapSupportTicketTypeToDevPlanTask(ticket.ticketType as SupportTicketTypeDto), }, ], }); const linked = (result as { linkedTasks?: Array<{ taskNo: string }> }).linkedTasks ?? []; return [ '**工单已通过评审**', `- 工单号:${ticket.ticketNo}`, `- 状态:开发中`, linked.length ? `- 已创建开发任务:${linked.map((t) => t.taskNo).join('、')}` : '', ] .filter(Boolean) .join('\n'); } private async rejectSupportTicket( bot: WecomBotRuntimeConfig, wecomUserId: string, ticketNo: string, reason: string, ) { this.requireReview(bot, wecomUserId, 'support_ticket.reject'); if (!ticketNo) return '请提供工单号,例如:`驳回 ST123 不符合需求`'; if (!reason?.trim()) return '请提供驳回理由,例如:`驳回 ST123 重复提单` 或 `审批 驳回 ST123 理由:…`'; const rejectReason = normalizeRejectReason(reason); if (!rejectReason) return '请提供驳回理由'; const ticket = await this.findSupportTicketByNo(ticketNo); if (!ticket) return `未找到工单:${ticketNo}`; const reviewer = await this.resolveReviewer(wecomUserId); await this.supportTicketService.review(ticket.id, reviewer, { decision: 'REJECT', rejectReason, }); return [ '**工单已驳回**', `- 工单号:${ticket.ticketNo}`, `- 理由:${rejectReason}`, ].join('\n'); } private async findSupportTicketByNo(ticketNo: string) { return this.prisma.commonSupportTicket.findFirst({ where: { ticketNo: { contains: ticketNo.trim() } }, }); } private async resolveReviewer(wecomUserId: string) { const admin = await this.prisma.hqAccount.findFirst({ where: { adminRole: 'SUPER_ADMIN', status: 'ACTIVE' }, orderBy: { id: 'asc' }, select: { id: true, name: true }, }); if (!admin) throw new Error('系统未配置 SuperAdmin HQ 账号'); return { id: admin.id, name: `${admin.name}(企微:${wecomUserId})` }; } private async startUserView(bot: WecomBotRuntimeConfig, wecomUserId: string, phone: string) { this.requirePerm(bot, 'user.read_sms'); if (!PHONE_RE.test(phone)) return '请输入 11 位手机号,例如:`查用户 13800138000`'; const user = await this.prisma.user.findFirst({ where: { phone }, select: { id: true, userNo: true, phone: true }, }); if (!user) return `未找到手机号 ${phone} 对应的用户`; await this.session.setPendingPhone(bot.key, wecomUserId, phone); await this.sms.send(phone, SMS_SCENE); return [ `已向 **${maskPhone(phone)}** 发送验证码(用户 ${user.userNo})。`, '请回复:`验证 123456`', ].join('\n'); } private async verifyUserView(bot: WecomBotRuntimeConfig, wecomUserId: string, code: string) { this.requirePerm(bot, 'user.read_sms'); const sess = await this.session.get(bot.key, wecomUserId); if (!sess?.phone) return '请先发送:`查用户 <手机号>`'; if (!code) return '请提供验证码'; try { await this.sms.verify(sess.phone, code, SMS_SCENE); } catch { return '验证码错误或已过期,请重新 `查用户`'; } const user = await this.prisma.user.findFirst({ where: { phone: sess.phone }, select: { id: true, userNo: true, phone: true, nickname: true, status: true, phoneVerifiedAt: true, createdAt: true, _count: { select: { orders: true } }, }, }); if (!user) return '用户不存在'; await this.session.markVerified(bot.key, wecomUserId, sess.phone, user.id.toString()); const coupons = await this.prisma.benefitCoupon.aggregate({ where: { userId: user.id, status: 'ACTIVE' }, _sum: { balance: true }, }); return [ '**用户摘要**(短信验证已通过)', `- 用户号:${user.userNo}`, `- 昵称:${user.nickname || '—'}`, `- 手机:${user.phone}`, `- 订单数:${user._count.orders}`, `- 权益余额:¥${Number(coupons._sum.balance ?? 0).toFixed(2)}`, ].join('\n'); } private async lookupDelivery(bot: WecomBotRuntimeConfig, q: string) { this.requirePerm(bot, 'delivery.read'); if (!q) return '请提供订单号或运单号'; const byOrder = await this.prisma.orderDelivery.findMany({ where: { order: { orderNo: { contains: q } } }, take: 5, orderBy: { updatedAt: 'desc' }, include: { order: { select: { orderNo: true, status: true, receiverName: true, receiverPhone: true, productName: true, }, }, }, }); const rows = byOrder.length > 0 ? byOrder : await this.prisma.orderDelivery.findMany({ where: { trackingNo: { contains: q } }, take: 5, orderBy: { updatedAt: 'desc' }, include: { order: { select: { orderNo: true, status: true, receiverName: true, receiverPhone: true, productName: true, }, }, }, }); if (!rows.length) return `未找到与「${q}」匹配的配送单`; return rows .map((d, i) => { const o = d.order; return [ `**配送 ${i + 1}**`, `- 订单:${o.orderNo}(${o.status})`, `- 商品:${o.productName}`, `- 运单:${d.trackingNo || '—'}`, `- 收货:${o.receiverName} ${maskPhone(o.receiverPhone || '')}`, ].join('\n'); }) .join('\n\n'); } private async createAfterSaleTicket( bot: WecomBotRuntimeConfig, rest: string, wecomUserId: string, ) { this.requirePerm(bot, 'ticket.create'); const parts = rest.split(/\s+/).filter(Boolean); if (parts.length < 2) { return '格式:`工单 <订单号> <类型> [备注]`'; } const orderNo = parts[0]; const typeRaw = parts[1]; const remark = parts.slice(2).join(' ') || `企微客服创建 by ${wecomUserId}`; const ticketType = TICKET_TYPE_ALIASES[typeRaw] || TICKET_TYPE_ALIASES[typeRaw.toLowerCase()]; if (!ticketType) return `未知类型「${typeRaw}」`; const order = await this.prisma.order.findFirst({ where: { orderNo } }); if (!order) return `订单不存在:${orderNo}`; const ticket = await this.ticketService.create({ ticketType, refType: 'ORDER', refId: order.id.toString(), remark: `${remark} [wecom:${wecomUserId}]`, }); return [ '**售后工单已创建**', `- 工单号:${ticket.ticketNo}`, `- 类型:${TICKET_TYPE_LABELS[ticketType]}`, `- 订单:${orderNo}`, ].join('\n'); } private async createSupportTicket( bot: WecomBotRuntimeConfig, rest: string, wecomUserId: string, ) { this.requirePerm(bot, 'support_ticket.create'); const m = rest.match(/^(\S+)\s+(.+)$/); if (!m) return '格式:`提单 <标题> [| 详情]`'; const typeRaw = m[1]; const ticketType = SUPPORT_TYPE_ALIASES[typeRaw] || SUPPORT_TYPE_ALIASES[typeRaw.toLowerCase()]; if (!ticketType) return '类型请使用:BUG / 建议 / 其他'; const [titlePart, ...contentParts] = m[2].split('|'); const title = titlePart.trim(); const content = contentParts.join('|').trim(); if (!title) return '请填写标题'; const creator = await this.resolveCreator(wecomUserId); const ticket = await this.supportTicketService.create( { ticketType, title, content: content || undefined, remark: '企微技术支持机器人' }, creator, ); return [ '**技术支持工单已创建**', `- 工单号:${ticket.ticketNo}`, `- 类型:${SUPPORT_TICKET_TYPE_LABELS[ticketType]}`, `- 标题:${title}`, ].join('\n'); } private async supportTicketDetail(bot: WecomBotRuntimeConfig, ticketNo: string) { this.requirePerm(bot, 'support_ticket.read'); return this.supportProgress(bot, ticketNo); } private async supportProgress(bot: WecomBotRuntimeConfig, ticketNo: string) { this.requirePerm(bot, 'support_ticket.read'); if (ticketNo) { const ticket = await this.findSupportTicketByNo(ticketNo); if (!ticket) return `未找到工单:${ticketNo}`; const detail = await this.supportTicketService.detail(ticket.id); const linked = (detail as { linkedTasks?: Array<{ taskNo: string; status: string }> }).linkedTasks ?? []; return [ `**${ticket.ticketNo}**`, `- 类型:${SUPPORT_TICKET_TYPE_LABELS[ticket.ticketType as SupportTicketTypeDto] || ticket.ticketType}`, `- 状态:${SUPPORT_TICKET_STATUS_LABELS[ticket.status as keyof typeof SUPPORT_TICKET_STATUS_LABELS] || ticket.status}`, `- 标题:${ticket.title}`, `- 创建人:${ticket.creatorName}`, linked.length ? `- 关联任务:${linked.map((t) => `${t.taskNo}(${t.status})`).join('、')}` : '', ticket.rejectReason ? `- 驳回原因:${ticket.rejectReason}` : '', ] .filter(Boolean) .join('\n'); } const { items } = await this.supportTicketService.list({ page: 1, pageSize: 8 }); if (!items.length) return '暂无技术支持工单'; const list = (items as Array<{ ticketNo: string; status: string; title: string }>) .map( (t) => `- ${t.ticketNo} [${SUPPORT_TICKET_STATUS_LABELS[t.status as keyof typeof SUPPORT_TICKET_STATUS_LABELS] || t.status}] ${t.title}`, ) .join('\n'); return [`**最近技术支持工单**`, list, '', '详情:`工单 <工单号>`'].join('\n'); } private async listOpenSupportTickets(bot: WecomBotRuntimeConfig) { this.requirePerm(bot, 'support_ticket.read'); const { items } = await this.supportTicketService.list({ page: 1, pageSize: 20 }); const open = (items as Array<{ ticketNo: string; status: string; title: string }>).filter((t) => ['PENDING_REVIEW', 'DEVELOPING', 'TESTING'].includes(t.status), ); if (!open.length) return '当前没有未完成的技术支持工单。'; const list = open .map( (t, i) => `${i + 1}. **${t.ticketNo}** [${SUPPORT_TICKET_STATUS_LABELS[t.status as keyof typeof SUPPORT_TICKET_STATUS_LABELS] || t.status}] ${t.title}`, ) .join('\n'); return [`**未完成技术支持工单**(${open.length} 条)`, list].join('\n'); } private async queryHandbook(bot: WecomBotRuntimeConfig, q: string): Promise { this.requirePerm(bot, 'handbook.read'); if (!q) { const catalog = searchHandbook('', 20) .map((e) => `- ${e.title}`) .join('\n'); return `**手册目录**\n${catalog}\n\n查询:\`手册 <关键词>\``; } const hits = searchHandbook(q, 3); if (!hits.length) return `未找到与「${q}」相关的手册内容`; return formatHandbook(hits); } private async queryServerLogs(bot: WecomBotRuntimeConfig, keyword: string) { this.requirePerm(bot, 'server_log.read'); const rows = await this.prisma.logUserAnalytics.findMany({ where: keyword ? { eventName: 'client_error', OR: [ { pagePath: { contains: keyword } }, { extraJson: { string_contains: keyword } }, ], } : { eventName: 'client_error' }, orderBy: { createdAt: 'desc' }, take: 8, }); if (!rows.length) return keyword ? `未找到相关客户端报错` : '暂无近期客户端报错'; return [ `**最近客户端报错**`, ...rows.map((row, i) => formatClientErrorLog(row, i + 1)), ].join('\n\n'); } private async queryThirdPartyLogs(bot: WecomBotRuntimeConfig, provider?: string) { this.requirePerm(bot, 'server_log.read'); const where = provider ? { provider: provider.toUpperCase() as never } : {}; const rows = await this.prisma.logThirdParty.findMany({ where, orderBy: { createdAt: 'desc' }, take: 8, }); if (!rows.length) return '暂无近期第三方调用日志'; return rows .map( (row, i) => `${i + 1}. **${row.provider}/${row.scene}** ${row.status} · ${row.createdAt.toISOString().slice(0, 19).replace('T', ' ')}`, ) .join('\n'); } private async queryOrder(bot: WecomBotRuntimeConfig, orderNo: string) { this.requirePerm(bot, 'order.read'); if (!orderNo) return '请提供订单号'; const order = await this.prisma.order.findFirst({ where: { orderNo: { contains: orderNo } }, include: { user: { select: { userNo: true, phone: true, nickname: true } }, delivery: true, }, }); if (!order) return `未找到订单:${orderNo}`; return [ '**订单摘要**', `- 订单号:${order.orderNo}`, `- 状态:${order.status}`, `- 商品:${order.productName}`, `- 实付:¥${Number(order.payAmount).toFixed(2)}`, `- 用户:${order.user?.nickname || '—'} / ${order.user?.userNo || '—'}`, order.delivery ? `- 配送:${order.delivery.provider} ${order.delivery.trackingNo || '—'}` : '- 配送:暂无', ].join('\n'); } private async queryUserByNo(bot: WecomBotRuntimeConfig, userNo: string) { this.requirePerm(bot, 'user.read'); if (!userNo) return '请提供用户号'; const user = await this.prisma.user.findFirst({ where: { userNo: { contains: userNo } }, select: { userNo: true, phone: true, nickname: true, status: true, _count: { select: { orders: true } }, }, }); if (!user) return `未找到用户号:${userNo}`; return [ '**用户摘要**', `- 用户号:${user.userNo}`, `- 昵称:${user.nickname || '—'}`, `- 手机:${maskPhone(user.phone || '')}`, `- 订单数:${user._count.orders}`, ].join('\n'); } private async queryStore(bot: WecomBotRuntimeConfig, keyword: string) { this.requirePerm(bot, 'store.read'); if (!keyword) return '请提供门店关键词'; const rows = await this.prisma.store.findMany({ where: { name: { contains: keyword } }, take: 5, orderBy: { updatedAt: 'desc' }, select: { id: true, name: true, status: true, cityName: true, address: true }, }); if (!rows.length) return `未找到门店:${keyword}`; return rows .map((s, i) => `${i + 1}. **${s.name}** · ${s.status} · ${s.cityName || '—'}`) .join('\n'); } private async queryRedeem(bot: WecomBotRuntimeConfig, q: string) { this.requirePerm(bot, 'redeem.read'); if (!q) return '请提供核销单号或门店名'; const byNo = await this.prisma.redeemRecord.findMany({ where: { redeemNo: { contains: q } }, take: 5, orderBy: { createdAt: 'desc' }, include: { store: { select: { name: true } } }, }); const rows = byNo.length > 0 ? byNo : await this.prisma.redeemRecord.findMany({ where: { store: { name: { contains: q } } }, take: 5, orderBy: { createdAt: 'desc' }, include: { store: { select: { name: true } } }, }); if (!rows.length) return `未找到核销记录:${q}`; return rows .map( (r, i) => `${i + 1}. **${r.redeemNo}** · 门店 ${r.store?.name || '—'} · ¥${Number(r.amount).toFixed(2)}`, ) .join('\n'); } private async queryAfterSaleTicket(bot: WecomBotRuntimeConfig, ticketNo: string) { this.requirePerm(bot, 'ticket.read'); if (!ticketNo) return '请提供售后工单号'; const ticket = await this.prisma.commonTicket.findFirst({ where: { ticketNo: { contains: ticketNo } }, }); if (!ticket) return `未找到售后工单:${ticketNo}`; return [ `**${ticket.ticketNo}**`, `- 类型:${ticket.ticketType}`, `- 状态:${ticket.status}`, `- 备注:${ticket.remark || '—'}`, ].join('\n'); } private async queryStoreBills(bot: WecomBotRuntimeConfig, q?: string) { this.requirePerm(bot, 'finance.store_bill.read'); let storeId: string | undefined; if (q?.trim()) { const store = await this.prisma.store.findFirst({ where: { name: { contains: q.trim() } }, select: { id: true }, }); if (store) storeId = store.id.toString(); } const res = await this.settlement.listAdminStoreBills({ page: 1, pageSize: 8, storeId }); const items = (res as { items: Array<{ billNo: string; status: string; store?: { name: string }; payoutAmount: unknown }> }).items; if (!items.length) return '暂无门店账单'; return items .map( (b, i) => `${i + 1}. **${b.billNo}** · ${b.store?.name || '—'} · ${b.status} · ¥${Number(b.payoutAmount).toFixed(2)}`, ) .join('\n'); } private async queryPartnerBills(bot: WecomBotRuntimeConfig) { this.requirePerm(bot, 'finance.partner_bill.read'); const res = await this.settlement.listAdminPartnerBills({ page: 1, pageSize: 8 }); const items = (res as { items: Array<{ billNo: string; status: string; totalAmount: unknown; partner?: { companyName: string } }> }).items; if (!items.length) return '暂无合伙人账单'; return items .map( (b, i) => `${i + 1}. **${b.billNo}** · ${b.partner?.companyName || '—'} · ¥${Number(b.totalAmount).toFixed(2)}`, ) .join('\n'); } private async queryWineryBills(bot: WecomBotRuntimeConfig) { this.requirePerm(bot, 'finance.winery_bill.read'); const res = await this.settlement.listAdminWineryBills({ page: 1, pageSize: 8 }); const items = (res as { items: Array<{ billNo: string; status: string; wineryAmount: unknown }> }).items; if (!items.length) return '暂无酒厂账单'; return items .map((b, i) => `${i + 1}. **${b.billNo}** · ${b.status} · ¥${Number(b.wineryAmount).toFixed(2)}`) .join('\n'); } private async queryLogisticsBills(bot: WecomBotRuntimeConfig) { this.requirePerm(bot, 'finance.logistics_bill.read'); const res = await this.settlement.listAdminLogisticsBills({ page: 1, pageSize: 8 }); const items = (res as { items: Array<{ billNo: string; status: string; logisticsAmount: unknown; providerName?: string }> }).items; if (!items.length) return '暂无物流账单'; return items .map( (b, i) => `${i + 1}. **${b.billNo}** · ${b.providerName || '—'} · ${b.status} · ¥${Number(b.logisticsAmount).toFixed(2)}`, ) .join('\n'); } private async queryStorePayouts(bot: WecomBotRuntimeConfig) { this.requirePerm(bot, 'finance.payout.read'); const res = await this.settlement.listAdminStorePayouts({ page: 1, pageSize: 8 }); const items = (res as { items: Array<{ id: bigint | string; status: string; payoutAmount: unknown; store?: { name: string } }> }).items; if (!items.length) return '暂无门店打款记录'; return items .map( (p, i) => `${i + 1}. **#${p.id}** · ${p.store?.name || '—'} · ¥${Number(p.payoutAmount).toFixed(2)} · ${p.status}`, ) .join('\n'); } private async queryStoreWithdrawals(bot: WecomBotRuntimeConfig) { this.requirePerm(bot, 'finance.withdrawal.read'); const res = await this.settlement.listAdminStoreWithdrawals({ page: 1, pageSize: 8 }); const items = (res as { items: Array<{ withdrawNo: string; status: string; amount: unknown; store?: { name: string } }> }).items; if (!items.length) return '暂无门店提现记录'; return items .map( (w, i) => `${i + 1}. **${w.withdrawNo}** · ${w.store?.name || '—'} · ¥${Number(w.amount).toFixed(2)} · ${w.status}`, ) .join('\n'); } private async queryDevPlanTasks(bot: WecomBotRuntimeConfig, status?: string) { this.requirePerm(bot, 'dev_plan.task.read'); const res = await this.devPlan.listTasks({ page: 1, pageSize: 10, status: status?.trim() || undefined, }); const items = (res as { items: Array<{ taskNo: string; type: string; status: string; content: string }> }).items; if (!items.length) return '暂无开发任务'; return items .map((t, i) => `${i + 1}. **${t.taskNo}** [${t.type}/${t.status}] ${t.content.slice(0, 80)}`) .join('\n'); } private async queryDevPlanVersions(bot: WecomBotRuntimeConfig, versionNo?: string) { this.requirePerm(bot, 'dev_plan.version.read'); const res = await this.devPlan.listVersions({ page: 1, pageSize: 10, }); let items = (res as { items: Array<{ versionNo: string; status: string; durationMinutes: number | null }> }).items; if (versionNo?.trim()) { items = items.filter((v) => v.versionNo.includes(versionNo.trim())); } if (!items.length) return '暂无开发版本'; return items .map( (v, i) => `${i + 1}. **${v.versionNo}** · ${v.status}${v.durationMinutes != null ? ` · ${v.durationMinutes}min` : ''}`, ) .join('\n'); } // ─── 开发计划 · 写操作 ─── private async createDevPlanTask(bot: WecomBotRuntimeConfig, wecomUserId: string, input: string) { this.requirePerm(bot, 'dev_plan.task.create'); const trimmed = input.trim(); if (!trimmed) return '请提供任务描述,格式:`新建任务 BUG|需求|优化 <描述>`'; const TYPE_MAP: Record = { BUG: 'BUG', bug: 'BUG', 缺陷: 'BUG', REQUIREMENT: 'REQUIREMENT', requirement: 'REQUIREMENT', 需求: 'REQUIREMENT', OPTIMIZATION: 'OPTIMIZATION', optimization: 'OPTIMIZATION', 优化: 'OPTIMIZATION' }; const firstWord = trimmed.split(/\s+/)[0]; const type = TYPE_MAP[firstWord]; if (!type) return `未知任务类型「${firstWord}」。可用:BUG / 需求 / 优化\n例如:\`新建任务 BUG 登录页报错\``; const content = trimmed.slice(firstWord.length).trim(); if (!content) return '请提供任务描述内容'; const creator = await this.resolveCreator(wecomUserId); const task = await this.devPlan.createTask({ content, type }, creator.id); return `✅ 已创建任务 **${(task as { taskNo: string }).taskNo}** [${type}] ${content.slice(0, 60)}`; } private async updateDevPlanTaskStatus(bot: WecomBotRuntimeConfig, wecomUserId: string, input: string) { this.requirePerm(bot, 'dev_plan.task.update_status'); const trimmed = input.trim(); if (!trimmed) return '请提供任务编号和状态,格式:`修改任务 STxxx TODO|DEVELOPED|RELEASED`'; const parts = trimmed.split(/\s+/).filter(Boolean); if (parts.length < 2) return '格式:`修改任务 <任务编号> <状态>`\n状态:TODO(待开发)· DEVELOPED(已开发)· RELEASED(已发布)'; const [taskNo, rawStatus] = parts; const STATUS_ALIAS: Record = { TODO: 'TODO', todo: 'TODO', 待开发: 'TODO', DEVELOPED: 'DEVELOPED', developed: 'DEVELOPED', 已开发: 'DEVELOPED', RELEASED: 'RELEASED', released: 'RELEASED', 已发布: 'RELEASED', }; const status = STATUS_ALIAS[rawStatus]; if (!status) return `未知状态「${rawStatus}」。可用:TODO(待开发)/ DEVELOPED(已开发)/ RELEASED(已发布)`; const row = await this.prisma.devPlanTask.findFirst({ where: { taskNo } }); if (!row) return `未找到任务「${taskNo}」`; const updated = await this.devPlan.updateTask(row.id, { status }); return `✅ 任务 **${taskNo}** → ${status}`; } private async createDevPlanVersion(bot: WecomBotRuntimeConfig, wecomUserId: string, input: string) { this.requirePerm(bot, 'dev_plan.version.create'); const versionNo = input.trim(); if (!versionNo) return '请提供版本号,格式:`新建版本 v3.6.0`'; const version = await this.devPlan.createVersion({ versionNo }); return `✅ 已创建版本 **${(version as { versionNo: string }).versionNo}**`; } private async updateDevPlanVersionStatus(bot: WecomBotRuntimeConfig, wecomUserId: string, input: string) { this.requirePerm(bot, 'dev_plan.version.update_status'); const trimmed = input.trim(); if (!trimmed) return '请提供版本号和状态,格式:`修改版本 v3.6.0 <状态>`'; const parts = trimmed.split(/\s+/).filter(Boolean); if (parts.length < 2) return '格式:`修改版本 <版本号> <状态>`\n状态:PENDING · IN_PROGRESS · TESTING · RELEASED'; const [versionNo, rawStatus] = parts; const STATUS_ALIAS: Record = { PENDING: 'PENDING', pending: 'PENDING', 计划中: 'PENDING', IN_PROGRESS: 'IN_PROGRESS', in_progress: 'IN_PROGRESS', 开发中: 'IN_PROGRESS', TESTING: 'TESTING', testing: 'TESTING', 测试中: 'TESTING', RELEASED: 'RELEASED', released: 'RELEASED', 已发布: 'RELEASED', }; const status = STATUS_ALIAS[rawStatus]; if (!status) return `未知状态「${rawStatus}」。可用:PENDING / IN_PROGRESS / TESTING / RELEASED`; const row = await this.prisma.devPlanVersion.findUnique({ where: { versionNo } }); if (!row) return `未找到版本「${versionNo}」`; const updated = await this.devPlan.updateVersion(row.id, { status }); return `✅ 版本 **${(updated as { versionNo: string }).versionNo}** → ${(updated as { status: string }).status}`; } private async linkTasksToVersion(bot: WecomBotRuntimeConfig, wecomUserId: string, input: string) { this.requirePerm(bot, 'dev_plan.version.link_tasks'); const trimmed = input.trim(); if (!trimmed) return '请提供版本号和任务编号,格式:`关联任务 v3.6.0 DPT-001,DPT-002`'; const parts = trimmed.split(/\s+/).filter(Boolean); if (parts.length < 2) return '格式:`关联任务 <版本号> <任务编号1,任务编号2>`'; const versionNo = parts[0]; const taskNos = parts[1].split(/[,,]+/).map((s) => s.trim()).filter(Boolean); if (!taskNos.length) return '请提供至少 1 个任务编号'; const version = await this.prisma.devPlanVersion.findUnique({ where: { versionNo } }); if (!version) return `未找到版本「${versionNo}」`; const tasks = await this.prisma.devPlanTask.findMany({ where: { taskNo: { in: taskNos } }, select: { id: true, taskNo: true }, }); if (!tasks.length) return `未找到任务「${taskNos.join(', ')}」`; const foundNos = tasks.map((t) => t.taskNo); const missing = taskNos.filter((n) => !foundNos.includes(n)); const taskIds = tasks.map((t) => String(t.id)); await this.devPlan.addVersionTasksApi(version.id, taskIds); const summary = `已关联 ${foundNos.length} 个任务到版本 **${versionNo}**:${foundNos.join(', ')}`; if (missing.length) return `⚠️ ${summary}\n未找到:${missing.join(', ')}`; return `✅ ${summary}`; } private async resolveCreator(wecomUserId: string) { const admin = await this.prisma.hqAccount.findFirst({ where: { status: 'ACTIVE' }, orderBy: [{ id: 'asc' }], select: { id: true, name: true }, }); if (!admin) throw new Error('系统未配置 HQ 账号'); return { id: admin.id, name: `${admin.name || 'HQ'}(企微:${wecomUserId})` }; } } function maskPhone(phone: string): string { if (!phone || phone.length < 7) return phone || '—'; return `${phone.slice(0, 3)}****${phone.slice(-4)}`; } type ReviewCommand = | { decision: 'APPROVE'; ticketNo: string } | { decision: 'REJECT'; ticketNo: string; reason: string }; /** 解析审批指令:支持「审批 驳回 STxxx 理由:…」等自然说法 */ function parseReviewCommand(content: string): ReviewCommand | null { const text = content.trim().replace(/\s+/g, ' '); const approve = text.match(/^(?:审批\s*)?(?:通过|approve)\s+(\S+)/i); if (approve) { return { decision: 'APPROVE', ticketNo: stripTicketNo(approve[1]) }; } const reject = text.match(/^(?:审批\s*)?(?:驳回|reject)\s+(\S+)(?:\s+(.*))?$/i); if (reject) { return { decision: 'REJECT', ticketNo: stripTicketNo(reject[1]), reason: normalizeRejectReason(reject[2] ?? ''), }; } return null; } function stripTicketNo(raw: string): string { return raw.replace(/^#/, '').trim(); } function normalizeRejectReason(raw: string): string { return raw.trim().replace(/^理由[::]?\s*/u, '').trim(); } function formatHandbook(entries: ReturnType): string { return entries.map((e) => `**${e.title}**\n${e.body}`).join('\n\n---\n\n'); } type ClientErrorLogRow = { clientApp: string | null; pagePath: string | null; extraJson: unknown; createdAt: Date; }; function formatClientErrorLog(row: ClientErrorLogRow, index: number): string { const extra = row.extraJson && typeof row.extraJson === 'object' ? (row.extraJson as Record) : {}; const message = typeof extra.message === 'string' ? extra.message.slice(0, 160) : '—'; return [ `**${index}.**`, `- 端:${row.clientApp || '—'}`, row.pagePath ? `- 页面:${row.pagePath}` : null, `- 消息:${message}`, `- 时间:${row.createdAt.toISOString().slice(0, 19).replace('T', ' ')}`, ] .filter(Boolean) .join('\n'); }