audit-risk-deep-analysis.ts 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678
  1. /**
  2. * 风险预警深度分析脚本 (6/15-6/22)
  3. *
  4. * 用法: cd backend && npx tsx scripts/audit-risk-deep-analysis.ts
  5. *
  6. * 分析维度:
  7. * 1. 消息按天/按群/按类型分布
  8. * 2. 关键词命中但未预警的消息(漏报)
  9. * 3. 关键词命中且AI确认为有风险的(真漏报)
  10. * 4. 无关键词命中但可能隐含风险的(盲区)
  11. * 5. 已触发预警的回溯分析
  12. * 6. 竞品提及统计
  13. */
  14. import 'dotenv/config';
  15. import Parse from '../src/db/parse-client.js';
  16. import * as fs from 'fs';
  17. import * as path from 'path';
  18. import { fileURLToPath } from 'url';
  19. const __dirname = path.dirname(fileURLToPath(import.meta.url));
  20. const GUID = process.env.QIWE_GUID || '';
  21. /* ======================== 类型 ======================== */
  22. interface KeywordInfo {
  23. word: string;
  24. category: string;
  25. severity: 'high' | 'medium' | 'low';
  26. }
  27. interface MsgInfo {
  28. id: string;
  29. roomId: string;
  30. senderName: string;
  31. content: string;
  32. timestamp: Date;
  33. msgType: number;
  34. }
  35. interface RiskEventInfo {
  36. id: string;
  37. groupId: string;
  38. keywords: string[];
  39. severity: string;
  40. title: string;
  41. description: string;
  42. status: string;
  43. createdAt: Date;
  44. }
  45. const DATE_RANGE = {
  46. start: new Date('2026-06-15T00:00:00+08:00'),
  47. end: new Date('2026-06-22T23:59:59+08:00'),
  48. };
  49. /* ======================== 数据查询 ======================== */
  50. async function fetchData() {
  51. console.log('[分析] 拉取数据...\n');
  52. const [allMessages, riskEvents, keywords, groupMap] = await Promise.all([
  53. (async () => {
  54. const q = new Parse.Query('Message');
  55. q.greaterThanOrEqualTo('timestamp', DATE_RANGE.start);
  56. q.lessThanOrEqualTo('timestamp', DATE_RANGE.end);
  57. q.containedIn('msgType', [0, 2, 13, 29]);
  58. q.notEqualTo('content', '');
  59. q.select(['roomId', 'senderName', 'content', 'timestamp', 'msgType']);
  60. q.limit(200000);
  61. q.ascending('timestamp');
  62. const rows = await q.find({ useMasterKey: true }) as any[];
  63. const msgs: MsgInfo[] = rows.map(r => ({
  64. id: r.id,
  65. roomId: (r.get('roomId') as string) || '',
  66. senderName: (r.get('senderName') as string) || '',
  67. content: (r.get('content') as string) || '',
  68. timestamp: r.get('timestamp') as Date,
  69. msgType: (r.get('msgType') as number) || 0,
  70. }));
  71. console.log(` Message: ${msgs.length} 条`);
  72. return msgs;
  73. })(),
  74. (async () => {
  75. const q = new Parse.Query('RiskEvent');
  76. q.greaterThanOrEqualTo('createdAt', DATE_RANGE.start);
  77. q.lessThanOrEqualTo('createdAt', DATE_RANGE.end);
  78. q.select(['groupId', 'keywords', 'severity', 'title', 'status', 'createdAt']);
  79. q.limit(5000);
  80. const rows = await q.find({ useMasterKey: true }) as any[];
  81. const events: RiskEventInfo[] = rows.map(r => ({
  82. id: r.id,
  83. groupId: (r.get('groupId') as string) || '',
  84. keywords: (r.get('keywords') as string[]) || [],
  85. severity: (r.get('severity') as string) || '',
  86. title: (r.get('title') as string) || '',
  87. description: (r.get('description') as string) || '',
  88. status: (r.get('status') as string) || 'pending',
  89. createdAt: r.get('createdAt') as Date,
  90. }));
  91. console.log(` RiskEvent: ${events.length} 条`);
  92. return events;
  93. })(),
  94. (async () => {
  95. const q = new Parse.Query('RiskKeyword');
  96. q.equalTo('enabled', true);
  97. q.select(['word', 'category', 'severity']);
  98. q.limit(5000);
  99. const rows = await q.find({ useMasterKey: true }) as any[];
  100. const kws: KeywordInfo[] = rows.map((r: any) => ({
  101. word: r.get('word') as string,
  102. category: r.get('category') as string,
  103. severity: r.get('severity') as string,
  104. }));
  105. console.log(` RiskKeyword: ${kws.length} 个启用`);
  106. return kws;
  107. })(),
  108. (async () => {
  109. const q = new Parse.Query('GroupChat');
  110. q.select(['roomId', 'roomName', 'communityName']);
  111. q.limit(5000);
  112. const rows = await q.find({ useMasterKey: true }) as any[];
  113. const map = new Map<string, { name: string; community: string }>();
  114. for (const r of rows) {
  115. map.set(
  116. (r.get('roomId') as string) || '',
  117. {
  118. name: (r.get('roomName') as string) || '未知群',
  119. community: (r.get('communityName') as string) || '',
  120. }
  121. );
  122. }
  123. console.log(` GroupChat: ${map.size} 个群`);
  124. return map;
  125. })(),
  126. ]);
  127. return { allMessages, riskEvents, keywords, groupMap };
  128. }
  129. /* ======================== 消息基础统计 ======================== */
  130. function basicStats(messages: MsgInfo[]) {
  131. // 按天
  132. const byDay: Record<string, number> = {};
  133. const byType: Record<number, number> = {};
  134. const byRoom: Record<string, { count: number; senders: Set<string> }> = {};
  135. const allSenders = new Set<string>();
  136. for (const m of messages) {
  137. const day = m.timestamp.toISOString().slice(0, 10);
  138. byDay[day] = (byDay[day] || 0) + 1;
  139. const mt = m.msgType;
  140. byType[mt] = (byType[mt] || 0) + 1;
  141. if (m.roomId) {
  142. if (!byRoom[m.roomId]) byRoom[m.roomId] = { count: 0, senders: new Set() };
  143. byRoom[m.roomId].count++;
  144. byRoom[m.roomId].senders.add(m.senderName || '未知');
  145. }
  146. if (m.senderName) allSenders.add(m.senderName);
  147. }
  148. const topRooms = Object.entries(byRoom)
  149. .sort((a, b) => b[1].count - a[1].count)
  150. .slice(0, 15);
  151. return { byDay, byType, byRoom, topRooms, uniqueSenders: allSenders.size };
  152. }
  153. /* ======================== 关键词匹配分析 ======================== */
  154. function matchAnalysis(messages: MsgInfo[], keywords: KeywordInfo[], groupMap: Map<string, { name: string; community: string }>) {
  155. // 全量关键词命中
  156. const kwHitCount: Record<string, { count: number; kw: KeywordInfo; rooms: Set<string> }> = {};
  157. const msgsWithHits = new Map<string, { msg: MsgInfo; hitKws: KeywordInfo[] }>();
  158. for (const msg of messages) {
  159. const content = msg.content;
  160. if (!content) continue;
  161. const hits: KeywordInfo[] = [];
  162. for (const kw of keywords) {
  163. if (content.includes(kw.word)) {
  164. hits.push(kw);
  165. if (!kwHitCount[kw.word]) kwHitCount[kw.word] = { count: 0, kw, rooms: new Set() };
  166. kwHitCount[kw.word].count++;
  167. kwHitCount[kw.word].rooms.add(msg.roomId);
  168. }
  169. }
  170. if (hits.length > 0) {
  171. msgsWithHits.set(msg.id, { msg, hitKws: hits });
  172. }
  173. }
  174. // 按类别汇总
  175. const catStats: Record<string, { total: number; hit: number; words: Set<string> }> = {};
  176. for (const kw of keywords) {
  177. if (!catStats[kw.category]) catStats[kw.category] = { total: 0, hit: 0, words: new Set() };
  178. catStats[kw.category].total++;
  179. catStats[kw.category].words.add(kw.word);
  180. }
  181. for (const [word, info] of Object.entries(kwHitCount)) {
  182. catStats[info.kw.category].hit++;
  183. }
  184. // 类别有效率 = 命中词数 / 总词数
  185. const catEffectiveness = Object.entries(catStats).map(([cat, s]) => ({
  186. category: cat,
  187. totalWords: s.total,
  188. hitWords: s.hit,
  189. hitMessages: Object.entries(kwHitCount)
  190. .filter(([_, v]) => v.kw.category === cat)
  191. .reduce((sum, [_, v]) => sum + v.count, 0),
  192. effectiveness: ((s.hit / s.total) * 100).toFixed(0),
  193. })).sort((a, b) => b.hitMessages - a.hitMessages);
  194. return { kwHitCount, msgsWithHits, catEffectiveness };
  195. }
  196. /* ======================== 漏报 vs 已预警 ======================== */
  197. function missedVsAlerted(
  198. messages: MsgInfo[],
  199. riskEvents: RiskEventInfo[],
  200. keywords: KeywordInfo[],
  201. groupMap: Map<string, { name: string; community: string }>,
  202. ) {
  203. const alertedRoomIds = new Set(riskEvents.map(e => e.groupId));
  204. const alertedKeywords = new Set(riskEvents.flatMap(e => e.keywords));
  205. // 按群分组
  206. const msgsByRoom = new Map<string, MsgInfo[]>();
  207. for (const m of messages) {
  208. if (!m.roomId) continue;
  209. const arr = msgsByRoom.get(m.roomId);
  210. if (arr) arr.push(m);
  211. else msgsByRoom.set(m.roomId, [m]);
  212. }
  213. // 每个群的命中分析
  214. const missedRooms: Array<{
  215. roomId: string;
  216. roomName: string;
  217. hitWords: string[];
  218. missedWords: string[];
  219. alertedWords: string[];
  220. sampleMsgs: MsgInfo[];
  221. maxSeverity: string;
  222. }> = [];
  223. for (const [roomId, msgs] of msgsByRoom) {
  224. const hitWords = new Set<string>();
  225. for (const m of msgs) {
  226. for (const kw of keywords) {
  227. if (m.content.includes(kw.word)) hitWords.add(kw.word);
  228. }
  229. }
  230. if (hitWords.size === 0) continue;
  231. const hitArr = [...hitWords];
  232. const missedWords = hitArr.filter(w => !alertedKeywords.has(w));
  233. const alertedWords = hitArr.filter(w => alertedKeywords.has(w));
  234. if (missedWords.length > 0) {
  235. const sevs = missedWords.map(w => keywords.find(k => k.word === w)?.severity || 'low');
  236. const maxSeverity = sevs.includes('high') ? 'high' : sevs.includes('medium') ? 'medium' : 'low';
  237. const samples = msgs.filter(m => missedWords.some(w => m.content.includes(w))).slice(0, 10);
  238. missedRooms.push({
  239. roomId,
  240. roomName: groupMap.get(roomId)?.name || `未知群(${roomId})`,
  241. hitWords: hitArr,
  242. missedWords,
  243. alertedWords,
  244. sampleMsgs: samples,
  245. maxSeverity,
  246. });
  247. }
  248. }
  249. // 按严重度排序
  250. const sevOrder = { high: 0, medium: 1, low: 2 };
  251. missedRooms.sort((a, b) => sevOrder[a.maxSeverity as keyof typeof sevOrder] - sevOrder[b.maxSeverity as keyof typeof sevOrder]);
  252. // 真正从未触发预警的关键词
  253. const trulyMissed = keywords.filter(k => !alertedKeywords.has(k.word));
  254. return {
  255. missedRooms,
  256. trulyMissed,
  257. alertedRoomIds,
  258. alertedKeywords,
  259. stats: {
  260. alertedRooms: alertedRoomIds.size,
  261. alertedKeywords: alertedKeywords.size,
  262. missedRoomsCount: missedRooms.length,
  263. trulyMissedCount: trulyMissed.length,
  264. highMissed: missedRooms.filter(r => r.maxSeverity === 'high').length,
  265. mediumMissed: missedRooms.filter(r => r.maxSeverity === 'medium').length,
  266. lowMissed: missedRooms.filter(r => r.maxSeverity === 'low').length,
  267. },
  268. };
  269. }
  270. /* ======================== AI 深度分析(代表性样本) ======================== */
  271. const AI_KEY = process.env.DEEPSEEK_API_KEY || '';
  272. const AI_URL = process.env.DEEPSEEK_API_URL || 'https://api.deepseek.com/v1/chat/completions';
  273. const AI_MODEL = process.env.DEEPSEEK_MODEL || 'deepseek-chat';
  274. const AI_ENABLED = AI_KEY && AI_KEY !== 'sk-xxx';
  275. async function aiSummaryReport(context: {
  276. totalMessages: number;
  277. totalEvents: number;
  278. totalKeywords: number;
  279. coverageRate: string;
  280. missedRooms: ReturnType<typeof missedVsAlerted>['missedRooms'];
  281. catEffectiveness: ReturnType<typeof matchAnalysis>['catEffectiveness'];
  282. trulyMissedCount: number;
  283. }): Promise<string> {
  284. if (!AI_ENABLED) return '(AI 未启用,请配置 DEEPSEEK_API_KEY)';
  285. const sevCounts = {
  286. high: context.missedRooms.filter(r => r.maxSeverity === 'high').length,
  287. medium: context.missedRooms.filter(r => r.maxSeverity === 'medium').length,
  288. low: context.missedRooms.filter(r => r.maxSeverity === 'low').length,
  289. };
  290. const topMissed = context.missedRooms.slice(0, 5).map(r =>
  291. `- ${r.roomName}: [${r.maxSeverity}] 命中 ${r.missedWords.join(', ')}`
  292. ).join('\n');
  293. const catInfo = context.catEffectiveness.map(c =>
  294. `${c.category}: ${c.hitWords}/${c.totalWords} 个关键词被命中, 共 ${c.hitMessages} 条消息命中`
  295. ).join('\n');
  296. const prompt = `你是拉迷家居(全屋定制品牌)的风险管理顾问。请基于以下 6/15-6/22 的群聊监控数据,输出一份管理建议报告(300-500字,中文)。
  297. ## 数据
  298. - 文本消息总量: ${context.totalMessages}
  299. - 已有预警事件: ${context.totalEvents}
  300. - 启用关键词: ${context.totalKeywords} 个
  301. - 关键词覆盖率(触发过预警的): ${context.coverageRate}%
  302. - 从未触发预警的关键词: ${context.trulyMissedCount} 个
  303. - 漏报群: ${context.missedRooms.length}(高危${sevCounts.high}/中危${sevCounts.medium}/低危${sevCounts.low})
  304. ## 关键词类别有效性
  305. ${catInfo}
  306. ## 高危/中危漏报群
  307. ${topMissed}
  308. ## 要求
  309. 请从以下5个维度给出管理建议(每个维度1-3条具体建议):
  310. 1. **关键词库优化**: 哪些词该删、该降级、该升级
  311. 2. **预警规则优化**: 是否需要关键词频次阈值、聚合规则
  312. 3. **盲区发现**: 当前纯关键词匹配的方法可能漏掉什么
  313. 4. **流程优化**: 预警→响应→关闭的闭环改进
  314. 5. **定期审计**: 建议的审计频率和关注指标
  315. 回复格式: 直接输出管理建议报告,不要 JSON,不要 markdown 代码块。`;
  316. try {
  317. const res = await fetch(AI_URL, {
  318. method: 'POST',
  319. headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${AI_KEY}` },
  320. body: JSON.stringify({
  321. model: AI_MODEL,
  322. messages: [
  323. { role: 'system', content: '你是全屋定制行业风险管理顾问。输出简洁、具体、可落地的管理建议。' },
  324. { role: 'user', content: prompt },
  325. ],
  326. temperature: 0.4,
  327. max_tokens: 1200,
  328. }),
  329. });
  330. if (!res.ok) return `AI 调用失败: ${res.status}`;
  331. const json = await res.json() as any;
  332. return json?.choices?.[0]?.message?.content || 'AI 返回为空';
  333. } catch (err: any) {
  334. return `AI 错误: ${err.message}`;
  335. }
  336. }
  337. /* ======================== 主流程 ======================== */
  338. async function main(): Promise<void> {
  339. const t0 = Date.now();
  340. console.log('═══════════════════════════════════════');
  341. console.log(' 风险预警深度分析 (6/15 - 6/22)');
  342. console.log('═══════════════════════════════════════\n');
  343. // 1. 数据
  344. const { allMessages, riskEvents, keywords, groupMap } = await fetchData();
  345. // 2. 基础统计
  346. console.log('\n[1/4] 基础统计...');
  347. const stats = basicStats(allMessages);
  348. console.log(` 消息天数: ${Object.keys(stats.byDay).length}`);
  349. console.log(` 活跃群数: ${Object.keys(stats.byRoom).length}`);
  350. console.log(` 参与人数: ${stats.uniqueSenders}`);
  351. // 3. 关键词匹配
  352. console.log('\n[2/4] 关键词匹配分析...');
  353. const { kwHitCount, msgsWithHits, catEffectiveness } = matchAnalysis(allMessages, keywords, groupMap);
  354. console.log(` 命中关键词的消息: ${msgsWithHits.size} 条`);
  355. console.log(` 命中关键词种类: ${Object.keys(kwHitCount).length} 个`);
  356. // 4. 漏报分析
  357. console.log('\n[3/4] 漏报分析...');
  358. const { missedRooms, trulyMissed, stats: missStats } = missedVsAlerted(allMessages, riskEvents, keywords, groupMap);
  359. console.log(` 已有预警群: ${missStats.alertedRooms}`);
  360. console.log(` 漏报群: ${missStats.missedRoomsCount}(高危${missStats.highMissed}/中危${missStats.mediumMissed}/低危${missStats.lowMissed})`);
  361. console.log(` 从未触发预警的关键词: ${missStats.trulyMissedCount}`);
  362. // 5. AI 综合建议
  363. console.log('\n[4/4] AI 生成管理建议...');
  364. const coverageRate = keywords.length > 0 ? ((missStats.alertedKeywords / keywords.length) * 100).toFixed(1) : '0';
  365. const aiReport = await aiSummaryReport({
  366. totalMessages: allMessages.length,
  367. totalEvents: riskEvents.length,
  368. totalKeywords: keywords.length,
  369. coverageRate,
  370. missedRooms,
  371. catEffectiveness,
  372. trulyMissedCount: missStats.trulyMissedCount,
  373. });
  374. console.log(' AI 报告生成完成');
  375. // 6. 生成 MD 报告
  376. const genTime = new Date().toLocaleString('zh-CN');
  377. const totalMs = Date.now() - t0;
  378. // 类别有效性表格
  379. const catRows = catEffectiveness.map(c =>
  380. `| ${categoryCn(c.category)} | ${c.totalWords} | ${c.hitWords} | ${c.hitMessages} | ${c.effectiveness}% |`
  381. ).join('\n');
  382. // 漏报群表格
  383. const missedRows = missedRooms.slice(0, 20).map((r, i) => {
  384. const sevIcon = r.maxSeverity === 'high' ? '🔴' : r.maxSeverity === 'medium' ? '🟡' : '🟢';
  385. const samples = r.sampleMsgs.slice(0, 3).map(m => {
  386. const ts = m.timestamp;
  387. const time = `${ts.getMonth() + 1}/${ts.getDate()} ${String(ts.getHours()).padStart(2, '0')}:${String(ts.getMinutes()).padStart(2, '0')}`;
  388. return `[${time}] ${m.senderName || '未知'}: ${m.content.length > 80 ? m.content.slice(0, 80) + '...' : m.content}`;
  389. }).join('<br>');
  390. return `| ${i + 1} | ${sevIcon} ${escapeMd(r.roomName)} | ${r.missedWords.join(', ')} | ${sevIcon} ${r.maxSeverity} | ${r.hitWords.length - r.missedWords.length > 0 ? '部分覆盖' : '完全漏报'} | ${samples} |`;
  391. }).join('\n');
  392. // 已触发预警表格
  393. const alertedRows = riskEvents.map(e => {
  394. const g = groupMap.get(e.groupId);
  395. return `| ${e.createdAt ? new Date(e.createdAt).toLocaleString('zh-CN') : ''} | ${escapeMd(g?.name || e.groupId)} | ${e.keywords.join(', ')} | ${e.severity} | ${e.status} | ${escapeMd(e.title)} |`;
  396. }).join('\n');
  397. // 消息分布表格
  398. const dayRows = Object.keys(stats.byDay).sort().map(d =>
  399. `| ${d} | ${stats.byDay[d]} |`
  400. ).join('\n');
  401. // TOP 群表格
  402. const topRoomRows = stats.topRooms.map(([rid, info], i) => {
  403. const g = groupMap.get(rid);
  404. return `| ${i + 1} | ${escapeMd(g?.name || '未知群')} | ${escapeMd(g?.community || '-')} | ${info.count} | ${info.senders.size} |`;
  405. }).join('\n');
  406. // 未覆盖关键词 TOP 30
  407. const trulyMissedTop = trulyMissed.slice(0, 30);
  408. const trulyMissedByCat: Record<string, KeywordInfo[]> = {};
  409. for (const kw of trulyMissedTop) {
  410. if (!trulyMissedByCat[kw.category]) trulyMissedByCat[kw.category] = [];
  411. trulyMissedByCat[kw.category].push(kw);
  412. }
  413. const trulyMissedSection = Object.entries(trulyMissedByCat).map(([cat, kws]) => {
  414. return `**${categoryCn(cat)}** (${kws.length}个): ${kws.map(k => {
  415. const s = k.severity === 'high' ? '🔴' : k.severity === 'medium' ? '🟡' : '🟢';
  416. return `${s}${k.word}`;
  417. }).join('、')}`;
  418. }).join('\n\n');
  419. const md = `# 🛡️ 拉迷家居 · 风险预警深度分析报告
  420. **审计期间**: 2026-06-15 ~ 2026-06-22(8 天)
  421. **生成时间**: ${genTime}
  422. **分析耗时**: ${(totalMs / 1000).toFixed(1)}s
  423. **AI 引擎**: ${AI_ENABLED ? `DeepSeek (${AI_MODEL})` : '未启用'}
  424. ---
  425. ## 一、数据概览
  426. | 指标 | 数值 |
  427. |------|------|
  428. | 文本消息总量 | ${allMessages.length.toLocaleString()} |
  429. | 活跃群数 | ${Object.keys(stats.byRoom).length} |
  430. | 参与人数 | ${stats.uniqueSenders} |
  431. | 已有风险事件 | ${riskEvents.length} |
  432. | 启用风险关键词 | ${keywords.length} |
  433. | 已触发预警的关键词 | ${missStats.alertedKeywords} |
  434. | 从未触发预警的关键词 | ${missStats.trulyMissedCount} |
  435. | **关键词覆盖率** | **${coverageRate}%** |
  436. | 漏报群数量 | ${missStats.missedRoomsCount} |
  437. | 🔴 高危漏报群 | ${missStats.highMissed} |
  438. | 🟡 中危漏报群 | ${missStats.mediumMissed} |
  439. | 🟢 低危漏报群 | ${missStats.lowMissed} |
  440. ---
  441. ## 二、消息活动分布
  442. ### 2.1 按天分布
  443. | 日期 | 消息数 |
  444. |------|--------|
  445. ${dayRows}
  446. ### 2.2 活跃群 TOP 15
  447. | 排名 | 群名称 | 小区 | 消息数 | 参与人数 |
  448. |------|--------|------|--------|----------|
  449. ${topRoomRows}
  450. ---
  451. ## 三、关键词有效性分析
  452. ### 3.1 各类别命中情况
  453. | 类别 | 总词数 | 命中词数 | 命中消息数 | 有效率 |
  454. |------|--------|----------|------------|--------|
  455. ${catRows}
  456. ### 3.2 完全未触发预警的关键词(TOP 30)
  457. ${trulyMissedSection}
  458. ---
  459. ## 四、已触发预警详情
  460. ${riskEvents.length > 0 ? `
  461. | 时间 | 群 | 关键词 | 严重度 | 状态 | 标题 |
  462. |------|-----|--------|--------|------|------|
  463. ${alertedRows}
  464. ` : '> ⚠️ 该期间无风险事件'}
  465. ---
  466. ## 五、漏报分析
  467. ### 5.1 漏报群详情
  468. | # | 群 | 未覆盖关键词 | 最高严重度 | 预警状态 | 消息样本 |
  469. |---|-----|-------------|------------|----------|----------|
  470. ${missedRows}
  471. ### 5.2 漏报原因分析
  472. #### 类型一:促销营销用词(占比最大)
  473. - **「定金」「订金」「优惠」「折扣」** 等关键词出现在大量 618 促销广播中
  474. - 这些词触发情景为拉迷员工在各业主群发布统一营销文案
  475. - AI 评审结论:正常促销,无真实风险
  476. - **建议**: 将此类词从预警列表移除或降级为"仅统计不预警"
  477. #### 类型二:正常业务沟通
  478. - **「色差」** 出现在"告别线上色差"的到店体验邀约中
  479. - **「垃圾」** 出现在询问垃圾桶尺寸的设计沟通中
  480. - **「宜家」** 出现在"宜家对杯"的游戏互动中
  481. - **建议**: 这些词在营销和日常沟通中高频出现,需更精准的上下文判断
  482. #### 类型三:真实质量风险(需关注)
  483. - **「脱胶」** 在拉迷VIP服务群中被提及,客户反映产品脱胶问题
  484. - 安装师傅在场并提出了修补方案,但此类问题应被预警
  485. - **建议**: 质量类关键词(脱胶、开裂、变形、发霉等)应独立为"即报即警"
  486. ---
  487. ## 六、AI 管理建议
  488. ${aiReport}
  489. ---
  490. ## 七、结论与行动项
  491. ### 立即执行(本周)
  492. 1. **清理促销类关键词**: 将"定金/订金/优惠/折扣/打折/砍价"标记为 "仅统计",不再触发预警
  493. 2. **质量关键词升级**: "脱胶/开裂/变形/发霉/装错/尺寸不对/色差(非营销语境)" 设为命中即报警
  494. 3. **竞品关键词独立统计**: 不触发预警,但每月统计竞品提及频次作为市场情报
  495. ### 短期优化(本月)
  496. 4. **聚合规则**: 同一群 1 小时内命中 ≥3 个不同关键词 → 自动升级预警
  497. 5. **频次阈值**: 关键词命中频次 TOP 10 的词自动进入月度复审
  498. 6. **盲区补充**: 增加"情绪检测"维度(感叹号密度、负面表情包、长语音消息密集度)
  499. ### 持续改进
  500. 7. **周审计**: 每周一运行本审计脚本,对比上周数据
  501. 8. **月度关键词大扫除**: 淘汰连续 30 天零命中的关键词
  502. 9. **预警闭环率**: 追踪 pending→resolved 的平均时长,目标 < 4 小时
  503. 10. **误报率监控**: 标记为 false_alarm 的事件占比应 < 30%
  504. ---
  505. ## 八、脚本使用说明
  506. ### 审计脚本清单
  507. | 脚本 | 用途 | 频率 |
  508. |------|------|------|
  509. | \`audit-risk-coverage.ts\` | 关键词覆盖审计 + HTML 报告 | 每周 |
  510. | \`audit-risk-deep-analysis.ts\` | 深度分析 + MD 报告 | 每月 |
  511. | \`backfill-member-nicknames.ts\` | 回填 GroupMember 昵称 | 按需 |
  512. ### 运行方式
  513. \`\`\`bash
  514. cd backend
  515. # 覆盖审计(含 AI 评审,约 30s)
  516. npx tsx scripts/audit-risk-coverage.ts
  517. # 深度分析(含 AI 管理建议,约 30s)
  518. npx tsx scripts/audit-risk-deep-analysis.ts
  519. \`\`\`
  520. ### 输出文件
  521. | 文件 | 说明 |
  522. |------|------|
  523. | \`audit-risk-YYYY-MM-DD.html\` | 可视化 HTML 报告(含消息样本和 AI 评审) |
  524. | \`audit-risk-deep-YYYY-MM-DD.md\` | 深度分析 MD 报告(含管理建议) |
  525. ---
  526. *报告由 audit-risk-deep-analysis.ts 自动生成*
  527. `;
  528. const outputPath = path.resolve(__dirname, 'audit-risk-deep-2026-06-15-22.md');
  529. fs.writeFileSync(outputPath, md, 'utf-8');
  530. console.log(`\n✅ 深度分析报告已生成: ${outputPath}`);
  531. console.log(` 文件大小: ${(Buffer.byteLength(md, 'utf-8') / 1024).toFixed(1)} KB`);
  532. console.log(` 总耗时: ${totalMs}ms\n`);
  533. // 控制台摘要
  534. console.log('═══════════════════════════════════════');
  535. console.log(' 关键发现');
  536. console.log('═══════════════════════════════════════');
  537. console.log(` 消息总量: ${allMessages.length} | 预警: ${riskEvents.length} | 关键词: ${keywords.length}`);
  538. console.log(` 覆盖率: ${coverageRate}% | 漏报群: ${missStats.missedRoomsCount}`);
  539. console.log(` 高危漏报: ${missStats.highMissed} | 中危: ${missStats.mediumMissed} | 低危: ${missStats.lowMissed}`);
  540. console.log(` 类别有效率排名: ${catEffectiveness.slice(0, 5).map(c => `${categoryCn(c.category)}(${c.effectiveness}%)`).join(' > ')}`);
  541. }
  542. /* ======================== 工具函数 ======================== */
  543. function categoryCn(cat: string): string {
  544. const map: Record<string, string> = {
  545. complaint: '投诉抱怨', quality: '质量工艺', sensitive: '敏感词',
  546. legal: '法律监管', competitor: '竞品对比', price: '价格讨论',
  547. negative: '负面情绪', installation: '安装施工',
  548. };
  549. return map[cat] || cat;
  550. }
  551. function escapeMd(s: string): string {
  552. return s.replace(/\|/g, '\\|').replace(/\n/g, ' ');
  553. }
  554. main().catch((err) => {
  555. console.error('[深度分析] 失败:', err);
  556. process.exit(1);
  557. });