# 上游移植对照(references/ → catbus) 移植某个平台时查这份文档:上游的鉴权、登录、API 方法、要复制的 JS 资产、游客态和长连接的代码位置。 - 行号是写这份文档时的位置,只用来定位。动手前以 `src/platforms/
/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=