624 lines
18 KiB
Markdown
624 lines
18 KiB
Markdown
# 星图找达人列表页看后搜率列增强设计(阶段 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 直接请求:
|
||
|
||
```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 就具备较高可行性。
|