aiconductor-cli 1.3.4 → 1.3.5

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.
Files changed (82) hide show
  1. package/AGENTS.md +44 -4
  2. package/README.md +26 -0
  3. package/api_docs/aic_web_search.md +84 -0
  4. package/api_docs/audio_converter.md +51 -0
  5. package/api_docs/balance_query.md +38 -0
  6. package/api_docs/bing_search.md +55 -0
  7. package/api_docs/china_stock_data.md +221 -0
  8. package/api_docs/delete_published_webpage.md +38 -0
  9. package/api_docs/doubao_search.md +80 -0
  10. package/api_docs/douyin_link_to_text_v1.md +50 -0
  11. package/api_docs/douyin_link_to_text_v2.md +50 -0
  12. package/api_docs/douyin_user_detail.md +65 -0
  13. package/api_docs/douyin_user_search.md +58 -0
  14. package/api_docs/douyin_user_videos.md +49 -0
  15. package/api_docs/douyin_user_videos_async.md +45 -0
  16. package/api_docs/douyin_user_videos_async_query.md +41 -0
  17. package/api_docs/douyin_user_videos_max.md +59 -0
  18. package/api_docs/douyin_video_comments.md +68 -0
  19. package/api_docs/douyin_video_info.md +70 -0
  20. package/api_docs/douyin_video_search.md +60 -0
  21. package/api_docs/douyin_video_search_fast.md +48 -0
  22. package/api_docs/fast_ocr.md +51 -0
  23. package/api_docs/flow_article2podcast_creat.md +41 -0
  24. package/api_docs/flow_article2podcast_query.md +41 -0
  25. package/api_docs/flow_wx_history_articles.md +45 -0
  26. package/api_docs/generate_image.md +68 -0
  27. package/api_docs/get_duration.md +37 -0
  28. package/api_docs/hot_list_mixed.md +122 -0
  29. package/api_docs/html_publish.md +50 -0
  30. package/api_docs/html_publish_flexible.md +65 -0
  31. package/api_docs/image_resize.md +58 -0
  32. package/api_docs/image_to_permalink.md +61 -0
  33. package/api_docs/link_expander.md +39 -0
  34. package/api_docs/llm_proxy.md +61 -0
  35. package/api_docs/markitdown.md +47 -0
  36. package/api_docs/md_to_docx.md +51 -0
  37. package/api_docs/md_to_html.md +53 -0
  38. package/api_docs/md_to_mindmap.md +52 -0
  39. package/api_docs/md_to_pdf.md +52 -0
  40. package/api_docs/md_to_png.md +53 -0
  41. package/api_docs/md_to_txt.md +51 -0
  42. package/api_docs/md_to_wxhtml.md +64 -0
  43. package/api_docs/md_to_xhs.md +87 -0
  44. package/api_docs/md_to_xlsx.md +52 -0
  45. package/api_docs/portrait_enhancement.md +69 -0
  46. package/api_docs/published_webpages.md +45 -0
  47. package/api_docs/r2_uploader.md +51 -0
  48. package/api_docs/schedulerx_job_create.md +56 -0
  49. package/api_docs/transaction_query.md +52 -0
  50. package/api_docs/url_to_markdown.md +50 -0
  51. package/api_docs/video_download_url.md +57 -0
  52. package/api_docs/video_download_url_oss.md +55 -0
  53. package/api_docs/video_to_text.md +48 -0
  54. package/api_docs/video_to_text_v2.md +49 -0
  55. package/api_docs/volcengine_face_pretty.md +55 -0
  56. package/api_docs/volcengine_faceswap.md +55 -0
  57. package/api_docs/website_screenshot.md +49 -0
  58. package/api_docs/wx_access_token.md +44 -0
  59. package/api_docs/wx_article_analysis_data_push.md +43 -0
  60. package/api_docs/wx_article_data.md +48 -0
  61. package/api_docs/wx_article_style_rewrite.md +45 -0
  62. package/api_docs/wx_draft_add.md +56 -0
  63. package/api_docs/wx_draft_publish.md +47 -0
  64. package/api_docs/wx_history_articles.md +70 -0
  65. package/api_docs/wx_image_upload.md +46 -0
  66. package/api_docs/wx_md_to_html.md +48 -0
  67. package/api_docs/wx_rss_reader.md +59 -0
  68. package/api_docs/xhs_hot_list.md +48 -0
  69. package/api_docs/xhs_note_comments_fast.md +54 -0
  70. package/api_docs/xhs_note_info_v2.md +45 -0
  71. package/api_docs/xhs_note_info_v3.md +69 -0
  72. package/api_docs/xhs_note_publish.md +54 -0
  73. package/api_docs/xhs_note_search_v2.md +73 -0
  74. package/api_docs/xhs_note_search_v3.md +54 -0
  75. package/api_docs/xhs_user_info_v2.md +57 -0
  76. package/api_docs/xhs_user_notes_v2.md +56 -0
  77. package/api_docs/xhs_user_notes_v3.md +46 -0
  78. package/bin/aiconductor-cli.js +23 -6
  79. package/package.json +1 -1
  80. package/src/api.js +45 -7
  81. package/src/api.test.js +67 -5
  82. package/src/commands/content.js +90 -0
