# 接口台账 · 视频号(shi_pin_hao)

> 唯一证据来源:`/tmp/aiddit_captures/` 下真实抓包。host=`http://crawler.aiddit.com`。
> 整体状态:**部分可用(搜索抖动 + 作者接口失败,但视频可下载)**。
> - 搜索:首次 `code=25011` 失败,重试 `code=0` 成功 → 服务端不稳定,需重试。
> - 作者 blogger:`code=25011` blocked;account_info:`code=10001` blocked。
> - 视频下载:`200 / video/mp4` 可下载 ✅。
> 鉴权:抓包请求体无显式 token 字段,统一走 crawler.aiddit.com 网关。

---

## 2026-06-17 URL 解包与 OSS v2 批量验证

本节来自 V4 诊断脚本 `tests/test_platform_video_url_candidates.py` 的真实探测。完整原始响应和临时 URL 仅保存在 gitignore 下的 `data/platform_url_probe/20260617_115219/`，本文只记录脱敏统计、字段路径、host 和状态。

### 探测规模

- 真实搜索 query：10 个。
- 搜索请求页数：18 页（page1 + cursor page2，部分 query 无结果或无 page2）。
- 搜索 item 样本：46 条。
- 抽取 URL 候选：`image=95`、`video_candidate=41`、`audio=2`。
- 推荐规则选中正片 URL：41 条。
- 其中 20 条 selected URL 做 OSS v2 批量验证。
- 历史对照 run：`v1_run_35f4f7ccd1cc`。

### 历史 run 对照

- 历史 run 决策：`ADD_TO_CONTENT_POOL=10`、`KEEP_CONTENT_FOR_REVIEW=5`、`REJECT_CONTENT=9`、`TECHNICAL_RETRY_REQUIRED=6`。
- 媒体状态：30 条全部为 `oss_upload_pending`，失败原因全部为 `oss_upload_response_invalid`。
- 6 条技术重试的决策直接原因均在 Gemini/OpenRouter 链路；但这 6 条 media 侧同时带有 OSS invalid 症状。
- 历史 runtime 不能反查原始字段路径：`platform_raw_payload` 只保留 `channel_content_id/channel_account_id`，没有完整 `video_url_list/image_url_list/content_link/bgm_data`。

### 25011 与翻页观察

- 本轮真实探测保留了 25011 retry 事件。
- 例：`怀旧经典歌曲` page1 前两次返回 `25011: 视频号接口异常: 获取搜索结果失败`，第 3 次返回 `code=0` 且 `item_count=9`。
- 例：`歌词同步滚动呈现` page1 第 1 次返回 25011，重试后成功。
- 例：`怀旧金曲` page1 重试后仍为 25011，最终 item_count=0。
- page 返回数量不固定：本轮见到 0、9、11、12 条；page2 也可能返回 0、9、11、12 条。

### 推荐 URL 解包规则

- 递归抽取搜索 item 中所有 URL，视频号也按广告/素材规则扫描，不能假设没有广告 MP4。
- 优先选择 `$.search.video_url_list[].video_url` 中的 findermp 正片 URL。
- 明确排除：`image_url_list` 封面/图片、`channel_account_avatar` 头像、音频/BGM、页面链接、广告/素材字段。
- 广告关键词不能用裸 `ad` 子串匹配，否则会误伤 `stodownload/head` 这类正常视频号 URL；应按字段路径 token 或分隔符匹配。
- 下载/轻量验证需带 `Referer: https://channels.weixin.qq.com/`。

### 本轮真实字段与 host

- selected 字段路径：`$.search.video_url_list[0].video_url=41`。
- selected host：`findermp.video.qq.com=41`。
- 所有 selected 样本轻量验证均为可下载视频：HTTP `200/206`，`content_type=video/mp4`，Range 返回字节范围或完整小文件。

### OSS v2 验证结论

- 使用生产当前 payload 形态（把 `referer` 作为 dict 字段传给 OSS v2）时：20/20 返回 HTTP 200 但 `oss_object.cdn_url` 缺失，分类为 `oss_upload_response_invalid`。
- 快手抽样复打证明该 invalid 与 `referer` dict payload 强相关；去掉 `referer` 后 OSS v2 能返回 `oss_object.cdn_url`。
- 使用同一批视频号 selected URL、去掉 `referer` 字段后做 no-referer 复验：20/20 成功返回 `oss_object.cdn_url`。
- 结论：视频号 selected URL 本身可下载、可转存；历史 30/30 invalid 更像 OSS v2 payload 兼容问题，不是 findermp URL 字段拆错。

### 生产改造建议

- 第一优先级：调整 OSS v2 调用 payload，不再把 `referer` dict 直接传给 v2；如 OSS 侧确需 headers，应与上传服务约定字符串/headers 字段格式后再传。
- 第二优先级：保留视频号 25011 retry 事件到 query failure / raw payload，区分“无结果”和“重试耗尽”。
- 第三优先级：生产 runtime 里补充 URL 候选摘要：`selected_path`、`selected_host`、`probe_status`、`content_type`、`content_range`、`oss_payload_mode`、`oss_failure_type`。

