star-chart-search-enhancer/docs/superpowers/specs/2026-04-15-star-chart-market-visible-page-columns-design.md

18 KiB
Raw Blame History

星图找达人列表页看后搜率列增强设计(阶段 1

1. 背景

前置实验已经验证:

  • Chrome MV3 扩展可以在巨量星图详情页稳定运行
  • 当前仓库已经存在详情页 content/page/shared 基础结构、构建脚本和测试
  • 插件已经能够识别真实接口响应,并确认可用指标接口为:
    • /gw/api/aggregator/get_author_ase_info?author_id=<id>&range=30
  • 已确认字段映射:
    • avg_search_after_view_rate -> 单视频看后搜率
    • personal_avg_search_after_view_rate -> 个人视频看后搜率

用户当前的新目标不是继续在详情页控制台验证,而是进入找达人列表页,在表格中直接新增两列,把这两个值自动补齐并显示出来。

本阶段是“现有插件能力的列表页扩展”,不是重新从空仓库设计一套插件。

2. 阶段拆分

本需求拆为两个阶段:

阶段 1

只处理当前列表页当前结果页:

  • 列表页自动新增两列
  • 页面进入后自动为当前页所有达人加载这两个值
  • 支持筛选、翻页、搜索、排序变化后的自动重跑
  • 支持同达人内存缓存
  • 支持失败后按整行重试

阶段 2

在阶段 1 稳定后再做:

  • 按当前筛选条件拉取全部达人
  • 插件自己的导出按钮与导出逻辑
  • 导出结果中带上这两个新增字段

本设计文档只覆盖阶段 1但会为阶段 2 预留状态结构与数据命名。

3. 目标

阶段 1 的目标是:

  • 在找达人列表页自动插入两列:
    • 单视频看后搜率
    • 个人视频看后搜率
  • 进入页面后自动批量加载当前页所有达人这两个值
  • 在列表变化后自动重新补齐当前页
  • 成功时展示真实值
  • 失败时展示 加载失败
  • 点击任一失败单元格,按整行重试
  • 对同达人做内存缓存,避免重复请求
  • 不回归现有详情页能力与已有测试

4. 非目标

阶段 1 明确不做:

  • 不处理全部结果导出
  • 不接管页面原生导出按钮
  • 不实现插件自己的导出按钮
  • 不支持按这两列排序
  • 不持久化缓存到 storage
  • 不做批量抓取全部分页结果
  • 不修改页面原始列表接口响应
  • 不把列表页逻辑继续堆进现有详情页 controller 内
  • 不复用详情页“通用响应扫描提取器”作为列表页主路径
  • 不接入后端服务

5. 用户已确认的产品约束

  • 两列形式:插入成两列
  • 列位置:放在最右侧 操作 列前
  • 首次状态:显示 加载中...
  • 失败状态:显示 加载失败
  • 失败重试:点击任一失败单元格,按整行重试
  • 加载方式:页面进入后自动批量加载当前页所有达人
  • 缓存方式:内存缓存
  • 表头文案:使用完整名称
  • 排序能力:不支持
  • 页面变化后行为:翻页、切筛选、搜索、排序变化后都自动重新补齐当前页
  • 阶段拆分:接受先做当前页列表增强,再做全部导出
  • 现有详情页实验链路继续保留,不因列表页阶段 1 被破坏

6. 方案对比

方案 1DOM 增强 + content script 主动请求已验证接口

在列表页中识别表头和数据行,插入两列,然后由列表页 content script 主动请求已验证的指标接口补齐值。

优点:

  • 最贴合当前仓库现状,不必引入新的页面注入资产
  • 不需要复用详情页的网络 hook 扫描逻辑
  • 风险集中在 DOM 识别、请求调度和状态回写上,边界清楚
  • 后续导出阶段可复用同一批量请求与字段规范化链路

缺点:

  • 需要稳定提取每行达人 author_id
  • 需要自己维护并发、缓存、失败重试和 DOM 更新
  • 需要显式处理列表变化后的陈旧结果抑制

方案 2DOM 增强 + 页面上下文请求桥

仍然做 DOM 增强,但实际请求不在 content script 中发,而是通过页面上下文桥接后由页面环境发起。

优点:

  • 如果列表页接口对隔离环境请求有限制,这条路径更稳

缺点:

  • 会引入新的页面资产、消息桥和构建改动
  • 阶段 1 复杂度明显上升
  • 当前没有证据表明必须这样做

方案 3劫持列表接口并补字段后再交还页面

拦截找达人列表接口响应,修改返回对象,尝试让页面自己渲染新列数据。

优点:

  • 理论上更接近“原生数据”

缺点:

  • 侵入性高
  • 列表接口未必包含或容易关联这两个字段
  • 需要同时控制页面渲染结构和数据结构,不适合阶段 1

推荐结论

采用 方案 1DOM 增强 + content script 主动请求已验证接口

同时明确一条边界:

  • 阶段 1 不做“双通道请求”
  • 如果后续实测证明 content script 的同源请求在列表页不可行,应停下重新修订设计,而不是临时在实现中偷偷加第二套请求桥

7. 页面范围

阶段 1 只在找达人列表页启用,例如:

  • https://*.xingtu.cn/ad/creator/market*

详情页逻辑保留,作为已验证链路和回归基线,但不作为阶段 1 的交付重点。

8. 数据源与请求策略

8.1 已确认接口

阶段 1 直接请求:

/gw/api/aggregator/get_author_ase_info?author_id=<id>&range=30

8.2 请求执行上下文

阶段 1 的请求由列表页 content script 发起,不复用详情页的页面 hook。

请求约束建议写死:

  • 使用 fetch
  • method: "GET"
  • credentials: "include"
  • AbortController 控制单请求超时
  • 不额外伪造签名,不改写页面 Cookie不依赖 search_session_id
  • 建议默认超时预算固定为 8000ms

这意味着列表页实现与详情页“拦截页面自身请求”的实验链路是两条不同路径,阶段 1 不需要把它们硬绑在一起。

8.3 响应映射策略

列表页阶段 1 不应再走 extractAfterSearchRates(payload) 这种“泛化扫描整个响应”的路径,而应使用一个针对已知接口的专用映射函数,例如:

mapAuthorAseInfoResponse(payload: unknown): {
  success: boolean
  rates?: {
    singleVideoAfterSearchRate: string
    personalVideoAfterSearchRate: string
  }
  reason?: "bad-response" | "missing-field"
}

原因:

  • 这个接口的字段已经明确,没必要再走启发式扫描
  • 继续复用详情页提取器会把阶段 1 绑定到不必要的共享行为上
  • 专用 mapper 更容易测、更不容易误判,也更适合阶段 2 复用

失败分类也应固定下来:

  • AbortError 或主动超时中止,归类为 timeout
  • 网络异常、非 2xx、JSON 解析失败,归类为 request-failed
  • 成功拿到响应但缺字段或结构不符,归类为 bad-response

8.4 字段与格式规范化

当前已确认的真实值格式包括:

  • <0.02%
  • 0.02 - 0.1%

展示前统一做轻量规范化:

  • <0.02% 保留
  • 0.02 - 0.1% 规范为 0.02% - 0.1%

若任一目标字段缺失,都视为本次响应不可用,不报成功。

9. 与当前仓库的适配约束

当前仓库已经有一套详情页入口、结果类型和测试基线。阶段 1 应按“并列模块”接入,而不是直接把市场页逻辑塞进现有详情页实现里。

建议约束:

  • src/content/index.ts 只负责做路由分发与公共 bootstrap
  • 现有详情页 controller 迁移为独立模块,避免被市场页逻辑污染
  • 市场页能力在 src/content/market/ 下实现
  • 详情页现有 src/page/* 注入链路阶段 1 不改或尽量少改

这样可以把列表页新增复杂度限制在 content 层,不去动已经稳定的详情页 page hook。

10. 列表页架构与数据流

整体数据流如下:

  1. 用户进入找达人列表页
  2. 内容脚本入口识别当前为 market 路由,启动 market controller
  3. market controller 识别主表、表头与当前行集合
  4. 操作 列前插入两列表头
  5. 为当前同步周期生成新的 listSeq
  6. 对每一行提取达人 author_id
  7. 先渲染行初始状态:
    • author_id 的行为 加载中...
    • 无法提取 author_id 的行为 加载失败
  8. 调度器按并发限制批量请求 get_author_ase_info
  9. 成功后把对应行更新为真实值
  10. 失败后将该行两列更新为 加载失败
  11. 点击失败单元格时,以该行 author_id 为单位重试
  12. 翻页、筛选、搜索、排序变化后,生成新的 listSeq 并重新同步当前页

11. DOM 识别与插入策略

11.1 表格识别

优先以“表头文本语义 + 表格结构”识别主列表,而不是依赖脆弱 class 名:

  • 找到包含 达人信息操作 等列标题的主表头
  • 找到其对应的数据行容器
  • 允许表头和数据区不是同一 DOM 层级

11.2 表头插入

插入规则固定为:

  • 找到标题为 操作 的列
  • 在其前插入:
    • 单视频看后搜率
    • 个人视频看后搜率

操作 列暂时识别失败:

  • 不盲目插列
  • 记录 debug 日志
  • 等待 DOM 下一次稳定后再尝试

11.3 行单元格插入

对每一行:

  • 找到对应的 操作 单元格
  • 在它前面插入两个插件单元格
  • 插件单元格带稳定 data-* 标记
  • 插件单元格额外记录:
    • data-sces-author-id
    • data-sces-list-seq
    • data-sces-column

11.4 DOM 复用边界

实现时不要把 HTMLElement 长久缓存到请求层或缓存层。

原因:

  • 列表页可能替换整块 DOM
  • 页面也可能复用行节点或重排节点
  • 长持有旧节点会导致结果回写错位

正确方式是:

  • 每次同步重新扫描当前可见行
  • 只在渲染阶段临时持有当前节点引用
  • 请求层、缓存层、批量调度层只保存 authorIdlistSeqsignature 等轻量标识
  • 异步结果回写前先校验 listSeqauthorId 绑定仍然匹配

12. 达人 ID 提取策略

阶段 1 的关键依赖是从每一行稳定拿到达人 author_id

优先级如下:

  1. 从行内详情页链接提取
    若头像、昵称、封面、跳转按钮链接指向达人详情页,则直接从 URL 中提取达人 ID。

  2. 从行内 data-*、埋点属性、按钮参数中提取
    若页面在行内直接存了作者 ID优先复用。

  3. 若当前行无法提取 ID
    不猜测,不发请求。该行两列显示 加载失败,并记录 debug 原因 missing-author-id

阶段 1 不依赖:

  • search_session_id
  • 当前页排序位置
  • 当前页行号

13. 列表身份与陈旧结果抑制

阶段 1 需要明确区分“当前这次列表同步”和“前一次列表同步”。

建议引入:

type ListSession = {
  listSeq: number
  signature: string
}

其中:

  • listSeq 每次检测到列表数据源变化时递增
  • signature 由当前页 authorId 列表和必要的 URL 查询参数组成

所有异步回写都必须满足:

  • 结果对应的 listSeq 仍等于当前 controller 的 listSeq
  • 行节点上的 data-sces-list-seq 与结果中的 listSeq 一致
  • 行节点上的 data-sces-author-id 与结果中的 authorId 一致

否则直接丢弃,不允许写回旧列表结果。

实现上还应补一条:

  • 真正写回前重新从当前 DOM 查询目标行和目标单元格,而不是信任旧闭包里的节点引用

14. 状态模型

每一行使用统一状态,驱动两列一起更新:

type RowStatus =
  | {
      state: "loading"
      authorId: string
      listSeq: number
    }
  | {
      state: "success"
      authorId: string
      listSeq: number
      source: "cache" | "network"
      singleVideoAfterSearchRate: string
      personalVideoAfterSearchRate: string
    }
  | {
      state: "error"
      authorId: string | null
      listSeq: number
      retryable: boolean
      reason: "request-failed" | "timeout" | "missing-author-id" | "bad-response"
    }

两个单元格共用一份行状态,而不是各自独立状态。

15. 缓存与请求去重

阶段 1 使用内存缓存:

Map<authorId, CacheEntry>

缓存项建议包含:

  • status
  • rates
  • updatedAt
  • inflightPromise

行为规则:

  • 同一达人在同一标签页会话内再次出现时,优先复用成功缓存
  • 若该达人正在请求中,不重复发请求,复用同一 inflightPromise
  • missing-author-id 不进入 authorId 缓存
  • 瞬时错误不作为长期成功缓存保存;用户点击重试时必须允许重新发请求

阶段 1 不做持久缓存。页面刷新后缓存丢失是接受的。

16. 批量加载、并发与重试策略

因为页面进入后要自动为当前页所有达人批量加载,所以必须限制并发,避免过载或拖慢页面。

建议:

  • 当前页自动批量加载
  • 并发上限设置为 4
  • 剩余任务排队
  • 单请求有独立超时

调度规则建议写清:

  • 当前页有 authorId 的行先全部进入 加载中...
  • 然后逐批更新
  • 列表变化后,未开始的旧任务直接丢弃
  • 已发出的旧任务即使返回,也必须经过 listSeq 校验后才能回写
  • 批量调度器只负责返回 authorId + listSeq + result,不直接持有或操作 DOM

失败重试规则:

  • 点击任一失败单元格,只重试该行对应的 authorId
  • 两列一起切回 加载中...
  • 若该达人当前已有 inflightPromise,则直接复用,不重复起请求

17. 列表变化监听

阶段 1 需要自动响应以下变化:

  • 翻页
  • 切换筛选
  • 重新搜索
  • 切换排序

推荐统一抽象成“列表数据源变化”,而不是分别写四套逻辑。

实现上建议组合使用:

  • MutationObserver 观察表格区域变化
  • 当前行 authorId 列表签名
  • 当前 URL / 查询参数变化

当以下任一变化发生时,触发一次新同步:

  • 当前页行集合改变
  • 列表主容器被替换
  • 搜索参数或分页参数改变

18. 渲染规则

18.1 初始态

当前页新行出现后:

  • authorId 的行两列显示 加载中...
  • authorId 的行两列显示 加载失败

18.2 成功态

成功后分别显示:

  • 单视频看后搜率
  • 个人视频看后搜率

18.3 失败态

失败后两个单元格都显示:

  • 加载失败

18.4 失败重试

点击任一失败单元格时:

  • 对应整行重试
  • 两列一起切回 加载中...
  • 再次根据结果统一更新

19. 错误处理与日志

阶段 1 的失败场景至少包括:

  • 当前行缺少 author_id
  • 请求超时
  • 请求失败
  • 响应结构异常
  • 返回值只拿到一项

处理原则:

  • 不阻断其他行
  • 单行失败不影响整页
  • 失败要有明确展示
  • 失败原因要能在日志中区分

日志建议保留统一前缀,例如:

  • [star-chart-search-enhancer] market-sync-start
  • [star-chart-search-enhancer] market-row-error
  • [star-chart-search-enhancer] market-stale-result-dropped

20. TDD 策略

阶段 1 必须继续用 TDD 推进尤其是列表页路由分发、DOM 增强和状态同步逻辑。

20.1 纯函数测试

新增或扩展:

  • 列表页详情链接中的达人 ID 提取
  • 指标接口响应到展示字段的专用 mapper
  • 值格式规范化
  • 列表签名生成
  • 行状态机变换

20.2 DOM 测试

新增基于最小 DOM fixture 的测试:

  • 操作 列前插入两列表头
  • 在每一行的 操作 单元格前插入两列
  • 已插入时不重复插入
  • 行状态在 loading -> success -> error -> retry 间正确切换

20.3 调度测试

新增测试覆盖:

  • 当前页自动批量加载
  • 同达人请求去重
  • 并发上限控制
  • 缓存复用
  • 列表变化后旧结果不回写新列表

20.4 路由与构建回归测试

必须保留并扩展现有测试:

  • Manifest 现在同时覆盖详情页和 creator/market
  • 详情页原有 content/page 行为不回归
  • 新的路由入口能在 market 页面启动正确 controller

21. 建议的代码结构调整

为了实现阶段 1建议在现有项目基础上调整为以下职责边界

src/
  content/
    index.ts
    detail/
      index.ts
    market/
      index.ts
      api-client.ts
      batch-loader.ts
      cache-store.ts
      dom-sync.ts
      id-extractor.ts
      list-signature.ts
      row-render.ts
      row-state.ts
  shared/
    get-star-id.ts
    normalize-rate-value.ts
    result-types.ts

说明:

  • 详情页 controller 应从当前 content/index.ts 中拆出去,避免市场页逻辑污染原实现
  • 列表页阶段 1 不建议修改 src/page/hook.ts
  • 列表页应使用专用 api-client + mapper,而不是借道详情页提取器

22. 交付边界

阶段 1 完成时,应当满足:

  • 找达人列表页能自动新增两列
  • 进入页面后当前页所有达人自动开始加载
  • 成功时显示真实值
  • 失败时显示 加载失败
  • 点击失败单元格可按整行重试
  • 翻页、搜索、筛选、排序变化后自动重跑
  • 同达人内存缓存生效
  • 自动化测试覆盖关键行为
  • 详情页现有能力与测试继续通过

阶段 1 完成时,仍然不要求:

  • 导出全部结果
  • 接管原生导出按钮
  • 排序这两列
  • 跨刷新缓存

23. 风险

  • 列表页 DOM 结构可能比详情页更容易变化
  • 行内达人 ID 不一定总能稳定拿到
  • 自动批量请求过多时可能受限流影响
  • 页面自身虚拟滚动或复用行 DOM 时,可能影响状态回写
  • 若 content script 的同源请求在实际页面环境受限,需要回到设计层重新决定是否引入页面请求桥

24. 当前结论

阶段 1 的正确方向已经明确:

  • 不继续做详情页控制台实验
  • 在现有仓库上增量添加 market page controller
  • 操作 列前插入两列
  • 用 content script 主动请求已验证接口
  • 用专用 mapper 处理已知字段,不复用详情页泛化提取器
  • listSeq、内存缓存、整行状态和失败重试形成闭环

只要列表页能够稳定拿到每行达人 author_id,且 content script 对该接口的同源请求可用,阶段 1 就具备较高可行性。