@@ -0,0 +1,46 @@
1
+ ## 接口信息
2
+ - **URL**: `http://plugin.aiconductor.fun/api/xhs_user_notes_v3`
3
+ - **方法**: `POST` / `GET`
4
+ - **Content-Type**: `application/json`
5
+ - **鉴权**: 请求头 `Authorization: Bearer <api_key>`(兼容 body/query 中 `api_key` 参数)
6
+
7
+ ## 功能说明
8
+ 根据用户 ID 获取小红书用户笔记列表,支持自动翻页(cursor 机制),兼容图文与视频笔记,按页计费。费用以 api_services 表配置为准(service_code: `xhs_user_notes_v2`)。
9
+
10
+ ## 请求参数
11
+
12
+ | 参数名 | 类型 | 必填 | 默认值 | 说明 |
13
+ |--------|------|------|--------|------|
14
+ | user_id | string | 是 | — | 小红书用户 ID |
15
+ | max_pages | int | 否 | 1 | 最大翻页数 |
16
+ | max_seconds | int | 否 | 50 | 最长运行时长(秒) |
17
+
18
+ > **鉴权参数**:api_key 通过请求头 `Authorization: Bearer <api_key>` 传递,不再列入请求参数表。
19
+
20
+ ## 响应参数
21
+
22
+ | 参数名 | 类型 | 说明 |
23
+ |--------|------|------|
24
+ | success | boolean | 是否成功 |
25
+ | message | string | 响应消息 |
26
+ | billing | object | 计费信息(service_fee / discount_applied / balance_before / balance_after / currency / pages_billed / total_service_fee / transaction_ids) |
27
+ | data.notes | array[object] | 笔记列表(见下) |
28
+
29
+ `data.notes[]` 字段:`noteId`(string)、`time`(int, 毫秒时间戳)、`title`(string)、`type`(string, normal/video)、`desc`(string)、`time_desc`(string)、`likes`(int)、`comments_count`(int)、`collected_count`(int)、`shared_count`(int)、`ip_location`(string)、`sticky`(bool)、`is_goods_note`(bool)、`has_music`(bool)、`user`(object{user_id, nickname, avatar})、`images`(array[{url, url_size_large, width, height}])。
30
+ 视频笔记额外包含 `video`(object):`duration`(int)、`width`(int)、`height`(int)、`cover_url`(string)、`thumbnail_url`(string)、`streams`(array[{codec, quality, url, backup_urls, width, height, duration, fps, format, size}])。
31
+
32
+ ## 请求示例
33
+
34
+ ```bash
35
+ curl -X POST "http://plugin.aiconductor.fun/api/xhs_user_notes_v3" \
36
+ -H "Authorization: Bearer your_api_key" \
37
+ -H "Content-Type: application/json" \
38
+ -d '{"user_id": "5f8e9c1b0000000001001a2b", "max_pages": 3}'
39
+ ```
40
+
41
+ ## 注意事项
42
+ 1. 按页计费:每成功获取一页计费一次,余额不足、超时或上游无更多数据自动停止翻页。
43
+ 2. 采用后扣费模式:仅成功获取并解析后才扣费,失败不扣费。
44
+ 3. 翻页由内部维护 `cursor`(取每页最后一条笔记的 cursor),调用方无需关心。
45
+ 4. 返回数据同时兼容图文(normal)与视频(video)笔记,视频笔记带 streams 流信息。
46
+ 5. 上游单页请求超时 55s,最多重试 3 次。
@@ -7,7 +7,8 @@ const {
7
7
  listCommand: webpageListCommand,
8
8
  deleteCommand: webpageDeleteCommand,
9
9
  } = require("../src/commands/md_publish");