---

## 1. 关键词搜索(PLT_SHIPINHAO_KEYWORD)

### ① 身份
- source_id:`PLT_SHIPINHAO_KEYWORD`
- 接口名:视频号关键词搜索
- 用途:按关键词检索视频号内容(搜索页 → 内容)
- 状态:**verified(不稳定)** —— 首次 `code=25011` 失败,重试 `code=0` 成功有数据。证据:`sph_search.json`(失败)+ `sph_search_retry.json`(成功)。

### ② 请求标准
- method:`POST`
- path:`/crawler/shi_pin_hao/keyword`
- 鉴权:无显式 token(走网关)
- 参数表:

| 名 | 类型 | 必填 | 示例 |
|---|---|---|---|
| keyword | string | 是 | `"彩虹"` |
| cursor | string | 否 | `""`(首页传空) |

### ③ 请求示例(真实 request_body)
```bash
curl -X POST 'http://crawler.aiddit.com/crawler/shi_pin_hao/keyword' \
  -H 'Content-Type: application/json' \
  -d '{"keyword":"彩虹","cursor":""}'
```

### ④ 响应信封 + 翻页
- 信封:`{code, msg, data}`
- 成功(retry):`code=0`,`msg=null`,`data` 为 `{has_more, next_cursor, data:[...]}`
- 失败(首次):`code=25011`,`msg="视频号接口异常: 获取搜索结果失败"`,`data=null`
- 翻页真实取值:`has_more=true`,`next_cursor=12`(数字型游标,翻下一页时回填到 `cursor`)

### ⑤ 字段台账(raw 名)
> 列表项位于 `data.data[]`,每项为内容对象。

| 字段名 | 类型 | 含义 | 提取标注 | 真实示例值 |
|---|---|---|---|---|
| channel_content_id | string | 内容ID | 直接可用 | `finderobjv03rf47ax8UTAiVchU7Uo2uexnucqpQD/XO8kw7opFqUs=` |
| title | string | 标题(含话题串) | 直接可用 / tag需正则抽 | `"…网友拍到圆形彩虹… #彩虹#祝你好运连连…"` |
| content_type | string | 内容类型 | 直接可用 | `video` |
| content_link | string | 内容链接(export 编码) | 直接可用 | `export/UzFfBgAAxJqjXGR8HB7MjMzT4DCK…` |
| video_url_list[].video_url | string | 视频地址(findermp) | 需后处理下载 | `https://findermp.video.qq.com/251/20302/stodownload?encfilekey=…` |
| video_url_list[].video_duration | int | 视频时长(秒) | 直接可用 | `8` |
| image_url_list[].image_url | string | 封面图地址 | 直接可用 | `https://findermp.video.qq.com/251/20304/stodownload?…` |
| channel_account_id | string | 作者ID | 直接可用 | `28ee8b6605e2288793f5386abaef5769` |
| channel_account_name | string | 作者昵称 | 直接可用 | `掌上巴彦淖尔` |
| channel_account_avatar | string | 作者头像 | 直接可用 | `https://wx.qlogo.cn/finderhead/ver_1/…/132` |
| like_count | int | 点赞数 | 直接可用 | `92` / `5513` |
| collect_count | int/null | 收藏数 | 直接可用(常为null) | `null` |
| comment_count | int/null | 评论数 | 直接可用(常为null) | `null` |
| share_count | int/null | 分享数 | 直接可用(常为null) | `null` |
| topic_list | array/null | 话题列表 | 需正则抽(列表常null,需从title解析) | `null`(标题内 `#彩虹` 需正则) |
| publish_timestamp | long | 发布时间(ms) | 需后处理(/1000) | `1780904037000` |
| has_more | bool | 是否有下一页 | 直接可用 | `true` |
| next_cursor | int | 下一页游标 | 直接可用 | `12` |

### ⑥ 提取与后处理规则
- **话题/tag**:`topic_list` 在搜索结果里基本为 `null`,tag 需从 `title` 用正则抽取(形如 `#彩虹`、`#见者好运`,以空格或 `#` 分隔)。
- **视频下载**:`video_url_list[].video_url` 为 findermp.video.qq.com 直链;实测(`sph_video_dl.json`)`http_status=200`、`content_type=video/mp4`、`content_range=bytes 0-784921/784922`、约 **784KB 可下载**。下载时需带 **Referer `https://channels.weixin.qq.com/`**。
- 时间戳为毫秒,落库前 `/1000`。

### ⑦ 边界与失败
- **搜索不稳定(需重试)**:同一请求首次返回 `code=25011: 视频号接口异常: 获取搜索结果失败`(`sph_search.json`),重试返回 `code=0` 并带数据(`sph_search_retry.json`)。**实现必须带重试**(建议指数退避,≥15s 限流间隔)。
- 限流:平台请求间隔 ≥15s。

### ⑧ 落点
- canonical:`platform_content_id`(←channel_content_id)、`title`、`video_url`(←video_url_list[0].video_url)、`platform_author_id`(←channel_account_id)、`author_name`(←channel_account_name)、`tags`(从title正则)、`like_count`、`publish_time`。
- 游走边:`search_page_to_content`(搜索→内容)、`video_to_author`(内容→作者,channel_account_id)、`video_to_hashtag`(title 抽 tag)。

