aiconductor-cli 1.4.0 → 1.5.1
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 +160 -78
- package/README.md +27 -2
- package/api_docs/douyin_user_videos.md +2 -2
- package/api_docs/wx_history_articles.md +39 -20
- 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 +134 -106
- package/package.json +1 -1
- package/src/api.js +362 -268
- package/src/api.test.js +149 -2
- package/src/commands/wx.js +116 -0
package/src/api.test.js
CHANGED
|
@@ -4,8 +4,7 @@ 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, xhsHotList } = require("./api");
|
|
8
|
-
const {
|
|
7
|
+
const { hotList, webSearch, xhsHotList } = require("./api");const {
|
|
9
8
|
buildBriefingResult,
|
|
10
9
|
formatHotListText,
|
|
11
10
|
parseLimit,
|
|
@@ -14,6 +13,14 @@ const {
|
|
|
14
13
|
} = require("./commands/content");
|
|
15
14
|
const { formatSearchText } = require("./commands/web_search");
|
|
16
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");
|
|
17
24
|
|
|
18
25
|
function captureRequest(callback, responseBody = { success: true, billing: {} }, transport = https) {
|
|
19
26
|
const originalRequest = transport.request;
|
|
@@ -366,3 +373,143 @@ test("parseLimit accepts the documented range", () => {
|
|
|
366
373
|
assert.throws(() => parseLimit("31"), { type: "validation_error" });
|
|
367
374
|
assert.throws(() => parseLimit("invalid"), { type: "validation_error" });
|
|
368
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 treats an upstream code inside a successful response as a failure", async () => {
|
|
437
|
+
// 2026-11-05 线上实测:平台仍回 success:true,把上游失败塞进 data.code(api_docs:code 0 为成功),
|
|
438
|
+
// 文章数组为空。若照 success 判断,会把「上游挂了」报成「这个号最近没发文」。
|
|
439
|
+
const upstreamFailure = {
|
|
440
|
+
success: true,
|
|
441
|
+
message: "未能获取到任何数据。请检查参数或账户余额。",
|
|
442
|
+
billing: { pages_billed: 0, transaction_ids: [] },
|
|
443
|
+
data: { code: 20001, msg: "error", data: [], now_page: 0, now_page_articles_num: 0 },
|
|
444
|
+
};
|
|
445
|
+
|
|
446
|
+
await assert.rejects(
|
|
447
|
+
() => captureRequest(() => wxHistoryArticles("test-key", { name: "人民日报" }), upstreamFailure),
|
|
448
|
+
(err) =>
|
|
449
|
+
err.type === "api_error" &&
|
|
450
|
+
err.message.includes("未能获取到任何数据") &&
|
|
451
|
+
err.message.includes("20001")
|
|
452
|
+
);
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
test("wx history still resolves an empty page when the upstream code is 0", async () => {
|
|
456
|
+
const emptyButOk = {
|
|
457
|
+
success: true,
|
|
458
|
+
message: "微信公众号历史文章获取完成。成功获取 1 页数据,共 0 篇文章。",
|
|
459
|
+
data: { code: 0, msg: "success", data: [], now_page: 1 },
|
|
460
|
+
};
|
|
461
|
+
|
|
462
|
+
const request = await captureRequest(
|
|
463
|
+
() => wxHistoryArticles("test-key", { name: "不存在的号" }),
|
|
464
|
+
emptyButOk
|
|
465
|
+
);
|
|
466
|
+
assert.deepEqual(request.result.data.data, []);
|
|
467
|
+
assert.equal(request.result.success, true);
|
|
468
|
+
});
|
|
469
|
+
|
|
470
|
+
test("wx history formatter lists documented article fields", () => {
|
|
471
|
+
const text = formatHistoryText({
|
|
472
|
+
data: {
|
|
473
|
+
mp_nickname: "人民日报",
|
|
474
|
+
mp_ghid: "gh_363b924965e9",
|
|
475
|
+
now_page: 1,
|
|
476
|
+
data: [{
|
|
477
|
+
title: "今日要闻",
|
|
478
|
+
post_time_str: "2026-08-27 16:32:59",
|
|
479
|
+
read: 12345,
|
|
480
|
+
zan: 67,
|
|
481
|
+
original: 1,
|
|
482
|
+
digest: "摘要内容",
|
|
483
|
+
url: "https://mp.weixin.qq.com/s/abc",
|
|
484
|
+
}],
|
|
485
|
+
},
|
|
486
|
+
});
|
|
487
|
+
assert.match(text, /公众号: 人民日报(gh_363b924965e9)/);
|
|
488
|
+
assert.match(text, /篇数: +1/);
|
|
489
|
+
assert.match(text, /\[1\] 今日要闻/);
|
|
490
|
+
assert.match(text, /时间: 2026-08-27 16:32:59/);
|
|
491
|
+
assert.match(text, /阅读 12345 \| 点赞 67 \| 原创/);
|
|
492
|
+
assert.match(text, /URL: +https:\/\/mp\.weixin\.qq\.com\/s\/abc/);
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
test("wx article-data formatter labels the fields the live endpoint returns", () => {
|
|
496
|
+
// 字段取自 2026-11-04 线上真实响应
|
|
497
|
+
const articleText = formatArticleDataText({
|
|
498
|
+
message: "微信公众号文章互动数据获取成功。",
|
|
499
|
+
data: { read: 100001, zan: 5685, looking: 3202, share_num: 21013, collect_num: 3129, comment_count: 12 },
|
|
500
|
+
});
|
|
501
|
+
assert.match(articleText, /阅读: 100001/);
|
|
502
|
+
assert.match(articleText, /点赞: 5685/);
|
|
503
|
+
assert.match(articleText, /在看: 3202/);
|
|
504
|
+
assert.match(articleText, /分享: 21013/);
|
|
505
|
+
assert.match(articleText, /收藏: 3129/);
|
|
506
|
+
assert.match(articleText, /评论: 12/);
|
|
507
|
+
|
|
508
|
+
// 上游新增字段时不能被吞掉
|
|
509
|
+
const withUnknown = formatArticleDataText({ data: { read: 1, new_metric: 7 } });
|
|
510
|
+
assert.match(withUnknown, /阅读: 1/);
|
|
511
|
+
assert.match(withUnknown, /new_metric: 7/);
|
|
512
|
+
|
|
513
|
+
const emptyText = formatArticleDataText({ data: {} });
|
|
514
|
+
assert.match(emptyText, /上游未返回字段/);
|
|
515
|
+
});
|
|
@@ -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
|
+
};
|