/** * 风险预警深度分析脚本 * * 用法: cp .env.example .env → 编辑 .env → npx tsx audit-deep-analysis.ts * 输出: audit-deep-YYYY-MM-DD.md(Markdown 格式) * * 分析维度: * 1. 消息按天/按群/按类型分布 * 2. 关键词类别有效性排名 * 3. 漏报三分类(促销营销/正常业务/真实风险) * 4. 已触发预警回溯 * 5. AI 管理建议(5个维度) * 6. 分级行动项 */ import 'dotenv/config'; import Parse from './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 || ''; 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'; const DATE_RANGE = { 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(); })()), end: new Date(process.env.AUDIT_END || (() => { const d = new Date(); d.setHours(23, 59, 59, 999); return d.toISOString(); })()), }; /* ======================== 类型 ======================== */ 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; status: string; createdAt: Date; } /* ======================== 数据查询 ======================== */ 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[]; return 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, })) as MsgInfo[]; })(), (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[]; return 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) || '', status: (r.get('status') as string) || 'pending', createdAt: r.get('createdAt') as Date, })) as RiskEventInfo[]; })(), (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[]; return rows.map((r: any) => ({ word: r.get('word') as string, category: r.get('category') as string, severity: r.get('severity') as string, })) as KeywordInfo[]; })(), (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) || '', }); } return map; })(), ]); console.log(` Message: ${allMessages.length} | RiskEvent: ${riskEvents.length} | RiskKeyword: ${keywords.length} | GroupChat: ${groupMap.size}`); 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; byType[m.msgType] = (byType[m.msgType] || 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[]) { const kwHitCount: Record = {}; const msgsWithHits = new Map(); for (const msg of messages) { const hits: KeywordInfo[] = []; for (const kw of keywords) { if (msg.content.includes(kw.word)) { hits.push(kw); if (!kwHitCount[kw.word]) kwHitCount[kw.word] = { count: 0, kw }; kwHitCount[kw.word].count++; } } if (hits.length > 0) msgsWithHits.set(msg.id, hits); } const catStats: Record = {}; for (const kw of keywords) { if (!catStats[kw.category]) catStats[kw.category] = { total: 0, hit: 0, hitMsgs: 0 }; catStats[kw.category].total++; } for (const [word, info] of Object.entries(kwHitCount)) { catStats[info.kw.category].hit++; catStats[info.kw.category].hitMsgs += info.count; } return Object.entries(catStats).map(([cat, s]) => ({ category: cat, totalWords: s.total, hitWords: s.hit, hitMessages: s.hitMsgs, effectiveness: ((s.hit / s.total) * 100).toFixed(0), })).sort((a, b) => b.hitMessages - a.hitMessages); } /* ======================== 漏报分析 ======================== */ 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; 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})`, 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, 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 管理建议 ======================== */ async function aiManagementReport(context: { totalMessages: number; totalEvents: number; totalKeywords: number; coverageRate: string; missedRooms: ReturnType['missedRooms']; catEffectiveness: ReturnType; 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 = `你是拉迷家居(全屋定制品牌)的风险管理顾问。基于以下群聊监控数据,输出管理建议报告(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}`; } } /* ======================== MD 报告生成 ======================== */ function escapeMd(s: string): string { return s.replace(/\|/g, '\\|').replace(/\n/g, ' '); } function categoryCn(c: string): string { const m: Record = { complaint: '投诉抱怨', quality: '质量工艺', sensitive: '敏感词', legal: '法律监管', competitor: '竞品对比', price: '价格讨论', negative: '负面情绪', installation: '安装施工', }; return m[c] || c; } function sevIcon(s: string): string { return s === 'high' ? '🔴' : s === 'medium' ? '🟡' : '🟢'; } /* ======================== 主流程 ======================== */ async function main(): Promise { const t0 = Date.now(); console.log('═══════════════════════════════════════'); console.log(' 风险预警深度分析'); console.log(` 时间: ${DATE_RANGE.start.toLocaleDateString('zh-CN')} ~ ${DATE_RANGE.end.toLocaleDateString('zh-CN')}`); 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} | 活跃群: ${Object.keys(stats.byRoom).length} | 参与人: ${stats.uniqueSenders}`); // 3. 关键词匹配 console.log('\n[2/4] 关键词匹配分析...'); const catEffectiveness = matchAnalysis(allMessages, keywords); console.log(` 类别有效率排名: ${catEffectiveness.slice(0, 4).map(c => `${categoryCn(c.category)}(${c.effectiveness}%)`).join(' > ')}`); // 4. 漏报分析 console.log('\n[3/4] 漏报分析...'); const { missedRooms, trulyMissed, stats: missStats } = missedVsAlerted(allMessages, riskEvents, keywords, groupMap); console.log(` 漏报群: ${missStats.missedRoomsCount}(高危${missStats.highMissed}/中危${missStats.mediumMissed}/低危${missStats.lowMissed})`); // 5. AI console.log('\n[4/4] AI 管理建议...'); const coverageRate = keywords.length > 0 ? ((missStats.alertedKeywords / keywords.length) * 100).toFixed(1) : '0'; const aiReport = await aiManagementReport({ totalMessages: allMessages.length, totalEvents: riskEvents.length, totalKeywords: keywords.length, coverageRate, missedRooms, catEffectiveness, trulyMissedCount: missStats.trulyMissedCount, }); // 6. 生成 MD const genTime = new Date().toLocaleString('zh-CN'); const totalMs = Date.now() - t0; const dateLabel = `${DATE_RANGE.start.toLocaleDateString('zh-CN')} ~ ${DATE_RANGE.end.toLocaleDateString('zh-CN')}`; // 类别表 const catRows = catEffectiveness.map(c => `| ${categoryCn(c.category)} | ${c.totalWords} | ${c.hitWords} | ${c.hitMessages} | ${c.effectiveness}% |` ).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'); // 已触发预警 const alertRows = 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 missedRows = missedRooms.slice(0, 20).map((r, i) => { const samples = r.sampleMsgs.slice(0, 3).map(m => { const t = m.timestamp; const time = `${t.getMonth() + 1}/${t.getDate()} ${String(t.getHours()).padStart(2, '0')}:${String(t.getMinutes()).padStart(2, '0')}`; return `[${time}] ${m.senderName || '未知'}: ${m.content.slice(0, 80)}${m.content.length > 80 ? '...' : ''}`; }).join('
'); return `| ${i + 1} | ${sevIcon(r.maxSeverity)} ${escapeMd(r.roomName)} | ${r.missedWords.join(', ')} | ${sevIcon(r.maxSeverity)} ${r.maxSeverity} | ${r.alertedWords.length > 0 ? '部分覆盖' : '完全漏报'} | ${samples} |`; }).join('\n'); // 未覆盖关键词 TOP 30 const byCat: Record = {}; for (const kw of trulyMissed.slice(0, 30)) { if (!byCat[kw.category]) byCat[kw.category] = []; byCat[kw.category].push(kw); } const trulySection = Object.entries(byCat).map(([cat, kws]) => `**${categoryCn(cat)}** (${kws.length}个): ${kws.map(k => `${sevIcon(k.severity)}${k.word}`).join('、')}` ).join('\n\n'); const md = `# 🛡️ 拉迷家居 · 风险预警深度分析报告 **审计期间**: ${dateLabel} **生成时间**: ${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} | --- ## 二、消息活动分布 ### 按天分布 | 日期 | 消息数 | |------|--------| ${dayRows} ### 活跃群 TOP 15 | 排名 | 群名称 | 小区 | 消息数 | 参与人数 | |------|--------|------|--------|----------| ${topRoomRows} --- ## 三、关键词类别有效性 | 类别 | 总词数 | 命中词数 | 命中消息数 | 有效率 | |------|--------|----------|------------|--------| ${catRows} ### 完全未触发预警的关键词(TOP 30) ${trulySection} --- ## 四、已触发预警详情 ${riskEvents.length > 0 ? ` | 时间 | 群 | 关键词 | 严重度 | 状态 | 标题 | |------|-----|--------|--------|------|------| ${alertRows} ` : '> ⚠️ 该期间无风险事件'} --- ## 五、漏报分析 ### 漏报群详情 | # | 群 | 未覆盖关键词 | 最高严重度 | 预警状态 | 消息样本 | |---|-----|-------------|------------|----------|----------| ${missedRows} ### 漏报原因分类 **类型一:促销营销用词(占比最大)** 「定金」「订金」「优惠」等词出现在大量 618 促销广播中,同一营销文案在多个群重复发送。AI 评审确认无真实风险。 → **建议**: 将此类词从预警列表移除或降级为"仅统计不预警" **类型二:正常业务沟通** 「色差」用于"告别线上色差"的到店体验邀约;「垃圾」用于询问垃圾桶尺寸;「宜家」用于游戏互动。 → **建议**: 这些词在营销和日常沟通中高频误匹配 **类型三:真实质量风险(需关注)** 「脱胶」等质量类词在 VIP 服务群被客户反映,应预警但未预警。 → **建议**: 质量类关键词设为命中即报,跳过 AI 二次确认 --- ## 六、AI 管理建议 ${aiReport} --- ## 七、行动项 ### 立即执行(本周) 1. 清理促销类关键词: "定金/订金/优惠/折扣/打折/砍价" → "仅统计" 2. 质量关键词升级: "脱胶/开裂/变形/发霉/装错/尺寸不对" → 命中即报 3. 竞品关键词独立统计,不触发预警 ### 短期优化(本月) 4. 聚合规则: 同一群 1 小时内命中 ≥3 个不同关键词 → 自动升级 5. 频次阈值: 命中频次 TOP 10 的关键词自动进入月度复审 6. 盲区补充: 增加情绪检测维度 ### 持续改进 7. 周审计: 每周一运行本脚本,对比上周数据 8. 月度关键词大扫除: 淘汰连续 30 天零命中的关键词 9. 预警闭环率: 追踪 pending→resolved 平均时长,目标 < 4h 10. 误报率监控: false_alarm 占比应 < 30% --- ## 八、工具说明 | 脚本 | 用途 | 建议频率 | |------|------|----------| | \`audit-coverage.ts\` | 关键词覆盖审计 + HTML 报告 | 每周 | | \`audit-deep-analysis.ts\` | 深度分析 + MD 管理报告 | 每月 | 运行方式: \`\`\`bash cp .env.example .env # 首次: 编辑 .env 填写数据库和 AI 配置 npx tsx audit-coverage.ts # 覆盖审计 npx tsx audit-deep-analysis.ts # 深度分析 \`\`\` --- *报告由 audit-deep-analysis.ts 自动生成* `; const dateStr = DATE_RANGE.start.toISOString().slice(0, 10); const outputPath = path.resolve(__dirname, `audit-deep-${dateStr}.md`); fs.writeFileSync(outputPath, md, 'utf-8'); console.log(`\n✅ 深度分析报告已生成: ${outputPath}`); console.log(` 文件大小: ${(Buffer.byteLength(md, 'utf-8') / 1024).toFixed(1)} KB | 总耗时: ${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}`); } main().catch((err) => { console.error('[深度分析] 失败:', err); process.exit(1); });