aiconductor-cli 1.2.0 → 1.2.2

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/src/api.js CHANGED
@@ -1,4 +1,4 @@
1
- const http = require("http");
1
+ const https = require("https");
2
2
  const fs = require("fs");
3
3
  const path = require("path");
4
4
 
@@ -6,6 +6,13 @@ const API_HOST = "plugin.aiconductor.fun";
6
6
  const API_PATH = "/api/html_publish";
7
7
  const MD_TO_HTML_API_PATH = "/api/md_to_html";
8
8
  const SEARCH_API_PATH = "/api/aic_web_search";
9
+ const DOUYIN_VIDEO_INFO_PATH = "/api/douyin_video_info";
10
+ const DOUYIN_VIDEO_COMMENTS_PATH = "/api/douyin_video_comments";
11
+ const DOUYIN_USER_VIDEOS_PATH = "/api/douyin_user_videos";
12
+ const DOUYIN_USER_DETAIL_PATH = "/api/douyin_user_detail";
13
+ const DOUYIN_USER_SEARCH_PATH = "/api/douyin_user_search";
14
+ const DOUYIN_VIDEO_SEARCH_PATH = "/api/douyin_video_search";
15
+ const DOUYIN_LINK_TO_TEXT_PATH = "/api/douyin_link_to_text_v2";
9
16
 
10
17
  /**
11
18
  * 解析 API Key,三级优先级:命令行参数 > 系统环境变量 > .env 文件
@@ -33,6 +40,31 @@ function resolveApiKey(optionKey) {
33
40
  return null;
34
41
  }
35
42
 
43
+ /**
44
+ * 过滤响应中的计费信息
45
+ * @param {object} resp
46
+ * @returns {object}
47
+ */
48
+ function stripBilling(resp) {
49
+ if (!resp || typeof resp !== "object") return resp;
50
+ const result = { ...resp };
51
+ delete result.billing;
52
+ return result;
53
+ }
54
+
55
+ /**
56
+ * 校验整数参数,非有效正整数时报错
57
+ * @param {*} value
58
+ * @param {string} name
59
+ */
60
+ function validateInt(value, name) {
61
+ if (value === undefined || value === null) return;
62
+ const n = Number(value);
63
+ if (!Number.isInteger(n) || n < 0) {
64
+ throw Object.assign(new Error(`${name} 须为非负整数,收到: ${value}`), { type: "validation_error" });
65
+ }
66
+ }
67
+
36
68
  /**
37
69
  * HTTP POST 请求封装
38
70
  * @param {string} host
@@ -46,7 +78,7 @@ function httpPost(host, path, body, headers = {}) {
46
78
 
47
79
  const options = {
48
80
  hostname: host,
49
- port: 80,
81
+ port: 443,
50
82
  path: path,
51
83
  method: "POST",
52
84
  headers: {
@@ -56,7 +88,7 @@ function httpPost(host, path, body, headers = {}) {
56
88
  },
57
89
  };
58
90
 
59
- const req = http.request(options, (res) => {
91
+ const req = https.request(options, (res) => {
60
92
  let chunks = [];
61
93
  res.on("data", (chunk) => chunks.push(chunk));
62
94
  res.on("end", () => {
@@ -105,10 +137,7 @@ async function publishHtml(apiKey, htmlCode, title) {
105
137
  if (title) reqBody.title = title;
106
138
 
107
139
  const resp = await httpPost(API_HOST, API_PATH, reqBody);
108
-
109
- // 过滤计费信息
110
- const { billing, ...safe } = resp;
111
- return safe;
140
+ return stripBilling(resp);
112
141
  }
113
142
 
114
143
  /**
@@ -118,6 +147,10 @@ async function publishHtml(apiKey, htmlCode, title) {
118
147
  * @returns {Promise<object>}
119
148
  */
120
149
  async function webSearch(apiKey, params) {
150
+ // 输入校验
151
+ validateInt(params.count, "count");
152
+ validateInt(params.authLevel, "authLevel");
153
+
121
154
  const reqBody = {
122
155
  query: params.query,
123
156
  search_type: params.searchType,
@@ -133,9 +166,7 @@ async function webSearch(apiKey, params) {
133
166
  Authorization: `Bearer ${apiKey}`,
134
167
  });
135
168
 
136
- // 过滤计费信息
137
- const { billing, ...safe } = resp;
138
- return safe;
169
+ return stripBilling(resp);
139
170
  }
140
171
 
