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

624 lines
18 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 星图找达人列表页看后搜率列增强设计(阶段 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 直接请求:
```text
/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)` 这种“泛化扫描整个响应”的路径,而应使用一个针对已知接口的专用映射函数,例如:
```ts
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
- 页面也可能复用行节点或重排节点
- 长持有旧节点会导致结果回写错位
正确方式是:
- 每次同步重新扫描当前可见行
- 只在渲染阶段临时持有当前节点引用
- 请求层、缓存层、批量调度层只保存 `authorId``listSeq``signature` 等轻量标识
- 异步结果回写前先校验 `listSeq``authorId` 绑定仍然匹配
## 12. 达人 ID 提取策略
阶段 1 的关键依赖是从每一行稳定拿到达人 `author_id`
优先级如下:
1. 从行内详情页链接提取
若头像、昵称、封面、跳转按钮链接指向达人详情页,则直接从 URL 中提取达人 ID。
2. 从行内 `data-*`、埋点属性、按钮参数中提取
若页面在行内直接存了作者 ID优先复用。
3. 若当前行无法提取 ID
不猜测,不发请求。该行两列显示 `加载失败`,并记录 debug 原因 `missing-author-id`
阶段 1 不依赖:
- `search_session_id`
- 当前页排序位置
- 当前页行号
## 13. 列表身份与陈旧结果抑制
阶段 1 需要明确区分“当前这次列表同步”和“前一次列表同步”。
建议引入:
```ts
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. 状态模型
每一行使用统一状态,驱动两列一起更新:
```ts
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 使用内存缓存:
```ts
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建议在现有项目基础上调整为以下职责边界
```text
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 就具备较高可行性。