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/commands/content.js
CHANGED
|
@@ -1,184 +1,184 @@
|
|
|
1
|
-
const { hotList, xhsHotList } = require("../api");
|
|
2
|
-
const { outputResult, outputError, resolveKeyOrExit, handleApiError } = require("../lib/cli-utils");
|
|
3
|
-
|
|
4
|
-
const DEFAULT_HOT_LIST_SOURCES = ["zhihu", "weibo", "douyin", "aihot"];
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* 解析并去重逗号分隔的平台来源 ID。
|
|
8
|
-
* @param {string|undefined} value
|
|
9
|
-
* @returns {string[]}
|
|
10
|
-
*/
|
|
11
|
-
function parseSourceIds(value) {
|
|
12
|
-
const rawSources = value === undefined ? DEFAULT_HOT_LIST_SOURCES : String(value).split(",");
|
|
13
|
-
const sourceIds = [...new Set(rawSources.map((source) => source.trim()).filter(Boolean))];
|
|
14
|
-
if (sourceIds.length === 0) {
|
|
15
|
-
throw Object.assign(new Error("--sources 至少需要提供一个来源 ID"), { type: "validation_error" });
|
|
16
|
-
}
|
|
17
|
-
if (sourceIds.length > 52) {
|
|
18
|
-
throw Object.assign(new Error("--sources 最多支持 52 个来源"), { type: "validation_error" });
|
|
19
|
-
}
|
|
20
|
-
return sourceIds;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
/**
|
|
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
|
-
|
|
38
|
-
function formatTimestamp(value) {
|
|
39
|
-
if (typeof value !== "number" || !Number.isFinite(value)) return value || "";
|
|
40
|
-
return new Date(value).toISOString();
|
|
41
|
-
}
|
|
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
|
-
|
|
88
|
-
/**
|
|
89
|
-
* text 模式的热点简报格式化。
|
|
90
|
-
* @param {object} result
|
|
91
|
-
* @returns {string}
|
|
92
|
-
*/
|
|
93
|
-
function formatHotListText(result = {}) {
|
|
94
|
-
const data = result.data || {};
|
|
95
|
-
const sources = data.sources || [];
|
|
96
|
-
const failedSources = data.failed_sources || [];
|
|
97
|
-
const lines = ["热点选题简报"];
|
|
98
|
-
|
|
99
|
-
if (!sources.length) {
|
|
100
|
-
lines.push("没有获取到可用热点来源。");
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
sources.forEach((source) => {
|
|
104
|
-
const status = source.status === "cache" ? "缓存" : source.status || "未知状态";
|
|
105
|
-
lines.push(`\n【${source.source_id}】${status}`);
|
|
106
|
-
if (source.updated_time) lines.push(`更新时间: ${formatTimestamp(source.updated_time)}`);
|
|
107
|
-
(source.items || []).forEach((item, index) => {
|
|
108
|
-
lines.push(`[${index + 1}] ${item.title || "未命名条目"}`);
|
|
109
|
-
if (item.url) lines.push(` URL: ${item.url}`);
|
|
110
|
-
if (item.pub_date) lines.push(` 发布时间: ${item.pub_date}`);
|
|
111
|
-
});
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
if (failedSources.length) {
|
|
115
|
-
lines.push("\n失败来源:");
|
|
116
|
-
failedSources.forEach((source) => {
|
|
117
|
-
lines.push(`- ${source.source_id}: ${source.message || "未知错误"}`);
|
|
118
|
-
});
|
|
119
|
-
}
|
|
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
|
-
|
|
143
|
-
return lines.join("\n").trimEnd();
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
/**
|
|
147
|
-
* content briefing 命令。
|
|
148
|
-
* @param {{ apiKey?: string, sources?: string, limit?: string, format?: "json"|"text" }} options
|
|
149
|
-
* @returns {Promise<void>}
|
|
150
|
-
*/
|
|
151
|
-
async function briefingCommand(options = {}) {
|
|
152
|
-
const format = options.format || "json";
|
|
153
|
-
const apiKey = resolveKeyOrExit(options.apiKey, format);
|
|
154
|
-
|
|
155
|
-
try {
|
|
156
|
-
const hotListParams = {
|
|
157
|
-
sourceIds: parseSourceIds(options.sources),
|
|
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);
|
|
165
|
-
outputResult(result, format, formatHotListText);
|
|
166
|
-
process.exit(0);
|
|
167
|
-
} catch (err) {
|
|
168
|
-
if (err.type === "validation_error") {
|
|
169
|
-
outputError(err, format);
|
|
170
|
-
} else {
|
|
171
|
-
handleApiError(err, format);
|
|
172
|
-
}
|
|
173
|
-
process.exit(1);
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
module.exports = {
|
|
178
|
-
briefingCommand,
|
|
179
|
-
buildBriefingResult,
|
|
180
|
-
formatHotListText,
|
|
181
|
-
parseLimit,
|
|
182
|
-
parseSourceIds,
|
|
183
|
-
DEFAULT_HOT_LIST_SOURCES,
|
|
184
|
-
};
|
|
1
|
+
const { hotList, xhsHotList } = require("../api");
|
|
2
|
+
const { outputResult, outputError, resolveKeyOrExit, handleApiError } = require("../lib/cli-utils");
|
|
3
|
+
|
|
4
|
+
const DEFAULT_HOT_LIST_SOURCES = ["zhihu", "weibo", "douyin", "aihot"];
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* 解析并去重逗号分隔的平台来源 ID。
|
|
8
|
+
* @param {string|undefined} value
|
|
9
|
+
* @returns {string[]}
|
|
10
|
+
*/
|
|
11
|
+
function parseSourceIds(value) {
|
|
12
|
+
const rawSources = value === undefined ? DEFAULT_HOT_LIST_SOURCES : String(value).split(",");
|
|
13
|
+
const sourceIds = [...new Set(rawSources.map((source) => source.trim()).filter(Boolean))];
|
|
14
|
+
if (sourceIds.length === 0) {
|
|
15
|
+
throw Object.assign(new Error("--sources 至少需要提供一个来源 ID"), { type: "validation_error" });
|
|
16
|
+
}
|
|
17
|
+
if (sourceIds.length > 52) {
|
|
18
|
+
throw Object.assign(new Error("--sources 最多支持 52 个来源"), { type: "validation_error" });
|
|
19
|
+
}
|
|
20
|
+
return sourceIds;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
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
|
+
|
|
38
|
+
function formatTimestamp(value) {
|
|
39
|
+
if (typeof value !== "number" || !Number.isFinite(value)) return value || "";
|
|
40
|
+
return new Date(value).toISOString();
|
|
41
|
+
}
|
|
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
|
+
|
|
88
|
+
/**
|
|
89
|
+
* text 模式的热点简报格式化。
|
|
90
|
+
* @param {object} result
|
|
91
|
+
* @returns {string}
|
|
92
|
+
*/
|
|
93
|
+
function formatHotListText(result = {}) {
|
|
94
|
+
const data = result.data || {};
|
|
95
|
+
const sources = data.sources || [];
|
|
96
|
+
const failedSources = data.failed_sources || [];
|
|
97
|
+
const lines = ["热点选题简报"];
|
|
98
|
+
|
|
99
|
+
if (!sources.length) {
|
|
100
|
+
lines.push("没有获取到可用热点来源。");
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
sources.forEach((source) => {
|
|
104
|
+
const status = source.status === "cache" ? "缓存" : source.status || "未知状态";
|
|
105
|
+
lines.push(`\n【${source.source_id}】${status}`);
|
|
106
|
+
if (source.updated_time) lines.push(`更新时间: ${formatTimestamp(source.updated_time)}`);
|
|
107
|
+
(source.items || []).forEach((item, index) => {
|
|
108
|
+
lines.push(`[${index + 1}] ${item.title || "未命名条目"}`);
|
|
109
|
+
if (item.url) lines.push(` URL: ${item.url}`);
|
|
110
|
+
if (item.pub_date) lines.push(` 发布时间: ${item.pub_date}`);
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
if (failedSources.length) {
|
|
115
|
+
lines.push("\n失败来源:");
|
|
116
|
+
failedSources.forEach((source) => {
|
|
117
|
+
lines.push(`- ${source.source_id}: ${source.message || "未知错误"}`);
|
|
118
|
+
});
|
|
119
|
+
}
|
|
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
|
+
|
|
143
|
+
return lines.join("\n").trimEnd();
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* content briefing 命令。
|
|
148
|
+
* @param {{ apiKey?: string, sources?: string, limit?: string, format?: "json"|"text" }} options
|
|
149
|
+
* @returns {Promise<void>}
|
|
150
|
+
*/
|
|
151
|
+
async function briefingCommand(options = {}) {
|
|
152
|
+
const format = options.format || "json";
|
|
153
|
+
const apiKey = resolveKeyOrExit(options.apiKey, format);
|
|
154
|
+
|
|
155
|
+
try {
|
|
156
|
+
const hotListParams = {
|
|
157
|
+
sourceIds: parseSourceIds(options.sources),
|
|
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);
|
|
165
|
+
outputResult(result, format, formatHotListText);
|
|
166
|
+
process.exit(0);
|
|
167
|
+
} catch (err) {
|
|
168
|
+
if (err.type === "validation_error") {
|
|
169
|
+
outputError(err, format);
|
|
170
|
+
} else {
|
|
171
|
+
handleApiError(err, format);
|
|
172
|
+
}
|
|
173
|
+
process.exit(1);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
module.exports = {
|
|
178
|
+
briefingCommand,
|
|
179
|
+
buildBriefingResult,
|
|
180
|
+
formatHotListText,
|
|
181
|
+
parseLimit,
|
|
182
|
+
parseSourceIds,
|
|
183
|
+
DEFAULT_HOT_LIST_SOURCES,
|
|
184
|
+
};
|
package/src/commands/douyin.js
CHANGED
|
@@ -6,6 +6,10 @@ const {
|
|
|
6
6
|
douyinUserSearch,
|
|
7
7
|
douyinVideoSearch,
|
|
8
8
|
douyinLinkToText,
|
|
9
|
+
douyinHotWords,
|
|
10
|
+
douyinHotTopics,
|
|
11
|
+
douyinRisingTopics,
|
|
12
|
+
douyinVideoOriginal,
|
|
9
13
|
} = require("../api");
|
|
10
14
|
const { outputResult, resolveKeyOrExit, handleApiError, safeParseInt } = require("../lib/cli-utils");
|
|
11
15
|
|
|
@@ -98,6 +102,171 @@ async function linkToTextCommand(videoUrl, options = {}) {
|
|
|
98
102
|
} catch (err) { handleApiError(err, format); process.exit(1); }
|
|
99
103
|
}
|
|
100
104
|
|
|
105
|
+
/**
|
|
106
|
+
* 把原子接口的业务失败(success:false,不抛异常)变成 rejected,让 allSettled 能识别。
|
|
107
|
+
* @param {object} resp
|
|
108
|
+
* @param {string} service
|
|
109
|
+
* @returns {object}
|
|
110
|
+
*/
|
|
111
|
+
function assertSegmentSucceeded(resp, service) {
|
|
112
|
+
if (resp && resp.success === false) {
|
|
113
|
+
throw Object.assign(
|
|
114
|
+
new Error(resp.message || `${service} 获取失败`),
|
|
115
|
+
{ type: "api_error", service, body: resp }
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
return resp;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function segmentErrorMessage(reason) {
|
|
122
|
+
if (reason?.body?.message) return reason.body.message;
|
|
123
|
+
return reason?.message || String(reason || "未知错误");
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* 把三个原子接口的并行结果合并成选题雷达响应。
|
|
128
|
+
* 任一段成功即 success;全失败抛 api_error。
|
|
129
|
+
* @param {PromiseSettledResult<object>} wordsResult
|
|
130
|
+
* @param {PromiseSettledResult<object>} topicsResult
|
|
131
|
+
* @param {PromiseSettledResult<object>} risingResult
|
|
132
|
+
* @returns {object}
|
|
133
|
+
*/
|
|
134
|
+
function buildTopicRadarResult(wordsResult, topicsResult, risingResult) {
|
|
135
|
+
const wordsOk = wordsResult.status === "fulfilled";
|
|
136
|
+
const topicsOk = topicsResult.status === "fulfilled";
|
|
137
|
+
const risingOk = risingResult.status === "fulfilled";
|
|
138
|
+
|
|
139
|
+
if (!wordsOk && !topicsOk && !risingOk) {
|
|
140
|
+
throw Object.assign(
|
|
141
|
+
new Error(
|
|
142
|
+
`热词榜、热点榜与飙升榜均获取失败: ${segmentErrorMessage(wordsResult.reason)}; ` +
|
|
143
|
+
`${segmentErrorMessage(topicsResult.reason)}; ${segmentErrorMessage(risingResult.reason)}`
|
|
144
|
+
),
|
|
145
|
+
{ type: "api_error" }
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const failedSources = [];
|
|
150
|
+
if (!wordsOk) failedSources.push({ service: "hot_words", message: segmentErrorMessage(wordsResult.reason) });
|
|
151
|
+
if (!topicsOk) failedSources.push({ service: "hot_topics", message: segmentErrorMessage(topicsResult.reason) });
|
|
152
|
+
if (!risingOk) failedSources.push({ service: "rising_topics", message: segmentErrorMessage(risingResult.reason) });
|
|
153
|
+
|
|
154
|
+
const wordsData = wordsOk ? wordsResult.value.data || {} : {};
|
|
155
|
+
const topicsData = topicsOk ? topicsResult.value.data || {} : {};
|
|
156
|
+
const risingData = risingOk ? risingResult.value.data || {} : {};
|
|
157
|
+
|
|
158
|
+
return {
|
|
159
|
+
success: true,
|
|
160
|
+
message: failedSources.length
|
|
161
|
+
? "选题雷达已生成,部分数据源不可用"
|
|
162
|
+
: "选题雷达已生成",
|
|
163
|
+
data: {
|
|
164
|
+
generated_at: new Date().toISOString(),
|
|
165
|
+
hot_words: wordsData.words || [],
|
|
166
|
+
hot_topics: topicsData.current || [],
|
|
167
|
+
rocketing_topics: topicsData.rocketing || [],
|
|
168
|
+
rising: risingData.items || [],
|
|
169
|
+
failed_sources: failedSources,
|
|
170
|
+
},
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* 抖音选题雷达(CLI 侧组合三个原子接口,不新增后端路由)
|
|
176
|
+
* @param {object} options
|
|
177
|
+
*/
|
|
178
|
+
async function topicRadarCommand(options = {}) {
|
|
179
|
+
const format = options.format || "json";
|
|
180
|
+
const apiKey = resolveKeyOrExit(options.apiKey, format);
|
|
181
|
+
const limit = safeParseInt(options.limit, undefined);
|
|
182
|
+
try {
|
|
183
|
+
const [wordsResult, topicsResult, risingResult] = await Promise.allSettled([
|
|
184
|
+
douyinHotWords(apiKey).then((r) => assertSegmentSucceeded(r, "hot_words")),
|
|
185
|
+
douyinHotTopics(apiKey).then((r) => assertSegmentSucceeded(r, "hot_topics")),
|
|
186
|
+
douyinRisingTopics(apiKey, limit).then((r) => assertSegmentSucceeded(r, "rising_topics")),
|
|
187
|
+
]);
|
|
188
|
+
const result = buildTopicRadarResult(wordsResult, topicsResult, risingResult);
|
|
189
|
+
outputResult(result, format, formatTopicRadarText);
|
|
190
|
+
process.exit(0);
|
|
191
|
+
} catch (err) { handleApiError(err, format); process.exit(1); }
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* text 输出:选题雷达
|
|
196
|
+
* @param {object} result
|
|
197
|
+
* @returns {string}
|
|
198
|
+
*/
|
|
199
|
+
function formatTopicRadarText(result = {}) {
|
|
200
|
+
const data = result.data || {};
|
|
201
|
+
const lines = ["✓ 抖音选题雷达"];
|
|
202
|
+
if (result.message) lines.push(` ${result.message}`);
|
|
203
|
+
|
|
204
|
+
const words = data.hot_words || [];
|
|
205
|
+
if (words.length) lines.push("", `【热词】共 ${words.length} 条`);
|
|
206
|
+
words.slice(0, 5).forEach((item, index) => {
|
|
207
|
+
lines.push(` ${index + 1}. ${item.keyword} 热度 ${item.hot_index} · ${item.trend_label}`);
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
const topics = data.hot_topics || [];
|
|
211
|
+
if (topics.length) lines.push("", `【当前热点】共 ${topics.length} 条`);
|
|
212
|
+
topics.slice(0, 5).forEach((item, index) => {
|
|
213
|
+
const category = item.category ? `[${item.category}] ` : "";
|
|
214
|
+
lines.push(` ${index + 1}. ${category}${item.topic_name} 播放 ${item.vv} · 内容 ${item.content_cnt}`);
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
const rocketing = data.rocketing_topics || [];
|
|
218
|
+
if (rocketing.length) lines.push("", `【飙升话题】共 ${rocketing.length} 条`);
|
|
219
|
+
rocketing.slice(0, 5).forEach((item, index) => {
|
|
220
|
+
lines.push(` ${index + 1}. ${item.topic_name} 播放 ${item.vv}`);
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
const rising = data.rising || [];
|
|
224
|
+
if (rising.length) lines.push("", `【飙升热点】共 ${rising.length} 条`);
|
|
225
|
+
rising.slice(0, 5).forEach((item, index) => {
|
|
226
|
+
lines.push(` ${index + 1}. ${item.sentence} 热度 ${item.hot_score} · 视频 ${item.video_count} · 上升 ${item.rank_diff}`);
|
|
227
|
+
const trend = item.trend || [];
|
|
228
|
+
if (trend.length >= 2) {
|
|
229
|
+
const first = trend[0];
|
|
230
|
+
const last = trend[trend.length - 1];
|
|
231
|
+
lines.push(` 趋势: ${first.datetime} ${first.hot_score} → ${last.datetime} ${last.hot_score}`);
|
|
232
|
+
}
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
const failed = data.failed_sources || [];
|
|
236
|
+
if (failed.length) {
|
|
237
|
+
lines.push("", "失败数据源:");
|
|
238
|
+
failed.forEach((item) => lines.push(` - ${item.service}: ${item.message}`));
|
|
239
|
+
}
|
|
240
|
+
return lines.join("\n");
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
async function videoOriginalCommand(videoUrl, options = {}) {
|
|
244
|
+
const format = options.format || "json";
|
|
245
|
+
const apiKey = resolveKeyOrExit(options.apiKey, format);
|
|
246
|
+
try {
|
|
247
|
+
const result = await douyinVideoOriginal(apiKey, videoUrl);
|
|
248
|
+
outputResult(result, format, formatVideoOriginalText);
|
|
249
|
+
process.exit(0);
|
|
250
|
+
} catch (err) { handleApiError(err, format); process.exit(1); }
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* text 输出:原画直链
|
|
255
|
+
* @param {object} result
|
|
256
|
+
* @returns {string}
|
|
257
|
+
*/
|
|
258
|
+
function formatVideoOriginalText(result = {}) {
|
|
259
|
+
const data = result.data || {};
|
|
260
|
+
const lines = ["✓ 抖音原画直链"];
|
|
261
|
+
if (result.message) lines.push(` ${result.message}`);
|
|
262
|
+
if (data.video_id) lines.push(` 作品: ${data.video_id}`);
|
|
263
|
+
if (data.file_size_mb !== undefined) lines.push(` 大小: ${data.file_size_mb} MB`);
|
|
264
|
+
if (data.content_type) lines.push(` 类型: ${data.content_type}`);
|
|
265
|
+
if (data.url) lines.push(` 直链: ${data.url}`);
|
|
266
|
+
lines.push(" 提示: 直链有时效,请立即下载");
|
|
267
|
+
return lines.join("\n");
|
|
268
|
+
}
|
|
269
|
+
|
|
101
270
|
module.exports = {
|
|
102
271
|
videoInfoCommand,
|
|
103
272
|
videoCommentsCommand,
|
|
@@ -106,4 +275,9 @@ module.exports = {
|
|
|
106
275
|
userSearchCommand,
|
|
107
276
|
videoSearchCommand,
|
|
108
277
|
linkToTextCommand,
|
|
278
|
+
topicRadarCommand,
|
|
279
|
+
buildTopicRadarResult,
|
|
280
|
+
formatTopicRadarText,
|
|
281
|
+
videoOriginalCommand,
|
|
282
|
+
formatVideoOriginalText,
|
|
109
283
|
};
|
|
@@ -1,54 +1,54 @@
|
|
|
1
|
-
const fs = require("fs");
|
|
2
|
-
const path = require("path");
|
|
3
|
-
const { publishHtml } = require("../api");
|
|
4
|
-
const { outputResult, outputError, resolveKeyOrExit, handleApiError } = require("../lib/cli-utils");
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* 智能解析输入:如果是已存在的文件路径则读取,否则当作原始 HTML 字符串
|
|
8
|
-
* @param {string} input
|
|
9
|
-
* @returns {string}
|
|
10
|
-
*/
|
|
11
|
-
function resolveHtmlInput(input) {
|
|
12
|
-
const trimmed = input.trim();
|
|
13
|
-
try {
|
|
14
|
-
const filePath = path.resolve(trimmed);
|
|
15
|
-
if (fs.existsSync(filePath) && fs.statSync(filePath).isFile()) {
|
|
16
|
-
return fs.readFileSync(filePath, "utf-8");
|
|
17
|
-
}
|
|
18
|
-
} catch (_) {
|
|
19
|
-
// 不是文件路径,按原始字符串处理
|
|
20
|
-
}
|
|
21
|
-
return trimmed;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
/** text 模式的自定义格式化 */
|
|
25
|
-
function formatPublishText(data) {
|
|
26
|
-
let out = "✓ 发布成功\n";
|
|
27
|
-
out += ` URL: ${data.online_url}\n`;
|
|
28
|
-
if (data.message) {
|
|
29
|
-
out += ` 信息: ${data.message}\n`;
|
|
30
|
-
}
|
|
31
|
-
return out.trimEnd();
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* publish 命令(子命令模式)
|
|
36
|
-
* @param {string} htmlCode - HTML 代码或文件路径
|
|
37
|
-
* @param {{ apiKey?: string, format?: string, title?: string }} options
|
|
38
|
-
*/
|
|
39
|
-
async function publishCommand(htmlCode, options = {}) {
|
|
40
|
-
const format = options.format || "json";
|
|
41
|
-
const apiKey = resolveKeyOrExit(options.apiKey, format);
|
|
42
|
-
|
|
43
|
-
try {
|
|
44
|
-
const code = resolveHtmlInput(htmlCode);
|
|
45
|
-
const result = await publishHtml(apiKey, code, options.title);
|
|
46
|
-
outputResult(result, format, formatPublishText);
|
|
47
|
-
process.exit(0);
|
|
48
|
-
} catch (err) {
|
|
49
|
-
handleApiError(err, format);
|
|
50
|
-
process.exit(1);
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
module.exports = { publishCommand };
|
|
1
|
+
const fs = require("fs");
|
|
2
|
+
const path = require("path");
|
|
3
|
+
const { publishHtml } = require("../api");
|
|
4
|
+
const { outputResult, outputError, resolveKeyOrExit, handleApiError } = require("../lib/cli-utils");
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* 智能解析输入:如果是已存在的文件路径则读取,否则当作原始 HTML 字符串
|
|
8
|
+
* @param {string} input
|
|
9
|
+
* @returns {string}
|
|
10
|
+
*/
|
|
11
|
+
function resolveHtmlInput(input) {
|
|
12
|
+
const trimmed = input.trim();
|
|
13
|
+
try {
|
|
14
|
+
const filePath = path.resolve(trimmed);
|
|
15
|
+
if (fs.existsSync(filePath) && fs.statSync(filePath).isFile()) {
|
|
16
|
+
return fs.readFileSync(filePath, "utf-8");
|
|
17
|
+
}
|
|
18
|
+
} catch (_) {
|
|
19
|
+
// 不是文件路径,按原始字符串处理
|
|
20
|
+
}
|
|
21
|
+
return trimmed;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** text 模式的自定义格式化 */
|
|
25
|
+
function formatPublishText(data) {
|
|
26
|
+
let out = "✓ 发布成功\n";
|
|
27
|
+
out += ` URL: ${data.online_url}\n`;
|
|
28
|
+
if (data.message) {
|
|
29
|
+
out += ` 信息: ${data.message}\n`;
|
|
30
|
+
}
|
|
31
|
+
return out.trimEnd();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* publish 命令(子命令模式)
|
|
36
|
+
* @param {string} htmlCode - HTML 代码或文件路径
|
|
37
|
+
* @param {{ apiKey?: string, format?: string, title?: string }} options
|
|
38
|
+
*/
|
|
39
|
+
async function publishCommand(htmlCode, options = {}) {
|
|
40
|
+
const format = options.format || "json";
|
|
41
|
+
const apiKey = resolveKeyOrExit(options.apiKey, format);
|
|
42
|
+
|
|
43
|
+
try {
|
|
44
|
+
const code = resolveHtmlInput(htmlCode);
|
|
45
|
+
const result = await publishHtml(apiKey, code, options.title);
|
|
46
|
+
outputResult(result, format, formatPublishText);
|
|
47
|
+
process.exit(0);
|
|
48
|
+
} catch (err) {
|
|
49
|
+
handleApiError(err, format);
|
|
50
|
+
process.exit(1);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
module.exports = { publishCommand };
|