audit-deep-analysis.ts 22 KB

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