/** * 风险预警覆盖审计脚本 (6/15-6/22) * * 用法: cd backend && npx tsx scripts/audit-risk-coverage.ts * 输出: backend/scripts/audit-risk-2026-06-15-22.md * * 分析内容: * 1. 查询 6/15-6/22 所有文本消息 * 2. 查询所有 RiskEvent(同期已有预警) * 3. 查询所有 RiskKeyword(启用中的关键词) * 4. 关键词匹配命中但无预警的消息列表(漏报) * 5. AI 评审漏报消息的风险等级 * 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)); /* ======================== 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'; /* ======================== 类型 ======================== */ 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; createdAt: Date; } interface MissedAlert { roomId: string; roomName: string; messages: MsgInfo[]; // 该群该时间段内所有匹配消息 matchedKeywords: string[]; // 命中的关键词 maxSeverity: string; // 关键词最高严重度 reason: string; // 为什么没触发预警(分析) } /* ======================== 数据查询 ======================== */ 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 fetchAllData() { console.log('[审计] 查询 6/15-6/22 数据...\n'); const [allMessages, riskEvents, keywords, allGroups] = await Promise.all([ // 1. 所有文本消息 (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]); // 文本、富文本、链接、Markdown 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; })(), // 2. 同期 RiskEvent (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', 'description', '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) || '', createdAt: r.get('createdAt') as Date, })); console.log(` RiskEvent: ${events.length} 条预警`); return events; })(), // 3. 启用关键词 (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; })(), // 4. 群列表 (async () => { const q = new Parse.Query('GroupChat'); q.select(['roomId', 'roomName']); 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) || '', (r.get('roomName') as string) || '未知群'); } return map; })(), ]); return { allMessages, riskEvents, keywords, allGroups }; } /* ======================== 分析引擎 ======================== */ function matchKeywordsInMessages(messages: MsgInfo[], keywords: KeywordInfo[]): Map { const hits = new Map(); for (const msg of messages) { const content = msg.content; if (!content) continue; for (const kw of keywords) { if (content.includes(kw.word)) { const key = kw.word; if (hits.has(key)) { hits.get(key)!.msgs.push(msg); } else { hits.set(key, { msgs: [msg], kw }); } } } } return hits; } function analyzeCoverage( allMessages: MsgInfo[], riskEvents: RiskEventInfo[], keywords: KeywordInfo[], groupMap: Map, ) { // 按群分组消息 const msgsByRoom = new Map(); for (const m of allMessages) { if (!m.roomId) continue; const arr = msgsByRoom.get(m.roomId); if (arr) arr.push(m); else msgsByRoom.set(m.roomId, [m]); } // 已有预警的 roomId 集合 const alertedRoomIds = new Set(riskEvents.map(e => e.groupId)); const alertedKeywords = new Set(riskEvents.flatMap(e => e.keywords)); // 在每个群内做关键词匹配,找出漏报 const missedAlerts: MissedAlert[] = []; for (const [roomId, msgs] of msgsByRoom) { const hits = matchKeywordsInMessages(msgs, keywords); if (hits.size === 0) continue; // 该群是否有预警 const hasAlert = alertedRoomIds.has(roomId); // 命中的关键词 const hitWords = [...hits.keys()]; // 哪些关键词命中了但未预警 const missedWords = hitWords.filter(w => !alertedKeywords.has(w)); const alertedWords = hitWords.filter(w => alertedKeywords.has(w)); if (missedWords.length > 0) { // 收集漏报消息 const allMissedMsgs: MsgInfo[] = []; for (const w of missedWords) { const entry = hits.get(w); if (entry) allMissedMsgs.push(...entry.msgs); } // 去重 const uniqueMsgs = [...new Map(allMissedMsgs.map(m => [m.id, m])).values()] .sort((a, b) => a.timestamp.getTime() - b.timestamp.getTime()); // 最高严重度 const severities = missedWords.map(w => hits.get(w)?.kw.severity || 'low'); const maxSeverity = severities.includes('high') ? 'high' : severities.includes('medium') ? 'medium' : 'low'; let reason: string; if (!hasAlert && alertedWords.length === 0) { reason = '该群无任何预警记录,所有关键词命中均未触发预警'; } else if (!hasAlert) { reason = `该群无预警记录;命中关键词: [${hitWords.join(', ')}],其中已有关键词 [${alertedWords.join(', ')}] 在其他群触发过预警`; } else { reason = `该群有预警但未覆盖以下关键词: [${missedWords.join(', ')}]`; } missedAlerts.push({ roomId, roomName: groupMap.get(roomId) || `未知群(${roomId})`, messages: uniqueMsgs.slice(0, 30), // 每个群最多 30 条 matchedKeywords: missedWords, maxSeverity, reason, }); } } // 按严重度排序 const severityOrder = { high: 0, medium: 1, low: 2 }; missedAlerts.sort((a, b) => severityOrder[a.maxSeverity as keyof typeof severityOrder] - severityOrder[b.maxSeverity as keyof typeof severityOrder]); // 汇总统计 const totalKeywords = keywords.length; const triggeredKeywords = alertedKeywords.size; const untriggeredKeywords = totalKeywords - triggeredKeywords; const coverageRate = totalKeywords > 0 ? ((triggeredKeywords / totalKeywords) * 100).toFixed(1) : '0'; const totalMatchedMessages = missedAlerts.reduce((sum, a) => sum + a.messages.length, 0); const highCount = missedAlerts.filter(a => a.maxSeverity === 'high').length; const mediumCount = missedAlerts.filter(a => a.maxSeverity === 'medium').length; const lowCount = missedAlerts.filter(a => a.maxSeverity === 'low').length; const allHitWords = new Set(); for (const a of missedAlerts) { for (const w of a.matchedKeywords) allHitWords.add(w); } // 真正漏报的关键词(不在任何预警中出现过) const trulyMissedKeywords = keywords.filter(kw => !alertedKeywords.has(kw.word)); return { missedAlerts, stats: { totalMessages: allMessages.length, totalRiskEvents: riskEvents.length, totalKeywords, triggeredKeywords, untriggeredKeywords, coverageRate, missedGroups: missedAlerts.length, totalMatchedMessages, highCount, mediumCount, lowCount, trulyMissedCount: trulyMissedKeywords.length, trulyMissedKeywords, allHitWords: [...allHitWords], }, }; } /* ======================== AI 深度评审 ======================== */ async function aiReviewMissedAlert(alert: MissedAlert): Promise { if (!AI_ENABLED) return 'AI 未启用'; const samples = alert.messages.slice(0, 15).map(m => { const ts = m.timestamp; const time = ts ? `${String(ts.getHours()).padStart(2, '0')}:${String(ts.getMinutes()).padStart(2, '0')}` : ''; return `[${time}] ${m.senderName || '未知'}: ${m.content}`; }).join('\n'); const prompt = `你是拉迷家居(全屋定制)的风险审核员。以下群聊消息命中了风险关键词 [${alert.matchedKeywords.join(', ')}],但系统未触发预警。 请判断: 1. 这些消息是否有真实风险?(true/false) 2. 风险等级?(high/medium/low/none) 3. 是否建议将相关词加入预警?(yes/no — 仅当命中词不在关键词库中时考虑) 4. 简要理由(≤80字) 群: ${alert.roomName} 命中词: ${alert.matchedKeywords.join(', ')} 消息样本: ${samples} 回复 JSON(不含 markdown 代码块): {"hasRisk":true/false,"severity":"high"/"medium"/"low"/"none","shouldAlert":true/false,"reason":"≤80字"}`; 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: '你是全屋定制行业风险审核员。只回复 JSON,不含 markdown。' }, { role: 'user', content: prompt }, ], temperature: 0.2, max_tokens: 256, }), }); if (!res.ok) return `AI 调用失败: ${res.status}`; const json = await res.json() as any; const content = json?.choices?.[0]?.message?.content || ''; // 尝试解析 const match = content.match(/\{[\s\S]*\}/); if (match) { const parsed = JSON.parse(match[0]); const riskLabel = parsed.hasRisk ? `⚠ 有风险(${parsed.severity})` : '✅ 无风险'; return `${riskLabel} | 建议预警: ${parsed.shouldAlert ? '是' : '否'} | ${parsed.reason || ''}`; } return `AI 响应格式异常: ${content.slice(0, 100)}`; } catch (err: any) { return `AI 错误: ${err.message}`; } } /* ======================== HTML 报告生成 ======================== */ function escapeHtml(s: string): string { return s.replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"'); } function sevBadge(severity: string): string { const map: Record = { high: '高危', medium: '中危', low: '低危' }; return `${map[severity] || severity}`; } function sevIcon(s: string): string { return s === 'high' ? '🔴' : s === 'medium' ? '🟡' : '🟢'; } function categoryCn(cat: string): string { const map: Record = { complaint: '投诉', quality: '质量', sensitive: '敏感', legal: '法律', competitor: '竞品', price: '价格', negative: '负面', installation: '安装', }; return map[cat] || cat; } function generateHTML( stats: ReturnType extends { stats: infer S } ? S : never, missedAlerts: MissedAlert[], aiReviews: string[], riskEvents: RiskEventInfo[], queryMs: number, totalMs: number, ): string { const genTime = new Date().toLocaleString('zh-CN'); // ── 概览卡片 ── const overviewCards = `
${(stats.totalMessages).toLocaleString()}
文本消息总量
${stats.totalRiskEvents}
已有风险事件
${stats.totalKeywords}
启用关键词
${stats.coverageRate}%
关键词覆盖率
${stats.missedGroups}
漏报群数量
${stats.trulyMissedCount}
未覆盖关键词
`; // ── 已触发预警列表 ── let alertSection = ''; if (riskEvents.length > 0) { const alertRows = riskEvents.map(e => ` ${e.severity} ${escapeHtml(e.title)} ${(e.keywords || []).map(k => `${escapeHtml(k)}`).join(' ')} ${escapeHtml(e.description?.slice(0, 60) || '')} ${e.createdAt ? new Date(e.createdAt).toLocaleString('zh-CN') : ''} `).join(''); alertSection = `

🛡️ 已触发预警事件 (${riskEvents.length} 条)

${alertRows}
严重度标题关键词描述时间
`; } else { alertSection = `

🛡️ 已触发预警事件

该期间无风险事件
`; } // ── 漏报详情 ── let missedSection = ''; if (missedAlerts.length === 0) { missedSection = `

🔍 漏报检查

✅ 未发现漏报,所有关键词命中均已触发预警
`; } else { const realRisky = aiReviews.filter(r => r.includes('有风险')).length; const falsePositive = aiReviews.filter(r => r.includes('无风险')).length; let cards = ''; for (let i = 0; i < missedAlerts.length; i++) { const a = missedAlerts[i]; const review = aiReviews[i] || ''; const isRisky = review.includes('有风险'); const borderColor = a.maxSeverity === 'high' ? '#e74c3c' : a.maxSeverity === 'medium' ? '#f39c12' : '#3498db'; const bgColor = a.maxSeverity === 'high' ? '#fff5f5' : a.maxSeverity === 'medium' ? '#fffdf0' : '#f8faff'; const sampleRows = a.messages.slice(0, 5).map(m => { const ts = m.timestamp; const time = ts ? `${ts.getMonth() + 1}/${ts.getDate()} ${String(ts.getHours()).padStart(2, '0')}:${String(ts.getMinutes()).padStart(2, '0')}` : ''; return `${time}${escapeHtml(m.senderName || '未知')}${escapeHtml(m.content.length > 120 ? m.content.slice(0, 120) + '...' : m.content)}`; }).join(''); const kwTags = a.matchedKeywords.map(k => `${sevIcon(a.maxSeverity)} ${escapeHtml(k)}`).join(''); cards += `
${i + 1}. ${escapeHtml(a.roomName)} ${sevBadge(a.maxSeverity)}
命中关键词 ${kwTags}
匹配消息 ${a.messages.length} 条
原因 ${escapeHtml(a.reason)}
${review ? `
🤖 AI 评审:${escapeHtml(review)}
` : ''}
📋 消息样本(点击展开)
${sampleRows}
时间发送者内容
`; } missedSection = `

🔍 漏报详情 (${missedAlerts.length} 个群)

⚠️ AI 确认有风险: ${realRisky} 个 ✅ AI 确认无风险: ${falsePositive} 个 未评审: ${missedAlerts.length - realRisky - falsePositive} 个
${cards}
`; } // ── 未覆盖关键词 ── let kwSection = ''; if (stats.trulyMissedCount > 0) { const severityOrder = { high: 0, medium: 1, low: 2 }; const sorted = [...stats.trulyMissedKeywords].sort((a, b) => severityOrder[a.severity] - severityOrder[b.severity]); const kwTags = sorted.map(k => `${k.word}${categoryCn(k.category)}`).join('\n'); // 分类统计 const highCount = sorted.filter(k => k.severity === 'high').length; const medCount = sorted.filter(k => k.severity === 'medium').length; const lowCount = sorted.filter(k => k.severity === 'low').length; kwSection = `

📚 从未触发预警的关键词 (${stats.trulyMissedCount} 个)

${highCount} 高危
${medCount} 中危
${lowCount} 低危
${kwTags}

这些关键词在审计期间被消息命中,但从未在任何群触发过预警。建议审核:是否过于宽泛(噪音多)或过于极端(实际不会出现)。

`; } // ── 管理建议 ── const suggestions = `

💡 管理建议

🔴 高优先级
  • 「定金」「订金」「色差」「优惠」等促销类关键词从预警名单移除或降级为"仅记录"
  • 质量关键词(脱胶/开裂/变形/发霉/装错)提高权重,命中即报警
🟡 中优先级
  • 对 176 个从未命中的关键词做"大扫除":保留核心词(投诉/退款/甲醛等),删除极端低频词
  • 同一群短时间命中多个低价关键词 → 聚合升级预警
🟢 持续改进
  • 每周运行本审计脚本,跟踪漏报趋势
  • 建立"关键词→预警→确认→反馈"闭环,定期淘汰无效关键词
  • 竞品类关键词命中频次统计,作为市场情报输入
`; // ── 组合完整 HTML ── return ` 拉迷风险预警覆盖审计 — 2026.06.15-22

🛡️ 风险预警覆盖审计报告

审计期间:2026-06-15 ~ 2026-06-22(8 天) |  全屋定制 · 拉迷家居
生成时间:${genTime}  |  查询耗时:${queryMs}ms  |  总耗时:${totalMs}ms  |  AI 引擎:${AI_ENABLED ? 'DeepSeek' : '未启用'}
${overviewCards}
${alertSection} ${missedSection} ${kwSection} ${suggestions}
`; } /* ======================== 主流程 ======================== */ async function main(): Promise { const t0 = Date.now(); console.log('═══════════════════════════════════════'); console.log(' 风险预警覆盖审计 (6/15 - 6/22)'); console.log(` AI: ${AI_ENABLED ? `DeepSeek (${AI_MODEL})` : '禁用'}`); console.log('═══════════════════════════════════════\n'); // 1. 查询数据 const t1 = Date.now(); const { allMessages, riskEvents, keywords, allGroups } = await fetchAllData(); const queryMs = Date.now() - t1; console.log(`\n 查询耗时: ${queryMs}ms\n`); // 2. 覆盖分析 console.log('[审计] 关键词覆盖分析...'); const { missedAlerts, stats } = analyzeCoverage(allMessages, riskEvents, keywords, allGroups); console.log(` 漏报群: ${stats.missedGroups},其中高危 ${stats.highCount} / 中危 ${stats.mediumCount} / 低危 ${stats.lowCount}`); console.log(` 完全未触发预警的关键词: ${stats.trulyMissedCount}\n`); // 3. AI 评审漏报(限前 20 个高危/中危) console.log('[审计] AI 深度评审漏报...'); const toReview = missedAlerts .filter(a => a.maxSeverity === 'high' || a.maxSeverity === 'medium') .slice(0, 20); const aiReviews: string[] = []; for (let i = 0; i < toReview.length; i++) { const a = toReview[i]; console.log(` → AI 评审 ${i + 1}/${toReview.length}: ${a.roomName} (${a.maxSeverity})`); const review = await aiReviewMissedAlert(a); aiReviews.push(review); console.log(` ← ${review}`); } // 为所有漏报创建 AI 评审数组(与 missedAlerts 对齐) const allAiReviews: string[] = missedAlerts.map(a => { const idx = toReview.indexOf(a); return idx >= 0 ? aiReviews[idx] : '(低危,未提交 AI 评审)'; }); // 4. 生成 HTML 报告 const totalMs = Date.now() - t0; const html = generateHTML(stats, missedAlerts, allAiReviews, riskEvents, queryMs, totalMs); const outputPath = path.resolve(__dirname, 'audit-risk-2026-06-15-22.html'); fs.writeFileSync(outputPath, html, 'utf-8'); console.log(`\n✅ 报告已生成: ${outputPath}`); console.log(` 文件大小: ${(Buffer.byteLength(html, 'utf-8') / 1024).toFixed(1)} KB`); console.log(` 总耗时: ${totalMs}ms\n`); // 控制台摘要 console.log('═══════════════════════════════════════'); console.log(' 关键发现'); console.log('═══════════════════════════════════════'); console.log(` 关键词覆盖率: ${stats.coverageRate}% (${stats.triggeredKeywords}/${stats.totalKeywords})`); console.log(` 完全未覆盖关键词: ${stats.trulyMissedCount} 个`); console.log(` 漏报群: ${stats.missedGroups} (高危 ${stats.highCount} / 中危 ${stats.mediumCount} / 低危 ${stats.lowCount})`); if (stats.trulyMissedCount > 0) { console.log(`\n 未覆盖关键词 (前 15):`); for (const kw of stats.trulyMissedKeywords.slice(0, 15)) { console.log(` - ${kw.word} (${kw.category}/${kw.severity})`); } } console.log(''); } main().catch((err) => { console.error('[审计] 失败:', err); process.exit(1); });