aiconductor-cli 1.5.0 → 1.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +17 -10
- package/README.md +71 -0
- package/api_docs/xhs_user_info_v2.md +5 -1
- package/api_docs/xhs_user_notes_v3.md +2 -0
- package/bin/aiconductor-cli.js +56 -1
- package/package.json +1 -1
- package/src/api.js +244 -1
- package/src/api.test.js +297 -1
- package/src/commands/xhs.js +345 -4
- package/src/lib/cli-utils.js +89 -1
- package/.agents/plans/2026-10-28T11-20-16-943Z-plan.md +0 -161
package/AGENTS.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
## 项目概述
|
|
4
4
|
|
|
5
|
-
`aiconductor-cli` 是 AIConductor 平台 API 的命令行封装:Node.js >= 18、CommonJS、唯一运行时依赖 `commander`,发布到 npm 供 agent 与终端用户直接调用。七组命令覆盖网页发布、豆包搜索、热点简报、抖音数据、小红书数据、公众号数据;`.
|
|
5
|
+
`aiconductor-cli` 是 AIConductor 平台 API 的命令行封装:Node.js >= 18、CommonJS、唯一运行时依赖 `commander`,发布到 npm 供 agent 与终端用户直接调用。七组命令覆盖网页发布、豆包搜索、热点简报、抖音数据、小红书数据、公众号数据;`.agents/skills/` 存放面向 agent 的工作流(pi 在项目受信任时会自动发现项目级 `.agents/skills/`),指导如何调用这些命令,其中对外发布的技能(`xhs-note-publish`、`douyin-script-extract`)还要满足 WorkBuddy 上架字段,见 `references/workbuddy技能规范.md`。`README.md` 是对外命令契约,本文档是开发与维护契约。
|
|
6
6
|
|
|
7
7
|
## 项目结构与模块组织
|
|
8
8
|
|
|
@@ -21,7 +21,7 @@ src/commands/
|
|
|
21
21
|
src/api.test.js 全部自动化测试(api 映射 + 纯格式化/聚合函数)
|
|
22
22
|
api_docs/<endpoint>.md 每个平台接口的请求/响应/计费契约(`aiconductor-platform` 对应文件的整文件副本)
|
|
23
23
|
references/ 平台侧规范(workbuddy技能规范.md),不参与 CLI 运行;已 gitignore + npmignore,只在本机
|
|
24
|
-
.
|
|
24
|
+
.agents/skills/<name>/ SKILL.md(路由与执行)+ references/(按条件加载)+ 可选 scripts/;仓库里唯一入库的 .agents 子目录
|
|
25
25
|
```
|
|
26
26
|
|
|
27
27
|
推导规则:命令组名与文件名**不一一对应**(`webpage` → `md_publish.js`,`search` → `web_search.js`),找实现先看 `bin/aiconductor-cli.js` 的注册处。命令文件只做四件事:解析选项、调用 `src/api.js`、格式化输出、`process.exit`;共享逻辑进 `src/lib/cli-utils.js`,HTTP 与鉴权细节不允许出现在命令文件里。
|
|
@@ -43,7 +43,7 @@ references/ 平台侧规范(workbuddy技能规范.md),不参
|
|
|
43
43
|
| 新增/修改接口契约 | platform 侧插件与 `api_docs`,再覆盖到 `aiconductor-cli/api_docs/` |
|
|
44
44
|
| CLI 接入 | `src/api.js`(端点常量 + `httpPost` + `stripBilling`)→ `src/commands/<组>.js` → `bin/aiconductor-cli.js` 注册 |
|
|
45
45
|
| 测试 | `src/api.test.js`,请求字段抄 `api_docs`,不抄实现 |
|
|
46
|
-
| 对外文档与技能 | `README.md`、`.
|
|
46
|
+
| 对外文档与技能 | `README.md`、`.agents/skills/*/SKILL.md` |
|
|
47
47
|
|
|
48
48
|
### 鉴权与计费的两侧分工
|
|
49
49
|
|
|
@@ -68,10 +68,10 @@ platform 侧还有、CLI 未接 10 个:`wx_rss_reader`、`flow_wx_history_arti
|
|
|
68
68
|
| 命令 | 用途 |
|
|
69
69
|
|---|---|
|
|
70
70
|
| `npm install` | 安装唯一依赖 `commander`(有 lock) |
|
|
71
|
-
| `npm test` | `node --test`,
|
|
71
|
+
| `npm test` | `node --test`,40 个用例,离线打桩,不需要 API Key |
|
|
72
72
|
| `node bin/aiconductor-cli.js --help` | 不安装也能本地运行,核对命令面 |
|
|
73
73
|
| `node bin/aiconductor-cli.js <组> <子命令> --help` | 核对某个命令的参数与默认值;CLI 契约以它为准 |
|
|
74
|
-
| `npm pack --dry-run` | 确认发布内容:含 `src/**`(含测试)、`api_docs/`、`AGENTS.md`,不含 `.
|
|
74
|
+
| `npm pack --dry-run` | 确认发布内容:含 `src/**`(含测试)、`api_docs/`、`AGENTS.md`,不含 `.agents/` 与 `references/`(平台侧规范只在本机,已从 `.npmignore`/`.gitignore` 排掉) |
|
|
75
75
|
|
|
76
76
|
无构建步骤、无 lint/formatter、无 CI。
|
|
77
77
|
|
|
@@ -83,7 +83,7 @@ platform 侧还有、CLI 未接 10 个:`wx_rss_reader`、`flow_wx_history_arti
|
|
|
83
83
|
2. 需要新端点时先读 `api_docs/<endpoint>.md`,在 `src/api.js` 加端点常量与函数,复用 `httpPost` + `stripBilling`。
|
|
84
84
|
3. 在 `bin/aiconductor-cli.js` 注册子命令与选项——这是**唯一注册点**。`-k, --api-key`、`-f, --format` 每个命令各自声明,没有全局注入;可重复选项用 `collect`。
|
|
85
85
|
4. 跑 `node bin/aiconductor-cli.js <组> <子命令> --help` 核对命令面,跑 `npm test`。
|
|
86
|
-
5. 同步受影响产物:`README.md`(synopsis、选项表、示例)、对应的 `.
|
|
86
|
+
5. 同步受影响产物:`README.md`(synopsis、选项表、示例)、对应的 `.agents/skills/*/SKILL.md`(命令、选项、`data.data.*` 路径)、`src/api.test.js`(改了请求映射或格式化时)。
|
|
87
87
|
|
|
88
88
|
### 新增平台接口
|
|
89
89
|
|
|
@@ -94,11 +94,13 @@ platform 侧还有、CLI 未接 10 个:`wx_rss_reader`、`flow_wx_history_arti
|
|
|
94
94
|
|
|
95
95
|
### 维护 skill
|
|
96
96
|
|
|
97
|
-
-
|
|
97
|
+
- 技能放 `.agents/skills/<name>/`(2026-11-04 从 `.codex/skills/` 迁来,内容逐字未变,`git mv` 效果):pi 从 cwd 向上到仓库根发现项目级 `.agents/skills/`(`dist/core/package-manager.js` 的 `collectAncestorAgentsSkillDirs`),`.codex/skills/` 从来不是 pi 的发现路径。代价:项目里有 `.agents/skills/` 会触发**项目信任门禁**(`hasTrustRequiringProjectResources` 返回 true),未信任时这 11 个技能不加载。**这是故意的**(2026-11-04):本仓技能是**上架产物**(WorkBuddy / 技能仓库的发布件),不是本机开发时要用的上下文,所以 `~/.pi/agent/trust.json` 已给 `D:/Development/aiconductor-cli` 显式写 `false`——pi 不弹信任提示、技能不进上下文;需要某条技能内容时直接 `read .agents/skills/<name>/SKILL.md`,**不要为了加载它去信任项目**。
|
|
98
|
+
- `.gitignore` 用 `.agents/*` + `!.agents/skills/` 只放行技能,`.agents/ledger.md`、`.agents/plans/` 属本机脚手架不入库——**别把 `.agents/` 整目录 ignore**,那会让技能从版本控制里消失(`git status` 表现为 `.codex/` 全删、新目录全是 `??`)。
|
|
99
|
+
- 目录名、frontmatter `name`、实际调用的命令必须一致;`description` 写能力 + 用户原话触发 + 与相邻 skill 的边界(简报→content-briefing,单点搜索→doubao-search,网页→webpage-publish,抖音数据→douyin-toolbox,抖音文案→douyin-script-extract,抖音选题→douyin-topic-radar,抖音评论→douyin-comment-insight,抖音账号→douyin-account-analyze,小红书发布→xhs-note-publish,小红书选题→xhs-topic-radar,小红书单篇拆解→xhs-note-insight,小红书账号→xhs-account-analyze,公众号发文列表→wx-account-articles,单篇公众号数据→wx-article-data)。
|
|
98
100
|
- `SKILL.md` 只放路由、决策、命令行、响应路径、安全规则;长选项表、安装与密钥恢复放 `references/`,并写明何时读。
|
|
99
101
|
- 默认 JSON 输出,文档写稳定的 `data.data.*` 路径(`outputResult` 会额外包一层 `{success, data}`)。
|
|
100
102
|
- 用户请求执行即等同授权:不要每次预检安装或密钥;安装说明只在需要时读一次,修复后重试原命令一次。
|
|
101
|
-
- 安装说明被复制成
|
|
103
|
+
- 安装说明被复制成 11 份 `references/aiconductor-skills-setup.md`(技能是独立安装单元,无法跨目录引用);改它必须同步 11 份。改完跑 `md5sum .agents/skills/*/references/aiconductor-skills-setup.md | awk '{print $1}' | sort -u | wc -l`,结果必须是 **1**。
|
|
102
104
|
|
|
103
105
|
### 上架到 WorkBuddy
|
|
104
106
|
|
|
@@ -122,8 +124,13 @@ platform 侧还有、CLI 未接 10 个:`wx_rss_reader`、`flow_wx_history_arti
|
|
|
122
124
|
- **小红书发布是两段式**:本地文件或 Base64 先经 `r2Upload` 换成临时公开 URL(`valid_days` 仅 1 或 7,默认 7;图片单张 ≤ 32MB,本地视频 ≤ 100MB),再调用计费接口生成 H5 页与二维码。Base64 参数支持 `@文件路径`。
|
|
123
125
|
- **热点简报是部分成功语义**:热榜与小红书热榜并行请求,任一成功即返回 `success:true`,失败写入 `data.failed_sources` / `data.failed_services`;仅两个服务都失败才退出 1。不要简化成"全有或全无"。
|
|
124
126
|
- **`wx` 组线上真实字段**(2026-11-04 实测调用):`wx_article_data` 返回 `read` / `zan` / `looking`(在看)/ `share_num` / `collect_num` / `comment_count`;`wx_history_articles` 用 `--name` 定位时业务层只有 `mp_nickname` 与 `mp_ghid`——**`api_docs` 写的 `mp_wxid` / `head_img` 实际不返回**;文章项字段与文档一致(`post_time_str` / `read` / `zan` / `original` / `url` / `digest`)。改 formatter 按实测字段,不要按文档新增字段。
|
|
127
|
+
- **`wx_history_articles` 的上游失败藏在 `data.code`**:平台侧仍回 `success:true`,只有 `data.code` 非 0 才是失败(实测 `107` = 名称/链接对不上,`20001` = 平台侧上游故障),`src/api.js` 的 `throwOnWxHistoryFailure` 把它转成 `api_error`(退出码 1)。别用「数组为空」推断「这个号没发文」。
|
|
125
128
|
- **抖音字段单位不统一**:`video-search` 的 `video.duration` 是**毫秒**,`user-videos` 的 `duration` 是 `"MM:SS"` 字符串——同名不同单位,跨端点复用代码必翻车。
|
|
126
129
|
- **抖音时间戳是北京时间编码**:`create_time` 按 UTC 渲染出来就是北京时间,**不要再 +8 小时**(2026-10-28 实测 `video-search`/`video-comments`/`user-videos` 三处:+8 会让半小时前发的视频落到 7 小时后)。`user-videos` 另带 `create_time_formatted`,同口径。
|
|
130
|
+
- **小红书时间戳正好相反**:`note_info.time`(秒)与 `user_notes/country` 的 `time`(毫秒)都是**真 UTC epoch**,展示必须 **+8**(2026-11-05 实测:同一视频笔记 `note_info.time`+8h 等于 `search` 返回的 `notePublishTime` 字符串)。上游 `time_desc` 的 `MM-DD` 是 **UTC-8 口径**(10-29 04:50 UTC 的笔记显示 `10-28`),只能用 `time +8` 自算。
|
|
131
|
+
- **小红书端点取名与上游是错位的**:`xhs_user_notes_v2` 走 TikHub(可用、20 条/页),`xhs_user_notes_v3` 原本走 YDDM 且长期返回 `code 5003`(已于 2026-11-05 迁到同一 TikHub 上游)。CLI 选定 `xhs_user_notes_v2`;换端点要同步改测试与 `api_docs`。
|
|
132
|
+
- **小红书 `share_text` 不能透传中文口令**:平台侧是原样转发上游的,带「复制内容,然后进入【小红书】…」的分享文案直接空结果(实测)。所以 `resolveNoteId` 先本地抽 URL、再跟 302 展开 `xhslink.cn` 短链。
|
|
133
|
+
- **小红书同名不同结构**:`interact_info.*_count`、`like_count`、`sub_comment_count` 都是**字符串**;评论的 `user` 用 `userid`/`images`/`original_name`,笔记的 `user` 用 `user_id`/`avatar`/`nickname`。`noteInfo.readNum` 与 `userInfo.fansNum` 实测**恒为 0**,不要当真实阅读量/粉丝数承诺。
|
|
127
134
|
- **密钥解析**:`-k` > `AICONDUCTOR_API_KEY` > **当前工作目录**的 `.env`(不向上查找);缺失时 `resolveKeyOrExit` 打印错误并退出 1。
|
|
128
135
|
|
|
129
136
|
## 禁区
|
|
@@ -147,7 +154,7 @@ platform 侧还有、CLI 未接 10 个:`wx_rss_reader`、`flow_wx_history_arti
|
|
|
147
154
|
|
|
148
155
|
## 最短验证闭环
|
|
149
156
|
|
|
150
|
-
`npm test`(
|
|
157
|
+
`npm test`(40 个用例,离线打桩,不需要 API Key)。改动涉及 `bin/`、`README.md`、skill 文字或新增命令时,再补一条人工核对:`node bin/aiconductor-cli.js <组> <子命令> --help`——`bin/` 的接线没有自动化测试覆盖。
|
|
151
158
|
|
|
152
159
|
## 任务执行与报告约定
|
|
153
160
|
|
|
@@ -172,7 +179,7 @@ platform 侧还有、CLI 未接 10 个:`wx_rss_reader`、`flow_wx_history_arti
|
|
|
172
179
|
|
|
173
180
|
- 格式 `<tag>: <描述>`,tag 取自既有历史:`feat:`、`fix:`、`refactor:`、`docs:`、`skills:`、`search:`、`content:`、`release:`;agent 提交在前面加 `[Agent]`(如 `[Agent] release(cli): 1.4.0 —— ...`),描述可用中文。
|
|
174
181
|
- 发版提交只改 `package.json` + `package-lock.json` 的版本号;发版前先跑 `npm test`。
|
|
175
|
-
- 新增或修改 skill 必须随 `.
|
|
182
|
+
- 新增或修改 skill 必须随 `.agents/skills/**` 一起进 Git(`.npmignore` 已排除 `.agents/`,所以技能在仓库里版本化、不进 npm 包)。
|
|
176
183
|
- 历史只有单作者、无 PR 记录,可直接在 `master` 提交;若走 PR,描述写清命令面/API 影响与验证命令,保持 diff 聚焦。
|
|
177
184
|
|
|
178
185
|
## 安全与配置
|
package/README.md
CHANGED
|
@@ -315,6 +315,70 @@ aiconductor-cli xhs hot-list [选项]
|
|
|
315
315
|
aiconductor-cli xhs hot-list -f text
|
|
316
316
|
```
|
|
317
317
|
|
|
318
|
+
#### 笔记详情 `xhs note`
|
|
319
|
+
|
|
320
|
+
```bash
|
|
321
|
+
aiconductor-cli xhs note <ref> [选项]
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
`ref` 支持笔记链接、`xhslink.cn`/`xhslink.com` 短链、完整分享口令,或 24 位 `note_id`。
|
|
325
|
+
|
|
326
|
+
```bash
|
|
327
|
+
aiconductor-cli xhs note "https://www.xiaohongshu.com/discovery/item/6ae972260000000015020d84" -f text
|
|
328
|
+
aiconductor-cli xhs note "标题 https://xhslink.cn/o/85lbqBJtO94 复制内容,然后进入【小红书】查看相关笔记。"
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
#### 笔记评论 `xhs comments`
|
|
332
|
+
|
|
333
|
+
```bash
|
|
334
|
+
aiconductor-cli xhs comments <ref> [选项]
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
| 选项 | 说明 | 默认值 |
|
|
338
|
+
|------|------|--------|
|
|
339
|
+
| `--max-pages <n>` | 最大翻页数,范围 1~5 | 1 |
|
|
340
|
+
| `--max-seconds <n>` | 最长运行时间(秒),上限 170 | 50 |
|
|
341
|
+
|
|
342
|
+
返回一级评论,并带上上游可见的楼内回复(`sub_comments`)。`data.data.comments[]` 里 `like_count`、`sub_comment_count` 是**字符串**,评论的 `user` 对象用 `userid`/`original_name`(与笔记的 `user_id`/`nickname` 不同)。
|
|
343
|
+
|
|
344
|
+
#### 笔记搜索 `xhs search`
|
|
345
|
+
|
|
346
|
+
```bash
|
|
347
|
+
aiconductor-cli xhs search <keyword> [选项]
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
| 选项 | 说明 | 默认值 |
|
|
351
|
+
|------|------|--------|
|
|
352
|
+
| `--sort <sort>` | `general` / `time_descending` / `popularity_descending` / `comment_descending` / `collect_descending` | 不限 |
|
|
353
|
+
| `--note-time <range>` | `day` / `week` / `halfyear` | 不限 |
|
|
354
|
+
| `--note-type <type>` | `video` / `note` | 不限 |
|
|
355
|
+
| `--max-pages <n>` | 最大翻页数,范围 1~5 | 1 |
|
|
356
|
+
|
|
357
|
+
示例:
|
|
358
|
+
```bash
|
|
359
|
+
aiconductor-cli xhs search "AI编程" --note-time week --sort popularity_descending --max-pages 2 -f text
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
结果在 `data.data.items[].noteInfo` 与 `.userInfo`。注意 `noteInfo.readNum` 与 `userInfo.fansNum` 实测恒为 0,不要当真实阅读量/粉丝数用。
|
|
363
|
+
|
|
364
|
+
#### 用户详情 `xhs user` / 用户笔记 `xhs user-notes`
|
|
365
|
+
|
|
366
|
+
```bash
|
|
367
|
+
aiconductor-cli xhs user <ref> [选项]
|
|
368
|
+
aiconductor-cli xhs user-notes <ref> [选项] # 支持 --max-pages,范围 1~5
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
`ref` 支持主页链接 `/user/profile/<id>` 或 24 位 `user_id`。
|
|
372
|
+
|
|
373
|
+
```bash
|
|
374
|
+
aiconductor-cli xhs user "https://www.xiaohongshu.com/user/profile/53cd3bbbb4c4d64f93463018" -f text
|
|
375
|
+
aiconductor-cli xhs user-notes 53cd3bbbb4c4d64f93463018 --max-pages 2 -f text
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
用户详情在 `user_info`(`basic_info` + 5 项中文 `interactions` + `tags`);用户笔记在 `data.data.notes[]`。
|
|
379
|
+
|
|
380
|
+
> 时间口径:小红书所有 `time` 都是 **UTC 时间戳**(笔记详情为秒、用户笔记为毫秒),`-f text` 已自动换算成北京时间;**不要**再额外加 8 小时。上游的 `time_desc`(如 `10-28`)是 UTC-8 口径,文本模式不使用它。
|
|
381
|
+
|
|
318
382
|
### 小红书扫码发布
|
|
319
383
|
|
|
320
384
|
生成图文或视频笔记的 H5 发布页与二维码。素材支持公网 URL、本地文件、Base64/data URL,以及包含 Base64 内容的文本文件。
|
|
@@ -374,6 +438,8 @@ aiconductor-cli wx history "https://mp.weixin.qq.com/s?__biz=xxxx" -f text
|
|
|
374
438
|
|
|
375
439
|
返回路径:`article-data` 的互动数据在 `data.data`(`read` / `zan` / `looking` / `share_num` / `collect_num` / `comment_count`,text 模式渲染成阅读/点赞/在看/分享/收藏/评论,未知字段原样输出);`history` 的文章在 `data.data.data[]`(标题 `title`、时间 `post_time_str`、阅读 `read`、点赞 `zan`、链接 `url`)。
|
|
376
440
|
|
|
441
|
+
`history` 另带 `data.data.code`(上游返回码):`0` 才表示成功;非 0(如 `107` 名称/链接对不上、`20001` 平台侧上游故障)时 CLI 以退出码 1 报 `api_error`,不会用空列表冒充成功。
|
|
442
|
+
|
|
377
443
|
### 通用选项
|
|
378
444
|
|
|
379
445
|
所有命令支持:
|
|
@@ -395,6 +461,11 @@ aiconductor-cli wx history "https://mp.weixin.qq.com/s?__biz=xxxx" -f text
|
|
|
395
461
|
| 抖音视频搜索 | 0.15 元/页 |
|
|
396
462
|
| 抖音链接转文字 | 0.01 元/次 + 0.0003 元/秒 |
|
|
397
463
|
| 小红书热榜 | 以平台服务配置为准 |
|
|
464
|
+
| 小红书笔记详情 | 以平台服务配置为准(按次) |
|
|
465
|
+
| 小红书笔记评论 | 以平台服务配置为准(按成功翻页数) |
|
|
466
|
+
| 小红书笔记搜索 | 以平台服务配置为准(按成功翻页数) |
|
|
467
|
+
| 小红书用户详情 | 以平台服务配置为准(按次) |
|
|
468
|
+
| 小红书用户笔记列表 | 以平台服务配置为准(按成功翻页数) |
|
|
398
469
|
| 小红书扫码发布 | 0.05 元/次 |
|
|
399
470
|
| 公众号文章互动数据 | 以平台服务配置为准(按次) |
|
|
400
471
|
| 公众号历史文章列表 | 以平台服务配置为准(按成功翻页数) |
|
|
@@ -5,7 +5,11 @@
|
|
|
5
5
|
- **鉴权**: 请求头 `Authorization: Bearer <api_key>`(兼容 body/query 中 `api_key` 参数)
|
|
6
6
|
|
|
7
7
|
## 功能说明
|
|
8
|
-
根据用户ID
|
|
8
|
+
根据用户ID获取小红书用户详情,包括基本信息、互动数据(关注/粉丝/获赞与收藏/获赞/收藏)、标签等。按次计费。
|
|
9
|
+
|
|
10
|
+
> **数据源变更(2026-11-05)**:上游由 YDDM `/xhs/user_detail`(实测 8.2s)切换为 TikHub `GET /api/v1/xiaohongshu/app_v2/get_user_info`(实测 2.3s),**入参与出参形状完全不变**:
|
|
11
|
+
> `user_info.basic_info{nickname, avatar, red_id, gender, ip_location, desc}` + `user_info.interactions[5]{type, name, count}`(关注/粉丝/获赞与收藏/获赞/收藏)+ `user_info.tags[string]`。
|
|
12
|
+
> 两个口径差异需知悉:① `ip_location` 统一给**中文省份**(优先取资料地区省份,回落英文 IP 归属地,如 `福建`);② `获赞与收藏` 取上游自带的合计值,缺失时用 `获赞 + 收藏` 兜底。
|
|
9
13
|
|
|
10
14
|
## 请求参数
|
|
11
15
|
|
|
@@ -7,6 +7,8 @@
|
|
|
7
7
|
## 功能说明
|
|
8
8
|
根据用户 ID 获取小红书用户笔记列表,支持自动翻页(cursor 机制),兼容图文与视频笔记,按页计费。费用以 api_services 表配置为准(service_code: `xhs_user_notes_v2`)。
|
|
9
9
|
|
|
10
|
+
> **数据源变更(2026-11-05)**:上游由 YDDM `/xhs/user_post2`(持续返回 `code 5003 网络连接失败`,接口表现为 `success:true` + 空 `notes`)切换为 TikHub `GET /api/v1/xiaohongshu/app_v2/get_user_posted_notes`(实测 20 条/页、1.9s)。**入参与出参形状完全不变**,与 `xhs_user_notes_v2` 同一上游、同一输出结构(差别只在 `desc` 等字段的原始内容)。
|
|
11
|
+
|
|
10
12
|
## 请求参数
|
|
11
13
|
|
|
12
14
|
| 参数名 | 类型 | 必填 | 默认值 | 说明 |
|
package/bin/aiconductor-cli.js
CHANGED
|
@@ -18,7 +18,15 @@ const {
|
|
|
18
18
|
videoSearchCommand,
|
|
19
19
|
linkToTextCommand,
|
|
20
20
|
} = require("../src/commands/douyin");
|
|
21
|
-
const {
|
|
21
|
+
const {
|
|
22
|
+
hotListCommand: xhsHotListCommand,
|
|
23
|
+
notePublishCommand,
|
|
24
|
+
noteCommand: xhsNoteCommand,
|
|
25
|
+
commentsCommand: xhsCommentsCommand,
|
|
26
|
+
searchCommand: xhsSearchCommand,
|
|
27
|
+
userCommand: xhsUserCommand,
|
|
28
|
+
userNotesCommand: xhsUserNotesCommand,
|
|
29
|
+
} = require("../src/commands/xhs");
|
|
22
30
|
const {
|
|
23
31
|
articleDataCommand: wxArticleDataCommand,
|
|
24
32
|
historyCommand: wxHistoryCommand,
|
|
@@ -257,6 +265,53 @@ xhsCmd
|
|
|
257
265
|
.option("-f, --format <format>", "输出格式:json 或 text", "json")
|
|
258
266
|
.action((type, options) => { notePublishCommand(type, options); });
|
|
259
267
|
|
|
268
|
+
xhsCmd
|
|
269
|
+
.command("note")
|
|
270
|
+
.description("获取小红书笔记详情(按次计费)")
|
|
271
|
+
.argument("<ref>", "笔记链接、xhslink 短链、分享口令或 24 位 note_id")
|
|
272
|
+
.option("-k, --api-key <key>", "AIConductor API Key")
|
|
273
|
+
.option("-f, --format <format>", "输出格式:json 或 text", "json")
|
|
274
|
+
.action((ref, options) => { xhsNoteCommand(ref, options); });
|
|
275
|
+
|
|
276
|
+
xhsCmd
|
|
277
|
+
.command("comments")
|
|
278
|
+
.description("获取小红书笔记评论,含可见的楼内回复(按成功翻页计费)")
|
|
279
|
+
.argument("<ref>", "笔记链接、xhslink 短链、分享口令或 24 位 note_id")
|
|
280
|
+
.option("-k, --api-key <key>", "AIConductor API Key")
|
|
281
|
+
.option("--max-pages <n>", "最大翻页数,范围 1~5(每页约 10 条)", "1")
|
|
282
|
+
.option("--max-seconds <n>", "最长运行时间(秒),最大 170", "50")
|
|
283
|
+
.option("-f, --format <format>", "输出格式:json 或 text", "json")
|
|
284
|
+
.action((ref, options) => { xhsCommentsCommand(ref, options); });
|
|
285
|
+
|
|
286
|
+
xhsCmd
|
|
287
|
+
.command("search")
|
|
288
|
+
.description("按关键词搜索小红书笔记(按成功翻页计费,每页约 20 条)")
|
|
289
|
+
.argument("<keyword>", "搜索关键词")
|
|
290
|
+
.option("-k, --api-key <key>", "AIConductor API Key")
|
|
291
|
+
.option("--sort <sort>", "排序:general/time_descending/popularity_descending/comment_descending/collect_descending")
|
|
292
|
+
.option("--note-time <range>", "发布时间:day/week/halfyear(不传=不限)")
|
|
293
|
+
.option("--note-type <type>", "笔记类型:video/note(不传=不限)")
|
|
294
|
+
.option("--max-pages <n>", "最大翻页数,范围 1~5", "1")
|
|
295
|
+
.option("-f, --format <format>", "输出格式:json 或 text", "json")
|
|
296
|
+
.action((keyword, options) => { xhsSearchCommand(keyword, options); });
|
|
297
|
+
|
|
298
|
+
xhsCmd
|
|
299
|
+
.command("user")
|
|
300
|
+
.description("获取小红书用户详情(按次计费)")
|
|
301
|
+
.argument("<ref>", "主页链接 /user/profile/ 或 24 位 user_id")
|
|
302
|
+
.option("-k, --api-key <key>", "AIConductor API Key")
|
|
303
|
+
.option("-f, --format <format>", "输出格式:json 或 text", "json")
|
|
304
|
+
.action((ref, options) => { xhsUserCommand(ref, options); });
|
|
305
|
+
|
|
306
|
+
xhsCmd
|
|
307
|
+
.command("user-notes")
|
|
308
|
+
.description("获取小红书用户笔记列表(按成功翻页计费,每页约 20 条)")
|
|
309
|
+
.argument("<ref>", "主页链接 /user/profile/ 或 24 位 user_id")
|
|
310
|
+
.option("-k, --api-key <key>", "AIConductor API Key")
|
|
311
|
+
.option("--max-pages <n>", "最大翻页数,范围 1~5", "1")
|
|
312
|
+
.option("-f, --format <format>", "输出格式:json 或 text", "json")
|
|
313
|
+
.action((ref, options) => { xhsUserNotesCommand(ref, options); });
|
|
314
|
+
|
|
260
315
|
// wx 子命令组(公众号数据)
|
|
261
316
|
const wxCmd = program
|
|
262
317
|
.command("wx")
|
package/package.json
CHANGED
package/src/api.js
CHANGED
|
@@ -19,6 +19,25 @@ const DOUYIN_USER_SEARCH_PATH = "/api/douyin_user_search";
|
|
|
19
19
|
const DOUYIN_VIDEO_SEARCH_PATH = "/api/douyin_video_search";
|
|
20
20
|
const DOUYIN_LINK_TO_TEXT_PATH = "/api/douyin_link_to_text_v2";
|
|
21
21
|
const XHS_NOTE_PUBLISH_PATH = "/api/xhs_note_publish";
|
|
22
|
+
const XHS_NOTE_INFO_PATH = "/api/xhs_note_info_v3";
|
|
23
|
+
const XHS_NOTE_COMMENTS_PATH = "/api/xhs_note_comments_fast";
|
|
24
|
+
const XHS_NOTE_SEARCH_PATH = "/api/xhs_note_search_v3";
|
|
25
|
+
const XHS_USER_INFO_PATH = "/api/xhs_user_info_v2";
|
|
26
|
+
// 刻意打 v2:v3 已于 2026-11-05 迁到同一上游且同输出,但 v2 是历史真实流量所在(api_docs 两篇一致)。
|
|
27
|
+
const XHS_USER_NOTES_PATH = "/api/xhs_user_notes_v2";
|
|
28
|
+
|
|
29
|
+
// 本地校验区间,命中就在发请求前拦截,不产生计费
|
|
30
|
+
const XHS_MAX_PAGES_LIMIT = 5;
|
|
31
|
+
const XHS_MAX_SECONDS_LIMIT = 170;
|
|
32
|
+
const XHS_SEARCH_SORTS = [
|
|
33
|
+
"general",
|
|
34
|
+
"time_descending",
|
|
35
|
+
"popularity_descending",
|
|
36
|
+
"comment_descending",
|
|
37
|
+
"collect_descending",
|
|
38
|
+
];
|
|
39
|
+
const XHS_SEARCH_NOTE_TIMES = ["day", "week", "halfyear"];
|
|
40
|
+
const XHS_SEARCH_NOTE_TYPES = ["video", "note"];
|
|
22
41
|
const WX_ARTICLE_DATA_PATH = "/api/wx_article_data";
|
|
23
42
|
const WX_HISTORY_ARTICLES_PATH = "/api/wx_history_articles";
|
|
24
43
|
const R2_UPLOADER_PATH = "/api/r2_uploader";
|
|
@@ -176,6 +195,22 @@ function throwOnBusinessFailure(resp) {
|
|
|
176
195
|
return resp;
|
|
177
196
|
}
|
|
178
197
|
|
|
198
|
+
/**
|
|
199
|
+
* wx_history_articles 把上游失败塞在 HTTP 200 + success:true 的 data.code 里
|
|
200
|
+
* (api_docs:data.code 为 0 才算成功,平台侧并不改写成 success:false)。
|
|
201
|
+
* 不认这个码,就会把「上游挂了」当成「这个号没有文章」。
|
|
202
|
+
* @param {object} resp
|
|
203
|
+
* @returns {object}
|
|
204
|
+
*/
|
|
205
|
+
function throwOnWxHistoryFailure(resp) {
|
|
206
|
+
const inner = resp && resp.data;
|
|
207
|
+
if (inner && typeof inner.code === "number" && inner.code !== 0) {
|
|
208
|
+
const detail = resp.message || inner.msg || "上游返回失败";
|
|
209
|
+
throw Object.assign(new Error(`${detail}(上游错误码 ${inner.code})`), { type: "api_error" });
|
|
210
|
+
}
|
|
211
|
+
return resp;
|
|
212
|
+
}
|
|
213
|
+
|
|
179
214
|
function contentTypeForFilename(filename) {
|
|
180
215
|
const types = {
|
|
181
216
|
".avif": "image/avif",
|
|
@@ -638,7 +673,7 @@ async function wxHistoryArticles(apiKey, params = {}) {
|
|
|
638
673
|
const resp = await httpPost(API_HOST, WX_HISTORY_ARTICLES_PATH, body, {
|
|
639
674
|
Authorization: `Bearer ${apiKey}`,
|
|
640
675
|
});
|
|
641
|
-
return throwOnBusinessFailure(stripBilling(resp));
|
|
676
|
+
return throwOnWxHistoryFailure(throwOnBusinessFailure(stripBilling(resp)));
|
|
642
677
|
}
|
|
643
678
|
|
|
644
679
|
/**
|
|
@@ -661,6 +696,208 @@ async function xhsNotePublish(apiKey, params) {
|
|
|
661
696
|
return stripBilling(resp);
|
|
662
697
|
}
|
|
663
698
|
|
|
699
|
+
/**
|
|
700
|
+
* 抛 api_error(上层按业务失败处理)
|
|
701
|
+
* @param {object} result
|
|
702
|
+
* @returns {object}
|
|
703
|
+
*/
|
|
704
|
+
function throwOnXhsFailure(result) {
|
|
705
|
+
if (result?.success === false) {
|
|
706
|
+
throw Object.assign(new Error(result.message || "小红书接口返回失败"), { type: "api_error" });
|
|
707
|
+
}
|
|
708
|
+
return result;
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
/**
|
|
712
|
+
* 校验 1~上限 的正整数(上限单独校验,避免与其他命令的 limit 语义混住)
|
|
713
|
+
* @param {*} value
|
|
714
|
+
* @param {string} name
|
|
715
|
+
* @param {number} max
|
|
716
|
+
* @returns {number|null}
|
|
717
|
+
*/
|
|
718
|
+
function validateXhsPositiveInt(value, name, max) {
|
|
719
|
+
if (value === undefined || value === null || value === "") return null;
|
|
720
|
+
const n = Number(value);
|
|
721
|
+
if (!Number.isInteger(n) || n < 1 || n > max) {
|
|
722
|
+
throw Object.assign(new Error(`${name} 须为 1~${max} 的整数,收到: ${value}`), {
|
|
723
|
+
type: "validation_error",
|
|
724
|
+
});
|
|
725
|
+
}
|
|
726
|
+
return n;
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
/**
|
|
730
|
+
* 获取小红书笔记详情。
|
|
731
|
+
* 注意:只传 note_id 即可(2026-11-05 实测,不需要 xsec_token)。
|
|
732
|
+
* @param {string} apiKey
|
|
733
|
+
* @param {string} noteId
|
|
734
|
+
* @returns {Promise<object>} `note_data` 在顶层,没有 data.data 嵌套
|
|
735
|
+
*/
|
|
736
|
+
async function xhsNoteInfo(apiKey, noteId) {
|
|
737
|
+
const resp = await httpPost(API_HOST, XHS_NOTE_INFO_PATH, { note_id: noteId }, {
|
|
738
|
+
Authorization: `Bearer ${apiKey}`,
|
|
739
|
+
}, http);
|
|
740
|
+
return throwOnXhsFailure(stripBilling(resp));
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
/**
|
|
744
|
+
* 获取小红书笔记一级评论(含可见的楼内回复 sub_comments)。
|
|
745
|
+
* @param {string} apiKey
|
|
746
|
+
* @param {{noteId?: string, shareText?: string, maxPages?: number, maxSeconds?: number}} params
|
|
747
|
+
* @returns {Promise<object>} `data.data.comments`,10 条/页
|
|
748
|
+
*/
|
|
749
|
+
async function xhsNoteComments(apiKey, params = {}) {
|
|
750
|
+
const body = {};
|
|
751
|
+
const maxPages = validateXhsPositiveInt(params.maxPages, "max_pages", XHS_MAX_PAGES_LIMIT);
|
|
752
|
+
const maxSeconds = validateXhsPositiveInt(params.maxSeconds, "max_seconds", XHS_MAX_SECONDS_LIMIT);
|
|
753
|
+
if (maxPages !== null) body.max_pages = maxPages;
|
|
754
|
+
if (maxSeconds !== null) body.max_seconds = maxSeconds;
|
|
755
|
+
|
|
756
|
+
if (params.noteId) {
|
|
757
|
+
body.note_id = params.noteId;
|
|
758
|
+
} else if (params.shareText) {
|
|
759
|
+
body.share_text = params.shareText;
|
|
760
|
+
} else {
|
|
761
|
+
throw Object.assign(new Error("note_id 与分享链接至少提供一个"), { type: "validation_error" });
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
const resp = await httpPost(API_HOST, XHS_NOTE_COMMENTS_PATH, body, {
|
|
765
|
+
Authorization: `Bearer ${apiKey}`,
|
|
766
|
+
}, http);
|
|
767
|
+
return throwOnXhsFailure(stripBilling(resp));
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
/**
|
|
771
|
+
* 按关键词搜索小红书笔记。
|
|
772
|
+
* @param {string} apiKey
|
|
773
|
+
* @param {{keyword: string, maxPages?: number, sort?: string, noteTime?: string, noteType?: string}} params
|
|
774
|
+
* @returns {Promise<object>} `data.data.items[]`,20 条/页
|
|
775
|
+
*/
|
|
776
|
+
async function xhsNoteSearch(apiKey, params = {}) {
|
|
777
|
+
const keyword = typeof params.keyword === "string" ? params.keyword.trim() : "";
|
|
778
|
+
if (!keyword) {
|
|
779
|
+
throw Object.assign(new Error("keyword 不能为空"), { type: "validation_error" });
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
const body = { keyword };
|
|
783
|
+
const maxPages = validateXhsPositiveInt(params.maxPages, "max_pages", XHS_MAX_PAGES_LIMIT);
|
|
784
|
+
if (maxPages !== null) body.max_pages = maxPages;
|
|
785
|
+
|
|
786
|
+
if (params.sort) {
|
|
787
|
+
if (!XHS_SEARCH_SORTS.includes(params.sort)) {
|
|
788
|
+
throw Object.assign(new Error(`sort 仅支持 ${XHS_SEARCH_SORTS.join("|")},收到: ${params.sort}`), {
|
|
789
|
+
type: "validation_error",
|
|
790
|
+
});
|
|
791
|
+
}
|
|
792
|
+
body.sort = params.sort;
|
|
793
|
+
}
|
|
794
|
+
if (params.noteTime) {
|
|
795
|
+
if (!XHS_SEARCH_NOTE_TIMES.includes(params.noteTime)) {
|
|
796
|
+
throw Object.assign(new Error(`note_time 仅支持 ${XHS_SEARCH_NOTE_TIMES.join("|")},收到: ${params.noteTime}`), {
|
|
797
|
+
type: "validation_error",
|
|
798
|
+
});
|
|
799
|
+
}
|
|
800
|
+
body.note_time = params.noteTime;
|
|
801
|
+
}
|
|
802
|
+
if (params.noteType) {
|
|
803
|
+
if (!XHS_SEARCH_NOTE_TYPES.includes(params.noteType)) {
|
|
804
|
+
throw Object.assign(new Error(`note_type 仅支持 ${XHS_SEARCH_NOTE_TYPES.join("|")},收到: ${params.noteType}`), {
|
|
805
|
+
type: "validation_error",
|
|
806
|
+
});
|
|
807
|
+
}
|
|
808
|
+
body.note_type = params.noteType;
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
const resp = await httpPost(API_HOST, XHS_NOTE_SEARCH_PATH, body, {
|
|
812
|
+
Authorization: `Bearer ${apiKey}`,
|
|
813
|
+
}, http);
|
|
814
|
+
return throwOnXhsFailure(stripBilling(resp));
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
/**
|
|
818
|
+
* 获取小红书用户详情。
|
|
819
|
+
* @param {string} apiKey
|
|
820
|
+
* @param {string} userId
|
|
821
|
+
* @returns {Promise<object>} `user_info` 在顶层,interactions 为 5 项中文
|
|
822
|
+
*/
|
|
823
|
+
async function xhsUserInfo(apiKey, userId) {
|
|
824
|
+
const resp = await httpPost(API_HOST, XHS_USER_INFO_PATH, { user_id: userId }, {
|
|
825
|
+
Authorization: `Bearer ${apiKey}`,
|
|
826
|
+
}, http);
|
|
827
|
+
return throwOnXhsFailure(stripBilling(resp));
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
/**
|
|
831
|
+
* 获取小红书用户笔记列表。
|
|
832
|
+
* @param {string} apiKey
|
|
833
|
+
* @param {{userId: string, maxPages?: number}} params
|
|
834
|
+
* @returns {Promise<object>} `data.data.notes[]`,20 条/页
|
|
835
|
+
*/
|
|
836
|
+
async function xhsUserNotes(apiKey, params = {}) {
|
|
837
|
+
if (!params.userId) {
|
|
838
|
+
throw Object.assign(new Error("user_id 不能为空"), { type: "validation_error" });
|
|
839
|
+
}
|
|
840
|
+
const body = { user_id: params.userId };
|
|
841
|
+
const maxPages = validateXhsPositiveInt(params.maxPages, "max_pages", XHS_MAX_PAGES_LIMIT);
|
|
842
|
+
if (maxPages !== null) body.max_pages = maxPages;
|
|
843
|
+
|
|
844
|
+
const resp = await httpPost(API_HOST, XHS_USER_NOTES_PATH, body, {
|
|
845
|
+
Authorization: `Bearer ${apiKey}`,
|
|
846
|
+
}, http);
|
|
847
|
+
return throwOnXhsFailure(stripBilling(resp));
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
/**
|
|
851
|
+
* 展开小红书分享短链(xhslink.cn / xhslink.com)为真实长链。
|
|
852
|
+
* 短链必须跟随 302,否则拿不到 note_id。
|
|
853
|
+
* @param {string} shortUrl
|
|
854
|
+
* @returns {Promise<string>} 长链;不是跳转时原样返回
|
|
855
|
+
*/
|
|
856
|
+
function resolveXhsShortLink(shortUrl) {
|
|
857
|
+
return new Promise((resolve, reject) => {
|
|
858
|
+
let parsed;
|
|
859
|
+
try {
|
|
860
|
+
parsed = new URL(shortUrl);
|
|
861
|
+
} catch {
|
|
862
|
+
reject(Object.assign(new Error(`短链格式无效: ${shortUrl}`), { type: "validation_error" }));
|
|
863
|
+
return;
|
|
864
|
+
}
|
|
865
|
+
if (parsed.protocol !== "https:" && parsed.protocol !== "http:") {
|
|
866
|
+
reject(Object.assign(new Error(`短链仅支持 HTTP/HTTPS: ${shortUrl}`), { type: "validation_error" }));
|
|
867
|
+
return;
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
const transport = parsed.protocol === "https:" ? https : http;
|
|
871
|
+
const req = transport.request({
|
|
872
|
+
hostname: parsed.hostname,
|
|
873
|
+
port: parsed.port || (parsed.protocol === "https:" ? 443 : 80),
|
|
874
|
+
path: `${parsed.pathname}${parsed.search}`,
|
|
875
|
+
method: "GET",
|
|
876
|
+
headers: {
|
|
877
|
+
"User-Agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Mobile/15E148 Safari/604.1",
|
|
878
|
+
},
|
|
879
|
+
}, (res) => {
|
|
880
|
+
const location = res.headers?.location;
|
|
881
|
+
if (res.statusCode >= 300 && res.statusCode < 400 && location) {
|
|
882
|
+
if (res.resume) res.resume();
|
|
883
|
+
resolve(location);
|
|
884
|
+
return;
|
|
885
|
+
}
|
|
886
|
+
if (res.resume) res.resume();
|
|
887
|
+
resolve(shortUrl);
|
|
888
|
+
});
|
|
889
|
+
|
|
890
|
+
req.setTimeout(15000, () => {
|
|
891
|
+
req.destroy();
|
|
892
|
+
reject(Object.assign(new Error(`短链解析超时: ${shortUrl}`), { type: "network_error" }));
|
|
893
|
+
});
|
|
894
|
+
req.on("error", (err) => {
|
|
895
|
+
reject(Object.assign(new Error(`短链解析失败: ${err.message}`), { type: "network_error" }));
|
|
896
|
+
});
|
|
897
|
+
req.end();
|
|
898
|
+
});
|
|
899
|
+
}
|
|
900
|
+
|
|
664
901
|
module.exports = {
|
|
665
902
|
resolveApiKey,
|
|
666
903
|
publishHtml,
|
|
@@ -679,6 +916,12 @@ module.exports = {
|
|
|
679
916
|
douyinLinkToText,
|
|
680
917
|
r2Upload,
|
|
681
918
|
xhsNotePublish,
|
|
919
|
+
xhsNoteInfo,
|
|
920
|
+
xhsNoteComments,
|
|
921
|
+
xhsNoteSearch,
|
|
922
|
+
xhsUserInfo,
|
|
923
|
+
xhsUserNotes,
|
|
924
|
+
resolveXhsShortLink,
|
|
682
925
|
wxArticleData,
|
|
683
926
|
wxHistoryArticles,
|
|
684
927
|
};
|