| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247 |
- /**
- * 群风控与异常干预模块 — 业务服务层
- *
- * 对应规范文档 §八「模块 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<number, RiskKeyword>();
- const alertStore = new Map<number, Alert>();
- const workOrderStore = new Map<number, WorkOrder>();
- let nextKeywordId = 1;
- let nextAlertId = 1;
- let nextWorkOrderId = 1;
- // ============================================================
- // 风险关键词库(§8.2)
- // ============================================================
- /** 创建关键词 */
- export function createKeyword(data: Omit<RiskKeyword, 'id' | 'createdAt'>): 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>): 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;
- }
|