141
172
  /**
@@ -156,8 +187,129 @@ async function publishMdToHtml(apiKey, mdText, filename, addCopyButton = false)
156
187
 
157
188
  const resp = await httpPost(API_HOST, MD_TO_HTML_API_PATH, reqBody);
158
189
 
159
- const { billing, ...safe } = resp;
160
- return safe;
190
+ return stripBilling(resp);
191
+ }
192
+
193
+ /**
194
+ * 获取抖音作品详情
195
+ * @param {string} apiKey
196
+ * @param {string} shareUrl
197
+ * @returns {Promise<object>}
198
+ */
199
+ async function douyinVideoInfo(apiKey, shareUrl) {
200
+ const resp = await httpPost(API_HOST, DOUYIN_VIDEO_INFO_PATH, {
201
+ api_key: apiKey,
202
+ share_url: shareUrl,
203
+ });
204
+ return stripBilling(resp);
205
+ }
206
+
207
+ /**
208
+ * 获取抖音作品评论
209
+ * @param {string} apiKey
210
+ * @param {string} videoUrl
211
+ * @param {number} [maxCount=50]
212
+ * @returns {Promise<object>}
213
+ */
214
+ async function douyinVideoComments(apiKey, videoUrl, maxCount) {
215
+ const body = { api_key: apiKey, video_url: videoUrl };
216
+ if (maxCount !== undefined && maxCount !== null) body.max_count = maxCount;
217
+ const resp = await httpPost(API_HOST, DOUYIN_VIDEO_COMMENTS_PATH, body);
218
+ return stripBilling(resp);
219
+ }
220
+
221
+ /**
222
+ * 获取抖音用户视频列表
223
+ * @param {string} apiKey
224
+ * @param {string} homePage
225
+ * @param {number} [maxSeconds=50]
226
+ * @param {number} [maxPages=1]
227
+ * @returns {Promise<object>}
228
+ */
229
+ async function douyinUserVideos(apiKey, homePage, maxSeconds, maxPages) {
230
+ const body = { api_key: apiKey, home_page: homePage };
231
+ if (maxSeconds !== undefined && maxSeconds !== null) body.max_seconds = maxSeconds;
232
+ if (maxPages !== undefined && maxPages !== null) body.max_pages = maxPages;
233
+ const resp = await httpPost(API_HOST, DOUYIN_USER_VIDEOS_PATH, body);
234
+ return stripBilling(resp);
235
+ }
236
+
237
+ /**
238
+ * 获取抖音用户详情
239
+ * @param {string} apiKey
240
+ * @param {string} homePage
241
+ * @returns {Promise<object>}
242
+ */
243
+ async function douyinUserDetail(apiKey, homePage) {
244
+ const resp = await httpPost(API_HOST, DOUYIN_USER_DETAIL_PATH, {
245
+ api_key: apiKey,
246
+ home_page: homePage,
247
+ });
248
+ return stripBilling(resp);
249
+ }
250
+
251
+ /**
252
+ * 搜索抖音用户
253
+ * @param {string} apiKey
254
+ * @param {string} keyword
255
+ * @param {object} [opts]
256
+ * @returns {Promise<object>}
257
+ */
258
+ async function douyinUserSearch(apiKey, keyword, opts = {}) {
259
+ const body = { api_key: apiKey, keyword };
260
+ if (opts.maxPages !== undefined && opts.maxPages !== null) body.max_pages = opts.maxPages;
261
+ if (opts.maxSeconds !== undefined && opts.maxSeconds !== null) body.max_seconds = opts.maxSeconds;
262
+ if (opts.fansRange) body.douyin_user_fans = opts.fansRange;
263
+ if (opts.userType) body.douyin_user_type = opts.userType;
264
+ const resp = await httpPost(API_HOST, DOUYIN_USER_SEARCH_PATH, body);
265
+ return stripBilling(resp);
266
+ }
267
+
268
+ /**
269
+ * 搜索抖音视频
270
+ * @param {string} apiKey
271
+ * @param {string} keyword
272
+ * @param {object} [opts]
273
+ * @returns {Promise<object>}
274
+ */
275
+ async function douyinVideoSearch(apiKey, keyword, opts = {}) {
276
+ const body = { keyword };
277
+ if (opts.sortType !== undefined) body.sort_type = opts.sortType;
278
+ if (opts.publishTime !== undefined) body.publish_time = opts.publishTime;
279
+ if (opts.filterDuration !== undefined) body.filter_duration = opts.filterDuration;
280
+ if (opts.contentType !== undefined) body.content_type = opts.contentType;
281
+ if (opts.maxPages !== undefined && opts.maxPages !== null) body.max_pages = opts.maxPages;
282
+ if (opts.maxSeconds !== undefined && opts.maxSeconds !== null) body.max_seconds = opts.maxSeconds;
283
+ const resp = await httpPost(API_HOST, DOUYIN_VIDEO_SEARCH_PATH, body, {
284
+ Authorization: `Bearer ${apiKey}`,
285
+ });
286
+ return stripBilling(resp);
287
+ }
288
+
289
+ /**
290
+ * 抖音作品链接转文字
291
+ * @param {string} apiKey
292
+ * @param {string} videoUrl
293
+ * @returns {Promise<object>}
294
+ */
295
+ async function douyinLinkToText(apiKey, videoUrl) {
296
+ const resp = await httpPost(API_HOST, DOUYIN_LINK_TO_TEXT_PATH, {
297
+ api_key: apiKey,
298
+ video_url: videoUrl,
299
+ });
300
+ return stripBilling(resp);
161
301
  }
