# AGENTS.md — catbus(猫巴士) > 本文件是本项目给所有 AI 编码代理(Claude Code / Codex / Cursor 等)的统一说明,也是 catbus 的规范本身。 > `CLAUDE.md` 通过 `@AGENTS.md` 引用本文件,**只维护这一份**。 ## 1. 目标 做**一个 CLI —— 操作各大平台接口的统一入口**。 - 一个命令 `catbus`,按统一的命令规范调用 10 个平台:小红书、抖音、TikTok、B 站、快手、微博、闲鱼、淘宝、京东、X。 - 能力来源是作者 [cv-cat](https://github.com/cv-cat) 的 10 个开源仓库,已克隆到 `references/`,作为**只读的原始来源**。 - 所有平台都在本仓库里**用 TypeScript 重写**,不在运行时调用上游的 Python。 - **用户只需要安装 Node**(版本见 7.1)。不需要 Python,不需要 C/C++ 编译器,也不需要其他系统依赖。 - `npm i -g catbus-cli` 装完就能离线使用。 - 必须支持全部目标系统(见 7.2)。 - 以 **npm 包**分发,不发 PyPI。安装体积不是约束,优先保证开箱即用。 - 上游目前只有 **web 端**接口。规范为 **app 端**、**pc 端**预留位置,例如 `catbus xianyu item get -e app`。 - 登录态统一保存在 `~/.catbus/`,**按 平台 × 端 × 账号** 隔离。web 端不登录几乎看不到内容,所以都需要登录;游客态留给 app / pc 端。 - 输出对人和对 Agent 都友好:stdout 只输出 JSON,字段跨平台统一。 - **同义同名**:含义相同的能力在所有平台上用同一个命令、同一组参数、同一种输出结构。例如商品一律叫 `item`,搜索一律叫 `search`。 - 将来的非 JS 上游(Go / Java……)通过 provider 协议接入(见 7.8)。 ### 1.1 命名(已定) | 项 | 取值 | |---|---| | 品牌 | **catbus(猫巴士)**:一只能载你去任何地方的猫,呼应 cv-cat | | slogan | 上车,开往任何平台 / all aboard, every platform | | 命令 | `catbus` | | npm 主包 | `catbus-cli`(npm 上的 `catbus` 已被别的库占用) | | npm scope | `@cv-cat`,用于 `@cv-cat/catbus-assets-jd`、`@cv-cat/catbus-assets-ocr` 和将来的 provider 包 | | 仓库 | https://github.com/cv-cat/catbus ,默认分支 `master` | | 许可证 | MIT(主包、assets-jd、assets-ocr 相同) | | 数据目录 | `~/.catbus/`,可用 `CATBUS_HOME` 覆盖 | | 环境变量前缀 | `CATBUS_` | | Logo | `assets/logo.svg`(图标)、`assets/logo-wordmark.svg`(横版)、`assets/banner.txt`(终端字符画)。猫巴士:车身就是一只橘猫,车头是猫脸和耳朵,车尾是尾巴,轮毂是肉垫;车顶路牌是终端提示符 `>_`;车窗里坐着小红书红、B 站粉、抖音青三只小猫乘客。会颠簸、转轮子、摇尾巴、眨眼、抖耳朵;字符画沿用 `( >ω_ )`。配色取自 Catppuccin Mocha | ## 2. 范围 - 目前支持第 3 节的 11 个平台的 web 端(10 个内容与电商平台 + 12306 铁路平台 `12306`);app / pc 端在注册表里为 planned(见 4.3)。 - 矩阵里的 ○(平台有这个概念、上游没有的能力)只占位:注册为 planned,执行时返回 NOT_IMPLEMENTED,有需要时再实现。移植中发现矩阵与上游不符(上游其实没有、或只是占位)的能力,按上游实际情况标为 ○ 或 ◐,以 `docs/capabilities.md` 为准。 - 新平台开始移植时,先把平台 id、别名、`item` 叫法等登记到第 3 节、4.2 和 4.5,再进注册表;在那之前 `catbus <平台>` 是未知平台(`USAGE`)。 - 改规范先改本文件,再写代码。 ## 3. 参考仓库 `references/` | 平台 id | 仓库 | 主要入口 | |---|---|---| | xhs | Spider_XHS | `apis/xhs_pc_apis.py`、`apis/xhs_creator_apis.py`、`xhs_utils/` | | douyin | DouYin_Spider | `dy_apis/douyin_api.py`、`dy_apis/douyin_creator_api.py`、`dy_apis/login_api.py` | | tiktok | TiktokApis | `api/tiktok.py`、`api/tiktok_web.py`、`signing/` | | bilibili | BilibiliApis | `apis/bili_apis.py`、`apis/bili_*_apis.py`、`builder/auth.py` | | kuaishou | KuaiShou-Spider | `ks_apis/kuaishou_api.py`、`ks_apis/publish_api.py`、`ks_apis/login_api.py` | | weibo | WeiboApis | `apis/weibo_apis.py`、`apis/weibo_mobile_apis.py`、`apis/weibo_creator_apis.py` | | xianyu | XianYuApis | `goofish_apis.py`、`goofish_live.py` | | taobao | TaoBaoApis | `taobao_apis.py`、`taobao_live.py`、`utils/taobao_utils.py` | | jd | JdApis | `jd_apis/jd_api.py`、`jd_apis/jd_login_api.py`、`builder/auth.py` | | x | XApis | `x_apis/x_api.py`、`x_apis/x_write_api.py`、`x_apis/login_api.py` | | 12306 | (无上游仓库,直接对接 12306 官方公开接口) | `https://kyfw.12306.cn`(接口端点见 `src/platforms/12306/UPSTREAM`) | 参考资料: - 每个仓库的鉴权、登录、API 方法、JS 资产、游客态生成位置,以及 Python → JS 的替换对照,见 [docs/upstream-map.md](docs/upstream-map.md)。 - 各平台的能力矩阵见 [docs/capabilities.md](docs/capabilities.md)。 规则: - `references/` 只读。 - 可以阅读,也可以运行(用来生成对拍数据,见 7.5)。 - 可以在上游 gitignore 的位置装依赖,例如 `node_modules/`。 - **不能修改其中任何受版本控制的文件**。 - `references/` 不进入本仓库的版本控制,它们各自是独立的 git 仓库。 - 每个平台的移植基于哪个上游 commit,记录在 `src/platforms/

