aiconductor-cli 1.3.5 → 1.5.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/plans/2026-10-28T11-20-16-943Z-plan.md +161 -0
- package/AGENTS.md +157 -78
- package/README.md +49 -5
- package/api_docs/douyin_user_videos.md +2 -2
- package/api_docs/wx_history_articles.md +39 -20
- package/bin/aiconductor-cli.js +129 -94
- package/package.json +1 -1
- package/src/api.js +364 -268
- package/src/api.test.js +265 -3
- package/src/commands/content.js +99 -5
- package/src/commands/wx.js +116 -0
- package/src/commands/xhs.js +268 -225
package/src/api.test.js
CHANGED
|
@@ -4,9 +4,23 @@ 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
|
-
|
|
7
|
+
const { hotList, webSearch, xhsHotList } = require("./api");const {
|
|
8
|
+
buildBriefingResult,
|
|
9
|
+
formatHotListText,
|
|
10
|
+
parseLimit,
|
|
11
|
+
parseSourceIds,
|
|
12
|
+
DEFAULT_HOT_LIST_SOURCES,
|
|
13
|
+
} = require("./commands/content");
|
|
9
14
|
const { formatSearchText } = require("./commands/web_search");
|
|
15
|
+
const { hotListTextFormatter, notePublishTextFormatter } = require("./commands/xhs");
|
|
16
|
+
const {
|
|
17
|
+
wxArticleData,
|
|
18
|
+
wxHistoryArticles,
|
|
19
|
+
} = require("./api");
|
|
20
|
+
const {
|
|
21
|
+
formatArticleDataText,
|
|
22
|
+
formatHistoryText,
|
|
23
|
+
} = require("./commands/wx");
|
|
10
24
|
|
|
11
25
|
function captureRequest(callback, responseBody = { success: true, billing: {} }, transport = https) {
|
|
12
26
|
const originalRequest = transport.request;
|
|
@@ -26,7 +40,10 @@ function captureRequest(callback, responseBody = { success: true, billing: {} },
|
|
|
26
40
|
return request;
|
|
27
41
|
};
|
|
28
42
|
|
|
29
|
-
return callback().then(() =>
|
|
43
|
+
return callback().then((result) => {
|
|
44
|
+
captured.result = result;
|
|
45
|
+
return captured;
|
|
46
|
+
}).finally(() => {
|
|
30
47
|
transport.request = originalRequest;
|
|
31
48
|
});
|
|
32
49
|
}
|
|
@@ -217,3 +234,248 @@ test("hotList raises API failures", async () => {
|
|
|
217
234
|
{ type: "api_error", message: "没有可用热榜来源" }
|
|
218
235
|
);
|
|
219
236
|
});
|
|
237
|
+
|
|
238
|
+
test("xhsHotList calls the XHS hot-list endpoint over HTTP", async () => {
|
|
239
|
+
const responseBody = {
|
|
240
|
+
success: true,
|
|
241
|
+
data: {
|
|
242
|
+
date: "2026-07-29",
|
|
243
|
+
items: [{ rank: 1, title: "夏日穿搭", hot: "123万", trend: "up", url: "https://example.com/xhs" }],
|
|
244
|
+
},
|
|
245
|
+
billing: { service_fee: 0.01 },
|
|
246
|
+
};
|
|
247
|
+
const request = await captureRequest(() => xhsHotList("test-key"), responseBody, http);
|
|
248
|
+
|
|
249
|
+
assert.equal(request.options.path, "/api/xhs_hot_list");
|
|
250
|
+
assert.equal(request.options.port, 80);
|
|
251
|
+
assert.equal(request.options.headers.Authorization, "Bearer test-key");
|
|
252
|
+
assert.deepEqual(request.body, {});
|
|
253
|
+
assert.equal(request.result.billing, undefined);
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
test("xhsHotList raises API failures", async () => {
|
|
257
|
+
await assert.rejects(
|
|
258
|
+
captureRequest(
|
|
259
|
+
() => xhsHotList("test-key"),
|
|
260
|
+
{ success: false, message: "小红书热榜暂不可用" },
|
|
261
|
+
http
|
|
262
|
+
),
|
|
263
|
+
{ type: "api_error", message: "小红书热榜暂不可用" }
|
|
264
|
+
);
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
test("buildBriefingResult preserves hot-list fields and adds XHS data", () => {
|
|
268
|
+
const result = buildBriefingResult(
|
|
269
|
+
{
|
|
270
|
+
status: "fulfilled",
|
|
271
|
+
value: {
|
|
272
|
+
success: true,
|
|
273
|
+
data: {
|
|
274
|
+
sources: [{ source_id: "zhihu", items: [{ title: "AI 应用趋势" }] }],
|
|
275
|
+
failed_sources: [{ source_id: "weibo", message: "上游暂不可用" }],
|
|
276
|
+
},
|
|
277
|
+
},
|
|
278
|
+
},
|
|
279
|
+
{
|
|
280
|
+
status: "fulfilled",
|
|
281
|
+
value: {
|
|
282
|
+
success: true,
|
|
283
|
+
data: { date: "2026-07-29", items: [{ rank: 1, title: "AI 工具" }] },
|
|
284
|
+
},
|
|
285
|
+
}
|
|
286
|
+
);
|
|
287
|
+
|
|
288
|
+
assert.equal(result.success, true);
|
|
289
|
+
assert.equal(result.data.sources[0].source_id, "zhihu");
|
|
290
|
+
assert.equal(result.data.failed_sources[0].source_id, "weibo");
|
|
291
|
+
assert.equal(result.data.xhs_hot_list.items[0].title, "AI 工具");
|
|
292
|
+
assert.deepEqual(result.data.failed_services, []);
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
test("buildBriefingResult keeps either service when the other one fails", () => {
|
|
296
|
+
const hotListOnly = buildBriefingResult(
|
|
297
|
+
{ status: "fulfilled", value: { success: true, data: { sources: [], failed_sources: [] } } },
|
|
298
|
+
{ status: "rejected", reason: Object.assign(new Error("小红书超时"), { type: "network_error" }) }
|
|
299
|
+
);
|
|
300
|
+
assert.equal(hotListOnly.data.xhs_hot_list, null);
|
|
301
|
+
assert.deepEqual(hotListOnly.data.failed_services, [{ service: "xhs_hot_list", message: "小红书超时" }]);
|
|
302
|
+
|
|
303
|
+
const xhsOnly = buildBriefingResult(
|
|
304
|
+
{ status: "rejected", reason: Object.assign(new Error("热榜超时"), { type: "network_error" }) },
|
|
305
|
+
{ status: "fulfilled", value: { success: true, data: { date: "2026-07-29", items: [] } } }
|
|
306
|
+
);
|
|
307
|
+
assert.deepEqual(xhsOnly.data.sources, []);
|
|
308
|
+
assert.equal(xhsOnly.data.xhs_hot_list.date, "2026-07-29");
|
|
309
|
+
assert.deepEqual(xhsOnly.data.failed_services, [{ service: "hot_list", message: "热榜超时" }]);
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
test("buildBriefingResult fails only when both services fail", () => {
|
|
313
|
+
assert.throws(
|
|
314
|
+
() => buildBriefingResult(
|
|
315
|
+
{ status: "rejected", reason: new Error("热榜超时") },
|
|
316
|
+
{ status: "rejected", reason: new Error("小红书超时") }
|
|
317
|
+
),
|
|
318
|
+
{ type: "api_error", message: /均获取失败/ }
|
|
319
|
+
);
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
test("hot-list formatters render XHS trends and service failures", () => {
|
|
323
|
+
const briefingText = formatHotListText({
|
|
324
|
+
data: {
|
|
325
|
+
sources: [],
|
|
326
|
+
xhs_hot_list: {
|
|
327
|
+
date: "2026-07-29",
|
|
328
|
+
items: [{ rank: 1, title: "夏日穿搭", hot: "123万", trend: "up", url: "https://example.com/xhs" }],
|
|
329
|
+
},
|
|
330
|
+
failed_services: [{ service: "hot_list", message: "上游暂不可用" }],
|
|
331
|
+
},
|
|
332
|
+
});
|
|
333
|
+
assert.match(briefingText, /【小红书热榜】2026-07-29/);
|
|
334
|
+
assert.match(briefingText, /\[1\] 夏日穿搭 up/);
|
|
335
|
+
assert.match(briefingText, /服务失败:/);
|
|
336
|
+
|
|
337
|
+
const xhsText = hotListTextFormatter({
|
|
338
|
+
data: { date: "2026-07-29", items: [{ rank: 2, title: "露营", hot_value: 88, trend: "new" }] },
|
|
339
|
+
});
|
|
340
|
+
assert.match(xhsText, /小红书热榜/);
|
|
341
|
+
assert.match(xhsText, /\[2\] 露营 new/);
|
|
342
|
+
assert.match(xhsText, /热度: 88/);
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
test("note-publish formatter surfaces the shareable qrcode image URL", () => {
|
|
346
|
+
const text = notePublishTextFormatter({
|
|
347
|
+
message: "发布页生成成功",
|
|
348
|
+
data: {
|
|
349
|
+
id: "note-1",
|
|
350
|
+
url: "https://oss.aiconductor.fun/temporary_7/xhs_note_publish/note-1/index.html",
|
|
351
|
+
qrcode: "data:image/png;base64,AAAA",
|
|
352
|
+
qrcode_url: "https://oss.aiconductor.fun/temporary_7/xhs_note_publish/note-1/qrcode.png",
|
|
353
|
+
},
|
|
354
|
+
});
|
|
355
|
+
assert.match(text, /H5 地址:/);
|
|
356
|
+
assert.match(text, /二维码图: https:\/\/oss\.aiconductor\.fun\/temporary_7\/xhs_note_publish\/note-1\/qrcode\.png/);
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
test("note-publish formatter tolerates responses without qrcode_url", () => {
|
|
360
|
+
// 旧版后端只返回 base64 二维码,formatter 不能因此报错
|
|
361
|
+
const text = notePublishTextFormatter({
|
|
362
|
+
data: { id: "note-2", url: "https://example.com/index.html" },
|
|
363
|
+
});
|
|
364
|
+
assert.match(text, /H5 地址:/);
|
|
365
|
+
assert.doesNotMatch(text, /二维码图/);
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
test("parseLimit accepts the documented range", () => {
|
|
369
|
+
assert.equal(parseLimit(), 30);
|
|
370
|
+
assert.equal(parseLimit("1"), 1);
|
|
371
|
+
assert.equal(parseLimit(30), 30);
|
|
372
|
+
assert.throws(() => parseLimit("0"), { type: "validation_error" });
|
|
373
|
+
assert.throws(() => parseLimit("31"), { type: "validation_error" });
|
|
374
|
+
assert.throws(() => parseLimit("invalid"), { type: "validation_error" });
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
test("wxArticleData posts the article url with a Bearer header and strips billing", async () => {
|
|
378
|
+
const request = await captureRequest(() => wxArticleData("test-key", " https://mp.weixin.qq.com/s?__biz=M1 "), {
|
|
379
|
+
success: true,
|
|
380
|
+
message: "微信公众号文章互动数据获取成功。",
|
|
381
|
+
billing: { service_fee: 0.02, transaction_ids: [7] },
|
|
382
|
+
data: { read_num: 1200, zan_num: 8 },
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
assert.equal(request.options.path, "/api/wx_article_data");
|
|
386
|
+
assert.equal(request.options.port, 443);
|
|
387
|
+
assert.equal(request.options.headers.Authorization, "Bearer test-key");
|
|
388
|
+
assert.deepEqual(request.body, { url: "https://mp.weixin.qq.com/s?__biz=M1" });
|
|
389
|
+
assert.equal(request.result.billing, undefined);
|
|
390
|
+
assert.equal(request.result.data.read_num, 1200);
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
test("wxArticleData rejects a blank url without calling the endpoint", async () => {
|
|
394
|
+
await assert.rejects(() => wxArticleData("test-key", " "), { type: "validation_error" });
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
test("wxHistoryArticles maps pagination fields from the api docs", async () => {
|
|
398
|
+
const request = await captureRequest(() => wxHistoryArticles("test-key", {
|
|
399
|
+
name: " 人民日报 ",
|
|
400
|
+
maxPages: "3",
|
|
401
|
+
maxSeconds: "50",
|
|
402
|
+
}), { success: true, data: { data: [] } });
|
|
403
|
+
|
|
404
|
+
assert.equal(request.options.path, "/api/wx_history_articles");
|
|
405
|
+
assert.equal(request.options.headers.Authorization, "Bearer test-key");
|
|
406
|
+
assert.deepEqual(request.body, { name: "人民日报", max_pages: 3, max_seconds: 50 });
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
test("wxHistoryArticles accepts a url instead of a name and rejects missing locators", async () => {
|
|
410
|
+
const request = await captureRequest(
|
|
411
|
+
() => wxHistoryArticles("test-key", { url: "https://mp.weixin.qq.com/s/abc" }),
|
|
412
|
+
{ success: true, data: { data: [] } }
|
|
413
|
+
);
|
|
414
|
+
assert.deepEqual(request.body, { url: "https://mp.weixin.qq.com/s/abc" });
|
|
415
|
+
|
|
416
|
+
await assert.rejects(() => wxHistoryArticles("test-key", {}), { type: "validation_error" });
|
|
417
|
+
await assert.rejects(
|
|
418
|
+
() => wxHistoryArticles("test-key", { name: "人民日报", maxSeconds: 171 }),
|
|
419
|
+
{ type: "validation_error" }
|
|
420
|
+
);
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
test("wx endpoints turn HTTP 200 business failures into api_error", async () => {
|
|
424
|
+
const failure = { success: false, message: "账户余额不足,当前余额:0.01,需要:0.02", billing: {} };
|
|
425
|
+
|
|
426
|
+
await assert.rejects(
|
|
427
|
+
() => captureRequest(() => wxArticleData("test-key", "https://mp.weixin.qq.com/s/abc"), failure),
|
|
428
|
+
(err) => err.type === "api_error" && err.message === failure.message
|
|
429
|
+
);
|
|
430
|
+
await assert.rejects(
|
|
431
|
+
() => captureRequest(() => wxHistoryArticles("test-key", { name: "人民日报" }), failure),
|
|
432
|
+
{ type: "api_error" }
|
|
433
|
+
);
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
test("wx history formatter lists documented article fields", () => {
|
|
437
|
+
const text = formatHistoryText({
|
|
438
|
+
data: {
|
|
439
|
+
mp_nickname: "人民日报",
|
|
440
|
+
mp_ghid: "gh_363b924965e9",
|
|
441
|
+
now_page: 1,
|
|
442
|
+
data: [{
|
|
443
|
+
title: "今日要闻",
|
|
444
|
+
post_time_str: "2026-08-27 16:32:59",
|
|
445
|
+
read: 12345,
|
|
446
|
+
zan: 67,
|
|
447
|
+
original: 1,
|
|
448
|
+
digest: "摘要内容",
|
|
449
|
+
url: "https://mp.weixin.qq.com/s/abc",
|
|
450
|
+
}],
|
|
451
|
+
},
|
|
452
|
+
});
|
|
453
|
+
assert.match(text, /公众号: 人民日报(gh_363b924965e9)/);
|
|
454
|
+
assert.match(text, /篇数: +1/);
|
|
455
|
+
assert.match(text, /\[1\] 今日要闻/);
|
|
456
|
+
assert.match(text, /时间: 2026-08-27 16:32:59/);
|
|
457
|
+
assert.match(text, /阅读 12345 \| 点赞 67 \| 原创/);
|
|
458
|
+
assert.match(text, /URL: +https:\/\/mp\.weixin\.qq\.com\/s\/abc/);
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
test("wx article-data formatter labels the fields the live endpoint returns", () => {
|
|
462
|
+
// 字段取自 2026-11-04 线上真实响应
|
|
463
|
+
const articleText = formatArticleDataText({
|
|
464
|
+
message: "微信公众号文章互动数据获取成功。",
|
|
465
|
+
data: { read: 100001, zan: 5685, looking: 3202, share_num: 21013, collect_num: 3129, comment_count: 12 },
|
|
466
|
+
});
|
|
467
|
+
assert.match(articleText, /阅读: 100001/);
|
|
468
|
+
assert.match(articleText, /点赞: 5685/);
|
|
469
|
+
assert.match(articleText, /在看: 3202/);
|
|
470
|
+
assert.match(articleText, /分享: 21013/);
|
|
471
|
+
assert.match(articleText, /收藏: 3129/);
|
|
472
|
+
assert.match(articleText, /评论: 12/);
|
|
473
|
+
|
|
474
|
+
// 上游新增字段时不能被吞掉
|
|
475
|
+
const withUnknown = formatArticleDataText({ data: { read: 1, new_metric: 7 } });
|
|
476
|
+
assert.match(withUnknown, /阅读: 1/);
|
|
477
|
+
assert.match(withUnknown, /new_metric: 7/);
|
|
478
|
+
|
|
479
|
+
const emptyText = formatArticleDataText({ data: {} });
|
|
480
|
+
assert.match(emptyText, /上游未返回字段/);
|
|
481
|
+
});
|
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
|
+
};
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
const { wxArticleData, wxHistoryArticles } = require("../api");
|
|
2
|
+
const { outputResult, resolveKeyOrExit, handleApiError } = require("../lib/cli-utils");
|
|
3
|
+
|
|
4
|
+
// wx_article_data 的真实返回字段(2026-11-04 线上实测);上游改名时未知键仍原样输出
|
|
5
|
+
const ARTICLE_DATA_LABELS = [
|
|
6
|
+
["read", "阅读"],
|
|
7
|
+
["zan", "点赞"],
|
|
8
|
+
["looking", "在看"],
|
|
9
|
+
["share_num", "分享"],
|
|
10
|
+
["collect_num", "收藏"],
|
|
11
|
+
["comment_count", "评论"],
|
|
12
|
+
];
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* 获取公众号文章互动数据
|
|
16
|
+
* @param {string} url 微信文章链接
|
|
17
|
+
* @param {object} options
|
|
18
|
+
*/
|
|
19
|
+
async function articleDataCommand(url, options = {}) {
|
|
20
|
+
const format = options.format || "json";
|
|
21
|
+
const apiKey = resolveKeyOrExit(options.apiKey, format);
|
|
22
|
+
try {
|
|
23
|
+
const result = await wxArticleData(apiKey, url);
|
|
24
|
+
outputResult(result, format, formatArticleDataText);
|
|
25
|
+
process.exit(0);
|
|
26
|
+
} catch (err) { handleApiError(err, format); process.exit(1); }
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* 获取公众号历史文章列表
|
|
31
|
+
* @param {string|undefined} url 微信文章链接(用于定位公众号)
|
|
32
|
+
* @param {object} options
|
|
33
|
+
*/
|
|
34
|
+
async function historyCommand(url, options = {}) {
|
|
35
|
+
const format = options.format || "json";
|
|
36
|
+
const apiKey = resolveKeyOrExit(options.apiKey, format);
|
|
37
|
+
try {
|
|
38
|
+
const result = await wxHistoryArticles(apiKey, {
|
|
39
|
+
url,
|
|
40
|
+
name: options.name,
|
|
41
|
+
maxPages: options.maxPages,
|
|
42
|
+
maxSeconds: options.maxSeconds,
|
|
43
|
+
});
|
|
44
|
+
outputResult(result, format, formatHistoryText);
|
|
45
|
+
process.exit(0);
|
|
46
|
+
} catch (err) { handleApiError(err, format); process.exit(1); }
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* text 输出:公众号文章互动数据(已知字段给中文标签,未知字段原样输出)
|
|
51
|
+
* @param {object} resp
|
|
52
|
+
* @returns {string}
|
|
53
|
+
*/
|
|
54
|
+
function formatArticleDataText(resp = {}) {
|
|
55
|
+
const data = resp.data || {};
|
|
56
|
+
const lines = ["✓ 公众号文章互动数据"];
|
|
57
|
+
if (resp.message) lines.push(` 消息: ${resp.message}`);
|
|
58
|
+
|
|
59
|
+
const printed = new Set();
|
|
60
|
+
const known = [];
|
|
61
|
+
ARTICLE_DATA_LABELS.forEach(([key, label]) => {
|
|
62
|
+
if (data[key] === undefined) return;
|
|
63
|
+
printed.add(key);
|
|
64
|
+
known.push(` ${label}: ${data[key]}`);
|
|
65
|
+
});
|
|
66
|
+
const extras = Object.keys(data)
|
|
67
|
+
.filter((key) => !printed.has(key))
|
|
68
|
+
.map((key) => {
|
|
69
|
+
const value = data[key];
|
|
70
|
+
return ` ${key}: ${value !== null && typeof value === "object" ? JSON.stringify(value) : String(value)}`;
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
if (known.length === 0 && extras.length === 0) {
|
|
74
|
+
lines.push(" 上游未返回字段(用 -f json 查看原始响应)");
|
|
75
|
+
return lines.join("\n");
|
|
76
|
+
}
|
|
77
|
+
return lines.concat(known, extras).join("\n");
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* text 输出:公众号历史文章列表
|
|
82
|
+
* @param {object} resp
|
|
83
|
+
* @returns {string}
|
|
84
|
+
*/
|
|
85
|
+
function formatHistoryText(resp = {}) {
|
|
86
|
+
const data = resp.data || {};
|
|
87
|
+
const articles = Array.isArray(data.data) ? data.data : [];
|
|
88
|
+
const lines = ["✓ 公众号历史文章"];
|
|
89
|
+
const nickname = data.mp_nickname || data.nickname;
|
|
90
|
+
// 实测用 name 定位时只有 mp_ghid,api_docs 里的 mp_wxid / head_img 并不返回
|
|
91
|
+
const accountId = data.mp_wxid || data.wxid || data.mp_ghid;
|
|
92
|
+
if (nickname) lines.push(` 公众号: ${nickname}${accountId ? `(${accountId})` : ""}`);
|
|
93
|
+
lines.push(` 篇数: ${articles.length}`);
|
|
94
|
+
if (data.now_page !== undefined) lines.push(` 翻页: ${data.now_page}`);
|
|
95
|
+
|
|
96
|
+
articles.forEach((item, index) => {
|
|
97
|
+
lines.push(`\n[${index + 1}] ${item.title || "未命名文章"}`);
|
|
98
|
+
if (item.post_time_str) lines.push(` 时间: ${item.post_time_str}`);
|
|
99
|
+
const stats = [];
|
|
100
|
+
if (item.read !== undefined) stats.push(`阅读 ${item.read}`);
|
|
101
|
+
if (item.zan !== undefined) stats.push(`点赞 ${item.zan}`);
|
|
102
|
+
if (item.original) stats.push("原创");
|
|
103
|
+
if (stats.length > 0) lines.push(` 数据: ${stats.join(" | ")}`);
|
|
104
|
+
if (item.digest) lines.push(` 摘要: ${item.digest}`);
|
|
105
|
+
if (item.url) lines.push(` URL: ${item.url}`);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
return lines.join("\n");
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
module.exports = {
|
|
112
|
+
articleDataCommand,
|
|
113
|
+
historyCommand,
|
|
114
|
+
formatArticleDataText,
|
|
115
|
+
formatHistoryText,
|
|
116
|
+
};
|