162
302
 
163
- module.exports = { resolveApiKey, publishHtml, publishMdToHtml, webSearch };
303
+ module.exports = {
304
+ resolveApiKey,
305
+ publishHtml,
306
+ publishMdToHtml,
307
+ webSearch,
308
+ douyinVideoInfo,
309
+ douyinVideoComments,
310
+ douyinUserVideos,
311
+ douyinUserDetail,
312
+ douyinUserSearch,
313
+ douyinVideoSearch,
314
+ douyinLinkToText,
315
+ };
@@ -0,0 +1,109 @@
1
+ const {
2
+ douyinVideoInfo,
3
+ douyinVideoComments,
4
+ douyinUserVideos,
5
+ douyinUserDetail,
6
+ douyinUserSearch,
7
+ douyinVideoSearch,
8
+ douyinLinkToText,
9
+ } = require("../api");
10
+ const { outputResult, resolveKeyOrExit, handleApiError, safeParseInt } = require("../lib/cli-utils");
11
+
12
+ async function videoInfoCommand(shareUrl, options = {}) {
13
+ const format = options.format || "json";
14
+ const apiKey = resolveKeyOrExit(options.apiKey, format);
15
+ try {
16
+ const result = await douyinVideoInfo(apiKey, shareUrl);
17
+ outputResult(result, format);
18
+ process.exit(0);
19
+ } catch (err) { handleApiError(err, format); process.exit(1); }
20
+ }
21
+
22
+ async function videoCommentsCommand(videoUrl, options = {}) {
23
+ const format = options.format || "json";
24
+ const apiKey = resolveKeyOrExit(options.apiKey, format);
25
+ try {
26
+ const maxCount = safeParseInt(options.maxCount, undefined);
27
+ const result = await douyinVideoComments(apiKey, videoUrl, maxCount);
28
+ outputResult(result, format);
29
+ process.exit(0);
30
+ } catch (err) { handleApiError(err, format); process.exit(1); }
31
+ }
32
+
33
+ async function userVideosCommand(homePage, options = {}) {
34
+ const format = options.format || "json";
35
+ const apiKey = resolveKeyOrExit(options.apiKey, format);
36
+ try {
37
+ const maxSeconds = safeParseInt(options.maxSeconds, undefined);
38
+ const maxPages = safeParseInt(options.maxPages, undefined);
39
+ const result = await douyinUserVideos(apiKey, homePage, maxSeconds, maxPages);
40
+ outputResult(result, format);
41
+ process.exit(0);
42
+ } catch (err) { handleApiError(err, format); process.exit(1); }
43
+ }
44
+
45
+ async function userDetailCommand(homePage, options = {}) {
46
+ const format = options.format || "json";
47
+ const apiKey = resolveKeyOrExit(options.apiKey, format);
48
+ try {
49
+ const result = await douyinUserDetail(apiKey, homePage);
50
+ outputResult(result, format);
51
+ process.exit(0);
52
+ } catch (err) { handleApiError(err, format); process.exit(1); }
53
+ }
54
+
55
+ async function userSearchCommand(keyword, options = {}) {
56
+ const format = options.format || "json";
57
+ const apiKey = resolveKeyOrExit(options.apiKey, format);
58
+ try {
59
+ const maxPages = safeParseInt(options.maxPages, undefined);
60
+ const maxSeconds = safeParseInt(options.maxSeconds, undefined);
61
+ const result = await douyinUserSearch(apiKey, keyword, {
62
+ maxPages,
63
+ maxSeconds,
64
+ fansRange: options.fansRange,
65
+ userType: options.userType,
66
+ });
67
+ outputResult(result, format);
68
+ process.exit(0);
69
+ } catch (err) { handleApiError(err, format); process.exit(1); }
70
+ }
71
+
72
+ async function videoSearchCommand(keyword, options = {}) {
73
+ const format = options.format || "json";
74
+ const apiKey = resolveKeyOrExit(options.apiKey, format);
75
+ try {
76
+ const maxPages = safeParseInt(options.maxPages, undefined);
77
+ const maxSeconds = safeParseInt(options.maxSeconds, undefined);
78
+ const result = await douyinVideoSearch(apiKey, keyword, {
79
+ maxPages,
80
+ maxSeconds,
81
+ sortType: options.sortType,
82
+ publishTime: options.publishTime,
83
+ filterDuration: options.filterDuration,
84
+ contentType: options.contentType,
85
+ });
86
+ outputResult(result, format);
87
+ process.exit(0);
88
+ } catch (err) { handleApiError(err, format); process.exit(1); }
89
+ }
90
+
91
+ async function linkToTextCommand(videoUrl, options = {}) {
92
+ const format = options.format || "json";
93
+ const apiKey = resolveKeyOrExit(options.apiKey, format);
94
+ try {
95
+ const result = await douyinLinkToText(apiKey, videoUrl);
96
+ outputResult(result, format);
97
+ process.exit(0);
98
+ } catch (err) { handleApiError(err, format); process.exit(1); }
99
+ }
100
+
101
+ module.exports = {
102
+ videoInfoCommand,
103
+ videoCommentsCommand,
104
+ userVideosCommand,
105
+ userDetailCommand,
106
+ userSearchCommand,
107
+ videoSearchCommand,
108
+ linkToTextCommand,
109
+ };
@@ -1,91 +1,54 @@
1
- const fs = require("fs");
2
- const path = require("path");
3
- const { resolveApiKey, publishHtml } = require("../api");
4
-
5
- /**
6
- * 智能解析输入:如果是已存在的文件路径则读取,否则当作原始 HTML 字符串
7
- * @param {string} input
8
- * @returns {string}
9
- */
10
- function resolveHtmlInput(input) {
11
- const trimmed = input.trim();
12
- // 尝试作为文件路径
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
- /**
25
- * 输出成功结果
26
- * @param {object} data
27
- * @param {"json"|"text"} format
28
- */
29
- function outputResult(data, format) {
30
- if (format === "json") {
31
- process.stdout.write(JSON.stringify({ success: true, data }, null, 2) + "\n");
32
- } else {
33
- process.stdout.write(`✓ 发布成功\n`);
34
- process.stdout.write(` URL: ${data.online_url}\n`);
35
- if (data.message) {
36
- process.stdout.write(` 信息: ${data.message}\n`);
37
- }
38
- }
39
- }
40
-
41
- /**
42
- * 输出错误
43
- * @param {object} err
44
- * @param {"json"|"text"} format
45
- */
46
- function outputError(err, format) {
47
- if (format === "json") {
48
- process.stderr.write(JSON.stringify({ success: false, error: err.message || String(err) }, null, 2) + "\n");
49
- } else {
50
- process.stderr.write(`✗ 错误: ${err.message || err}\n`);
51
- }
52
- }
53
-
54
- /**
55
- * publish 命令(子命令模式)
56
- * @param {string} htmlCode - HTML 代码或文件路径
57
- * @param {{ apiKey?: string, format?: string, title?: string }} options
58
- */
59
- async function publishCommand(htmlCode, options = {}) {
60
- const format = options.format || "json";
61
- const resolvedKey = resolveApiKey(options.apiKey);
62
-
63
- if (!resolvedKey) {
64
- outputError(
65
- { message: "缺少 API Key。请通过 -k 参数、AICONDUCTOR_API_KEY 环境变量或 .env 文件提供。" },
66
- format
67
- );
68
- process.exit(1);
69
- }
70
-
71
- try {
72
- const code = resolveHtmlInput(htmlCode);
73
- const result = await publishHtml(resolvedKey, code, options.title);
74
- outputResult(result, format);
75
- process.exit(0);
76
- } catch (err) {
77
- if (err.type === "http_error") {
78
- outputError(
79
- { message: `API 返回错误 (HTTP ${err.statusCode}): ${err.body?.message || JSON.stringify(err.body)}` },
80
- format
81
- );
82
- } else if (err.type === "network_error") {
83
- outputError({ message: `网络错误: ${err.message}` }, format);
84
- } else {
85
- outputError({ message: `未知错误: ${err.message || err}` }, format);
86
- }
87
- process.exit(1);
88
- }
89
- }
90
-
91
- 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 };