/UPSTREAM`。 - `12306` 平台直接对接 12306 官方公开接口,无上游 `references/` 仓库,基准与接口说明见 `src/platforms/12306/UPSTREAM`;离线测试不采用 Python 对拍(golden),改用基于真实响应裁剪的 fixture,通过 `mockSender` / `fakeResponse` 回放测试。 ## 4. 命令规范 ### 4.1 语法 ``` catbus [参数...] [选项...] ``` - 选项可以出现在任意位置。`--` 之后的内容一律按参数处理,用于以 `-` 开头的文本。 - 端用选项 `-e/--endpoint` 指定,默认 `web`(见 4.3)。 示例: ```bash catbus xhs item get "https://www.xiaohongshu.com/explore/?xsec_token=..." catbus xhs user items --all -o jsonl catbus bilibili item get BV1xx411c7mD catbus douyin comment list --limit 100 catbus x user get elonmusk catbus jd item search 机械键盘 --sort sales catbus taobao msg send "在吗" --item <商品 url> catbus bilibili live listen --duration 10m catbus xhs auth login catbus kuaishou auth status -a work catbus xianyu item get -e app # app 端尚未实现:NOT_IMPLEMENTED,退出码 4 ``` ### 4.2 平台 id 与别名 | 规范 id | 别名 | |---|---| | `xhs` | `xiaohongshu`, `rednote` | | `douyin` | `dy` | | `tiktok` | `tt` | | `bilibili` | `bili`, `b` | | `kuaishou` | `ks` | | `weibo` | `wb` | | `xianyu` | `goofish`, `xy` | | `taobao` | `tb` | | `jd` | `jingdong` | | `x` | `twitter` | | `12306` | — | 目录名、配置键、输出里的 `platform` 字段一律使用**规范 id**。只有平台有别名;resource 和 action 没有别名。 ### 4.3 端(endpoint) - `-e/--endpoint web|app|pc`,默认 `web`,没有配置项可以改默认值。 - 每个平台在注册表里声明各端的状态:目前 web 为 available,app / pc 为 planned。 命令无法执行时,按下面的顺序判断: | 情况 | code | 退出码 | |---|---|---| | 端为 planned,命令在词表内(第 4.5、4.7 节) | `NOT_IMPLEMENTED` | 4 | | 端可用,命令是矩阵里的 ○(规划中) | `NOT_IMPLEMENTED` | 4 | | 命令是矩阵里的 —(平台没有这个概念),或者不在词表内 | `UNSUPPORTED` | 2 | - `UNSUPPORTED` 时,如果别的端有这个命令,`hint` 里提示加 `-e <端>`。 - 用了平台原生叫法(如 `note`、`video`),`hint` 里提示对应的规范词(见 4.6)。 - 参数和选项的校验(`USAGE`,以及筛选取值的 `UNSUPPORTED`)先于 ○ 的判断:写错的命令先报用法错误。端为 planned 时不做校验。 ### 4.4 动作语义 | 动作 | 语义 | |---|---| | `get` | 取单个对象 | | `list` | 取一个集合,不需要关键词。默认是当前账号自己的,或者推荐流 | | `search` | 按关键词搜索 | | 成对动作 | `like/unlike`、`collect/uncollect`、`repost/unrepost`、`follow/unfollow` | | 写入 | `add`、`send`、`publish`、`create`、`update`、`delete`、`upload` | | 长连接 | `listen`:持续输出 jsonl,直到 Ctrl-C 或 `--duration` 到期 | ### 4.5 通用词表 | resource | 含义 | action | |---|---|---| | `auth` | 登录态 | `login` `status` `logout` `list` `use` | | `user` | 用户 | `get` `search` `items` `likes` `collects` `reposts` `followers` `following` `follow` `unfollow` | | `item` | 平台的**主内容单元**(见下表) | `get` `search` `related` `list` `media` `download` `like` `unlike` `collect` `uncollect` `repost` `unrepost` `publish` `delete` `categories` | | `product` | 内容平台里挂的商品(抖音 / TikTok 商城等) | `get` | | `comment` | 评论,也包括商品评价 | `list` `replies` `add` `delete` `like` `unlike` | | `feed` | 推荐 / 热门 / 关注流 | `list` `categories` | | `live` | 直播 | `get` `list` `search` `categories` `listen` `history` `send` `like` `rank` `gifts` `products` `media` `replays` `start` `stop` | | `keyword` | 搜索词 | `suggest` `hot` | | `notice` | 通知(评论 / @ / 赞 / 关注 / 系统) | `list` `count` | | `msg` | 私信 / IM / 客服 | `list` `history` `send` `listen` `read` `revoke` `delete` | | `media` | 媒体上传 | `upload` | | `folder` | 收藏夹 | `list` `items` `create` `update` `delete` | | `series` | 合集 / 播放列表 | `list` `items` | | `history` | 浏览历史 | `list` | | `topic` | 话题 | `search` | | `poi` | 地点 | `search` | `item` 在各平台的含义: | 平台 | item = | |---|---| | xhs | 笔记 | | douyin | 作品 | | tiktok | 视频 | | bilibili | 稿件 | | kuaishou | 作品 | | weibo | 微博 | | x | 推文 | | xianyu | 闲置商品 | | taobao | 商品 | | jd | 商品 SKU | 各动作的说明: - `item list`:当前账号自己发布的内容,带审核状态,数据来自创作者中心。 - `item related`:相关推荐。 - `item media`:可播放 / 可下载的媒体地址。 - `item download`:下载到本地。 - `item categories`:发布和搜索时 `--category` 的取值。 - `feed list --kind recommend|hot|following`:默认 `recommend`。 - `live listen`:弹幕 / 礼物 / 进场等事件流。 - `live history`:最近的弹幕。 - `live media`:直播流地址。 - `live replays `:回放列表。 - `live start` / `stop`:开播 / 下播。 ### 4.6 平台原生叫法 → 规范词 | 平台原生叫法 | 规范词 | |---|---| | note / aweme / video / photo / work / post / status / tweet / goods / sku | `item` | | digg、favorite(X 的点赞) | `like` | | favour、bookmark、想要、关注商品 | `collect` | | retweet | `repost` | | board、collection(TikTok 收藏夹) | `folder` | | mix、playlist、collection(快手合集) | `series` | | reply、评价 | `comment` | | DM、IM、咚咚 | `msg` | | playurl | `item media` | | location | `poi` | | watch | `listen` | 原生叫法**不做别名**。用了原生叫法会报 `UNSUPPORTED`,`hint` 里给出规范词。 ### 4.7 平台扩展 平台特有、没有跨平台对应物的能力,才用扩展:可以在通用 resource 上加 action,也可以新增 resource。**新增扩展前先登记到本节,再写代码。** | 平台 | 命令 | 说明 | 输出 | |---|---|---|---| | tiktok | `folder add ` | 把视频加入收藏夹(一次一个视频)。TikTok 只能把已收藏的视频加进收藏夹 | `{id}`(视频 id) | | tiktok | `folder create [--visibility public\|private]` | 收藏夹公开 / 私密:用 4.9 的标准选项 `--visibility`,只取 `public`、`private`;默认 `private`(与上游一致) | Folder | | tiktok | `folder update [--name ] [--visibility public\|private]` | 同上。`--name`、`--visibility` 至少给一个,没给的保持原值 | Folder | | tiktok | `item publish [--allow-comment on\|off]` | 私有选项:允许评论,默认 `on` | Item | | tiktok | `item publish [--allow-duet on\|off]` | 私有选项:允许合拍(Duet),默认视频 `off`、图文 `on`(照上游) | Item | | tiktok | `item publish [--allow-stitch on\|off]` | 私有选项:允许拼接(Stitch),默认视频 `off`、图文 `on`(照上游) | Item | | tiktok | `item publish [--allow-content-reuse on\|off]` | 私有选项:允许他人复用内容,默认 `on` | Item | | tiktok | `item publish [--allow-ai-remix on\|off]` | 私有选项:允许 AI 改编,默认 `on` | Item | | bilibili | `item coin [--count 1\|2] [--like]` | 投币;`--like` 同时点赞 | `{id}` | | bilibili | `item triple ` | 一键三连 | `{id}` | | bilibili | `item subtitles ` | 字幕 | Subtitle[] | | bilibili | `danmaku list ` | 视频弹幕 | Danmaku[] | | bilibili | `danmaku send --offset <秒> [--color] [--font-size] [--position]` | 发视频弹幕。`--color` 为 `#RRGGBB` 或十进制,`--font-size` 默认 25,`--position scroll\|top\|bottom` 默认 `scroll` | `{id}` | | bilibili | `dynamic publish --text [--image]` | 发动态 | `{id url}` | | bilibili | `dynamic delete ` | 删动态。`` 为动态 ID 或动态链接(`dynamic publish` 输出的 `id`、`url` 都可以) | `{id}` | | bilibili | `article publish --title --text [--category] [--tag] [--summary] [--draft]` | 发专栏:先存草稿,再提交。`--category` 为专栏分区 ID(数字,默认 0),没有对应的 categories 命令;`--summary` 为摘要;`--draft` 只存草稿、不提交。上游没有专栏封面,所以没有 `--cover` | `{id url}` | | bilibili | `draft get ` | 专栏草稿 | Item(`kind` 为 `article`,`status` 为 `draft`) | | bilibili | `draft delete ` | 删专栏草稿 | `{id}` | | bilibili | `user items [--keyword <词>]` | 私有选项:只看投稿里匹配关键词的 | Item[] | | bilibili | `item collect` / `uncollect [--folder <收藏夹 id>]` | 私有选项:收藏进 / 移出指定收藏夹,多个用逗号分隔。不给时收藏进第一个收藏夹,取消时从所有收藏夹移出 | `{id}` | | bilibili | `item publish [--source <来源>] [--dynamic <文案>] [--allow-reprint]` | 私有选项:`--source` 表示转载并给出来源,不给为自制;`--dynamic` 为同步到动态的文案;默认禁止转载,`--allow-reprint` 允许 | Item | | bilibili | `comment add --reply-to [--root ]` | 私有选项:回复楼中楼时 `--root` 给根评论、`--reply-to` 给被回复的那条;不给 `--root` 时 `--reply-to` 就是根评论 | Comment | | bilibili | `live send [--color] [--font-size] [--position] [--reply-user ]` | 私有选项:弹幕样式同 `danmaku send`;`--reply-user` 为回复的观众(UID 或空间链接) | `{id}` | | xianyu | `item publish [--original-price <金额>]` | 私有选项:原价(元),要和 `--price` 一起用 | Item | | xianyu | `item publish [--shipping free\|distance\|fixed\|none]` | 私有选项:运费方式,默认 `free` 包邮;`distance` 按距离计费,`fixed` 一口价,`none` 无需邮寄 | Item | | xianyu | `item publish [--postage <金额>]` | 私有选项:一口价运费(元),配合 `--shipping fixed` | Item | | xianyu | `item publish [--pickup]` | 私有选项:支持自提 | Item | | douyin | `item search [--length all\|short\|medium\|long] [--range all\|seen\|unseen\|following]` | 私有选项:视频时长(不限 / 1 分钟内 / 1–5 分钟 / 5 分钟以上)、搜索范围(不限 / 看过 / 没看过 / 关注的人)。`--type video` 走视频频道搜索,其余走综合频道(筛选值随 `filter_selected` 发出);`--type image` 只要图文 | Item[] | | douyin | `user search [--fans 0_1k\|1k_1w\|1w_10w\|10w_100w\|100w_] [--user-type common\|enterprise\|personal]` | 私有选项:粉丝数区间、用户类型(普通 / 企业 / 个人认证) | User[] | | douyin | `item publish [--poi-name <名称>] [--series <合集 id>] [--hotspot <热点词>] [--no-download]` | 私有选项:地点名称(配合 `--poi`)、加入合集、关联热点、不允许下载 | Item | | douyin | `item collect --folder `、`item uncollect --folder ` | 私有选项:收藏后移进某个收藏夹 / 从某个收藏夹移出(仍保留收藏)。`` 是收藏夹 ID 或名字 | `{id}` | | douyin | `msg send [--file ] [--share ]` | 私有选项:发文件(≤10MB)、分享作品 / 用户名片 / 网页卡片 | Message | | douyin | `live rank [--ranking contribution\|thousand]` | 私有选项:榜单,默认贡献榜,`thousand` 为千票榜 | Rank[] | | douyin | `live like [--count N]` | 私有选项:一次点赞的次数 | `{id}` | | douyin | `comment list --product [--label <标签名或 id>]` | 私有选项:商品评价按标签筛选(好评 / 差评 / 有图等) | Comment[] | | douyin | `notice list [--group all\|fans\|mention\|comment\|like\|danmaku]` | 私有选项:通知分组,不带时为默认分组 | Notice[] | | xhs | `item publish [--poi-name <名称>] [--poi ]` | 私有选项:地点名称。小红书的地点只能按名称搜,搜不到 id,所以 `--poi` 要和 `--poi-name` 一起用(取 id 相同的那个);只给 `--poi-name` 时取名称完全相同的。都找不到时报 `USAGE` 并列出相近的地点 | Item | | weibo | `item publish [--poi-name <名称>]` | 私有选项:地点名称,发成「#名称[地点]#」的地点标签。微博发布没有地点 id,不支持 `--poi` | Item | | douyin | `auth login --method sms --sso` | 私有选项:短信登录改走 `login.douyin.com` 页的 SSO 链 | Account | | jd | `order list` | 订单 | Order[] | | jd | `order list [--range 3m\|this_year\|<年份>]` | 私有选项:订单的时间范围。`3m` 近三个月(默认),`this_year` 今年内,`2025` 这样的四位年份表示那一年 | Order[] | | jd | `cart count` | 购物车数量 | `{count}` | | jd | `coupon list ` | 商品可用优惠券 | Coupon[] | | jd | `item get` / `item search` / `item related` / `coupon list` / `cart count` / `history list` / `user collects` `[--area <地区编码>]` | 私有选项:收货地区,影响价格和库存。取值是京东的地区编码 `省_市_区_镇`,用 `_` 或 `-` 分隔,例如 `1_2800_55812_0`;默认取登录态里的 `ipLoc-djd` cookie,没有时为 `1_2800_55812_0` | Item / Item[] / Coupon[] / `{count}` | | jd | `msg send --order <订单> [--item \| --conversation ]` | 私有选项:按订单咨询客服,会话和消息都带上订单号。`<订单>` 是订单号(`order list` 输出的 id),也接受订单详情页 URL。可以和 `--item`、`--conversation` 之一一起用;单独用时联系京东自营客服 | Message | | x | `item publish [--thread ...]` | 私有选项:发 thread。`--text` 是第一条,每个 `--thread` 是后面的一条,依次回复上一条;`--image` / `--video` / `--quote` 只作用于第一条。返回第一条 | Item | | x | `article publish --text [--title] [--cover]` | 发文章(Premium 长文):`--text` 是 Markdown,依次建草稿、写标题、正文、封面,再发布。不给 `--title` 时取正文第一行的 `# 标题`。独占一行的 `![](图片)` 上传成插图,相对路径按 `--text @file` 的文件所在目录解析(直接给正文时按当前目录) | `{id url}` | | x | `article delete

