/** * 群风控与异常干预模块 — 业务服务层 * * 对应规范文档 §八「模块 5:群风控与异常干预模块」 * * 业务流程: * §8.1 实时监听群消息:由 qiwei 模块的 webhook 消费者完成 * §8.2 风险关键词库配置:CRUD 管理风险词 * §8.3 命中敏感词自动预警:消息内容与词库匹配 → 生成 alert * §8.4 人数骤降等阈值异常:统计退群事件、零互动天数 → 生成 alert * §8.5 异常提醒到企微:通过 API-14 发送 * §8.6 预警自动生成干预工单:alert 创建时联动生成 work_order * §8.7 工单处理与关闭:更新处理记录 → 计时统计响应时长 * §8.8 异常处理案例知识库:已关闭工单归档为 kb_case(二期) */ import { AppError } from '../../../../shared/errors/app-error.js'; import type { RiskKeyword, Alert, AlertType, WorkOrder, WorkOrderStatus, } from '../models/risk.model.js'; // ---------- 内存存储(TODO: 替换为数据库)---------- const keywordStore = new Map(); const alertStore = new Map(); const workOrderStore = new Map(); let nextKeywordId = 1; let nextAlertId = 1; let nextWorkOrderId = 1; // ============================================================ // 风险关键词库(§8.2) // ============================================================ /** 创建关键词 */ export function createKeyword(data: Omit): RiskKeyword { const kw: RiskKeyword = { id: nextKeywordId++, ...data, createdAt: new Date().toISOString(), }; keywordStore.set(kw.id, kw); return kw; } /** 查询关键词列表 */ export function listKeywords(category?: string, enabled?: number): RiskKeyword[] { let result = Array.from(keywordStore.values()); if (category) result = result.filter((k) => k.category === category); if (enabled !== undefined) result = result.filter((k) => k.enabled === enabled); return result; } /** 更新关键词 */ export function updateKeyword(id: number, data: Partial): RiskKeyword { const kw = keywordStore.get(id); if (!kw) throw new AppError(404, 'KEYWORD_NOT_FOUND', `关键词 ID=${id} 不存在`); Object.assign(kw, data); keywordStore.set(id, kw); return kw; } /** 删除关键词 */ export function deleteKeyword(id: number): void { if (!keywordStore.has(id)) throw new AppError(404, 'KEYWORD_NOT_FOUND', `关键词 ID=${id} 不存在`); keywordStore.delete(id); } /** * 扫描消息内容,匹配风险关键词 * * 实际生产环境在 webhook 消费者中对每条消息实时执行。 * * @param content - 消息文本内容 * @param roomId - 群 ID * @param messageId - 消息 ID * @returns 命中的预警列表(如无命中则返回空数组) */ export function scanMessageForKeywords( content: string, roomId: string, messageId: number, ): Alert[] { const alerts: Alert[] = []; const activeKeywords = listKeywords(undefined, 1); for (const kw of activeKeywords) { if (content.includes(kw.keyword)) { const alert = createAlert({ roomId, type: 'keyword', level: kw.severity, title: `命中风险关键词"${kw.keyword}"`, description: `群 ${roomId} 的消息命中风险关键词"${kw.keyword}"(分类:${kw.category})`, matchedKeyword: kw.keyword, messageId, }); alerts.push(alert); } } return alerts; } // ============================================================ // 预警管理(§8.3 ~ §8.5) // ============================================================ interface CreateAlertParams { roomId: string; type: AlertType; level: 1 | 2 | 3; title: string; description: string; matchedKeyword?: string; messageId?: number; } /** 创建预警记录 */ export function createAlert(params: CreateAlertParams): Alert { const now = new Date().toISOString(); const alert: Alert = { id: nextAlertId++, roomId: params.roomId, type: params.type, level: params.level, title: params.title, description: params.description, matchedKeyword: params.matchedKeyword || '', messageId: params.messageId || 0, status: 'open', handlerId: 0, handleNote: '', workOrderId: 0, createdAt: now, handledAt: '', }; alertStore.set(alert.id, alert); // ---- §8.6:高危预警自动生成干预工单 ---- if (params.level >= 3) { const order = createWorkOrder({ alertId: alert.id, title: `【自动】${params.title}`, description: params.description, assigneeId: 0, // 待分配 }); alert.workOrderId = order.id; alertStore.set(alert.id, alert); } return alert; } /** 查询预警列表 */ export function listAlerts( roomId?: string, type?: AlertType, status?: 'open' | 'handled' | 'ignored', ): Alert[] { let result = Array.from(alertStore.values()); if (roomId) result = result.filter((a) => a.roomId === roomId); if (type) result = result.filter((a) => a.type === type); if (status) result = result.filter((a) => a.status === status); return result.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()); } /** 处理预警 */ export function handleAlert( id: number, status: 'handled' | 'ignored', handlerId: number, note: string, ): Alert { const alert = alertStore.get(id); if (!alert) throw new AppError(404, 'ALERT_NOT_FOUND', `预警 ID=${id} 不存在`); alert.status = status; alert.handlerId = handlerId; alert.handleNote = note; alert.handledAt = new Date().toISOString(); alertStore.set(id, alert); return alert; } // ============================================================ // 干预工单(§8.6 ~ §8.7) // ============================================================ interface CreateWorkOrderParams { alertId: number; title: string; description: string; assigneeId: number; deadline?: string; } /** 创建工单 */ export function createWorkOrder(params: CreateWorkOrderParams): WorkOrder { const now = new Date().toISOString(); // 默认截止时间为 24h 后 const deadline = params.deadline || new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(); const order: WorkOrder = { id: nextWorkOrderId++, alertId: params.alertId, title: params.title, description: params.description, assigneeId: params.assigneeId, deadline, status: 'pending', handleRecord: '', createdAt: now, completedAt: '', }; workOrderStore.set(order.id, order); return order; } /** 查询工单列表 */ export function listWorkOrders(status?: WorkOrderStatus, assigneeId?: number): WorkOrder[] { let result = Array.from(workOrderStore.values()); if (status) result = result.filter((o) => o.status === status); if (assigneeId !== undefined) result = result.filter((o) => o.assigneeId === assigneeId); return result.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()); } /** 更新工单状态 */ export function updateWorkOrder( id: number, status: WorkOrderStatus, handleRecord?: string, ): WorkOrder { const order = workOrderStore.get(id); if (!order) throw new AppError(404, 'WORK_ORDER_NOT_FOUND', `工单 ID=${id} 不存在`); order.status = status; if (handleRecord) order.handleRecord = handleRecord; if (status === 'completed') order.completedAt = new Date().toISOString(); workOrderStore.set(id, order); return order; }