diff --git a/docs/ai-brand-monitoring-tech-design-v5.md b/docs/ai-brand-monitoring-tech-design-v5.md index 799a3af..03d9386 100644 --- a/docs/ai-brand-monitoring-tech-design-v5.md +++ b/docs/ai-brand-monitoring-tech-design-v5.md @@ -2,42 +2,55 @@ ## 1. 文档信息 -| 项目 | 内容 | -| --- | --- | -| 文档名称 | AI 品牌曝光监测系统技术方案 V5(采样型趋势监测 + 异步解耦) | -| 文档版本 | V5.1 | -| 文档状态 | 待评审 | -| 创建日期 | 2026-04-08 | -| 基线文档 | `docs/ai-brand-monitoring-tech-design-v4.md` | -| 适用范围 | 数据追踪模块全部页面 | -| 关联文档 | `docs/geo-platform-prd-v1.md`(PRD 8.4 数据追踪) | -| 关联文档 | `docs/question-driven-monitoring-design-v1.md`(问题驱动模型) | -| 核心约束 | 单租户仅 1 个监测插件实例,插件所在设备不保证全天在线 | +| 项目 | 内容 | +| -------- | --------------------------------------------------------------------- | +| 文档名称 | AI 品牌曝光监测系统技术方案 V5(插件采样首期交付 / API 精确采集预留) | +| 文档版本 | V5.9 | +| 文档状态 | 待评审 | +| 创建日期 | 2026-04-09 | +| 基线文档 | `docs/ai-brand-monitoring-tech-design-v4.md` | +| 适用范围 | 数据追踪模块全部页面 | +| 关联文档 | `docs/geo-platform-prd-v1.md`(PRD 8.4 数据追踪) | +| 关联文档 | `docs/question-driven-monitoring-design-v1.md`(问题驱动模型) | +| 核心约束 | 单租户仅 1 个监测插件实例,插件所在设备不保证全天在线 | ## 2. V5 核心结论 -V5 不再把该能力定义为“全量、精确、准实时监控”,而是明确收敛为: +V5 在架构上定义了两种采集模式,但**首期只实现插件采集模式**: -1. **采样型趋势监测系统**,不是审计系统。 -2. **单实例插件 + 非全天在线设备**前提下,只承诺趋势参考,不承诺全量覆盖。 -3. **页面百分比全部按实际采样成功样本计算**,不再暗示“全量真实占比”。 -4. **页面必须展示采样覆盖信息**,包括计划采样数、实际采样数、覆盖率、最近更新时间。 -5. **引入 RabbitMQ 和独立 Monitoring PostgreSQL 隔离写链路**,但聚合仍以日批为主,不承诺准实时展示。 +1. **插件采集模式(首期实现)**:免费,依赖本地插件与设备在线率,结果用于趋势参考。 +2. **API 采集模式(`[V6]` 预留)**:付费,走服务端统一 provider 调用,结果作为精确口径。 +3. **数据模型和协议层预留双模式扩展点**,首期所有租户固定 `collection_mode = plugin`。 +4. **页面百分比按实际采样成功样本计算**,展示覆盖率和置信度。 +5. **引入 RabbitMQ 和独立 Monitoring PostgreSQL 隔离写链路**,插件结果通过统一 ingest 队列处理。 +6. **数据详情页按“每日单快照”展示**:用户可手动催发,同步存在自动调度,但同一自然日只保留一份展示快照。 ### 2.1 相比 V4 的根本变化 -| 维度 | V4 | V5 | -| --- | --- | --- | -| 产品定位 | 全量品牌曝光监测 | 采样型趋势参考 | -| 采集目标 | 尽量覆盖全部问题 × 全平台 | 每日仅覆盖活跃监测题池 | -| 插件节点假设 | 大量用户端节点可互补 | 单租户单主插件实例 | -| 数据时效 | 准实时 + T+1 | T+1 为主,允许延迟 | -| 数据精度承诺 | 接近真实全量 | 趋势可参考,精度受覆盖率影响 | -| 任务处理 | PG + Redis 锁 + RabbitMQ | Monitoring PG 任务表 + PG 租约状态机 + RabbitMQ 结果异步化 | -| 存储边界 | 单业务库承载配置与监测结果 | 主业务 PG 存配置,Monitoring PG 存监测数据 | -| 页面表达 | 指标卡直接展示百分比 | 指标卡 + 采样覆盖信息 + 置信度提示 | +| 维度 | V4 | V5(首期) | +| ------------ | -------------------------- | ----------------------------------------------------- | +| 产品定位 | 全量品牌曝光监测 | 插件采样趋势参考(数据模型预留 API 精确模式) | +| 采集目标 | 尽量覆盖全部问题 × 全平台 | 插件模式尽量覆盖,受预算和在线时长限制 | +| 插件节点假设 | 大量用户端节点可互补 | 单租户单主插件实例 | +| 数据时效 | 准实时 + T+1 | T+1 为主,允许延迟 | +| 数据精度承诺 | 接近真实全量 | 趋势参考,精度受覆盖率影响 | +| 任务处理 | PG + Redis 锁 + RabbitMQ | Monitoring PG 任务表 + 插件租约 + RabbitMQ 结果异步化 | +| 存储边界 | 单业务库承载配置与监测结果 | 主业务 PG 存配置,Monitoring PG 存监测数据 | +| 页面表达 | 指标卡直接展示百分比 | 指标卡 + 采样覆盖信息 + 置信度提示 | -### 2.2 非目标 +### 2.2 首期实施范围 + +V5.9 首期**只实现插件采集模式**。API 采集模式在数据模型和协议层预留扩展点,但不进入首期实施: + +1. 不实现 API provider adapter 和 API worker +2. 不实现 API 模式三级限流 +3. 不实现 `collection_mode` 切换状态机和切换 UI +4. 不对外暴露模式选择入口,所有租户固定 `collection_mode = plugin` +5. 数据模型中的 `collector_type`、`collection_mode` 字段首期保留并写入 `plugin`,为后续扩展做好铺垫 + +文档中标注 **`[V6]`** 的段落为预留设计,首期跳过实现。 + +### 2.3 非目标 V5 明确不做以下承诺: @@ -54,9 +67,9 @@ V5 明确不做以下承诺: 数据追踪页展示的是: -- 在“当前监测题池”上的 AI 曝光趋势 -- 在“实际采样成功样本”上的品牌表现 -- 在“可归因引用样本”上的文章引用趋势 +- 在当前采集模式下的 AI 曝光结果 +- 在当前采集模式下的品牌表现 +- 在当前采集模式下的文章引用趋势 不是: @@ -67,31 +80,139 @@ V5 明确不做以下承诺: 为避免误导,数据追踪页和 Dashboard composite 接口必须返回并展示: -1. `planned_sample_count`:计划采样数 -2. `actual_sample_count`:实际采样成功数 -3. `coverage_rate`:覆盖率 = `actual / planned` -4. `last_sampled_at`:最近成功采样时间 -5. `confidence_level`:`high / medium / low` +1. `collection_mode`:`plugin | api` +2. `desired_sample_count`:按全部启用问题推导出的目标采样数 +3. `planned_sample_count`:受预算截断后实际下发的计划采样数 +4. `actual_sample_count`:实际采样成功数 +5. `coverage_rate`:覆盖率 = `actual / planned` +6. `last_sampled_at`:最近成功采样时间 +7. `confidence_level`:`high / medium / low` +8. `last_trigger_source`:最近一次成功更新来自 `manual | automatic` 推荐前端呈现方式: -- 顶部右侧信息条:`采样模式` -- 趋势图上方副文案:`已采样 18/30,覆盖率 60%,更新于 04-08 09:20` -- 覆盖率过低时展示 `样本不足,仅供参考` +- 顶部右侧信息条:首期展示 `插件采样`;`API 精确采集` 为 `[V6]` 预留 +- 插件模式副文案:`目标 60,计划 30,已采样 18,覆盖率 60%,更新于 04-08 09:20 · 自动` +- `[V6]` API 模式副文案:`已按 API 精确模式执行,更新于 04-08 09:20` +- 仅插件模式下展示 `样本不足,仅供参考` +- 品牌时序区首期使用**离散采样时序视图**,不使用连续折线图 +- 数据详情页提供 `立即采集` 入口,但文案必须明确:**同一自然日只保留最新一份有效快照** ### 3.3 低覆盖场景的展示规则 -| 覆盖率 | 置信度 | 页面表现 | -| --- | --- | --- | -| >= 70% | high | 正常展示百分比和趋势 | -| 40% - 69% | medium | 展示百分比,并提示“样本偏少” | -| < 40% | low | 保留原始计数,百分比弱化展示或标记“仅供参考” | +| 覆盖率 | 置信度 | 页面表现 | +| --------- | ------ | -------------------------------------------- | +| >= 70% | high | 正常展示百分比和趋势 | +| 40% - 69% | medium | 展示百分比,并提示“样本偏少” | +| < 40% | low | 保留原始计数,百分比弱化展示或标记“仅供参考” | -当 `actual_sample_count < 5` 时: +当 `collection_mode = plugin` 且 `actual_sample_count < 5` 时: 1. 不展示“精确到整数”的百分比变化解释。 -2. 趋势线可以显示,但需打低置信度标记。 +2. 不使用连续趋势线,改为“快照脉冲条 + 当期聚合指标”。 3. 引用排行允许为空,不用补零。 +4. 当日尚无有效快照时,页面显示“等待下一次有效快照”,而不是大面积重复展示“未采样”字样。 + +### 3.4 数据详情页首屏设计 + +首期数据详情页采用**「结果驱动 + 按需探索」**信息层次,主视觉聚焦品牌最新表现,而非采样过程: + +1. **第一层:顶部筛选 + 操作区** + - 筛选器:品牌、关键词 + - 信息条:`插件采样 · 最近更新: 04-08 09:20 · 自动` + - 操作区:`立即采集` 按钮 + - 模型筛选器仅影响指标卡数值,不影响平台对比矩阵 +2. **第二层左:最新快照概览(约 60% 宽度)** + - 4 个指标卡(2x2 网格):提及率、首位提及率、首选推荐率、正面提及率 + - 每个卡片内容:指标名 + 大号百分比数字 + 趋势箭头(对比上一份有效快照)+ mini sparkline 离散点(最近 N 个有效快照点位,不连线) + - 当窗口内有效快照 < 2 时,不展示趋势箭头,只展示当前值 + - 当窗口内无有效快照时,指标卡显示 `--`,mini sparkline 区域留白 +3. **第二层右:平台对比矩阵(约 40% 宽度)** + - 紧凑表格,列头:`平台 | 提及率 | 首位提及 | 状态` + - 始终展示全部已接入平台,不受模型筛选器影响 + - 状态值:`已采样 / 等待采样 / 未登录 / 不可用` + - 已采样平台展示指标数值;未采样或未登录平台对应指标显示 `--` + - 点击某平台行可展开该平台的详细采样记录(tooltip 或抽屉) +4. **第三层左:采样覆盖条(约 60% 宽度)** + - 水平进度条:展示覆盖率百分比 + - 文案:`目标 30 · 已采样 21 · 覆盖率 70%` + - 日指示点行:最近 N 天每天一个圆点,实心点 = 有效快照日,空心点 = 无数据日(类 GitHub 贡献图极简版) + - 日指示点悬浮时 tooltip 展示该日 `sample_status / trigger_source / actual_sample_count` +5. **第三层右:高频问题(约 40% 宽度,可搜索)** + - 顶部搜索框,搜索范围为当前所选关键词下的全部问题 + - 问题列表:按采样成功次数排序,每行展示问题文案 + 采样次数 + 提及率 + - 每个问题行右侧展示展开图标,点击后弹出**问题详情面板**(见 3.5 节) + - 列表底部提供链接跳转到品牌词库问题管理,方便用户提报新问题 +6. **第四层:底部明细区** + - 引用排行榜(保留原设计):饼图 + 模型平台引用表格(模型平台 | 引用文章数 | 引用率) + - 被引用文章列表 + +设计原则: + +1. 主视觉传达品牌在各 AI 平台的最新表现,而非采样过程是否完整。 +2. 采样覆盖信息降维为第三层的辅助进度条,不喧宾夺主。 +3. 未采样日不以 `0` 值或错误提示呈现,而是日指示点留空 + tooltip 按需解释。 +4. `sample_status` 只作为日指示点 tooltip 和 API 字段,不作为逐日主文案。 +5. 平台对比矩阵是品牌监测最有价值的洞察维度,独立成区常驻展示。 +6. 高频问题支持搜索和问题提报闭环,实现问题驱动的监控体验。 + +### 3.5 高频问题展开详情面板设计 + +![](refer/问题驱动的问题数据.png) + +每个高频问题均可点击展开,展示该问题在各 AI 平台的完整回答及引用信息。详情面板分为以下四个区域: + +#### 3.5.1 答案内容区 + +- **位置**:面板左上主区域(约 60% 宽度) +- **内容**:展示当前所选 AI 平台对该问题的完整回答原文(支持 Markdown 渲染) +- **平台切换器**:面板右上角展示平台切换按钮组(如 `DeepSeek` / `通义千问` / `豆包`),支持左右箭头翻页或 Tab 点击 + - 切换时答案内容区实时更新为对应平台的回答原文 + - 若某平台当日未采样或登录态失效,对应 Tab 显示为灰色,回答区展示 `暂无数据` + - 默认展示最近采样成功的平台回答 +- **回答元信息**:回答区底部展示:`平台名称 · 采样时间 · 模型版本(如有)` +- **品牌高亮**:回答中涉及当前监测品牌的词汇自动高亮显示 + +#### 3.5.2 引用来源区 + +- **位置**:面板右侧列表区(约 40% 宽度,与答案内容区并排) +- **内容**:展示当前所选平台在该问题回答中引用的外部链接列表 +- **每条引用**展示: + - 来源站点图标(favicon)+ 来源站点名称(`registrable_domain` 映射后的 `site_name`) + - 引用 URL 标题(截断展示,完整 title 可 hover 查看) + - 引用 URL 链接(可点击跳转) + - 是否为本品牌内部文章(标记 `内容引用` 徽标) +- **空状态**:若当前平台回答无可解析引用,展示 `该平台无引用来源` +- 引用来源区随平台切换联动更新 + +#### 3.5.3 引用分析区 + +- **位置**:面板下半部分左侧(答案内容区下方) +- **内容**:以表格形式汇总该问题在各平台的引用来源分布 +- **表格列**:`来源平台 | 引用次数 | 引用站点数 | 是否含品牌内容` +- **表格行**:每行对应一个已有引用记录的 AI 平台(按引用次数降序) +- **说明**:该分析基于当前时间窗口内所有采样成功记录中该问题的全部引用事实聚合,而不仅限于当日单次回答 +- 若当日仅有一次采样,引用分析等同于单次引用列表;若多次采样(手动 + 自动),则聚合所有采样的引用事实 + +#### 3.5.4 内容引用区 + +- **位置**:面板下半部分右侧(引用来源区下方) +- **内容**:展示在该问题回答中被 AI 引用的品牌内部文章列表 +- **每条内容引用**展示: + - 文章标题(可点击进入文章管理页面) + - 发布平台名称 + - 被引用次数(当前时间窗口内) + - 引用率(`该文章引用次数 / 该问题采样成功次数`) +- **空状态**:若无内部文章被引用,展示 `暂无内容引用` +- 内容引用区随平台切换联动更新(仅展示当前所选平台的内容引用记录) + +#### 3.5.5 面板交互规则 + +1. 点击问题行展开详情面板,再次点击或点击面板外区域收起 +2. 同一时刻只有一个问题详情面板处于展开状态 +3. 面板展开时,问题列表区域内容保持可见和可滚动 +4. 平台切换器 Tab 状态在面板展开期间持久化,切换问题时重置为默认平台 +5. 详情面板内数据加载失败时展示错误提示,不影响外层问题列表展示 --- @@ -99,46 +220,71 @@ V5 明确不做以下承诺: ### 4.1 套餐定义 -V5 中配额控制的不是“题库总量”,而是“每日活跃监测问题数”。 +V5.9 架构支持双采集模式,但**首期只开放插件模式**。 -| 配额项 | Free | Pro | Enterprise | -| --- | --- | --- | --- | -| 最大品牌数 | 1 | 3 | 自定义 | -| 题库总量 | 不限于监测能力,只做配置资产 | 不限于监测能力,只做配置资产 | 自定义 | -| **每日活跃监测问题数** | **3-5** | **5-10** | 自定义 | -| 采集频率 | 每天 | 每天 | 每天 / 自定义 | -| 平台范围 | 当前已接入且已登录的平台 | 当前已接入且已登录的平台 | 同左 | -| 数据承诺 | 趋势参考 | 趋势参考 | 趋势参考 / 专属增强 | +| 模式 | 计费 | 数据特征 | 适用场景 | 首期状态 | +| -------- | ---- | -------------------------------- | -------- | ------------ | +| `plugin` | 免费 | 依赖插件在线率,结果可能不稳定 | 趋势参考 | **首期实现** | +| `api` | 付费 | 服务端统一执行,结果更稳定更准确 | 精确监测 | `[V6]` 预留 | -### 4.2 活跃监测题池 +首期规则: -系统将问题分成两层: +1. 所有租户固定 `collection_mode = plugin`,不提供切换入口 +2. 数据模型保留 `collection_mode` / `collector_type` 字段,查询始终带模式过滤条件 +3. 后续引入 API 模式时,只需实现 adapter + worker,不需要迁移历史数据 -1. **问题题库**:品牌下维护的全部问题,仅做配置和运营资产。 -2. **活跃监测题池**:每日实际参与采样的问题集合,数量受套餐限制。 +| 配额项 | Free | Pro | Enterprise | +| ------------ | ------------------------------ | ------------------------------ | --------------------------------- | +| 最大品牌数 | 1 | 3 | 自定义 | +| 题库总量 | 不限于监测能力,只做配置资产 | 不限于监测能力,只做配置资产 | 自定义 | +| 问题覆盖策略 | 尽量覆盖全部启用问题 | 尽量覆盖全部启用问题 | 自定义 | +| 采集频率 | 每天 | 每天 | 每天 / 自定义 | +| 平台范围 | 当前已接入且满足访问条件的平台 | 当前已接入且满足访问条件的平台 | 同左 | +| 采集模式 | `plugin`(首期唯一) | `plugin`(首期唯一) | `plugin`;`api` `[V6]` | +| 数据承诺 | 趋势参考 | 趋势参考 | 趋势参考;API 模式精确口径 `[V6]` | -推荐实现: +### 4.2 问题执行策略 -- `brand_questions` 保留全部题库 -- 新增 `monitor_priority`、`monitor_enabled` -- 调度器每日只选 Top N 问题进入任务生成 +系统不再限制“每日活跃问题数”,而是采用 **尽量覆盖全部启用问题** 的策略。 + +实现原则: + +1. `brand_questions` 保留全部问题题库 +2. 只要问题为 `monitor_enabled = true`,就进入当日候选集合 +3. 调度器会尽量让每个品牌的全部启用问题去各个模型提问 +4. 若当日预算、在线时长或平台可访问性不足,则按优先级做 best-effort 截断 + +这意味着: + +1. 约束从“问题数限制”转成“执行预算限制” +2. 对用户的承诺是“尽量多覆盖问题”,不是“每天只跑固定几题” +3. 页面通过 `desired_sample_count / planned_sample_count / actual_sample_count` 解释覆盖差异 ### 4.3 单实例前提下的目标任务量 -按首批 6 个平台测算: +按“每个品牌尽量覆盖全部启用问题 × 全部可访问平台”的原则,理论任务量为: -| 套餐 | 活跃问题数 | 平台数 | 目标计划任务/天 | -| --- | --- | --- | --- | -| Free | 3-5 | 6 | 18-30 | -| Pro | 5-10 | 6 | 30-60 | +```text +desired_sample_count = + 启用品牌数 + × 每品牌启用问题数 + × 当日可访问平台数 +``` + +但实际下发任务量受 `task_daily_hard_cap` 和设备在线约束影响: + +```text +planned_sample_count = min(desired_sample_count, task_daily_hard_cap, runtime_accessible_capacity) +``` 说明: -1. 这是 **计划采样量**,不是保证完成量。 -2. 实际完成量取决于: +1. `desired_sample_count` 代表理想覆盖量。 +2. `planned_sample_count` 才是当天真正发出的任务量。 +3. 实际完成量取决于: - 主监测插件是否在线 - 插件机器当日在线时长 - - 对应 AI 平台是否已登录 + - 平台是否要求登录,以及对应登录态是否可用 - 平台风控和采集成功率 ### 4.4 核心调度原则 @@ -148,8 +294,9 @@ V5 的调度器不追求“补齐所有漏采任务”,而追求“稳定输 因此: 1. 每天只生成当天计划任务,不补历史大积压。 -2. 超过 48 小时未完成的任务自动过期,不回补到新一天的分母中。 -3. 读侧按 `planned_sample_count` 和 `actual_sample_count` 共同解释结果。 +2. **在线就采集,不在线就放弃**;当日未执行任务不顺延、不补跑。 +3. 超过当天统计窗口的未执行任务直接过期,不回补到新一天的分母中。 +4. 读侧按 `planned_sample_count` 和 `actual_sample_count` 共同解释结果。 ### 4.5 主插件在线约束 @@ -160,12 +307,23 @@ V5 的调度器不追求“补齐所有漏采任务”,而追求“稳定输 - `last_seen_at <= 24h`:生成当天任务 - `last_seen_at > 24h`:不生成新任务,页面沿用最近一次成功数据并提示“近期无新采样” +其中: + +- `last_seen_at` 由插件心跳接口持续刷新 +- 插件心跳除刷新安装实例活跃时间外,还同步上报监测平台 `detect()` 结果,供 `monitoring_platform_access_snapshots` 更新 + 这条规则的目的: 1. 防止设备长期离线时任务无限堆积 2. 防止“未执行任务”污染覆盖率统计 3. 让系统行为和单实例现实约束一致 +这意味着: + +1. 系统不是“补数型监控”,而是“机会型采样” +2. 用户离线期间缺失的数据天然放弃 +3. 趋势连续性依赖设备在线率,而不是依赖补采机制 + --- ## 5. 数据口径定义 @@ -174,43 +332,109 @@ V5 的调度器不追求“补齐所有漏采任务”,而追求“稳定输 所有百分比的分母统一为 **实际采样成功回答数**,不是计划任务数,也不是题库总数。 -| 指标 | 定义 | -| --- | --- | -| 提及率 | `mentioned_count / actual_sample_count` | -| 首位提及率 | `top1_mentioned_count / actual_sample_count` | -| 首选推荐率 | `first_recommended_count / actual_sample_count` | +前提: + +1. 所有口径都必须在同一 `collection_mode` 下计算 +2. 插件模式和 API 模式的数据禁止在同一指标中混算 + +| 指标 | 定义 | +| ---------- | ------------------------------------------------ | +| 提及率 | `mentioned_count / actual_sample_count` | +| 首位提及率 | `top1_mentioned_count / actual_sample_count` | +| 首选推荐率 | `first_recommended_count / actual_sample_count` | | 正面提及率 | `positive_mentioned_count / actual_sample_count` | -| 品牌可见率 | 与提及率同源,可作为 UI 别名展示 | -| 文章引用率 | `cited_answer_count / actual_sample_count` | +| 品牌可见率 | 与提及率同源,可作为 UI 别名展示 | +| 文章引用率 | `cited_answer_count / actual_sample_count` | -### 5.2 趋势图口径 +### 5.2 品牌时序视图口径 -趋势图展示的是按天聚合后的样本表现,不是平台真实总体份额。 +首期 plugin 模式下,品牌数据**不适合使用连续折线图**,因为插件不保证 24h 在线,日样本天然可能中断。 -每个点必须同时具备: +V5.9 将时序展示从独立主图区降维为两个辅助元素: +1. **指标卡内 mini sparkline**:在最新快照概览的每个指标卡右侧展示离散点,每个点代表一个有效快照日的 `metric_value`,点之间不连线 +2. **采样覆盖条内日指示点行**:最近 N 天每天一个圆点,实心 = 有效快照日,空心 = 无数据日 + +数据层仍沿用完整时间桶结构,每个时间桶必须同时具备: + +- `date` +- `sample_status`:`sampled | partial | unsampled` - `metric_value` +- `planned_sample_count` - `actual_sample_count` - `coverage_rate` +- `trigger_source`:`manual | automatic | null` +- `snapshot_updated_at` -可选增强: +展示规则: -- 默认展示 7 天滚动均值 -- 用户切换后可看原始日值 +1. `sample_status = unsampled` 时,`metric_value = null`,mini sparkline 跳过该日(不画点),日指示行渲染为空心点 +2. `sample_status = partial` 时,mini sparkline 画半透明点,日指示行渲染为半实心点 +3. `sample_status` 只作为日指示点 tooltip 的解释依据,不作为任何逐日直出标签 +4. 只有 API 精确模式 `[V6]` 才可考虑恢复独立连续折线图 +5. 同一 `business_date` 只展示一个日快照;手动催发成功后刷新当日点位,不追加第二个点 + +首期推荐: + +1. 默认展示最近 7/14/30 天的时间桶 +2. mini sparkline 最多展示最近 7 个有效快照点,超出部分截断 +3. 当窗口内 `sampled + partial < 2` 时,mini sparkline 不展示,指标卡只展示当前值不展示趋势箭头 + +### 5.2A 平台对比矩阵口径 + +平台对比矩阵为 V5.9 新增的展示区域,用于直观对比各 AI 平台的品牌表现差异。 + +数据来源: + +1. 按 `(tenant_id, brand_id, business_date, ai_platform_id)` 维度从 `monitoring_*_daily` 汇总 +2. 始终展示全部已接入平台,不受页面模型筛选器影响 +3. 各平台独立计算 `mention_rate / top1_mention_rate / actual_sample_count` + +状态判定规则: + +| 条件 | `platform_sample_status` | 说明 | +| --------------------------------------------------------- | ------------------------ | ------------ | +| `actual_sample_count >= 1` | `sampled` | 已有有效样本 | +| 平台在计划内但尚未采样 | `pending` | 等待采样 | +| 平台 `auth_requirement = login_required` 且未检测到登录态 | `not_logged_in` | 未登录 | +| 平台 adapter 不可用 | `unavailable` | 不可用 | + +展示规则: + +1. `sampled` 状态展示具体指标数值 +2. 其他状态指标列显示 `--` +3. 平台行默认按 `actual_sample_count` 降序排列 +4. 悬浮或点击平台行可查看该平台最近采样详情 ### 5.3 高频问题口径 -高频问题区不再解释为“全站高频触发问题”,而是: +高频问题区不再解释为"全站高频触发问题",而是: > 在当前时间窗口内,被实际采样成功次数最多的问题 -推荐展示字段: +**问题列表展示字段:** - 问题文案 - 成功采样次数 - 提及率 - 最近采样时间 +**问题详情面板数据口径:** + +问题详情面板通过独立接口(见 12.1A 节)按需加载,数据口径如下: + +1. **答案内容**:按 `ai_platform_id` 分组,取最近一次 `status = succeeded` 的 `raw_answer_text`;随平台切换按需请求,不在首屏预加载全部平台回答 +2. **引用来源**:从 `monitoring_citation_facts` 按 `(run_id, ai_platform_id)` 聚合,每条引用对应 `cited_url / site_name / article_id`;随平台切换联动更新 +3. **引用分析**:按 `ai_platform_id` 聚合,跨平台汇总展示各平台 `citation_count / distinct_site_count / has_brand_article_citation`;不受平台切换过滤影响,始终展示全平台汇总 +4. **内容引用**:关联 `monitoring_article_url_aliases`,过滤 `article_id IS NOT NULL`,聚合 `article_citation_count` 和 `article_citation_rate = article_citation_count / question_sample_count`;随平台切换联动 + +**口径边界:** + +- 引用分析和内容引用的聚合时间窗口与外层问题列表保持一致(默认当日快照) +- 若当日有多次采样(手动 + 自动),则聚合全部成功运行的引用事实 +- 平台切换仅影响「答案内容」和「引用来源」;「引用分析」默认展示所有平台的聚合不随切换变化 +- 「内容引用」随平台切换联动,仅展示当前所选平台中归因成功的内部文章记录 + ### 5.4 引用排行口径 引用排行按模型平台展示: @@ -219,9 +443,9 @@ V5 的调度器不追求“补齐所有漏采任务”,而追求“稳定输 - 引用文章数 - 引用率 -若平台当日无样本或平台未登录: +若平台当日无样本,或平台要求登录但当前未登录: -- 展示为 `未采样` +- 在 tooltip 或侧边说明中展示为 `暂无快照` - 不强行按 `0` 并入分母 --- @@ -230,7 +454,7 @@ V5 的调度器不追求“补齐所有漏采任务”,而追求“稳定输 ### 6.1 双 PostgreSQL 存储边界 -V5.1 将数据按“业务配置”和“监测结果”拆到两个 PostgreSQL: +V5.9 将数据按“业务配置”和“监测结果”拆到两个 PostgreSQL: **主业务 PostgreSQL** @@ -242,14 +466,19 @@ V5.1 将数据按“业务配置”和“监测结果”拆到两个 PostgreSQL - `publish_records` - `plugin_installations` - `tenant_monitoring_quotas` +- `tenant_collection_mode_switches` `[V6]` +- `monitoring_config_outbox` **Monitoring PostgreSQL** - `monitoring_collect_tasks` +- `monitoring_question_config_snapshots` +- `monitoring_platform_access_snapshots` - `question_monitor_runs` - `question_monitor_parse_results` - `monitoring_citation_facts` - `monitoring_article_url_aliases` +- `site_domain_mappings` - `monitoring_*_daily` 设计原则: @@ -257,6 +486,7 @@ V5.1 将数据按“业务配置”和“监测结果”拆到两个 PostgreSQL 1. 不做跨库物理外键,只保留逻辑 ID 引用。 2. 监测链路的高频写入、重试、归档不影响主业务 PG。 3. 读侧监测查询优先从 Monitoring PG 获取。 +4. 当前代码库只有单数据库配置与单套 sqlc 产物,V5.9 需新增 `monitoring_database` 配置、第二个 `pgxpool.Pool`、第二套 sqlc query/codegen。 ### 6.2 `question_version_id` 移除后的替代方案 @@ -264,19 +494,31 @@ V5 接受原系统不再维护 `question_version_id`,但仍然保留“输入 替代方案: -1. 在任务表和运行表写入 `question_text_snapshot` -2. 同时写入 `question_hash` -3. 历史汇总按 `question_hash` 区分口径 +1. 任务生成时在任务表写入 `question_hash` +2. 运行结果入库时在运行表写入 `question_text_snapshot` +3. 历史汇总按 `question_hash` 区分口径,必要时可回查 `question_text_snapshot` 这样即使 `question_id` 不变、文案发生修改,历史数据也不会被新文案污染。 +实现建议: + +1. 文档中继续使用 `question_hash` 表达语义 +2. 实际落库使用定长二进制指纹或等价短哈希,而不是 `VARCHAR(64)` +3. 任务表负责调度,尽量不承载大文本;`question_text_snapshot` 只保留在运行表 +4. 任务执行时需要的题目文案,由服务端按 `(tenant_id, question_id, question_hash)` 从 `monitoring_question_config_snapshots` 读取并组装到执行请求 +5. Go 层建议封装 `QuestionHash` 值对象,统一存储层 `[]byte` 与 API 层字符串之间的编解码,避免 sqlc 写入点散落转换逻辑 +6. 目的是保留版本语义,同时控制复合索引体积和任务表膨胀 +7. `question_hash` 统一建议使用 `SHA-256` 前 `16` 字节截断(`128bit`)的定长二进制指纹;在 `question_id` 已参与主键约束的前提下,碰撞概率可忽略 +8. 存储层落为 Monitoring PG 的 `BYTEA(16)` 等价二进制指纹;API / JSON / MQ envelope 的 wire format 统一为 `v1:`,其中 `v1` 表示当前哈希算法与编码版本 + ### 6.3 `tenant_monitoring_quotas` ```sql CREATE TABLE tenant_monitoring_quotas ( tenant_id BIGINT PRIMARY KEY REFERENCES tenants(id), max_brands INT NOT NULL DEFAULT 1, - daily_active_question_limit INT NOT NULL DEFAULT 5, + collection_mode VARCHAR(20) NOT NULL DEFAULT 'plugin', + question_selection_mode VARCHAR(20) NOT NULL DEFAULT 'best_effort_all', collect_frequency VARCHAR(20) NOT NULL DEFAULT 'daily', enabled_platforms JSONB NOT NULL DEFAULT '["deepseek","qwen","doubao"]', primary_installation_id BIGINT REFERENCES plugin_installations(id), @@ -289,17 +531,19 @@ CREATE TABLE tenant_monitoring_quotas ( 建议默认值: -- Free:`daily_active_question_limit = 5`,`task_daily_hard_cap = 36` -- Pro:`daily_active_question_limit = 10`,`task_daily_hard_cap = 72` +- Free:`collection_mode = plugin`,`question_selection_mode = best_effort_all`,`task_daily_hard_cap = 36` +- Pro:`collection_mode = plugin`,`question_selection_mode = best_effort_all`,`task_daily_hard_cap = 72` 说明: 1. `tenant_monitoring_quotas` 放在主业务 PostgreSQL。 2. `primary_installation_id` 继续引用主业务库中的 `plugin_installations`。 +3. `collection_mode` 首期固定写入 `plugin`,字段保留是为了后续 API 模式 `[V6]` 扩展时无需迁移。 +4. `question_selection_mode` 当前默认 `best_effort_all`,预留模式位。 ### 6.4 `monitoring_collect_tasks` -V5.1 使用 Monitoring PG 单表状态机进行任务租约,并用 RabbitMQ 解耦结果写入。 +V5.9 使用 Monitoring PG 单表状态机进行任务租约,并用 RabbitMQ 解耦结果写入。 ```sql CREATE TABLE monitoring_collect_tasks ( @@ -307,26 +551,28 @@ CREATE TABLE monitoring_collect_tasks ( tenant_id BIGINT NOT NULL, brand_id BIGINT NOT NULL, question_id BIGINT NOT NULL, - question_text_snapshot TEXT NOT NULL, - question_hash VARCHAR(64) NOT NULL, + question_hash BYTEA NOT NULL, ai_platform_id VARCHAR(30) NOT NULL, + collector_type VARCHAR(20) NOT NULL DEFAULT 'plugin', + trigger_source VARCHAR(20) NOT NULL DEFAULT 'automatic', run_mode VARCHAR(30) NOT NULL DEFAULT 'plugin_standard', business_date DATE NOT NULL, planned_at TIMESTAMPTZ NOT NULL DEFAULT now(), status VARCHAR(20) NOT NULL DEFAULT 'pending', lease_token_hash VARCHAR(128), - leased_to_installation BIGINT, + leased_to_executor VARCHAR(100), leased_at TIMESTAMPTZ, lease_expires_at TIMESTAMPTZ, callback_received_at TIMESTAMPTZ, completed_at TIMESTAMPTZ, skip_reason VARCHAR(50), + request_payload_json JSONB, error_message TEXT, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), CONSTRAINT uk_collect_task_idempotent - UNIQUE (tenant_id, brand_id, question_id, question_hash, ai_platform_id, run_mode, business_date) + UNIQUE (tenant_id, brand_id, question_id, ai_platform_id, collector_type, run_mode, business_date) ); CREATE INDEX idx_collect_tasks_lease @@ -343,40 +589,183 @@ CREATE INDEX idx_collect_tasks_lease - `expired` - `failed` +补充说明: + +1. `trigger_source` 表示该任务最终由“自动调度”还是“用户手动催发”进入执行链路。 +2. 首期**不为手动采集创建第二套同日任务**;手动采集的本质是把当日尚未完成的任务提权/刷新为 `manual` 来源。 +3. 因为同日同题同平台仍只有一条任务记录,所以页面天然只会生成一个日快照点位。 +4. 同一 `business_date` 内若问题文案发生修改,已生成任务**不重新分裂出第二条同日任务**;任务继续使用生成时锁定的 `question_hash` + 说明: 1. 任务租约状态在 Monitoring PG 中维护。 -2. `received` 表示 API 已接收插件结果并成功投递 MQ,但 worker 尚未完成最终入库。 +2. `received` 表示执行通道已把结果成功提交到 ingest 队列,但 worker 尚未完成最终入库。 +3. `collector_type` 首期固定为 `plugin`,字段保留为 `[V6]` API 模式扩展预留。 +4. `leased_to_executor` 首期填入插件 installation ID;后续 API 模式 `[V6]` 可填入 API worker 标识。 +5. `question_hash` 仍然落库,用于锁定该任务对应的题目版本和后续运行快照,但不再参与同日任务幂等键 +6. 问题文案修改从**下一个 `business_date`** 开始生效;不追溯重写当日已创建任务 -### 6.5 `question_monitor_runs` +### 6.5 `monitoring_question_config_snapshots` -在 V5 中至少补充: +该表属于 Monitoring PG 中的**配置快照表**,职责是为任务生成、租约接口和 API 执行器提供“只读且不可变”的问题版本视图。 ```sql -ALTER TABLE question_monitor_runs - ADD COLUMN IF NOT EXISTS tenant_id BIGINT, - ADD COLUMN IF NOT EXISTS business_date DATE, - ADD COLUMN IF NOT EXISTS run_mode VARCHAR(30), - ADD COLUMN IF NOT EXISTS question_text_snapshot TEXT, - ADD COLUMN IF NOT EXISTS question_hash VARCHAR(64), - ADD COLUMN IF NOT EXISTS provider_model VARCHAR(100), - ADD COLUMN IF NOT EXISTS request_id VARCHAR(100), - ADD COLUMN IF NOT EXISTS raw_answer_storage_key TEXT; +CREATE TABLE monitoring_question_config_snapshots ( + id BIGSERIAL PRIMARY KEY, + tenant_id BIGINT NOT NULL, + brand_id BIGINT NOT NULL, + question_id BIGINT NOT NULL, + keyword_id BIGINT, + question_hash BYTEA NOT NULL, + question_text_snapshot TEXT NOT NULL, + monitor_enabled BOOLEAN NOT NULL DEFAULT true, + superseded_at TIMESTAMPTZ, + deleted_at TIMESTAMPTZ, + projected_at TIMESTAMPTZ NOT NULL DEFAULT now(), + created_at TIMESTAMPTZ NOT NULL DEFAULT now(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), + + CONSTRAINT uk_monitoring_question_snapshot + UNIQUE (tenant_id, question_id, question_hash) +); + +CREATE INDEX idx_monitoring_question_snapshot_active +ON monitoring_question_config_snapshots(tenant_id, brand_id, monitor_enabled, deleted_at, superseded_at); ``` -建议幂等键: +约束: + +1. 调度器生成任务只读取 `monitor_enabled = true`、`deleted_at IS NULL` 且 `superseded_at IS NULL` 的**当前版本**快照记录。 +2. 租约接口和 API 执行器装配题目文案时,只读取这张快照表,不回源主业务 PG。 +3. 问题文案变更时,projector 先 `INSERT` 新 `question_hash` 快照,再将旧当前版本标记 `superseded_at = now()`;不覆盖历史版本。 +4. `brand_question.updated` 只负责“新版本追加 + 旧当前版本 supersede”,不会把历史快照物理删除。 +5. `brand_question.disabled / brand.deleted` 负责把当前快照标记 `deleted_at`,而不是复用 `superseded_at` 语义。 + +### 6.5A `monitoring_platform_access_snapshots` + +该表用于沉淀主监测插件最近一次探测到的“平台可访问性”快照,作为调度器与读侧共享的状态源,避免页面根据“有没有成功运行记录”反推 `未登录 / 不可用`。 ```sql -CREATE UNIQUE INDEX uk_monitor_run_idempotent -ON question_monitor_runs(tenant_id, brand_id, question_id, question_hash, ai_platform_id, run_mode, business_date); +CREATE TABLE monitoring_platform_access_snapshots ( + id BIGSERIAL PRIMARY KEY, + tenant_id BIGINT NOT NULL, + installation_id BIGINT NOT NULL, + ai_platform_id VARCHAR(30) NOT NULL, + business_date DATE NOT NULL, + access_status VARCHAR(20) NOT NULL, + connected BOOLEAN NOT NULL DEFAULT false, + accessible BOOLEAN NOT NULL DEFAULT false, + detected_at TIMESTAMPTZ NOT NULL, + reason_code VARCHAR(50), + reason_message TEXT, + created_at TIMESTAMPTZ NOT NULL DEFAULT now(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), + + CONSTRAINT uk_platform_access_snapshot + UNIQUE (tenant_id, installation_id, ai_platform_id, business_date) +); + +CREATE INDEX idx_platform_access_snapshot_lookup +ON monitoring_platform_access_snapshots(tenant_id, business_date, ai_platform_id, access_status); +``` + +约束: + +1. `access_status` 推荐值:`accessible | not_logged_in | unavailable` +2. 数据语义与插件当前 `detect()` 返回的 `connected` 状态保持一致,并扩展出调度可用的 `accessible` +3. 调度器只为 `access_status = accessible` 的平台生成当天任务 +4. 读侧平台矩阵和问题详情面板使用这张表 + `monitoring_collect_tasks` + `question_monitor_runs` 共同渲染 `sampled / pending / not_logged_in / unavailable` +5. 主监测插件每次心跳(默认 `15` 分钟)都应同时上报各 AI 平台 `detect()` 结果,服务端按 `(tenant_id, installation_id, ai_platform_id, business_date)` 执行 `UPSERT` +6. 新 `business_date` 的第一条心跳会创建当日快照;在此之前,`00:10` 调度器允许回退读取最近 `24h` 内的上一份可访问快照做机会型计划 +7. 若 `00:10` 后同日首次心跳把平台状态更新为 `not_logged_in / unavailable`,服务端需立即对该平台当日仍为 `pending` 的任务执行 reconcile,转为 `skipped` 并写入 `skip_reason = platform_not_accessible` +8. 对已进入 `leased` 的任务不做强制回收;允许其继续执行、自然完成或按过期策略回收 + +### 6.6 `question_monitor_runs` + +首期直接定义为独立结果表,不采用“先建空表再 ALTER”的增量方式。 + +```sql +CREATE TABLE question_monitor_runs ( + id BIGSERIAL PRIMARY KEY, + tenant_id BIGINT NOT NULL, + brand_id BIGINT NOT NULL, + question_id BIGINT NOT NULL, + question_text_snapshot TEXT NOT NULL, + question_hash BYTEA NOT NULL, + ai_platform_id VARCHAR(30) NOT NULL, + collector_type VARCHAR(20) NOT NULL, + trigger_source VARCHAR(20) NOT NULL DEFAULT 'automatic', + run_mode VARCHAR(30) NOT NULL DEFAULT 'plugin_standard', + business_date DATE NOT NULL, + provider_model VARCHAR(100), + provider_request_id VARCHAR(100), + request_id VARCHAR(100), + raw_answer_text TEXT, + raw_answer_storage_key TEXT, + raw_response_json JSONB, + status VARCHAR(20) NOT NULL DEFAULT 'succeeded', + completed_at TIMESTAMPTZ, + created_at TIMESTAMPTZ NOT NULL DEFAULT now(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), + + CONSTRAINT uk_monitor_run_idempotent + UNIQUE (tenant_id, brand_id, question_id, ai_platform_id, collector_type, run_mode, business_date) +); + +CREATE INDEX idx_monitor_runs_query +ON question_monitor_runs(tenant_id, business_date, ai_platform_id, brand_id); ``` 建议: 1. `question_monitor_runs` 落在 Monitoring PostgreSQL。 2. 若原始回答文本不超过阈值,可直接存库;超大原文再转对象存储。 +3. `trigger_source` 用于读侧展示“最近一次有效快照来自手动还是自动”,并支持 tooltip 解释层。 +4. `collector_type` 和 `provider_request_id` 为插件/API 二选一模式打通统一入库协议。 +5. 所有读侧查询必须显式按 `collector_type` 或等价 `collection_mode` 过滤,禁止混算。 +6. 运行表沿用任务创建时锁定的 `question_hash`,用于保证回答口径与当日任务快照一致 +7. 运行表唯一键故意与任务表幂等键保持一致,保证同日同题同平台只有一条运行记录,不因当日文案修改而分裂出第二条结果 -### 6.6 文章 URL 归因表 +### 6.6A `question_monitor_parse_results` + +该表为 `question_monitor_runs` 的一对一解析结果表,用于承接 3.4 / 3.5 页面所需的回答层语义字段,并作为日聚合的事实来源之一。 + +```sql +CREATE TABLE question_monitor_parse_results ( + id BIGSERIAL PRIMARY KEY, + tenant_id BIGINT NOT NULL, + run_id BIGINT NOT NULL, + brand_id BIGINT NOT NULL, + question_id BIGINT NOT NULL, + question_hash BYTEA NOT NULL, + ai_platform_id VARCHAR(30) NOT NULL, + business_date DATE NOT NULL, + brand_mentioned BOOLEAN, + brand_mention_position VARCHAR(20), + first_recommended BOOLEAN, + sentiment_label VARCHAR(20), + matched_brand_terms JSONB, + parse_version VARCHAR(40) NOT NULL DEFAULT 'v1', + parse_status VARCHAR(20) NOT NULL DEFAULT 'succeeded', + created_at TIMESTAMPTZ NOT NULL DEFAULT now(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), + + CONSTRAINT uk_question_parse_run + UNIQUE (tenant_id, run_id) +); + +CREATE INDEX idx_question_parse_lookup +ON question_monitor_parse_results(tenant_id, business_date, brand_id, question_id, ai_platform_id); +``` + +字段约定: + +1. `brand_mention_position`:`top1 | mentioned | not_mentioned | null` +2. `sentiment_label`:`positive | neutral | negative | unknown` +3. `matched_brand_terms` 用于回答区品牌高亮;若为空,前端可退化为关键词字符串高亮 +4. 日级 `mention_rate / top1_mention_rate / first_recommend_rate / positive_mention_rate` 均由本表聚合得出 + +### 6.7 文章 URL 归因表 为支撑“总引用数 / 文章引用率 / 引用文章数”,新增 URL 归因别名表: @@ -387,25 +776,174 @@ CREATE TABLE monitoring_article_url_aliases ( article_id BIGINT NOT NULL, publish_record_id BIGINT, platform_id VARCHAR(64), + publish_platform_name_snapshot VARCHAR(100), external_article_id VARCHAR(128), + article_title_snapshot TEXT, original_url TEXT NOT NULL, normalized_url TEXT NOT NULL, last_path_segment VARCHAR(255), confidence VARCHAR(20) NOT NULL DEFAULT 'high', - created_at TIMESTAMPTZ NOT NULL DEFAULT now() + created_at TIMESTAMPTZ NOT NULL DEFAULT now(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT now() ); CREATE UNIQUE INDEX uk_article_url_alias ON monitoring_article_url_aliases(tenant_id, normalized_url); ``` +补充说明: + +1. `article_title_snapshot / publish_platform_name_snapshot` 允许冗余存储,优先服务读侧问题详情接口,避免逐条回源主业务 PG +2. 若历史数据尚未补齐上述快照字段,读侧允许退化为批量 `IN` 查询主业务 PG 并配合本地短 TTL 缓存 + +### 6.8 跨库一致性策略 + +主业务 PG 是配置真源,Monitoring PG 是结果投影库。 + +为避免跨库数据漂移,V5.9 采用: + +1. **软删除优先**:品牌、问题、文章删除时先写主业务库 `deleted_at`,不立即物理删除 Monitoring PG 历史数据 +2. **Outbox 事件传播**:主业务库通过 outbox 记录配置变更事件,由 projector 异步投影到 Monitoring PG +3. **最终一致**:监测任务生成只读取“已投影完成”的配置快照 + +首期实现方式明确为: + +1. 使用**应用层 outbox**,不在首期引入 Debezium/CDC +2. 主业务事务提交配置变更时,同事务写入 `monitoring_config_outbox` +3. `projector` 进程每 5 秒轮询一次 outbox,按批次使用 `FOR UPDATE SKIP LOCKED` 投影到 Monitoring PG +4. 正常配置投影延迟目标为 `<= 30 秒`,超过 `2 分钟` 进入告警 +5. 未完成投影的配置变更不得参与新任务生成 +6. 首期维持 `PostgreSQL + 应用层 outbox + polling projector`,不切到 CDC/WAL 订阅;原因是当前同步对象为低频配置变更、下游单一,CDC 运维复杂度高于收益 + +主业务 PG 中的 outbox 表建议为: + +```sql +CREATE TABLE monitoring_config_outbox ( + id BIGSERIAL PRIMARY KEY, + tenant_id BIGINT NOT NULL REFERENCES tenants(id), + event_type VARCHAR(64) NOT NULL, + aggregate_type VARCHAR(50) NOT NULL, + aggregate_id BIGINT NOT NULL, + payload JSONB NOT NULL, + status VARCHAR(20) NOT NULL DEFAULT 'pending', + retry_count INT NOT NULL DEFAULT 0, + available_at TIMESTAMPTZ NOT NULL DEFAULT now(), + created_at TIMESTAMPTZ NOT NULL DEFAULT now(), + projected_at TIMESTAMPTZ, + last_error TEXT +); + +CREATE INDEX idx_monitoring_config_outbox_pending +ON monitoring_config_outbox(status, available_at, id); +``` + +首期推荐事件类型: + +- `brand.deleted` +- `brand_question.disabled` +- `brand_question.updated`(文案变更触发新快照投影) +- `tenant_monitoring_quota.updated` +- `article.deleted` + +`[V6]` 预留事件类型(首期不实现): + +- `tenant.collection_mode.switch_requested` + +处理原则: + +1. 主业务配置被软删除后,不再生成新任务 +2. Monitoring PG 保留历史结果用于趋势回看 +3. 读侧默认过滤已删除配置的“新任务入口”,但允许查询历史统计 + +Projector 运行要求: + +1. `projector` 作为独立 deployable/worker role 运行,不内嵌在 Monitor Worker 的 goroutine 中 +2. 生产环境推荐至少 `2` 个无状态副本,通过 `FOR UPDATE SKIP LOCKED` 竞争领取 outbox 批次,任一实例退出不影响整体投影 +3. 调度器生成任务前,只读取 `monitoring_question_config_snapshots` 与本地缓存配置,不回源主业务 PG +4. 若租户存在超过阈值仍未投影完成的 outbox 事件,则当天任务生成跳过该租户并输出告警,避免读到过期快照 + +Replay / 补偿要求: + +1. 支持按 `outbox_id` 范围、`tenant_id`、`aggregate_type + aggregate_id` 进行手动 replay +2. replay 必须保持幂等,重复投影只允许 `UPSERT` 当前快照/缓存表,不重复生成业务结果 +3. 每日 `06:00` 增加一次轻量 reconcile,校验主业务有效配置与 Monitoring PG 快照数量是否一致,发现漂移时输出告警并允许重放 +4. `06:00` reconcile 的目标是在 `06:30` 日聚合前先完成配置快照校验,降低聚合读取过期配置的风险 + +### 6.9 `[V6]` `collection_mode` 切换状态机 + +> **首期不实现。** 所有租户固定 `collection_mode = plugin`,无切换入口。以下为 API 模式上线时的预留设计。 + +插件模式和 API 模式是租户级二选一,因此模式切换必须显式建模。 + +该表属于**主业务 PostgreSQL**,因为它本质上是配置域状态,而不是监测结果。 + +推荐新增表(`[V6]` 时建表): + +```sql +CREATE TABLE tenant_collection_mode_switches ( + id BIGSERIAL PRIMARY KEY, + tenant_id BIGINT NOT NULL REFERENCES tenants(id), + from_mode VARCHAR(20) NOT NULL, + to_mode VARCHAR(20) NOT NULL, + status VARCHAR(20) NOT NULL, + requested_at TIMESTAMPTZ NOT NULL DEFAULT now(), + effective_at TIMESTAMPTZ NOT NULL, + grace_until TIMESTAMPTZ NOT NULL, + completed_at TIMESTAMPTZ, + cancelled_reason TEXT, + created_at TIMESTAMPTZ NOT NULL DEFAULT now(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT now() +); +``` + +状态建议: + +- `pending` +- `freezing_old_mode` +- `cutover_active` +- `completed` +- `cancelled` + +切换规则: + +1. `effective_at` 之前,不改变当前查询口径 +2. 到达 `effective_at` 后,旧模式停止生成新任务 +3. 旧模式已 `leased` / 已下发的任务允许执行到 `grace_until` +4. 超过 `grace_until` 未完成的旧模式任务直接过期 +5. 新模式任务从 `effective_at` 起生成 +6. 同一租户 `completed` 状态的模式切换,最短间隔为 30 天 +7. 当存在 `pending / freezing_old_mode / cutover_active` 记录时,不允许再次发起新切换 + +前端展示规则: + +1. 趋势图按 `collection_mode` 分段展示 +2. 切换点展示模式切换标记 +3. 禁止把切换前后的数据拼成单一连续口径 + +### 6.10 分区与数据生命周期 + +Monitoring PG 中以下表建议按 `business_date` 做月分区: + +1. `monitoring_collect_tasks` +2. `question_monitor_runs` +3. `question_monitor_parse_results` +4. `monitoring_citation_facts` +5. `monitoring_*_daily` + +推荐默认保留策略: + +1. 原始任务与原始回答:60 天热数据 +2. 引用事实:60 天热数据,必要时归档到对象存储或冷库 +3. 日聚合结果:30 天 +4. 超期数据按分区归档或删除,不做逐行清理 + --- ## 7. 解析标准化与 SoV 统计模型 ### 7.1 解析分层 -V5.1 的解析分为 3 层: +V5.9 的解析分为 3 层: 1. **回答层**:品牌提及、首位提及、首选推荐、情感倾向 2. **引用层**:从回答、citation JSON、search_results 中提取 URL 事实 @@ -415,6 +953,11 @@ V5.1 的解析分为 3 层: 为避免 `www`、二级域名、国家后缀等问题导致同一站点重复计数,引用归因与来源排行统一采用 **Public Suffix List + `tldextract`** 口径。 +Go 栈实现建议: + +1. 首期优先使用 `golang.org/x/net/publicsuffix` + 自封装完成 `registrable_domain` 提取。 +2. 若后续需要更接近 Python `tldextract` 的行为,再评估引入等价 Go 实现。 + 处理目标: 1. 从原始 URL 中提取 `subdomain / domain / suffix` @@ -424,13 +967,13 @@ V5.1 的解析分为 3 层: 示例: -| 原始 URL | `registrable_domain` | 说明 | -| --- | --- | --- | -| `https://www.zhihu.com/question/123` | `zhihu.com` | `www` 折叠 | -| `https://zhuanlan.zhihu.com/p/123` | `zhihu.com` | 子域归并到站点 | -| `https://mp.weixin.qq.com/s/abc` | `weixin.qq.com` | 基于 PSL 提取可注册域 | -| `https://finance.sina.com.cn/roll/123.html` | `sina.com.cn` | 正确处理 `com.cn` | -| `https://sub.example.co.uk/a` | `example.co.uk` | 正确处理 `co.uk` | +| 原始 URL | `registrable_domain` | 说明 | +| ------------------------------------------- | -------------------- | --------------------- | +| `https://www.zhihu.com/question/123` | `zhihu.com` | `www` 折叠 | +| `https://zhuanlan.zhihu.com/p/123` | `zhihu.com` | 子域归并到站点 | +| `https://mp.weixin.qq.com/s/abc` | `weixin.qq.com` | 基于 PSL 提取可注册域 | +| `https://finance.sina.com.cn/roll/123.html` | `sina.com.cn` | 正确处理 `com.cn` | +| `https://sub.example.co.uk/a` | `example.co.uk` | 正确处理 `co.uk` | 推荐清洗流程: @@ -459,6 +1002,7 @@ CREATE TABLE monitoring_citation_facts ( ai_platform_id VARCHAR(30) NOT NULL, business_date DATE NOT NULL, cited_url TEXT NOT NULL, + cited_title TEXT, normalized_url TEXT NOT NULL, host VARCHAR(255) NOT NULL, registrable_domain VARCHAR(255) NOT NULL, @@ -474,6 +1018,9 @@ CREATE TABLE monitoring_citation_facts ( CREATE INDEX idx_citation_facts_domain ON monitoring_citation_facts(tenant_id, business_date, registrable_domain); + +CREATE INDEX idx_citation_facts_run +ON monitoring_citation_facts(run_id); ``` 约定: @@ -482,6 +1029,7 @@ ON monitoring_citation_facts(tenant_id, business_date, registrable_domain); 2. `registrable_domain` 用于站点级来源排行 3. `site_key` 默认等于 `registrable_domain` 4. 如业务需要区分站群,可对特定域名做覆写映射 +5. `cited_title` 优先使用平台返回的 citation title;缺失时读侧允许退化为 URL 文本 ### 7.4 `site_key` 映射规则 @@ -500,6 +1048,7 @@ ON monitoring_citation_facts(tenant_id, business_date, registrable_domain); ```sql CREATE TABLE site_domain_mappings ( id BIGSERIAL PRIMARY KEY, + tenant_id BIGINT, registrable_domain VARCHAR(255) NOT NULL, site_key VARCHAR(255) NOT NULL, site_name VARCHAR(255) NOT NULL, @@ -508,9 +1057,20 @@ CREATE TABLE site_domain_mappings ( ); ``` +映射优先级: + +1. 租户级映射(`tenant_id = 当前租户`) +2. 全局默认映射(`tenant_id IS NULL`) + +落库建议: + +1. `site_domain_mappings` 的配置真源放在主业务域 +2. Monitoring 读侧使用由 projector 投影到 Monitoring PG 的本地只读缓存,避免问题详情接口在高频查询时跨库查站点映射 +3. 读侧若未命中映射,则 `site_name` 回退为 `registrable_domain` + ### 7.5 SoV 统计模型 -V5.1 中的 SoV 统一解释为: +V5.9 中的 SoV 统一解释为: > 在当前采样窗口内,某品牌、某站点或某文章在全部有效样本中的相对占比 @@ -626,6 +1186,7 @@ V5 删除 `GET tasks -> POST claim` 两步式协议,统一改为: ### 8.2 插件 API ```text +POST /api/plugin/monitoring/heartbeat POST /api/plugin/monitoring/tasks/lease POST /api/plugin/monitoring/tasks/{id}/result POST /api/plugin/monitoring/tasks/{id}/skip @@ -636,6 +1197,13 @@ POST /api/plugin/monitoring/tasks/{id}/skip - `X-Geo-Installation-Token` - `X-Geo-Installation-Id` +说明: + +1. `POST /api/plugin/monitoring/heartbeat` 用于刷新 `plugin_installations.last_seen_at` 并批量上报平台 `detect()` 结果 +2. 心跳接口建议在同一事务内完成:安装实例鉴权、`last_seen_at` 更新、`monitoring_platform_access_snapshots` 批量 `UPSERT` +3. 这组接口只服务 `collector_type = plugin` +4. `collection_mode = api` 的服务端调度入口为 `[V6]` 预留,首期不实现 + ### 8.3 租约接口行为 租约接口建议按 PG 原子领取: @@ -657,24 +1225,62 @@ LIMIT $3; 2. 生成短期 `lease_token` 3. 更新 `status = leased` 4. 写入 `lease_expires_at = now() + interval '15 minutes'` +5. 按 `(tenant_id, question_id, question_hash)` 从 `monitoring_question_config_snapshots` 装配题目文案并返回给插件 + +### 8.3A 租户侧“立即采集”入口 + +数据详情页提供租户侧手动触发入口: + +```text +POST /api/tenant/monitoring/brands/{brand_id}/collect-now +``` + +行为定义: + +1. 仅当 `primary_monitoring_installation` 在线时可触发;离线则返回 `installation_offline` +2. 手动触发**不创建第二套同日任务**,而是把当日该品牌下尚未完成的 `pending / expired / failed` 任务刷新为可再次租约的状态,更新 `planned_at = now()`,并将 `trigger_source` 更新为 `manual` +3. 若当日已存在成功快照,则接口直接返回“今日已有有效快照”,不新增第二个日点位 +4. 若存在已被插件领取的 `leased` 任务,接口**不等待当前租约批次完成**;这些任务继续执行,本次手动触发只刷新未被领取的任务 +5. 接口返回值建议包含 `refreshed_task_count / leased_task_count / completed_task_count`,前端文案明确为“已刷新未领取任务,已领取任务继续执行” +6. 手动触发成功后,页面先进入“手动采集中”状态;待新结果成功入库并完成局部聚合后,再刷新“今日快照”和对应日桶的 `snapshot_updated_at / trigger_source` +7. 首期仍遵守“在线就采集,不在线就放弃”的原则,不做离线补数 +8. 若用户当天修改问题文案后立即手动触发,仍执行当日已生成任务锁定的题目版本;新文案从下一 `business_date` 起生效 ### 8.4 插件执行策略 -V5 继续复用现有插件基础设施: +V5.9 复用的是现有扩展框架,而不是现有发布 adapter 体系: - [background.ts](/Users/liangxu/Documents/test/geo-rankly/apps/browser-extension/entrypoints/background.ts) - [runtime.ts](/Users/liangxu/Documents/test/geo-rankly/apps/browser-extension/src/runtime.ts) - [storage.ts](/Users/liangxu/Documents/test/geo-rankly/apps/browser-extension/src/storage.ts) +代码库对齐说明: + +1. 可复用的是 WXT 框架、background service worker、alarm/message passing、平台状态存储等基础设施 +2. 现有 `src/adapters/` 是媒体发布 adapter,接口为 `detect() + publish()`,继续服务文章发布链路 +3. 监测侧需要新建独立 AI 监测 adapter 模块,例如 `src/monitoring-adapters/`,接口建议为 `detect() + query() + parseResponse()` +4. 发布 adapter 与监测 adapter 可以共享平台状态探测能力,但不共享同一接口定义、平台枚举和执行流水线 + 但采样策略改为“小批量、固定预算”: -| 项目 | 建议值 | -| --- | --- | -| 心跳频率 | 15 分钟 | -| 单次租约任务数 | 2-3 | -| 单任务超时 | 60 秒 | -| 租约时长 | 15 分钟 | -| 每日硬上限 | 按套餐 36 / 72 | +| 项目 | 建议值 | +| -------------- | -------------- | +| 心跳频率 | 15 分钟 | +| 单次租约任务数 | 2-3 | +| 单任务超时 | 60 秒 | +| 租约时长 | 15 分钟 | +| 每日硬上限 | 按套餐 36 / 72 | + +### 8.4A 平台状态上报闭环 + +为让 `monitoring_platform_access_snapshots` 真正成为调度器与读侧共享状态源,首期明确采用以下闭环: + +1. 主监测插件每次心跳都执行监测平台 `detect()`,并把 `connected / accessible / reason_code / reason_message` 随心跳一并回传服务端 +2. 服务端以心跳接收时间所属的 `business_date` 为准,`UPSERT monitoring_platform_access_snapshots` +3. `00:10` 调度器生成当天任务时,优先读取当日快照;若当日快照尚未出现,则回退读取最近 `24h` 内最新快照做机会型计划 +4. 若调度后首次心跳把平台状态修正为 `not_logged_in / unavailable`,服务端立即对该平台当日仍为 `pending` 的任务执行 reconcile,改为 `skipped` +5. `leased` 中的任务不因状态回传被强制中断;仍按正常回调或过期回收处理 +6. 读侧平台矩阵与问题详情面板统一读取该表,不直接根据前端本地状态猜测 `未登录 / 不可用` ### 8.5 任务过期与回收 @@ -685,6 +1291,70 @@ V5 继续复用现有插件基础设施: 3. 同一 `business_date` 的 `expired` 任务最多重试 1 次 4. 超过 `business_date + 2 days` 的未完成任务不再重试 +### 8.6 `[V6]` API 采集模式接口预留 + +> **首期不实现。** 本节仅保留数据模型和协议契约,供后续 API 模式接入时复用。 + +V5.9 中插件采集和 API 采集是二选一模式,因此必须提前统一“结果入库协议”。 + +建议内部抽象: + +```text +CollectTask + -> collector_type = plugin | api + +CollectorResultEnvelope + -> collector_type + -> task_id + -> tenant_id + -> brand_id + -> question_id + -> question_hash + -> ai_platform_id + -> run_mode + -> provider_model + -> provider_request_id + -> answer + -> citations + -> search_results + -> raw_response_json + -> status +``` + +统一原则: + +1. 插件采集和 API 采集最终都投递到同一个 `monitor.result.ingest` +2. parse worker 只认标准化后的 `CollectorResultEnvelope` +3. 采集通道差异在 adapter 层消化,不传播到聚合层和查询层 +4. 同一租户、同一统计窗口只能启用一种 `collector_type` +5. 传输层中的 `question_hash` 统一使用 `v1:` 字符串表示,ingest 后统一解码落为 Monitoring PG 的二进制指纹 + +### 8.7 `[V6]` API 模式执行器并发模型 + +> **首期不实现。** 本节为 API 模式上线时的执行器并发和限流预留设计。 + +API 模式不复用插件租约接口,但仍复用同一任务表和同一领取模型。 + +建议: + +1. API worker 从 `monitoring_collect_tasks` 中仅领取 `collector_type = api` 的任务 +2. API worker 同样使用 `FOR UPDATE SKIP LOCKED` 控制并发 +3. 并发粒度按 `ai_platform_id` 和租户配额限流 +4. 同一问题同一平台同一天仍由幂等键保证不重复落数 + +推荐并发层级: + +1. 租户级并发上限 +2. 平台级并发上限 +3. 全局 provider QPS / TPM 限流 + +首期实现建议: + +1. 全局 provider QPS / TPM 限流先在 API worker 进程内以 token bucket 实现 +2. worker 集群总吞吐通过部署副本数和进程内并发参数共同控制 +3. 当后续进入多实例横向扩容或 provider 配额需要跨实例共享时,再升级为 Redis 分布式限流 +4. 限流接口在代码层保持抽象,避免首期实现和后续分布式版本耦合 + --- ## 9. 采集、解析、聚合架构 @@ -692,13 +1362,17 @@ V5 继续复用现有插件基础设施: ### 9.1 首期简化版链路 ```text -每日 00:10 调度器生成当天任务 +插件每 15 分钟心跳并上报 detect() 结果 ↓ -插件后台 15 分钟心跳租约任务 +服务端 upsert monitoring_platform_access_snapshots ↓ -插件执行提问并回传结果 +每日 00:10 调度器生成当天 plugin 任务 ↓ -tenant-api:校验租约 -> 标记 received -> 投递 RabbitMQ +用户可在数据详情页手动催发当日未完成任务 + ↓ +插件心跳租约并执行 + ↓ +插件回传结果 -> 写入 RabbitMQ ingest ↓ Monitor Worker:消费结果 -> 写 Monitoring PG -> 解析结果 -> 完成任务 ↓ @@ -707,9 +1381,21 @@ Monitor Worker:消费结果 -> 写 Monitoring PG -> 解析结果 -> 完成任 读侧 API + Redis 缓存 ``` +`[V6]` 预留分支: + +1. API 模式上线后,再在调度器后增加服务端 provider adapter 执行链路 +2. 该扩展不改变下游 ingest / parse / aggregate 主链路 + +首期调度器实现选择: + +1. 不复用当前 `schedule_tasks` 表 +2. 原因是当前代码库中 `schedule_tasks` 只有 CRUD 和 schema,尚无 executor / dispatcher +3. 首期监测调度器直接作为 Monitor Worker 进程内独立 cron goroutine 实现,同时负责每日任务生成和过期回收 +4. 待通用调度执行器成熟后,再评估是否统一收敛到平台级调度框架 + ### 9.2 RabbitMQ 的职责边界 -V5.1 中 RabbitMQ 只承担“结果异步化”,不承担“任务分发”。 +V5.9 中 RabbitMQ 只承担“结果异步化”,不承担“任务分发”。 使用 RabbitMQ 的原因: @@ -730,6 +1416,34 @@ V5.1 中 RabbitMQ 只承担“结果异步化”,不承担“任务分发” - `monitor.result.dlq` - `monitor.aggregate.trigger` +说明: + +1. 首期 `monitor.result.ingest` 只接收 `plugin` 采集结果 +2. `[V6]` 引入 API 模式后,继续复用同一 ingest 队列和同一标准化 envelope +3. 队列消费者不关心采集来源,只关心统一 envelope + +MQ 部署建议: + +1. 开发 / docker-compose 环境先落单节点 RabbitMQ,验证 publisher、consumer、DLQ 全链路 +2. 生产环境使用 3 节点 RabbitMQ 集群 +3. 核心队列在生产环境使用 durable + quorum queue +4. 发布端启用 publisher confirm +5. 消息使用 persistent delivery mode + +消费与幂等建议: + +1. worker 在 Monitoring PG 事务提交成功后再 ack +2. 以 `task_id + collector_type + business_date` 或运行表唯一键做最终幂等 +3. 同一消息重复投递时,允许消费端幂等落空后直接 ack + +DLQ 处理流程: + +1. 首次失败按重试策略回投主队列,重试 3 次,退避间隔为 `5s / 30s / 120s` +2. 重试次数通过消息 header 传递,超过阈值进入 `monitor.result.dlq` +3. 运维或研发基于任务 ID、错误类型、原始 payload 做人工排查 +4. 排查完成后支持“重放到 ingest”或“标记任务 failed” +5. 所有重放动作必须写审计日志,记录操作人、时间、原因、task_id 和消息摘要 + ### 9.3 Monitoring PostgreSQL 的职责 独立 Monitoring PG 的目的不是“多一套库”,而是隔离监测链路的写放大。 @@ -750,7 +1464,7 @@ V5.1 中 RabbitMQ 只承担“结果异步化”,不承担“任务分发” ### 9.4 原始回答存储 -V5.1 中抓取结果优先进入 Monitoring PG: +V5.9 中抓取结果优先进入 Monitoring PG: 1. 原始回答文本 2. 平台响应元数据 @@ -809,10 +1523,36 @@ V5.1 中抓取结果优先进入 Monitoring PG: ### 11.2 页面与套餐表达 -页面和套餐都必须按“当前已接入且已登录的平台”解释: +平台访问条件不能一刀切,V5.9 统一按“平台访问模式”处理。 -- 已接入且已登录:进入采样计划 -- 已接入未登录:展示 `未登录` +推荐在平台配置中增加: + +- `auth_requirement = login_required | anonymous_allowed` + +当前已知口径: + +| 平台 | 访问模式 | 说明 | +| ---------------- | ------------------- | ------------------------ | +| DeepSeek | `login_required` | 必须登录后才能提问 | +| Kimi | `login_required` | 必须登录后才能提问 | +| 其他当前接入平台 | `anonymous_allowed` | 可在未登录状态下直接提问 | + +任务生成规则: + +1. `login_required` 平台:只有检测到有效登录态才进入当天采样计划 +2. `anonymous_allowed` 平台:默认可进入当天采样计划 +3. 若免登录平台后续改版为必须登录,应通过平台配置热更新切换,不改调度代码 + +实现建议: + +1. 平台访问模式放入 `ai_platforms` 或等价配置表 +2. 插件 `detect()` 返回 `accessible` 与 `connected` 两个概念 +3. 调度器判断的是 `accessible`,不是简单判断 `connected` + +页面和套餐都必须按“当前已接入且满足访问条件的平台”解释: + +- 已接入且满足访问条件:进入采样计划 +- 已接入但不满足访问条件:展示 `未登录` 或 `不可访问` - 未接入:展示 `未接入` 不得再用“全 6 平台稳定可用”作为首期对外承诺。 @@ -823,33 +1563,320 @@ V5.1 中抓取结果优先进入 Monitoring PG: V5 读侧继续沿用 V3 的聚合表和缓存思路,但口径增加采样字段: -1. `planned_sample_count` -2. `actual_sample_count` -3. `coverage_rate` -4. `confidence_level` +1. `desired_sample_count` +2. `planned_sample_count` +3. `actual_sample_count` +4. `coverage_rate` +5. `confidence_level` 推荐所有 `monitoring_*_daily` 汇总表统一增加以上字段。 +### 12.0 `monitoring_brand_daily` 推荐 DDL + +为避免实现期对“品牌日聚合表”字段理解不一致,至少明确一张完整日聚合表定义: + +```sql +CREATE TABLE monitoring_brand_daily ( + id BIGSERIAL PRIMARY KEY, + tenant_id BIGINT NOT NULL, + brand_id BIGINT NOT NULL, + collector_type VARCHAR(20) NOT NULL DEFAULT 'plugin', + business_date DATE NOT NULL, + sample_status VARCHAR(20) NOT NULL, + desired_sample_count INT NOT NULL DEFAULT 0, + planned_sample_count INT NOT NULL DEFAULT 0, + actual_sample_count INT NOT NULL DEFAULT 0, + coverage_rate NUMERIC(8,4), + confidence_level VARCHAR(20) NOT NULL, + trigger_source VARCHAR(20), + snapshot_updated_at TIMESTAMPTZ, + mentioned_count INT NOT NULL DEFAULT 0, + top1_mentioned_count INT NOT NULL DEFAULT 0, + first_recommended_count INT NOT NULL DEFAULT 0, + positive_mentioned_count INT NOT NULL DEFAULT 0, + cited_answer_count INT NOT NULL DEFAULT 0, + mention_rate NUMERIC(8,4), + top1_mention_rate NUMERIC(8,4), + first_recommend_rate NUMERIC(8,4), + positive_mention_rate NUMERIC(8,4), + citation_rate NUMERIC(8,4), + created_at TIMESTAMPTZ NOT NULL DEFAULT now(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), + + CONSTRAINT uk_monitoring_brand_daily + UNIQUE (tenant_id, brand_id, collector_type, business_date) +); + +CREATE INDEX idx_monitoring_brand_daily_lookup +ON monitoring_brand_daily(tenant_id, brand_id, business_date DESC, collector_type); +``` + +说明: + +1. `overview` 与 `brand_time_buckets` 可直接读取本表 +2. `desired_sample_count` 用于支撑 3.2 节“目标 60,计划 30”的副文案;不建议每次读接口动态回算 +3. 平台矩阵建议使用同风格的 `monitoring_brand_platform_daily`,仅额外加入 `ai_platform_id` 并纳入唯一键 +4. `sample_status / trigger_source / snapshot_updated_at` 为页面 tooltip 和手动刷新反馈保留,不建议只在缓存层临时拼装 + +推荐最小结构: + +```sql +CREATE TABLE monitoring_brand_platform_daily ( + id BIGSERIAL PRIMARY KEY, + tenant_id BIGINT NOT NULL, + brand_id BIGINT NOT NULL, + ai_platform_id VARCHAR(30) NOT NULL, + collector_type VARCHAR(20) NOT NULL DEFAULT 'plugin', + business_date DATE NOT NULL, + platform_sample_status VARCHAR(20) NOT NULL, + planned_sample_count INT NOT NULL DEFAULT 0, + actual_sample_count INT NOT NULL DEFAULT 0, + mention_rate NUMERIC(8,4), + top1_mention_rate NUMERIC(8,4), + last_sampled_at TIMESTAMPTZ, + created_at TIMESTAMPTZ NOT NULL DEFAULT now(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), + + CONSTRAINT uk_monitoring_brand_platform_daily + UNIQUE (tenant_id, brand_id, ai_platform_id, collector_type, business_date) +); +``` + ### 12.1 Dashboard composite -`/api/tenant/monitoring/dashboard/composite` 继续保留,但返回结构需扩展: +`/api/tenant/monitoring/dashboard/composite` 继续保留,但返回结构需扩展以支撑新的四层页面布局: ```json { "overview": { + "collection_mode": "plugin", "mention_rate": 0.42, + "top1_mention_rate": 0.18, + "first_recommend_rate": 0.25, + "positive_mention_rate": 0.67, "citation_rate": 0.18, + "desired_sample_count": 60, "planned_sample_count": 30, "actual_sample_count": 21, - "coverage_rate": 0.70, + "coverage_rate": 0.7, "confidence_level": "high", - "last_sampled_at": "2026-04-08T09:20:00Z" - } + "last_sampled_at": "2026-04-08T09:20:00Z", + "last_trigger_source": "automatic", + "prev_snapshot_mention_rate": 0.37, + "prev_snapshot_top1_mention_rate": 0.2, + "prev_snapshot_first_recommend_rate": 0.22, + "prev_snapshot_positive_mention_rate": 0.67 + }, + "platform_breakdown": [ + { + "ai_platform_id": "doubao", + "platform_name": "豆包", + "mention_rate": 0.45, + "top1_mention_rate": 0.2, + "platform_sample_status": "sampled", + "actual_sample_count": 8, + "last_sampled_at": "2026-04-08T09:20:00Z" + }, + { + "ai_platform_id": "deepseek", + "platform_name": "DeepSeek", + "mention_rate": 0.38, + "top1_mention_rate": 0.15, + "platform_sample_status": "sampled", + "actual_sample_count": 6, + "last_sampled_at": "2026-04-08T08:45:00Z" + }, + { + "ai_platform_id": "qwen", + "platform_name": "通义千问", + "mention_rate": null, + "top1_mention_rate": null, + "platform_sample_status": "not_logged_in", + "actual_sample_count": 0, + "last_sampled_at": null + } + ], + "brand_time_buckets": [ + { + "date": "2026-04-03", + "sample_status": "sampled", + "metric_value": 0.42, + "planned_sample_count": 12, + "actual_sample_count": 8, + "coverage_rate": 0.67, + "trigger_source": "automatic", + "snapshot_updated_at": "2026-04-03T09:12:00Z" + }, + { + "date": "2026-04-04", + "sample_status": "unsampled", + "metric_value": null, + "planned_sample_count": 0, + "actual_sample_count": 0, + "coverage_rate": null, + "trigger_source": null, + "snapshot_updated_at": null + } + ], + "hot_questions": [ + { + "question_id": 42, + "question_hash": "v1:0f3be2f94a4e7b51d9d1f9a6c2208d15", + "question_text": "北京口腔医院推荐?", + "sample_count": 3, + "mention_rate": 0.67, + "last_sampled_at": "2026-04-08T09:15:00Z" + } + ], + "citation_ranking": [ + { + "ai_platform_id": "deepseek", + "platform_name": "DeepSeek", + "sample_status": "sampled", + "cited_answer_count": 2, + "cited_article_count": 1, + "citation_rate": 0.33 + } + ], + "cited_articles": [ + { + "article_id": 88, + "article_title": "北京口腔医院全面测评", + "publish_platform": "微信公众号", + "citation_count": 3, + "citation_rate": 0.14 + } + ] } ``` +补充规则: + +1. 查询接口必须返回当前 `collection_mode` +2. 首期所有查询固定按 `collector_type = plugin` 过滤 +3. `[V6]` 若后续引入模式切换,历史趋势需按模式分段或按模式筛选,不做跨模式拼接 +4. `overview` 新增 `prev_snapshot_*` 字段,供前端计算指标卡趋势箭头 +5. `platform_breakdown` 始终返回全部已接入平台,不受前端模型筛选器影响 +6. `brand_time_buckets` 继续保留,供 mini sparkline 和日指示点渲染 +7. `sample_status` 是读侧解释字段,前端只在日指示点 tooltip 中使用,不作为逐日主文案 +8. 同一自然日若发生手动催发,接口返回的仍然是一个日桶;只更新 `trigger_source / snapshot_updated_at` +9. `hot_questions` 按 `(question_id, question_hash)` 聚合,避免同一 `question_id` 在窗口内因文案变更而混算 +10. 前端打开问题详情面板时,应从 `hot_questions` 透传 `question_id + question_hash` +11. `hot_questions` 的 `sample_count / mention_rate / last_sampled_at` 由 `question_monitor_runs + question_monitor_parse_results` 聚合得出 +12. `citation_ranking` 与 `cited_articles` 缺数据时返回空数组,不以 `0` 强补 +13. `platform_sample_status` 由 `question_monitor_runs + monitoring_collect_tasks + monitoring_platform_access_snapshots` 共同判定,而不是只看成功运行记录 + +### 12.1A 高频问题详情接口 + +问题详情面板通过独立接口按需加载,不合并到 composite 接口,避免拖慢首屏渲染。 + +```text +GET /api/tenant/monitoring/brands/{brand_id}/questions/{question_id}/detail + ?date_from=2026-04-08 + &date_to=2026-04-08 + &question_hash=v1:0f3be2f94a4e7b51d9d1f9a6c2208d15 + &ai_platform_id=deepseek // 可选,默认返回全部平台数据 +``` + +返回结构示例: + +```json +{ + "question_id": 42, + "question_hash": "v1:0f3be2f94a4e7b51d9d1f9a6c2208d15", + "question_text": "北京口腔医院推荐?", + "time_window": { + "date_from": "2026-04-08", + "date_to": "2026-04-08" + }, + "sample_count": 3, + "platforms": [ + { + "ai_platform_id": "deepseek", + "platform_name": "DeepSeek", + "sample_status": "sampled", + "run_id": 1001, + "sampled_at": "2026-04-08T09:15:00Z", + "provider_model": "deepseek-chat", + "answer_text": "根据用户口碑和专业评价,推荐以下几家...", + "brand_mentioned": true, + "brand_mention_position": "top1", + "citations": [ + { + "cited_url": "https://www.zhihu.com/question/123", + "cited_title": "北京口腔医院哪家比较好?", + "site_name": "知乎", + "site_key": "zhihu.com", + "favicon_url": "https://zhihu.com/favicon.ico", + "article_id": null, + "resolution_status": "resolved", + "resolution_confidence": "high" + }, + { + "cited_url": "https://mp.weixin.qq.com/s/abc123", + "cited_title": "北京口腔医院全面测评", + "site_name": "微信公众号", + "site_key": "weixin.qq.com", + "favicon_url": "https://weixin.qq.com/favicon.ico", + "article_id": 88, + "article_title": "北京口腔医院全面测评", + "resolution_status": "resolved", + "resolution_confidence": "high" + } + ] + }, + { + "ai_platform_id": "qwen", + "platform_name": "通义千问", + "sample_status": "not_logged_in", + "run_id": null, + "sampled_at": null, + "provider_model": null, + "answer_text": null, + "brand_mentioned": null, + "citations": [] + } + ], + "citation_analysis": [ + { + "ai_platform_id": "deepseek", + "platform_name": "DeepSeek", + "citation_count": 4, + "distinct_site_count": 3, + "brand_article_citation_count": 1, + "has_brand_article_citation": true + } + ], + "content_citations": [ + { + "article_id": 88, + "article_title": "北京口腔医院全面测评", + "publish_platform": "微信公众号", + "ai_platform_id": "deepseek", + "platform_name": "DeepSeek", + "citation_count": 1, + "citation_rate": 0.33 + } + ] +} +``` + +接口说明: + +1. `platforms` 数组始终返回全部已接入平台,未采样平台的 `answer_text / citations` 为空 +2. 若请求时携带 `ai_platform_id` 参数,`platforms` 数组只返回指定平台的数据 +3. `citation_analysis` 为跨平台引用汇总,不受 `ai_platform_id` 参数过滤,始终展示全平台聚合 +4. `content_citations` 按 `ai_platform_id` 参数过滤;未传时返回全部平台的内容引用记录 +5. 该接口主读 `question_monitor_runs`、`question_monitor_parse_results`、`monitoring_citation_facts`、`monitoring_collect_tasks`、`monitoring_platform_access_snapshots`、`site_domain_mappings`、`monitoring_article_url_aliases` +6. `brand_mention_position`:`top1 | mentioned | not_mentioned | null` +7. `question_hash` 由外层 `hot_questions` 透传,用于锁定同一问题文案版本;若省略,后端只允许解析为窗口内最新版本,不保证历史版本一致性 +8. `favicon_url` 由 `site_key` 或 `registrable_domain` 在读侧运行时派生,不要求持久化到 Monitoring PG +9. `content_citations.citation_rate` 的分母固定为同一 `question_id + question_hash` 在当前时间窗口内的成功采样次数 +10. `article_title / publish_platform` 优先读取 `monitoring_article_url_aliases` 中的快照字段;若缺失,只允许按 `article_id / publish_record_id` 执行单次批量 `IN` 查询并配合 `TTL = 5 分钟` 本地缓存,禁止按引用逐条回源主业务 PG + ### 12.2 缓存策略 + 读侧缓存策略可继续沿用 V3: - composite 聚合缓存 @@ -861,40 +1888,104 @@ V5 读侧继续沿用 V3 的聚合表和缓存思路,但口径增加采样字 1. 每日聚合完成 2. 用户切换品牌 / 时间窗口 +3. 手动采集触发的局部增量聚合完成 不需要为单条采样结果做准实时逐条失效。 +### 12.3 聚合补算策略 + +为避免 06:30 后仍有 `received` / `completed` 结果陆续入库导致统计偏差,V5.9 采用“双阶段聚合”: + +1. `06:30` 产出首版日聚合 +2. `08:00` 对当日和前一日做一次增量修正 + +若 `08:00` 后仍有迟到结果: + +1. 更新原始表与事实表 +2. 在下一次修正窗口内补算 +3. 不要求逐条触发读侧实时失效 + +手动触发例外: + +1. 当 `collect-now` 触发的任务产生新的成功结果后,worker 额外投递一条 `monitor.aggregate.trigger` +2. 聚合消费者按 `(tenant_id, brand_id, business_date)` 做一次局部增量重算,覆盖 `monitoring_brand_daily`、高频问题列表所需汇总以及问题详情相关引用汇总 +3. 局部聚合成功后,主动失效该品牌当日的 `dashboard/composite` 与 `questions/{question_id}/detail` 缓存 +4. 该路径仅服务“手动采集后的用户可见刷新”,目标 SLA 为 `<= 1 分钟`,不把全链路改造成逐条准实时聚合 + --- ## 13. 降级、告警与风险 ### 13.1 降级策略 -| 场景 | V5 行为 | -| --- | --- | -| 插件 24h 未活跃 | 暂停生成新任务,页面展示上次数据 | -| 平台未登录 | 该平台不进入当天计划样本 | -| 当日覆盖率低 | 页面提示“样本不足,仅供参考” | -| 某平台 adapter 失效 | 该平台展示“暂不可用”,其余平台继续 | -| URL 无法归因 | 计入 unresolved,不强行挂到文章 | +| 场景 | V5 行为 | +| ------------------------- | ---------------------------------- | +| 插件模式下插件 24h 未活跃 | 暂停生成新任务,页面展示上次数据 | +| 登录必需平台未登录 | 该平台不进入当天计划样本 | +| 当日覆盖率低 | 页面提示“样本不足,仅供参考” | +| 某平台 adapter 失效 | 该平台展示“暂不可用”,其余平台继续 | +| URL 无法归因 | 计入 unresolved,不强行挂到文章 | ### 13.2 关键告警 -| 指标 | 阈值 | -| --- | --- | -| 主插件离线时长 | > 24h | -| 日覆盖率 | < 40% | -| 单平台连续失败 | > 20 次 / 天 | -| unresolved 引用占比 | > 30% | -| 每日聚合延迟 | > 08:00 未完成 | +| 指标 | 阈值 | +| ------------------- | -------------- | +| 主插件离线时长 | > 24h | +| 日覆盖率 | < 40% | +| 单平台连续失败 | > 20 次 / 天 | +| unresolved 引用占比 | > 30% | +| 每日聚合延迟 | > 08:00 未完成 | +| projector 无心跳 | > 1 分钟 | +| outbox 投影延迟 | > 2 分钟 | +| outbox 堆积量 | > 100 持续 10 分钟 | -### 13.3 风险接受说明 +### 13.3 Token 生命周期与回调安全 -V5 明确接受以下风险: +`X-Geo-Installation-Token` 需要完整生命周期管理。 + +建议: + +1. 安装注册时签发长期 token,仅存 hash +2. 支持手动吊销与重新签发 +3. 插件结果回调除安装 token 外,仍要求携带短期 `lease_token` +4. `task_id + lease_token + collector_type` 共同校验,防止 replay attack +5. 任务进入 `received / completed` 后,重复结果直接按幂等处理,不重复计数 + +### 13.4 可观测性建设 + +V5.9 需要最小可运维基线,而不只是阈值表。 + +建议输出: + +1. Prometheus metrics: + - task lease latency + - queue depth + - worker consume lag + - projector heartbeat + - outbox pending count + - outbox projection lag + - outbox replay count + - projector reconcile drift + - parse success rate + - unresolved citation ratio + - per-mode coverage rate +2. 结构化日志: + - task_id + - tenant_id + - collector_type + - ai_platform_id + - provider_request_id +3. 链路追踪: + - lease -> execute -> ingest -> parse -> aggregate + +### 13.5 风险接受说明 + +V5.9 明确接受以下风险: 1. 单日样本偏少导致波动大 2. 插件离线导致当天无新数据 3. 部分 URL 只能低置信度归因 +4. 调度窗口前 `24h` 内曾在线但次日实际未再上线,导致当日任务自然过期;首期接受该浪费,不做离线补数 但不接受以下风险: @@ -908,41 +1999,59 @@ V5 明确接受以下风险: ### 14.1 Phase 分期 -| Phase | 内容 | 估时 | -| --- | --- | --- | -| A | 双库方案落地:主业务 PG / Monitoring PG schema 与连接配置 | 4 天 | -| B | RabbitMQ 基础设施 + Monitor Worker 骨架 + DLQ | 4 天 | -| C | 插件:AI adapter 框架 + 3 平台 + 租约式任务执行 | 6 天 | -| D | 后端:租约 API、结果回调、MQ 消费、解析入库、日聚合 | 7 天 | -| E | 前端:数据追踪页补覆盖率、更新时间、置信度提示 | 4 天 | -| F | 联调、坏样本验证、口径验收 | 4 天 | -| **合计** | | **29 天** | +| Phase | 内容 | 估时 | +| ------------ | --------------------------------------------------------------------------------------------------------- | ------------ | +| A | 双库基建:Monitoring PG 实例、双连接池、`monitoring_database` 配置、双 sqlc codegen、主业务/监测库 schema | 4-5 天 | +| B | 后台基建:RabbitMQ、publisher/consumer 骨架、DLQ、outbox polling projector | 4 天 | +| C | 插件:在现有扩展框架内新建 AI 监测 adapter 模块、3 平台、租约轮询、结果回传 | 10-12 天 | +| D | 后端:租约 API、监测调度 cron、结果回调、MQ 消费、解析入库、日聚合 | 5-6 天 | +| E | 前端:数据追踪页补覆盖率、更新时间、置信度提示、每日单快照脉冲视图 | 3 天 | +| F | 可观测性:metrics、日志、告警、DLQ 运维脚本 | 3 天 | +| G | 联调、坏样本验证、口径验收 | 4 天 | +| **基线合计** | | **33-37 天** | + +建议: + +1. Phase A 需拆成 `A1 Monitoring PG + docker-compose`、`A2 双连接池 + 双 sqlc`、`A3 schema / outbox / snapshot 表` +2. Outbox DDL 归 Phase A,outbox polling projector 归 Phase B;`collection_mode` 切换状态机延后到 `[V6]` +3. Phase C 建议拆成 `C1 框架 + 1 平台 + 结果回传` 与 `C2 +2 平台 + 登录态/风控收尾` 两个子阶段,以降低 DOM 适配风险 +4. `33-37 天` 是按“首期只做 plugin 模式”校准后的**基线工期** +5. 若按 20% buffer 对外承诺,建议按 **40-44 天** 评估 ### 14.2 验收标准 1. 页面可展示 `planned_sample_count / actual_sample_count / coverage_rate` -2. 页面可正常展示趋势图、高频问题、引用排行 +2. 页面可正常展示离散采样时序视图、高频问题、引用排行 3. 当覆盖率不足时,页面有明确提示 4. 同一任务不会被重复计入统计 5. 问题文案修改后,历史趋势不被污染 6. 引用文章数可通过 URL alias 规则稳定归因,无法归因时进入 unresolved +7. 首期所有读侧查询固定按 `collector_type = plugin` 过滤,为 `[V6]` 扩展保留模式边界 +8. MQ、DLQ、worker lag、覆盖率等关键指标可观测 +9. 未采样日期不会被渲染为 0 值折线点 +10. 数据详情页支持“立即采集”,但同一自然日不会新增第二个展示点位 +11. 前端不会把 `sampled / partial / unsampled` 直接逐日渲染为主文案 --- ## 15. V4 → V5 变更摘要 -| 编号 | V4 | V5 | -| --- | --- | --- | -| C1 | 全量监测叙事 | 采样趋势叙事 | -| C2 | Free 1 品牌 / 40 问题 | Free 3-5 个每日活跃问题 | -| C3 | Pro 3 品牌 / 40 问题 | Pro 5-10 个每日活跃问题 | -| C4 | GET + claim 两步任务协议 | 一步租约式任务协议 | -| C5 | `question_version_id` | `question_hash + question_text_snapshot` | -| C6 | RabbitMQ + 增量聚合 | RabbitMQ 仅做结果异步化,不参与任务分发或前端准实时 | -| C7 | 准实时可见 | T+1 / 延迟可接受 | -| C8 | 仅展示百分比 | 百分比 + 采样覆盖信息 | -| C9 | 首期 6 平台叙事 | 首期只承诺 3 平台 | -| C10 | 单一业务库承载全部数据 | 主业务 PG + Monitoring PG 双库隔离 | +| 编号 | V4 | V5 | +| ---- | ------------------------ | ------------------------------------------------------------------- | +| C1 | 全量监测叙事 | 采样趋势叙事 | +| C2 | Free 1 品牌 / 40 问题 | Free 1 品牌,尽量覆盖全部启用问题,受执行预算限制 | +| C3 | Pro 3 品牌 / 40 问题 | Pro 3 品牌,尽量覆盖全部启用问题,受执行预算限制 | +| C4 | GET + claim 两步任务协议 | 一步租约式任务协议 | +| C5 | `question_version_id` | `question_hash` 负责任务快照锁定,`question_text_snapshot` 负责运行快照 | +| C6 | RabbitMQ + 增量聚合 | RabbitMQ 仅做结果异步化,不参与任务分发或前端准实时 | +| C7 | 准实时可见 | T+1 / 延迟可接受 | +| C8 | 仅展示百分比 | 百分比 + 采样覆盖信息 | +| C9 | 首期 6 平台叙事 | 首期只承诺 3 平台 | +| C10 | 单一业务库承载全部数据 | 主业务 PG + Monitoring PG 双库隔离 | +| C11 | 模式切换未定义 | `collection_mode` 状态机 + grace period | +| C12 | MQ 仅概念化引入 | durable + quorum + publisher confirm + DLQ 运维流程 | +| C13 | 未定义分区与保留 | Monitoring PG 按 `business_date` 分区 + 生命周期策略 | +| C14 | 告警阈值表 | 告警 + metrics + tracing + 结构化日志 | ## 16. 最终建议 diff --git a/docs/question-driven-monitoring-design-v1.md b/docs/question-driven-monitoring-design-v1.md deleted file mode 100644 index 207d362..0000000 --- a/docs/question-driven-monitoring-design-v1.md +++ /dev/null @@ -1,494 +0,0 @@ -# GEO 平台问题驱动的数据监控设计 V1 - -## 1. 文档信息 - -| 项目 | 内容 | -| --- | --- | -| 文档名称 | GEO 平台问题驱动的数据监控设计 V1 | -| 文档版本 | V1.0 | -| 文档状态 | 待评审 | -| 创建日期 | 2026-03-25 | -| 适用范围 | 品牌词库、问题集、数据追踪页 | -| 关联文档 | `docs/geo-platform-prd-v1.md` | -| 关联文档 | `docs/geo-platform-tech-architecture-v1-50k.md` | - -## 2. 设计背景 - -当前数据监控页不是通用 BI,而是明确来源于: - -- 品牌 -- 关键词 -- 关键词下的问题集 -- 模型 -- 最近 30 天时间窗口 - -因此,数据监控的真实逻辑源头不是“文章”本身,也不是“关键词”本身,而是: - -- `品牌关键词下配置的问题` -- `系统按周期执行这些问题后的模型回答结果` - -这意味着整套监控系统必须按“问题驱动”来设计,否则后续会出现: - -- 指标口径不清 -- 历史数据与问题版本混淆 -- 任务量失控 -- 查询逻辑和数据模型对不上 - -## 3. 核心结论 - -V1 建议采用以下原则: - -1. `问题` 是监控数据的最小采集单元。 -2. `关键词` 只是问题集合的容器和聚合维度。 -3. `品牌监控页` 展示的是问题运行结果的聚合视图。 -4. `历史数据必须绑定问题版本`,不能因为改了问题文案就覆盖历史口径。 -5. `页面查询只查汇总表`,不扫描原始问答记录。 - -## 4. 页面逻辑拆解 - -结合当前页面,用户选择条件是: - -- 品牌 -- 关键词 -- 模型 -- 时间范围,最多近 30 天 - -页面展示内容包括: - -- 提及率 -- 首位提及率 -- 首选推荐率 -- 正面提及率 -- 日趋势折线 -- 高频问题 -- 引用排行榜 - -这些数据不应该直接从原始问答明细实时计算,而应该来自预聚合结果。 - -## 5. 数据采集逻辑 - -## 5.1 问题来源 - -问题来源于品牌词库中的关键词问题集。 - -例如: - -- 品牌:12 -- 关键词:数字时尚 -- 问题: - - 数字时尚品牌有哪些值得关注的新趋势 - - 如何选择适合自己的数字时尚单品 - - 数字时尚和传统时尚的主要区别是什么 - -这些问题是监控任务的输入,不是单纯展示内容。 - -## 5.2 采集单元 - -建议将一次监控采集定义为: - -- 一个问题 -- 一个模型 -- 一次执行时间 - -即: - -`1 question x 1 model x 1 run_time = 1 条原始监控记录` - -## 5.3 指标来源 - -每次模型回答返回后,系统进行解析,得到结构化结果: - -- 是否提及品牌 -- 是否首位提及品牌 -- 是否首选推荐品牌 -- 情感倾向是否正向 -- 是否引用系统文章 -- 引用了哪篇文章 - -然后再做日级汇总。 - -## 6. 推荐数据模型 - -## 6.1 主数据表 - -### `brand_keywords` - -- `id` -- `brand_id` -- `name` -- `status` -- `created_at` - -### `brand_questions` - -- `id` -- `brand_id` -- `keyword_id` -- `current_version_id` -- `status` -- `created_at` - -说明: - -- 一条问题属于一个品牌和一个关键词 -- 数据监控页中的“高频问题”和“问题集”都可以从这里关联出来 - -### `brand_question_versions` - -- `id` -- `question_id` -- `question_text` -- `question_hash` -- `version_no` -- `is_active` -- `created_at` - -说明: - -- 问题文案一旦修改,新增版本,不覆盖历史版本 -- 历史监控记录绑定到具体 `question_version_id` - -## 6.2 原始采集表 - -### `question_monitor_runs` - -- `id` -- `brand_id` -- `keyword_id` -- `question_id` -- `question_version_id` -- `model_id` -- `run_date` -- `run_at` -- `task_batch_id` -- `status` -- `raw_answer_storage_key` -- `created_at` - -说明: - -- 一次执行一条记录 -- 模型原始回答建议放对象存储,数据库只存引用路径和摘要字段 - -### `question_monitor_parse_results` - -- `id` -- `run_id` -- `mentioned_brand` -- `top1_mentioned` -- `first_recommended` -- `positive_mentioned` -- `citation_article_count` -- `citation_platform_count` -- `parser_version` -- `created_at` - -说明: - -- 解析结果是后续聚合的直接来源 - -## 6.3 汇总表 - -### `question_model_daily_metrics` - -粒度: - -- `brand_id + keyword_id + question_id + question_version_id + model_id + date` - -字段: - -- `ask_count` -- `mention_count` -- `top1_mention_count` -- `first_recommend_count` -- `positive_mention_count` -- `citation_count` -- `updated_at` - -用途: - -- 回看单个问题在每天的表现 -- 支撑高频问题排行和问题详情 - -### `keyword_model_daily_metrics` - -粒度: - -- `brand_id + keyword_id + model_id + date` - -字段: - -- `question_count` -- `ask_count` -- `mention_count` -- `top1_mention_count` -- `first_recommend_count` -- `positive_mention_count` -- `citation_count` -- `mention_rate` -- `top1_mention_rate` -- `first_recommend_rate` -- `positive_mention_rate` -- `updated_at` - -用途: - -- 直接支撑你截图里的 30 天趋势和顶部指标卡 - -### `keyword_model_top_questions_daily` - -粒度: - -- `brand_id + keyword_id + model_id + date + question_id` - -字段: - -- `question_text_snapshot` -- `ask_count` -- `mention_count` -- `score` - -用途: - -- 支撑右侧“高频问题” - -### `keyword_model_citation_rank_daily` - -粒度: - -- `brand_id + keyword_id + model_id + date + cited_article_id` - -字段: - -- `citation_count` -- `citation_rate` - -用途: - -- 支撑引用排行榜 - -## 7. 查询口径定义 - -## 7.1 顶部指标卡 - -以所选: - -- 品牌 -- 关键词 -- 模型 -- 时间范围 - -为过滤条件,从 `keyword_model_daily_metrics` 汇总得出: - -- 提及率 = `sum(mention_count) / sum(ask_count)` -- 首位提及率 = `sum(top1_mention_count) / sum(ask_count)` -- 首选推荐率 = `sum(first_recommend_count) / sum(ask_count)` -- 正面提及率 = `sum(positive_mention_count) / sum(ask_count)` - -## 7.2 趋势图 - -直接查询 `keyword_model_daily_metrics` 最近 30 天数据。 - -每个点只是一行日汇总,查询量很小。 - -## 7.3 高频问题 - -从 `keyword_model_top_questions_daily` 取最近 30 天累计 TopN。 - -## 7.4 引用排行榜 - -从 `keyword_model_citation_rank_daily` 取最近 30 天累计 TopN。 - -## 8. 任务调度设计 - -## 8.1 为什么要控制任务量 - -监控任务量不是由页面访问量决定,而是由: - -- 品牌数 -- 每个品牌下关键词数 -- 每个关键词下问题数 -- 模型数 -- 每天执行次数 - -共同决定。 - -## 8.2 数据量公式 - -每日原始采集量近似为: - -`品牌数 x 每品牌关键词数 x 每关键词问题数 x 模型数 x 每日执行次数` - -例如: - -### 轻量场景 - -- 50 个品牌 -- 每品牌 10 个关键词 -- 每关键词 10 个问题 -- 5 个模型 -- 每天 1 次 - -则: - -`50 x 10 x 10 x 5 x 1 = 25,000 条/天` - -### 中等场景 - -- 100 个品牌 -- 每品牌 20 个关键词 -- 每关键词 20 个问题 -- 5 个模型 -- 每天 2 次 - -则: - -`100 x 20 x 20 x 5 x 2 = 400,000 条/天` - -如果每条原始回答按 5KB 估算: - -- 每天约 2GB 原始文本 -- 30 天约 60GB 原始文本 - -因此: - -- 原始回答不建议长期放 PostgreSQL -- 建议放对象存储 -- PostgreSQL 只保存解析结果和汇总结果 - -## 8.3 V1 推荐执行频率 - -为了控制数据量,V1 建议: - -- 每个问题每天默认执行 `1 次` -- 重要品牌或付费用户可提升为 `2 次` -- 不建议 V1 做高频小时级轮询 - -## 8.4 去重策略 - -如果同一品牌下多个关键词出现完全相同的问题文案,建议: - -- 通过 `question_hash` 检测重复问题 -- 同一品牌、同一模型、同一天内同 hash 的问题尽量复用一次执行结果 - -但聚合归属仍可按映射关系分别记账。 - -这样可以显著减少任务量。 - -## 9. 版本与历史口径 - -## 9.1 为什么必须版本化 - -如果用户把问题: - -- “数字时尚品牌有哪些值得关注的新趋势” - -改成: - -- “2026 年数字时尚品牌有哪些值得关注的新趋势” - -那前后两者其实已经不是同一个监控口径。 - -如果直接覆盖旧问题,历史趋势就会失真。 - -## 9.2 推荐做法 - -- 问题修改时新建 `question_version` -- 新运行记录绑定到新版本 -- 历史数据仍归属旧版本 -- 页面默认展示当前版本问题的聚合数据 - -如果要做历史总览,可按 `question_id` 聚合,但需要在文档里说明“跨版本聚合”。 - -## 10. 页面查询策略 - -## 10.1 页面不查原始表 - -数据页不能直接查: - -- `question_monitor_runs` -- `question_monitor_parse_results` - -这些表只用于: - -- 问题详情 -- badcase 回放 -- 算法验证 - -## 10.2 页面只查汇总表 - -数据页查询路径应为: - -1. 先查 Redis -2. Redis miss 后查 PostgreSQL 汇总表 -3. 后台异步更新缓存 - -## 10.3 Redis Key 建议 - -可按以下维度缓存: - -- `brand:{brand_id}:keyword:{keyword_id}:model:{model_id}:range:30d:summary` -- `brand:{brand_id}:keyword:{keyword_id}:model:{model_id}:range:30d:trend` -- `brand:{brand_id}:keyword:{keyword_id}:model:{model_id}:range:30d:top_questions` -- `brand:{brand_id}:keyword:{keyword_id}:model:{model_id}:range:30d:citation_rank` - -## 11. V1 性能判断 - -针对当前页面,如果采用“按问题采集、按天汇总、按 30 天查询”的模型,PostgreSQL 是够用的。 - -原因是: - -- 页面查询只查 30 天汇总数据 -- 每次查询最多几十到几百行 -- 高频问题和排行榜都可提前汇总 -- Redis 可以承接高频重复查询 - -真正可能变大的不是页面查询,而是: - -- 原始问题执行任务量 -- 原始模型回答存储量 - -所以要重点控制的是任务调度和原始结果留存,而不是页面 SQL 本身。 - -## 12. V1 推荐保留策略 - -建议: - -- 原始运行记录保留 `30 ~ 90 天` -- 原始回答正文放对象存储 -- PostgreSQL 解析结果保留 `90 天` -- 日级汇总表保留 `180 天` 或更久 - -如果页面只看 30 天,也建议汇总表多保留一段时间,方便后续扩展。 - -## 13. 升级边界 - -当出现以下情况时,再升级架构: - -- 问题量暴涨 -- 模型数明显增加 -- 每日执行次数明显增加 -- 页面支持自由筛选、明细钻取 -- 监控维度从 30 天扩展到半年、一年 - -此时可以逐步升级为: - -- 独立分析库 -- 更大规模任务调度 -- 更复杂的去重复用逻辑 -- 专用分析引擎 - -## 14. V1 最终建议 - -结合你当前页面和品牌词库结构,V1 最合理的设计是: - -1. 以 `问题` 为最小监控采集单元。 -2. 以 `关键词` 作为问题的聚合容器。 -3. 以 `问题版本` 保证历史口径稳定。 -4. 以 `日级汇总表` 支撑页面查询。 -5. 以 `对象存储` 保存原始回答,避免 PostgreSQL 膨胀。 -6. 以 `问题 hash 去重` 控制执行量。 -7. 默认每题每天执行 1 次,避免 V1 数据量失控。 - -这样做,逻辑、数据量、页面性能三者才能对齐。 - diff --git a/docs/refer/问题驱动的问题数据.png b/docs/refer/问题驱动的问题数据.png new file mode 100644 index 0000000..705fa3e Binary files /dev/null and b/docs/refer/问题驱动的问题数据.png differ