` | 删文章:草稿或已发布的都可以,已发布的连同文章推文一起删 | `{id}` | | 12306 | `station search ` | 车站搜索(支持站名、拼音前缀、电报码) | Station[] | | 12306 | `ticket search [--date ] [--type ] [--available]` | 直达车次与余票(`--date` 默认今天;`--type` 过滤车型;`--available` 只看有票车次) | Ticket[] | | 12306 | `ticket price --from-no <序号> --to-no <序号> --seat-types <编码> [--date ]` | 票价详情(参数取自 ticket search 输出的 train_no、from.no、to.no、seat_types) | Fare | | 12306 | `route get [--date ]` | 经停站与时刻(`` 为 train_no,``/`` 为出发站与到达站) | Stop[] | | 12306 | `transfer search [--date ] [--via <中转站>] [--limit N]` | 中转方案搜索(同一车次分段有座标为「同车换座」) | Transfer[] | 扩展类型的字段见 6.2。 ### 4.8 参数 目标参数同时接受以下几种形式,由平台实现归一化: - ID; - URL,包括分享短链; - 平台上的自然标识,例如 B 站的 BV 号、X 的用户名; - `me`:只用于用户,表示当前账号。 所有输出对象都带 `id` 和 `url`,可以直接作为下一条命令的参数。xhs 的 `url` 带 `xsec_token`,因此 xhs 优先传 URL。 各 resource 的位置参数: | resource | 形式 | |---|---| | user | `get` / `items` / `follow` / `unfollow `;`likes` / `collects` / `reposts` / `followers` / `following [user]`,省略时为 `me`;`search ` | | item | `get` / `related` / `media` / `download` / `like` / `unlike` / `collect` / `uncollect` / `repost` / `unrepost` / `delete `;`search `;`list`、`categories`、`publish` 不带位置参数 | | product | `get ` | | comment | `list `;`replies` / `delete` / `like` / `unlike `;`add [--reply-to ]` | | feed | `list`、`categories` | | live | `get` / `listen` / `history` / `like` / `rank` / `gifts` / `products` / `media `;`send `;`send --gift [--count N]`;`replays `;`list`、`categories`;`search `;`start` / `stop` | | keyword | `suggest `、`hot` | | notice | `list`、`count` | | msg | `list`;`history` / `read` / `delete `;`revoke `;`send --to \| --conversation \| --item `,三者用一个,只有 `--to` 与 `--item` 可以同时用;可加 `--image` / `--video` | | media | `upload ` | | folder | `list [user]`,省略时为 `me`;`items `;`create `;`update --name `;`delete ` | | series | `list [user]`,省略时为 `me`;`items ` | | history | `list` | | topic / poi | `search ` | - 商品评价用 `comment list `。 - 传商品 URL 时,自动识别为商品。 - 传纯 ID 时要加 `--product`,否则按 item 处理。 - bilibili 的 `comment replies ` 支持视频、专栏和图文动态:`` 接受 BV 号、av 号、aid、视频链接(含分享短链),`cv` / 专栏链接,或动态链接 / ID。纯文字和转发动态的楼中楼尚未验证,不开放。`` 是 `comment list` 返回的根评论 ID。分页读取该根评论下的可见楼中楼,沿用 `--limit`、`--cursor`、`--all`;`comment list --raw` 中的少量 `replies` 只是预览。 - bilibili 的 `comment list` / `replies` 读取图文动态时,先用非上游的动态详情接口取 `basic.comment_type=11` 与 `comment_id_str`(相簿 ID),不能把动态 ID 当作评论区 oid;输出 `Comment.item_id` 仍为动态 ID。同一条命令翻页时复用解析结果。`comment add` / `delete` 仍只支持纯文字和转发动态。 - `msg send --item `:对商品卖家 / 客服发消息(闲鱼、淘宝、京东)。 - `msg send --to --item `:就这件商品给指定用户发消息(闲鱼:卖家主动联系买家)。不支持的平台报 `UNSUPPORTED`。 - taobao 没有按用户查询、按用户发起会话的接口:`user get ` 的 `` 只接受商品链接,返回这件商品的卖家;`msg send` 不支持 `--to`(报 `UNSUPPORTED`)。 ### 4.9 选项 **全局选项**: | 选项 | 说明 | |---|---| | `-a, --account ` | 使用哪个账号(见 5.2) | | `-e, --endpoint web\|app\|pc` | 端,默认 `web` | | `-o, --output json\|jsonl` | 输出格式,默认 `json`(见 6.1) | | `--raw` | 用平台原始对象代替归一化对象 | | `--proxy ` | 本次调用使用的代理 | | `-v, --verbose` | stderr 输出调试日志(已脱敏) | | `-q, --quiet` | 不输出提示和进度,只保留错误 | | `-y, --yes` | 跳过危险操作的确认(见 4.11) | | `-h, --help` | 帮助 | **分页**: | 选项 | 说明 | |---|---| | `--limit N` | 默认只取一页。指定 N 后自动翻页,直到取满 N 条。翻页间隔用平台默认值 | | `--cursor ` | 从上次返回的 `page.cursor` 继续翻。cursor 是不透明字符串。`--limit` 截在一页中间时,`page.cursor` 指回这一页,续翻时跳过已经输出的,不丢数据 | | `--all` | 一直翻到没有更多为止 | **筛选**:各平台在注册表里声明支持哪些筛选、取哪些值。取值只能从下表里选;需要新取值时先改本表。 | 选项 | 标准取值 | |---|---| | `--sort` | `general` `latest` `popular` `views` `comments` `collects` `sales` `price_asc` `price_desc` | | `--type` | `all` `video` `image` `text` `article` `goods` | | `--time` | `all` `day` `week` `month` `half_year` `year` | | `--kind` | `recommend` `hot` `following`(`feed list`,默认 `recommend`) | | `--category ` | 取值来自对应的 `categories` 命令 | 取值是标准值、但该平台不支持时报 `UNSUPPORTED`;不是标准值时报 `USAGE`。 **发布**(`item publish`、`dynamic publish` 等): | 选项 | 说明 | |---|---| | `--title` | 标题 | | `--text ` | 正文。`@file` 表示从文件读取,正文里的相对路径(如 Markdown 插图)按该文件所在目录解析 | | `--image ` | 图片,可重复 | | `--video ` | 视频 | | `--cover ` | 封面 | | `--tag`、`--topic`、`--mention` | 均可重复 | | `--poi ` | 地点 | | `--category ` | 分类 | | `--visibility public\|private\|friends\|fans` | 默认 `public`。`friends` 为好友(互相关注)可见,`fans` 为粉丝可见。平台在注册表里声明支持哪些取值,不声明时为 `public` `private` `friends`;平台不支持时报 `UNSUPPORTED`,不是标准值时报 `USAGE` | | `--schedule ` | 定时发布 | | `--price <金额>` | 商品价格(闲鱼) | | `--quote ` | 引用一条内容(ID 或 URL),发成引用。目前只有 x 支持,其他平台没有这个选项 | 平台在注册表里用 `supports` 声明 `item publish` 支持上表中的哪些选项。没声明支持的选项不出现在帮助里,用了报 `UNSUPPORTED`(`hint` 列出可用的选项),不会被静默忽略。各平台的取舍见 capabilities.md。 本地文件会自动上传,不需要先调 `media upload`。 **下载**: - `--dir <目录>`:默认当前目录。文件名为 `__<序号>.`。 - `--overwrite`:覆盖已有文件,默认跳过已有文件。 **长连接**:`listen` 总是输出 jsonl,断线自动重连。`--duration <秒|10m|1h>` 指定运行时长,Ctrl-C 正常退出,退出码 0。 平台私有选项在注册表里声明,**不能与上面的标准选项重名**。 ### 4.10 全局命令与帮助 全局命令不带平台,名字与平台 id、别名互斥: | 命令 | 输出 | |---|---| | `catbus platforms [platform]` | 能力矩阵:平台 × 端 × resource.action 及其状态,数据来自注册表 | | `catbus doctor` | 逐项检查:Node 版本;原生依赖能否加载(HTTP 库、canvas、onnx);`@cv-cat/catbus-assets-jd`、`@cv-cat/catbus-assets-ocr` 是否就位;`~/.catbus` 的权限;签名 vm 能否运行 | | `catbus auth list` | 所有平台、所有端下的账号 | | `catbus config get\|set\|unset\|list` | 读写 `config.toml`(见 5.5) | | `catbus version`(或 `--version`) | 版本、Node 版本、系统与架构 | 帮助: - 不带参数的 `catbus` 等同于 `catbus --help`。 - 帮助分四层:`catbus --help`、`catbus xhs --help`、`catbus xhs item --help`、`catbus xhs item get --help`。内容由注册表生成,写到 stdout,这是 stdout 上唯一不是 JSON 的输出。 - 命令级帮助列出参数、选项、示例,以及下面两行: ``` 端: web ✓ · app ○ planned · pc ○ planned 登录: 需要 | 可选 ``` - 面向人的文字(帮助、`message`、`hint`、提示)一律用中文。 `doctor` 有检查项不通过时,`ok` 为 false、退出码 1,`data` 里仍然是完整的检查结果。 ### 4.11 危险操作确认 以下操作执行前需要确认: - 所有 `delete`; - `msg revoke`; - bilibili 的 `item coin` / `item triple`; - `live send --gift`。 确认方式: - stdin 和 stderr 都是 TTY 时,在 stderr 上询问 y/N。 - 否则必须带 `-y`,不带时报 `CONFIRM_REQUIRED`,退出码 2。 ### 4.12 不提供的能力 以下上游能力**不暴露为命令**: - 埋点 / 心跳 / 上报; - 弹窗 / 合规 / 钱包类接口; - 系统配置; - 闲鱼的自动回复示例; - B 站笔记; - 关系查询; - xhs 的 share_code; - 抖音的 PK / 连麦 / 评论标签; - TikTok 的 story 和收藏夹移动; - xhs 的蒲公英(达人)和千帆(分销达人):只对开通了的品牌 / 机构账号有用,普通账号访问会被拒,被拒后几分钟内主站的评论等接口还会要求人机验证; - 12306(`12306`)的登录、下单、候补提交、退票改签:购票由用户本人在 12306 APP 完成,catbus 仅提供免登录的只读查询能力。 如果某条命令内部需要其中的能力(例如心跳),由实现内部调用。 ## 5. 登录态 ### 5.1 目录 ``` ~/.catbus/ # 可用 CATBUS_HOME 覆盖 ├── config.toml # 见 5.5 ├── auth/ │ └── / # 规范 id │ └── / # web | app | pc │ ├── .json # 一个账号一个文件 │ ├── guest.json # 游客态:设备 cookie、guest token 等,自动生成 │ └── _current # 当前账号名 └── cache// # 可随时删除:二维码 PNG、短信登录的中间态 ``` - 账号名匹配 `[a-z0-9][a-z0-9_-]{0,31}`。`guest` 是保留名。 - 权限: - 目录 0700,文件 0600; - Windows 依赖用户目录本身的 ACL。 - 所有写入都是原子的:先写临时文件,再 rename。 ### 5.2 身份选择与游客态 身份按以下优先级选择:`-a ` > `_current` > 游客。 - `-a guest` 强制使用游客身份(只在支持游客态的端上有意义)。`auth login` / `logout` / `use` 不接受 `-a guest`(保留名),报 `USAGE`。 - `-a` 指定的账号不存在时,报 `AUTH_REQUIRED`,`hint` 为 `catbus

