aiconductor-cli 1.3.1 → 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/AGENTS.md +47 -56
- package/README.md +51 -9
- package/bin/aiconductor-cli.js +50 -15
- package/package.json +4 -2
- package/src/api.js +121 -29
- package/src/api.test.js +98 -0
- package/src/commands/md_publish.js +90 -2
- package/src/commands/web_search.js +24 -9
- package/.codebuddy/skills/aic-web-search/SKILL.md +0 -141
- package/.codebuddy/skills/aic-web-search/references/aiconductor-skills-setup.md +0 -50
- package/.codebuddy/skills/douyin-toolbox/SKILL.md +0 -219
- package/.codebuddy/skills/douyin-toolbox/references/aiconductor-skills-setup.md +0 -50
- package/.codebuddy/skills/npm-cli-publisher/SKILL.md +0 -166
- package/.codebuddy/skills/webpage-publish/SKILL.md +0 -119
- package/.codebuddy/skills/webpage-publish/references/aiconductor-skills-setup.md +0 -50
- package/.codebuddy/skills/xhs-note-publish/SKILL.md +0 -172
- package/.codebuddy/skills/xhs-note-publish/references/aiconductor-skills-setup.md +0 -50
package/AGENTS.md
CHANGED
|
@@ -1,76 +1,67 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Repository Guidelines
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
## Project Structure & Module Organization
|
|
4
4
|
|
|
5
|
-
## Commands
|
|
6
|
-
|
|
7
|
-
### 本地运行
|
|
8
|
-
|
|
9
|
-
```bash
|
|
10
|
-
node bin/aiconductor-cli.js --help # 查看所有命令
|
|
11
|
-
node bin/aiconductor-cli.js <command> --help # 查看子命令选项
|
|
12
|
-
node bin/aiconductor-cli.js search web "关键词" -n 3 -f text # 真实调用示例
|
|
13
5
|
```
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
6
|
+
bin/aiconductor-cli.js CLI entry point (Commander.js)
|
|
7
|
+
src/
|
|
8
|
+
api.js HTTP API client — all platform calls live here
|
|
9
|
+
lib/cli-utils.js Shared helpers: output, error handling, key resolution
|
|
10
|
+
commands/
|
|
11
|
+
html_publish.js `html publish` command
|
|
12
|
+
md_publish.js `webpage publish` command (Markdown/HTML)
|
|
13
|
+
web_search.js `search web` / `search image` commands
|
|
14
|
+
douyin.js All `douyin` subcommands
|
|
15
|
+
xhs.js `xhs note-publish` command
|
|
21
16
|
```
|
|
22
17
|
|
|
23
|
-
|
|
18
|
+
- Commands are thin wrappers: parse options, call `src/api.js`, format output, and exit.
|
|
19
|
+
- Shared logic (key resolution, error formatting, output) goes into `src/lib/cli-utils.js`.
|
|
20
|
+
- Each command file exports an async function; no side effects on load.
|
|
24
21
|
|
|
25
|
-
##
|
|
22
|
+
## Build, Test, and Development Commands
|
|
26
23
|
|
|
27
|
-
|
|
24
|
+
- `node bin/aiconductor-cli.js --help` — run locally without a build step
|
|
25
|
+
- `npm install` — install the single runtime dependency (`commander`)
|
|
26
|
+
- No test suite currently exists
|
|
28
27
|
|
|
29
|
-
|
|
30
|
-
bin/aiconductor-cli.js ← 入口层:commander 注册子命令树,不含业务逻辑
|
|
31
|
-
src/api.js ← API 层:HTTPS 请求封装 + API Key 解析
|
|
32
|
-
src/lib/cli-utils.js ← 共享工具:output/outputError/key/error 统一处理
|
|
33
|
-
src/commands/<topic>.js ← 命令层:参数组装、调用 API、格式化输出
|
|
34
|
-
```
|
|
28
|
+
## Coding Style & Naming Conventions
|
|
35
29
|
|
|
36
|
-
|
|
30
|
+
- 2-space indentation, no tabs
|
|
31
|
+
- CommonJS (`require` / `module.exports`), no ESM
|
|
32
|
+
- camelCase for JS identifiers; snake_case for API request fields
|
|
33
|
+
- Every exported function gets a JSDoc block describing parameters and return type
|
|
34
|
+
- Use `const` by default; `let` only when reassignment is needed
|
|
35
|
+
- No formatter or linter configured — follow surrounding code style
|
|
37
36
|
|
|
38
|
-
|
|
39
|
-
- `resolveApiKey(optionKey)` — 三级优先级:`-k` 参数 > `AICONDUCTOR_API_KEY` 环境变量 > `.env` 文件
|
|
40
|
-
- `httpPost()` — 通用 HTTPS POST(端口 443),统一处理 HTTP 错误和网络错误
|
|
41
|
-
- 每个 API 端点对应一个独立函数,通过 `stripBilling()` 过滤计费信息
|
|
42
|
-
- 新增 API 端点只改此文件
|
|
37
|
+
## Error Handling
|
|
43
38
|
|
|
44
|
-
|
|
45
|
-
-
|
|
46
|
-
-
|
|
47
|
-
- `resolveKeyOrExit(optionKey, format)` — 解析 API Key,失败自动输出错误并 exit(1)
|
|
48
|
-
- `handleApiError(err, format)` — 统一处理 http_error / network_error / 未知错误
|
|
49
|
-
- `safeParseInt(value, defaultValue)` — 安全整数解析,防 NaN
|
|
39
|
+
- Never swallow errors with empty `catch`. At minimum, log and re-throw.
|
|
40
|
+
- API errors carry a `.type` property (`http_error`, `network_error`, `validation_error`, `api_error`)
|
|
41
|
+
- Commands exit with code 0 on success, 1 on failure
|
|
50
42
|
|
|
51
|
-
|
|
52
|
-
- 导入 `cli-utils` 的 4 个共享函数,消除重复代码
|
|
53
|
-
- 业务逻辑函数接收 argument + options,组装参数 → 调 API → `outputResult`
|
|
54
|
-
- 成功 `exit(0)`,失败 `exit(1)`
|
|
43
|
+
## Testing Guidelines
|
|
55
44
|
|
|
56
|
-
|
|
45
|
+
No test framework is wired up yet. If adding tests:
|
|
46
|
+
- Use a standard Node.js test runner or Jest
|
|
47
|
+
- Place test files next to the module they cover (`src/commands/web_search.test.js`)
|
|
57
48
|
|
|
58
|
-
|
|
59
|
-
- **JSON(默认)**:成功 stdout `{ success: true, data: {...} }`,失败 stderr `{ success: false, error: "..." }`
|
|
60
|
-
- **text**:人类可读格式,成功以 `✓` 开头
|
|
49
|
+
## Commit & Pull Request Guidelines
|
|
61
50
|
|
|
62
|
-
|
|
51
|
+
From the existing history:
|
|
63
52
|
|
|
64
|
-
|
|
65
|
-
-
|
|
66
|
-
-
|
|
53
|
+
- Messages are in English, often prefixed with conventional commit tags: `feat:`, `fix:`, `refactor:`, `chore:`, `style:`, `ci:`
|
|
54
|
+
- Agent-authored commits include `[Agent]` before the description
|
|
55
|
+
- PRs should describe the change, note API surface impacts, and keep diffs focused
|
|
67
56
|
|
|
68
|
-
|
|
57
|
+
## Adding a New Command
|
|
69
58
|
|
|
70
|
-
1. `src/
|
|
71
|
-
2. `src/
|
|
72
|
-
3. `bin/aiconductor-cli.js
|
|
59
|
+
1. Create `src/commands/<name>.js` with an async handler that calls `src/api.js`
|
|
60
|
+
2. Add the API function to `src/api.js` if it calls a new endpoint
|
|
61
|
+
3. Register the subcommand in `bin/aiconductor-cli.js`
|
|
62
|
+
4. Update `README.md` — command synopsis, option table, and example
|
|
73
63
|
|
|
74
|
-
|
|
64
|
+
## Security & Configuration
|
|
75
65
|
|
|
76
|
-
|
|
66
|
+
- API Key is read from three sources in order: `-k` CLI argument → `AICONDUCTOR_API_KEY` env var → `.env` file
|
|
67
|
+
- Never commit `.env` files or hardcoded credentials
|
package/README.md
CHANGED
|
@@ -77,6 +77,36 @@ aiconductor-cli webpage publish index.html -t my-page
|
|
|
77
77
|
aiconductor-cli webpage publish "# Hello\n\nThis is **markdown**" -t hello
|
|
78
78
|
```
|
|
79
79
|
|
|
80
|
+
### 管理已发布网页
|
|
81
|
+
|
|
82
|
+
查询当前 API Key 所属用户的网页发布记录:
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
aiconductor-cli webpage list [选项]
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
| 选项 | 说明 | 默认值 |
|
|
89
|
+
|------|------|--------|
|
|
90
|
+
| `--page` | 页码,最小为 1 | 1 |
|
|
91
|
+
| `--page-size` | 每页条数,范围为 1 到 100 | 20 |
|
|
92
|
+
| `-k, --api-key` | API Key(可选) | 环境变量 |
|
|
93
|
+
| `-f, --format` | 输出格式:`json` 或 `text` | json |
|
|
94
|
+
|
|
95
|
+
删除指定的已发布网页及其公开 HTML 对象:
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
aiconductor-cli webpage delete <webpage_id> [选项]
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
示例:
|
|
102
|
+
```bash
|
|
103
|
+
# 查看第 2 页,每页 50 条
|
|
104
|
+
aiconductor-cli webpage list --page 2 --page-size 50 -f text
|
|
105
|
+
|
|
106
|
+
# 删除 ID 为 123 的网页
|
|
107
|
+
aiconductor-cli webpage delete 123
|
|
108
|
+
```
|
|
109
|
+
|
|
80
110
|
### 网页搜索
|
|
81
111
|
|
|
82
112
|
```bash
|
|
@@ -85,18 +115,23 @@ aiconductor-cli search web <关键词> [选项]
|
|
|
85
115
|
|
|
86
116
|
| 选项 | 说明 | 默认值 |
|
|
87
117
|
|------|------|--------|
|
|
88
|
-
| `-n, --count` | 返回条数(最多50) |
|
|
118
|
+
| `-n, --count` | 返回条数(最多50) | 10 |
|
|
89
119
|
| `--time-range` | 时间范围:预设 `OneDay`/`OneWeek`/`OneMonth`/`OneYear`,或自定义 `YYYY-MM-DD..YYYY-MM-DD` | 无 |
|
|
90
|
-
| `--
|
|
91
|
-
| `--
|
|
92
|
-
| `--
|
|
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` | 无 |
|
|
93
128
|
| `--query-rewrite` | 开启 Query 改写 | 关闭 |
|
|
94
129
|
| `-f, --format` | 输出格式:`json` 或 `text` | json |
|
|
95
130
|
|
|
96
131
|
示例:
|
|
97
132
|
```bash
|
|
98
|
-
aiconductor-cli search web "北京今日天气" -n 3 --auth-level 1
|
|
99
|
-
aiconductor-cli search web "AI 最新进展" --time-range OneWeek --content-format
|
|
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
|
|
100
135
|
aiconductor-cli search web "技术文章" --time-range 2025-01-01..2025-06-30 -n 10
|
|
101
136
|
```
|
|
102
137
|
|
|
@@ -109,12 +144,17 @@ aiconductor-cli search image <关键词> [选项]
|
|
|
109
144
|
| 选项 | 说明 | 默认值 |
|
|
110
145
|
|------|------|--------|
|
|
111
146
|
| `-n, --count` | 返回条数(最多5) | 5 |
|
|
112
|
-
| `--
|
|
147
|
+
| `--image-width-min` | 图片最小宽度 | 无 |
|
|
148
|
+
| `--image-width-max` | 图片最大宽度 | 无 |
|
|
149
|
+
| `--image-height-min` | 图片最小高度 | 无 |
|
|
150
|
+
| `--image-height-max` | 图片最大高度 | 无 |
|
|
151
|
+
| `--image-shapes` | 图片形状,以 `\|` 分隔:`横长方形`、`竖长方形`、`方形` | 无 |
|
|
152
|
+
| `--query-rewrite` | 开启 Query 改写 | 关闭 |
|
|
113
153
|
| `-f, --format` | 输出格式:`json` 或 `text` | json |
|
|
114
154
|
|
|
115
155
|
示例:
|
|
116
156
|
```bash
|
|
117
|
-
aiconductor-cli search image "长城风景" -n 3 -f text
|
|
157
|
+
aiconductor-cli search image "长城风景" -n 3 --image-width-min 1200 --image-shapes "横长方形|方形" -f text
|
|
118
158
|
```
|
|
119
159
|
|
|
120
160
|
### 抖音数据工具箱
|
|
@@ -279,7 +319,7 @@ aiconductor-cli xhs note-publish video \
|
|
|
279
319
|
|------|------|
|
|
280
320
|
| HTML 发布 | 0.01 元/次 |
|
|
281
321
|
| Markdown/HTML 网页发布 | 0.01 元/次 |
|
|
282
|
-
|
|
|
322
|
+
| 豆包网页/图片搜索 | 0.02 元/次 |
|
|
283
323
|
| 抖音作品详情 | 0.01 元/次 |
|
|
284
324
|
| 抖音作品评论 | 0.01 元/次 |
|
|
285
325
|
| 抖音用户视频列表 | 0.03 元/页 |
|
|
@@ -299,6 +339,8 @@ cd aiconductor-cli
|
|
|
299
339
|
npm install
|
|
300
340
|
node bin/aiconductor-cli.js --help # 本地运行
|
|
301
341
|
```
|
|
342
|
+
|
|
343
|
+
贡献前请阅读 [AGENTS.md](./AGENTS.md) 了解项目结构、编码风格与提交流程。
|
|
302
344
|
|
|
303
345
|
## License
|
|
304
346
|
|
package/bin/aiconductor-cli.js
CHANGED
|
@@ -2,7 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
const { Command } = require("commander");
|
|
4
4
|
const { publishCommand } = require("../src/commands/html_publish");
|
|
5
|
-
const {
|
|
5
|
+
const {
|
|
6
|
+
publishCommand: webpagePublishCommand,
|
|
7
|
+
listCommand: webpageListCommand,
|
|
8
|
+
deleteCommand: webpageDeleteCommand,
|
|
9
|
+
} = require("../src/commands/md_publish");
|
|
6
10
|
const { searchCommand } = require("../src/commands/web_search");
|
|
7
11
|
const {
|
|
8
12
|
videoInfoCommand,
|
|
@@ -47,8 +51,8 @@ const webpageCmd = program
|
|
|
47
51
|
.command("webpage")
|
|
48
52
|
.description("网页发布(支持 Markdown 和 HTML)");
|
|
49
53
|
|
|
50
|
-
webpageCmd
|
|
51
|
-
.command("publish")
|
|
54
|
+
webpageCmd
|
|
55
|
+
.command("publish")
|
|
52
56
|
.description("将 Markdown 或 HTML 发布为在线网页(自动识别类型)")
|
|
53
57
|
.argument("<input>", "Markdown/HTML 文本字符串或文件路径")
|
|
54
58
|
.option("-k, --api-key <key>", "AIConductor API Key(也可通过 AICONDUCTOR_API_KEY 环境变量或 .env 文件提供)")
|
|
@@ -56,9 +60,30 @@ webpageCmd
|
|
|
56
60
|
.option("--type <type>", "强制指定类型:md 或 html(默认自动识别)")
|
|
57
61
|
.option("--copy-button", "添加代码复制按钮(仅 Markdown 类型生效)")
|
|
58
62
|
.option("-f, --format <format>", "输出格式:json 或 text", "json")
|
|
59
|
-
.action((input, options) => {
|
|
60
|
-
webpagePublishCommand(input, options);
|
|
61
|
-
});
|
|
63
|
+
.action((input, options) => {
|
|
64
|
+
webpagePublishCommand(input, options);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
webpageCmd
|
|
68
|
+
.command("list")
|
|
69
|
+
.description("查询当前用户已发布的网页")
|
|
70
|
+
.option("-k, --api-key <key>", "AIConductor API Key(也可通过 AICONDUCTOR_API_KEY 环境变量或 .env 文件提供)")
|
|
71
|
+
.option("--page <page>", "页码,最小为 1", "1")
|
|
72
|
+
.option("--page-size <size>", "每页条数,范围为 1 到 100", "20")
|
|
73
|
+
.option("-f, --format <format>", "输出格式:json 或 text", "json")
|
|
74
|
+
.action((options) => {
|
|
75
|
+
webpageListCommand(options);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
webpageCmd
|
|
79
|
+
.command("delete")
|
|
80
|
+
.description("删除当前用户指定的已发布网页")
|
|
81
|
+
.argument("<webpage_id>", "网页记录 ID,可通过 webpage list 获取")
|
|
82
|
+
.option("-k, --api-key <key>", "AIConductor API Key(也可通过 AICONDUCTOR_API_KEY 环境变量或 .env 文件提供)")
|
|
83
|
+
.option("-f, --format <format>", "输出格式:json 或 text", "json")
|
|
84
|
+
.action((webpageId, options) => {
|
|
85
|
+
webpageDeleteCommand(webpageId, options);
|
|
86
|
+
});
|
|
62
87
|
|
|
63
88
|
// search 子命令组
|
|
64
89
|
const searchCmd = program
|
|
@@ -70,12 +95,17 @@ searchCmd
|
|
|
70
95
|
.description("网页搜索")
|
|
71
96
|
.argument("<query>", "搜索关键词(1~100字符)")
|
|
72
97
|
.option("-k, --api-key <key>", "AIConductor API Key(也可通过 AICONDUCTOR_API_KEY 环境变量或 .env 文件提供)")
|
|
73
|
-
.option("-n, --count <n>", "返回条数(最多50)")
|
|
74
|
-
.option("--time-range <range>", "时间范围:OneDay/OneWeek/OneMonth/OneYear 或 YYYY-MM-DD..YYYY-MM-DD")
|
|
75
|
-
.option("--
|
|
76
|
-
.option("--
|
|
77
|
-
.option("--
|
|
78
|
-
.option("--
|
|
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 改写")
|
|
79
109
|
.option("-f, --format <format>", "输出格式:json 或 text", "json")
|
|
80
110
|
.action((query, options) => {
|
|
81
111
|
searchCommand(query, "web", options);
|
|
@@ -85,9 +115,14 @@ searchCmd
|
|
|
85
115
|
.command("image")
|
|
86
116
|
.description("图片搜索")
|
|
87
117
|
.argument("<query>", "搜索关键词(1~100字符)")
|
|
88
|
-
.option("-k, --api-key <key>", "AIConductor API Key(也可通过 AICONDUCTOR_API_KEY 环境变量或 .env 文件提供)")
|
|
89
|
-
.option("-n, --count <n>", "返回条数(最多5)")
|
|
90
|
-
.option("--
|
|
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 改写")
|
|
91
126
|
.option("-f, --format <format>", "输出格式:json 或 text", "json")
|
|
92
127
|
.action((query, options) => {
|
|
93
128
|
searchCommand(query, "image", options);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "aiconductor-cli",
|
|
3
|
-
"version": "1.3.
|
|
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
|
@@ -3,9 +3,11 @@ const fs = require("fs");
|
|
|
3
3
|
const path = require("path");
|
|
4
4
|
|
|
5
5
|
const API_HOST = "plugin.aiconductor.fun";
|
|
6
|
-
const API_PATH = "/api/html_publish";
|
|
7
|
-
const MD_TO_HTML_API_PATH = "/api/md_to_html";
|
|
8
|
-
const
|
|
6
|
+
const API_PATH = "/api/html_publish";
|
|
7
|
+
const MD_TO_HTML_API_PATH = "/api/md_to_html";
|
|
8
|
+
const PUBLISHED_WEBPAGES_API_PATH = "/api/published_webpages";
|
|
9
|
+
const DELETE_PUBLISHED_WEBPAGE_API_PATH = "/api/delete_published_webpage";
|
|
10
|
+
const SEARCH_API_PATH = "/api/doubao_search";
|
|
9
11
|
const DOUYIN_VIDEO_INFO_PATH = "/api/douyin_video_info";
|
|
10
12
|
const DOUYIN_VIDEO_COMMENTS_PATH = "/api/douyin_video_comments";
|
|
11
13
|
const DOUYIN_USER_VIDEOS_PATH = "/api/douyin_user_videos";
|
|
@@ -60,7 +62,7 @@ function stripBilling(resp) {
|
|
|
60
62
|
* @param {*} value
|
|
61
63
|
* @param {string} name
|
|
62
64
|
*/
|
|
63
|
-
function validateInt(value, name) {
|
|
65
|
+
function validateInt(value, name) {
|
|
64
66
|
if (value === undefined || value === null) return;
|
|
65
67
|
const n = Number(value);
|
|
66
68
|
if (!Number.isInteger(n) || n < 0) {
|
|
@@ -124,6 +126,22 @@ function httpPost(host, requestPath, body, headers = {}) {
|
|
|
124
126
|
});
|
|
125
127
|
}
|
|
126
128
|
|
|
129
|
+
/**
|
|
130
|
+
* 校验正整数参数
|
|
131
|
+
* @param {*} value
|
|
132
|
+
* @param {string} name
|
|
133
|
+
* @param {number} [max]
|
|
134
|
+
* @returns {number}
|
|
135
|
+
*/
|
|
136
|
+
function validatePositiveInt(value, name, max) {
|
|
137
|
+
const n = Number(value);
|
|
138
|
+
if (!Number.isInteger(n) || n < 1 || (max !== undefined && n > max)) {
|
|
139
|
+
const range = max === undefined ? "大于等于 1" : `1 到 ${max}`;
|
|
140
|
+
throw Object.assign(new Error(`${name} 须为 ${range} 的整数,收到: ${value}`), { type: "validation_error" });
|
|
141
|
+
}
|
|
142
|
+
return n;
|
|
143
|
+
}
|
|
144
|
+
|
|
127
145
|
function contentTypeForFilename(filename) {
|
|
128
146
|
const types = {
|
|
129
147
|
".avif": "image/avif",
|
|
@@ -255,26 +273,62 @@ async function publishHtml(apiKey, htmlCode, title) {
|
|
|
255
273
|
}
|
|
256
274
|
|
|
257
275
|
/**
|
|
258
|
-
*
|
|
259
|
-
* @param {string} apiKey
|
|
260
|
-
* @param {{ query: string, searchType:
|
|
261
|
-
* @returns {Promise<object>}
|
|
262
|
-
*/
|
|
263
|
-
async function webSearch(apiKey, params) {
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
const
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
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
|
+
}
|
|
278
332
|
|
|
279
333
|
const resp = await httpPost(API_HOST, SEARCH_API_PATH, reqBody, {
|
|
280
334
|
Authorization: `Bearer ${apiKey}`,
|
|
@@ -291,7 +345,7 @@ async function webSearch(apiKey, params) {
|
|
|
291
345
|
* @param {boolean} [addCopyButton=true]
|
|
292
346
|
* @returns {Promise<{success: boolean, message: string, file_url: string, online_url: string}>}
|
|
293
347
|
*/
|
|
294
|
-
async function publishMdToHtml(apiKey, mdText, filename, addCopyButton = false) {
|
|
348
|
+
async function publishMdToHtml(apiKey, mdText, filename, addCopyButton = false) {
|
|
295
349
|
const reqBody = {
|
|
296
350
|
api_key: apiKey,
|
|
297
351
|
md_text: mdText,
|
|
@@ -301,8 +355,44 @@ async function publishMdToHtml(apiKey, mdText, filename, addCopyButton = false)
|
|
|
301
355
|
|
|
302
356
|
const resp = await httpPost(API_HOST, MD_TO_HTML_API_PATH, reqBody);
|
|
303
357
|
|
|
304
|
-
return stripBilling(resp);
|
|
305
|
-
}
|
|
358
|
+
return stripBilling(resp);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* 查询当前用户已发布的网页列表
|
|
363
|
+
* @param {string} apiKey
|
|
364
|
+
* @param {{page?: number|string, pageSize?: number|string}} [params]
|
|
365
|
+
* @returns {Promise<{success: boolean, webpages: Array, total: number, page: number, page_size: number}>}
|
|
366
|
+
*/
|
|
367
|
+
async function getPublishedWebpages(apiKey, params = {}) {
|
|
368
|
+
const page = validatePositiveInt(params.page === undefined ? 1 : params.page, "page");
|
|
369
|
+
const pageSize = validatePositiveInt(params.pageSize === undefined ? 20 : params.pageSize, "page_size", 100);
|
|
370
|
+
const resp = await httpPost(API_HOST, PUBLISHED_WEBPAGES_API_PATH, {
|
|
371
|
+
page,
|
|
372
|
+
page_size: pageSize,
|
|
373
|
+
}, {
|
|
374
|
+
Authorization: `Bearer ${apiKey}`,
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
return stripBilling(resp);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* 删除当前用户指定的已发布网页
|
|
382
|
+
* @param {string} apiKey
|
|
383
|
+
* @param {number|string} webpageId
|
|
384
|
+
* @returns {Promise<{success: boolean, message: string, webpage_id: number|null}>}
|
|
385
|
+
*/
|
|
386
|
+
async function deletePublishedWebpage(apiKey, webpageId) {
|
|
387
|
+
const id = validatePositiveInt(webpageId, "webpage_id");
|
|
388
|
+
const resp = await httpPost(API_HOST, DELETE_PUBLISHED_WEBPAGE_API_PATH, {
|
|
389
|
+
webpage_id: id,
|
|
390
|
+
}, {
|
|
391
|
+
Authorization: `Bearer ${apiKey}`,
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
return stripBilling(resp);
|
|
395
|
+
}
|
|
306
396
|
|
|
307
397
|
/**
|
|
308
398
|
* 获取抖音作品详情
|
|
@@ -436,8 +526,10 @@ async function xhsNotePublish(apiKey, params) {
|
|
|
436
526
|
|
|
437
527
|
module.exports = {
|
|
438
528
|
resolveApiKey,
|
|
439
|
-
publishHtml,
|
|
440
|
-
publishMdToHtml,
|
|
529
|
+
publishHtml,
|
|
530
|
+
publishMdToHtml,
|
|
531
|
+
getPublishedWebpages,
|
|
532
|
+
deletePublishedWebpage,
|
|
441
533
|
webSearch,
|
|
442
534
|
douyinVideoInfo,
|
|
443
535
|
douyinVideoComments,
|
package/src/api.test.js
ADDED
|
@@ -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
|
+
});
|