aiconductor-cli 1.3.2 → 1.3.3

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 CHANGED
@@ -115,18 +115,23 @@ aiconductor-cli search web <关键词> [选项]
115
115
 
116
116
  | 选项 | 说明 | 默认值 |
117
117
  |------|------|--------|
118
- | `-n, --count` | 返回条数(最多50) | 5 |
118
+ | `-n, --count` | 返回条数(最多50) | 10 |
119
119
  | `--time-range` | 时间范围:预设 `OneDay`/`OneWeek`/`OneMonth`/`OneYear`,或自定义 `YYYY-MM-DD..YYYY-MM-DD` | 无 |
120
- | `--no-summary` | 不返回内容摘要 | 默认返回 |
121
- | `--content-format` | 内容格式:`Text` 或 `Markdown` | Text |
122
- | `--auth-level` | 权威过滤:0=不过滤,1=优先权威来源 | 0 |
120
+ | `--need-content` | 仅返回包含正文的结果 | 关闭 |
121
+ | `--need-url` | 仅返回带原文链接的结果 | 关闭 |
122
+ | `--sites` | 限定搜索站点,完整域名以 `\|` 分隔,最多20个 | 无 |
123
+ | `--block-hosts` | 屏蔽站点,完整域名以 `\|` 分隔,最多5个 | 无 |
124
+ | `--auth-info-level` | 权威度过滤:0=不限制,1=仅非常权威内容 | 0 |
125
+ | `--auth-level` | `--auth-info-level` 的兼容别名 | 无 |
126
+ | `--content-format` | 正文格式:`text` 或 `markdown` | text |
127
+ | `--industry` | 行业搜索:`finance`、`game` 或 `gov` | 无 |
123
128
  | `--query-rewrite` | 开启 Query 改写 | 关闭 |
124
129
  | `-f, --format` | 输出格式:`json` 或 `text` | json |
125
130
 
126
131
  示例:
127
132
  ```bash
128
- aiconductor-cli search web "北京今日天气" -n 3 --auth-level 1
129
- aiconductor-cli search web "AI 最新进展" --time-range OneWeek --content-format Markdown
133
+ aiconductor-cli search web "北京今日天气" -n 3 --auth-info-level 1 --need-url
134
+ aiconductor-cli search web "AI 最新进展" --time-range OneWeek --content-format markdown --need-content
130
135
  aiconductor-cli search web "技术文章" --time-range 2025-01-01..2025-06-30 -n 10
131
136
  ```
132
137
 
@@ -139,12 +144,17 @@ aiconductor-cli search image <关键词> [选项]
139
144
  | 选项 | 说明 | 默认值 |
140
145
  |------|------|--------|
141
146
  | `-n, --count` | 返回条数(最多5) | 5 |
142
- | `--time-range` | 时间范围:预设或自定义日期范围 | 无 |
147
+ | `--image-width-min` | 图片最小宽度 | 无 |
148
+ | `--image-width-max` | 图片最大宽度 | 无 |
149
+ | `--image-height-min` | 图片最小高度 | 无 |
150
+ | `--image-height-max` | 图片最大高度 | 无 |
151
+ | `--image-shapes` | 图片形状,以 `\|` 分隔:`横长方形`、`竖长方形`、`方形` | 无 |
152
+ | `--query-rewrite` | 开启 Query 改写 | 关闭 |
143
153
  | `-f, --format` | 输出格式:`json` 或 `text` | json |
144
154
 
145
155
  示例:
146
156
  ```bash
147
- aiconductor-cli search image "长城风景" -n 3 -f text
157
+ aiconductor-cli search image "长城风景" -n 3 --image-width-min 1200 --image-shapes "横长方形|方形" -f text
148
158
  ```
149
159
 
150
160
  ### 抖音数据工具箱
@@ -309,7 +319,7 @@ aiconductor-cli xhs note-publish video \
309
319
  |------|------|
310
320
  | HTML 发布 | 0.01 元/次 |
311
321
  | Markdown/HTML 网页发布 | 0.01 元/次 |
312
- | 网页/图片搜索 | 0.03 元/次 |
322
+ | 豆包网页/图片搜索 | 0.02 元/次 |
313
323
  | 抖音作品详情 | 0.01 元/次 |