auth login -a `。 **游客态只在声明了 `guest: true` 的端上存在**(见 7.6)。不支持游客态的端上没有 `guest.json`:`auth status` 等 auth 命令没有账号时,用一份只在内存里的空凭证,不落盘。 - **web 端除 `12306` 外都不支持游客态**:各内容与电商平台的 web 端不登录几乎看不到内容,所以 web 端除 `auth` 命令外全部需要登录,未登录时直接报 `AUTH_REQUIRED` 并提示登录命令。游客态主要留给 app 端。**特例**:12306(`12306`)的车站、余票、票价、经停、中转查询均为公开免登录接口,其 web 端声明 `guest: true`,查询命令为 `auth: optional`,无需登录即可直接使用;且不提供 `auth login`。 - 支持游客态的端上,很多平台匿名访问也需要设备 cookie 或 token(例如 xhs 的 `a1`、抖音的 `ttwid`、B 站的 `buvid`、X 的 guest token)。catbus 自动生成这些数据,缓存到 `guest.json`,过期后重新生成;各平台的生成方式见 upstream-map。 - 登录流程本身仍会先生成这些设备数据(很多平台的登录接口要求先有设备 cookie),这与游客态无关。 每个命令在注册表里标注 `auth: required | optional`;不支持游客态的端上一律是 `required`: | 情况 | 行为 | |---|---| | `required`,当前是游客 | `AUTH_REQUIRED`,退出码 3,`hint: catbus xhs auth login`(非 web 端时带上 `-e`) | | `optional`,当前是游客 | 照常执行(信封里 `account` 为 `"guest"`),同时在 stderr 提示一行(若该端支持登录且非 `-q` / `-a guest`;见下) | | 平台返回登录墙 | `AUTH_REQUIRED`,退出码 3 | | 登录态失效 | `AUTH_EXPIRED`,退出码 3,`hint` 为重新登录的命令 | `optional` 时在 stderr 输出的提示: ``` [catbus] 未登录 xhs (web),本次以游客身份访问,结果可能不完整;很多操作需要登录。登录:catbus xhs auth login ``` - `-q` 或 `-a guest` 时不输出这行提示。 - 若该端未注册 `auth login` 命令(例如 `12306`),则不输出该行提示(避免提示不存在的登录命令)。 - 只提示,不在命令中途弹出交互式登录。 ### 5.3 登录与登出 ```bash catbus

