Files
catbus/docs/upstream-map.md
cv-cat dc078b248b catbus:一个命令操作小红书、抖音、TikTok、B 站、快手、微博、闲鱼、淘宝、京东、X
10 个平台的 web 端全部用 TypeScript 移植自 cv-cat 的开源项目,请求与上游逐字节对拍。
统一的命令语法与 JSON 输出、按 平台 × 端 × 账号 隔离的登录态、给 AI Agent 用的技能与 llms.txt。
2026-10-06 19:24:55 +08:00

323 lines
39 KiB
Markdown
Raw Permalink 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.
# 上游移植对照(references/ → catbus)
移植某个平台时查这份文档:上游的鉴权、登录、API 方法、要复制的 JS 资产、游客态和长连接的代码位置。
- 行号是写这份文档时的位置,只用来定位。动手前以 `src/platforms/<p>/UPSTREAM` 记录的 commit 为准,重新核对一遍。
- catbus 的规范见 [AGENTS.md](../AGENTS.md),能力矩阵见 [capabilities.md](capabilities.md)。
- 「上游凭证来源」只用于理解上游代码和在本机跑上游对比;catbus 不读这些位置。
## 1. 通用
- 10 个上游都是 Python。
- HTTP 以 `curl_cffi` 为主,用来模拟 Chrome 的 TLS / JA3 与 HTTP/2 指纹。
- Weibo、Xianyu、Taobao 用的是 `requests`。
- 签名 JS 在上游里有两种调用方式:
- `subprocess` 调 `node`;
- `PyExecJS`,共三处:XHS 的 `xhs_creator_util.py`、Xianyu 的 `utils/goofish_utils.py`、Taobao 的 `utils/taobao_utils.py`。
catbus 里统一改成 `node:vm` 执行(见 AGENTS.md 7.4)。
- 上游 JS 实际 `require` 的第三方 npm 模块只有三个,其余都是 Node 内置模块:
- `jsdom`:JD;
- `crypto-js`:XHS;
- `@napi-rs/canvas`:JD;KS 的 `kwf_oracle.js` 里是 try/catch 的可选依赖。
### 1.1 Python → TS 替换对照
| 上游 Python | catbus |
|---|---|
| `curl_cffi`、`requests` | wreq-js(`core/http.ts`) |
| `websockets`、`websocket-client` | wreq-js 的 WebSocket(`core/stream.ts`) |
| `protobuf` + pb2 | protobufjs |
| `blackboxprotobuf`(无 schema 解码) | protobufjs 的 Reader |
| `onnxruntime` | onnxruntime-web |
| `opencv`、`scikit-image`、`scipy`、`numpy` | @techstark/opencv-js,其余手工移植 |
| `opencv` 的视频探测(`cv2.VideoCapture` 取帧率、帧数、宽高) | `core/mp4.ts` 读 MP4 / MOV 的盒子(xhs、tiktok);截帧要解码,不做 |
| `Pillow` | @napi-rs/canvas |
| `ddddocr` | 移植到 onnxruntime-web 上 |
| `pydantic` | zod |
| `loguru` | catbus 的 stderr logger |
| `subprocess node x.js`、`PyExecJS` | `node:vm`(见 AGENTS.md 7.4) |
| `qrcode` | qrcode |
### 1.2 游客态生成
对应 AGENTS.md 5.2 的 `guest.json`。
| 平台 | 生成位置 | 内容 |
|---|---|---|
| xhs | `xhs_utils/common_util.py:34` `generate_a1`、`:42` `generate_web_id` | `a1`、`webId` |
| douyin | `dy_apis/login_api.py:861` `register_ttwid`;`utils/dy_util.py:107` `generate_msToken`、`:185` `generate_webid`;`utils/mstoken.py:308` | `ttwid`、`msToken`、`webid` |
| tiktok | 没有生成器,`msToken` 来自 cookie(`builder/auth.py:156`) | 游客态需要在 catbus 里补 |
| bilibili | `builder/auth.py:52` `BiliAuth.anonymous`;`utils/device.py` | `buvid3` / `buvid4` 等 |
| kuaishou | `ks_apis/login_api.py:732` `bootstrap_device_fingerprint` | 设备指纹 cookie |
| weibo | 上游没有:`WeiboMobileApis` 不带 cookie 访问 m.weibo.cn | 现在不带 cookie 会被 302 到访客页。catbus 访问 m.weibo.cn 时用新浪访客系统 `genvisitor2` 下发的访客 SUB / SUBP(登录后也是,登录 cookie 属于 .weibo.com);这只是 m.weibo.cn 的访问方式,web 端没有游客态 |
| xianyu | `utils/build_cookies.py:52` `build_initial_cookies` | 先调一次 mtop 拿 `_m_h5_tk`,再调一次拿 `cookie2`;含 `tfstk` |
| taobao | `taobao_apis.py:29` `get_token` | `_m_h5_tk` |
| jd | `utils/device_token.py:112` `get_device_fields` | 设备字段 |
| x | `builder/auth.py:26` `GUEST_ACTIVATE_URL` = `https://api.x.com/1.1/guest/activate.json`,`:87` `XAuth.guest_token` 惰性换取 | guest token,约 3 小时有效。上游的读接口并不带它;web 端不支持游客态,catbus 没有移植 |
### 1.3 长连接
对应 `live listen` / `msg listen`。
| 用途 | 位置 | 地址 |
|---|---|---|
| douyin 私信 | `dy_apis/douyin_recv_msg.py:42` | `wss://frontier-im.douyin.com/ws/v2` |
| douyin 直播 | `dy_live/server.py:178` | `wss://webcast100-ws-web-hl.douyin.com/webcast/im/push/v2/` |
| tiktok 私信 | `api/tiktok_chat.py:21` | `wss://im-ws.tiktok.com/ws/v2` |
| tiktok 直播 | `api/tiktok_web.py:5597` | `wss://webcast-ws.tiktok.com/webcast/im/ws_proxy/` |
| xhs 直播 | `apis/xhs_live.py:31` | `wss://apppush-rws.xiaohongshu.com/rwp` |
| kuaishou 直播 | `ks_apis/live_api.py:1169`、`ks_apis/live_ws.py` 的 `LiveDanmakuClient` | 由接口下发 |
| xianyu 私信 | `goofish_live.py:18` | `wss://wss-goofish.dingtalk.com/` |
| taobao 私信 | `taobao_live.py:19` | `wss://wss-cntaobao.dingtalk.com/` |
| jd 客服 | `jd_apis/jd_chat_ws.py:5` | `wss://ws1-dd.jd.com/` |
---
## 2. 各仓库
### xhs — Spider_XHS
- **鉴权**
- `xhs_utils/xhs_auth.py` 的 `XHSUnifiedAuth` 包含两个子鉴权:`.pc` 为 `XHSPcAuth`,`.creator` 为 `XHSCreatorAuth`,都继承自 `xhs_core/auth.py` 的 `XHSAuth`。
- 构造方式:`from_qrcode_login`、`from_phone_login`、`from_cookie`。
- 必需的 cookie 是 `a1` 和 `web_session`,按 host 分域存储。创作者中心对应 catbus 的 `creator` scope。
- **登录**:`apis/xhs_pc_login_apis.py` 的 `XHSLoginApi` 和 `apis/xhs_creator_login_apis.py` 的 `XHSCreatorLoginApi`,都有 `qrcode_login`、`phone_login`。
- **上游凭证来源**:`.env` 的 `COOKIES`。
- **API**(实例方法)
- `apis/xhs_pc_apis.py` 的 `XHS_Apis`:`get_note_info(url)`、`get_user_info`、`get_user_me`、`get_user_all_notes`、`get_user_all_like_note_info`、`get_user_all_collect_note_info`、`search_note`(`note_time` 对应 `--time`)、`search_some_note`、`search_user`、`get_search_keyword`、`get_note_all_comment`、`get_homefeed_recommend`、`get_unread_message`、`get_all_metions`、`get_note_no_water_video/img`
- `apis/xhs_creator_apis.py` 的 `XHS_Creator_Apis`:`post_note`、`upload_media`、`get_all_posted_notes`。视频的时长、宽高、帧率上游用 opencv(`extract_video_cover_and_metadata`),catbus 读 MP4 盒子算出同样的值;首帧封面要解码,catbus 不做,视频发布要 `--cover`
- 直播与私信:`apis/xhs_live.py` 的 `XHSLiveAPI`(HTTP:直播间、`get_chats` / `get_group_chats`、`get_message_history` / `get_group_message_history`、`get_following`、已读 / 撤回 / 删除)和 `XHSWebSocket`(RWP 长连:私信收发、直播间事件、`send_room_text` 发弹幕)
- `msg send` 先走长连(`send_private_message`),等发送回执最多 10 秒(上游只发不等);长连没发出去(连不上、握手失败)才走 HTTP 短链 `send_short_link_message`。帧已发出却没等到回执时报 `NETWORK`(`detail.sent` 为 true),不重发,免得对方收到两条。`live send` 的长连退路同样处理
- 创作者中心的登录态由主站会话桥接(`xhs_utils/xhs_auth.py` 的 `XHSUnifiedAuth.creator_auth` → `XHSCreatorAuth.from_pc_auth`):catbus 存进 `creator` scope 复用,失效时自动重新桥接一次。桥接在凭证副本上做,全部成功后才写回,中途失败时凭证不变
- `item publish --poi-name [--poi]`:上游按地点名搜索后取第一个;catbus 按 `--poi-name` 搜,给了 `--poi` 时取 id 相同的那个,否则取名称完全相同的,都没有时报 `USAGE`(按 id 搜不到地点,所以 `--poi` 不能单独用)
- 蒲公英 `apis/xhs_pugongying_apis.py`、千帆 `apis/xhs_qianfan_apis.py`:不移植(AGENTS 4.12)
- **JS 资产**(npm 依赖 `crypto-js`)
- `xhs_utils/xhs_core/js/`:`sign.js`、`xs_common.js`、`b1.js`、`mns.js`、`websectiga_cli.js`、`websectiga_env.js`,以及 mns keystream 的 JSON
- `xhs_utils/xhs_pc/js/`:`sign.js`、`xs_common.js`、`b1.js`、`mns.js`、`rap.js`、`rap_cli.js`、`rap_crypto.js`、`web_ssk.js`、`aes_encrypt.js`、`aes_decrypt.js`、`deflate.js`、`profile.js`,以及模板 / profile 的 JSON
- `xhs_utils/xhs_creator/js/`:`xhs_creator_sign.js`、`xhs_creator_signature.js`、`profile.js`,以及指纹 / profile 的 JSON
- **注意**:拿笔记详情需要 `xsec_token`,所以优先传 URL;catbus 输出的 xhs `url` 都带上它。
### douyin — DouYin_Spider
- **鉴权**
- `builder/auth.py` 的 `DouyinAuth.perepare_auth(cookieStr, web_protect_, keys_)`(方法名原文如此拼写)。
- 私信签名还需要 `ticket`、`ts_sign`、`client_cert`、`private_key`。catbus 里放在凭证文件的 `tokens` / `device`。
- 写接口和 passport 请求带 `x-tt-session-dtrait`:外层由 `utils/dtrait.py` 按请求 path 现算,内层是绑定设备的特征 blob。上游 `DouyinAuth.session_dtrait_header` 取内层的顺序是 `DY_DTRAIT_BLOB`(抓到的 blob)> 设备档案(`DY_DTRAIT_PROFILE` 指定的,否则是随包的 `utils/dtrait_profile.json`,由 `utils/dtrait_features.py` 的 `build_blob` 现算)。默认档案来自上游 `fix-dtrait-blob` 分支(还没合进 master,catbus 以它为基线,见 `UPSTREAM`),是从 Chrome 153 DevTools 取证固化的。
- 哪些请求必须按 path 现算(上游 `Header.with_session_dtrait`,`strict=True`、`allow_static=False`:缺素材就报错,不用成品头):发布(`create_v2`)、评论(`publish_comment` 的 `with_bd(require_dtrait=True)`)、收藏(`collect_aweme`)、收藏夹移动(`move_collect_aweme` / `remove_collect_aweme`)。`digg` 仍是算得出就带(成品头也行)。catbus 对应 `Douyin.withSessionDtrait`、`withBd(..., { requireDtrait: true })`。
- catbus 对应:凭证 `device` 里的 `dtrait_blob`、`dtrait_profile`(都可选,用 `--cookie` 的 JSON 对象导入),没有时用 `static/douyin/dtrait_profile.json`;`build_blob` 移植在 `web/dtrait.ts`。Math 指纹的入参全是字面常量,直接写 Chrome 的结果:Node 24 自带的 V8 在 `atanh(0.5)`、`log(3)`、`expm1(1)` 上与 Chrome 差 1 ULP(实测),macOS 的 libm 在 `tan(-1e300)` 上与 V8 差 1 ULP(对拍脚本把它固定成 V8 的值)。
- **登录**:`dy_apis/login_api.py` 的 `DYLoginApi`:`qrcode_login`、`send_sms_code`、`phone_login`、`save_credential`、`get_login_auth`。
- 短信默认走 passport-web(`/passport/web/send_code`、`sms_login`);`DY_PHONE_LOGIN_PROFILE=sso` 时改走 `login.douyin.com` 页的 SSO 链(`bootstrap_phone_auth`、`_send_sms_code_sso` → `/send_activation_code/v2/`、`_phone_login_sso` → `/quick_login/v2/`)。上游把它当显式开关,不是失败后的退路;catbus 对应 `auth login --method sms --sso`。
- **上游凭证来源**:`.env` 的 `DY_COOKIES`、`DY_LIVE_COOKIES`、`DY_TICKET`、`DY_TS_SIGN`、`DY_CLIENT_CERT`、`DY_PRIVATE_KEY`、`DY_DTRAIT_BLOB`、`DY_DTRAIT_PROFILE`(设备档案的路径)、`DY_SESSION_DTRAIT`(成品头,只用于非严格的请求,如 `digg`;发布、评论、收藏、收藏夹移动不接受)。
- **API**(静态方法,第一个参数为 `auth`)
- `dy_apis/douyin_api.py` 的 `DouyinAPI`:`get_work_info(url)`、`get_user_info(user_url)`、`get_user_all_work_info`、`get_work_all_comment`、`search_general_work`(五个筛选值 `sort_type` / `publish_time` / `filter_duration` / `search_range` / `content_type` 作为紧凑 JSON 放进 `filter_selected`,`content_type` 2 是图文 → `--type image`;`search_source` 为 `tab_search`)、`search_video_work`(视频频道,筛选值进 query,→ `item search --type video`)、`search_user`(`douyin_user_fans` / `douyin_user_type`)、`search_live`、`get_user_favorite`、`get_collect_list`(上游只取 `cursor=0` 的第一页;catbus 按响应的 `cursor` / `has_more` 翻页,`--folder` 也逐页找)、`move_collect_aweme` / `remove_collect_aweme`(→ `item collect / uncollect --folder`)、`get_user_follower_list/following`、`get_notice_list`(`notice_group` → `notice list --group`)、`get_feed`、`get_live_info`、`digg`、`publish_comment`、`collect_aweme`、`create_conversation`、`send_msg/image/video/file`、`send_share_aweme/share_photos/share_web/user_card`(→ `msg send --share`)
- 直播:`get_live_room_enter`(→ `live get`,其中的 `stream_url` → `live media`)、`get_webcast_detail`(im/fetch,带回的最近 15 条 → `live history`)、`get_live_contribution_rank` / `get_live_thousand_ticket_rank`(→ `live rank --ranking`)、`diggLiveRoom`(`count` → `live like --count`)、`sendMsgInRoom`
- 商品:`get_live_production` / `get_all_live_production`(直播间商品)、`get_live_production_detail`(→ `product get`)、`get_product_comments`(→ 商品评价,需要 `product_id` 和 `shop_id`;`tag_id` 来自 `get_product_comment_counter`,→ `--label`)
- `dy_apis/douyin_creator_api.py` 的 `DouyinCreatorAPI`:`post_images`、`post_video`(`poi` / `mix_id` / `hot_spot` / `allow_download` / 图集 `cover_index`、`cover_uri`)、`get_preview_video_list`(work_list → `item list`)
- TOS 上传:创作者中心的 `upload_video_direct` / `upload_video_parts` 与私信的 `_upload_source` 流程相同,catbus 合成一份 `web/tos.ts`;两边上游的差别(origin / referer、`content-crc32` 头的位置、uploadid 是否 quote、成功码的宽严)作为两套预设保留
- `dy_apis/douyin_im_media.py` 的 `DouyinIMMedia`:`upload_image` / `upload_video` / `upload_file`,以及卡片的 `build_share_aweme_content` / `build_share_photos_content` / `build_share_web_content` / `build_user_card_content`
- 没有移植:`send_sticker`(表情的 CDN 元数据要调用方给,上游没有取表情列表的接口)、`send_audio`(PC 端没有语音上传协议)、`get_conversation_list`、`get_live_pk_*`(AGENTS 4.12)
- **JS 与 proto 资产**(无第三方 npm 依赖)
- `static/{Live,PK,Request,Response}.proto`,旁边是对应的 pb2
- `utils/acrawler_runtime/`:`ac_vm.js`、`run_ac_node.js`、`browser_window_shape.json`、`canvas_actual_exact.json`
- `utils/challenge_template_runner.js`
- `utils/challenge_profile.json`、`utils/mstoken_common_profile.json`、`utils/mstoken_profile.json`、`utils/dtrait_profile.json`
- **注意**:`newsign/package.json` 声明了 `jsdom`、`canvas`、`sdenv`、`jsrsasign`,但没有任何代码引用,是遗留文件,不用管。
### tiktok — TiktokApis
- **鉴权**
- `builder/auth.py` 的 `TiktokAuth.from_cookie(cookie_str)`。
- 以存在 `sessionid`、`sid_tt` 或 `multi_sids` 之一判断已登录。
- **登录**:`api/tiktok_login.py` 的 `TiktokLoginAPI` 只有占位,调用即抛 `NotImplementedError`。所以只支持 cookie 登录。
- **上游凭证来源**:`demo.py` 读 `TIKTOK_COOKIE`。
- **API**:`api/tiktok.py` 的 `TiktokAPI`,继承自 `api/tiktok_web.py` 的 `TiktokWebAPI`,方法接收 `cookies_str`:
- `get_user_posted(secUid)`、`get_user_info(user_url)`、`get_video_detail(video_url)`、`get_all_comments`、`post_comment`
- 商城:`get_shop_product_detail`(→ `product get`)、`get_shop_product_reviews` / `get_all_shop_product_reviews`(→ 商品评价)
- 其他:收藏、关注与粉丝列表、`post_item_digg`、`post_follow_user`、私信(`api/tiktok_chat.py`)、`creator_publish`、`creator_publish_photos`
- 收藏夹:`post_collection_create` / `post_collection_modify_info` 的 `collection_status` 是网页 IDL 的 `CollectionStatus`(1 私密、3 公开),上游默认 1;`post_collection_modify_items`(→ `folder add`)只能加入,`commitIds` 一次一个;`post_collection_move_items` 是在收藏夹之间移动(AGENTS 4.12 不提供);`get_collection_candidate_item_list` 只是网页「加入收藏夹」对话框的候选列表,不单独做命令
- 通知:`get_notice_multi` 默认 group 500(动态:赞 / 评论 / @ / 关注),`get_inbox_notice_list` 默认 group 661(网页收件箱的系统通知 All,`template_notice` 结构),都 → `notice list`
- 直播:`enter_live_room`、`check_live_rooms` 用于只给房间号的 `live get` / `like` / `rank` / `send` / `media`;拉流地址上游不解析,catbus 读 `liveRoom.streamData` 或房间对象的 `stream_url`
- **不移植**:`_prepare_creator_media` 里用 ffmpeg 读任意容器、截首帧(catbus 要求 `--cover`,只读 MP4);`get_following_request_list`;旧版私信 WS;直播页的活动 / 游戏 / podcast 等辅助接口
- **JS 与 proto 资产**
- `static/Tiktok_Request_pb2.py`:只有 pb2,没有 .proto。需要从 pb2 里的序列化 descriptor 导出 schema 给 protobufjs。
- `signing/env/`:`env_core.js`、`sign.js`
- `signing/source/`:`webmssdk.js`、`webmssdk_ex.js`、`webmssdk_legacy.js`
- `signing/sdk_config.json`
- `reverse/tiktok_shop_bsid/env/`:7 个 JS,商城签名用
- 要移植成 TS 的 Python 签名:`signing/` 下的 `aws_v4.py`、`live_wire.py`、`protobuf.py`、`pure.py`、`shop_bsid.py`、`ticket_guard.py`
- **注意**:上游用了 `protobuf3_to_dict`,移植后用 protobufjs 的 `toObject` 代替。
### bilibili — BilibiliApis
- **鉴权**
- `builder/auth.py` 的 `BiliAuth`:`from_cookie`、`from_qrcode_login`、`from_sms_login`、`from_session(path)`(读 session.json,支持自动刷新)、`anonymous`。
- `is_login` 看 `SESSDATA`,csrf 取自 `bili_jct`。
- **登录**:`apis/bili_login_apis.py` 的 `BiliLoginApi`:`qrcode_login`、`sms_login`、`password_login`、`refresh_cookies`、`logout`。
- `refresh_cookies` 对应 catbus 的自动刷新;`logout` 对应 `auth logout` 的服务端登出(10 个平台里有服务端登出的只有 B 站和快手)。
- **上游凭证来源**:仓库根目录的 `session.json`(`utils/session.py` 写入),`cookies.txt` 是兼容镜像。
- **API**(静态方法,第一个参数为 `auth`)
- `apis/bili_apis.py` 的 `BiliApi`:`get_nav`、`search_type`、`get_video_info(bvid/aid)`、`get_video_detail`、`get_play_url`、`get_player_info`、`get_danmaku_seg`、`get_replies(oid)`、`get_user_info(mid)`、`get_user_videos`、`get_all_user_videos`、`get_rcmd_feed`、`get_popular`、`report_heartbeat`
- `apis/bili_creator_apis.py`:`post_video`、`submit_archive`(转载来源、同步动态、禁止转载)、`delete_archive`、`get_my_archives`、`post_dynamic`、`remove_dynamic`、`save_article_draft`、`submit_article`、`get_article_draft`、`delete_article_draft`
- `apis/bili_interact_apis.py`:`like`、`add_coin(also_like)`、`favour(add_media_ids, del_media_ids)`、`triple`、`add_reply(type_, root, parent)`、`delete_reply(type_)`、`send_danmaku(color, fontsize, mode)`
- `apis/bili_live_apis.py`:`get_room_init`、`get_room_by_mid`、`get_room_info`、`send_gift`、`send_danmaku(color, mode, reply_mid, reply_uname)`、`start_live`、`stop_live`
- **catbus 的对应**(不直观的几处)
- `item related` 取 `get_video_detail` 返回的 `Related`;`item search --type article` 是 `search_type(search_type='article')`。
- 评论区类型按参数识别:稿件 1、专栏(cv 号)12、动态 17。动态只对纯文字和转发动态成立:图文动态的评论区是 type 11 + 相簿 rid,专栏动态是 type 12 + cv 号,从动态 ID 换过去要动态详情接口,上游没有,所以 `comment list / add / delete` 标 ◐。
- `item subtitles` 用 `get_player_info` 返回的 `subtitle.subtitles[].subtitle_url` 下载字幕 JSON;上游的 `get_subtitle_view` 返回 protobuf,没有用。
- `folder items` 是非上游的:上游没有收藏夹内容接口,按网页端补的 `x/v3/fav/resource/list`(`api.favResources`),没有对拍。`item uncollect` 不带 `--folder` 时,用 `get_fav_folders` 多带 `type=2&rid=<aid>` 的请求(非上游,`api.favFoldersOf`)按 `fav_state` 找收着它的收藏夹,没有 `fav_state` 时从全部收藏夹移出。
- `article publish`:上游的 `save_article_draft` / `submit_article` 把 `banner_url` 固定为空串,没有专栏封面,所以没有 `--cover`。
- upos 上传:上游四段都走 `http_util.request`,cookie 是整份 B 站 cookie,发往 upos 节点(bilivideo.com)时也带,catbus 照做。
- 直播间参数传主播时,先用 `get_room_by_mid` 换房间号。
- 推荐流翻页时,把上一批的 `av_<aid>`(已关注为 `av_n_<aid>`)作为 `last_showlist` 带上;格式见 bilibili-API-collect,上游只给了参数。
- 长连接 `live/server.py`:op 3 的人气值和没有专门映射的 cmd 都输出为 Event `other`。
- `delete_archive` 需要极验结果(`validate` / `seccode` / `challenge`),上游没有自动过验证,catbus 的 `item delete` 标 ◐,不带验证时报 `RISK_CONTROL`。
- **JS 资产**
- `tools/sc_encrypt_bridge.js`:`apis/bili_gaia_apis.py:662` 调用,做风控指纹的加密上报。
- 它还需要 `_gt/bili-sc-sdk.js`(`bili_gaia_apis.py:67`)。这是从 CDN 下载的第三方 SDK,上游不入库,多数环境里不存在。缺它时上游只发明文上报,已实证写接口照样成功。
- catbus 的做法:只移植明文上报这一路,不复制 `sc_encrypt_bridge.js`。
- **验证码**:极验点选,`tools/geetest_solve.py`、`utils/geetest_hybrid.py`、`utils/geetest_metric.py`、`utils/geetest_ocr.py`,依赖 ddddocr、scipy、numpy、Pillow。
- catbus 的对应:`web/geetest.ts`(`utils/geetest_w.py` 的 w 加密,`geetest_solve.py` 的 fetch / auto / _post_ajax / _refresh / solve),`web/geetest-vision.ts`(geetest_ocr / geetest_metric / geetest_hybrid,匈牙利算法照抄 scipy 的 rectangular_lsap),`web/ddddocr.ts`(ddddocr 1.6.1 的检测与识别,模型在 `@cv-cat/catbus-assets-ocr`,PIL 的 LANCZOS 与转灰度重写成 TS),`web/geetest-manual.ts`(`tools/geetest_helper.py` 的 serve:自动识别失败时在终端里改为本地页面手动验证)。
- RSA 段(PKCS#1 v1.5)的随机填充改从 `core/rand.ts` 取,对拍时 gen.py 把上游的 `rsa_encrypt_key` 换成用框架随机字节的同一套填充。
- `geetest_vision.prepare()` 只是把切图落盘给人看,不影响请求,没有移植。
### kuaishou — KuaiShou-Spider
- **鉴权**
- `builder/auth.py` 的 `KuaishouAuth`:`prepare_auth(cookie_str)`、`initialize()`。
- `kwscode` / `kwssectoken` 只有 6 分钟有效期,每次使用时现算,不落盘。
- cookie 分三个站点:www、cp(创作者)、live。发布需要 `kuaishou.web.cp.api_st` 和 `kuaishou.web.cp.api_ph`。
- **登录**:`ks_apis/login_api.py` 的 `KuaishouLoginAPI`:`login_by_qr`、`request_mobile_code`、`login_by_mobile_code`、`sts_login`、`pass_token_login`、`login_browser_session`。
- `sts_login` / `pass_token_login` 用来换取子站点凭证,登录时一并完成。
- **服务端登出**:`ks_apis/live_api.py` 的 `logout_session`(passport `login/logout` → `/live_api/baseuser/userLogout`)+ `builder/auth.py` 的 `apply_live_logout`。上游只放行一个写死的房间页 Referer。
- **上游凭证来源**:`.env` 的 `KS_COOKIES`。
- **API**
- `ks_apis/kuaishou_api.py` 的 `KuaishouAPI`:`get_feed_hot`(new-reco 推荐流 → `feed list --kind recommend`)、`get_work_info`、`get_video_detail`、`get_comment_list`、`get_all_comment`、`get_sub_comment_list`、`get_profile`、`get_profile_feed`、`get_user_all_work`、`search_feed`、`search_user`、`get_relation`、`get_liked_list`、`get_collect_list`、`get_history_list`、`graphql`
- 直播:`ks_apis/live_api.py` 的 `KuaishouLiveAPI`(礼物:`gift_list` 首屏与 `sort_type=0` 的“更多礼物”,`emoji_all_gifts` 完整礼物字典);弹幕:`ks_apis/live_ws.py` 的 `LiveDanmakuClient`
- 发布:`ks_apis/publish_api.py` 的 `KuaishouPublishAPI`;发布后 `video_photo_list(query_type=2, post_publish=True)` 取 publishId,`video_publish_refresh` 取发布状态
- 翻页(catbus 补充):`get_feed_hot` 每页都回 `pcursor="1"`,catbus 的游标写成 `<pcursor>:<已取页数>`,请求时只用 pcursor;`video_photo_list` 的时间范围服务端限一年,`item list` 在一年窗口里按 `nextCursor` 翻,翻完把窗口前移一年(游标 `<cursor>:<窗口终点>`),某个窗口没有作品时停止
- 不在 `_LIVE_CURRENT_PATHS` 放行名单里的直播接口(`comment_add`、`comment_like`、`profile_like`、`liveroom_status` 等)上游运行时直接拒绝发包,不移植
- **上游基线**:当前移植的是 `feat/fix-slider-fingerprint-http2` 分支的 552cf60(还没合进上游 master),比 master d4fcfd0 多一个提交:刷新验证码指纹、gdfp 接受 HTTP/1.1。
- **浏览器档案**:`utils/fingerprint.py` 的 `get_profile()`(UA、平台、语言、时区、CPU、WebGL、屏幕 / 可用区 / 内外窗口几何)→ catbus `web/profile.ts` 的 `PROFILE`。请求头、gdfp manMachine 载荷、`captchaExtraParam` 都从它取值,不各自写死。
- **滑块验证码**:`KuaishouAPI._post` / `_get` / `graphql` 遇到 400002(GraphQL 是 `errors` + `data.captcha.url`)时调 `_pass_captcha` 过一次滑块,再重新序列化同一条 Cookie 线序重发。
- `builder/auth.py` 的 `prepare_captcha_context`:验证码 iframe 以 `verification-captcha` 产品名单独引导 webweapon,Cookie 线序 `captcha`(`kwpsecproductname` 出现两次)
- `utils/captcha.py`:`find_gap_x`(cv2 边缘图 + 带掩码的模板匹配,catbus 用 @techstark/opencv-js)、`build_trajectory`、`SlidingCaptcha`
- `utils/captcha_fp.py`(`gpuInfo` / `captchaExtraParam`;key35 / key36 是稳定的浏览器哈希,只有 key18–key26 每次现算)、`utils/gdfp_manmachine.py`(SDK_INIT → core / whole `/n/a/b`)、`utils/sign/captcha_crypto.py`(三路 LFSR + 32 字节头)
- 对应 catbus 的 `web/captcha.ts`、`gap.ts`、`captcha-fp.ts`、`gdfp.ts`、`captcha-crypto.ts`
- **未移植**:`KuaishouAuth.set_captcha_fingerprint`(传给 `SlidingCaptcha` 的 `fingerprint`,用浏览器现抓的 `gpuInfo` / `captchaExtraParam` 代替随包默认值):只有 Python API,没有 CLI 入口。catbus 始终用 `captcha-fp.ts` 的默认值。
- **HTTP 版本检查**:上游对 gdfp(`/s/w/c`、manMachine、vision、gameLive)检查协商出的协议,现在默认接受 HTTP/1.1,设 `KS_STRICT_GDFP_HTTP2=1` 才只认 HTTP/2;`utils/transport.py` 的 `_curl_http_version_value` 也接受字符串形式的枚举值。catbus 不查协议版本(wreq-js 拿不到协商出的协议),与上游的默认行为一致。
- **JS 与 proto 资产**
- `reverse/bundles/weapon/`:21 个 JS(kwf,以及 kws-0 到 kws-19)
- `reverse/js/cp-kwf.js`
- `reverse/tools/`:`kwf_oracle.js`、`kws_oracle.js`、`like_token_oracle.js`
- `reverse/fixtures/live_ws_proto.json`:直播 proto,可直接作为 protobufjs `Root.fromJSON` 的输入
- `reverse/fixtures/live_ws_maps.json`:`utils/live_proto.py` 用到的映射表
- **Python 侧签名**:`utils/sign/weapon_oracle.py`、`like_token.py`,要移植成 TS。
### weibo — WeiboApis
- **鉴权**:没有鉴权对象,各方法直接接收 `cookies_str`。
- **登录**:上游没有,只支持 cookie 登录。
- **上游凭证来源**:无。仓库另带一个 FastAPI 服务 `app.py`,不移植。
- **API**
- `apis/weibo_apis.py` 的 `WeiboApis`:`get_self_info`、`getUserInfo`、`getUserPosted`、`getWordComments`、`getWorkInfo`、`searchSome`、`get_user_all_posted`
- `apis/weibo_mobile_apis.py` 的 `WeiboMobileApis`(m.weibo.cn,上游不带 cookie;catbus 带访客 cookie,见 1.2):`getWorkInfo`(`item get`,`comment list` 缺作者 uid 时也用它查)、`searchSome`(`item search`:访客从第二页起是登录墙,只取第一页)。
- `apis/weibo_creator_apis.py` 的 `WeiboCreaterApis.post_weibo`。`type`(请求里的 `visible`):0 公开、1 仅自己、6 朋友圈(互相关注)、10 粉丝,对应 `--visibility public / private / friends / fans`
- **没有移植的**
- `WeiboApis.getWorkInfo(url)`:请求 `weibo.com/<uid>/<mblogid>`,只取页面里的 `window.$CONFIG`。现在的 weibo.com 是单页应用,页面只是一个壳,`$CONFIG` 是全站配置(登录时多一个当前用户 `user`,`get_self_info` 取的就是它),没有微博正文;正文由前端另外请求 `/ajax/statuses/show`,上游没有这个接口。上游自己的 `app.py` 取详情也走 `WeiboMobileApis.getWorkInfo`。
- `WeiboApis.searchSome`:只发请求,解析被注释掉,永远返回 None。
- **JS 资产**:`static/weibo.js` 存在,但 Python 里没有引用,不复制。
### xianyu — XianYuApis
- **鉴权**:没有独立的鉴权类。`build_initial_cookies()` 生成初始 cookie(含 `tfstk`,扫码登录前用),`XianyuApis(cookies, device_id)` 直接接收 cookie 字典;mtop 签名用 `_m_h5_tk`。
- **登录**:`goofish_apis.py` 里的函数 `qrcode_login()`。
- **上游凭证来源**:无。
- **API**
- `goofish_apis.py` 的 `XianyuApis`:`get_token`(私信长连的 accessToken)、`refresh_token`(`mtop.taobao.idlemessage.pc.loginuser.get`:cookie 登录的校验、`auth status`、`user get me`,以及 `msg listen` 每 10 分钟的续期,对应上游 `user_alive`)、`get_item_info(item_id)`、`upload_media`、`get_public_channel`(按标题和图片推荐类目,`public` 内部调用)、`get_default_location`、`public`(发布商品;`price.original_price` → 私有选项 `--original-price`)
- 私信:`goofish_live.py` 的 `XianyuLive`(async WebSocket):`create_chat`(`item_id` 默认 `891198795482`,→ `msg send --to`;`--to` 加 `--item` 时用指定商品)、`send_msg`、`list_all_conversations(cid)`(→ `msg history`:同一条连接上按 nextCursor 翻页,结果从旧到新;`--limit` 截在一页中间时游标为 `<这一页的起始游标>+<已输出条数>`,续翻时重取这一页、跳过已输出的)、`main` / `heart_beat`(→ `msg listen`)
- 连接、注册、ack、心跳、翻页与淘宝逐行相同,catbus 共用 `src/platforms/_shared/impaas.ts`
- **JS 资产**(catbus 原样复制到 `static/xianyu/`)
- `static/goofish_js_version_2.js`:`utils/goofish_utils.py` 经 PyExecJS 调用的 `generate_sign`、`generate_mid`、`generate_uuid`、`generate_device_id`、`decrypt`(base64 + MessagePack)
- `utils/gen_tfstk.js` + `utils/et_f.js`:生成 cookie `tfstk`(上游起 node 子进程)
- `static/goofish_js_origin_version_2.js`、`static/goofish_js_version_1.js` 上游没有用到,没有复制
- **注意**:上游用相对 CWD 的路径读 JS。catbus 从 `static/xianyu/` 按包内路径加载,不受 CWD 影响。
### taobao — TaoBaoApis
与闲鱼同属阿里系,结构几乎一致:mtop 签名 + 钉钉 wss 私信。
- **鉴权**:没有独立的鉴权类,`TaobaoApis(cookies, device_id)` 直接接收 cookie 字典。`get_token()` 调 `mtop.taobao.login.token.get.h5`,签名用 `_m_h5_tk`。
- **登录**:上游没有,只支持 cookie 登录。
- **上游凭证来源**:无。
- **API**
- `taobao_apis.py` 的 `TaobaoApis`:`get_token`、`get_goods_uid_encrypt_uid(goods_url)`(从商品页 HTML 里取卖家 `uid` / `encrypt_uid`,供 `msg send --item` 与 `user get <商品链接>` 用;上游没有按用户 ID 查询的接口)、`upload_media`
- 私信:`taobao_live.py` 的 `taobaoLive(cookies_str)`(async WebSocket):`list_all_conversations(cid)`(→ `msg history`:同一条连接上按 nextCursor 翻页,结果从旧到新;`--limit` 截断时的游标同闲鱼)、`create_chat`、`send_msg`(`sender_nick` 是 cookie `_nk_` 的原值,不解码)、`main` / `heart_beat`(→ `msg listen`)
- 连接、注册、ack、心跳、翻页与闲鱼逐行相同,catbus 共用 `src/platforms/_shared/impaas.ts`
- `utils/taobao_utils.py`:`generate_sign`、`generate_mid`、`generate_uuid`、`generate_device_id`、`decrypt`(实际是 base64 + MessagePack,由上游 JS 解码;import 了 blackboxprotobuf 但没用)、`trans_cookies`
- **JS 资产**:`static/taobao_js_20260407.js`
- **注意**
- 上游**没有商品详情和商品搜索接口**。`item_detail_url` 字段是从闲鱼代码带过来的,没有使用。矩阵里 `item get` / `item search` 是 ○。
- 与闲鱼一样,上游用相对 CWD 的路径读 JS;catbus 按包内路径加载。
### jd — JdApis
- **鉴权**
- `builder/auth.py` 的 `JdAuth`:`is_login` 要求有登录票据 `thor`(PC 扫码下发)或 `pt_key`(M 端),再加上 `pin`(依次取 `pin`、`pt_pin`、`_pst`)。
- 按 origin 保存 localStorage,catbus 里放在凭证文件的 `device`。
- `update_cookies(persist=)`、`flush()`。
- **登录**:`jd_apis/jd_login_api.py` 的 `JdLoginAPI.qr_login`,以及 `jd_apis/jd_sms_login_api.py` 的 `JdSmsLoginAPI.login`。
- **上游凭证来源**:`default_auth_path()`,可用 `JDAPIS_AUTH_FILE` 覆盖。
- Windows:`%LOCALAPPDATA%/JdApis/auth.json`
- macOS:`~/Library/Application Support/JdApis/auth.json`
- Linux:`$XDG_STATE_HOME/JdApis/auth.json`
- **API**:`jd_apis/jd_api.py` 的 `JdAPI`
- `get_product_detail(sku)`、`get_product_comments`、`search`、`search_wares`、`get_browse_history`、`get_follow_products`、`get_cart_num`、`check_session`、`diagnose`
- `get_order_list`(`:1348`,→ 扩展 `order list`)、`get_recommend_coupon(sku)`(`:929`,→ 扩展 `coupon list`)
- 客服:聊天相关方法,以及 `jd_apis/jd_chat_ws.py` 的 `JdChatWS`
- 参数对应:各接口的 `area` → 私有选项 `--area`;`get_order_list(date_range)` 的 `1` / `2` / 年份 → `order list --range 3m|this_year|<年份>`;`get_product_comments(count)` → `comment list --limit`;`get_chat_info` / `send_hello` / `send_text` 的 `order_id` → `msg send --order`;短信登录 `login(area_code)` 的国家码 → 从 `--phone` 的 `+` / `00` 前缀识别
- `diagnose`(`:800`):业务接口重试后仍 403 空 body 时只跑第一步 `check_session`,失效报 `AUTH_EXPIRED`,仍登录报 `RISK_CONTROL`(`rate_limit`);不再发 hotwords / getCartNum 探针
- `call_api` 命中 `_read_disposal`(`:413`,处置常常只在 `x-rp-content` 响应头里,body 为空、状态码 200)时照样返回原始结果:catbus 把解开的处置挂到结果上,报 `RISK_CONTROL`(`captcha`)
- 上游只返回原始 JSON、不判业务失败。catbus 的映射:非 JSON 响应、网关错误 `{code, echo}`(如 601)、`success: false` → `UPSTREAM`;登录墙(code 3、购物车的 `pin is null`、浏览历史的 `resultCode: -100`)→ `AUTH_EXPIRED`。`code` 非 0 只对核对过正常值的接口(hotwords、getAidInfo、getChatSessionLog)判失败,其余接口正常响应里 code 的含义没核对过(getRecommendCoupon 会回只有 `{"code":"1"}` 的响应)
- 没有移植:`get_diviner`(`:942`)的首请求要 `securityToken`,分页变体要「类目 `p`」和 `shopId`,上游注释要求传浏览器抓包值、没有调用处,也没有从详情接口推出 `p` 的方法,所以 `item related` 仍用 relsearch 的相关词搜索
- **JS 资产**(npm 依赖 `jsdom`、`@napi-rs/canvas`)
- `static/`:`h5st5_env.js`、`h5st5_lib.js`、`h5st5_server.js`、`pc_tk_lib.js`、`pc_tk_server.js`、`summer_cryptico_runner.js`
- `static/webm/env/run.js`、`static/webm/run/jdwebm-riskhandle.js`
- `static/jcap/env/`:`env_core.js`、`run.js`;网络桥 `http_bridge.py` 移植为 `src/platforms/jd/web/jcap/helper.ts` 的 `bridge`
- `static/jcap/run/`:`jcap_ujb96b.js`;图像求解 `captcha_solver.py` 移植为 `jcap/solver.ts`(由 `helper.ts` 的 `solve` 调用)。run.js 起这两个 Python 子进程的地方由预加载的 `jcap/host.ts` 换成 `helper.js`
- `static/jcap/run/models/`:`orientation_model_v2_0.9882.onnx`、`u2netp.onnx`,共约 81 MB,放进 `@cv-cat/catbus-assets-jd`,不复制到 `static/`
- 调用这些 JS 的 Python:`utils/h5st5.py`、`utils/device_token.py`、`utils/jcap_solver.py`、`utils/summer_cryptico.py`、`utils/webm.py`
- **注意**:上游 JS 用 `createRequire(<项目根>/package.json)` 解析 npm 模块。catbus 在 vm 里注入的 `require` 指向自己的 `node_modules`(见 AGENTS.md 7.4)。
- **验证码**:JCAP,依赖 onnxruntime、opencv、scikit-image、scipy。
### x — XApis
- **鉴权**
- `builder/auth.py` 的 `XAuth.prepare_auth(cookies_str)`,需要 `auth_token` 和 `ct0`。
- 支持 guest token(见 1.2),web 端不支持游客态,不移植;`prepare_auth` 未登录时本地生成 ct0 的分支同样不移植。
- **登录**:`x_apis/login_api.py`
- `XLoginApi.login_by_password`
- `XJetfuelLoginApi`:`login_by_password`、`login_by_password_pure`,后者需要 Castle profile。
- **账密登录不移植**(依赖 Castle token):catbus 只支持从浏览器导入 cookie。
- **上游凭证来源**:`.env` 的 `X_COOKIES`、`X_USERNAME`、`X_PASSWORD`、`X_PROXY`,以及 Castle profile 的 `CASTLE_PURE_RL_FILE`、`CASTLE_PURE_ONLY`。
- **API**(静态方法,第一个参数为 `auth`)
- `x_apis/x_api.py` 的 `XAPI`:`get_work_info`、`get_work_comments`、`get_all_work_comments`、`search_work`、`get_user_info(user_name)`、`get_user_post_note`、`get_user_all_post_note`、`get_home_timeline`、`get_viewer`
- `search_work` 的 `product`:`Top` / `Latest` 对应 `item search --sort general|latest`,`People` 对应 `user search`,`Media` 对应 `item search --type video|image`(结果里图片和视频混在一起,catbus 按 `Item.kind` 过滤);`Lists` 不做。
- `x_apis/x_write_api.py` 的 `XWriteAPI`:`post_tweet`(正文权重超过 280 时走 `create_note_tweet`,即 CreateNoteTweet 长推)、`post_thread`、`delete_tweet`、`favorite`、`retweet`、`bookmark`、`follow/unfollow`
- 权重算法在 `utils/x_util.py` 的 `tweet_weight`(twitter-text v3)。
- `create_tweet` 的 `quote_url` 对应 `item publish --quote`,`post_thread` 对应 `item publish --thread`。`comment add` 也走 `post_tweet`(带 `reply_to`,同 `main.py post --reply-to`),超长时同样自动长推。
- `_rest_post`(关注)的业务拒绝(158 / 160 / 161 / 162)是 HTTP 403 + `errors`,catbus 先按错误码映射,没有错误码的 401 / 403 才当作登录态失效。
- `x_apis/x_article_api.py` 的 `XArticleAPI`(文章,Premium 长文):`create_draft`、`update_title`、`update_content`、`update_cover`、`publish`、`delete`、`list_articles`、`upload_image`,编排入口 `post_article`
- 对应扩展命令 `article publish`(`post_article` 的 publish=True)、`article delete`;`list_articles`(草稿 / 已发布列表)没有暴露。
- Markdown → Draft.js content_state 在 `utils/article_util.py`,catbus 移植到 `web/article.ts`。
- 正文插图的相对路径按 `base_dir`(`main.py` 传 Markdown 文件所在目录)解析:catbus 用 `--text @file` 的文件目录(dispatch 记在 `textFile`)。上游的封面也按 `base_dir` 解析,catbus 的 `--cover` 是命令行参数,按当前目录。
- `extract_tweet_id`(发布时生成的文章推文):catbus 在 stderr 提示这条推文的链接,输出仍是文章的 `{id url}`。
- 媒体上传:`x_apis/x_media_api.py` 的 `XMediaAPI.upload`(`with_metadata=False` 时不登记元数据,文章图片用)
- 私信:`x_apis/x_dm_api.py` 的 `XChatAPI`:`get_initial_chat_page`(`msg list`)、`get_conversation_page`(`msg history`)、`get_users_by_ids`(`msg list` 补对方资料)
- `get_initial_chat_page` 的 `max_local_sequence_id` / `message_pull_version` 是增量同步的水位(取某个序号之后的新消息事件),不是翻页游标。翻更早的会话要 `GetInboxPageRequestQuery`(`continue_cursor`),上游没有,所以 `msg list` 只取首页。
- **JS 资产**(账密登录不移植,Castle 与 ui_metrics 不复制)
- `static/castle/`:`castle.js`、`castle.umd-BneRArir.js`、`env_core.js`、`rolldown-runtime-XXLRXQBO.js`、`run.js`、`castle_meta.json`、`chromium_zlib.meta.json`
- `static/ui_metrics.js`
- `static/graphql.json`(GraphQL 操作表)、`static/transaction_l1.json`
- 调用这些 JS 的 Python:`utils/castle_token.py`、`utils/ui_metrics.py`