314
324
  | 抖音作品评论 | 0.01 元/次 |
315
325
  | 抖音用户视频列表 | 0.03 元/页 |
@@ -95,12 +95,17 @@ searchCmd
95
95
  .description("网页搜索")
96
96
  .argument("<query>", "搜索关键词(1~100字符)")
97
97
  .option("-k, --api-key <key>", "AIConductor API Key(也可通过 AICONDUCTOR_API_KEY 环境变量或 .env 文件提供)")
98
- .option("-n, --count <n>", "返回条数(最多50)")
99
- .option("--time-range <range>", "时间范围:OneDay/OneWeek/OneMonth/OneYear 或 YYYY-MM-DD..YYYY-MM-DD")
100
- .option("--no-summary", "不返回摘要")
101
- .option("--content-format <format>", "内容格式:Text 或 Markdown", "Text")
102
- .option("--auth-level <level>", "权威来源过滤:0=不过滤,1=优先权威来源", "0")
103
- .option("--query-rewrite", "开启 Query 改写")
98
+ .option("-n, --count <n>", "返回条数(最多50)")
99
+ .option("--time-range <range>", "时间范围:OneDay/OneWeek/OneMonth/OneYear 或 YYYY-MM-DD..YYYY-MM-DD")
100
+ .option("--need-content", "仅返回包含正文的结果")
101
+ .option("--need-url", "仅返回带原文链接的结果")
102
+ .option("--sites <domains>", "限定搜索站点,完整域名以 | 分隔,最多20个")
103
+ .option("--block-hosts <domains>", "屏蔽站点,完整域名以 | 分隔,最多5个")
104
+ .option("--auth-info-level <level>", "权威度过滤:0=不限制,1=仅非常权威内容")
105
+ .option("--auth-level <level>", "兼容别名,等同 --auth-info-level")
106
+ .option("--content-format <format>", "正文格式:text 或 markdown", "text")
107
+ .option("--industry <type>", "行业搜索:finance、game 或 gov")
108
+ .option("--query-rewrite", "开启 Query 改写")
104
109
  .option("-f, --format <format>", "输出格式:json 或 text", "json")