10
- const { searchCommand } = require("../src/commands/web_search");
10
+ const { searchCommand } = require("../src/commands/web_search");
11
+ const { briefingCommand } = require("../src/commands/content");
11
12
  const {
12
13
  videoInfoCommand,
13
14
  videoCommentsCommand,
@@ -111,8 +112,8 @@ searchCmd
111
112
  searchCommand(query, "web", options);
112
113
  });
113
114
 
114
- searchCmd
115
- .command("image")
115
+ searchCmd
116
+ .command("image")
116
117
  .description("图片搜索")
117
118
  .argument("<query>", "搜索关键词(1~100字符)")
118
119
  .option("-k, --api-key <key>", "AIConductor API Key(也可通过 AICONDUCTOR_API_KEY 环境变量或 .env 文件提供)")
@@ -124,9 +125,25 @@ searchCmd
124
125
  .option("--image-shapes <shapes>", "图片形状,以 | 分隔:横长方形、竖长方形、方形")
125
126
  .option("--query-rewrite", "开启 Query 改写")
126
127
  .option("-f, --format <format>", "输出格式:json 或 text", "json")
127
- .action((query, options) => {
128
- searchCommand(query, "image", options);
129
- });
128
+ .action((query, options) => {
129
+ searchCommand(query, "image", options);
130
+ });
131
+
132
+ // content 子命令组
133
+ const contentCmd = program
134
+ .command("content")
135
+ .description("内容选题与素材工具");
136
+
137
+ contentCmd
138
+ .command("briefing")
139
+ .description("聚合多平台热点,生成选题原料")
140
+ .option("-k, --api-key <key>", "AIConductor API Key(也可通过 AICONDUCTOR_API_KEY 环境变量或 .env 文件提供)")
141
+ .option("--sources <ids>", "来源 ID,逗号分隔;默认 zhihu,weibo,douyin,aihot")
142
+ .option("--limit <n>", "每个来源返回条数,范围 1 到 30", "30")
143
+ .option("-f, --format <format>", "输出格式:json 或 text", "json")
144
+ .action((options) => {
145
+ briefingCommand(options);
146
+ });
130
147
 
131
148
  // 未知命令处理
132
149
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aiconductor-cli",
3
- "version": "1.3.4",
3
+ "version": "1.3.5",
4
4
  "description": "AIConductor CLI 工具箱",
