# AI 品牌曝光监测系统技术方案 V4 ## 1. 文档信息 | 项目 | 内容 | | --- | --- | | 文档名称 | AI 品牌曝光监测系统技术方案 V4(插件采集 + 配额约束) | | 文档版本 | V4.0 | | 文档状态 | 待评审 | | 创建日期 | 2026-04-08 | | 基线文档 | `docs/ai-brand-monitoring-tech-design-v3.md` | | 适用范围 | 数据追踪模块全部页面 | | 关联文档 | `docs/geo-platform-prd-v1.md` (PRD 8.4 数据追踪) | | 关联文档 | `docs/ai-brand-monitoring-collection-feasibility-v1.md` (采集可行性) | | 容量目标 | 5 万用户(B2B 共享租户,~3,000 租户) | ## 2. V4 修订范围 V4 在 V3 基础上做两项根本性变更: 1. **采集方式**:从服务端 AI API 调用改为 **浏览器插件采集**,消除 AI API 成本。 2. **配额约束**:引入租户级品牌/关键词/问题配额,控制采集规模。 ### 2.1 与 V3 的关系 | V3 章节 | V4 处理 | 原因 | | --- | --- | --- | | 2.1 数据口径分层 | 保留,`run_mode` 值调整为 `plugin_*` | 口径逻辑不变,采集来源变了 | | 2.2 Schema Delta | 保留 + 扩展 | 新增配额表、任务分配字段 | | 3. 读流量与缓存 | **完全保留** | 读侧与采集方式无关 | | 4. 采集流水线 | **全部替换** | 插件采集替代服务端 Collector | | 5. 部署拓扑 | 精简 | 去掉 Collector Pod、Queue Redis | | 6. 降级与容灾 | 保留读侧 + 新增插件降级 | 新增采集不足的降级策略 | | 7. 压测验收 | 保留读侧 + 新增插件压测 | 新增插件并发场景 | | 8. 实施计划 | 重写 | 工期和顺序均有变化 | V3 其他章节(页面需求、前端组件、API 响应结构、指标定义)保持不变。 --- ## 3. 用户配额体系 ### 3.1 配额规则 | 配额项 | 普通租户(Free) | 高级租户(Pro) | 企业租户(Enterprise) | | --- | --- | --- | --- | | 最大品牌数 | 1 | 3 | 自定义 | | 每品牌关键词数 | 8 | 8 | 自定义 | | 每关键词问题数 | 5 | 5 | 自定义 | | **每品牌最大问题数** | **40** | **40** | **自定义** | | 采集频率 | 每 3 天 | 每天 | 每天 + 按需触发 | | AI 平台数 | 3(首批) | 6(全部) | 6 + 自定义 | | 搜索增强采集 | 不支持 | 支持 | 支持 | ### 3.2 配额表 Schema ```sql CREATE TABLE tenant_monitoring_quotas ( tenant_id UUID PRIMARY KEY REFERENCES tenants(id), max_brands INT NOT NULL DEFAULT 1, max_keywords_per_brand INT NOT NULL DEFAULT 8, max_questions_per_keyword INT NOT NULL DEFAULT 5, collect_frequency VARCHAR(20) NOT NULL DEFAULT 'every_3_days', enabled_platforms JSONB NOT NULL DEFAULT '["deepseek","qwen","doubao"]', search_enhanced BOOLEAN NOT NULL DEFAULT false, plan_tier VARCHAR(20) NOT NULL DEFAULT 'free', created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now() ); COMMENT ON COLUMN tenant_monitoring_quotas.collect_frequency IS 'daily / every_3_days / weekly'; COMMENT ON COLUMN tenant_monitoring_quotas.plan_tier IS 'free / pro / enterprise'; ``` ### 3.3 采集规模估算 | 参数 | 估值 | | --- | --- | | 总用户 | 50,000 | | 租户数 | ~3,000(平均 15-20 人/租户) | | 高级租户(10%) | 300,品牌数 900 | | 普通租户(90%) | 2,700,品牌数 2,700 | | 品牌实例总数(最大) | 3,600 | | 监测采用率(30%) | 活跃监测品牌 ~1,080 | | 实际问题数/品牌(70% 使用率) | ~28 | 每日采集任务量(中等场景): ``` 标准模式: 1,080 品牌 × 28 问题 × 6 平台 = 181,440 任务/天 考虑采集频率分层(Free 每 3 天): 高级 (30%): 324 品牌 × 28q × 6 平台 = 54,432/天 普通 (70%): 756 品牌 × 28q × 6 平台 ÷ 3 = 42,336/天 合计 ≈ 96,768 任务/天(均摊) ``` --- ## 4. 插件采集架构 ### 4.1 现有插件能力复用 当前 GEO Publisher 插件(`apps/browser-extension/`)已具备完整基础设施: | 现有能力 | 文件位置 | 监测复用方式 | | --- | --- | --- | | 平台适配器模式 | `src/adapters/` (13 个) | 新增 AI 平台 adapter | | 登录状态检测 | 各 adapter 的 `detect()` | 检测 AI 平台登录态 | | 后台 Service Worker | `entrypoints/background.ts` | 定时触发采集任务 | | 页面 ↔ 插件通信 | `entrypoints/content.ts` (postMessage) | 前端可触发手动采集(辅助通道) | | 后端回调 | `src/runtime.ts` (HTTP POST) | 采集结果回传后端 | | 后端直连 | `installation_token` + `api_base_url` | **插件后台直接拉取任务 + 回传结果** | | Chrome Storage | `src/storage.ts` | 存储采集队列和进度 | | DNR 跨域规则 | `public/rules.json` (18 条) | 新增 AI 平台规则 | | 安装认证 | `installation_token` + `installation_id` | 复用,校验采集权限 | ### 4.2 整体采集流程 ``` ┌──────────────────────────────────────────────────────────────────┐ │ 后端调度器 │ │ 1. 根据 tenant_monitoring_quotas 和 collect_frequency │ │ 生成当日采集任务(品牌 × 去重问题 × 平台) │ │ 2. 写入 monitoring_collect_tasks 表 │ │ 3. 状态机: pending → assigned → completed / failed / expired │ └───────────────────────────────┬──────────────────────────────────┘ │ 插件 Background SW 定时心跳(每 15 分钟) ▼ ┌──────────────────────────────────────────────────────────────────┐ │ 浏览器插件 Background Service Worker │ │ │ │ 主通道:插件后台直接拉取(不依赖前端页面) │ │ ────────────────────────────────────────── │ │ 4. 心跳触发 → GET /api/plugin/monitoring/tasks │ │ (Header: X-Geo-Installation-Token) │ │ → 后端根据 installation → tenant 映射,返回待采集任务 │ │ 5. POST /api/plugin/monitoring/tasks/{id}/claim → 领取任务 │ │ 6. 按 AI 平台分发到对应 adapter │ │ 7. 检测用户是否登录该 AI 平台(Cookie 检测) │ │ 8. 已登录 → 调用平台 Web API 创建对话 → 输入问题 → 等待回答 │ │ 未登录 → 标记 skip │ │ 9. 提取回答文本 + 引用信息(如有) │ │ 10. POST /api/callback/plugin/monitor → 结果回传后端 │ │ │ │ 辅助通道:前端 postMessage(用户主动触发时) │ │ ────────────────────────────────────────── │ │ Admin Web 点击"立即采集" → postMessage → 插件立即执行 │ │ (仅作为加速手段,不是必须路径) │ └───────────────────────────────┬──────────────────────────────────┘ │ POST /api/callback/plugin/monitor ▼ ┌──────────────────────────────────────────────────────────────────┐ │ 后端采集服务 │ │ 11. 校验 installation_token + 任务归属 │ │ 12. 解析回答:品牌提及、位置、情感、引用 │ │ 13. 写入 question_monitor_runs + question_monitor_parse_results │ │ 14. 原始回答存 MinIO(对象存储) │ │ 15. 标记任务 completed │ └──────────────────────────────────────────────────────────────────┘ ↓ 每日 6:30 聚合 ┌──────────────────────────────────────────────────────────────────┐ │ Aggregator Job(不变) │ │ 16. 按 business_date 聚合已完成的采集数据 │ │ 17. 写入 10 张 monitoring_*_daily 汇总表 │ │ 18. 预热热门品牌缓存 │ └──────────────────────────────────────────────────────────────────┘ ``` **关键设计**:插件通过 Background SW 的定时心跳(每 15 分钟)**直接调用后端 API** 拉取和领取任务,不依赖 Admin Web 前端页面是否打开。前端 postMessage 仅作为用户主动触发"立即采集"的辅助通道。 后端需要新增 **插件专用 API**(不走 JWT 认证,走 installation_token 认证): ``` GET /api/plugin/monitoring/tasks → 返回当前插件所属租户的 pending 任务 POST /api/plugin/monitoring/tasks/{id}/claim → 领取任务(分布式锁) POST /api/callback/plugin/monitor → 回传采集结果(已有回调模式) POST /api/callback/plugin/monitor/batch-skip → 批量跳过未登录平台的任务 ``` ### 4.3 AI 平台适配器 #### 4.3.1 适配器接口 ```typescript // apps/browser-extension/src/adapters/ai/types.ts export interface AIMonitorAdapter { /** 平台标识 */ platformId: AIPlatformId; /** 检测用户是否已登录该 AI 平台 */ detect(): Promise; /** 向 AI 平台提问并获取回答 */ ask(question: string, options?: AskOptions): Promise; } export type AIPlatformId = 'deepseek' | 'qwen' | 'doubao' | 'kimi' | 'ernie' | 'hunyuan'; export interface AIMonitorPlatformState { platformId: AIPlatformId; connected: boolean; userId?: string; nickname?: string; message?: string; } export interface AskOptions { /** 是否启用联网搜索 */ searchEnabled?: boolean; /** 任务超时(毫秒) */ timeout?: number; } export interface AIMonitorResult { success: boolean; question: string; answer: string; citations: Citation[]; searchResults: SearchResult[]; responseTime: number; model: string; timestamp: string; rawHtml?: string; } export interface Citation { url: string; title: string; domain: string; snippet?: string; } export interface SearchResult { url: string; title: string; snippet: string; } ``` #### 4.3.2 各平台采集方式 | AI 平台 | Web 端 URL | 采集方式 | 登录检测 | 引用支持 | | --- | --- | --- | --- | --- | | DeepSeek | chat.deepseek.com | 调用 Web 端 chat API(SSE 流) → 拼接完整回答 | Cookie 检测 | 无原生引用 | | 千问 | tongyi.aliyun.com | 调用 Web 端对话 API → 等待回答完成 | Cookie 检测 | 联网搜索模式支持 | | 豆包 | doubao.com | 调用 Web 端对话 API → 等待回答 | Cookie 检测 | 联网搜索支持 | | Kimi | kimi.moonshot.cn | 调用 Web 端对话 API → 等待回答 | Cookie 检测 | 联网搜索支持 | | 文心一言 | yiyan.baidu.com | 调用 Web 端对话 API → 等待回答 | 百度账号 Cookie | 支持 | | 混元 | hunyuan.tencent.com | 调用 Web 端对话 API → 等待回答 | Cookie 检测 | 搜索增强支持 | **核心实现模式**(以 DeepSeek 为例): ```typescript // apps/browser-extension/src/adapters/ai/deepseek.ts import type { AIMonitorAdapter, AIMonitorPlatformState, AIMonitorResult, AskOptions } from './types'; const DEEPSEEK_CHAT_URL = 'https://chat.deepseek.com'; const DEEPSEEK_API_BASE = 'https://chat.deepseek.com/api/v0'; export const deepseekAdapter: AIMonitorAdapter = { platformId: 'deepseek', async detect(): Promise { try { // 通过 Cookie 判断登录状态,类似现有 zhihu/bilibili adapter const cookies = await browser.cookies.getAll({ domain: '.deepseek.com' }); const hasSession = cookies.some(c => c.name === 'ds_session' || c.name === 'token'); if (!hasSession) { return { platformId: 'deepseek', connected: false, message: '未登录' }; } // 验证 token 有效性 const resp = await fetch(`${DEEPSEEK_API_BASE}/user/current`, { credentials: 'include', }); if (!resp.ok) { return { platformId: 'deepseek', connected: false, message: '登录已过期' }; } const data = await resp.json(); return { platformId: 'deepseek', connected: true, userId: data.data?.id, nickname: data.data?.nickname, }; } catch { return { platformId: 'deepseek', connected: false, message: '检测失败' }; } }, async ask(question: string, options?: AskOptions): Promise { const startTime = Date.now(); const timeout = options?.timeout ?? 60_000; try { // 1. 创建新对话 const createResp = await fetch(`${DEEPSEEK_API_BASE}/chat/create`, { method: 'POST', credentials: 'include', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({}), }); const { data: chat } = await createResp.json(); // 2. 发送问题并监听 SSE 流 const answer = await streamChat(chat.id, question, timeout); return { success: true, question, answer: answer.text, citations: [], // DeepSeek 标准模式无引用 searchResults: [], responseTime: Date.now() - startTime, model: answer.model ?? 'deepseek-chat', timestamp: new Date().toISOString(), }; } catch (err: any) { return { success: false, question, answer: '', citations: [], searchResults: [], responseTime: Date.now() - startTime, model: '', timestamp: new Date().toISOString(), }; } }, }; async function streamChat(chatId: string, question: string, timeout: number) { // SSE 流式读取,拼接完整回答 // 具体实现需根据 DeepSeek Web 端实际 API 格式调整 const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), timeout); try { const resp = await fetch(`${DEEPSEEK_API_BASE}/chat/completion`, { method: 'POST', credentials: 'include', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ chat_id: chatId, prompt: question, stream: true, }), signal: controller.signal, }); const reader = resp.body!.getReader(); const decoder = new TextDecoder(); let fullText = ''; let model = ''; while (true) { const { done, value } = await reader.read(); if (done) break; const chunk = decoder.decode(value, { stream: true }); // 解析 SSE data 行 for (const line of chunk.split('\n')) { if (line.startsWith('data: ')) { try { const json = JSON.parse(line.slice(6)); if (json.choices?.[0]?.delta?.content) { fullText += json.choices[0].delta.content; } if (json.model) model = json.model; } catch { /* 跳过非 JSON 行 */ } } } } return { text: fullText, model }; } finally { clearTimeout(timer); } } ``` #### 4.3.3 适配器注册表 ```typescript // apps/browser-extension/src/adapters/ai/index.ts import { deepseekAdapter } from './deepseek'; import { qwenAdapter } from './qwen'; import { doubaoAdapter } from './doubao'; import { kimiAdapter } from './kimi'; import { ernieAdapter } from './ernie'; import { hunyuanAdapter } from './hunyuan'; import type { AIMonitorAdapter, AIPlatformId } from './types'; export const aiAdapters: Record = { deepseek: deepseekAdapter, qwen: qwenAdapter, doubao: doubaoAdapter, kimi: kimiAdapter, ernie: ernieAdapter, hunyuan: hunyuanAdapter, }; export function getAIAdapter(platformId: AIPlatformId): AIMonitorAdapter | undefined { return aiAdapters[platformId]; } ``` ### 4.4 采集任务调度器(插件侧) ```typescript // apps/browser-extension/src/monitor/scheduler.ts import { getAIAdapter } from '../adapters/ai'; import type { AIMonitorResult, AIPlatformId } from '../adapters/ai/types'; import { getState } from '../storage'; /** 反检测配置 */ const ANTI_DETECT = { maxQueriesPerPlatformPerHour: 3, minIntervalSec: 30, maxIntervalSec: 120, maxDailyQueries: 50, }; interface CollectTask { id: number; question_text: string; platform_id: AIPlatformId; brand_id: number; question_id: number; search_enabled: boolean; } /** 执行一批采集任务 */ export async function executeCollectTasks(tasks: CollectTask[]): Promise { const state = await getState(); if (!state.installation_token || !state.api_base_url) return; // 按平台分组,串行执行(避免同时操作多个平台) const byPlatform = groupBy(tasks, t => t.platform_id); for (const [platformId, platformTasks] of Object.entries(byPlatform)) { const adapter = getAIAdapter(platformId as AIPlatformId); if (!adapter) continue; // 检测登录状态 const detection = await adapter.detect(); if (!detection.connected) { // 标记该平台全部任务为 skip await reportSkipped(state, platformTasks, '未登录'); continue; } // 逐条执行,加入随机延迟 for (const task of platformTasks) { // 检查日限额 if (await getDailyCount() >= ANTI_DETECT.maxDailyQueries) break; // 检查平台小时限额 if (await getPlatformHourlyCount(platformId) >= ANTI_DETECT.maxQueriesPerPlatformPerHour) break; const result = await adapter.ask(task.question_text, { searchEnabled: task.search_enabled, timeout: 60_000, }); // 回传后端 await reportResult(state, task, result); await incrementCounters(platformId); // 随机延迟 const delay = randomBetween(ANTI_DETECT.minIntervalSec, ANTI_DETECT.maxIntervalSec); await sleep(delay * 1000); } } } async function reportResult( state: ExtensionStorageState, task: CollectTask, result: AIMonitorResult, ) { await fetch(`${state.api_base_url}/api/callback/plugin/monitor`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Geo-Installation-Token': state.installation_token!, 'X-Geo-Installation-Id': String(state.plugin_installation_id), }, body: JSON.stringify({ task_id: task.id, platform_id: task.platform_id, brand_id: task.brand_id, question_id: task.question_id, success: result.success, answer: result.answer, citations: result.citations, search_results: result.searchResults, response_time: result.responseTime, model: result.model, timestamp: result.timestamp, raw_html: result.rawHtml, client_version: browser.runtime.getManifest().version, }), }); } async function reportSkipped(state: ExtensionStorageState, tasks: CollectTask[], reason: string) { await fetch(`${state.api_base_url}/api/callback/plugin/monitor/batch-skip`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Geo-Installation-Token': state.installation_token!, 'X-Geo-Installation-Id': String(state.plugin_installation_id), }, body: JSON.stringify({ task_ids: tasks.map(t => t.id), reason, }), }); } function randomBetween(min: number, max: number): number { return Math.floor(Math.random() * (max - min + 1)) + min; } function sleep(ms: number): Promise { return new Promise(resolve => setTimeout(resolve, ms)); } function groupBy(arr: T[], key: (item: T) => string): Record { return arr.reduce((acc, item) => { const k = key(item); (acc[k] ??= []).push(item); return acc; }, {} as Record); } ``` ### 4.5 采集执行策略 采用**插件自主 + 前端辅助**模式: | 模式 | 触发方式 | 单次任务数 | 频率 | 依赖前端 | | --- | --- | --- | --- | --- | | **心跳自主采集(主通道)** | Background SW 定时心跳 | 3-5 个任务 | 每 15 分钟 | 否 | | **用户主动采集(辅助)** | Dashboard 点击"立即采集" → postMessage | 批量 pending 任务 | 用户触发 | 是 | 心跳自主采集保证:**只要用户安装了插件且浏览器在运行,采集就在持续进行**,无需用户打开 Admin Web。 每日限额:单用户最多 50 个采集任务。 ### 4.6 Background SW 扩展 #### 4.6.1 新增采集心跳(15 分钟间隔) ```typescript // entrypoints/background.ts // 原有 60 分钟心跳保持不变(平台检测 + 状态同步) // 新增 15 分钟采集心跳 browser.alarms.create('monitor-heartbeat', { periodInMinutes: 15 }); browser.alarms.onAlarm.addListener(async (alarm) => { if (alarm.name === 'heartbeat') { // 原有逻辑:刷新内容平台登录状态 await refreshPlatformState(); } if (alarm.name === 'monitor-heartbeat') { // 新增:插件后台直接拉取并执行采集任务 await pullAndExecuteMonitorTasks(); } }); async function pullAndExecuteMonitorTasks() { const state = await getState(); if (!state.installation_token || !state.api_base_url) return; try { // 1. 直接调后端 API 拉取待采集任务(不经过前端) const resp = await fetch(`${state.api_base_url}/api/plugin/monitoring/tasks`, { headers: { 'X-Geo-Installation-Token': state.installation_token, 'X-Geo-Installation-Id': String(state.plugin_installation_id), }, }); if (!resp.ok) return; const { data: tasks } = await resp.json(); if (!tasks?.length) return; // 2. 执行采集(串行,带反检测延迟) await executeCollectTasks(tasks); } catch { // 静默失败,下次心跳再试 } } ``` #### 4.6.2 前端辅助通道(用户主动触发) ```typescript // 新增 action 处理(前端 postMessage 触发) case 'collectMonitorData': // 用户点击"立即采集"时,前端传入任务列表 return await handleCollectMonitorData(payload); case 'detectAIPlatforms': // 检测 AI 平台登录状态(供前端展示) return await handleDetectAIPlatforms(); ``` ### 4.7 分布式任务调度(后端侧) #### 4.7.1 任务生成 ```go // 每日凌晨由 Aggregator 或独立 CronJob 生成 func (s *MonitoringScheduler) GenerateDailyTasks(ctx context.Context, businessDate time.Time) error { // 1. 获取所有启用监测的租户 tenants, _ := s.repo.ListMonitoringTenants(ctx) for _, tenant := range tenants { quota, _ := s.repo.GetMonitoringQuota(ctx, tenant.ID) // 检查采集频率 if !shouldCollectToday(quota.CollectFrequency, businessDate) { continue } // 2. 获取租户的品牌和问题 brands, _ := s.repo.ListBrandsWithQuestions(ctx, tenant.ID, quota.MaxBrands) for _, brand := range brands { for _, question := range brand.Questions { for _, platformID := range quota.EnabledPlatforms { // 3. 生成任务(幂等,按唯一键去重) s.repo.UpsertCollectTask(ctx, MonitorCollectTask{ TenantID: tenant.ID, BrandID: brand.ID, QuestionID: question.ID, PlatformID: platformID, BusinessDate: businessDate, RunMode: "plugin_standard", Status: "pending", SearchEnabled: quota.SearchEnhanced && platformSupportsSearch(platformID), }) } } } } return nil } ``` #### 4.7.2 任务领取(分布式锁) ```go // GET /api/tenant/monitoring/collect-tasks func (s *MonitoringService) GetPendingTasks(ctx context.Context, tenantID uuid.UUID, limit int) ([]CollectTask, error) { // 只返回 status=pending 的任务 return s.repo.ListPendingCollectTasks(ctx, tenantID, limit) } // POST /api/tenant/monitoring/collect-tasks/{id}/claim func (s *MonitoringService) ClaimTask(ctx context.Context, taskID int64, installationID string) error { // Redis 分布式锁,TTL 10 分钟 lockKey := fmt.Sprintf("mon:task_lock:%d", taskID) acquired, _ := s.cache.SetNX(ctx, lockKey, installationID, 10*time.Minute) if !acquired { return ErrTaskAlreadyClaimed } // 更新任务状态 return s.repo.UpdateCollectTaskStatus(ctx, taskID, "assigned", installationID) } ``` #### 4.7.3 结果接收(HTTP → RabbitMQ 异步处理) 插件回传的采集结果通过 RabbitMQ 异步处理,API 层只做校验和投递,**不阻塞插件等待解析完成**: ```go // POST /api/callback/plugin/monitor func (h *MonitorCallbackHandler) HandleMonitorResult(c *gin.Context) { var req MonitorResultRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(400, gin.H{"error": "invalid request"}) return } // 校验 installation_token installationToken := c.GetHeader("X-Geo-Installation-Token") if !h.service.ValidateInstallation(c, installationToken) { c.JSON(401, gin.H{"error": "unauthorized"}) return } // 仅做两件事:标记任务 assigned → received,投递到 MQ // 解析、入库、MinIO 存储全部由 Consumer 异步完成 if err := h.service.MarkTaskReceived(c, req.TaskID); err != nil { c.JSON(409, gin.H{"error": "task not found or already completed"}) return } // 投递到 RabbitMQ if err := h.mq.Publish(c, "monitor.result", req); err != nil { // MQ 投递失败 → 回退任务状态,插件下次心跳会重试 h.service.MarkTaskPending(c, req.TaskID) c.JSON(500, gin.H{"error": "queue unavailable"}) return } c.JSON(200, gin.H{"ok": true}) } ``` ### 4.8 RabbitMQ 异步处理架构 #### 4.8.1 队列拓扑 ``` 插件 HTTP 回传 │ ▼ ┌──────────────────────────────────────────────────┐ │ tenant-api (HTTP 层) │ │ 校验 token → 标记 received → publish to MQ │ └───────────────────────┬──────────────────────────┘ │ ▼ ┌──────────────────────────────────────────────────┐ │ RabbitMQ Broker │ │ │ │ Exchange: monitor.direct (direct) │ │ ┌─────────────────────────────────────────────┐ │ │ │ Queue: monitor.result.parse │ │ │ │ routing_key: monitor.result │ │ │ │ consumer: MonitorParseWorker × 3 │ │ │ │ 作用: 解析回答 + 入库 + MinIO 存储 │ │ │ └─────────────────────────────────────────────┘ │ │ ┌─────────────────────────────────────────────┐ │ │ │ Queue: monitor.task.generate │ │ │ │ routing_key: monitor.task.generate │ │ │ │ consumer: TaskGenerateWorker × 1 │ │ │ │ 作用: 凌晨批量生成采集任务 │ │ │ └─────────────────────────────────────────────┘ │ │ ┌─────────────────────────────────────────────┐ │ │ │ Queue: monitor.aggregate.trigger │ │ │ │ routing_key: monitor.aggregate │ │ │ │ consumer: AggregateWorker × 1 │ │ │ │ 作用: 按品牌触发增量聚合 │ │ │ └─────────────────────────────────────────────┘ │ │ ┌─────────────────────────────────────────────┐ │ │ │ Queue: monitor.result.dlq (死信队列) │ │ │ │ routing_key: monitor.result.dlq │ │ │ │ 作用: 处理失败的消息暂存,人工排查 │ │ │ └─────────────────────────────────────────────┘ │ └──────────────────────────────────────────────────┘ ``` #### 4.8.2 Parse Worker(核心消费者) ```go // server/internal/monitoring/worker/parse_worker.go type MonitorParseWorker struct { repo *repository.MonitoringRepository parser *MonitorAnswerParser storage objectstorage.Client mq *rabbitmq.Client } func (w *MonitorParseWorker) Handle(ctx context.Context, msg MonitorResultMessage) error { // 1. 存原始回答到 MinIO objectKey := fmt.Sprintf("monitor/%s/%d/%d.json", msg.Timestamp[:10], msg.TaskID, time.Now().UnixMilli()) if err := w.storage.PutJSON(ctx, objectKey, msg); err != nil { return fmt.Errorf("minio put failed: %w", err) } // 2. 写 question_monitor_runs run, err := w.repo.CreateMonitorRun(ctx, CreateMonitorRunInput{ TaskID: msg.TaskID, PlatformID: msg.PlatformID, BrandID: msg.BrandID, QuestionID: msg.QuestionID, RunMode: "plugin_standard", RawAnswer: msg.Answer, ResponseTime: msg.ResponseTime, Model: msg.Model, ObjectKey: objectKey, }) if err != nil { return fmt.Errorf("create run failed: %w", err) } // 3. 解析回答(品牌提及、位置、情感、引用) parseResults := w.parser.Parse(ctx, run, msg.Answer, msg.Citations) if err := w.repo.SaveParseResults(ctx, run.ID, parseResults); err != nil { return fmt.Errorf("save parse results failed: %w", err) } // 4. 标记任务 completed if err := w.repo.CompleteTask(ctx, msg.TaskID); err != nil { return fmt.Errorf("complete task failed: %w", err) } // 5. 检查该品牌当日任务是否全部完成 → 触发增量聚合 allDone, _ := w.repo.IsBrandDailyTasksComplete(ctx, msg.BrandID, msg.BusinessDate) if allDone { w.mq.Publish(ctx, "monitor.aggregate", AggregateMessage{ BrandID: msg.BrandID, TenantID: msg.TenantID, BusinessDate: msg.BusinessDate, }) } return nil } ``` #### 4.8.3 Aggregate Worker(增量聚合) ```go // server/internal/monitoring/worker/aggregate_worker.go type AggregateWorker struct { repo *repository.MonitoringRepository cache cache.Cache } func (w *AggregateWorker) Handle(ctx context.Context, msg AggregateMessage) error { // 按品牌 + business_date 增量聚合(替代 V3 的全量定时聚合) // 好处:品牌任务完成即聚合,不必等到每日 6:30 if err := w.repo.AggregateBrandDaily(ctx, msg.TenantID, msg.BrandID, msg.BusinessDate); err != nil { return err } // 预热该品牌缓存 w.cache.WarmBrandComposite(ctx, msg.TenantID, msg.BrandID) return nil } ``` #### 4.8.4 RabbitMQ 配置 ```yaml # server/configs/config.yaml 新增 rabbitmq: url: "amqp://guest:guest@localhost:5672/" exchange: "monitor.direct" queues: result_parse: name: "monitor.result.parse" routing_key: "monitor.result" concurrency: 3 # 3 个并发消费者 prefetch: 5 # 每个消费者预取 5 条 retry_max: 3 # 最大重试 3 次 retry_delay: "5s" # 重试间隔 dlq: "monitor.result.dlq" task_generate: name: "monitor.task.generate" routing_key: "monitor.task.generate" concurrency: 1 aggregate: name: "monitor.aggregate.trigger" routing_key: "monitor.aggregate" concurrency: 1 prefetch: 1 ``` #### 4.8.5 Go 客户端初始化 ```go // server/internal/shared/mq/rabbitmq.go import amqp "github.com/rabbitmq/amqp091-go" type Client struct { conn *amqp.Connection channel *amqp.Channel exchange string } func NewClient(url, exchange string) (*Client, error) { conn, err := amqp.Dial(url) if err != nil { return nil, err } ch, err := conn.Channel() if err != nil { return nil, err } // 声明 exchange err = ch.ExchangeDeclare(exchange, "direct", true, false, false, false, nil) if err != nil { return nil, err } return &Client{conn: conn, channel: ch, exchange: exchange}, nil } func (c *Client) Publish(ctx context.Context, routingKey string, body any) error { data, _ := json.Marshal(body) return c.channel.PublishWithContext(ctx, c.exchange, routingKey, false, false, amqp.Publishing{ DeliveryMode: amqp.Persistent, ContentType: "application/json", Body: data, MessageId: uuid.NewString(), Timestamp: time.Now(), }) } func (c *Client) Consume(queueName string, prefetch int, handler func(amqp.Delivery)) error { c.channel.Qos(prefetch, 0, false) msgs, err := c.channel.Consume(queueName, "", false, false, false, false, nil) if err != nil { return err } for msg := range msgs { handler(msg) } return nil } ``` #### 4.8.6 异步化带来的架构优势 | 对比项 | 同步处理(V4 原方案) | RabbitMQ 异步 | | --- | --- | --- | | 插件回调延迟 | ~500ms(含解析+入库+MinIO) | ~10ms(仅校验+投递) | | API Pod 负载 | 高(CPU 密集的解析在 API 进程) | 低(解析转移到 Worker) | | 解析失败影响 | 插件收到 500,需重试整个采集 | 消息重试,不影响插件 | | 聚合时机 | 每日 6:30 全量聚合 | **品牌级增量聚合**(任务完成即触发) | | 数据可见延迟 | T+1(次日早上) | **准实时**(品牌任务完成后分钟级可见) | | 峰值吸收 | API 直接承压 | MQ 削峰填谷 | | 故障隔离 | 解析崩溃 → API 不可用 | Worker 崩溃 → 消息堆积 → 恢复后自动消化 | ### 4.8 DNR 规则扩展 在 `apps/browser-extension/public/rules.json` 中新增 AI 平台的跨域规则: ```json [ { "id": 101, "action": { "type": "modifyHeaders", "requestHeaders": [ {"header": "Referer", "operation": "set", "value": "https://chat.deepseek.com"}, {"header": "origin", "operation": "set", "value": "https://chat.deepseek.com"} ] }, "condition": { "urlFilter": "https://chat.deepseek.com/*", "resourceTypes": ["xmlhttprequest"] } }, { "id": 102, "action": { "type": "modifyHeaders", "requestHeaders": [ {"header": "Referer", "operation": "set", "value": "https://tongyi.aliyun.com"}, {"header": "origin", "operation": "set", "value": "https://tongyi.aliyun.com"} ] }, "condition": { "urlFilter": "https://tongyi.aliyun.com/*", "resourceTypes": ["xmlhttprequest"] } }, { "id": 103, "action": { "type": "modifyHeaders", "requestHeaders": [ {"header": "Referer", "operation": "set", "value": "https://www.doubao.com"}, {"header": "origin", "operation": "set", "value": "https://www.doubao.com"} ] }, "condition": { "urlFilter": "https://www.doubao.com/*", "resourceTypes": ["xmlhttprequest"] } }, { "id": 104, "action": { "type": "modifyHeaders", "requestHeaders": [ {"header": "Referer", "operation": "set", "value": "https://kimi.moonshot.cn"}, {"header": "origin", "operation": "set", "value": "https://kimi.moonshot.cn"} ] }, "condition": { "urlFilter": "https://kimi.moonshot.cn/*", "resourceTypes": ["xmlhttprequest"] } }, { "id": 105, "action": { "type": "modifyHeaders", "requestHeaders": [ {"header": "Referer", "operation": "set", "value": "https://yiyan.baidu.com"}, {"header": "origin", "operation": "set", "value": "https://yiyan.baidu.com"} ] }, "condition": { "urlFilter": "https://yiyan.baidu.com/*", "resourceTypes": ["xmlhttprequest"] } }, { "id": 106, "action": { "type": "modifyHeaders", "requestHeaders": [ {"header": "Referer", "operation": "set", "value": "https://hunyuan.tencent.com"}, {"header": "origin", "operation": "set", "value": "https://hunyuan.tencent.com"} ] }, "condition": { "urlFilter": "https://hunyuan.tencent.com/*", "resourceTypes": ["xmlhttprequest"] } } ] ``` --- ## 5. 数据口径修订(覆盖 V3 2.1) ### 5.1 `run_mode` 调整 | `run_mode` | 含义 | 进入主统计 | 默认消费页面 | | --- | --- | --- | --- | | `plugin_standard` | 插件在 AI Web 端标准对话采集 | 是 | Dashboard、平台占比、竞争对手、业务主题、AI 对话问题、品牌印象 | | `plugin_search` | 插件在 AI Web 端开启联网搜索后采集 | 是,单独统计 | AI 引用排名 | | `api_standard` | 后端 API 直接调用(降级/验证用) | 是 | 同 plugin_standard | 插件采集的回答更接近真实用户体验。采集可行性方案已确认 DeepSeek API 与 Web 版本不同,插件方案反而是优势。 ### 5.2 Schema Delta 补充 在 V3 Schema Delta 基础上,新增/修改: #### 5.2.1 `monitoring_collect_tasks` 表扩展 ```sql CREATE TABLE monitoring_collect_tasks ( id BIGSERIAL PRIMARY KEY, tenant_id UUID NOT NULL REFERENCES tenants(id), brand_id INT NOT NULL, question_id INT NOT NULL, question_version_id INT NOT NULL, ai_platform_id VARCHAR(30) NOT NULL, run_mode VARCHAR(30) NOT NULL DEFAULT 'plugin_standard', business_date DATE NOT NULL, search_enabled BOOLEAN NOT NULL DEFAULT false, status VARCHAR(20) NOT NULL DEFAULT 'pending', assigned_to VARCHAR(100), -- 插件 installation_id assigned_at TIMESTAMPTZ, completed_at TIMESTAMPTZ, error_message TEXT, retry_count INT NOT NULL DEFAULT 0, priority INT NOT NULL DEFAULT 0, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), CONSTRAINT uk_collect_task_idempotent UNIQUE (tenant_id, brand_id, question_id, ai_platform_id, run_mode, business_date) ); CREATE INDEX idx_collect_tasks_pending ON monitoring_collect_tasks(tenant_id, status, business_date) WHERE status = 'pending'; COMMENT ON COLUMN monitoring_collect_tasks.status IS 'pending / assigned / completed / failed / expired / skipped'; COMMENT ON COLUMN monitoring_collect_tasks.assigned_to IS '领取该任务的插件 installation_id'; ``` #### 5.2.2 `ai_platforms` 表 ```sql CREATE TABLE ai_platforms ( id VARCHAR(30) PRIMARY KEY, name VARCHAR(100) NOT NULL, web_url VARCHAR(200) NOT NULL, supports_standard BOOLEAN NOT NULL DEFAULT true, supports_search BOOLEAN NOT NULL DEFAULT false, supports_citation BOOLEAN NOT NULL DEFAULT false, plugin_adapter_available BOOLEAN NOT NULL DEFAULT false, display_order INT NOT NULL DEFAULT 0, enabled BOOLEAN NOT NULL DEFAULT true, created_at TIMESTAMPTZ NOT NULL DEFAULT now() ); -- 初始数据 INSERT INTO ai_platforms (id, name, web_url, supports_search, supports_citation, plugin_adapter_available, display_order) VALUES ('deepseek', 'DeepSeek', 'https://chat.deepseek.com', false, false, true, 1), ('qwen', '通义千问', 'https://tongyi.aliyun.com', true, true, true, 2), ('doubao', '豆包', 'https://www.doubao.com', true, true, true, 3), ('kimi', 'Kimi', 'https://kimi.moonshot.cn', true, true, false, 4), ('ernie', '文心一言', 'https://yiyan.baidu.com', true, true, false, 5), ('hunyuan', '混元', 'https://hunyuan.tencent.com', true, true, false, 6); ``` --- ## 6. 读流量模型与缓存架构(保留 V3 不变) **完全保留 V3 第 3 章的设计**,包括: - 3.1 页面 Fan-out 矩阵 - 3.2 QPS 模型(13,500 设计容量 vs 实际 ~900 QPS,绰绰有余) - 3.3 BFF 聚合层(Dashboard composite 接口) - 3.4 缓存防雪崩(singleflight + TTL 抖动 + stale-while-revalidate + L1) - 3.5 缓存 Key 修订(`scope` 改为对应 `plugin_standard`/`plugin_search`) - 3.6 缓存失效策略(渐进式失效 + 预热) - 3.7 TTL 矩阵 --- ## 7. 部署拓扑与资源(修订 V3 第 5 章) ### 7.1 部署架构 ``` ┌─────────────────────────────┐ │ CDN (静态资源) │ └──────────────┬──────────────┘ │ ┌──────────────▼──────────────┐ │ Nginx / SLB │ │ 限流: 每 IP 200 req/s │ └──────────┬───────────────────┘ │ ┌─────────────────────┼─────────────────────┐ ▼ ▼ ▼ ┌────────────┐ ┌────────────┐ ┌────────────┐ │ tenant-api │ │ tenant-api │ │ (HPA 弹性) │ │ Pod #1 │ │ Pod #2 │ │ Pod #3 │ │ L1 cache │ │ L1 cache │ │ │ └──┬────┬─────┘ └──┬────┬─────┘ └──┬────┬─────┘ │ │ │ │ │ │ 查询读取 │ │ 投递消息 │ │ │ │ │ │ │ │ │ │ ┌──▼────┴─────────────────▼────┴─────────────────▼────┴──┐ │ Cache Redis (主从) │ │ 用途: API 缓存 + JWT 会话 + 任务分布式锁 │ └────────────────────────┬────────────────────────────────┘ │ ┌────────────────────────┼────────────────────────┬───────────────┐ │ │ │ │ ▼ ▼ ▼ ▼ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌──────────────┐ │ PG Primary │────────▶│ PG Replica │ │ MinIO │ │ RabbitMQ │ │ (读写) │ 流复制 │ (只读) │ │ (原始回答) │ │ (异步队列) │ └─────────────┘ └─────────────┘ └─────────────┘ └──────┬───────┘ ▲ ▲ │ │ │ │ │ │ 消费消息 │ │ │ ▼ ┌──────┴────────────────────────┴────────────────────────────────────────────┐ │ Worker 进程(与 API 独立部署) │ │ │ │ ┌─────────────────────────────────────────────────────────┐ │ │ │ Monitor Worker (单进程,多 goroutine) │ │ │ │ • ParseWorker × 3 ← consume: monitor.result.parse │ │ │ │ • AggregateWorker × 1 ← consume: monitor.aggregate │ │ │ │ • TaskGenWorker × 1 ← consume: monitor.task.generate │ │ │ │ • 写 PG Primary / 读 PG Replica / 写 MinIO │ │ │ └─────────────────────────────────────────────────────────┘ │ ╚═══════════════════════════════════════════════════════════════════════════╝ ┌─────────────────────────────────────┐ │ 浏览器插件 × 30,000(用户端) │ │ 替代 V3 的 Collector Pod × 3 │ │ 采集结果通过 HTTP 回传 API │ │ API 投递到 RabbitMQ 异步处理 │ └─────────────────────────────────────┘ ``` **对比 V3 的变更**: - 去掉 Collector Pod × 3(插件替代) - 去掉 Queue Redis Sentinel → 替换为 **RabbitMQ**(功能更强:死信队列、重试、多队列路由) - 新增 **Monitor Worker** 进程(消费 MQ 消息,执行解析/入库/聚合) - V3 的 Aggregator 独立进程合并到 Monitor Worker 中(AggregateWorker 消费者) ### 7.2 资源规格 | 组件 | 实例数 | 规格 | 月成本(阿里云) | | --- | --- | --- | --- | | **tenant-api** | 2(HPA 2-4) | 4C8G | ¥2,400 | | **Cache Redis** | 1 主 1 从 | 4G 内存 | ¥1,500 | | **RabbitMQ** | 1(镜像队列可选主从) | 2C4G | ¥800 | | **PG Primary** | 1 | 4C16G RDS | ¥2,500 | | **PG Replica** | 1 | 2C8G 只读 | ¥1,000 | | **MinIO / OSS** | 1 | 50G | ¥200 | | **Monitor Worker** | 1 | 2C4G | ¥600 | | **SLB + CDN** | - | - | ¥500 | | **合计** | | | **¥9,500/月** | ### 7.3 连接池修订 ```yaml # 每个 API 实例 database: max_open_conns: 20 # 2 API 实例 → PG 总连接: 40 # 1 Monitor Worker → PG 连接: 15 # 合计: 55 连接 # PostgreSQL: max_connections = 100 (留余量) ``` --- ## 8. 降级与容灾(扩展 V3 第 6 章) ### 8.1 采集侧降级 | 故障场景 | 降级措施 | 用户感知 | | --- | --- | --- | | 某 AI 平台改版导致 adapter 失效 | 该平台任务自动跳过,其他平台正常 | 该平台数据缺失,标记"暂不可用" | | 插件安装率不足 / 用户不在线 | 未完成任务次日继续;Dashboard 展示已采集数据 | 部分平台数据延迟 1-2 天 | | 采集完成率 < 70% | 告警通知;可选开启少量 API 采集补充 | 无感知(API 降级透明) | | AI 平台风控封禁用户 | 降低该平台采集频率;通知用户重新登录 | 该平台数据暂停 | | 全部 AI 平台不可用 | Dashboard 展示最后一次成功聚合数据 | 显示"数据更新时间" | ### 8.2 API 采集降级通道(可选) 保留 V3 的 API 采集能力作为降级通道。当插件采集完成率连续 3 天 < 70% 时,自动为该租户开启 API 采集: ```go // 降级判断(每日聚合后执行) func (s *MonitoringService) CheckCollectionCoverage(ctx context.Context, tenantID uuid.UUID) { rate := s.repo.GetCollectionCompletionRate(ctx, tenantID, 3) // 近 3 天 if rate < 0.70 { s.enableAPIFallback(ctx, tenantID) } } ``` ### 8.3 读侧降级(保留 V3 不变) V3 第 6 章的 Cache Redis 故障、PG 故障、Aggregator 异常等降级策略完全保留。 ### 8.4 监控告警 | 指标 | 告警阈值 | 说明 | | --- | --- | --- | | API P99 延迟 | > 2s 持续 5 分钟 | 同 V3 | | Cache Redis 命中率 | < 80% | 同 V3 | | PG 连接池使用率 | > 80% | 同 V3 | | 日采集完成率 | < 70% | **新增**,按平台分别告警 | | 单平台 adapter 连续失败 | > 50 次/小时 | **新增**,可能平台改版 | | 聚合 Job 未在 8:00 前完成 | 超时 | 同 V3 | | 待采集任务堆积 | > 50,000 pending | **新增**,可能插件采集能力不足 | --- ## 9. 插件方案的风险与反检测 ### 9.1 风险矩阵 | 风险 | 严重性 | 概率 | 对策 | | --- | --- | --- | --- | | AI 平台 Web 端改版 | 高 | 中 | adapter 独立维护,改版快速热更新;多平台冗余 | | AI 平台检测自动化行为 | 高 | 中 | 控制频率 ≤ 3 次/平台/小时;模拟真实行为;随机延迟 | | 用户未登录目标 AI 平台 | 中 | 高 | 跳过未登录平台;引导登录;多用户互补覆盖 | | 采集数据质量不一致 | 中 | 中 | 同一问题多次采集取中位;异常过滤 | | 采集延迟(依赖用户在线) | 中 | 中 | T+1 模型兼容延迟;次日继续 | ### 9.2 反检测策略 ```typescript const ANTI_DETECT_CONFIG = { // 频率控制 maxQueriesPerPlatformPerHour: 3, maxDailyQueries: 50, // 时间随机化 minIntervalSec: 30, maxIntervalSec: 120, // 行为模拟 typeDelay: { min: 50, max: 150 }, // 逐字输入延迟 ms postAnswerDelay: { min: 2000, max: 5000 }, // 回答后停留 ms // 请求特征 randomizeUserAgent: false, // 使用浏览器真实 UA addRandomHeaders: false, // 不添加额外头,避免特征 }; ``` --- ## 10. 压测验收标准(扩展 V3 第 7 章) ### 10.1 读侧压测(保留 V3 不变) V3 第 7 章的 S1-S5 场景、SLA 指标完全保留。 ### 10.2 新增:插件采集压测 | 场景 | 方法 | 目标 | | --- | --- | --- | | S7: 1000 并发插件实例同时回传结果 | 模拟 1000 个 POST /api/callback/plugin/monitor 并发 | P99 < 500ms,0 丢失 | | S8: 任务领取竞争 | 500 个并发 claim 同一任务 | 只有 1 个成功,其余 409 | | S9: 高频任务生成 | 生成 100K 采集任务 | 生成耗时 < 30s | ### 10.3 数据质量验证 | 验证项 | 方法 | 目标 | | --- | --- | --- | | 回答完整性 | 同一问题 3 次采集,对比回答长度 | 长度标准差 < 30% | | 品牌提及准确率 | 人工标注 100 条回答,对比解析结果 | 准确率 > 90% | | 引用提取率 | 有引用的平台,对比插件提取 vs 手动检查 | 召回率 > 85% | --- ## 11. 实施计划 ### 11.1 Phase 分期 | Phase | 内容 | 估时 | 依赖 | | --- | --- | --- | --- | | A | 插件 AI 适配器框架 + 首批 3 平台(DeepSeek/千问/豆包) | 8 天 | 无 | | B | 后端:Migration + 配额 + RabbitMQ 基础设施 + 采集回调 API + 插件认证 API | 8 天 | A(接口定义) | | C | 后端:Monitor Worker(ParseWorker + AggregateWorker)+ 缓存层 + 查询 API | 8 天 | B | | D | 前端:6 个数据追踪页面 + 采集控制面板 | 7 天 | C(API 就绪) | | E | 第二批 3 个 AI 适配器(Kimi/文心/混元)+ 联调 | 5 天 | A + D | | F | 压测 + 数据质量验证 + 验收 | 5 天 | E | | **合计** | | **41 天** | | Phase B 细分: - RabbitMQ 客户端封装 + Exchange/Queue 声明(1 天) - Migration(配额表 + 任务表 + AI 平台表 + Schema Delta)(2 天) - 插件认证 API(`/api/plugin/monitoring/tasks`、`/claim`)(2 天) - 采集回调 API(接收 → 投递 MQ)(1 天) - TaskGenerateWorker(凌晨批量生成任务)(2 天) Phase C 细分: - ParseWorker(MQ 消费 → 解析 → 入库 → MinIO)(3 天) - AggregateWorker(品牌级增量聚合)(2 天) - 缓存层(singleflight + stale-while-revalidate + L1)(1 天) - 查询 API(6 个页面 + composite)(2 天) 双人并行方案(前后端各 1 人): ``` Week 1-2: 后端 Phase B (MQ + Migration + API) | 插件 Phase A (适配器) Week 3-4: 后端 Phase C (Worker + 缓存 + 查询) | 前端 Phase D (页面) Week 5: 联调 Phase E | 第二批适配器 Week 6: 压测 Phase F 总计: 28 天 ``` ### 11.2 关键里程碑 | 时间点 | 里程碑 | 验收标准 | | --- | --- | --- | | Day 8 | 首个 AI 适配器可用 | DeepSeek 登录检测 + 提问 + 获取回答 E2E 通过 | | Day 10 | RabbitMQ 基础设施就绪 | Exchange/Queue 声明完成,Publish/Consume 冒烟测试通过 | | Day 16 | 后端采集链路通 | 插件回传 → MQ → ParseWorker → PG 入库全链路通过 | | Day 24 | 查询 API + 增量聚合可用 | 品牌任务完成后分钟级数据可见 | | Day 31 | 前端页面完成 | 6 个页面 + 采集面板可交互 | | Day 36 | 全部 6 平台适配器可用 | 全平台检测 + 采集 E2E | | Day 41 | 验收通过 | 读侧压测 SLA + MQ 吞吐压测 + 数据质量验证 | --- ## 12. V3 → V4 变更汇总 | 变更编号 | V3 方案 | V4 方案 | 原因 | | --- | --- | --- | --- | | **C1** | 服务端 AI API 采集(30 worker × 3 Pod) | 浏览器插件采集(30,000 用户端节点) | 消除 AI API 成本 | | **C2** | Queue Redis Stream 任务队列 | RabbitMQ 异步处理(解析/入库/聚合) + PG 任务表 + Redis 分布式锁 | 插件 HTTP 回传 → MQ 异步解耦,API 不阻塞 | | **C3** | 100 品牌 × 100 问题 | 配额限制(Free 1 品牌 / Pro 3 品牌,每品牌 40 问题) | 控制采集规模 | | **C4** | 全品牌每日采集 | Free 每 3 天 / Pro 每天 | 降低采集压力 | | **C5** | `run_mode`: api_standard / api_search_grounded | `run_mode`: plugin_standard / plugin_search | 采集来源变更 | | **C6** | 月成本 ~¥42K(含 API ¥22K) | 月成本 ~¥9.5K(无 API 费用) | 成本优化 77% | | **C7** | 无配额机制 | `tenant_monitoring_quotas` 租户级配额 | 支撑多租户规模化 | | **C8** | 3 个 Collector Pod + Queue Redis | Collector → 插件替代;Queue Redis → RabbitMQ + Monitor Worker | 采集去中心化,处理异步化 | | **C9** | 数据一致性高(固定 prompt/温度) | 数据更接近真实用户(Web 端采集) | 采集可行性方案确认 API ≠ Web | | **新增** | 无 | 反检测策略 | 插件采集必须的安全措施 | | **新增** | 无 | 采集控制面板(前端) | 用户管理采集进度和平台登录 | | **新增** | 无 | API 降级通道 | 插件采集不足时的兜底方案 | | **新增** | 无 | RabbitMQ 异步处理架构 | 解析/入库/聚合与 API 层解耦,插件回调 ~10ms 返回 | | **新增** | 每日 6:30 全量定时聚合 | 品牌级增量聚合(MQ 触发) | 数据可见延迟从 T+1 降至准实时(分钟级) | | **新增** | 无 | Monitor Worker 独立进程 | ParseWorker × 3 + AggregateWorker + TaskGenWorker | | **新增** | 无 | 死信队列 (DLQ) | 处理失败的消息暂存,不丢数据 |