aiconductor-cli 1.3.5 → 1.4.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/README.md +24 -3
- package/bin/aiconductor-cli.js +14 -7
- package/package.json +1 -1
- package/src/api.js +18 -0
- package/src/api.test.js +152 -3
- package/src/commands/content.js +99 -5
- package/src/commands/xhs.js +268 -225
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# AIConductor CLI
|
|
2
2
|
|
|
3
|
-
AIConductor
|
|
3
|
+
AIConductor 平台命令行工具箱,提供内容选题简报、网页发布、搜索、抖音数据和小红书工具。
|
|
4
4
|
|
|
5
5
|
## 安装
|
|
6
6
|
|
|
@@ -159,7 +159,7 @@ aiconductor-cli search image "长城风景" -n 3 --image-width-min 1200 --image-
|
|
|
159
159
|
|
|
160
160
|
### 热点选题简报
|
|
161
161
|
|
|
162
|
-
|
|
162
|
+
聚合多个平台热点与小红书实时热榜,为内容选题提供带来源链接的原料。默认查询知乎、微博、抖音和 AIHOT,每个来源返回 30 条,并额外请求一次小红书热榜。
|
|
163
163
|
|
|
164
164
|
```bash
|
|
165
165
|
aiconductor-cli content briefing [选项]
|
|
@@ -181,7 +181,14 @@ aiconductor-cli content briefing -f text
|
|
|
181
181
|
aiconductor-cli content briefing --sources aihot,hackernews,producthunt,juejin --limit 15 -f text
|
|
182
182
|
```
|
|
183
183
|
|
|
184
|
-
默认 JSON
|
|
184
|
+
默认 JSON 输出保留以下字段,便于后续选题处理:
|
|
185
|
+
|
|
186
|
+
- `data.data.sources`:成功的综合热榜来源,兼容旧版本。
|
|
187
|
+
- `data.data.failed_sources`:综合热榜中失败的平台来源。
|
|
188
|
+
- `data.data.xhs_hot_list`:小红书热榜结果,包含日期与话题条目;小红书热榜接口失败时为 `null`。
|
|
189
|
+
- `data.data.failed_services`:综合热榜或小红书热榜接口级失败信息。
|
|
190
|
+
|
|
191
|
+
任一服务成功时命令仍会返回可用数据;只有两个服务都失败时才退出失败。成功来源和成功的小红书热榜调用均按平台现有规则计费。
|
|
185
192
|
|
|
186
193
|
### 抖音数据工具箱
|
|
187
194
|
|
|
@@ -295,6 +302,19 @@ aiconductor-cli douyin link-to-text <视频URL> [选项]
|
|
|
295
302
|
aiconductor-cli douyin link-to-text "https://www.douyin.com/video/xxxxx"
|
|
296
303
|
```
|
|
297
304
|
|
|
305
|
+
### 小红书数据工具
|
|
306
|
+
|
|
307
|
+
获取小红书实时热榜:
|
|
308
|
+
|
|
309
|
+
```bash
|
|
310
|
+
aiconductor-cli xhs hot-list [选项]
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
示例:
|
|
314
|
+
```bash
|
|
315
|
+
aiconductor-cli xhs hot-list -f text
|
|
316
|
+
```
|
|
317
|
+
|
|
298
318
|
### 小红书扫码发布
|
|
299
319
|
|
|
300
320
|
生成图文或视频笔记的 H5 发布页与二维码。素材支持公网 URL、本地文件、Base64/data URL,以及包含 Base64 内容的文本文件。
|
|
@@ -353,6 +373,7 @@ aiconductor-cli xhs note-publish video \
|
|
|
353
373
|
| 抖音用户搜索 | 0.15 元/页 |
|
|
354
374
|
| 抖音视频搜索 | 0.15 元/页 |
|
|
355
375
|
| 抖音链接转文字 | 0.01 元/次 + 0.0003 元/秒 |
|
|
376
|
+
| 小红书热榜 | 以平台服务配置为准 |
|
|
356
377
|
| 小红书扫码发布 | 0.05 元/次 |
|
|
357
378
|
|
|
358
379
|
> 搜索/调用失败不扣费,仅成功时计费。
|
package/bin/aiconductor-cli.js
CHANGED
|
@@ -18,7 +18,7 @@ const {
|
|
|
18
18
|
videoSearchCommand,
|
|
19
19
|
linkToTextCommand,
|
|
20
20
|
} = require("../src/commands/douyin");
|
|
21
|
-
const { notePublishCommand } = require("../src/commands/xhs");
|
|
21
|
+
const { hotListCommand: xhsHotListCommand, notePublishCommand } = require("../src/commands/xhs");
|
|
22
22
|
|
|
23
23
|
const program = new Command();
|
|
24
24
|
|
|
@@ -136,7 +136,7 @@ const contentCmd = program
|
|
|
136
136
|
|
|
137
137
|
contentCmd
|
|
138
138
|
.command("briefing")
|
|
139
|
-
.description("
|
|
139
|
+
.description("聚合多平台热点与小红书热榜,生成选题原料")
|
|
140
140
|
.option("-k, --api-key <key>", "AIConductor API Key(也可通过 AICONDUCTOR_API_KEY 环境变量或 .env 文件提供)")
|
|
141
141
|
.option("--sources <ids>", "来源 ID,逗号分隔;默认 zhihu,weibo,douyin,aihot")
|
|
142
142
|
.option("--limit <n>", "每个来源返回条数,范围 1 到 30", "30")
|
|
@@ -222,11 +222,18 @@ douyinCmd
|
|
|
222
222
|
.action((videoUrl, options) => { linkToTextCommand(videoUrl, options); });
|
|
223
223
|
|
|
224
224
|
// xhs 子命令组
|
|
225
|
-
const xhsCmd = program
|
|
226
|
-
.command("xhs")
|
|
227
|
-
.description("小红书数据工具箱");
|
|
228
|
-
|
|
229
|
-
xhsCmd
|
|
225
|
+
const xhsCmd = program
|
|
226
|
+
.command("xhs")
|
|
227
|
+
.description("小红书数据工具箱");
|
|
228
|
+
|
|
229
|
+
xhsCmd
|
|
230
|
+
.command("hot-list")
|
|
231
|
+
.description("获取小红书实时热榜")
|
|
232
|
+
.option("-k, --api-key <key>", "AIConductor API Key")
|
|
233
|
+
.option("-f, --format <format>", "输出格式:json 或 text", "json")
|
|
234
|
+
.action((options) => { xhsHotListCommand(options); });
|
|
235
|
+
|
|
236
|
+
xhsCmd
|
|
230
237
|
.command("note-publish")
|
|
231
238
|
.description("生成小红书扫码发布页与二维码(0.05元/次)")
|
|
232
239
|
.argument("<type>", "笔记类型:normal(图文)或 video(视频)")
|
package/package.json
CHANGED
package/src/api.js
CHANGED
|
@@ -10,6 +10,7 @@ const PUBLISHED_WEBPAGES_API_PATH = "/api/published_webpages";
|
|
|
10
10
|
const DELETE_PUBLISHED_WEBPAGE_API_PATH = "/api/delete_published_webpage";
|
|
11
11
|
const SEARCH_API_PATH = "/api/doubao_search";
|
|
12
12
|
const HOT_LIST_API_PATH = "/api/hot_list_mixed";
|
|
13
|
+
const XHS_HOT_LIST_API_PATH = "/api/xhs_hot_list";
|
|
13
14
|
const DOUYIN_VIDEO_INFO_PATH = "/api/douyin_video_info";
|
|
14
15
|
const DOUYIN_VIDEO_COMMENTS_PATH = "/api/douyin_video_comments";
|
|
15
16
|
const DOUYIN_USER_VIDEOS_PATH = "/api/douyin_user_videos";
|
|
@@ -380,6 +381,22 @@ async function hotList(apiKey, params) {
|
|
|
380
381
|
}
|
|
381
382
|
return result;
|
|
382
383
|
}
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* 获取小红书实时热榜。
|
|
387
|
+
* @param {string} apiKey
|
|
388
|
+
* @returns {Promise<object>}
|
|
389
|
+
*/
|
|
390
|
+
async function xhsHotList(apiKey) {
|
|
391
|
+
const resp = await httpPost(API_HOST, XHS_HOT_LIST_API_PATH, {}, {
|
|
392
|
+
Authorization: `Bearer ${apiKey}`,
|
|
393
|
+
}, http);
|
|
394
|
+
const result = stripBilling(resp);
|
|
395
|
+
if (result?.success === false) {
|
|
396
|
+
throw Object.assign(new Error(result.message || "小红书热榜接口返回失败"), { type: "api_error" });
|
|
397
|
+
}
|
|
398
|
+
return result;
|
|
399
|
+
}
|
|
383
400
|
|
|
384
401
|
/**
|
|
385
402
|
* Markdown 转 HTML 并发布
|
|
@@ -576,6 +593,7 @@ module.exports = {
|
|
|
576
593
|
deletePublishedWebpage,
|
|
577
594
|
webSearch,
|
|
578
595
|
hotList,
|
|
596
|
+
xhsHotList,
|
|
579
597
|
douyinVideoInfo,
|
|
580
598
|
douyinVideoComments,
|
|
581
599
|
douyinUserVideos,
|
package/src/api.test.js
CHANGED
|
@@ -4,9 +4,16 @@ const http = require("node:http");
|
|
|
4
4
|
const https = require("node:https");
|
|
5
5
|
const test = require("node:test");
|
|
6
6
|
|
|
7
|
-
const { hotList, webSearch } = require("./api");
|
|
8
|
-
const {
|
|
7
|
+
const { hotList, webSearch, xhsHotList } = require("./api");
|
|
8
|
+
const {
|
|
9
|
+
buildBriefingResult,
|
|
10
|
+
formatHotListText,
|
|
11
|
+
parseLimit,
|
|
12
|
+
parseSourceIds,
|
|
13
|
+
DEFAULT_HOT_LIST_SOURCES,
|
|
14
|
+
} = require("./commands/content");
|
|
9
15
|
const { formatSearchText } = require("./commands/web_search");
|
|
16
|
+
const { hotListTextFormatter, notePublishTextFormatter } = require("./commands/xhs");
|
|
10
17
|
|
|
11
18
|
function captureRequest(callback, responseBody = { success: true, billing: {} }, transport = https) {
|
|
12
19
|
const originalRequest = transport.request;
|
|
@@ -26,7 +33,10 @@ function captureRequest(callback, responseBody = { success: true, billing: {} },
|
|
|
26
33
|
return request;
|
|
27
34
|
};
|
|
28
35
|
|
|
29
|
-
return callback().then(() =>
|
|
36
|
+
return callback().then((result) => {
|
|
37
|
+
captured.result = result;
|
|
38
|
+
return captured;
|
|
39
|
+
}).finally(() => {
|
|
30
40
|
transport.request = originalRequest;
|
|
31
41
|
});
|
|
32
42
|
}
|
|
@@ -217,3 +227,142 @@ test("hotList raises API failures", async () => {
|
|
|
217
227
|
{ type: "api_error", message: "没有可用热榜来源" }
|
|
218
228
|
);
|
|
219
229
|
});
|
|
230
|
+
|
|
231
|
+
test("xhsHotList calls the XHS hot-list endpoint over HTTP", async () => {
|
|
232
|
+
const responseBody = {
|
|
233
|
+
success: true,
|
|
234
|
+
data: {
|
|
235
|
+
date: "2026-07-29",
|
|
236
|
+
items: [{ rank: 1, title: "夏日穿搭", hot: "123万", trend: "up", url: "https://example.com/xhs" }],
|
|
237
|
+
},
|
|
238
|
+
billing: { service_fee: 0.01 },
|
|
239
|
+
};
|
|
240
|
+
const request = await captureRequest(() => xhsHotList("test-key"), responseBody, http);
|
|
241
|
+
|
|
242
|
+
assert.equal(request.options.path, "/api/xhs_hot_list");
|
|
243
|
+
assert.equal(request.options.port, 80);
|
|
244
|
+
assert.equal(request.options.headers.Authorization, "Bearer test-key");
|
|
245
|
+
assert.deepEqual(request.body, {});
|
|
246
|
+
assert.equal(request.result.billing, undefined);
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
test("xhsHotList raises API failures", async () => {
|
|
250
|
+
await assert.rejects(
|
|
251
|
+
captureRequest(
|
|
252
|
+
() => xhsHotList("test-key"),
|
|
253
|
+
{ success: false, message: "小红书热榜暂不可用" },
|
|
254
|
+
http
|
|
255
|
+
),
|
|
256
|
+
{ type: "api_error", message: "小红书热榜暂不可用" }
|
|
257
|
+
);
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
test("buildBriefingResult preserves hot-list fields and adds XHS data", () => {
|
|
261
|
+
const result = buildBriefingResult(
|
|
262
|
+
{
|
|
263
|
+
status: "fulfilled",
|
|
264
|
+
value: {
|
|
265
|
+
success: true,
|
|
266
|
+
data: {
|
|
267
|
+
sources: [{ source_id: "zhihu", items: [{ title: "AI 应用趋势" }] }],
|
|
268
|
+
failed_sources: [{ source_id: "weibo", message: "上游暂不可用" }],
|
|
269
|
+
},
|
|
270
|
+
},
|
|
271
|
+
},
|
|
272
|
+
{
|
|
273
|
+
status: "fulfilled",
|
|
274
|
+
value: {
|
|
275
|
+
success: true,
|
|
276
|
+
data: { date: "2026-07-29", items: [{ rank: 1, title: "AI 工具" }] },
|
|
277
|
+
},
|
|
278
|
+
}
|
|
279
|
+
);
|
|
280
|
+
|
|
281
|
+
assert.equal(result.success, true);
|
|
282
|
+
assert.equal(result.data.sources[0].source_id, "zhihu");
|
|
283
|
+
assert.equal(result.data.failed_sources[0].source_id, "weibo");
|
|
284
|
+
assert.equal(result.data.xhs_hot_list.items[0].title, "AI 工具");
|
|
285
|
+
assert.deepEqual(result.data.failed_services, []);
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
test("buildBriefingResult keeps either service when the other one fails", () => {
|
|
289
|
+
const hotListOnly = buildBriefingResult(
|
|
290
|
+
{ status: "fulfilled", value: { success: true, data: { sources: [], failed_sources: [] } } },
|
|
291
|
+
{ status: "rejected", reason: Object.assign(new Error("小红书超时"), { type: "network_error" }) }
|
|
292
|
+
);
|
|
293
|
+
assert.equal(hotListOnly.data.xhs_hot_list, null);
|
|
294
|
+
assert.deepEqual(hotListOnly.data.failed_services, [{ service: "xhs_hot_list", message: "小红书超时" }]);
|
|
295
|
+
|
|
296
|
+
const xhsOnly = buildBriefingResult(
|
|
297
|
+
{ status: "rejected", reason: Object.assign(new Error("热榜超时"), { type: "network_error" }) },
|
|
298
|
+
{ status: "fulfilled", value: { success: true, data: { date: "2026-07-29", items: [] } } }
|
|
299
|
+
);
|
|
300
|
+
assert.deepEqual(xhsOnly.data.sources, []);
|
|
301
|
+
assert.equal(xhsOnly.data.xhs_hot_list.date, "2026-07-29");
|
|
302
|
+
assert.deepEqual(xhsOnly.data.failed_services, [{ service: "hot_list", message: "热榜超时" }]);
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
test("buildBriefingResult fails only when both services fail", () => {
|
|
306
|
+
assert.throws(
|
|
307
|
+
() => buildBriefingResult(
|
|
308
|
+
{ status: "rejected", reason: new Error("热榜超时") },
|
|
309
|
+
{ status: "rejected", reason: new Error("小红书超时") }
|
|
310
|
+
),
|
|
311
|
+
{ type: "api_error", message: /均获取失败/ }
|
|
312
|
+
);
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
test("hot-list formatters render XHS trends and service failures", () => {
|
|
316
|
+
const briefingText = formatHotListText({
|
|
317
|
+
data: {
|
|
318
|
+
sources: [],
|
|
319
|
+
xhs_hot_list: {
|
|
320
|
+
date: "2026-07-29",
|
|
321
|
+
items: [{ rank: 1, title: "夏日穿搭", hot: "123万", trend: "up", url: "https://example.com/xhs" }],
|
|
322
|
+
},
|
|
323
|
+
failed_services: [{ service: "hot_list", message: "上游暂不可用" }],
|
|
324
|
+
},
|
|
325
|
+
});
|
|
326
|
+
assert.match(briefingText, /【小红书热榜】2026-07-29/);
|
|
327
|
+
assert.match(briefingText, /\[1\] 夏日穿搭 up/);
|
|
328
|
+
assert.match(briefingText, /服务失败:/);
|
|
329
|
+
|
|
330
|
+
const xhsText = hotListTextFormatter({
|
|
331
|
+
data: { date: "2026-07-29", items: [{ rank: 2, title: "露营", hot_value: 88, trend: "new" }] },
|
|
332
|
+
});
|
|
333
|
+
assert.match(xhsText, /小红书热榜/);
|
|
334
|
+
assert.match(xhsText, /\[2\] 露营 new/);
|
|
335
|
+
assert.match(xhsText, /热度: 88/);
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
test("note-publish formatter surfaces the shareable qrcode image URL", () => {
|
|
339
|
+
const text = notePublishTextFormatter({
|
|
340
|
+
message: "发布页生成成功",
|
|
341
|
+
data: {
|
|
342
|
+
id: "note-1",
|
|
343
|
+
url: "https://oss.aiconductor.fun/temporary_7/xhs_note_publish/note-1/index.html",
|
|
344
|
+
qrcode: "data:image/png;base64,AAAA",
|
|
345
|
+
qrcode_url: "https://oss.aiconductor.fun/temporary_7/xhs_note_publish/note-1/qrcode.png",
|
|
346
|
+
},
|
|
347
|
+
});
|
|
348
|
+
assert.match(text, /H5 地址:/);
|
|
349
|
+
assert.match(text, /二维码图: https:\/\/oss\.aiconductor\.fun\/temporary_7\/xhs_note_publish\/note-1\/qrcode\.png/);
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
test("note-publish formatter tolerates responses without qrcode_url", () => {
|
|
353
|
+
// 旧版后端只返回 base64 二维码,formatter 不能因此报错
|
|
354
|
+
const text = notePublishTextFormatter({
|
|
355
|
+
data: { id: "note-2", url: "https://example.com/index.html" },
|
|
356
|
+
});
|
|
357
|
+
assert.match(text, /H5 地址:/);
|
|
358
|
+
assert.doesNotMatch(text, /二维码图/);
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
test("parseLimit accepts the documented range", () => {
|
|
362
|
+
assert.equal(parseLimit(), 30);
|
|
363
|
+
assert.equal(parseLimit("1"), 1);
|
|
364
|
+
assert.equal(parseLimit(30), 30);
|
|
365
|
+
assert.throws(() => parseLimit("0"), { type: "validation_error" });
|
|
366
|
+
assert.throws(() => parseLimit("31"), { type: "validation_error" });
|
|
367
|
+
assert.throws(() => parseLimit("invalid"), { type: "validation_error" });
|
|
368
|
+
});
|
package/src/commands/content.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const { hotList } = require("../api");
|
|
1
|
+
const { hotList, xhsHotList } = require("../api");
|
|
2
2
|
const { outputResult, outputError, resolveKeyOrExit, handleApiError } = require("../lib/cli-utils");
|
|
3
3
|
|
|
4
4
|
const DEFAULT_HOT_LIST_SOURCES = ["zhihu", "weibo", "douyin", "aihot"];
|
|
@@ -20,11 +20,71 @@ function parseSourceIds(value) {
|
|
|
20
20
|
return sourceIds;
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
+
/**
|
|
24
|
+
* 解析每个热榜来源的返回条数。
|
|
25
|
+
* @param {string|number|undefined} value
|
|
26
|
+
* @returns {number}
|
|
27
|
+
*/
|
|
28
|
+
function parseLimit(value) {
|
|
29
|
+
const limit = Number(value === undefined ? 30 : value);
|
|
30
|
+
if (!Number.isInteger(limit) || limit < 1 || limit > 30) {
|
|
31
|
+
throw Object.assign(new Error(`--limit 须为 1 到 30 的整数,收到: ${value}`), {
|
|
32
|
+
type: "validation_error",
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
return limit;
|
|
36
|
+
}
|
|
37
|
+
|
|
23
38
|
function formatTimestamp(value) {
|
|
24
39
|
if (typeof value !== "number" || !Number.isFinite(value)) return value || "";
|
|
25
40
|
return new Date(value).toISOString();
|
|
26
41
|
}
|
|
27
42
|
|
|
43
|
+
function errorMessage(reason) {
|
|
44
|
+
if (reason?.body?.message) return reason.body.message;
|
|
45
|
+
return reason?.message || String(reason || "未知错误");
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* 将热榜和小红书热榜的并行结果组合为兼容既有字段的简报响应。
|
|
50
|
+
* @param {PromiseSettledResult<object>} hotListResult
|
|
51
|
+
* @param {PromiseSettledResult<object>} xhsHotListResult
|
|
52
|
+
* @returns {object}
|
|
53
|
+
*/
|
|
54
|
+
function buildBriefingResult(hotListResult, xhsHotListResult) {
|
|
55
|
+
const hotListSucceeded = hotListResult.status === "fulfilled";
|
|
56
|
+
const xhsHotListSucceeded = xhsHotListResult.status === "fulfilled";
|
|
57
|
+
|
|
58
|
+
if (!hotListSucceeded && !xhsHotListSucceeded) {
|
|
59
|
+
throw Object.assign(
|
|
60
|
+
new Error(`热点与小红书热榜均获取失败: ${errorMessage(hotListResult.reason)}; ${errorMessage(xhsHotListResult.reason)}`),
|
|
61
|
+
{ type: "api_error" }
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const failedServices = [];
|
|
66
|
+
if (!hotListSucceeded) {
|
|
67
|
+
failedServices.push({ service: "hot_list", message: errorMessage(hotListResult.reason) });
|
|
68
|
+
}
|
|
69
|
+
if (!xhsHotListSucceeded) {
|
|
70
|
+
failedServices.push({ service: "xhs_hot_list", message: errorMessage(xhsHotListResult.reason) });
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const hotListData = hotListSucceeded
|
|
74
|
+
? hotListResult.value.data || {}
|
|
75
|
+
: { sources: [], failed_sources: [] };
|
|
76
|
+
const xhsHotListData = xhsHotListSucceeded ? xhsHotListResult.value.data || {} : null;
|
|
77
|
+
return {
|
|
78
|
+
success: true,
|
|
79
|
+
message: failedServices.length ? "热点简报已生成,部分服务不可用" : "热点简报已生成",
|
|
80
|
+
data: {
|
|
81
|
+
...hotListData,
|
|
82
|
+
xhs_hot_list: xhsHotListData,
|
|
83
|
+
failed_services: failedServices,
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
28
88
|
/**
|
|
29
89
|
* text 模式的热点简报格式化。
|
|
30
90
|
* @param {object} result
|
|
@@ -58,6 +118,28 @@ function formatHotListText(result = {}) {
|
|
|
58
118
|
});
|
|
59
119
|
}
|
|
60
120
|
|
|
121
|
+
const xhsHotList = data.xhs_hot_list;
|
|
122
|
+
if (xhsHotList) {
|
|
123
|
+
lines.push(`\n【小红书热榜】${xhsHotList.date || ""}`.trimEnd());
|
|
124
|
+
(xhsHotList.items || []).forEach((item) => {
|
|
125
|
+
const rank = item.rank === undefined ? "-" : item.rank;
|
|
126
|
+
const trend = item.trend ? ` ${item.trend}` : "";
|
|
127
|
+
lines.push(`[${rank}] ${item.title || "未命名话题"}${trend}`);
|
|
128
|
+
if (item.hot || item.hot_value !== undefined) {
|
|
129
|
+
lines.push(` 热度: ${item.hot || item.hot_value}`);
|
|
130
|
+
}
|
|
131
|
+
if (item.url) lines.push(` URL: ${item.url}`);
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const failedServices = data.failed_services || [];
|
|
136
|
+
if (failedServices.length) {
|
|
137
|
+
lines.push("\n服务失败:");
|
|
138
|
+
failedServices.forEach((service) => {
|
|
139
|
+
lines.push(`- ${service.service || "未知服务"}: ${service.message || "未知错误"}`);
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
|
|
61
143
|
return lines.join("\n").trimEnd();
|
|
62
144
|
}
|
|
63
145
|
|
|
@@ -71,10 +153,15 @@ async function briefingCommand(options = {}) {
|
|
|
71
153
|
const apiKey = resolveKeyOrExit(options.apiKey, format);
|
|
72
154
|
|
|
73
155
|
try {
|
|
74
|
-
const
|
|
156
|
+
const hotListParams = {
|
|
75
157
|
sourceIds: parseSourceIds(options.sources),
|
|
76
|
-
limit: options.limit
|
|
77
|
-
}
|
|
158
|
+
limit: parseLimit(options.limit),
|
|
159
|
+
};
|
|
160
|
+
const [hotListResult, xhsHotListResult] = await Promise.allSettled([
|
|
161
|
+
hotList(apiKey, hotListParams),
|
|
162
|
+
xhsHotList(apiKey),
|
|
163
|
+
]);
|
|
164
|
+
const result = buildBriefingResult(hotListResult, xhsHotListResult);
|
|
78
165
|
outputResult(result, format, formatHotListText);
|
|
79
166
|
process.exit(0);
|
|
80
167
|
} catch (err) {
|
|
@@ -87,4 +174,11 @@ async function briefingCommand(options = {}) {
|
|
|
87
174
|
}
|
|
88
175
|
}
|
|
89
176
|
|
|
90
|
-
module.exports = {
|
|
177
|
+
module.exports = {
|
|
178
|
+
briefingCommand,
|
|
179
|
+
buildBriefingResult,
|
|
180
|
+
formatHotListText,
|
|
181
|
+
parseLimit,
|
|
182
|
+
parseSourceIds,
|
|
183
|
+
DEFAULT_HOT_LIST_SOURCES,
|
|
184
|
+
};
|
package/src/commands/xhs.js
CHANGED
|
@@ -1,225 +1,268 @@
|
|
|
1
|
-
const fs = require("fs");
|
|
2
|
-
const path = require("path");
|
|
3
|
-
const { r2Upload, xhsNotePublish } = require("../api");
|
|
4
|
-
const { outputResult, resolveKeyOrExit, handleApiError } = require("../lib/cli-utils");
|
|
5
|
-
|
|
6
|
-
const MAX_IMAGE_SIZE = 32 * 1024 * 1024;
|
|
7
|
-
const MAX_UPLOAD_SIZE = 100 * 1024 * 1024;
|
|
8
|
-
|
|
9
|
-
function validationError(message) {
|
|
10
|
-
return Object.assign(new Error(message), { type: "validation_error" });
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
function resolveBase64Input(value, label) {
|
|
14
|
-
if (!value.startsWith("@")) return value;
|
|
15
|
-
const filePath = path.resolve(value.slice(1));
|
|
16
|
-
let content;
|
|
17
|
-
try {
|
|
18
|
-
content = fs.readFileSync(filePath, "utf-8").trim();
|
|
19
|
-
} catch (err) {
|
|
20
|
-
throw validationError(`${label} Base64 文件读取失败 ${filePath}: ${err.message}`);
|
|
21
|
-
}
|
|
22
|
-
if (!content) throw validationError(`${label} Base64 文件内容为空: ${filePath}`);
|
|
23
|
-
return content;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
function validateLocalFile(filePath, label, maxSize) {
|
|
27
|
-
const absolutePath = path.resolve(filePath);
|
|
28
|
-
let stat;
|
|
29
|
-
try {
|
|
30
|
-
stat = fs.statSync(absolutePath);
|
|
31
|
-
} catch (err) {
|
|
32
|
-
throw validationError(`${label}文件读取失败 ${absolutePath}: ${err.message}`);
|
|
33
|
-
}
|
|
34
|
-
if (!stat.isFile()) throw validationError(`${label}路径不是文件: ${absolutePath}`);
|
|
35
|
-
if (stat.size > maxSize) {
|
|
36
|
-
throw validationError(`${label}文件超过 ${maxSize / 1024 / 1024}MB: ${absolutePath}`);
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
function validateUrl(value, label) {
|
|
41
|
-
let parsed;
|
|
42
|
-
try {
|
|
43
|
-
parsed = new URL(value);
|
|
44
|
-
} catch (_) {
|
|
45
|
-
throw validationError(`${label}不是有效 URL: ${value}`);
|
|
46
|
-
}
|
|
47
|
-
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
48
|
-
throw validationError(`${label}仅支持 HTTP/HTTPS URL: ${value}`);
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
function validateBase64Size(value, label, maxSize) {
|
|
53
|
-
const dataUrlPrefix = value.match(/^data:[^,]*,/i)?.[0];
|
|
54
|
-
const payload = (dataUrlPrefix ? value.slice(dataUrlPrefix.length) : value).replace(/\s/g, "");
|
|
55
|
-
const padding = payload.endsWith("==") ? 2 : payload.endsWith("=") ? 1 : 0;
|
|
56
|
-
const size = Math.max(0, Math.floor(payload.length * 3 / 4) - padding);
|
|
57
|
-
if (size > maxSize) {
|
|
58
|
-
throw validationError(`${label} Base64 内容超过 ${maxSize / 1024 / 1024}MB`);
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
async function uploadMedia(apiKey, params, label) {
|
|
63
|
-
try {
|
|
64
|
-
const result = await r2Upload(apiKey, params);
|
|
65
|
-
return result.file_url;
|
|
66
|
-
} catch (err) {
|
|
67
|
-
const detail = err.type === "http_error"
|
|
68
|
-
? `HTTP ${err.statusCode}: ${err.body?.message || JSON.stringify(err.body)}`
|
|
69
|
-
: err.message || err;
|
|
70
|
-
const wrapped = new Error(`${label}上传失败: ${detail}`);
|
|
71
|
-
wrapped.type = err.type === "validation_error" || err.type === "network_error"
|
|
72
|
-
? err.type
|
|
73
|
-
: "api_error";
|
|
74
|
-
wrapped.statusCode = err.statusCode;
|
|
75
|
-
wrapped.body = err.body;
|
|
76
|
-
throw wrapped;
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
/**
|
|
81
|
-
* text 格式输出:用 ✓ 开头展示关键信息
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
lines
|
|
88
|
-
lines.push(
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
if (
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
)
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
|
|
1
|
+
const fs = require("fs");
|
|
2
|
+
const path = require("path");
|
|
3
|
+
const { r2Upload, xhsHotList, xhsNotePublish } = require("../api");
|
|
4
|
+
const { outputResult, resolveKeyOrExit, handleApiError } = require("../lib/cli-utils");
|
|
5
|
+
|
|
6
|
+
const MAX_IMAGE_SIZE = 32 * 1024 * 1024;
|
|
7
|
+
const MAX_UPLOAD_SIZE = 100 * 1024 * 1024;
|
|
8
|
+
|
|
9
|
+
function validationError(message) {
|
|
10
|
+
return Object.assign(new Error(message), { type: "validation_error" });
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function resolveBase64Input(value, label) {
|
|
14
|
+
if (!value.startsWith("@")) return value;
|
|
15
|
+
const filePath = path.resolve(value.slice(1));
|
|
16
|
+
let content;
|
|
17
|
+
try {
|
|
18
|
+
content = fs.readFileSync(filePath, "utf-8").trim();
|
|
19
|
+
} catch (err) {
|
|
20
|
+
throw validationError(`${label} Base64 文件读取失败 ${filePath}: ${err.message}`);
|
|
21
|
+
}
|
|
22
|
+
if (!content) throw validationError(`${label} Base64 文件内容为空: ${filePath}`);
|
|
23
|
+
return content;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function validateLocalFile(filePath, label, maxSize) {
|
|
27
|
+
const absolutePath = path.resolve(filePath);
|
|
28
|
+
let stat;
|
|
29
|
+
try {
|
|
30
|
+
stat = fs.statSync(absolutePath);
|
|
31
|
+
} catch (err) {
|
|
32
|
+
throw validationError(`${label}文件读取失败 ${absolutePath}: ${err.message}`);
|
|
33
|
+
}
|
|
34
|
+
if (!stat.isFile()) throw validationError(`${label}路径不是文件: ${absolutePath}`);
|
|
35
|
+
if (stat.size > maxSize) {
|
|
36
|
+
throw validationError(`${label}文件超过 ${maxSize / 1024 / 1024}MB: ${absolutePath}`);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function validateUrl(value, label) {
|
|
41
|
+
let parsed;
|
|
42
|
+
try {
|
|
43
|
+
parsed = new URL(value);
|
|
44
|
+
} catch (_) {
|
|
45
|
+
throw validationError(`${label}不是有效 URL: ${value}`);
|
|
46
|
+
}
|
|
47
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
48
|
+
throw validationError(`${label}仅支持 HTTP/HTTPS URL: ${value}`);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function validateBase64Size(value, label, maxSize) {
|
|
53
|
+
const dataUrlPrefix = value.match(/^data:[^,]*,/i)?.[0];
|
|
54
|
+
const payload = (dataUrlPrefix ? value.slice(dataUrlPrefix.length) : value).replace(/\s/g, "");
|
|
55
|
+
const padding = payload.endsWith("==") ? 2 : payload.endsWith("=") ? 1 : 0;
|
|
56
|
+
const size = Math.max(0, Math.floor(payload.length * 3 / 4) - padding);
|
|
57
|
+
if (size > maxSize) {
|
|
58
|
+
throw validationError(`${label} Base64 内容超过 ${maxSize / 1024 / 1024}MB`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async function uploadMedia(apiKey, params, label) {
|
|
63
|
+
try {
|
|
64
|
+
const result = await r2Upload(apiKey, params);
|
|
65
|
+
return result.file_url;
|
|
66
|
+
} catch (err) {
|
|
67
|
+
const detail = err.type === "http_error"
|
|
68
|
+
? `HTTP ${err.statusCode}: ${err.body?.message || JSON.stringify(err.body)}`
|
|
69
|
+
: err.message || err;
|
|
70
|
+
const wrapped = new Error(`${label}上传失败: ${detail}`);
|
|
71
|
+
wrapped.type = err.type === "validation_error" || err.type === "network_error"
|
|
72
|
+
? err.type
|
|
73
|
+
: "api_error";
|
|
74
|
+
wrapped.statusCode = err.statusCode;
|
|
75
|
+
wrapped.body = err.body;
|
|
76
|
+
throw wrapped;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* text 格式输出:用 ✓ 开头展示关键信息
|
|
82
|
+
* @param {object} resp
|
|
83
|
+
* @returns {string}
|
|
84
|
+
*/
|
|
85
|
+
function notePublishTextFormatter(resp) {
|
|
86
|
+
const d = resp.data || {};
|
|
87
|
+
const lines = [];
|
|
88
|
+
lines.push("✓ 小红书笔记发布页已生成");
|
|
89
|
+
lines.push(` ID: ${d.id}`);
|
|
90
|
+
lines.push(` H5 地址: ${d.url}`);
|
|
91
|
+
if (resp.message) lines.push(` 消息: ${resp.message}`);
|
|
92
|
+
if (d.qrcode_url) lines.push(` 二维码图: ${d.qrcode_url}`);
|
|
93
|
+
if (d.qrcode) lines.push(` 二维码: ${d.qrcode.substring(0, 50)}...`);
|
|
94
|
+
if (d.media) {
|
|
95
|
+
if (d.media.images) lines.push(` 图片: ${d.media.images.length} 张`);
|
|
96
|
+
if (d.media.video) lines.push(` 视频: ${d.media.video}`);
|
|
97
|
+
if (d.media.cover) lines.push(` 封面: ${d.media.cover}`);
|
|
98
|
+
}
|
|
99
|
+
return lines.join("\n");
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* text 格式输出:展示小红书实时热榜。
|
|
104
|
+
* @param {object} resp
|
|
105
|
+
* @returns {string}
|
|
106
|
+
*/
|
|
107
|
+
function hotListTextFormatter(resp = {}) {
|
|
108
|
+
const data = resp.data || {};
|
|
109
|
+
const lines = ["小红书热榜"];
|
|
110
|
+
if (data.date) lines.push(`日期: ${data.date}`);
|
|
111
|
+
|
|
112
|
+
(data.items || []).forEach((item) => {
|
|
113
|
+
const rank = item.rank === undefined ? "-" : item.rank;
|
|
114
|
+
const trend = item.trend ? ` ${item.trend}` : "";
|
|
115
|
+
lines.push(`\n[${rank}] ${item.title || "未命名话题"}${trend}`);
|
|
116
|
+
if (item.hot || item.hot_value !== undefined) lines.push(` 热度: ${item.hot || item.hot_value}`);
|
|
117
|
+
if (item.url) lines.push(` URL: ${item.url}`);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
return lines.join("\n");
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* 获取小红书实时热榜。
|
|
125
|
+
* @param {{ apiKey?: string, format?: "json"|"text" }} options
|
|
126
|
+
* @returns {Promise<void>}
|
|
127
|
+
*/
|
|
128
|
+
async function hotListCommand(options = {}) {
|
|
129
|
+
const format = options.format || "json";
|
|
130
|
+
const apiKey = resolveKeyOrExit(options.apiKey, format);
|
|
131
|
+
|
|
132
|
+
try {
|
|
133
|
+
const result = await xhsHotList(apiKey);
|
|
134
|
+
outputResult(result, format, hotListTextFormatter);
|
|
135
|
+
process.exit(0);
|
|
136
|
+
} catch (err) {
|
|
137
|
+
handleApiError(err, format);
|
|
138
|
+
process.exit(1);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
async function notePublishCommand(type, options = {}) {
|
|
143
|
+
const format = options.format || "json";
|
|
144
|
+
const apiKey = resolveKeyOrExit(options.apiKey, format);
|
|
145
|
+
|
|
146
|
+
try {
|
|
147
|
+
if (type !== "normal" && type !== "video") {
|
|
148
|
+
throw validationError(`笔记类型仅支持 normal 或 video,收到: ${type}`);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const imageUrls = options.images
|
|
152
|
+
? options.images.split(",").map((value) => value.trim()).filter(Boolean)
|
|
153
|
+
: [];
|
|
154
|
+
const imageFiles = options.imageFile || [];
|
|
155
|
+
const imageBase64 = (options.imageBase64 || []).map((value, index) =>
|
|
156
|
+
resolveBase64Input(value, `第 ${imageUrls.length + imageFiles.length + index + 1} 张图片`)
|
|
157
|
+
);
|
|
158
|
+
const videoBase64 = options.videoBase64
|
|
159
|
+
? resolveBase64Input(options.videoBase64, "视频")
|
|
160
|
+
: undefined;
|
|
161
|
+
const coverBase64 = options.coverBase64
|
|
162
|
+
? resolveBase64Input(options.coverBase64, "视频封面")
|
|
163
|
+
: undefined;
|
|
164
|
+
const imageCount = imageUrls.length + imageFiles.length + imageBase64.length;
|
|
165
|
+
const videoSources = [options.video, options.videoFile, videoBase64].filter(Boolean);
|
|
166
|
+
const coverSources = [options.cover, options.coverFile, coverBase64].filter(Boolean);
|
|
167
|
+
const validDays = Number(options.uploadValidDays || 7);
|
|
168
|
+
|
|
169
|
+
if (validDays !== 1 && validDays !== 7) {
|
|
170
|
+
throw validationError(`--upload-valid-days 仅支持 1 或 7,收到: ${options.uploadValidDays}`);
|
|
171
|
+
}
|
|
172
|
+
if (type === "normal") {
|
|
173
|
+
if (videoSources.length > 0 || coverSources.length > 0) {
|
|
174
|
+
throw validationError("normal 类型不能使用视频或封面参数");
|
|
175
|
+
}
|
|
176
|
+
if (imageCount === 0) {
|
|
177
|
+
throw validationError("normal 类型至少需要一张图片,请使用 --images、--image-file 或 --image-base64");
|
|
178
|
+
}
|
|
179
|
+
if (imageCount > 18) {
|
|
180
|
+
throw validationError(`图片最多 18 张,当前共 ${imageCount} 张`);
|
|
181
|
+
}
|
|
182
|
+
imageUrls.forEach((url, index) => validateUrl(url, `第 ${index + 1} 张图片`));
|
|
183
|
+
imageFiles.forEach((filePath, index) => validateLocalFile(
|
|
184
|
+
filePath,
|
|
185
|
+
`第 ${imageUrls.length + index + 1} 张图片`,
|
|
186
|
+
MAX_IMAGE_SIZE
|
|
187
|
+
));
|
|
188
|
+
imageBase64.forEach((value, index) => validateBase64Size(
|
|
189
|
+
value,
|
|
190
|
+
`第 ${imageUrls.length + imageFiles.length + index + 1} 张图片`,
|
|
191
|
+
MAX_IMAGE_SIZE
|
|
192
|
+
));
|
|
193
|
+
} else {
|
|
194
|
+
if (imageCount > 0) {
|
|
195
|
+
throw validationError("video 类型不能使用图片参数");
|
|
196
|
+
}
|
|
197
|
+
if (videoSources.length !== 1) {
|
|
198
|
+
throw validationError("video 类型必须且只能使用 --video、--video-file、--video-base64 之一");
|
|
199
|
+
}
|
|
200
|
+
if (coverSources.length > 1) {
|
|
201
|
+
throw validationError("视频封面只能使用 --cover、--cover-file、--cover-base64 之一");
|
|
202
|
+
}
|
|
203
|
+
if (options.video) validateUrl(options.video, "视频");
|
|
204
|
+
if (options.videoFile) validateLocalFile(options.videoFile, "视频", MAX_UPLOAD_SIZE);
|
|
205
|
+
if (videoBase64) validateBase64Size(videoBase64, "视频", MAX_UPLOAD_SIZE);
|
|
206
|
+
if (options.cover) validateUrl(options.cover, "视频封面");
|
|
207
|
+
if (options.coverFile) validateLocalFile(options.coverFile, "视频封面", MAX_IMAGE_SIZE);
|
|
208
|
+
if (coverBase64) validateBase64Size(coverBase64, "视频封面", MAX_IMAGE_SIZE);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
let images;
|
|
212
|
+
let video;
|
|
213
|
+
let cover;
|
|
214
|
+
if (type === "normal") {
|
|
215
|
+
const uploadedImages = await Promise.all([
|
|
216
|
+
...imageFiles.map((filePath, index) => uploadMedia(
|
|
217
|
+
apiKey,
|
|
218
|
+
{ filePath, validDays },
|
|
219
|
+
`第 ${imageUrls.length + index + 1} 张图片`
|
|
220
|
+
)),
|
|
221
|
+
...imageBase64.map((fileBase64, index) => uploadMedia(
|
|
222
|
+
apiKey,
|
|
223
|
+
{ fileBase64, defaultContentType: "image/jpeg", validDays },
|
|
224
|
+
`第 ${imageUrls.length + imageFiles.length + index + 1} 张图片`
|
|
225
|
+
)),
|
|
226
|
+
]);
|
|
227
|
+
images = [...imageUrls, ...uploadedImages];
|
|
228
|
+
} else {
|
|
229
|
+
video = options.video;
|
|
230
|
+
if (options.videoFile) {
|
|
231
|
+
video = await uploadMedia(apiKey, { filePath: options.videoFile, validDays }, "视频");
|
|
232
|
+
} else if (videoBase64) {
|
|
233
|
+
video = await uploadMedia(
|
|
234
|
+
apiKey,
|
|
235
|
+
{ fileBase64: videoBase64, defaultContentType: "video/mp4", validDays },
|
|
236
|
+
"视频"
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
cover = options.cover;
|
|
241
|
+
if (options.coverFile) {
|
|
242
|
+
cover = await uploadMedia(apiKey, { filePath: options.coverFile, validDays }, "视频封面");
|
|
243
|
+
} else if (coverBase64) {
|
|
244
|
+
cover = await uploadMedia(
|
|
245
|
+
apiKey,
|
|
246
|
+
{ fileBase64: coverBase64, defaultContentType: "image/jpeg", validDays },
|
|
247
|
+
"视频封面"
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const result = await xhsNotePublish(apiKey, {
|
|
253
|
+
type,
|
|
254
|
+
title: options.title,
|
|
255
|
+
content: options.content,
|
|
256
|
+
images,
|
|
257
|
+
video,
|
|
258
|
+
cover,
|
|
259
|
+
});
|
|
260
|
+
outputResult(result, format, notePublishTextFormatter);
|
|
261
|
+
process.exit(0);
|
|
262
|
+
} catch (err) {
|
|
263
|
+
handleApiError(err, format);
|
|
264
|
+
process.exit(1);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
module.exports = { hotListCommand, hotListTextFormatter, notePublishCommand, notePublishTextFormatter };
|