auth login [-a ] [--method qrcode|sms|password|cookie] [--scope ] ``` **账号名**: - 不带 `-a` 时:有当前账号就登录到当前账号,否则登录到 `default`。 - 登录成功后,如果还没有当前账号,就把它设为当前账号。 - 如果该名字下已经存了另一个用户,报 `USAGE`,`hint` 为"用 `-a <新名字>`,或者先 logout",新凭证不保存。 **登录方式**:每个平台在注册表里声明支持哪些方式、默认用哪种,见 capabilities.md。 | 方式 | 选项 | 流程 | |---|---|---| | `qrcode` | — | 二维码画到 stderr,同时把 PNG 存到 `cache/

/`,然后等待扫码 | | `sms` | `--phone`、`--code` | TTY 下先发验证码,再交互输入。非 TTY 下分两步:`--phone` 发验证码并保存中间态(10 分钟有效),再单独用 `--code` 完成登录 | | `password` | `--username`、`--password-stdin` | 密码只从 stdin 读取,TTY 下隐藏输入 | | `cookie` | `--cookie ` | 导入后调一次 `me` 校验。也接受浏览器导出的 cookie JSON 数组;douyin、tiktok 另外接受一个 JSON 对象,把 ticket、证书、私钥等设备数据一并导入(这两个平台的写操作需要它们) | - **验证码**:上游能自动过的就自动过,过不了报 `RISK_CONTROL`(`detail.kind = captcha`),退出码 5。 - **子站点**(创作者中心、直播、IM 等): - 能自动换取的凭证,登录时或第一次用到时自动获取,不需要用户再登录一次(例如 xhs 的创作者中心:用主站登录态换取); - 需要单独登录的,用 `--scope ` 登录,各平台可用的 scope 在注册表里声明; - 命令缺少所需 scope 时,报 `AUTH_REQUIRED`,`hint` 带上 `--scope`。 - **`auth status`**:在线校验,返回 AuthStatus。未登录或已失效时返回 `logged_in: false`,不报错。 - **`auth logout`**:删除本地凭证文件(上游支持服务端登出的,同时调用服务端登出,目前有 B 站和快手),并清除指向它的 `_current`。 - **`auth use `**:设置 `_current`。 - **`auth list`**:列出本平台、本端的账号。 - **自动刷新**:会自动刷新的 token(如 B 站的 refresh)和响应里的 Set-Cookie,都会合并回凭证文件并原子写入。 ### 5.4 凭证文件 ```jsonc { "schema": 1, "platform": "xhs", "endpoint": "web", "account": "default", // 游客文件为 "guest" "user": { "id": "5f...", "name": "...", "url": "..." }, // UserRef;游客为 null "method": "qrcode", // qrcode | sms | password | cookie | guest "created_at": "2026-09-27T12:00:00+08:00", "updated_at": "2026-09-27T12:00:00+08:00", "scopes": { // 子站点分区;单站点平台只有 main "main": { "cookies": [ { "name": "a1", "value": "...", "domain": ".xiaohongshu.com", "path": "/", "expires": null } ], "tokens": {} // ticket / ct0 / access_token / refresh_token 等 }, "creator": { "cookies": [], "tokens": {} } }, "device": {}, // 设备指纹、IM 私钥与证书、localStorage 等设备绑定数据 "extra": {} // 平台私有字段 } ``` - cookie 统一存成带 domain 的列表。只导入了一段 cookie 字符串时,domain 取平台默认域。 - 凭证文件**只由 core 读写**。平台实现从 core 拿凭证,更新后交回 core 落盘。 ### 5.5 配置与代理 `config.toml` 只有这几项: ```toml proxy = "http://127.0.0.1:7890" # 全局代理 timeout = 30 # 单次请求超时(秒) [xhs] proxy = "socks5://..." # 平台代理 ``` - 代理的优先级:`--proxy` > `.proxy` > `proxy`。 - 不读取 `HTTP(S)_PROXY` 环境变量,代理只能显式配置。 - `catbus config set xhs.proxy `,用点号表示平台段。 ## 6. 输出 ### 6.1 信封与格式 stdout 只输出结果。日志、提示、进度、二维码一律输出到 stderr。 信封: ```json { "ok": true, "platform": "xhs", "endpoint": "web", "resource": "item", "action": "get", "account": "default", "data": {}, "page": null, "error": null } ``` | 字段 | 说明 | |---|---| | `account` | 使用的账号名,游客为 `"guest"` | | `page` | 分页命令为 `{ "cursor": "...", "has_more": true }`,其他命令为 `null` | | `error` | 出错时 `ok` 为 false,`data` 为 null,`error` 为 `{ "code", "message", "hint", "detail" }`。`hint` 是建议执行的命令或做法,可以为 null | 全局命令的信封: - `platform`、`endpoint`、`account` 都为 null; - `resource` 是命令名,`action` 是子命令,没有子命令时为 null。 - 例如 `catbus auth list` 的 resource 是 `auth`,action 是 `list`。 `-o` 的两种格式: - **`json`**(默认):stdout 恰好输出一个信封,成功和失败都是。stdout 是 TTY 时缩进输出,否则输出紧凑的一行。 - **`jsonl`**:stdout 每行一个 `data` 条目。列表命令一行一条;非列表命令只有一行。 - 结束时在 stderr 写一行摘要信封,`data` 为 null,带 `page` 和 `error`,用来取 next cursor 或错误信息。 `--raw`:用平台原始对象代替每个归一化对象,信封不变。 ### 6.2 归一化类型 所有平台的同一种对象都输出同一结构。 **基础类型**: | 类型 | 字段 | |---|---| | UserRef | `id name url` | | Price | `amount currency`。`amount` 用货币主单位(元)的十进制数。虚拟货币也用它,`currency` 写平台货币代码 | | Media | `id type url width height duration`。`url` 在刚上传、平台还没给出可访问地址时(例如视频转码前)为 null | **内容与互动**: | 类型 | 字段 | |---|---| | User | `id name handle avatar url bio stats{followers following items likes}` | | Item | `id kind url title text author:UserRef created_at cover media:Media[] stats{views likes comments collects shares} price:Price\|null status\|null` | | Comment | `id item_id parent_id author:UserRef text created_at stats{likes replies}` | | Folder / Series | `id name count url` | | Category | `id name parent_id` | | Keyword | `text heat` | | Topic | `id name url stats{views items}` | | Poi | `id name address` | `Comment.parent_id` 表示直接被回复的评论 ID;楼中楼回复可能指向另一条子回复,不能统一填成根评论 ID。bilibili 的原始 `root` / `parent` 通过 `--raw` 保留。 **直播**: | 类型 | 字段 | |---|---| | Live | `id url title status host:UserRef cover stats{viewers}` | | Event | `type time user:UserRef text gift{name count}\|null` | | Gift | `id name price:Price` | | Rank | `rank user:UserRef score` | **消息与通知**: | 类型 | 字段 | |---|---| | Conversation | `id peer:UserRef unread last_message updated_at` | | Message | `id conversation_id from:UserRef type text media:Media[] created_at` | | Notice | `id type user:UserRef target{id url}\|null text created_at` | | NoticeCount | `total comment mention like follow system` | **账号与本地**: | 类型 | 字段 | |---|---| | AuthStatus | `logged_in user:UserRef\|null method expires_at` | | Account | `platform endpoint account current user:UserRef\|null method updated_at` | | File | `path type url size` | **扩展类型**(见 4.7): | 类型 | 字段 | |---|---| | Subtitle | `lang name url lines[{from to text}]`,`lang` 缺失时为 null | | Danmaku | `id item_id offset text created_at`,`offset` 为视频内的秒数 | | Order | `id status total:Price items:Item[] created_at` | | Coupon | `id title discount:Price threshold:Price\|null start_at end_at` | | Station | `id name code pinyin abbr city url:null` | | Ticket | `id url train_no type from{code name no} to{code name no} depart arrive duration date bookable has_ticket seats remaining prices seat_types` | | Stop | `id station arrive depart stopover in_range url:null` | | Fare | `id train_no date prices url:null` | | Transfer | `id via same_station same_train kind depart arrive duration_minutes wait legs url:null` | | TransferLeg | `id train_no from to depart arrive duration seats remaining prices url:null` | **铁路与出行扩展字段说明**: - `Ticket.url` 为 12306 官方余票查询深链(`https://kyfw.12306.cn/otn/leftTicket/init?...`),可直接在浏览器打开。 - `prices` 中每个席别的值均为 `Price { amount, currency: 'CNY' }`,单位为元。 - `remaining` 为每个席别的余票张数:≤20 时为精确整数,≥21 时为封顶对象 `{ min: 21 }`;无座张数不封顶,为精确整数。 - `Transfer.kind`:同一车次分段有座(`same_train: true`)标为 `同车换座`,不同车次换乘标为 `换乘`。 **枚举值**: | 字段 | 取值 | |---|---| | Item.kind | `video` `image` `text` `article` `goods` | | Item.status | `published` `reviewing` `rejected` `private` `draft`;商品另有 `on_sale` `sold` `off_shelf`。只有自己的内容和商品才有,其余为 null | | Media.type | `image` `video` `audio` | | Live.status | `live` `offline` | | Event.type | `chat` `gift` `like` `enter` `follow` `other` | | Message.type | `text` `image` `video` `card` `other` | | Notice.type | `comment` `mention` `like` `follow` `system` | 规则: - ID 一律是字符串。 - 计数是整数或 null,例如 "1.2万" → 12000。 - 时间用带时区的 ISO 8601。 - 字段总是存在,缺失时为 null 或 `[]`。 - 不追加平台私有字段;需要原始字段时用 `--raw`。 - 新增类型或字段,先改本节。 ### 6.3 各命令的输出 读取类命令按资源返回对应类型。列表类命令返回数组,并带 `page`。 | 命令 | data | |---|---| | `user get` | User | | `user search` / `followers` / `following` | User[] | | `user items` / `likes` / `collects` / `reposts`,`item search` / `related` / `list`,`feed list`,`folder items`,`series items`,`history list`,`live replays` | Item[] | | `item get`、`product get` | Item | | `live products` | Item[](`kind` 为 `goods`) | | `item media`、`live media` | Media[] | | `comment list` / `replies` | Comment[] | | `live get` | Live | | `live list` / `search` | Live[] | | `live listen`(流) | Event | | `live history` | Event[] | | `live gifts` | Gift[] | | `live rank` | Rank[] | | `msg list` | Conversation[] | | `msg history` | Message[] | | `msg listen`(流) | Message | | `notice list` | Notice[] | | `notice count` | NoticeCount | | `*categories` | Category[] | | `keyword suggest` / `hot` | Keyword[] | | `topic search` | Topic[] | | `poi search` | Poi[] | | `folder list` | Folder[] | | `series list` | Series[] | | `auth status` | AuthStatus | | `auth list` | Account[] | | `auth login` / `use` / `logout` | Account | | `station search` | Station[] | | `ticket search` | Ticket[] | | `ticket price` | Fare | | `route get` | Stop[] | | `transfer search` | Transfer[] | 写操作的返回: | 动作 | data | |---|---| | 成对动作、`delete`、`msg read` / `revoke`、`live send` / `like` / `stop` | `{id}`,即目标对象的 id | | `item publish` | Item | | `comment add` | Comment | | `msg send` | Message | | `media upload` | Media | | `item download` | File[] | | `folder create` / `update` | Folder | | `live start` | `{id push{url key}}`,推流地址 | 全局命令的返回: | 命令 | data | |---|---| | `platforms` | Platform[] `{id name aliases endpoints{web app pc} commands[{endpoint resource action auth status upstream note}]}`;带平台参数时返回单个对象 | | `doctor` | `{name ok message}[]` | | `config list` | 对象:只含已设置的项,键用点号展开,例如 `xhs.proxy` | | `config get` | 值 | | `config set` / `unset` | `{key value}` | | `version` | `{version node platform arch}` | ### 6.4 退出码 | 退出码 | code | 含义 | |---|---|---| | 0 | — | 成功 | | 1 | `ERROR` | 未分类错误 | | 2 | `USAGE` / `UNSUPPORTED` / `CONFIRM_REQUIRED` | 参数错误 / 平台或端不支持该命令 / 危险操作未确认 | | 3 | `AUTH_REQUIRED` / `AUTH_EXPIRED` | 未登录或登录态失效 | | 4 | `NOT_IMPLEMENTED` | planned 的端或能力 | | 5 | `RISK_CONTROL` | 验证码或风控,`detail.kind` 为 `captcha` / `rate_limit` / `blocked` | | 6 | `NETWORK` | 网络 / 代理错误 | | 7 | `UPSTREAM` | 平台返回业务错误,原始错误码放在 `detail` 里 | ## 7. 架构 ### 7.1 技术栈 - **语言**:TypeScript,strict,ESM。用 `tsc` 编译到 `dist/`,发布的是编译后的 JS。Node 拒绝对 `node_modules` 里的文件做类型剥离,所以必须有编译这一步。 - **Node 版本**:`engines` 为 `^22.22.2 || ^24.15.0 || >=26.0.0`,跟随 jsdom 30 的要求。 - **测试**:vitest。 - **开发**:用 npm scripts,不引入额外的构建工具。 ### 7.2 打包与分发 | 包 | 内容 | |---|---| | `catbus-cli` | `dist/`(CLI、core、全部平台)和 `static/`(上游签名 JS 等)。bin 为 `catbus` → `dist/cli/main.js` | | `@cv-cat/catbus-assets-jd` | JD 验证码(JCAP)的 onnx 模型,约 81 MB。作为 `catbus-cli` 的普通依赖,精确钉版本 | | `@cv-cat/catbus-assets-ocr` | 验证码 OCR 模型:ddddocr 1.6.1 的检测模型 `common_det.onnx`、识别模型 `common_old.onnx` 和字符集,约 34 MB(B 站极验点选在用)。同样是精确钉版本的普通依赖 | - 拆出 assets-jd / assets-ocr 是因为模型很少变。放在主包里的话,每次发版都要上传 100 多 MB。 - 两个包的源码在 `packages/assets-jd/`、`packages/assets-ocr/`,是 npm workspace,开发时直接链接。 - 模型文件不进 git,由 `npm run assets:jd`(`scripts/fetch-jd-models.mjs`)按 `src/platforms/jd/UPSTREAM` 的 commit 取回并校验 sha256:有 `references/JdApis` 时从本地复制,否则从 GitHub 下载。打包 assets-jd 时会自动执行。 - OCR 模型同样不进 git,由 `npm run assets:ocr`(`scripts/fetch-ocr-models.mjs`)从 PyPI 上固定版本的 ddddocr wheel 里取出,wheel 和每个文件都校验 sha256:本机装过同版本 ddddocr(如 `.golden/

` 虚拟环境)时直接复制,否则下载 wheel(也可用 `CATBUS_DDDDOCR_WHEEL` 指定本地 wheel)。打包 assets-ocr 时会自动执行。ddddocr 是 MIT 许可证,包里附带它的原许可证 `LICENSE-ddddocr`。 - catbus 自己**不做**按系统划分的运行时包。原生依赖(HTTP 库、`@napi-rs/canvas` 等)各自通过 `optionalDependencies` 带上本机的预编译包。 - `npm i` 之后完全离线可用,npmmirror 可以完整镜像。 - **目标系统**: - win32-x64、win32-arm64; - darwin-arm64、darwin-x64; - linux-x64、linux-arm64,glibc 和 musl 都要支持。 - CI 在全部目标系统上跑冒烟测试,musl 用 Alpine。**任何一个目标系统缺预编译包的依赖都不能用。** - 发布: 1. 改 `package.json` 的版本号。模型变了时同时改 `packages/assets-*/package.json` 的版本,并把主包里钉住的依赖改成同一个版本(测试会检查)。 2. 在 `CHANGELOG.md` 写好 `## [<版本>] - <日期>` 一节。 3. 推送 `v<版本>` tag,触发 `.github/workflows/release.yml`:先检查 tag 与版本号一致、CHANGELOG 里有这个版本;跑完整 CI;发布 CI 里测过的同一份 tarball;最后用 `scripts/release-notes.mjs` 从 CHANGELOG 生成 GitHub Release。 - 顺序:先发 assets-jd、assets-ocr,再发 `catbus-cli`。已经在 npm 上的版本跳过,失败后重跑不会重复发布。 - 认证:npm 上给三个包配了 trusted publishing(GitHub Actions,`cv-cat/catbus`,`release.yml`,允许 `npm publish`)时走 OIDC,否则用仓库 secret `NPM_TOKEN`。trusted publishing 配好后两天内要成功发布一次,否则失效。 - 仓库公开时发布带 `--provenance`,私有时不带(npm 只给公开仓库生成 provenance),由 release.yml 按仓库可见性自动判断。 - **Docker 镜像**(可选):`Dockerfile` 走与 npm 发布相同的路径,先打出三个包,再在 `node:<版本>-bookworm-slim` 里全局安装这三个 tarball;`CATBUS_HOME=/data`(卷),工作目录 `/work`;默认以 root 运行(挂进来的目录都能写),Linux 上可以 `--user "$(id -u):$(id -g)"` 配合挂自己的数据目录。`compose.yaml` 提供 `catbus`(一次性命令)和 `live-listen`(长时间监听直播)两个服务。CI 的 `.github/workflows/docker.yml` 在 x64 与 arm64 上构建并跑 `version` / `doctor`,不推送到任何镜像仓库。 - 以后要提供 SDK 时,通过 `package.json` 的 `exports` 暴露,现在不做。 ### 7.3 依赖选型 原则: - 优先纯 JS / WASM。 - 原生依赖必须在全部目标系统上都有预编译包,不能有 install 脚本,也不能需要 node-gyp。 - 一律用最新版,由 `package-lock.json` 锁定。 | 用途 | 选择 | |---|---| | 带浏览器 TLS / HTTP2 指纹的 HTTP(替代 curl_cffi) | wreq-js:JA4 / H2 指纹与真 Chrome 一致,可关闭默认头以精确控制头顺序。impit 指纹不符、没有 WebSocket,不用 | | WebSocket | wreq-js 自带的 `websocket()`,与 HTTP 同指纹、共享 cookie | | protobuf | protobufjs。它的 Reader 也用来做无 schema 解码,替代 blackboxprotobuf | | onnx 推理 | onnxruntime-web(WASM)。太慢再换 onnxruntime-node | | 图像处理 | @techstark/opencv-js | | 图片编解码 | @napi-rs/canvas,不用 sharp | | 验证码 OCR | 移植 ddddocr:模型跑在 onnxruntime-web 上,可参考 ddddocr-node。模型来自 ddddocr 1.6.1 的 wheel,放在 `@cv-cat/catbus-assets-ocr`(见 7.2) | | 签名 JS 的依赖 | jsdom(JD)、crypto-js(XHS)、@napi-rs/canvas(JD、KS) | | 校验 | zod | | 参数解析 | `node:util` 的 `parseArgs`,加上注册表。不用 CLI 框架 | | 配置 | smol-toml | | 终端二维码 | qrcode | | 日志 | 自己写的 stderr logger,负责给凭证打码 | wreq-js 默认会读 `HTTP(S)_PROXY` 环境变量和 Windows 系统代理。为遵守 5.5,`core/http` 在启动时设置 `NO_PROXY=*`,代理只通过 per-request 的 `proxy` 传入。 ### 7.4 签名 JS - 上游的签名 JS **原样复制**到 `static/

/`,进版本控制,不手改。需要适配的地方在 TS 一侧包装。 - **执行方式**:放到 `node:vm` 的 context 里执行。 - context 按脚本懒创建,创建后复用。 - 需要 DOM 的脚本(JD)注入 jsdom。 - `require` 用 `createRequire` 指向 catbus 自己的 `node_modules`,这样 `crypto-js` / `jsdom` / `@napi-rs/canvas` 从主包依赖里解析。 - 原来按命令行脚本写的 JS(读 argv / stdin、往 stdout 打印),由 TS 包装层喂参数、收结果。 - vm 跑不了的脚本(依赖进程级状态等),退回到 `worker_threads`,或者用 `process.execPath` 起子进程。 - 上游 Python 里围绕 JS 的胶水代码,以及纯 Python 实现的签名(如 wbi、mtop sign),都移植成 TS。 ### 7.5 移植与对拍测试 - **移植**:上游 Python 的请求构造、签名、解析逻辑都重写成 TS。行为以上游为准:同样的输入,产出同样的请求(URL、query、header、body、签名)。 - **对拍数据**:用 `scripts/golden/

/gen.py` 生成,框架是 `scripts/golden/catbus_golden.py`。 - 在开发机上用 Python 3.13 + uv 建一个不进 git 的虚拟环境 `.golden/

/`(`uv venv .golden/

--python 3.13`,再装上游的依赖),运行 `references/` 里的上游代码。 - 框架替换了 Python 的 `random` / `secrets` / `uuid` / `os.urandom` / `time`,并截获 curl_cffi 与 requests 的请求(不联网,按用例给的响应回复);上游起的 node 子进程通过 `NODE_OPTIONS` 预加载 `node_determinism.cjs`,固定 `Math.random` 与 `Date`。 - 时区固定为北京时间(`Asia/Shanghai`):框架会设置 `TZ`,TS 侧的测试在 `vitest.config.ts` 里固定同一时区。 - 输出到 `tests/golden/

/.json`。**只能用假凭证。** - **对拍测试**:`tests/

.test.ts` 用 `tests/golden.ts` 的 `replay()` 在同样的随机数与时钟下运行 TS 实现,`expectRequests()` 逐字节比较请求(URL、header 顺序、cookie、body)。请求构造和签名都必须有对拍测试。 - **平台实现的约定**(参考实现是 `src/platforms/bilibili/web/`): - 文件:`client.ts`(会话:设备初始化、签名密钥、带重试的请求、业务码 → 错误)、`api.ts`(一个函数对应一个上游方法,字段与顺序照抄)、`commands.ts`(命令 handler)、`normalize.ts`(原始对象 → 6.2 的类型,挂上原始对象供 `--raw`)、`resolve.ts`(参数归一化)。 - 随机数和当前时间只经 `core/rand.ts`,等待用 `rand.sleep`;与 Python 对应的编码(`urlencode`、`quote`、`json.dumps`)用 `core/py.ts`;需要精确键序的 JSON 用 Map。解析平台响应用 `core/py.ts` 的 `jsonLoads`(与 `json.loads` 一样保留 64 位整数 ID),不直接用 `JSON.parse`。 - HTTP 用 `core/http.ts` 的 `HttpClient`(`toolkit.httpClient(ctx)`),请求头按上游顺序显式给出;cookie 在凭证的 cookie 罐里,Set-Cookie 自动写回,命令结束时由 core 落盘。 - 签名 JS 复制到 `static/

/`,用 `core/vm.ts` 的 `loadScript` / `callScript` 执行。 - 业务错误映射到 6.4 的错误码:登录墙 → `toolkit.authError`,风控 → `RISK_CONTROL`,其余 → `UPSTREAM`(原始错误码放 `detail`)。 - **上游基线**:`src/platforms/

/UPSTREAM` 记录上游仓库地址和 commit。无上游仓库的平台(如 `12306`)记录对接的官方服务端点与接口说明。 - **上游更新时**: 1. 用 `git -C references/ log ..` 查看变更; 2. 同步移植; 3. 重新生成对拍数据; 4. 更新 UPSTREAM。 - **12306 平台离线与在线测试**:`12306` 平台直接对接 12306 官方公开接口,无 Python 参考实现与 golden 对拍数据。离线测试在 `tests/12306.test.ts` 中采用真实响应裁剪的 fixture,配合 `mockSender` / `fakeResponse` 覆盖信封、退出码、字段解析与降级容错;在线测试在 `tests/e2e/12306.e2e.ts` 中覆盖免登录查询全链路。 - **在线测试**:`npm run test:e2e`,用单独的配置 `tests/e2e/vitest.config.ts`,不在 `npm test` 和 CI 里。使用开发者本机 `~/.catbus` 里的登录态,没登录的平台整组跳过;`npm run test:e2e -- -t ` 只跑一个平台(`-t` 按名字前缀匹配,x 要写成 `-t '^x '`,否则会连 xhs、xianyu 一起跑)。 - 只跑只读命令:从注册表枚举每个平台已实现的读取类命令,前面命令返回的对象(item、用户、评论、直播间、会话、收藏夹……)的 `url` 或 `id` 作为后面命令的参数,逐条校验信封和 6.2 的字段结构。写操作、下载、长连接不跑。 - 命令之间默认间隔 1.5 秒,小红书 4 秒,避免触发风控。以下记为跳过:子站点没登录(`AUTH_REQUIRED`)、平台风控(`RISK_CONTROL`)、默认参数落在规划中的取值上(`NOT_IMPLEMENTED`)、注册表 note 写明的限制(有 note 的命令报 `UNSUPPORTED`)。 - 每条命令的信封(列表只留前 3 条)写到 `.e2e/.json`,不进版本控制,用来检查归一化的字段是否取到了值。 - **CI**(`.github/workflows/ci.yml`):只跑离线测试,再加上全部目标系统的冒烟测试(`scripts/smoke.mjs`),冒烟测试包括: - 安装包; - 运行 `version`、`doctor`、`platforms`; - 跑一次签名 vm; - 加载原生依赖。 ### 7.6 能力注册表 每个平台的 `src/platforms/

/index.ts` 导出平台声明: ```ts export default definePlatform({ id: 'xhs', name: '小红书', aliases: ['xiaohongshu', 'rednote'], item: '笔记', endpoints: { web: { login: { methods: ['qrcode', 'sms', 'cookie'], default: 'qrcode', scopes: ['creator'] }, commands: { 'user get': 'full', 'item get': { upstream: 'full', handler: () => import('./web/item.js').then((m) => m.get) }, 'feed list': { upstream: 'partial', options: { kind: filter.kind('recommend', 'following') } }, 'order list': { upstream: 'full', summary: '订单列表', args: [], output: 'Order[]', auth: 'required', paged: true }, // 扩展命令(示意):字段写全 }, }, app: 'planned', pc: 'planned', }, }) ``` - 第 4.5 节词表里的命令,默认的 summary、args、选项、auth、output 都来自 `src/core/vocab.ts`(summary 里的 `{item}` 替换成平台的 item 叫法)。平台只写与默认不同的部分。 - 命令的值可以是 `'full' | 'partial' | 'none'` 的简写,也可以是对象。 - 矩阵里 — 的命令不写。 - `auth list` / `use` / `logout` 由 core 自动注册。 - 端的字段:`login`(登录方式与子站点)、`logout?`(服务端登出)、`guest?`(是否支持游客态,默认 false)、`commands`。`guest` 为 false 时,除 `auth` 外的命令一律按 `auth: required` 处理,声明 `optional` 会在启动时报错。web 端都不设 `guest`。 命令的字段: | 字段 | 说明 | |---|---| | `summary` | 一句话说明 | | `args` | 位置参数 | | `options` | 私有选项和筛选取值,用 zod 声明 | | `supports?` | 词表给这个命令的标准选项里,平台支持哪些(如 `item publish` 的发布选项)。不写时全部支持;没列出的不出现在帮助里,用了报 `UNSUPPORTED` | | `auth` | `required` / `optional`;只在 `guest: true` 的端上可以是 `optional` | | `confirm?` | 是否需要危险操作确认 | | `stream?` | 是否长连接 | | `paged?` | 是否分页 | | `output` | 输出类型名 | | `upstream` | 上游支持程度:`full` / `partial` / `none`,即矩阵里的 ✓ / ◐ / ○ | | `note?` | 能力说明,例如"只支持图文" | | `handler?` | 动态 import,懒加载。有 handler 的命令 status 为 `implemented`,否则为 `planned` | 注册表是唯一的事实来源,以下都从它生成: - argv 解析、`--help`; - `catbus platforms`; - NOT_IMPLEMENTED / UNSUPPORTED 的判定; - `docs/capabilities.md`:用 `npm run gen:capabilities` 生成,不手改,测试会检查它是否最新; - 将来的 MCP 工具,工具名为 `__`。 执行一条命令时只加载这一个平台的这一个 handler,不会连带加载别的平台的依赖(如 JD 的 onnx)。 ### 7.7 目录结构 ``` All-In-One/ ├── AGENTS.md CLAUDE.md→AGENTS.md ├── README.md(另有 en、zh-TW、ja、ko 译本) CHANGELOG.md llms.txt ├── package.json tsconfig.json tsconfig.build.json # npm 主包 catbus-cli ├── Dockerfile compose.yaml Makefile .dockerignore ├── .github/ # 仓库治理:CONTRIBUTING、TESTING、SECURITY、PRIVACY、CODE_OF_CONDUCT ├── assets/ # logo、banner、README 用的图 ├── docs/ │ ├── guide/ platforms/ # 用户指南、各平台说明 │ ├── examples/ # 组合 catbus 的示例脚本 │ ├── capabilities.md # 能力矩阵(由注册表生成,不手改) │ ├── upstream-map.md # 上游移植对照 │ └── trouble.md roadmap.md # 内部文档(真机验证记录、路线图):只在本地,不进版本控制 ├── .claude/skills/catbus/ # 给 AI Agent 的技能:SKILL.md 总纲,platforms/、workflows/ 按需读;与注册表的一致性由 tests/skill.test.ts 检查 ├── references/ # 上游仓库:只读,不进版本控制 ├── src/ │ ├── cli/ # 入口 main.ts、argv 解析、帮助、信封输出、确认 │ ├── core/ # registry、auth store、config、errors、http、schemas、signing(vm)、log │ ├── platforms/_shared/ # 多个平台共用、但不属于 core 的实现,例如 impaas.ts(闲鱼、淘宝的钉钉 IMPaaS 私信长连) │ └── platforms/

/ │ ├── index.ts # 平台声明与命令注册 │ ├── UPSTREAM # 移植所基于的上游仓库与 commit │ └── web/ # web 端实现;app/、pc/ 等真正实现时再建 ├── static/

/ # 上游签名 JS、proto 等,原样复制 ├── packages/assets-jd/ # @cv-cat/catbus-assets-jd ├── packages/assets-ocr/ # @cv-cat/catbus-assets-ocr ├── scripts/golden/

/ # 调用上游 Python 生成对拍数据 └── tests/ # vitest;golden/

/ 放对拍数据 ``` `.gitignore`:`references/`、`node_modules/`、`dist/`、`packages/assets-jd/`、`packages/assets-ocr/` 下的模型文件,以及内部文档 `docs/trouble.md`、`docs/roadmap.md`。 ### 7.8 provider 协议(规划中) 给将来的非 JS 上游(Go / Java……)用:任意语言的可执行文件,以子进程形式运行,通过 stdin / stdout 传 JSON。 - **清单**:`provider.json` 声明 `id`、支持的 platform / endpoint、能力列表(`resource.action`,同样遵守第 4 节词表)和启动命令。 - **安装位置**:`~/.catbus/providers//`,或者 npm 包 `@cv-cat/catbus-provider-`。 - **请求**:core 往 stdin 写一个 JSON:`{v, platform, endpoint, resource, action, args, options, account, credential}`。 - **响应**:provider 往 stdout 逐行写 JSON,消息类型有: - `result`:最终结果; - `item`:流式条目; - `credential_update`:刷新后的凭证; - `error`:错误,code 取 6.4。 - **日志**:写到 stderr,core 原样转发。 - **凭证**:provider 不直接读写 `~/.catbus/`。凭证由 core 放在请求里给它,更新后通过 `credential_update` 交回,由 core 落盘。 - **注册表**:provider 的能力合并进注册表,`platforms`、`--help`、NOT_IMPLEMENTED 对它一视同仁。 ## 8. 开发约定 - **语言**:与用户沟通用中文;面向用户的文字用中文;代码标识符用英文。 - **工具链**: - 用户侧只需要 Node(版本见 7.1)。 - 开发侧需要 Node、Python 3.13 和 `uv`。Python 只用来生成对拍数据。 - **依赖**:不引入需要现场编译的依赖(node-gyp、install 脚本)。新增原生依赖前,先确认全部目标系统都有预编译包。 - **凭证**:严禁把真实 cookie / token / 私钥写进代码、测试 fixture、日志或提交记录。凭证只在 `~/.catbus/` 里。logger 对 cookie、token、Authorization 等字段打码。 - **stdout 纯净**:调试输出一律走 stderr / logger。 - **新增命令**: 1. 先查第 4 节词表,不够用就先改本文件; 2. 在注册表里声明; 3. 再实现。 - **移植**: - 移植以上游行为为准,请求构造和签名必须有对拍测试。 - 发现上游 bug 时,修到对应的上游仓库,再按新 commit 重新同步。 - **`references/`**:只读。可以运行,可以在 gitignore 的位置装依赖,但不能修改受版本控制的文件。