---

## 2. 视频下载(PLT_SHIPINHAO_VIDEO_DL)

### ① 身份
- source_id:`PLT_SHIPINHAO_VIDEO_DL`
- 接口名:视频号视频直链下载
- 用途:下载搜索/详情返回的 `video_url`
- 状态:**verified** ✅。证据:`sph_video_dl.json`。

### ② 请求标准
- method:`GET`(直链)
- path:findermp.video.qq.com 直链(非 crawler 网关),来自 `video_url_list[].video_url`
- 鉴权:URL 自带 `encfilekey`/`token`;需 Header `Referer: https://channels.weixin.qq.com/`

### ③ 请求示例
```bash
curl -L 'https://findermp.video.qq.com/251/20302/stodownload?encfilekey=Cvvj5Ix3eew…&token=ic1n0xDG6aw9Kicy8QAXnibeBK0' \
  -H 'Referer: https://channels.weixin.qq.com/' \
  -o sph_video.mp4
```

### ④ 响应信封
- 非 JSON,二进制流。`http_status=200`,`content_type=video/mp4`,`content_range=bytes 0-784921/784922`,`bytes_received=784922`,`looks_like_video=true`。

### ⑤ 字段台账
| 字段名 | 类型 | 含义 | 提取标注 | 真实示例值 |
|---|---|---|---|---|
| (binary) | bytes | mp4 视频流 | 需后处理下载 | 784922 bytes |
| content_type | header | 内容类型 | 直接可用 | `video/mp4` |

### ⑥ 提取与后处理
- 直接落地为 `.mp4` 文件;约 784KB(本样本)。后续可送多模态分析。

### ⑦ 边界与失败
- token/encfilekey 有时效,需在搜索后尽快下载。缺 Referer 可能被拒。

### ⑧ 落点
- 物料落点:`video_local_path` / `video_url`;不新增游走边(从属于内容)。

---

## 3. 作者主页 blogger(PLT_SHIPINHAO_BLOGGER)

### ① 身份
- source_id:`PLT_SHIPINHAO_BLOGGER`
- 接口名:视频号博主作品列表
- 用途:作者 → 作品(author_to_works)
- 状态:**blocked(code=25011)**。证据:`sph_blogger.json`。

### ② 请求标准
- method:`POST` · path:`/crawler/shi_pin_hao/blogger`

| 名 | 类型 | 必填 | 示例 |
|---|---|---|---|
| account_id | string | 是 | `28ee8b6605e2288793f5386abaef5769` |
| is_cache | bool | 否 | `false` |

### ③ 请求示例
```bash
curl -X POST 'http://crawler.aiddit.com/crawler/shi_pin_hao/blogger' \
  -H 'Content-Type: application/json' \
  -d '{"account_id":"28ee8b6605e2288793f5386abaef5769","is_cache":false}'
```

### ④ 响应信封
- `code=25011`,`msg="视频号接口异常"`,`data=null` → **失败**。

### ⑤ 字段台账
- 无数据(blocked),字段未知。

### ⑥/⑦ 边界与失败
- blocked:作者作品接口当前不可用(25011),与搜索同源抖动。可考虑用搜索结果的 `channel_account_id` 暂时无法展开作品列表。

### ⑧ 落点
- 期望游走边 `author_to_works`,**当前不可用**。

---

## 4. 作者信息 account_info(PLT_SHIPINHAO_ACCOUNT_INFO)

### ① 身份
- source_id:`PLT_SHIPINHAO_ACCOUNT_INFO`
- 接口名:视频号账号信息
- 用途:作者画像补全
- 状态:**blocked(code=10001 参数异常)**。证据:`sph_account_info.json`。

### ② 请求标准
- method:`POST` · path:`/crawler/shi_pin_hao/account_info`

| 名 | 类型 | 必填 | 示例 |
|---|---|---|---|
| account_id | string | (实测被拒) | `28ee8b6605e2288793f5386abaef5769` |
| is_cache | bool | 否 | `false` |
| app_id 或 account_name | string | **至少一个**(据错误提示) | —(本次未传) |

### ③ 请求示例
```bash
curl -X POST 'http://crawler.aiddit.com/crawler/shi_pin_hao/account_info' \
  -H 'Content-Type: application/json' \
  -d '{"account_id":"28ee8b6605e2288793f5386abaef5769","is_cache":false}'
```

### ④ 响应信封
- `code=10001`,`msg="参数异常: app_id或account_name至少需要一个"`,`data=null` → **失败**。

### ⑤ 字段台账
- 无数据(blocked)。

### ⑥/⑦ 边界与失败
- blocked:本次只传了 `account_id`,网关要求 `app_id` 或 `account_name` 至少其一。后续若拿到 `account_name`(搜索结果有 `channel_account_name`)可再试。

### ⑧ 落点
- 期望补全 `platform_author_id` 画像,**当前参数不满足,blocked**。
