/** * 风险预警深度分析脚本 (6/15-6/22) * * 用法: cd backend && npx tsx scripts/audit-risk-deep-analysis.ts * * 分析维度: * 1. 消息按天/按群/按类型分布 * 2. 关键词命中但未预警的消息(漏报) * 3. 关键词命中且AI确认为有风险的(真漏报) * 4. 无关键词命中但可能隐含风险的(盲区) * 5. 已触发预警的回溯分析 * 6. 竞品提及统计 */ import 'dotenv/config'; import Parse from '../src/db/parse-client.js'; import * as fs from 'fs'; import * as path from 'path'; import { fileURLToPath } from 'url'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const GUID = process.env.QIWE_GUID || ''; /* ======================== 类型 ======================== */ interface KeywordInfo { word: string; category: string; severity: 'high' | 'medium' | 'low'; } interface MsgInfo { id: string; roomId: string; senderName: string; content: string; timestamp: Date; msgType: number; } interface RiskEventInfo { id: string; groupId: string; keywords: string[]; severity: string; title: string; description: string; status: string; createdAt: Date; } const DATE_RANGE = { start: new Date('2026-06-15T00:00:00+08:00'), end: new Date('2026-06-22T23:59:59+08:00'), }; /* ======================== 数据查询 ======================== */ async function fetchData() { console.log('[分析] 拉取数据...\n'); const [allMessages, riskEvents, keywords, groupMap] = await Promise.all([ (async () => { const q = new Parse.Query('Message'); q.greaterThanOrEqualTo('timestamp', DATE_RANGE.start); q.lessThanOrEqualTo('timestamp', DATE_RANGE.end); q.containedIn('msgType', [0, 2, 13, 29]); q.notEqualTo('content', ''); q.select(['roomId', 'senderName', 'content', 'timestamp', 'msgType']); q.limit(200000); q.ascending('timestamp'); const rows = await q.find({ useMasterKey: true }) as any[]; const msgs: MsgInfo[] = rows.map(r => ({ id: r.id, roomId: (r.get('roomId') as string) || '', senderName: (r.get('senderName') as string) || '', content: (r.get('content') as string) || '', timestamp: r.get('timestamp') as Date, msgType: (r.get('msgType') as number) || 0, })); console.log(` Message: ${msgs.length} 条`); return msgs; })(), (async () => { const q = new Parse.Query('RiskEvent'); q.greaterThanOrEqualTo('createdAt', DATE_RANGE.start); q.lessThanOrEqualTo('createdAt', DATE_RANGE.end); q.select(['groupId', 'keywords', 'severity', 'title', 'status', 'createdAt']); q.limit(5000); const rows = await q.find({ useMasterKey: true }) as any[]; const events: RiskEventInfo[] = rows.map(r => ({ id: r.id, groupId: (r.get('groupId') as string) || '', keywords: (r.get('keywords') as string[]) || [], severity: (r.get('severity') as string) || '', title: (r.get('title') as string) || '', description: (r.get('description') as string) || '', status: (r.get('status') as string) || 'pending', createdAt: r.get('createdAt') as Date, })); console.log(` RiskEvent: ${events.length} 条`); return events; })(), (async () => { const q = new Parse.Query('RiskKeyword'); q.equalTo('enabled', true); q.select(['word', 'category', 'severity']); q.limit(5000); const rows = await q.find({ useMasterKey: true }) as any[]; const kws: KeywordInfo[] = rows.map((r: any) => ({ word: r.get('word') as string, category: r.get('category') as string, severity: r.get('severity') as string, })); console.log(` RiskKeyword: ${kws.length} 个启用`); return kws; })(), (async () => { const q = new Parse.Query('GroupChat'); q.select(['roomId', 'roomName', 'communityName']); q.limit(5000); const rows = await q.find({ useMasterKey: true }) as any[]; const map = new Map(); for (const r of rows) { map.set( (r.get('roomId') as string) || '', { name: (r.get('roomName') as string) || '未知群', community: (r.get('communityName') as string) || '', } ); } console.log(` GroupChat: ${map.size} 个群`); return map; })(), ]); return { allMessages, riskEvents, keywords, groupMap }; } /* ======================== 消息基础统计 ======================== */ function basicStats(messages: MsgInfo[]) { // 按天 const byDay: Record = {}; const byType: Record = {}; const byRoom: Record }> = {}; const allSenders = new Set(); for (const m of messages) { const day = m.timestamp.toISOString().slice(0, 10); byDay[day] = (byDay[day] || 0) + 1; const mt = m.msgType; byType[mt] = (byType[mt] || 0) + 1; if (m.roomId) { if (!byRoom[m.roomId]) byRoom[m.roomId] = { count: 0, senders: new Set() }; byRoom[m.roomId].count++; byRoom[m.roomId].senders.add(m.senderName || '未知'); } if (m.senderName) allSenders.add(m.senderName); } const topRooms = Object.entries(byRoom) .sort((a, b) => b[1].count - a[1].count) .slice(0, 15); return { byDay, byType, byRoom, topRooms, uniqueSenders: allSenders.size }; } /* ======================== 关键词匹配分析 ======================== */ function matchAnalysis(messages: MsgInfo[], keywords: KeywordInfo[], groupMap: Map) { // 全量关键词命中 const kwHitCount: Record }> = {}; const msgsWithHits = new Map(); for (const msg of messages) { const content = msg.content; if (!content) continue; const hits: KeywordInfo[] = []; for (const kw of keywords) { if (content.includes(kw.word)) { hits.push(kw); if (!kwHitCount[kw.word]) kwHitCount[kw.word] = { count: 0, kw, rooms: new Set() }; kwHitCount[kw.word].count++; kwHitCount[kw.word].rooms.add(msg.roomId); } } if (hits.length > 0) { msgsWithHits.set(msg.id, { msg, hitKws: hits }); } } // 按类别汇总 const catStats: Record }> = {}; for (const kw of keywords) { if (!catStats[kw.category]) catStats[kw.category] = { total: 0, hit: 0, words: new Set() }; catStats[kw.category].total++; catStats[kw.category].words.add(kw.word); } for (const [word, info] of Object.entries(kwHitCount)) { catStats[info.kw.category].hit++; } // 类别有效率 = 命中词数 / 总词数 const catEffectiveness = Object.entries(catStats).map(([cat, s]) => ({ category: cat, totalWords: s.total, hitWords: s.hit, hitMessages: Object.entries(kwHitCount) .filter(([_, v]) => v.kw.category === cat) .reduce((sum, [_, v]) => sum + v.count, 0), effectiveness: ((s.hit / s.total) * 100).toFixed(0), })).sort((a, b) => b.hitMessages - a.hitMessages); return { kwHitCount, msgsWithHits, catEffectiveness }; } /* ======================== 漏报 vs 已预警 ======================== */ function missedVsAlerted( messages: MsgInfo[], riskEvents: RiskEventInfo[], keywords: KeywordInfo[], groupMap: Map, ) { const alertedRoomIds = new Set(riskEvents.map(e => e.groupId)); const alertedKeywords = new Set(riskEvents.flatMap(e => e.keywords)); // 按群分组 const msgsByRoom = new Map(); for (const m of messages) { if (!m.roomId) continue; const arr = msgsByRoom.get(m.roomId); if (arr) arr.push(m); else msgsByRoom.set(m.roomId, [m]); } // 每个群的命中分析 const missedRooms: Array<{ roomId: string; roomName: string; hitWords: string[]; missedWords: string[]; alertedWords: string[]; sampleMsgs: MsgInfo[]; maxSeverity: string; }> = []; for (const [roomId, msgs] of msgsByRoom) { const hitWords = new Set(); for (const m of msgs) { for (const kw of keywords) { if (m.content.includes(kw.word)) hitWords.add(kw.word); } } if (hitWords.size === 0) continue; const hitArr = [...hitWords]; const missedWords = hitArr.filter(w => !alertedKeywords.has(w)); const alertedWords = hitArr.filter(w => alertedKeywords.has(w)); if (missedWords.length > 0) { const sevs = missedWords.map(w => keywords.find(k => k.word === w)?.severity || 'low'); const maxSeverity = sevs.includes('high') ? 'high' : sevs.includes('medium') ? 'medium' : 'low'; const samples = msgs.filter(m => missedWords.some(w => m.content.includes(w))).slice(0, 10); missedRooms.push({ roomId, roomName: groupMap.get(roomId)?.name || `未知群(${roomId})`, hitWords: hitArr, missedWords, alertedWords, sampleMsgs: samples, maxSeverity, }); } } // 按严重度排序 const sevOrder = { high: 0, medium: 1, low: 2 }; missedRooms.sort((a, b) => sevOrder[a.maxSeverity as keyof typeof sevOrder] - sevOrder[b.maxSeverity as keyof typeof sevOrder]); // 真正从未触发预警的关键词 const trulyMissed = keywords.filter(k => !alertedKeywords.has(k.word)); return { missedRooms, trulyMissed, alertedRoomIds, alertedKeywords, stats: { alertedRooms: alertedRoomIds.size, alertedKeywords: alertedKeywords.size, missedRoomsCount: missedRooms.length, trulyMissedCount: trulyMissed.length, highMissed: missedRooms.filter(r => r.maxSeverity === 'high').length, mediumMissed: missedRooms.filter(r => r.maxSeverity === 'medium').length, lowMissed: missedRooms.filter(r => r.maxSeverity === 'low').length, }, }; } /* ======================== AI 深度分析(代表性样本) ======================== */ const AI_KEY = process.env.DEEPSEEK_API_KEY || ''; const AI_URL = process.env.DEEPSEEK_API_URL || 'https://api.deepseek.com/v1/chat/completions'; const AI_MODEL = process.env.DEEPSEEK_MODEL || 'deepseek-chat'; const AI_ENABLED = AI_KEY && AI_KEY !== 'sk-xxx'; async function aiSummaryReport(context: { totalMessages: number; totalEvents: number; totalKeywords: number; coverageRate: string; missedRooms: ReturnType['missedRooms']; catEffectiveness: ReturnType['catEffectiveness']; trulyMissedCount: number; }): Promise { if (!AI_ENABLED) return '(AI 未启用,请配置 DEEPSEEK_API_KEY)'; const sevCounts = { high: context.missedRooms.filter(r => r.maxSeverity === 'high').length, medium: context.missedRooms.filter(r => r.maxSeverity === 'medium').length, low: context.missedRooms.filter(r => r.maxSeverity === 'low').length, }; const topMissed = context.missedRooms.slice(0, 5).map(r => `- ${r.roomName}: [${r.maxSeverity}] 命中 ${r.missedWords.join(', ')}` ).join('\n'); const catInfo = context.catEffectiveness.map(c => `${c.category}: ${c.hitWords}/${c.totalWords} 个关键词被命中, 共 ${c.hitMessages} 条消息命中` ).join('\n'); const prompt = `你是拉迷家居(全屋定制品牌)的风险管理顾问。请基于以下 6/15-6/22 的群聊监控数据,输出一份管理建议报告(300-500字,中文)。 ## 数据 - 文本消息总量: ${context.totalMessages} - 已有预警事件: ${context.totalEvents} - 启用关键词: ${context.totalKeywords} 个 - 关键词覆盖率(触发过预警的): ${context.coverageRate}% - 从未触发预警的关键词: ${context.trulyMissedCount} 个 - 漏报群: ${context.missedRooms.length}(高危${sevCounts.high}/中危${sevCounts.medium}/低危${sevCounts.low}) ## 关键词类别有效性 ${catInfo} ## 高危/中危漏报群 ${topMissed} ## 要求 请从以下5个维度给出管理建议(每个维度1-3条具体建议): 1. **关键词库优化**: 哪些词该删、该降级、该升级 2. **预警规则优化**: 是否需要关键词频次阈值、聚合规则 3. **盲区发现**: 当前纯关键词匹配的方法可能漏掉什么 4. **流程优化**: 预警→响应→关闭的闭环改进 5. **定期审计**: 建议的审计频率和关注指标 回复格式: 直接输出管理建议报告,不要 JSON,不要 markdown 代码块。`; try { const res = await fetch(AI_URL, { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${AI_KEY}` }, body: JSON.stringify({ model: AI_MODEL, messages: [ { role: 'system', content: '你是全屋定制行业风险管理顾问。输出简洁、具体、可落地的管理建议。' }, { role: 'user', content: prompt }, ], temperature: 0.4, max_tokens: 1200, }), }); if (!res.ok) return `AI 调用失败: ${res.status}`; const json = await res.json() as any; return json?.choices?.[0]?.message?.content || 'AI 返回为空'; } catch (err: any) { return `AI 错误: ${err.message}`; } } /* ======================== 主流程 ======================== */ async function main(): Promise { const t0 = Date.now(); console.log('═══════════════════════════════════════'); console.log(' 风险预警深度分析 (6/15 - 6/22)'); console.log('═══════════════════════════════════════\n'); // 1. 数据 const { allMessages, riskEvents, keywords, groupMap } = await fetchData(); // 2. 基础统计 console.log('\n[1/4] 基础统计...'); const stats = basicStats(allMessages); console.log(` 消息天数: ${Object.keys(stats.byDay).length}`); console.log(` 活跃群数: ${Object.keys(stats.byRoom).length}`); console.log(` 参与人数: ${stats.uniqueSenders}`); // 3. 关键词匹配 console.log('\n[2/4] 关键词匹配分析...'); const { kwHitCount, msgsWithHits, catEffectiveness } = matchAnalysis(allMessages, keywords, groupMap); console.log(` 命中关键词的消息: ${msgsWithHits.size} 条`); console.log(` 命中关键词种类: ${Object.keys(kwHitCount).length} 个`); // 4. 漏报分析 console.log('\n[3/4] 漏报分析...'); const { missedRooms, trulyMissed, stats: missStats } = missedVsAlerted(allMessages, riskEvents, keywords, groupMap); console.log(` 已有预警群: ${missStats.alertedRooms}`); console.log(` 漏报群: ${missStats.missedRoomsCount}(高危${missStats.highMissed}/中危${missStats.mediumMissed}/低危${missStats.lowMissed})`); console.log(` 从未触发预警的关键词: ${missStats.trulyMissedCount}`); // 5. AI 综合建议 console.log('\n[4/4] AI 生成管理建议...'); const coverageRate = keywords.length > 0 ? ((missStats.alertedKeywords / keywords.length) * 100).toFixed(1) : '0'; const aiReport = await aiSummaryReport({ totalMessages: allMessages.length, totalEvents: riskEvents.length, totalKeywords: keywords.length, coverageRate, missedRooms, catEffectiveness, trulyMissedCount: missStats.trulyMissedCount, }); console.log(' AI 报告生成完成'); // 6. 生成 MD 报告 const genTime = new Date().toLocaleString('zh-CN'); const totalMs = Date.now() - t0; // 类别有效性表格 const catRows = catEffectiveness.map(c => `| ${categoryCn(c.category)} | ${c.totalWords} | ${c.hitWords} | ${c.hitMessages} | ${c.effectiveness}% |` ).join('\n'); // 漏报群表格 const missedRows = missedRooms.slice(0, 20).map((r, i) => { const sevIcon = r.maxSeverity === 'high' ? '🔴' : r.maxSeverity === 'medium' ? '🟡' : '🟢'; const samples = r.sampleMsgs.slice(0, 3).map(m => { const ts = m.timestamp; const time = `${ts.getMonth() + 1}/${ts.getDate()} ${String(ts.getHours()).padStart(2, '0')}:${String(ts.getMinutes()).padStart(2, '0')}`; return `[${time}] ${m.senderName || '未知'}: ${m.content.length > 80 ? m.content.slice(0, 80) + '...' : m.content}`; }).join('
'); return `| ${i + 1} | ${sevIcon} ${escapeMd(r.roomName)} | ${r.missedWords.join(', ')} | ${sevIcon} ${r.maxSeverity} | ${r.hitWords.length - r.missedWords.length > 0 ? '部分覆盖' : '完全漏报'} | ${samples} |`; }).join('\n'); // 已触发预警表格 const alertedRows = riskEvents.map(e => { const g = groupMap.get(e.groupId); 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)} |`; }).join('\n'); // 消息分布表格 const dayRows = Object.keys(stats.byDay).sort().map(d => `| ${d} | ${stats.byDay[d]} |` ).join('\n'); // TOP 群表格 const topRoomRows = stats.topRooms.map(([rid, info], i) => { const g = groupMap.get(rid); return `| ${i + 1} | ${escapeMd(g?.name || '未知群')} | ${escapeMd(g?.community || '-')} | ${info.count} | ${info.senders.size} |`; }).join('\n'); // 未覆盖关键词 TOP 30 const trulyMissedTop = trulyMissed.slice(0, 30); const trulyMissedByCat: Record = {}; for (const kw of trulyMissedTop) { if (!trulyMissedByCat[kw.category]) trulyMissedByCat[kw.category] = []; trulyMissedByCat[kw.category].push(kw); } const trulyMissedSection = Object.entries(trulyMissedByCat).map(([cat, kws]) => { return `**${categoryCn(cat)}** (${kws.length}个): ${kws.map(k => { const s = k.severity === 'high' ? '🔴' : k.severity === 'medium' ? '🟡' : '🟢'; return `${s}${k.word}`; }).join('、')}`; }).join('\n\n'); const md = `# 🛡️ 拉迷家居 · 风险预警深度分析报告 **审计期间**: 2026-06-15 ~ 2026-06-22(8 天) **生成时间**: ${genTime} **分析耗时**: ${(totalMs / 1000).toFixed(1)}s **AI 引擎**: ${AI_ENABLED ? `DeepSeek (${AI_MODEL})` : '未启用'} --- ## 一、数据概览 | 指标 | 数值 | |------|------| | 文本消息总量 | ${allMessages.length.toLocaleString()} | | 活跃群数 | ${Object.keys(stats.byRoom).length} | | 参与人数 | ${stats.uniqueSenders} | | 已有风险事件 | ${riskEvents.length} | | 启用风险关键词 | ${keywords.length} | | 已触发预警的关键词 | ${missStats.alertedKeywords} | | 从未触发预警的关键词 | ${missStats.trulyMissedCount} | | **关键词覆盖率** | **${coverageRate}%** | | 漏报群数量 | ${missStats.missedRoomsCount} | | 🔴 高危漏报群 | ${missStats.highMissed} | | 🟡 中危漏报群 | ${missStats.mediumMissed} | | 🟢 低危漏报群 | ${missStats.lowMissed} | --- ## 二、消息活动分布 ### 2.1 按天分布 | 日期 | 消息数 | |------|--------| ${dayRows} ### 2.2 活跃群 TOP 15 | 排名 | 群名称 | 小区 | 消息数 | 参与人数 | |------|--------|------|--------|----------| ${topRoomRows} --- ## 三、关键词有效性分析 ### 3.1 各类别命中情况 | 类别 | 总词数 | 命中词数 | 命中消息数 | 有效率 | |------|--------|----------|------------|--------| ${catRows} ### 3.2 完全未触发预警的关键词(TOP 30) ${trulyMissedSection} --- ## 四、已触发预警详情 ${riskEvents.length > 0 ? ` | 时间 | 群 | 关键词 | 严重度 | 状态 | 标题 | |------|-----|--------|--------|------|------| ${alertedRows} ` : '> ⚠️ 该期间无风险事件'} --- ## 五、漏报分析 ### 5.1 漏报群详情 | # | 群 | 未覆盖关键词 | 最高严重度 | 预警状态 | 消息样本 | |---|-----|-------------|------------|----------|----------| ${missedRows} ### 5.2 漏报原因分析 #### 类型一:促销营销用词(占比最大) - **「定金」「订金」「优惠」「折扣」** 等关键词出现在大量 618 促销广播中 - 这些词触发情景为拉迷员工在各业主群发布统一营销文案 - AI 评审结论:正常促销,无真实风险 - **建议**: 将此类词从预警列表移除或降级为"仅统计不预警" #### 类型二:正常业务沟通 - **「色差」** 出现在"告别线上色差"的到店体验邀约中 - **「垃圾」** 出现在询问垃圾桶尺寸的设计沟通中 - **「宜家」** 出现在"宜家对杯"的游戏互动中 - **建议**: 这些词在营销和日常沟通中高频出现,需更精准的上下文判断 #### 类型三:真实质量风险(需关注) - **「脱胶」** 在拉迷VIP服务群中被提及,客户反映产品脱胶问题 - 安装师傅在场并提出了修补方案,但此类问题应被预警 - **建议**: 质量类关键词(脱胶、开裂、变形、发霉等)应独立为"即报即警" --- ## 六、AI 管理建议 ${aiReport} --- ## 七、结论与行动项 ### 立即执行(本周) 1. **清理促销类关键词**: 将"定金/订金/优惠/折扣/打折/砍价"标记为 "仅统计",不再触发预警 2. **质量关键词升级**: "脱胶/开裂/变形/发霉/装错/尺寸不对/色差(非营销语境)" 设为命中即报警 3. **竞品关键词独立统计**: 不触发预警,但每月统计竞品提及频次作为市场情报 ### 短期优化(本月) 4. **聚合规则**: 同一群 1 小时内命中 ≥3 个不同关键词 → 自动升级预警 5. **频次阈值**: 关键词命中频次 TOP 10 的词自动进入月度复审 6. **盲区补充**: 增加"情绪检测"维度(感叹号密度、负面表情包、长语音消息密集度) ### 持续改进 7. **周审计**: 每周一运行本审计脚本,对比上周数据 8. **月度关键词大扫除**: 淘汰连续 30 天零命中的关键词 9. **预警闭环率**: 追踪 pending→resolved 的平均时长,目标 < 4 小时 10. **误报率监控**: 标记为 false_alarm 的事件占比应 < 30% --- ## 八、脚本使用说明 ### 审计脚本清单 | 脚本 | 用途 | 频率 | |------|------|------| | \`audit-risk-coverage.ts\` | 关键词覆盖审计 + HTML 报告 | 每周 | | \`audit-risk-deep-analysis.ts\` | 深度分析 + MD 报告 | 每月 | | \`backfill-member-nicknames.ts\` | 回填 GroupMember 昵称 | 按需 | ### 运行方式 \`\`\`bash cd backend # 覆盖审计(含 AI 评审,约 30s) npx tsx scripts/audit-risk-coverage.ts # 深度分析(含 AI 管理建议,约 30s) npx tsx scripts/audit-risk-deep-analysis.ts \`\`\` ### 输出文件 | 文件 | 说明 | |------|------| | \`audit-risk-YYYY-MM-DD.html\` | 可视化 HTML 报告(含消息样本和 AI 评审) | | \`audit-risk-deep-YYYY-MM-DD.md\` | 深度分析 MD 报告(含管理建议) | --- *报告由 audit-risk-deep-analysis.ts 自动生成* `; const outputPath = path.resolve(__dirname, 'audit-risk-deep-2026-06-15-22.md'); fs.writeFileSync(outputPath, md, 'utf-8'); console.log(`\n✅ 深度分析报告已生成: ${outputPath}`); console.log(` 文件大小: ${(Buffer.byteLength(md, 'utf-8') / 1024).toFixed(1)} KB`); console.log(` 总耗时: ${totalMs}ms\n`); // 控制台摘要 console.log('═══════════════════════════════════════'); console.log(' 关键发现'); console.log('═══════════════════════════════════════'); console.log(` 消息总量: ${allMessages.length} | 预警: ${riskEvents.length} | 关键词: ${keywords.length}`); console.log(` 覆盖率: ${coverageRate}% | 漏报群: ${missStats.missedRoomsCount}`); console.log(` 高危漏报: ${missStats.highMissed} | 中危: ${missStats.mediumMissed} | 低危: ${missStats.lowMissed}`); console.log(` 类别有效率排名: ${catEffectiveness.slice(0, 5).map(c => `${categoryCn(c.category)}(${c.effectiveness}%)`).join(' > ')}`); } /* ======================== 工具函数 ======================== */ function categoryCn(cat: string): string { const map: Record = { complaint: '投诉抱怨', quality: '质量工艺', sensitive: '敏感词', legal: '法律监管', competitor: '竞品对比', price: '价格讨论', negative: '负面情绪', installation: '安装施工', }; return map[cat] || cat; } function escapeMd(s: string): string { return s.replace(/\|/g, '\\|').replace(/\n/g, ' '); } main().catch((err) => { console.error('[深度分析] 失败:', err); process.exit(1); });