Files

959 lines
59 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.
# 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 <id> -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 节的 10 个平台的 web 端;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` |
参考资料:
- 每个仓库的鉴权、登录、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/<p>/UPSTREAM`。
## 4. 命令规范
### 4.1 语法
```
catbus <platform> <resource> <action> [参数...] [选项...]
```
- 选项可以出现在任意位置。`--` 之后的内容一律按参数处理,用于以 `-` 开头的文本。
- 端用选项 `-e/--endpoint` 指定,默认 `web`(见 4.3)。
示例:
```bash
catbus xhs item get "https://www.xiaohongshu.com/explore/<id>?xsec_token=..."
catbus xhs user items <user> --all -o jsonl
catbus bilibili item get BV1xx411c7mD
catbus douyin comment list <item> --limit 100
catbus x user get elonmusk
catbus jd item search 机械键盘 --sort sales
catbus taobao msg send "在吗" --item <商品 url>
catbus bilibili live listen <room> --duration 10m
catbus xhs auth login
catbus kuaishou auth status -a work
catbus xianyu item get <id> -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` |
目录名、配置键、输出里的 `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 <user>`:回放列表。
- `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 <folder> <item>` | 把视频加入收藏夹(一次一个视频)。TikTok 只能把已收藏的视频加进收藏夹 | `{id}`(视频 id) |
| tiktok | `folder create <name> [--visibility public\|private]` | 收藏夹公开 / 私密:用 4.9 的标准选项 `--visibility`,只取 `public`、`private`;默认 `private`(与上游一致) | Folder |
| tiktok | `folder update <folder> [--name <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 <item> [--count 1\|2] [--like]` | 投币;`--like` 同时点赞 | `{id}` |
| bilibili | `item triple <item>` | 一键三连 | `{id}` |
| bilibili | `item subtitles <item>` | 字幕 | Subtitle[] |
| bilibili | `danmaku list <item>` | 视频弹幕 | Danmaku[] |
| bilibili | `danmaku send <item> <text> --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>` | 删动态。`<id>` 为动态 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 <id>` | 专栏草稿 | Item(`kind` 为 `article`,`status` 为 `draft`) |
| bilibili | `draft delete <id>` | 删专栏草稿 | `{id}` |
| bilibili | `user items <user> [--keyword <词>]` | 私有选项:只看投稿里匹配关键词的 | Item[] |
| bilibili | `item collect` / `uncollect <item> [--folder <收藏夹 id>]` | 私有选项:收藏进 / 移出指定收藏夹,多个用逗号分隔。不给时收藏进第一个收藏夹,取消时从所有收藏夹移出 | `{id}` |
| bilibili | `item publish [--source <来源>] [--dynamic <文案>] [--allow-reprint]` | 私有选项:`--source` 表示转载并给出来源,不给为自制;`--dynamic` 为同步到动态的文案;默认禁止转载,`--allow-reprint` 允许 | Item |
| bilibili | `comment add <item> <text> --reply-to <comment> [--root <comment>]` | 私有选项:回复楼中楼时 `--root` 给根评论、`--reply-to` 给被回复的那条;不给 `--root` 时 `--reply-to` 就是根评论 | Comment |
| bilibili | `live send <room> <text> [--color] [--font-size] [--position] [--reply-user <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 <kw> [--length all\|short\|medium\|long] [--range all\|seen\|unseen\|following]` | 私有选项:视频时长(不限 / 1 分钟内 / 1–5 分钟 / 5 分钟以上)、搜索范围(不限 / 看过 / 没看过 / 关注的人)。`--type video` 走视频频道搜索,其余走综合频道(筛选值随 `filter_selected` 发出);`--type image` 只要图文 | Item[] |
| douyin | `user search <kw> [--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 <item> --folder <folder>`、`item uncollect <item> --folder <folder>` | 私有选项:收藏后移进某个收藏夹 / 从某个收藏夹移出(仍保留收藏)。`<folder>` 是收藏夹 ID 或名字 | `{id}` |
| douyin | `msg send [--file <path>] [--share <item\|user\|url>]` | 私有选项:发文件(≤10MB)、分享作品 / 用户名片 / 网页卡片 | Message |
| douyin | `live rank <room> [--ranking contribution\|thousand]` | 私有选项:榜单,默认贡献榜,`thousand` 为千票榜 | Rank[] |
| douyin | `live like <room> [--count N]` | 私有选项:一次点赞的次数 | `{id}` |
| douyin | `comment list <product> --product [--label <标签名或 id>]` | 私有选项:商品评价按标签筛选(好评 / 差评 / 有图等) | Comment[] |
| douyin | `notice list [--group all\|fans\|mention\|comment\|like\|danmaku]` | 私有选项:通知分组,不带时为默认分组 | Notice[] |
| xhs | `item publish [--poi-name <名称>] [--poi <id>]` | 私有选项:地点名称。小红书的地点只能按名称搜,搜不到 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 <item>` | 商品可用优惠券 | 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 <text> --order <订单> [--item <item> \| --conversation <venderId>]` | 私有选项:按订单咨询客服,会话和消息都带上订单号。`<订单>` 是订单号(`order list` 输出的 id),也接受订单详情页 URL。可以和 `--item`、`--conversation` 之一一起用;单独用时联系京东自营客服 | Message |
| x | `item publish [--thread <text> ...]` | 私有选项:发 thread。`--text` 是第一条,每个 `--thread` 是后面的一条,依次回复上一条;`--image` / `--video` / `--quote` 只作用于第一条。返回第一条 | Item |
| x | `article publish --text [--title] [--cover]` | 发文章(Premium 长文):`--text` 是 Markdown,依次建草稿、写标题、正文、封面,再发布。不给 `--title` 时取正文第一行的 `# 标题`。独占一行的 `![](图片)` 上传成插图,相对路径按 `--text @file` 的文件所在目录解析(直接给正文时按当前目录) | `{id url}` |
| x | `article delete <article>` | 删文章:草稿或已发布的都可以,已发布的连同文章推文一起删 | `{id}` |
扩展类型的字段见 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 <user>`;`likes` / `collects` / `reposts` / `followers` / `following [user]`,省略时为 `me`;`search <kw>` |
| item | `get` / `related` / `media` / `download` / `like` / `unlike` / `collect` / `uncollect` / `repost` / `unrepost` / `delete <item>`;`search <kw>`;`list`、`categories`、`publish` 不带位置参数 |
| product | `get <product>` |
| comment | `list <item>`;`replies` / `delete` / `like` / `unlike <item> <comment>`;`add <item> <text> [--reply-to <comment>]` |
| feed | `list`、`categories` |
| live | `get` / `listen` / `history` / `like` / `rank` / `gifts` / `products` / `media <room>`;`send <room> <text>`;`send <room> --gift <gift> [--count N]`;`replays <user>`;`list`、`categories`;`search <kw>`;`start` / `stop` |
| keyword | `suggest <prefix>`、`hot` |
| notice | `list`、`count` |
| msg | `list`;`history` / `read` / `delete <conversation>`;`revoke <conversation> <message>`;`send <text> --to <user> \| --conversation <id> \| --item <item>`,三者用一个,只有 `--to` 与 `--item` 可以同时用;可加 `--image` / `--video` |
| media | `upload <file>` |
| folder | `list [user]`,省略时为 `me`;`items <folder>`;`create <name>`;`update <folder> --name <name>`;`delete <folder>` |
| series | `list [user]`,省略时为 `me`;`items <series>` |
| history | `list` |
| topic / poi | `search <kw>` |
- 商品评价用 `comment list <product>`。
- 传商品 URL 时,自动识别为商品。
- 传纯 ID 时要加 `--product`,否则按 item 处理。
- `msg send --item <item>`:对商品卖家 / 客服发消息(闲鱼、淘宝、京东)。
- `msg send --to <user> --item <item>`:就这件商品给指定用户发消息(闲鱼:卖家主动联系买家)。不支持的平台报 `UNSUPPORTED`。
- taobao 没有按用户查询、按用户发起会话的接口:`user get <user>` 的 `<user>` 只接受商品链接,返回这件商品的卖家;`msg send` 不支持 `--to`(报 `UNSUPPORTED`)。
### 4.9 选项
**全局选项**:
| 选项 | 说明 |
|---|---|
| `-a, --account <name>` | 使用哪个账号(见 5.2) |
| `-e, --endpoint web\|app\|pc` | 端,默认 `web` |
| `-o, --output json\|jsonl` | 输出格式,默认 `json`(见 6.1) |
| `--raw` | 用平台原始对象代替归一化对象 |
| `--proxy <url>` | 本次调用使用的代理 |
| `-v, --verbose` | stderr 输出调试日志(已脱敏) |
| `-q, --quiet` | 不输出提示和进度,只保留错误 |
| `-y, --yes` | 跳过危险操作的确认(见 4.11) |
| `-h, --help` | 帮助 |
**分页**:
| 选项 | 说明 |
|---|---|
| `--limit N` | 默认只取一页。指定 N 后自动翻页,直到取满 N 条。翻页间隔用平台默认值 |
| `--cursor <c>` | 从上次返回的 `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 <id>` | 取值来自对应的 `categories` 命令 |
取值是标准值、但该平台不支持时报 `UNSUPPORTED`;不是标准值时报 `USAGE`。
**发布**(`item publish`、`dynamic publish` 等):
| 选项 | 说明 |
|---|---|
| `--title` | 标题 |
| `--text <str\|@file>` | 正文。`@file` 表示从文件读取,正文里的相对路径(如 Markdown 插图)按该文件所在目录解析 |
| `--image <path\|url>` | 图片,可重复 |
| `--video <path\|url>` | 视频 |
| `--cover <path\|url>` | 封面 |
| `--tag`、`--topic`、`--mention` | 均可重复 |
| `--poi <id>` | 地点 |
| `--category <id>` | 分类 |
| `--visibility public\|private\|friends\|fans` | 默认 `public`。`friends` 为好友(互相关注)可见,`fans` 为粉丝可见。平台在注册表里声明支持哪些取值,不声明时为 `public` `private` `friends`;平台不支持时报 `UNSUPPORTED`,不是标准值时报 `USAGE` |
| `--schedule <ISO 时间>` | 定时发布 |
| `--price <金额>` | 商品价格(闲鱼) |
| `--quote <item>` | 引用一条内容(ID 或 URL),发成引用。目前只有 x 支持,其他平台没有这个选项 |
平台在注册表里用 `supports` 声明 `item publish` 支持上表中的哪些选项。没声明支持的选项不出现在帮助里,用了报 `UNSUPPORTED`(`hint` 列出可用的选项),不会被静默忽略。各平台的取舍见 capabilities.md。
本地文件会自动上传,不需要先调 `media upload`。
**下载**:
- `--dir <目录>`:默认当前目录。文件名为 `<platform>_<id>_<序号>.<ext>`。
- `--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 的蒲公英(达人)和千帆(分销达人):只对开通了的品牌 / 机构账号有用,普通账号访问会被拒,被拒后几分钟内主站的评论等接口还会要求人机验证。
如果某条命令内部需要其中的能力(例如心跳),由实现内部调用。
## 5. 登录态
### 5.1 目录
```
~/.catbus/ # 可用 CATBUS_HOME 覆盖
├── config.toml # 见 5.5
├── auth/
│ └── <platform>/ # 规范 id
│ └── <endpoint>/ # web | app | pc
│ ├── <account>.json # 一个账号一个文件
│ ├── guest.json # 游客态:设备 cookie、guest token 等,自动生成
│ └── _current # 当前账号名
└── cache/<platform>/ # 可随时删除:二维码 PNG、短信登录的中间态
```
- 账号名匹配 `[a-z0-9][a-z0-9_-]{0,31}`。`guest` 是保留名。
- 权限:
- 目录 0700,文件 0600;
- Windows 依赖用户目录本身的 ACL。
- 所有写入都是原子的:先写临时文件,再 rename。
### 5.2 身份选择与游客态
身份按以下优先级选择:`-a <name>` > `_current` > 游客。
- `-a guest` 强制使用游客身份(只在支持游客态的端上有意义)。`auth login` / `logout` / `use` 不接受 `-a guest`(保留名),报 `USAGE`。
- `-a` 指定的账号不存在时,报 `AUTH_REQUIRED`,`hint` 为 `catbus <p> auth login -a <name>`。
**游客态只在声明了 `guest: true` 的端上存在**(见 7.6)。不支持游客态的端上没有 `guest.json`:`auth status` 等 auth 命令没有账号时,用一份只在内存里的空凭证,不落盘。
- **web 端都不支持游客态**:各平台的 web 端不登录几乎看不到内容,所以 web 端除 `auth` 命令外全部需要登录,未登录时直接报 `AUTH_REQUIRED` 并提示登录命令。游客态主要留给 app 端。
- 支持游客态的端上,很多平台匿名访问也需要设备 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 提示一行(见下) |
| 平台返回登录墙 | `AUTH_REQUIRED`,退出码 3 |
| 登录态失效 | `AUTH_EXPIRED`,退出码 3,`hint` 为重新登录的命令 |
`optional` 时在 stderr 输出的提示:
```
[catbus] 未登录 xhs (web),本次以游客身份访问,结果可能不完整;很多操作需要登录。登录:catbus xhs auth login
```
- `-q` 或 `-a guest` 时不输出这行提示。
- 只提示,不在命令中途弹出交互式登录。
### 5.3 登录与登出
```bash
catbus <p> auth login [-a <name>] [--method qrcode|sms|password|cookie] [--scope <scope>]
```
**账号名**:
- 不带 `-a` 时:有当前账号就登录到当前账号,否则登录到 `default`。
- 登录成功后,如果还没有当前账号,就把它设为当前账号。
- 如果该名字下已经存了另一个用户,报 `USAGE`,`hint` 为"用 `-a <新名字>`,或者先 logout",新凭证不保存。
**登录方式**:每个平台在注册表里声明支持哪些方式、默认用哪种,见 capabilities.md。
| 方式 | 选项 | 流程 |
|---|---|---|
| `qrcode` | — | 二维码画到 stderr,同时把 PNG 存到 `cache/<p>/`,然后等待扫码 |
| `sms` | `--phone`、`--code` | TTY 下先发验证码,再交互输入。非 TTY 下分两步:`--phone` 发验证码并保存中间态(10 分钟有效),再单独用 `--code` 完成登录 |
| `password` | `--username`、`--password-stdin` | 密码只从 stdin 读取,TTY 下隐藏输入 |
| `cookie` | `--cookie <str\|@file\|->` | 导入后调一次 `me` 校验。也接受浏览器导出的 cookie JSON 数组;douyin、tiktok 另外接受一个 JSON 对象,把 ticket、证书、私钥等设备数据一并导入(这两个平台的写操作需要它们) |
- **验证码**:上游能自动过的就自动过,过不了报 `RISK_CONTROL`(`detail.kind = captcha`),退出码 5。
- **子站点**(创作者中心、直播、IM 等):
- 能自动换取的凭证,登录时或第一次用到时自动获取,不需要用户再登录一次(例如 xhs 的创作者中心:用主站登录态换取);
- 需要单独登录的,用 `--scope <name>` 登录,各平台可用的 scope 在注册表里声明;
- 命令缺少所需 scope 时,报 `AUTH_REQUIRED`,`hint` 带上 `--scope`。
- **`auth status`**:在线校验,返回 AuthStatus。未登录或已失效时返回 `logged_in: false`,不报错。
- **`auth logout`**:删除本地凭证文件(上游支持服务端登出的,同时调用服务端登出,目前有 B 站和快手),并清除指向它的 `_current`。
- **`auth use <account>`**:设置 `_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` > `<platform>.proxy` > `proxy`。
- 不读取 `HTTP(S)_PROXY` 环境变量,代理只能显式配置。
- `catbus config set xhs.proxy <url>`,用点号表示平台段。
## 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` |
**直播**:
| 类型 | 字段 |
|---|---|
| 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` |
**枚举值**:
| 字段 | 取值 |
|---|---|
| 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 |
写操作的返回:
| 动作 | 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/<p>` 虚拟环境)时直接复制,否则下载 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/<p>/`,进版本控制,不手改。需要适配的地方在 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/<p>/gen.py` 生成,框架是 `scripts/golden/catbus_golden.py`。
- 在开发机上用 Python 3.13 + uv 建一个不进 git 的虚拟环境 `.golden/<p>/`(`uv venv .golden/<p> --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/<p>/<case>.json`。**只能用假凭证。**
- **对拍测试**:`tests/<p>.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/<p>/`,用 `core/vm.ts` 的 `loadScript` / `callScript` 执行。
- 业务错误映射到 6.4 的错误码:登录墙 → `toolkit.authError`,风控 → `RISK_CONTROL`,其余 → `UPSTREAM`(原始错误码放 `detail`)。
- **上游基线**:`src/platforms/<p>/UPSTREAM` 记录上游仓库地址和 commit。
- **上游更新时**:
1. 用 `git -C references/<repo> log <commit>..` 查看变更;
2. 同步移植;
3. 重新生成对拍数据;
4. 更新 UPSTREAM。
- **在线测试**:`npm run test:e2e`,用单独的配置 `tests/e2e/vitest.config.ts`,不在 `npm test` 和 CI 里。使用开发者本机 `~/.catbus` 里的登录态,没登录的平台整组跳过;`npm run test:e2e -- -t <platform>` 只跑一个平台(`-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/<platform>.json`,不进版本控制,用来检查归一化的字段是否取到了值。
- **CI**(`.github/workflows/ci.yml`):只跑离线测试,再加上全部目标系统的冒烟测试(`scripts/smoke.mjs`),冒烟测试包括:
- 安装包;
- 运行 `version`、`doctor`、`platforms`;
- 跑一次签名 vm;
- 加载原生依赖。
### 7.6 能力注册表
每个平台的 `src/platforms/<p>/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 工具,工具名为 `<platform>_<resource>_<action>`。
执行一条命令时只加载这一个平台的这一个 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/<p>/
│ ├── index.ts # 平台声明与命令注册
│ ├── UPSTREAM # 移植所基于的上游仓库与 commit
│ └── web/ # web 端实现;app/、pc/ 等真正实现时再建
├── static/<p>/ # 上游签名 JS、proto 等,原样复制
├── packages/assets-jd/ # @cv-cat/catbus-assets-jd
├── packages/assets-ocr/ # @cv-cat/catbus-assets-ocr
├── scripts/golden/<p>/ # 调用上游 Python 生成对拍数据
└── tests/ # vitest;golden/<p>/ 放对拍数据
```
`.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/<name>/`,或者 npm 包 `@cv-cat/catbus-provider-<name>`。
- **请求**: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 的位置装依赖,但不能修改受版本控制的文件。