aiconductor-cli 1.5.1 → 1.7.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 +565 -419
- package/bin/aiconductor-cli.js +401 -296
- package/package.json +1 -1
- package/src/api.js +361 -2
- package/src/api.test.js +553 -3
- package/src/commands/content.js +184 -184
- package/src/commands/douyin.js +174 -0
- package/src/commands/html_publish.js +54 -54
- package/src/commands/md_publish.js +175 -175
- package/src/commands/web.js +69 -0
- package/src/commands/web_search.js +99 -99
- package/src/commands/wx.js +116 -116
- package/src/commands/xhs.js +763 -268
- package/src/lib/cli-utils.js +172 -84
- package/src/lib/daren-score.js +91 -0
- package/src/lib/daren-score.test.js +57 -0
- package/AGENTS.md +0 -189
- package/api_docs/aic_web_search.md +0 -84
- package/api_docs/audio_converter.md +0 -51
- package/api_docs/balance_query.md +0 -38
- package/api_docs/bing_search.md +0 -55
- package/api_docs/china_stock_data.md +0 -221
- package/api_docs/delete_published_webpage.md +0 -38
- package/api_docs/doubao_search.md +0 -80
- package/api_docs/douyin_link_to_text_v1.md +0 -50
- package/api_docs/douyin_link_to_text_v2.md +0 -50
- package/api_docs/douyin_user_detail.md +0 -65
- package/api_docs/douyin_user_search.md +0 -58
- package/api_docs/douyin_user_videos.md +0 -49
- package/api_docs/douyin_user_videos_async.md +0 -45
- package/api_docs/douyin_user_videos_async_query.md +0 -41
- package/api_docs/douyin_user_videos_max.md +0 -59
- package/api_docs/douyin_video_comments.md +0 -68
- package/api_docs/douyin_video_info.md +0 -70
- package/api_docs/douyin_video_search.md +0 -60
- package/api_docs/douyin_video_search_fast.md +0 -48
- package/api_docs/fast_ocr.md +0 -51
- package/api_docs/flow_article2podcast_creat.md +0 -41
- package/api_docs/flow_article2podcast_query.md +0 -41
- package/api_docs/flow_wx_history_articles.md +0 -45
- package/api_docs/generate_image.md +0 -68
- package/api_docs/get_duration.md +0 -37
- package/api_docs/hot_list_mixed.md +0 -122
- package/api_docs/html_publish.md +0 -50
- package/api_docs/html_publish_flexible.md +0 -65
- package/api_docs/image_resize.md +0 -58
- package/api_docs/image_to_permalink.md +0 -61
- package/api_docs/link_expander.md +0 -39
- package/api_docs/llm_proxy.md +0 -61
- package/api_docs/markitdown.md +0 -47
- package/api_docs/md_to_docx.md +0 -51
- package/api_docs/md_to_html.md +0 -53
- package/api_docs/md_to_mindmap.md +0 -52
- package/api_docs/md_to_pdf.md +0 -52
- package/api_docs/md_to_png.md +0 -53
- package/api_docs/md_to_txt.md +0 -51
- package/api_docs/md_to_wxhtml.md +0 -64
- package/api_docs/md_to_xhs.md +0 -87
- package/api_docs/md_to_xlsx.md +0 -52
- package/api_docs/portrait_enhancement.md +0 -69
- package/api_docs/published_webpages.md +0 -45
- package/api_docs/r2_uploader.md +0 -51
- package/api_docs/schedulerx_job_create.md +0 -56
- package/api_docs/transaction_query.md +0 -52
- package/api_docs/url_to_markdown.md +0 -50
- package/api_docs/video_download_url.md +0 -57
- package/api_docs/video_download_url_oss.md +0 -55
- package/api_docs/video_to_text.md +0 -48
- package/api_docs/video_to_text_v2.md +0 -49
- package/api_docs/volcengine_face_pretty.md +0 -55
- package/api_docs/volcengine_faceswap.md +0 -55
- package/api_docs/website_screenshot.md +0 -49
- package/api_docs/wx_access_token.md +0 -44
- package/api_docs/wx_article_analysis_data_push.md +0 -43
- package/api_docs/wx_article_data.md +0 -48
- package/api_docs/wx_article_style_rewrite.md +0 -45
- package/api_docs/wx_draft_add.md +0 -56
- package/api_docs/wx_draft_publish.md +0 -47
- package/api_docs/wx_history_articles.md +0 -89
- package/api_docs/wx_image_upload.md +0 -46
- package/api_docs/wx_md_to_html.md +0 -48
- package/api_docs/wx_rss_reader.md +0 -59
- package/api_docs/xhs_hot_list.md +0 -48
- package/api_docs/xhs_note_comments_fast.md +0 -54
- package/api_docs/xhs_note_info_v2.md +0 -45
- package/api_docs/xhs_note_info_v3.md +0 -69
- package/api_docs/xhs_note_publish.md +0 -54
- package/api_docs/xhs_note_search_v2.md +0 -73
- package/api_docs/xhs_note_search_v3.md +0 -54
- package/api_docs/xhs_user_info_v2.md +0 -61
- package/api_docs/xhs_user_notes_v2.md +0 -56
- package/api_docs/xhs_user_notes_v3.md +0 -48
package/src/api.test.js
CHANGED
|
@@ -4,7 +4,35 @@ const http = require("node:http");
|
|
|
4
4
|
const https = require("node:https");
|
|
5
5
|
const test = require("node:test");
|
|
6
6
|
|
|
7
|
-
const {
|
|
7
|
+
const {
|
|
8
|
+
hotList,
|
|
9
|
+
webSearch,
|
|
10
|
+
xhsHotList,
|
|
11
|
+
urlToMarkdown,
|
|
12
|
+
douyinHotWords,
|
|
13
|
+
douyinHotTopics,
|
|
14
|
+
douyinRisingTopics,
|
|
15
|
+
xhsPgyBlogger,
|
|
16
|
+
xhsPgyAudience,
|
|
17
|
+
xhsPgyPerformance,
|
|
18
|
+
douyinVideoOriginal,
|
|
19
|
+
} = require("./api");
|
|
20
|
+
const {
|
|
21
|
+
xhsNoteInfo,
|
|
22
|
+
xhsNoteComments,
|
|
23
|
+
xhsNoteSearch,
|
|
24
|
+
xhsUserInfo,
|
|
25
|
+
xhsUserNotes,
|
|
26
|
+
resolveXhsShortLink,
|
|
27
|
+
} = require("./api");
|
|
28
|
+
const {
|
|
29
|
+
extractUrlFromText,
|
|
30
|
+
parseXhsNoteId,
|
|
31
|
+
parseXhsUserId,
|
|
32
|
+
formatXhsTime,
|
|
33
|
+
toCount,
|
|
34
|
+
} = require("./lib/cli-utils");
|
|
35
|
+
const {
|
|
8
36
|
buildBriefingResult,
|
|
9
37
|
formatHotListText,
|
|
10
38
|
parseLimit,
|
|
@@ -12,7 +40,18 @@ const { hotList, webSearch, xhsHotList } = require("./api");const {
|
|
|
12
40
|
DEFAULT_HOT_LIST_SOURCES,
|
|
13
41
|
} = require("./commands/content");
|
|
14
42
|
const { formatSearchText } = require("./commands/web_search");
|
|
15
|
-
const {
|
|
43
|
+
const { formatToMdText, readFrontmatter } = require("./commands/web");
|
|
44
|
+
const {
|
|
45
|
+
hotListTextFormatter,
|
|
46
|
+
notePublishTextFormatter,
|
|
47
|
+
buildDarenReportResult,
|
|
48
|
+
formatDarenReportText,
|
|
49
|
+
} = require("./commands/xhs");
|
|
50
|
+
const {
|
|
51
|
+
buildTopicRadarResult,
|
|
52
|
+
formatTopicRadarText,
|
|
53
|
+
formatVideoOriginalText,
|
|
54
|
+
} = require("./commands/douyin");
|
|
16
55
|
const {
|
|
17
56
|
wxArticleData,
|
|
18
57
|
wxHistoryArticles,
|
|
@@ -434,7 +473,7 @@ test("wx endpoints turn HTTP 200 business failures into api_error", async () =>
|
|
|
434
473
|
});
|
|
435
474
|
|
|
436
475
|
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 为成功),
|
|
476
|
+
// 2026-11-05 线上实测:平台仍回 success:true,把上游失败塞进 data.code(backend/api_docs:code 0 为成功),
|
|
438
477
|
// 文章数组为空。若照 success 判断,会把「上游挂了」报成「这个号最近没发文」。
|
|
439
478
|
const upstreamFailure = {
|
|
440
479
|
success: true,
|
|
@@ -513,3 +552,514 @@ test("wx article-data formatter labels the fields the live endpoint returns", ()
|
|
|
513
552
|
const emptyText = formatArticleDataText({ data: {} });
|
|
514
553
|
assert.match(emptyText, /上游未返回字段/);
|
|
515
554
|
});
|
|
555
|
+
|
|
556
|
+
// ---------------------------------------------------------------------------
|
|
557
|
+
// 小红书(xhs)数据命令组
|
|
558
|
+
// 依据:backend/api_docs/xhs_*.md 的请求字段 + 2026-11-05 线上真实响应
|
|
559
|
+
// ---------------------------------------------------------------------------
|
|
560
|
+
|
|
561
|
+
test("xhsNoteInfo posts note_id to the v3 note detail endpoint over http", async () => {
|
|
562
|
+
const request = await captureRequest(
|
|
563
|
+
() => xhsNoteInfo("test-key", "6ae972260000000015020d84"),
|
|
564
|
+
{ success: true, message: "笔记详情获取成功", note_data: { note_id: "6ae972260000000015020d84" }, billing: { service_fee: 0.15 } },
|
|
565
|
+
http
|
|
566
|
+
);
|
|
567
|
+
|
|
568
|
+
assert.equal(request.options.hostname, "plugin.aiconductor.fun");
|
|
569
|
+
assert.equal(request.options.path, "/api/xhs_note_info_v3");
|
|
570
|
+
assert.equal(request.options.port, 80);
|
|
571
|
+
assert.equal(request.options.method, "POST");
|
|
572
|
+
assert.equal(request.options.headers.Authorization, "Bearer test-key");
|
|
573
|
+
assert.deepEqual(request.body, { note_id: "6ae972260000000015020d84" });
|
|
574
|
+
// billing 不进任何输出
|
|
575
|
+
assert.equal(request.result.billing, undefined);
|
|
576
|
+
assert.equal(request.result.note_data.note_id, "6ae972260000000015020d84");
|
|
577
|
+
});
|
|
578
|
+
|
|
579
|
+
test("xhsNoteComments sends pagination fields and accepts share_text", async () => {
|
|
580
|
+
const request = await captureRequest(
|
|
581
|
+
() => xhsNoteComments("test-key", { noteId: "6ae972260000000015020d84", maxPages: 2, maxSeconds: 40 }),
|
|
582
|
+
{ success: true, data: { comments: [], total_comments: 0 } },
|
|
583
|
+
http
|
|
584
|
+
);
|
|
585
|
+
|
|
586
|
+
assert.equal(request.options.path, "/api/xhs_note_comments_fast");
|
|
587
|
+
assert.equal(request.options.port, 80);
|
|
588
|
+
assert.deepEqual(request.body, {
|
|
589
|
+
note_id: "6ae972260000000015020d84",
|
|
590
|
+
max_pages: 2,
|
|
591
|
+
max_seconds: 40,
|
|
592
|
+
});
|
|
593
|
+
});
|
|
594
|
+
|
|
595
|
+
test("xhsNoteComments prefers share_text only when note id is absent", async () => {
|
|
596
|
+
const withShare = await captureRequest(
|
|
597
|
+
() => xhsNoteComments("test-key", { shareText: "https://xhslink.cn/o/85lbqBJtO94" }),
|
|
598
|
+
{ success: true, data: { comments: [] } },
|
|
599
|
+
http
|
|
600
|
+
);
|
|
601
|
+
assert.deepEqual(withShare.body, { share_text: "https://xhslink.cn/o/85lbqBJtO94" });
|
|
602
|
+
|
|
603
|
+
// 两个都没给:本地拒绝,不发请求
|
|
604
|
+
await assert.rejects(() => xhsNoteComments("test-key", {}), { type: "validation_error" });
|
|
605
|
+
// 越界:max_pages 只允许 1~5,max_seconds 不超过平台 170s 上限
|
|
606
|
+
await assert.rejects(
|
|
607
|
+
() => xhsNoteComments("test-key", { noteId: "6ae972260000000015020d84", maxPages: 6 }),
|
|
608
|
+
{ type: "validation_error" }
|
|
609
|
+
);
|
|
610
|
+
await assert.rejects(
|
|
611
|
+
() => xhsNoteComments("test-key", { noteId: "6ae972260000000015020d84", maxSeconds: 171 }),
|
|
612
|
+
{ type: "validation_error" }
|
|
613
|
+
);
|
|
614
|
+
});
|
|
615
|
+
|
|
616
|
+
test("xhsNoteSearch maps keyword filters to the v3 search endpoint", async () => {
|
|
617
|
+
const request = await captureRequest(
|
|
618
|
+
() => xhsNoteSearch("test-key", {
|
|
619
|
+
keyword: "AI编程",
|
|
620
|
+
maxPages: 3,
|
|
621
|
+
sort: "popularity_descending",
|
|
622
|
+
noteTime: "week",
|
|
623
|
+
noteType: "video",
|
|
624
|
+
}),
|
|
625
|
+
{ success: true, data: { items: [], has_more: false } },
|
|
626
|
+
http
|
|
627
|
+
);
|
|
628
|
+
|
|
629
|
+
assert.equal(request.options.path, "/api/xhs_note_search_v3");
|
|
630
|
+
assert.equal(request.options.port, 80);
|
|
631
|
+
assert.equal(request.options.headers.Authorization, "Bearer test-key");
|
|
632
|
+
assert.deepEqual(request.body, {
|
|
633
|
+
keyword: "AI编程",
|
|
634
|
+
max_pages: 3,
|
|
635
|
+
sort: "popularity_descending",
|
|
636
|
+
note_time: "week",
|
|
637
|
+
note_type: "video",
|
|
638
|
+
});
|
|
639
|
+
});
|
|
640
|
+
|
|
641
|
+
test("xhsNoteSearch rejects unknown enum values and empty keywords", async () => {
|
|
642
|
+
await assert.rejects(() => xhsNoteSearch("test-key", { keyword: " " }), { type: "validation_error" });
|
|
643
|
+
await assert.rejects(
|
|
644
|
+
() => xhsNoteSearch("test-key", { keyword: "AI", sort: "likes" }),
|
|
645
|
+
{ type: "validation_error" }
|
|
646
|
+
);
|
|
647
|
+
await assert.rejects(
|
|
648
|
+
() => xhsNoteSearch("test-key", { keyword: "AI", noteTime: "year" }),
|
|
649
|
+
{ type: "validation_error" }
|
|
650
|
+
);
|
|
651
|
+
await assert.rejects(
|
|
652
|
+
() => xhsNoteSearch("test-key", { keyword: "AI", noteType: "image" }),
|
|
653
|
+
{ type: "validation_error" }
|
|
654
|
+
);
|
|
655
|
+
});
|
|
656
|
+
|
|
657
|
+
test("xhsUserInfo maps user_id and xhsUserNotes maps pagination", async () => {
|
|
658
|
+
const userRequest = await captureRequest(
|
|
659
|
+
() => xhsUserInfo("test-key", "53cd3bbbb4c4d64f93463018"),
|
|
660
|
+
{ success: true, user_info: { basic_info: {} } },
|
|
661
|
+
http
|
|
662
|
+
);
|
|
663
|
+
assert.equal(userRequest.options.path, "/api/xhs_user_info_v2");
|
|
664
|
+
assert.equal(userRequest.options.port, 80);
|
|
665
|
+
assert.deepEqual(userRequest.body, { user_id: "53cd3bbbb4c4d64f93463018" });
|
|
666
|
+
|
|
667
|
+
const notesRequest = await captureRequest(
|
|
668
|
+
() => xhsUserNotes("test-key", { userId: "53cd3bbbb4c4d64f93463018", maxPages: 2 }),
|
|
669
|
+
{ success: true, data: { notes: [] } },
|
|
670
|
+
http
|
|
671
|
+
);
|
|
672
|
+
// 刻意打 v2 端点:v2/v3 现在同上游同输出,v2 是历史真实流量所在
|
|
673
|
+
assert.equal(notesRequest.options.path, "/api/xhs_user_notes_v2");
|
|
674
|
+
assert.equal(notesRequest.options.port, 80);
|
|
675
|
+
assert.deepEqual(notesRequest.body, { user_id: "53cd3bbbb4c4d64f93463018", max_pages: 2 });
|
|
676
|
+
});
|
|
677
|
+
|
|
678
|
+
test("xhs endpoints turn business failures into api_error", async () => {
|
|
679
|
+
await assert.rejects(
|
|
680
|
+
() => captureRequest(
|
|
681
|
+
() => xhsNoteInfo("test-key", "6ae972260000000015020d84"),
|
|
682
|
+
{ success: false, message: "笔记ID无效或笔记不可访问" },
|
|
683
|
+
http
|
|
684
|
+
),
|
|
685
|
+
{ type: "api_error", message: "笔记ID无效或笔记不可访问" }
|
|
686
|
+
);
|
|
687
|
+
});
|
|
688
|
+
|
|
689
|
+
test("parseXhsNoteId and parseXhsUserId accept ids, long links and share copy", () => {
|
|
690
|
+
const id = "6ae972260000000015020d84";
|
|
691
|
+
assert.equal(parseXhsNoteId(id), id);
|
|
692
|
+
assert.equal(
|
|
693
|
+
parseXhsNoteId(`字节早就不是下狗了 https://www.xiaohongshu.com/discovery/item/${id}?xsec_token=abc 复制内容`),
|
|
694
|
+
id
|
|
695
|
+
);
|
|
696
|
+
assert.equal(parseXhsNoteId(`https://www.xiaohongshu.com/explore/${id}?xsec_source=pc_share`), id);
|
|
697
|
+
// 小红书号/主页/空串都不该被当成 note id
|
|
698
|
+
assert.equal(parseXhsNoteId("hainingshushu"), null);
|
|
699
|
+
assert.equal(parseXhsNoteId(""), null);
|
|
700
|
+
assert.equal(parseXhsNoteId(undefined), null);
|
|
701
|
+
|
|
702
|
+
const userId = "53cd3bbbb4c4d64f93463018";
|
|
703
|
+
assert.equal(parseXhsUserId(userId), userId);
|
|
704
|
+
assert.equal(parseXhsUserId(`https://www.xiaohongshu.com/user/profile/${userId}?xsec_token=x`), userId);
|
|
705
|
+
assert.equal(parseXhsUserId("https://www.xiaohongshu.com/explore/6ae972260000000015020d84"), null);
|
|
706
|
+
});
|
|
707
|
+
|
|
708
|
+
test("extractUrlFromText pulls the first url out of share copy", () => {
|
|
709
|
+
assert.equal(
|
|
710
|
+
extractUrlFromText("字节早就不是下狗了,不想承认也不行 https://xhslink.cn/o/85lbqBJtO94 复制内容,然后进入【小红书】查看相关笔记。"),
|
|
711
|
+
"https://xhslink.cn/o/85lbqBJtO94"
|
|
712
|
+
);
|
|
713
|
+
// 中文全角标点紧贴链接时不能带进 url
|
|
714
|
+
assert.equal(extractUrlFromText("看这个https://xhslink.cn/o/abc),很准"), "https://xhslink.cn/o/abc");
|
|
715
|
+
assert.equal(extractUrlFromText("没有链接"), null);
|
|
716
|
+
});
|
|
717
|
+
|
|
718
|
+
test("formatXhsTime renders UTC epoch as Beijing time", () => {
|
|
719
|
+
// 独立来源:同一视频笔记 note_info.time = 1793578330,+8h 等于 search 返回的 notePublishTime "2026-11-02 08:12:10"
|
|
720
|
+
assert.equal(formatXhsTime(1793578330), "2026-11-02 08:12:10");
|
|
721
|
+
// 毫秒(用户笔记列表)同口径
|
|
722
|
+
assert.equal(formatXhsTime(1793690772000), "2026-11-03 15:26:12");
|
|
723
|
+
assert.equal(formatXhsTime(0), "");
|
|
724
|
+
assert.equal(formatXhsTime(null), "");
|
|
725
|
+
});
|
|
726
|
+
|
|
727
|
+
test("toCount normalises string counts coming from the xhs upstream", () => {
|
|
728
|
+
assert.equal(toCount("54"), 54);
|
|
729
|
+
assert.equal(toCount("1,234"), 1234);
|
|
730
|
+
assert.equal(toCount(7), 7);
|
|
731
|
+
assert.equal(toCount("1.2万"), 12000);
|
|
732
|
+
assert.equal(toCount("1万+"), 10000);
|
|
733
|
+
assert.equal(toCount(undefined), 0);
|
|
734
|
+
assert.equal(toCount("abc"), 0);
|
|
735
|
+
});
|
|
736
|
+
|
|
737
|
+
test("resolveXhsShortLink follows the 302 to the real note url", async () => {
|
|
738
|
+
const finalUrl = "https://www.xiaohongshu.com/discovery/item/6ae972260000000015020d84?type=normal&xsec_token=abc";
|
|
739
|
+
const originalRequest = https.request;
|
|
740
|
+
https.request = (options, responseCallback) => {
|
|
741
|
+
const request = new EventEmitter();
|
|
742
|
+
request.setTimeout = () => {};
|
|
743
|
+
request.end = () => {
|
|
744
|
+
const response = new EventEmitter();
|
|
745
|
+
response.statusCode = 302;
|
|
746
|
+
response.headers = { location: finalUrl };
|
|
747
|
+
response.resume = () => {};
|
|
748
|
+
responseCallback(response);
|
|
749
|
+
};
|
|
750
|
+
request.destroy = () => {};
|
|
751
|
+
return request;
|
|
752
|
+
};
|
|
753
|
+
|
|
754
|
+
try {
|
|
755
|
+
const url = await resolveXhsShortLink("https://xhslink.cn/o/85lbqBJtO94");
|
|
756
|
+
assert.equal(url, finalUrl);
|
|
757
|
+
} finally {
|
|
758
|
+
https.request = originalRequest;
|
|
759
|
+
}
|
|
760
|
+
});
|
|
761
|
+
|
|
762
|
+
test("xhs text formatters: 评论按赞排序并展开楼内回复,搜索按赞排序", () => {
|
|
763
|
+
const { commentsTextFormatter, searchTextFormatter, noteTextFormatter } = require("./commands/xhs");
|
|
764
|
+
|
|
765
|
+
const commentsText = commentsTextFormatter({
|
|
766
|
+
data: {
|
|
767
|
+
has_more: true,
|
|
768
|
+
total_pages: 1,
|
|
769
|
+
comments: [
|
|
770
|
+
{ content: "低赞", like_count: "1", sub_comment_count: "0", user: { original_name: "甲" } },
|
|
771
|
+
{
|
|
772
|
+
content: "高赞",
|
|
773
|
+
like_count: "43",
|
|
774
|
+
sub_comment_count: "1",
|
|
775
|
+
user: { original_name: "乙" },
|
|
776
|
+
sub_comments: [{ content: "楼内回复", user: { original_name: "丙" } }],
|
|
777
|
+
},
|
|
778
|
+
],
|
|
779
|
+
},
|
|
780
|
+
});
|
|
781
|
+
assert.match(commentsText, /共取到 2 条评论/);
|
|
782
|
+
assert.ok(commentsText.indexOf("高赞") < commentsText.indexOf("低赞"), "评论应按点赞降序");
|
|
783
|
+
assert.match(commentsText, /↳ 丙: 楼内回复/);
|
|
784
|
+
assert.match(commentsText, /还有更多评论/);
|
|
785
|
+
|
|
786
|
+
const searchText = searchTextFormatter({
|
|
787
|
+
data: {
|
|
788
|
+
has_more: false,
|
|
789
|
+
items: [
|
|
790
|
+
{ noteInfo: { title: "低", likeNum: 5, favNum: 1, cmtNum: 0, noteType: "normal" }, userInfo: { nickName: "A" } },
|
|
791
|
+
{ noteInfo: { title: "高", likeNum: 900, favNum: 10, cmtNum: 3, noteType: "video" }, userInfo: { nickName: "B" } },
|
|
792
|
+
],
|
|
793
|
+
},
|
|
794
|
+
});
|
|
795
|
+
assert.ok(searchText.indexOf("高") < searchText.indexOf("低"), "搜索结果应按点赞降序");
|
|
796
|
+
|
|
797
|
+
// 时间口径:上游是 UTC epoch,展示必须 +8(独立来源:+8h 等于上游 notePublishTime)
|
|
798
|
+
const noteText = noteTextFormatter({ note_data: { title: "T", type: "normal", time: 1793578330 } });
|
|
799
|
+
assert.match(noteText, /2026-11-02 08:12:10/);
|
|
800
|
+
});
|
|
801
|
+
|
|
802
|
+
test("urlToMarkdown posts the url with a Bearer header and strips billing", async () => {
|
|
803
|
+
const request = await captureRequest(
|
|
804
|
+
() => urlToMarkdown("test-key", { url: " https://mp.weixin.qq.com/s?__biz=M1 " }),
|
|
805
|
+
{
|
|
806
|
+
success: true,
|
|
807
|
+
message: "转换成功",
|
|
808
|
+
markdown: "# 标题\n\n正文段落",
|
|
809
|
+
billing: { service_fee: 0.02, transaction_id: 9 },
|
|
810
|
+
}
|
|
811
|
+
);
|
|
812
|
+
|
|
813
|
+
assert.equal(request.options.path, "/api/url_to_markdown");
|
|
814
|
+
assert.equal(request.options.port, 443);
|
|
815
|
+
assert.equal(request.options.headers.Authorization, "Bearer test-key");
|
|
816
|
+
assert.deepEqual(request.body, { url: "https://mp.weixin.qq.com/s?__biz=M1" });
|
|
817
|
+
assert.equal(request.result.billing, undefined);
|
|
818
|
+
assert.equal(request.result.markdown, "# 标题\n\n正文段落");
|
|
819
|
+
});
|
|
820
|
+
|
|
821
|
+
test("urlToMarkdown rejects a blank url without calling the endpoint", async () => {
|
|
822
|
+
await assert.rejects(() => urlToMarkdown("test-key", { url: " " }), { type: "validation_error" });
|
|
823
|
+
await assert.rejects(() => urlToMarkdown("test-key", {}), { type: "validation_error" });
|
|
824
|
+
});
|
|
825
|
+
|
|
826
|
+
test("urlToMarkdown only forwards an explicit timeout", async () => {
|
|
827
|
+
const withTimeout = await captureRequest(
|
|
828
|
+
() => urlToMarkdown("test-key", { url: "https://example.com/a", timeout: "45" }),
|
|
829
|
+
{ success: true, markdown: "x", billing: {} }
|
|
830
|
+
);
|
|
831
|
+
assert.deepEqual(withTimeout.body, { url: "https://example.com/a", timeout: 45 });
|
|
832
|
+
|
|
833
|
+
const withoutTimeout = await captureRequest(
|
|
834
|
+
() => urlToMarkdown("test-key", { url: "https://example.com/a" }),
|
|
835
|
+
{ success: true, markdown: "x", billing: {} }
|
|
836
|
+
);
|
|
837
|
+
assert.deepEqual(withoutTimeout.body, { url: "https://example.com/a" });
|
|
838
|
+
});
|
|
839
|
+
|
|
840
|
+
test("formatToMdText previews frontmatter and truncates long bodies", () => {
|
|
841
|
+
const body = Array.from({ length: 30 }, (_, i) => `第${i + 1}行`).join("\n");
|
|
842
|
+
const text = formatToMdText({
|
|
843
|
+
markdown: `---\ntitle: "测试标题"\nword_count: 1120\n---\n${body}`,
|
|
844
|
+
});
|
|
845
|
+
assert.match(text, /✓ 已转换为 Markdown/);
|
|
846
|
+
assert.match(text, /标题: 测试标题/);
|
|
847
|
+
assert.match(text, /字数: 1120/);
|
|
848
|
+
assert.match(text, /第1行/);
|
|
849
|
+
assert.doesNotMatch(text, /第25行/, "超过 20 行应被截断");
|
|
850
|
+
assert.match(text, /省略/);
|
|
851
|
+
});
|
|
852
|
+
|
|
853
|
+
test("formatToMdText handles empty and frontmatter-less markdown", () => {
|
|
854
|
+
assert.equal(formatToMdText({ markdown: " " }), "✗ 未取到 Markdown 内容");
|
|
855
|
+
const plain = formatToMdText({ markdown: "# 只有正文" });
|
|
856
|
+
assert.match(plain, /字符: 6/);
|
|
857
|
+
assert.doesNotMatch(plain, /标题:/);
|
|
858
|
+
});
|
|
859
|
+
|
|
860
|
+
test("readFrontmatter returns empty for malformed blocks", () => {
|
|
861
|
+
assert.equal(readFrontmatter("no frontmatter", "title"), "");
|
|
862
|
+
assert.equal(readFrontmatter("---\ntitle: 没闭合", "title"), "");
|
|
863
|
+
assert.equal(readFrontmatter('---\nauthor: "张三"\n---\n正文', "author"), "张三");
|
|
864
|
+
});
|
|
865
|
+
|
|
866
|
+
test("douyin hot endpoints post api_key in the body and map their paths", async () => {
|
|
867
|
+
const words = await captureRequest(() => douyinHotWords("test-key"), { success: true, data: {} });
|
|
868
|
+
assert.equal(words.options.path, "/api/douyin_hot_words");
|
|
869
|
+
assert.deepEqual(words.body, { api_key: "test-key" });
|
|
870
|
+
|
|
871
|
+
const topics = await captureRequest(() => douyinHotTopics("test-key"), { success: true, data: {} });
|
|
872
|
+
assert.equal(topics.options.path, "/api/douyin_hot_topics");
|
|
873
|
+
assert.deepEqual(topics.body, { api_key: "test-key" });
|
|
874
|
+
|
|
875
|
+
const rising = await captureRequest(() => douyinRisingTopics("test-key", 5), { success: true, data: {} });
|
|
876
|
+
assert.equal(rising.options.path, "/api/douyin_rising_topics");
|
|
877
|
+
assert.deepEqual(rising.body, { api_key: "test-key", limit: 5 });
|
|
878
|
+
|
|
879
|
+
const risingDefault = await captureRequest(() => douyinRisingTopics("test-key"), { success: true, data: {} });
|
|
880
|
+
assert.deepEqual(risingDefault.body, { api_key: "test-key" });
|
|
881
|
+
});
|
|
882
|
+
|
|
883
|
+
test("buildTopicRadarResult keeps whatever segment succeeded", () => {
|
|
884
|
+
const result = buildTopicRadarResult(
|
|
885
|
+
{ status: "fulfilled", value: { data: { words: [{ keyword: "美食" }] } } },
|
|
886
|
+
{ status: "rejected", reason: Object.assign(new Error("热点挂了"), { type: "api_error" }) },
|
|
887
|
+
{ status: "fulfilled", value: { data: { items: [{ sentence: "卡点" }] } } }
|
|
888
|
+
);
|
|
889
|
+
|
|
890
|
+
assert.equal(result.success, true);
|
|
891
|
+
assert.deepEqual(result.data.hot_words, [{ keyword: "美食" }]);
|
|
892
|
+
assert.deepEqual(result.data.rising, [{ sentence: "卡点" }]);
|
|
893
|
+
assert.deepEqual(result.data.hot_topics, []);
|
|
894
|
+
assert.equal(result.data.failed_sources.length, 1);
|
|
895
|
+
assert.equal(result.data.failed_sources[0].service, "hot_topics");
|
|
896
|
+
assert.equal(result.data.failed_sources[0].message, "热点挂了");
|
|
897
|
+
assert.match(result.message, /部分数据源不可用/);
|
|
898
|
+
});
|
|
899
|
+
|
|
900
|
+
test("buildTopicRadarResult throws api_error only when every segment fails", () => {
|
|
901
|
+
const rejected = (message) => ({ status: "rejected", reason: new Error(message) });
|
|
902
|
+
assert.throws(
|
|
903
|
+
() => buildTopicRadarResult(rejected("a"), rejected("b"), rejected("c")),
|
|
904
|
+
(err) => err.type === "api_error" && /均获取失败/.test(err.message)
|
|
905
|
+
);
|
|
906
|
+
});
|
|
907
|
+
|
|
908
|
+
test("formatTopicRadarText renders every section and failed sources", () => {
|
|
909
|
+
const text = formatTopicRadarText({
|
|
910
|
+
message: "选题雷达已生成",
|
|
911
|
+
data: {
|
|
912
|
+
hot_words: [{ keyword: "美食", hot_index: 100, trend_label: "升温" }],
|
|
913
|
+
hot_topics: [{ topic_name: "开拓者惨败马刺", category: "体育", vv: 33860082, content_cnt: 15 }],
|
|
914
|
+
rocketing_topics: [{ topic_name: "卡点", vv: 3686735 }],
|
|
915
|
+
rising: [{
|
|
916
|
+
sentence: "卡点视频",
|
|
917
|
+
hot_score: 7620270,
|
|
918
|
+
video_count: 2,
|
|
919
|
+
rank_diff: 3,
|
|
920
|
+
trend: [
|
|
921
|
+
{ datetime: "2026-11-08 10", hot_score: 7640313 },
|
|
922
|
+
{ datetime: "2026-11-08 16", hot_score: 7638220 },
|
|
923
|
+
],
|
|
924
|
+
}],
|
|
925
|
+
failed_sources: [{ service: "hot_topics", message: "超时" }],
|
|
926
|
+
},
|
|
927
|
+
});
|
|
928
|
+
|
|
929
|
+
assert.match(text, /抖音选题雷达/);
|
|
930
|
+
assert.match(text, /【热词】共 1 条/);
|
|
931
|
+
assert.match(text, /美食 热度 100 · 升温/);
|
|
932
|
+
assert.match(text, /【当前热点】共 1 条/);
|
|
933
|
+
assert.match(text, /\[体育\] 开拓者惨败马刺/);
|
|
934
|
+
assert.match(text, /【飙升话题】共 1 条/);
|
|
935
|
+
assert.match(text, /【飙升热点】共 1 条/);
|
|
936
|
+
assert.match(text, /趋势: 2026-11-08 10 7640313 → 2026-11-08 16 7638220/);
|
|
937
|
+
assert.match(text, /失败数据源:/);
|
|
938
|
+
assert.match(text, /hot_topics: 超时/);
|
|
939
|
+
});
|
|
940
|
+
|
|
941
|
+
test("douyinVideoOriginal posts video_url with api_key and strips billing", async () => {
|
|
942
|
+
const request = await captureRequest(
|
|
943
|
+
() => douyinVideoOriginal("test-key", "https://www.douyin.com/video/7669664934044795354"),
|
|
944
|
+
{
|
|
945
|
+
success: true,
|
|
946
|
+
message: "原画直链获取成功(219.1 MB)",
|
|
947
|
+
data: { video_id: "7669664934044795354", file_size_mb: 219.11 },
|
|
948
|
+
billing: { service_fee: 0.1 },
|
|
949
|
+
}
|
|
950
|
+
);
|
|
951
|
+
assert.equal(request.options.path, "/api/douyin_video_original");
|
|
952
|
+
assert.deepEqual(request.body, {
|
|
953
|
+
api_key: "test-key",
|
|
954
|
+
video_url: "https://www.douyin.com/video/7669664934044795354",
|
|
955
|
+
});
|
|
956
|
+
assert.equal(request.result.billing, undefined);
|
|
957
|
+
assert.equal(request.result.data.file_size_mb, 219.11);
|
|
958
|
+
});
|
|
959
|
+
|
|
960
|
+
test("formatVideoOriginalText renders size, link and expiry hint", () => {
|
|
961
|
+
const text = formatVideoOriginalText({
|
|
962
|
+
message: "原画直链获取成功(219.1 MB)",
|
|
963
|
+
data: {
|
|
964
|
+
video_id: "7669664934044795354",
|
|
965
|
+
url: "https://v5-dy-ov-experiment.zjcdn.com/orig.mp4",
|
|
966
|
+
file_size_mb: 219.11,
|
|
967
|
+
content_type: "video/mp4",
|
|
968
|
+
},
|
|
969
|
+
});
|
|
970
|
+
assert.match(text, /抖音原画直链/);
|
|
971
|
+
assert.match(text, /作品: 7669664934044795354/);
|
|
972
|
+
assert.match(text, /大小: 219\.11 MB/);
|
|
973
|
+
assert.match(text, /类型: video\/mp4/);
|
|
974
|
+
assert.match(text, /直链: https:\/\/v5-dy-ov-experiment/);
|
|
975
|
+
assert.match(text, /直链有时效/);
|
|
976
|
+
});
|
|
977
|
+
|
|
978
|
+
test("pgy endpoints switch between user_id and profile_url", async () => {
|
|
979
|
+
const byId = await captureRequest(
|
|
980
|
+
() => xhsPgyBlogger("test-key", "5c5f8f4c000000001801612c"),
|
|
981
|
+
{ success: true, data: {} }
|
|
982
|
+
);
|
|
983
|
+
assert.equal(byId.options.path, "/api/xhs_pgy_blogger");
|
|
984
|
+
assert.deepEqual(byId.body, { api_key: "test-key", user_id: "5c5f8f4c000000001801612c" });
|
|
985
|
+
|
|
986
|
+
const byUrl = await captureRequest(
|
|
987
|
+
() => xhsPgyAudience("test-key", " https://www.xiaohongshu.com/user/profile/5c5f8f4c000000001801612c "),
|
|
988
|
+
{ success: true, data: {} }
|
|
989
|
+
);
|
|
990
|
+
assert.equal(byUrl.options.path, "/api/xhs_pgy_audience");
|
|
991
|
+
assert.equal(byUrl.body.profile_url, "https://www.xiaohongshu.com/user/profile/5c5f8f4c000000001801612c");
|
|
992
|
+
assert.equal(byUrl.body.user_id, undefined);
|
|
993
|
+
|
|
994
|
+
const performance = await captureRequest(() => xhsPgyPerformance("test-key", "5c5f8f4c000000001801612c"), { success: true, data: {} });
|
|
995
|
+
assert.equal(performance.options.path, "/api/xhs_pgy_performance");
|
|
996
|
+
});
|
|
997
|
+
|
|
998
|
+
test("pgy endpoints reject a blank input before calling", async () => {
|
|
999
|
+
await assert.rejects(() => xhsPgyBlogger("test-key", " "), { type: "validation_error" });
|
|
1000
|
+
await assert.rejects(() => xhsPgyPerformance("test-key", undefined), { type: "validation_error" });
|
|
1001
|
+
});
|
|
1002
|
+
|
|
1003
|
+
test("buildDarenReportResult merges two segments and scores them", () => {
|
|
1004
|
+
const result = buildDarenReportResult(
|
|
1005
|
+
{ status: "fulfilled", value: { data: { name: "十七楼.", fans_count: 4205 } } },
|
|
1006
|
+
{ status: "rejected", reason: new Error("画像挂了") },
|
|
1007
|
+
{ status: "fulfilled", value: { data: { interaction_rate: 25.6, hundred_like_percent: 72.4 } } }
|
|
1008
|
+
);
|
|
1009
|
+
|
|
1010
|
+
assert.equal(result.success, true);
|
|
1011
|
+
assert.equal(result.data.profile.name, "十七楼.");
|
|
1012
|
+
assert.equal(result.data.partial, true);
|
|
1013
|
+
assert.equal(result.data.failed_sources.length, 1);
|
|
1014
|
+
assert.equal(result.data.failed_sources[0].service, "audience");
|
|
1015
|
+
// 粉丝 4205 → 10/40(≥1千 档);互动率 25.6 → 30/30;百赞率 72.4 → 30/30 → 70/100
|
|
1016
|
+
assert.equal(result.data.score.value, 70);
|
|
1017
|
+
assert.equal(result.data.score.tier, "腰部");
|
|
1018
|
+
assert.equal(result.data.score.basis.length, 3);
|
|
1019
|
+
});
|
|
1020
|
+
|
|
1021
|
+
test("buildDarenReportResult fails when fewer than two segments succeed", () => {
|
|
1022
|
+
const rejected = (message) => ({ status: "rejected", reason: new Error(message) });
|
|
1023
|
+
assert.throws(
|
|
1024
|
+
() => buildDarenReportResult(
|
|
1025
|
+
{ status: "fulfilled", value: { data: {} } },
|
|
1026
|
+
rejected("b"),
|
|
1027
|
+
rejected("c")
|
|
1028
|
+
),
|
|
1029
|
+
(err) => err.type === "api_error" && /达人数据获取不完整/.test(err.message)
|
|
1030
|
+
);
|
|
1031
|
+
});
|
|
1032
|
+
|
|
1033
|
+
test("formatDarenReportText renders profile, audience, performance and score", () => {
|
|
1034
|
+
const text = formatDarenReportText({
|
|
1035
|
+
data: {
|
|
1036
|
+
profile: { name: "十七楼.", red_id: "615758825", location: "江苏 南京", fans_count: 4205, business_note_count: 107 },
|
|
1037
|
+
audience: {
|
|
1038
|
+
gender: { male: 0.25147, female: 0.74853 },
|
|
1039
|
+
ages: [{ group: "25-34", percent: 0.5076 }],
|
|
1040
|
+
interests: [{ name: "美食", percent: 0.1339 }],
|
|
1041
|
+
},
|
|
1042
|
+
performance: {
|
|
1043
|
+
note_number: 29,
|
|
1044
|
+
hundred_like_percent: 72.4,
|
|
1045
|
+
imp_median: 2690,
|
|
1046
|
+
imp_median_beyond_rate: 88.4,
|
|
1047
|
+
read_median: 341,
|
|
1048
|
+
read_median_beyond_rate: 84.9,
|
|
1049
|
+
interaction_rate: 25.6,
|
|
1050
|
+
interaction_beyond_rate: 99.0,
|
|
1051
|
+
},
|
|
1052
|
+
score: { value: 78, tier: "腰部", partial: true, basis: [{ item: "粉丝数", got: 18, max: 40 }] },
|
|
1053
|
+
failed_sources: [{ service: "audience", message: "超时" }],
|
|
1054
|
+
},
|
|
1055
|
+
});
|
|
1056
|
+
|
|
1057
|
+
assert.match(text, /小红书达人报告/);
|
|
1058
|
+
assert.match(text, /达人: 十七楼\.(小红书号 615758825)/);
|
|
1059
|
+
assert.match(text, /性别: 女 74\.9% \/ 男 25\.1%/);
|
|
1060
|
+
assert.match(text, /年龄: 25-34 50\.8%/);
|
|
1061
|
+
assert.match(text, /百赞率: 72\.4%/);
|
|
1062
|
+
assert.match(text, /互动率: 25\.6%(超越 99%)/);
|
|
1063
|
+
assert.match(text, /【评分】78 分 · 腰部(部分维度缺失,已归一)/);
|
|
1064
|
+
assert.match(text, /失败数据源:/);
|
|
1065
|
+
});
|