105
110
  .action((query, options) => {
106
111
  searchCommand(query, "web", options);
@@ -110,9 +115,14 @@ searchCmd
110
115
  .command("image")
111
116
  .description("图片搜索")
112
117
  .argument("<query>", "搜索关键词(1~100字符)")
113
- .option("-k, --api-key <key>", "AIConductor API Key(也可通过 AICONDUCTOR_API_KEY 环境变量或 .env 文件提供)")
114
- .option("-n, --count <n>", "返回条数(最多5)")
115
- .option("--time-range <range>", "时间范围:OneDay/OneWeek/OneMonth/OneYear 或 YYYY-MM-DD..YYYY-MM-DD")
118
+ .option("-k, --api-key <key>", "AIConductor API Key(也可通过 AICONDUCTOR_API_KEY 环境变量或 .env 文件提供)")
119
+ .option("-n, --count <n>", "返回条数(最多5)")
120
+ .option("--image-width-min <px>", "图片最小宽度")
121
+ .option("--image-width-max <px>", "图片最大宽度")
122
+ .option("--image-height-min <px>", "图片最小高度")
123
+ .option("--image-height-max <px>", "图片最大高度")
124
+ .option("--image-shapes <shapes>", "图片形状,以 | 分隔:横长方形、竖长方形、方形")
125
+ .option("--query-rewrite", "开启 Query 改写")
116
126
  .option("-f, --format <format>", "输出格式:json 或 text", "json")
117
127
  .action((query, options) => {
118
128
  searchCommand(query, "image", options);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aiconductor-cli",
3
- "version": "1.3.2",
3
+ "version": "1.3.3",
4
4
  "description": "AIConductor CLI 工具箱",
5
5
  "keywords": [
6
6
  "aiconductor",
@@ -21,7 +21,9 @@
21
21
  "engines": {
22
22
  "node": ">=18.0.0"
23
23
  },
24
- "scripts": {},
24
+ "scripts": {
25
+ "test": "node --test"
26
+ },
25
27
  "dependencies": {
26
28
  "commander": "^12.1.0"
27
29
  }
package/src/api.js CHANGED
@@ -7,7 +7,7 @@ const API_PATH = "/api/html_publish";
7
7
  const MD_TO_HTML_API_PATH = "/api/md_to_html";
8
8
  const PUBLISHED_WEBPAGES_API_PATH = "/api/published_webpages";
9
9
  const DELETE_PUBLISHED_WEBPAGE_API_PATH = "/api/delete_published_webpage";
10
- const SEARCH_API_PATH = "/api/aic_web_search";
10
+ const SEARCH_API_PATH = "/api/doubao_search";
11
11
  const DOUYIN_VIDEO_INFO_PATH = "/api/douyin_video_info";
12
12
  const DOUYIN_VIDEO_COMMENTS_PATH = "/api/douyin_video_comments";
13
13
  const DOUYIN_USER_VIDEOS_PATH = "/api/douyin_user_videos";
@@ -273,26 +273,62 @@ async function publishHtml(apiKey, htmlCode, title) {
273
273
  }
274
274
 
275
275
  /**
276
- * 网页/图片搜索
277
- * @param {string} apiKey
278
- * @param {{ query: string, searchType: string, count?: number, timeRange?: string, needSummary?: boolean, contentFormats?: string, authLevel?: number, queryRewrite?: boolean }} params
279
- * @returns {Promise<object>}
280
- */
281
- async function webSearch(apiKey, params) {
282
- // 输入校验
283
- validateInt(params.count, "count");
284
- validateInt(params.authLevel, "authLevel");
285
-
286
- const reqBody = {
287
- query: params.query,
288
- search_type: params.searchType,
289
- };
290
- if (params.count !== undefined) reqBody.count = params.count;
291
- if (params.timeRange) reqBody.time_range = params.timeRange;
292
- if (params.needSummary !== undefined) reqBody.need_summary = params.needSummary;
293
- if (params.contentFormats) reqBody.content_formats = params.contentFormats;
294
- if (params.authLevel !== undefined) reqBody.auth_level = params.authLevel;
295
- if (params.queryRewrite !== undefined) reqBody.query_rewrite = params.queryRewrite;
276
+ * 豆包网页/图片搜索
277
+ * @param {string} apiKey
278
+ * @param {{ query: string, searchType: "web"|"image", count?: number, queryRewrite?: boolean, needContent?: boolean, needUrl?: boolean, sites?: string, blockHosts?: string, authInfoLevel?: number, timeRange?: string, contentFormats?: string, industry?: string, imageWidthMin?: number, imageWidthMax?: number, imageHeightMin?: number, imageHeightMax?: number, imageShapes?: string[]|string }} params
279
+ * @returns {Promise<object>}
280
+ */
281
+ async function webSearch(apiKey, params) {
282
+ const maxCount = params.searchType === "web" ? 50 : 5;
283
+ if (params.searchType !== "web" && params.searchType !== "image") {
284
+ throw Object.assign(new Error("searchType 只能是 web 或 image"), { type: "validation_error" });
285
+ }
286
+ const count = params.count === undefined ? undefined : validatePositiveInt(params.count, "count", maxCount);
287
+ validateInt(params.authInfoLevel, "authInfoLevel");
288
+ validateInt(params.imageWidthMin, "imageWidthMin");
289
+ validateInt(params.imageWidthMax, "imageWidthMax");
290
+ validateInt(params.imageHeightMin, "imageHeightMin");
291
+ validateInt(params.imageHeightMax, "imageHeightMax");
292
+
293
+ const authInfoLevel = params.authInfoLevel === undefined ? undefined : Number(params.authInfoLevel);
294
+ const imageWidthMin = params.imageWidthMin === undefined ? undefined : Number(params.imageWidthMin);
295
+ const imageWidthMax = params.imageWidthMax === undefined ? undefined : Number(params.imageWidthMax);
296
+ const imageHeightMin = params.imageHeightMin === undefined ? undefined : Number(params.imageHeightMin);
297
+ const imageHeightMax = params.imageHeightMax === undefined ? undefined : Number(params.imageHeightMax);
298
+
299
+ if (authInfoLevel !== undefined && ![0, 1].includes(authInfoLevel)) {
300
+ throw Object.assign(new Error("authInfoLevel 只能是 0 或 1"), { type: "validation_error" });
301
+ }
302
+ if (imageWidthMin !== undefined && imageWidthMax !== undefined && imageWidthMin > imageWidthMax) {
303
+ throw Object.assign(new Error("imageWidthMin 不能大于 imageWidthMax"), { type: "validation_error" });
304
+ }
305
+ if (imageHeightMin !== undefined && imageHeightMax !== undefined && imageHeightMin > imageHeightMax) {
306
+ throw Object.assign(new Error("imageHeightMin 不能大于 imageHeightMax"), { type: "validation_error" });
307
+ }
308
+
309
+ const reqBody = {
310
+ query: params.query,
311
+ search_type: params.searchType,
312
+ };
313
+ if (count !== undefined) reqBody.count = count;
314
+ if (params.queryRewrite !== undefined) reqBody.query_rewrite = params.queryRewrite;
315
+
316
+ if (params.searchType === "web") {
317
+ if (params.needContent !== undefined) reqBody.need_content = params.needContent;
318
+ if (params.needUrl !== undefined) reqBody.need_url = params.needUrl;
319
+ if (params.sites) reqBody.sites = params.sites;
320
+ if (params.blockHosts) reqBody.block_hosts = params.blockHosts;
321
+ if (authInfoLevel !== undefined) reqBody.auth_info_level = authInfoLevel;
322
+ if (params.timeRange) reqBody.time_range = params.timeRange;
323
+ if (params.contentFormats) reqBody.content_formats = params.contentFormats;
324
+ if (params.industry) reqBody.industry = params.industry;
325
+ } else {
326
+ if (imageWidthMin !== undefined) reqBody.image_width_min = imageWidthMin;
327
+ if (imageWidthMax !== undefined) reqBody.image_width_max = imageWidthMax;
328
+ if (imageHeightMin !== undefined) reqBody.image_height_min = imageHeightMin;
329
+ if (imageHeightMax !== undefined) reqBody.image_height_max = imageHeightMax;
330
+ if (params.imageShapes !== undefined) reqBody.image_shapes = params.imageShapes;
331
+ }
296
332
 
297
333
  const resp = await httpPost(API_HOST, SEARCH_API_PATH, reqBody, {
298
334
  Authorization: `Bearer ${apiKey}`,
@@ -0,0 +1,98 @@
1
+ const assert = require("node:assert/strict");
2
+ const { EventEmitter } = require("node:events");
3
+ const https = require("node:https");
4
+ const test = require("node:test");
5
+
6
+ const { webSearch } = require("./api");
7
+
8
+ function captureRequest(callback) {
9
+ const originalRequest = https.request;
10
+ let captured;
11
+ https.request = (options, responseCallback) => {
12
+ const request = new EventEmitter();
13
+ const chunks = [];
14
+ request.write = (chunk) => chunks.push(Buffer.from(chunk));
15
+ request.end = () => {
16
+ captured = { options, body: JSON.parse(Buffer.concat(chunks).toString("utf-8")) };
17
+ const response = new EventEmitter();
18
+ response.statusCode = 200;
19
+ responseCallback(response);
20
+ response.emit("data", Buffer.from(JSON.stringify({ success: true, billing: {} })));
21
+ response.emit("end");
22
+ };
23
+ return request;
24
+ };
25
+
26
+ return callback().then(() => captured).finally(() => {
27
+ https.request = originalRequest;
28
+ });
29
+ }
30
+
31
+ test("webSearch maps current web search options to the Doubao endpoint", async () => {
32
+ const request = await captureRequest(() => webSearch("test-key", {
33
+ query: "最新的北京游玩攻略",
34
+ searchType: "web",
35
+ count: 10,
36
+ queryRewrite: true,
37
+ needContent: true,
38
+ needUrl: true,
39
+ sites: "example.com|volcengine.com",
40
+ blockHosts: "spam.example",
41
+ authInfoLevel: 1,
42
+ timeRange: "OneYear",
43
+ contentFormats: "markdown",
44
+ industry: "finance",
45
+ }));
46
+
47
+ assert.equal(request.options.path, "/api/doubao_search");
48
+ assert.equal(request.options.headers.Authorization, "Bearer test-key");
49
+ assert.deepEqual(request.body, {
50
+ query: "最新的北京游玩攻略",
51
+ search_type: "web",
52
+ count: 10,
53
+ query_rewrite: true,
54
+ need_content: true,
55
+ need_url: true,
56
+ sites: "example.com|volcengine.com",
57
+ block_hosts: "spam.example",
58
+ auth_info_level: 1,
59
+ time_range: "OneYear",
60
+ content_formats: "markdown",
61
+ industry: "finance",
62
+ });
63
+ });
64
+
65
+ test("webSearch maps current image filters and rejects invalid count", async () => {
66
+ const request = await captureRequest(() => webSearch("test-key", {
67
+ query: "长城风景",
68
+ searchType: "image",
69
+ count: 5,
70
+ queryRewrite: true,
71
+ imageWidthMin: 1200,
72
+ imageWidthMax: 1920,
73
+ imageHeightMin: 800,
74
+ imageHeightMax: 1080,
75
+ imageShapes: "横长方形|方形",
76
+ }));
77
+
78
+ assert.deepEqual(request.body, {
79
+ query: "长城风景",
80
+ search_type: "image",
81
+ count: 5,
82
+ query_rewrite: true,
83
+ image_width_min: 1200,
84
+ image_width_max: 1920,
85
+ image_height_min: 800,
86
+ image_height_max: 1080,
87
+ image_shapes: "横长方形|方形",
88
+ });
89
+
90
+ await assert.rejects(
91
+ webSearch("test-key", { query: "长城", searchType: "image", count: 6 }),
92
+ { type: "validation_error" }
93
+ );
94
+ await assert.rejects(
95
+ webSearch("test-key", { query: "长城", searchType: "image", imageWidthMin: "invalid" }),
96
+ { type: "validation_error" }
97
+ );
98
+ });
@@ -1,5 +1,5 @@
1
1
  const { webSearch } = require("../api");
2
- const { outputResult, outputError, resolveKeyOrExit, handleApiError, safeParseInt } = require("../lib/cli-utils");
2
+ const { outputResult, outputError, resolveKeyOrExit, handleApiError } = require("../lib/cli-utils");
3
3
 
4
4
  /** text 模式的自定义格式化 */
5
5
  function formatSearchText(data) {
@@ -49,19 +49,34 @@ async function searchCommand(query, searchType, options = {}) {
49
49
  const params = { query, searchType };
50
50
 
51
51
  if (options.count !== undefined) {
52
- params.count = safeParseInt(options.count, undefined);
52
+ params.count = options.count;
53
53
  }
54
54
  if (options.timeRange) {
55
55
  params.timeRange = options.timeRange;
56
56
  }
57
57
  if (searchType === "web") {
58
- params.needSummary = options.summary !== false;
59
- }
60
- if (options.contentFormat) {
61
- params.contentFormats = options.contentFormat;
62
- }
63
- if (options.authLevel !== undefined) {
64
- params.authLevel = safeParseInt(options.authLevel, undefined);
58
+ if (options.needContent) params.needContent = true;
59
+ if (options.needUrl) params.needUrl = true;
60
+ if (options.sites) params.sites = options.sites;
61
+ if (options.blockHosts) params.blockHosts = options.blockHosts;
62
+ const authInfoLevel = options.authInfoLevel ?? options.authLevel;
63
+ if (authInfoLevel !== undefined) {
64
+ params.authInfoLevel = authInfoLevel;
65
+ }
66
+ if (options.contentFormat) params.contentFormats = options.contentFormat;
67
+ if (options.industry) params.industry = options.industry;
68
+ } else {
69
+ for (const [optionName, paramName] of [
70
+ ["imageWidthMin", "imageWidthMin"],
71
+ ["imageWidthMax", "imageWidthMax"],
72
+ ["imageHeightMin", "imageHeightMin"],
73
+ ["imageHeightMax", "imageHeightMax"],
74
+ ]) {
75
+ if (options[optionName] !== undefined) {
76
+ params[paramName] = options[optionName];
77
+ }
78
+ }
79
+ if (options.imageShapes) params.imageShapes = options.imageShapes;
65
80
  }
66
81
  if (options.queryRewrite) {
67
82
  params.queryRewrite = true;