569 lines
18 KiB
Markdown
569 lines
18 KiB
Markdown
# 星图达人详情页看后搜率插件实验设计
|
||
|
||
## 1. 背景
|
||
|
||
目标站点为巨量星图达人详情页。当前已经人工确认:
|
||
|
||
- 达人详情页右侧可以看到两个“看后搜率”指标
|
||
- 页面 URL 中可观察到稳定的星图达人 ID
|
||
- `search_session_id` 在短时间实验中看起来可能不变,但不应作为稳定依赖
|
||
|
||
本次工作不是直接做完整产品,而是做一个最小可验证实验,确认浏览器插件能否自动获取这两个指标。
|
||
|
||
## 2. 目标
|
||
|
||
本实验的目标只有一个:
|
||
|
||
- 在巨量星图达人详情页内,由 Chrome 插件自动拿到两个看后搜率,并以结构化结果输出到控制台或插件侧日志中
|
||
|
||
成功标准:
|
||
|
||
- 进入达人详情页后,插件无需人工复制数据
|
||
- 插件能自动拿到两个目标值
|
||
- 输出结果中包含达人标识、页面 URL、命中的请求 URL、两个看后搜率值
|
||
- 当两项值都提取成功时,明确标记成功
|
||
- 插件注入与拦截不能影响页面原有请求、渲染与交互
|
||
- 同一次详情页进入只输出一个最终结果;如果先失败后成功,可以升级为成功结果
|
||
|
||
## 3. 非目标
|
||
|
||
本实验明确不做以下内容:
|
||
|
||
- 不在找达人列表页批量抓取多个达人数据
|
||
- 不在列表页直接渲染看后搜率列
|
||
- 不复现页面接口签名或自行构造后台请求
|
||
- 不依赖 `search_session_id`
|
||
- 不做插件 UI 美化
|
||
- 不做导出、缓存、排序、批处理
|
||
- 不接入后端服务或数据库
|
||
|
||
## 4. 关键判断
|
||
|
||
### 4.1 达人标识
|
||
|
||
实验阶段将 URL 中的星图达人 ID 作为当前页面达人标识候选值。插件应优先从详情页路径中提取该 ID,并在日志输出中保留。
|
||
|
||
### 4.2 会话参数
|
||
|
||
`search_session_id` 不参与任何业务判断。它更像搜索会话或埋点参数,可能随着入口、刷新、筛选条件或路由变化而变化,不应作为稳定主键。
|
||
|
||
### 4.3 数据来源策略
|
||
|
||
本实验接受复用页面自己发出的请求来拿数据,因此优先方案为:
|
||
|
||
- 在页面上下文中拦截 `fetch` / `XMLHttpRequest`
|
||
- 解析页面真实收到的接口响应
|
||
- 从响应中提取两个看后搜率
|
||
|
||
这比插件自行复刻请求更适合当前最小实验,因为它不需要单独处理 CORS、Cookie、鉴权、签名和接口重放。
|
||
|
||
### 4.4 结果解释边界
|
||
|
||
如果插件在观察窗口内始终没有拿到完整结果,这只能说明“当前网络拦截路径尚未被证实可行”,不能直接推断为插件实现失败。
|
||
|
||
也就是说:
|
||
|
||
- 成功命中时,可以证明“网络拦截方案可行”
|
||
- 持续超时或只拿到无关响应时,只能证明“本轮实验未证实该路径可行”
|
||
- 若连续多次超时,应转向检查 DOM、内联 bootstrap 数据或更早期的数据注入方式,而不是继续盲目扩展字段匹配规则
|
||
|
||
## 5. 方案概览
|
||
|
||
插件采用 Chrome Manifest V3,先只支持达人详情页。
|
||
|
||
核心由两层脚本组成:
|
||
|
||
- `content script`
|
||
- 负责在详情页尽早注入页面脚本
|
||
- 负责接收页面脚本传回的数据
|
||
- 负责统一打印结构化结果
|
||
|
||
- `page hook script`
|
||
- 运行在页面上下文
|
||
- 包装 `window.fetch` 与 `XMLHttpRequest`
|
||
- 拦截和分析候选响应
|
||
- 一旦提取到两个看后搜率,通过 `window.postMessage` 发回内容脚本
|
||
|
||
首版默认不引入以下能力,避免实验面过大:
|
||
|
||
- 不使用 `background service worker`
|
||
- 不申请 `storage`、`tabs`、`scripting` 等额外权限,除非实现阶段确认必需
|
||
- 不做 popup、options page 或独立调试面板
|
||
|
||
## 6. 页面范围与前提
|
||
|
||
### 6.1 页面匹配范围
|
||
|
||
首版只匹配达人详情页,例如:
|
||
|
||
- `https://*.xingtu.cn/ad/creator/author-homepage/*`
|
||
|
||
Manifest 约束建议同时写清:
|
||
|
||
- `content_scripts.matches` 只覆盖上述详情页
|
||
- `run_at` 使用 `document_start`
|
||
- 注入到页面上下文的脚本通过 `chrome.runtime.getURL(...)` + `<script>` 标签加载
|
||
- 被注入的页面脚本必须放入 `web_accessible_resources`
|
||
|
||
如果实现阶段发现页面 CSP 会拦截上述脚本标签注入,则需要切换为单一路径兜底,而不是同时堆叠多套方案:
|
||
|
||
- 优先评估 `content_scripts.world = "MAIN"` 是否足够
|
||
- 若仍不足,再评估引入最小 `background` + `chrome.scripting` 路径
|
||
- 首版不应在尚未证实必要性时提前引入两套注入机制
|
||
|
||
### 6.2 前提假设
|
||
|
||
- 目标值来源于页面请求返回的数据,而不是纯 DOM 计算结果
|
||
- 页面在进入详情页时会触发至少一个包含目标数据的 JSON 请求
|
||
- 为提高命中率,实验允许用户在页面打开后刷新一次
|
||
|
||
## 7. 架构与数据流
|
||
|
||
整体数据流如下:
|
||
|
||
1. 用户进入达人详情页
|
||
2. `content script` 在 `document_start` 注入 `page hook script`
|
||
3. `page hook script` 拦截后续 `fetch` / `XHR`
|
||
4. 过滤候选响应并尝试解析 JSON
|
||
5. 从响应中提取两个看后搜率
|
||
6. 通过 `window.postMessage` 将结果发回 `content script`
|
||
7. `content script` 记录结构化日志,作为实验成功依据
|
||
|
||
建议统一输出如下结构:
|
||
|
||
```js
|
||
{
|
||
success: true,
|
||
stage: "captured",
|
||
routeKey: "6629661559960371207::/ad/creator/author-homepage/6629661559960371207::1",
|
||
pageStarId: "6629661559960371207",
|
||
responseStarId: "6629661559960371207",
|
||
pageUrl: "https://xingtu.cn/ad/creator/author-homepage/...",
|
||
matchedRequestUrl: "https://...",
|
||
requestMethod: "GET",
|
||
status: 200,
|
||
extractorLevel: "label-value",
|
||
rates: {
|
||
singleVideoAfterSearchRate: "0.5% - 1%",
|
||
personalVideoAfterSearchRate: "0.5% - 1%"
|
||
},
|
||
rawPathHints: [
|
||
"data.xxx.card_list[2].metrics[0]",
|
||
"data.xxx.card_list[2].metrics[1]"
|
||
],
|
||
source: "network",
|
||
capturedAt: 1776210000000
|
||
}
|
||
```
|
||
|
||
当无法完整提取时,`success` 必须为 `false`,并附带错误阶段信息。无论成功还是失败,最终结果结构都应稳定包含:
|
||
|
||
- `success`
|
||
- `stage`
|
||
- `routeKey`
|
||
- `pageStarId`
|
||
- `pageUrl`
|
||
- `capturedAt`
|
||
- `reason` 或 `rates`
|
||
|
||
## 8. 网络拦截策略
|
||
|
||
### 8.1 拦截范围
|
||
|
||
页面脚本同时拦截:
|
||
|
||
- `window.fetch`
|
||
- `XMLHttpRequest.prototype.open`
|
||
- `XMLHttpRequest.prototype.send`
|
||
|
||
原因:
|
||
|
||
- 站点可能混用 `fetch` 与 `XHR`
|
||
- 实验阶段不应预设调用方式
|
||
|
||
### 8.2 拦截实现约束
|
||
|
||
无论采用什么包装方式,都必须满足以下不破坏页面的约束:
|
||
|
||
- 包装 `fetch` / `XHR` 时不得改变原始返回值、异常行为或 `this` 绑定
|
||
- 读取 `fetch` 响应时只能消费 `response.clone()`,绝不能读取原始 body
|
||
- `XHR` 只在请求完成后读取 `response` / `responseText`,不得篡改业务侧回调链
|
||
- 对 `blob`、`arraybuffer`、`document` 等非文本响应直接跳过
|
||
- 所有 hook 逻辑必须 `fail-open`:自身异常只能吞掉并记录 `debug` 日志,不能阻断页面请求
|
||
- 页面脚本要有一次性 patch guard,避免重复注入导致多层包装
|
||
|
||
### 8.3 候选响应筛选
|
||
|
||
不是所有响应都值得解析。为减少噪声,先做轻量筛选:
|
||
|
||
- 响应 `content-type` 包含 `json`
|
||
- 请求 URL 包含达人详情、商业能力、种草价值、author-homepage、creator 等相关片段时优先
|
||
- 非 JSON 响应直接跳过
|
||
- 若响应头缺失 `content-type`,但 URL 命中高相关片段且响应文本形态像 JSON,可做一次受控解析尝试
|
||
- 为控制成本,可给响应文本设置大小上限;超限时只记录摘要,不进入深度扫描
|
||
|
||
注意:
|
||
|
||
- URL 过滤只是性能优化,不是唯一命中依据
|
||
- 真正成功与否仍由数据层提取结果决定
|
||
|
||
## 9. 数据提取策略
|
||
|
||
提取逻辑必须从拦截层中拆出,做成独立纯函数,以便单元测试。
|
||
|
||
推荐接口:
|
||
|
||
```ts
|
||
extractAfterSearchRates(payload: unknown): {
|
||
matched: boolean
|
||
success: boolean
|
||
extractorLevel: "exact-key" | "label-value" | "text-fallback" | "none"
|
||
rates?: {
|
||
singleVideoAfterSearchRate?: string
|
||
personalVideoAfterSearchRate?: string
|
||
}
|
||
rawPathHints: string[]
|
||
matchedLabels?: string[]
|
||
candidateStarId?: string
|
||
reason?: string
|
||
}
|
||
```
|
||
|
||
### 9.1 一级:精确字段匹配
|
||
|
||
优先尝试命中明显字段名,例如包含:
|
||
|
||
- `after_search_rate`
|
||
- `search_rate`
|
||
- `lookback_search`
|
||
- `kanhousou`
|
||
|
||
如果未来真实响应中存在明确 key,这一层应最稳定。
|
||
|
||
但要避免把泛化字段误判为目标值:
|
||
|
||
- 不要因为单独出现 `search_rate` 就直接判定命中
|
||
- 至少要求同层或近邻上下文同时出现 `after_search`、`看后搜` 等语义信号
|
||
- 若只能命中一个疑似字段,应降级为 `matched: true` 但 `success: false`
|
||
|
||
### 9.2 二级:半结构化 label/value 匹配
|
||
|
||
若响应是卡片化数据,可能不存在固定 key,而是类似:
|
||
|
||
- 某个数组项里包含 label 与 value
|
||
- label 为“单视频看后搜率”“单条视频看后搜率”“个人视频看后搜率”或近似文案
|
||
|
||
此时应按 label 识别两项值,并记录命中的对象路径。
|
||
|
||
实现上建议先做 label 标准化,再走小型同义词表:
|
||
|
||
- 去掉空格、全半角差异和无意义标点
|
||
- 将近义 label 归一到固定内部字段名
|
||
- 只在同一局部对象或同一卡片内成对提取,避免跨模块拼接出伪成功
|
||
|
||
### 9.3 三级:文本兜底匹配
|
||
|
||
若响应中没有明显结构,但存在文本片段,可用正则从附近文本中提取类似:
|
||
|
||
- `0.5% - 1%`
|
||
- `0.5%-1%`
|
||
|
||
文本兜底只用于实验,不应作为长期主路径。
|
||
|
||
为降低误判,文本兜底应限制在“已经被判定为相关卡片或相关子树”的局部文本中,不建议对整份响应做无上下文全文扫描。
|
||
|
||
### 9.4 成功判定
|
||
|
||
只有当以下条件同时满足时才记为成功:
|
||
|
||
- 找到单视频或单条视频看后搜率
|
||
- 找到个人视频看后搜率
|
||
|
||
任意一项缺失,都视为失败或半命中,不得报成功。
|
||
|
||
## 10. 达人 ID 提取规则
|
||
|
||
达人 ID 提取优先级:
|
||
|
||
1. 从当前详情页 URL 路径中提取 `pageStarId`
|
||
2. 如果响应体中也出现达人 ID,则作为 `responseStarId` 附加记录
|
||
3. 若两者不一致,不得用响应值覆盖页面值,而是保留双方并标记 `idMismatch: true`
|
||
4. `routeKey` 统一基于页面路由生成,不基于响应体里的达人 ID 反推
|
||
|
||
## 11. 消息桥设计
|
||
|
||
页面上下文与内容脚本之间使用 `window.postMessage` 通信。
|
||
|
||
消息建议形如:
|
||
|
||
```js
|
||
{
|
||
source: "star-chart-search-enhancer",
|
||
type: "AFTER_SEARCH_RATE_RESULT",
|
||
payload: {
|
||
success: true,
|
||
stage: "captured",
|
||
routeKey: "...",
|
||
pageStarId: "...",
|
||
matchedRequestUrl: "...",
|
||
extractorLevel: "label-value",
|
||
rates: {
|
||
singleVideoAfterSearchRate: "...",
|
||
personalVideoAfterSearchRate: "..."
|
||
},
|
||
rawPathHints: []
|
||
}
|
||
}
|
||
```
|
||
|
||
内容脚本需要做来源过滤:
|
||
|
||
- 只接受 `window === event.source` 的消息
|
||
- 只接受固定 `source` 与 `type`
|
||
- 校验 `payload` 的必要字段和字段类型
|
||
- 丢弃 `routeKey` 不是当前页面路由快照的陈旧消息
|
||
|
||
## 12. 日志与调试策略
|
||
|
||
实验阶段先不做复杂 UI,统一走控制台日志。
|
||
|
||
建议分三级输出:
|
||
|
||
- `info`
|
||
- 插件已注入
|
||
- 当前页面匹配详情页
|
||
- 当前达人 ID
|
||
- 当前 `routeKey`
|
||
|
||
- `debug`
|
||
- 命中候选请求 URL
|
||
- JSON 解析是否成功
|
||
- 提取逻辑命中了哪一级规则
|
||
|
||
- `result`
|
||
- 最终结构化结果对象
|
||
|
||
所有日志建议统一加前缀,例如 `[star-chart-search-enhancer]`。
|
||
|
||
日志只打印结构化结果、阶段信息、候选请求摘要和路径提示,不打印整份原始响应体。原因:
|
||
|
||
- 原始响应通常体积较大,会降低调试可读性
|
||
- 站点响应可能包含与本实验无关的业务字段,不应在控制台无约束暴露
|
||
- 对提取逻辑来说,`matchedRequestUrl`、`extractorLevel`、`rawPathHints`、候选 label 摘要通常已经足够排查
|
||
|
||
如确需保留真实样本做后续测试,应该采用脱敏后的 fixture 文件,而不是直接把原始 payload 打到控制台。
|
||
|
||
如果同页面多次命中:
|
||
|
||
- 同一 `routeKey` 默认只打印一个最终 `result`
|
||
- 若先出现失败终态、后出现完整成功结果,可以用成功结果覆盖前一次终态
|
||
- 若命中的是相同结果指纹,则不重复打印,避免控制台刷屏
|
||
|
||
## 13. SPA 与重复进入处理
|
||
|
||
巨量星图页面可能存在单页路由切换,因此需要兼顾以下情况:
|
||
|
||
- `history.pushState`
|
||
- `history.replaceState`
|
||
- `popstate`
|
||
|
||
建议为每次详情页进入生成一个新的 `routeKey`,例如:
|
||
|
||
```text
|
||
${pageStarId ?? "unknown"}::${location.pathname}::${navigationSeq}
|
||
```
|
||
|
||
在检测到路由切换到新的达人详情页时,插件应:
|
||
|
||
- 递增 `navigationSeq`
|
||
- 重置当前页面的命中缓存
|
||
- 更新当前达人 ID
|
||
- 广播新的 `routeKey` 给注入层或在注入层同步读取
|
||
- 等待后续请求再次命中
|
||
|
||
这样可以避免前一路由的慢响应在新页面落地后被错误归到当前达人。
|
||
|
||
## 14. 失败兜底策略
|
||
|
||
### 14.1 注入太晚
|
||
|
||
如果内容脚本注入时页面关键请求已经发完,则本轮可能拿不到数据。实验接受手工刷新一次页面作为前提条件。
|
||
|
||
### 14.2 拿到响应但不是可解析 JSON
|
||
|
||
记录:
|
||
|
||
- 请求 URL
|
||
- 请求方法
|
||
- 状态码
|
||
|
||
但不记为成功。
|
||
|
||
### 14.3 命中候选响应但字段未识别
|
||
|
||
输出:
|
||
|
||
- 命中的请求 URL
|
||
- 失败原因
|
||
- 可疑路径提示或 label 值摘要
|
||
|
||
便于后续迭代提取规则。
|
||
|
||
### 14.4 观察窗口超时
|
||
|
||
若在单次详情页进入或路由切换后的观察窗口内始终没有拿到完整成功结果,应输出一个明确的失败终态,避免用户无法区分“还在等待”与“本轮失败”。
|
||
|
||
建议:
|
||
|
||
- 观察窗口默认设为 8 到 10 秒
|
||
- 超时结果使用 `success: false` 与 `stage: "timeout"`
|
||
- 结果中附带 `candidateRequestCount`、`lastCandidateRequestUrl`、`pageStarId`、`routeKey`
|
||
|
||
### 14.5 多次命中或重复消息
|
||
|
||
同一页面多次命中时:
|
||
|
||
- 如果前一次不完整、后一次完整,保留后一次
|
||
- 如果都是完整结果,默认保留最新一次
|
||
- 对完全相同的 `routeKey + matchedRequestUrl + normalizedRates` 结果做去重
|
||
|
||
## 15. TDD 与测试策略
|
||
|
||
本实验虽然是浏览器插件,但提取逻辑必须先做测试驱动,避免每次改规则都依赖人工打开目标站点。
|
||
|
||
### 15.1 待测模块拆分
|
||
|
||
建议拆为三类模块:
|
||
|
||
- `extractors`
|
||
- 只负责从 JSON 中提取两个看后搜率
|
||
- 纯函数,可完整单测
|
||
|
||
- `page-hook`
|
||
- 负责拦截网络请求、调用提取器、发消息
|
||
- 可做轻量行为测试
|
||
|
||
- `content-bridge`
|
||
- 负责注入、接收消息、打印结果
|
||
- 可做消息桥测试
|
||
|
||
### 15.2 单元测试优先级
|
||
|
||
首批测试用例至少覆盖:
|
||
|
||
- 标准固定 key 命中
|
||
- label/value 结构命中
|
||
- 文本兜底命中
|
||
- 仅命中一个值时不能算成功
|
||
- 完全无关响应不应误判
|
||
- 百分比范围字符串存在空格或无空格时仍可识别
|
||
- 路由切换后的陈旧消息会被丢弃
|
||
- `fetch` 包装通过 `clone()` 读取,不消费原始响应
|
||
- hook 内部抛错时请求仍然正常返回
|
||
- 超时路径会输出明确失败结果
|
||
|
||
一旦首次命中真实页面响应,应立刻补充一组匿名化 fixture 测试:
|
||
|
||
- 从真实响应中删去无关字段与敏感标识
|
||
- 固化为最小可复现样本
|
||
- 用该 fixture 保护当前提取规则,避免后续重构把已验证路径改坏
|
||
|
||
### 15.3 集成验证
|
||
|
||
最小人工验证步骤:
|
||
|
||
1. 加载未打包的 Chrome 插件
|
||
2. 打开巨量星图达人详情页
|
||
3. 刷新页面一次
|
||
4. 打开 DevTools Console
|
||
5. 检查是否出现结构化结果对象
|
||
6. 将结果中的两个值与页面展示值逐项比对
|
||
|
||
### 15.4 成功证据
|
||
|
||
只有同时满足以下条件,才可宣布实验通过:
|
||
|
||
- 控制台出现结构化结果
|
||
- 两个值都存在
|
||
- 两个值与页面右侧显示一致
|
||
|
||
## 16. 建议的最小代码结构
|
||
|
||
建议以最小清晰结构开始,不为未来需求过度设计:
|
||
|
||
```text
|
||
src/
|
||
manifest.json
|
||
content/
|
||
index.ts
|
||
route-state.ts
|
||
page/
|
||
hook.ts
|
||
network-interceptor.ts
|
||
shared/
|
||
extract-after-search-rates.ts
|
||
normalize-rate-label.ts
|
||
get-star-id.ts
|
||
route-key.ts
|
||
message-types.ts
|
||
result-types.ts
|
||
tests/
|
||
extract-after-search-rates.test.ts
|
||
content-bridge.test.ts
|
||
page-hook.test.ts
|
||
route-state.test.ts
|
||
```
|
||
|
||
如果当前项目尚未有脚手架,可在实现阶段再补构建工具,不在本设计阶段提前锁死。
|
||
|
||
## 17. 实现边界
|
||
|
||
首版只交付以下能力:
|
||
|
||
- 一个可加载到 Chrome 的最小 MV3 扩展
|
||
- 在达人详情页自动注入 hook
|
||
- 自动抓取并输出两个看后搜率
|
||
- 覆盖关键提取逻辑的测试
|
||
- 明确的超时失败结果与去重策略
|
||
|
||
首版不交付:
|
||
|
||
- 列表页注入指标
|
||
- 批量抓取多个达人
|
||
- 导出和缓存
|
||
- 插件弹窗管理界面
|
||
- 后台服务
|
||
- 后台常驻 worker 与云端同步
|
||
|
||
## 18. 后续扩展方向
|
||
|
||
当详情页实验成功后,下一阶段可以考虑:
|
||
|
||
- 在找达人列表页按行补充这两个指标
|
||
- 从详情页拦截结果建立字段映射,逐步定位稳定接口
|
||
- 将成功命中的接口 URL 与字段路径固化,减少全量扫描成本
|
||
- 增加插件调试面板,展示最近一次命中的原始来源与提取路径
|
||
|
||
## 19. 风险与注意事项
|
||
|
||
- 目标站点前端实现可能变化,尤其是 label 文案和响应结构
|
||
- 若站点在页面初始化前就发出关键请求,注入时机不够早会影响命中率
|
||
- 若接口数据经过额外加密、压缩或二次映射,网络拦截不一定能直接得到最终值
|
||
- 若页面使用 Service Worker、流式响应或非常规封装,可能需要补充拦截策略
|
||
- 若两个看后搜率实际来自 SSR、内联脚本或前端二次计算,网络拦截实验会出现稳定超时,此时应尽快切换实验路径
|
||
|
||
## 20. 当前结论
|
||
|
||
在当前约束下,最合理的最小实验路径是:
|
||
|
||
- 不依赖 `search_session_id`
|
||
- 仅支持达人详情页
|
||
- 通过页面上下文拦截 `fetch/XHR`
|
||
- 将数据提取逻辑做成纯函数并用测试保护
|
||
- 用 `routeKey`、超时终态和去重规则保证日志可判读
|
||
- 成功后先用控制台结构化日志作为验收依据
|
||
|
||
只要详情页的两个看后搜率确实来自页面可见的 JSON 响应,这个实验具备较高可行性。
|
||
|
||
如果连续多次只得到 `timeout` 或无关候选响应,下一步应优先验证“数据是否根本不走网络响应体”这一前提,而不是继续扩大提取规则范围。
|