5
5
  "keywords": [
6
6
  "aiconductor",
package/src/api.js CHANGED
@@ -1,4 +1,5 @@
1
- const https = require("https");
1
+ const http = require("http");
2
+ const https = require("https");
2
3
  const fs = require("fs");
3
4
  const path = require("path");
4
5
 
@@ -8,6 +9,7 @@ const MD_TO_HTML_API_PATH = "/api/md_to_html";
8
9
  const PUBLISHED_WEBPAGES_API_PATH = "/api/published_webpages";
9
10
  const DELETE_PUBLISHED_WEBPAGE_API_PATH = "/api/delete_published_webpage";
10
11
  const SEARCH_API_PATH = "/api/doubao_search";
12
+ const HOT_LIST_API_PATH = "/api/hot_list_mixed";
11
13
  const DOUYIN_VIDEO_INFO_PATH = "/api/douyin_video_info";
12
14
  const DOUYIN_VIDEO_COMMENTS_PATH = "/api/douyin_video_comments";
13
15
  const DOUYIN_USER_VIDEOS_PATH = "/api/douyin_user_videos";
@@ -75,14 +77,15 @@ function validateInt(value, name) {
75
77
  * @param {string} host
76
78
  * @param {string} path
77
79
  * @param {object} body
78
- * @returns {Promise<object>}
79
- */
80
- function httpPost(host, requestPath, body, headers = {}) {
80
+ * @param {typeof http|typeof https} [transport]
81
+ * @returns {Promise<object>}
82
+ */
83
+ function httpPost(host, requestPath, body, headers = {}, transport = https) {
81
84
  return new Promise((resolve, reject) => {
82
85
  const data = JSON.stringify(body);
83
86
  const options = {
84
87
  hostname: host,
85
- port: 443,
88
+ port: transport === http ? 80 : 443,
86
89
  path: requestPath,
87
90
  method: "POST",
88
91
  headers: {
@@ -92,7 +95,7 @@ function httpPost(host, requestPath, body, headers = {}) {
92
95
  },
93
96
  };
94
97
 
95
- const req = https.request(options, (res) => {
98
+ const req = transport.request(options, (res) => {
96
99
  let chunks = [];
97
100
  res.on("data", (chunk) => chunks.push(chunk));
98
101
  res.on("end", () => {
@@ -343,6 +346,40 @@ async function webSearch(apiKey, params) {
343
346
 
344
347
  return result;
345
348
  }
349
+
350
+ /**
351
+ * 聚合多个平台的热榜条目。
352
+ * @param {string} apiKey
353
+ * @param {{ sourceIds: string[]|string, limit?: number|string }} params
354
+ * @returns {Promise<object>}
355
+ */
356
+ async function hotList(apiKey, params) {
357
+ const inputSources = Array.isArray(params.sourceIds)
358
+ ? params.sourceIds
359
+ : String(params.sourceIds || "").split(",");
360
+ const sourceIds = [...new Set(inputSources.map((source) => String(source).trim()).filter(Boolean))];
361
+ if (sourceIds.length === 0) {
362
+ throw Object.assign(new Error("sourceIds 至少需要提供一个来源"), { type: "validation_error" });
363
+ }
364
+ if (sourceIds.length > 52) {
365
+ throw Object.assign(new Error("sourceIds 最多支持 52 个来源"), { type: "validation_error" });
366
+ }
367
+
368
+ const limit = validatePositiveInt(params.limit === undefined ? 30 : params.limit, "limit", 30);
369
+ // The hot-list service is currently served over HTTP; see api_docs/hot_list_mixed.md.
370
+ const resp = await httpPost(API_HOST, HOT_LIST_API_PATH, {
371
+ source_ids: sourceIds,
372
+ latest: true,
373
+ limit,
374
+ }, {
375
+ Authorization: `Bearer ${apiKey}`,
376
+ }, http);
377
+ const result = stripBilling(resp);
378
+ if (result?.success === false) {
379
+ throw Object.assign(new Error(result.message || "热榜接口返回失败"), { type: "api_error" });
380
+ }
381
+ return result;
382
+ }
346
383
 
347
384
  /**
348
385
  * Markdown 转 HTML 并发布
@@ -537,7 +574,8 @@ module.exports = {
537
574
  publishMdToHtml,
538
575
  getPublishedWebpages,
539
576
  deletePublishedWebpage,
540
- webSearch,
577
+ webSearch,
578
+ hotList,
541
579
  douyinVideoInfo,
542
580
  douyinVideoComments,
543
581
  douyinUserVideos,
package/src/api.test.js CHANGED
@@ -1,15 +1,17 @@
1
1
  const assert = require("node:assert/strict");
2
2
  const { EventEmitter } = require("node:events");
3
+ const http = require("node:http");
3
4
  const https = require("node:https");
4
5
  const test = require("node:test");
5
6
 
6
- const { webSearch } = require("./api");
7
+ const { hotList, webSearch } = require("./api");
8
+ const { formatHotListText, parseSourceIds, DEFAULT_HOT_LIST_SOURCES } = require("./commands/content");
7
9
  const { formatSearchText } = require("./commands/web_search");
8
10
 
9
- function captureRequest(callback, responseBody = { success: true, billing: {} }) {
10
- const originalRequest = https.request;
11
+ function captureRequest(callback, responseBody = { success: true, billing: {} }, transport = https) {
12
+ const originalRequest = transport.request;
11
13
  let captured;
12
- https.request = (options, responseCallback) => {
14
+ transport.request = (options, responseCallback) => {
13
15
  const request = new EventEmitter();
14
16
  const chunks = [];
15
17
  request.write = (chunk) => chunks.push(Buffer.from(chunk));
@@ -25,7 +27,7 @@ function captureRequest(callback, responseBody = { success: true, billing: {} })
25
27
  };
26
28
 
27
29
  return callback().then(() => captured).finally(() => {
28
- https.request = originalRequest;
30
+ transport.request = originalRequest;
29
31
  });
30
32
  }
31
33
 
@@ -155,3 +157,63 @@ test("webSearch raises nested API failures", async () => {
155
157
  { type: "api_error", message: "image 搜索的 count 必须为 1~5" }
156
158
  );
157
159
  });
160
+
161
+ test("hotList sends deduplicated sources with current defaults", async () => {
162
+ const request = await captureRequest(
163
+ () => hotList("test-key", { sourceIds: ["zhihu", "weibo", "zhihu", "aihot"] }),
164
+ undefined,
165
+ http
166
+ );
167
+
168
+ assert.equal(request.options.path, "/api/hot_list_mixed");
169
+ assert.equal(request.options.port, 80);
170
+ assert.equal(request.options.headers.Authorization, "Bearer test-key");
171
+ assert.deepEqual(request.body, {
172
+ source_ids: ["zhihu", "weibo", "aihot"],
173
+ latest: true,
174
+ limit: 30,
175
+ });
176
+ assert.deepEqual(parseSourceIds(), DEFAULT_HOT_LIST_SOURCES);
177
+ assert.deepEqual(parseSourceIds("zhihu, weibo,zhihu"), ["zhihu", "weibo"]);
178
+ });
179
+
180
+ test("hotList validates source IDs and limits", async () => {
181
+ await assert.rejects(hotList("test-key", { sourceIds: [] }), { type: "validation_error" });
182
+ await assert.rejects(
183
+ hotList("test-key", { sourceIds: Array.from({ length: 53 }, (_, index) => `source-${index}`) }),
184
+ { type: "validation_error" }
185
+ );
186
+ await assert.rejects(hotList("test-key", { sourceIds: ["zhihu"], limit: 0 }), { type: "validation_error" });
187
+ await assert.rejects(hotList("test-key", { sourceIds: ["zhihu"], limit: 31 }), { type: "validation_error" });
188
+ });
189
+
190
+ test("formatHotListText keeps successful sources and reports failures", () => {
191
+ const text = formatHotListText({
192
+ data: {
193
+ sources: [{
194
+ source_id: "zhihu",
195
+ status: "cache",
196
+ updated_time: 0,
197
+ items: [{ title: "AI 应用趋势", url: "https://example.com/topic", pub_date: "今天" }],
198
+ }],
199
+ failed_sources: [{ source_id: "weibo", message: "上游暂不可用" }],
200
+ },
201
+ });
202
+
203
+ assert.match(text, /【zhihu】缓存/);
204
+ assert.match(text, /AI 应用趋势/);
205
+ assert.match(text, /URL: https:\/\/example\.com\/topic/);
206
+ assert.match(text, /失败来源:/);
207
+ assert.match(text, /weibo: 上游暂不可用/);
208
+ });
209
+
210
+ test("hotList raises API failures", async () => {
211
+ await assert.rejects(
212
+ captureRequest(
213
+ () => hotList("test-key", { sourceIds: ["zhihu"] }),
214
+ { success: false, message: "没有可用热榜来源" },
215
+ http
216
+ ),
217
+ { type: "api_error", message: "没有可用热榜来源" }
218
+ );
219
+ });
@@ -0,0 +1,90 @@
1
+ const { hotList } = require("../api");
2
+ const { outputResult, outputError, resolveKeyOrExit, handleApiError } = require("../lib/cli-utils");
3
+
4
+ const DEFAULT_HOT_LIST_SOURCES = ["zhihu", "weibo", "douyin", "aihot"];
5
+
6
+ /**
7
+ * 解析并去重逗号分隔的平台来源 ID。
8
+ * @param {string|undefined} value
9
+ * @returns {string[]}
10
+ */
11
+ function parseSourceIds(value) {
12
+ const rawSources = value === undefined ? DEFAULT_HOT_LIST_SOURCES : String(value).split(",");
13
+ const sourceIds = [...new Set(rawSources.map((source) => source.trim()).filter(Boolean))];
14
+ if (sourceIds.length === 0) {
15
+ throw Object.assign(new Error("--sources 至少需要提供一个来源 ID"), { type: "validation_error" });
16
+ }
17
+ if (sourceIds.length > 52) {
18
+ throw Object.assign(new Error("--sources 最多支持 52 个来源"), { type: "validation_error" });
19
+ }
20
+ return sourceIds;
21
+ }
22
+
23
+ function formatTimestamp(value) {
24
+ if (typeof value !== "number" || !Number.isFinite(value)) return value || "";
25
+ return new Date(value).toISOString();
26
+ }
27
+
28
+ /**
29
+ * text 模式的热点简报格式化。
30
+ * @param {object} result
31
+ * @returns {string}
32
+ */
33
+ function formatHotListText(result = {}) {
34
+ const data = result.data || {};
35
+ const sources = data.sources || [];
36
+ const failedSources = data.failed_sources || [];
37
+ const lines = ["热点选题简报"];
38
+
39
+ if (!sources.length) {
40
+ lines.push("没有获取到可用热点来源。");
41
+ }
42
+
43
+ sources.forEach((source) => {
44
+ const status = source.status === "cache" ? "缓存" : source.status || "未知状态";
45
+ lines.push(`\n【${source.source_id}】${status}`);
46
+ if (source.updated_time) lines.push(`更新时间: ${formatTimestamp(source.updated_time)}`);
47
+ (source.items || []).forEach((item, index) => {
48
+ lines.push(`[${index + 1}] ${item.title || "未命名条目"}`);
49
+ if (item.url) lines.push(` URL: ${item.url}`);
50
+ if (item.pub_date) lines.push(` 发布时间: ${item.pub_date}`);
51
+ });
52
+ });
53
+
54
+ if (failedSources.length) {
55
+ lines.push("\n失败来源:");
56
+ failedSources.forEach((source) => {
57
+ lines.push(`- ${source.source_id}: ${source.message || "未知错误"}`);
58
+ });
59
+ }
60
+
61
+ return lines.join("\n").trimEnd();
62
+ }
63
+
64
+ /**
65
+ * content briefing 命令。
66
+ * @param {{ apiKey?: string, sources?: string, limit?: string, format?: "json"|"text" }} options
67
+ * @returns {Promise<void>}
68
+ */
69
+ async function briefingCommand(options = {}) {
70
+ const format = options.format || "json";
71
+ const apiKey = resolveKeyOrExit(options.apiKey, format);
72
+
73
+ try {
74
+ const result = await hotList(apiKey, {
75
+ sourceIds: parseSourceIds(options.sources),
76
+ limit: options.limit === undefined ? 30 : options.limit,
77
+ });
78
+ outputResult(result, format, formatHotListText);
79
+ process.exit(0);
80
+ } catch (err) {
81
+ if (err.type === "validation_error") {
82
+ outputError(err, format);
83
+ } else {
84
+ handleApiError(err, format);
85
+ }
86
+ process.exit(1);
87
+ }
88
+ }
89
+
90
+ module.exports = { briefingCommand, formatHotListText, parseSourceIds, DEFAULT_HOT_LIST_SOURCES };