# 星图找达人列表页看后搜率列增强设计(阶段 1) ## 1. 背景 前置实验已经验证: - Chrome MV3 扩展可以在巨量星图详情页稳定运行 - 当前仓库已经存在详情页 `content/page/shared` 基础结构、构建脚本和测试 - 插件已经能够识别真实接口响应,并确认可用指标接口为: - `/gw/api/aggregator/get_author_ase_info?author_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=&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 ``` 缓存项建议包含: - `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 就具备较高可行性。