18 KiB
星图找达人列表页看后搜率列增强设计(阶段 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. 方案对比
方案 1:DOM 增强 + content script 主动请求已验证接口
在列表页中识别表头和数据行,插入两列,然后由列表页 content script 主动请求已验证的指标接口补齐值。
优点:
- 最贴合当前仓库现状,不必引入新的页面注入资产
- 不需要复用详情页的网络 hook 扫描逻辑
- 风险集中在 DOM 识别、请求调度和状态回写上,边界清楚
- 后续导出阶段可复用同一批量请求与字段规范化链路
缺点:
- 需要稳定提取每行达人
author_id - 需要自己维护并发、缓存、失败重试和 DOM 更新
- 需要显式处理列表变化后的陈旧结果抑制
方案 2:DOM 增强 + 页面上下文请求桥
仍然做 DOM 增强,但实际请求不在 content script 中发,而是通过页面上下文桥接后由页面环境发起。
优点:
- 如果列表页接口对隔离环境请求有限制,这条路径更稳
缺点:
- 会引入新的页面资产、消息桥和构建改动
- 阶段 1 复杂度明显上升
- 当前没有证据表明必须这样做
方案 3:劫持列表接口并补字段后再交还页面
拦截找达人列表接口响应,修改返回对象,尝试让页面自己渲染新列数据。
优点:
- 理论上更接近“原生数据”
缺点:
- 侵入性高
- 列表接口未必包含或容易关联这两个字段
- 需要同时控制页面渲染结构和数据结构,不适合阶段 1
推荐结论
采用 方案 1:DOM 增强 + 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. 列表页架构与数据流
整体数据流如下:
- 用户进入找达人列表页
- 内容脚本入口识别当前为
market路由,启动 market controller - market controller 识别主表、表头与当前行集合
- 在
操作列前插入两列表头 - 为当前同步周期生成新的
listSeq - 对每一行提取达人
author_id - 先渲染行初始状态:
- 有
author_id的行为加载中... - 无法提取
author_id的行为加载失败
- 有
- 调度器按并发限制批量请求
get_author_ase_info - 成功后把对应行更新为真实值
- 失败后将该行两列更新为
加载失败 - 点击失败单元格时,以该行
author_id为单位重试 - 翻页、筛选、搜索、排序变化后,生成新的
listSeq并重新同步当前页
11. DOM 识别与插入策略
11.1 表格识别
优先以“表头文本语义 + 表格结构”识别主列表,而不是依赖脆弱 class 名:
- 找到包含
达人信息、操作等列标题的主表头 - 找到其对应的数据行容器
- 允许表头和数据区不是同一 DOM 层级
11.2 表头插入
插入规则固定为:
- 找到标题为
操作的列 - 在其前插入:
单视频看后搜率个人视频看后搜率
若 操作 列暂时识别失败:
- 不盲目插列
- 记录 debug 日志
- 等待 DOM 下一次稳定后再尝试
11.3 行单元格插入
对每一行:
- 找到对应的
操作单元格 - 在它前面插入两个插件单元格
- 插件单元格带稳定
data-*标记 - 插件单元格额外记录:
data-sces-author-iddata-sces-list-seqdata-sces-column
11.4 DOM 复用边界
实现时不要把 HTMLElement 长久缓存到请求层或缓存层。
原因:
- 列表页可能替换整块 DOM
- 页面也可能复用行节点或重排节点
- 长持有旧节点会导致结果回写错位
正确方式是:
- 每次同步重新扫描当前可见行
- 只在渲染阶段临时持有当前节点引用
- 请求层、缓存层、批量调度层只保存
authorId、listSeq、signature等轻量标识 - 异步结果回写前先校验
listSeq与authorId绑定仍然匹配
12. 达人 ID 提取策略
阶段 1 的关键依赖是从每一行稳定拿到达人 author_id。
优先级如下:
-
从行内详情页链接提取
若头像、昵称、封面、跳转按钮链接指向达人详情页,则直接从 URL 中提取达人 ID。 -
从行内
data-*、埋点属性、按钮参数中提取
若页面在行内直接存了作者 ID,优先复用。 -
若当前行无法提取 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>
缓存项建议包含:
statusratesupdatedAtinflightPromise
行为规则:
- 同一达人在同一标签页会话内再次出现时,优先复用成功缓存
- 若该达人正在请求中,不重复发请求,复用同一
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 就具备较高可行性。