| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678 |
- /**
- * 风险预警深度分析脚本 (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<string, { name: string; community: string }>();
- 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<string, number> = {};
- const byType: Record<number, number> = {};
- const byRoom: Record<string, { count: number; senders: Set<string> }> = {};
- const allSenders = new Set<string>();
- 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<string, { name: string; community: string }>) {
- // 全量关键词命中
- const kwHitCount: Record<string, { count: number; kw: KeywordInfo; rooms: Set<string> }> = {};
- const msgsWithHits = new Map<string, { msg: MsgInfo; hitKws: KeywordInfo[] }>();
- 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<string, { total: number; hit: number; words: Set<string> }> = {};
- 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<string, { name: string; community: string }>,
- ) {
- const alertedRoomIds = new Set(riskEvents.map(e => e.groupId));
- const alertedKeywords = new Set(riskEvents.flatMap(e => e.keywords));
- // 按群分组
- const msgsByRoom = new Map<string, MsgInfo[]>();
- 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<string>();
- 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<typeof missedVsAlerted>['missedRooms'];
- catEffectiveness: ReturnType<typeof matchAnalysis>['catEffectiveness'];
- trulyMissedCount: number;
- }): Promise<string> {
- 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<void> {
- 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('<br>');
- 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<string, KeywordInfo[]> = {};
- 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<string, string> = {
- 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);
- });
|