aiconductor-cli 1.1.0 → 1.2.1

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 ADDED
@@ -0,0 +1,79 @@
1
+ # AGENTS.md This file provides guidance to Agent when working with code in this repository.
2
+
3
+ ## Commands
4
+
5
+ ### 本地运行(开发调试)
6
+
7
+ ```bash
8
+ node bin/aiconductor-cli.js --help # 查看所有命令
9
+ node bin/aiconductor-cli.js html publish --help # 查看某命令选项
10
+ node bin/aiconductor-cli.js search web "关键词" -n 3 -f text # 运行网页搜索(text 输出)
11
+ node bin/aiconductor-cli.js search image "关键词" -n 2 # 运行图片搜索(JSON 输出)
12
+ ```
13
+
14
+ ### 发布到 npm
15
+
16
+ ```bash
17
+ npm version patch|minor|major # 升版本号(新增功能升 minor,修复升 patch)
18
+ npm pkg fix # 修复 package.json 格式问题
19
+ npm publish # 发布(如开启 2FA 需加 --otp=<验证码>)
20
+ ```
21
+
22
+ ### 验证流程
23
+
24
+ 发布前必须对本轮新增/修改的每个命令执行真实调用,确认 exit code 0 且输出格式正确。用 `node bin/aiconductor-cli.js <command> --help` 检查选项完整性。
25
+
26
+ ## Architecture
27
+
28
+ ### 分层设计
29
+
30
+ 项目采用三层分离架构,每层职责明确:
31
+
32
+ ```
33
+ bin/aiconductor-cli.js ← 入口层:commander 注册子命令树,不含业务逻辑
34
+ src/api.js ← API 层:HTTP 请求封装 + API Key 解析
35
+ src/commands/<topic>.js ← 命令层:参数组装、调用 API、格式化输出
36
+ ```
37
+
38
+ **`bin/aiconductor-cli.js`** 是纯聚合入口,用 commander 的 `.command().addCommand()` 结构将各 topic 模块注册为子命令树。action 回调只做参数传递,不含业务逻辑。新增命令分类只需在此文件引入并注册。
39
+
40
+ **`src/api.js`** 是所有外部 API 调用的底层封装:
41
+ - `resolveApiKey(optionKey)` — 三级优先级查找 API Key:`-k` 参数 > `AICONDUCTOR_API_KEY` 环境变量 > 当前目录 `.env` 文件
42
+ - `httpPost(host, path, body, headers)` — 通用 HTTP POST 封装,支持自定义 headers,统一处理 HTTP 错误(`type: "http_error"`)和网络错误(`type: "network_error"`)
43
+ - 每个外部 API 对应一个独立函数(如 `publishHtml`、`webSearch`),函数内过滤 `billing` 字段后返回
44
+ - 新增外部 API 渠道只改此文件
45
+
46
+ **`src/commands/<topic>.js`** 是命令编排层,每个文件对应一个命令分组:
47
+ - 接收业务参数 + options 对象
48
+ - 调用 `resolveApiKey` 解析 Key(失败则 exit 1)
49
+ - 组装参数调用 `api.js` 中的函数
50
+ - 通过 `outputResult` / `outputError` 统一处理输出
51
+ - 成功 `process.exit(0)`,失败 `process.exit(1)`
52
+
53
+ ### 命令文件命名规则
54
+
55
+ `src/commands/` 下的文件名与 API 端点名一一对应:
56
+ - `/api/html_publish` → `html_publish.js`
57
+ - `/api/aic_web_search` → `web_search.js`
58
+
59
+ 新增命令时遵循同样规则:文件名 = API 端点名(snake_case)。
60
+
61
+ ### 输出格式约定
62
+
63
+ 所有命令支持 `-f json|text` 两种输出格式:
64
+ - **JSON(默认)**:成功输出 `{ success: true, data: {...} }`,失败输出 `{ success: false, error: "..." }`,便于 AI Agent 解析
65
+ - **text**:人类可读格式,成功以 `✓` 开头,失败以 `✗` 开头
66
+
67
+ ### API Key 鉴权
68
+
69
+ 不同 API 使用不同鉴权方式:
70
+ - `html_publish`:api_key 放入请求 body
71
+ - `aic_web_search`:使用 `Authorization: Bearer <api_key>` 请求头
72
+
73
+ ### 计费信息过滤
74
+
75
+ 所有 API 函数在返回前通过解构 `const { billing, ...safe } = resp` 过滤掉计费字段,不暴露给终端用户。
76
+
77
+ ### Skills
78
+
79
+ `.codebuddy/skills/` 下存放 CodeBuddy Skill 文档(SKILL.md),描述 CLI 命令的使用方法供 AI Agent 调用。每个 Skill 对应一个 CLI 命令分组,目录名使用 kebab-case。此目录已被 `.npmignore` 排除,不会发布到 npm。
@@ -0,0 +1,417 @@
1
+ # 获取作品详情
2
+
3
+ ## 接口信息
4
+ - **URL**: `http://plugin.aiconductor.fun/api/douyin_video_info`
5
+ - **方法**: `POST` / `GET`
6
+ - **Content-Type**: `application/json`
7
+
8
+ ## 功能说明
9
+ 根据抖音视频分享链接获取视频详细信息,包括作者信息、统计数据、音乐信息等,按次计费(0.01元/次)。
10
+
11
+ ## 请求参数
12
+
13
+ | 参数名 | 类型 | 必填 | 默认值 | 说明 |
14
+ |--------|------|------|--------|------|
15
+ | share_url | string | 是 | - | 抖音视频分享链接 |
16
+ | api_key | string | 是 | - | 计费系统API密钥 |
17
+
18
+ ## 响应参数
19
+
20
+ | 参数名 | 类型 | 说明 |
21
+ |--------|------|------|
22
+ | success | boolean | 是否成功 |
23
+ | message | string | 响应消息 |
24
+ | data | object | 视频详情数据 |
25
+ | billing | object | 计费信息 |
26
+ | billing.service_fee | float | 服务费 |
27
+ | billing.total_fee | float | 总费用 |
28
+ | billing.discount_applied | boolean | 是否应用折扣 |
29
+ | billing.balance_before | float | 扣费前余额 |
30
+ | billing.balance_after | float | 扣费后余额 |
31
+ | billing.currency | string | 货币类型(CNY) |
32
+ | billing.transaction_id | string | 交易ID |
33
+
34
+ ## 注意事项
35
+ 1. 支持抖音分享链接和完整视频URL
36
+ 2. 自动解析短链接
37
+ 3. 返回视频详细信息包括作者、统计、音乐等
38
+
39
+ # 获取作品评论
40
+
41
+ ## 接口信息
42
+ - **URL**: `http://plugin.aiconductor.fun/api/douyin_video_comments`
43
+ - **方法**: `POST` / `GET`
44
+ - **Content-Type**: `application/json`
45
+
46
+ ## 功能说明
47
+ 根据抖音视频URL获取该视频的评论列表,按次计费(0.01元/次)。
48
+
49
+ ## 请求参数
50
+
51
+ | 参数名 | 类型 | 必填 | 默认值 | 说明 |
52
+ |--------|------|------|--------|------|
53
+ | video_url | string | 是 | - | 抖音视频URL |
54
+ | max_count | integer | 否 | 50 | 获取评论数量 |
55
+ | api_key | string | 是 | - | 计费系统API密钥 |
56
+
57
+ ## 响应参数
58
+
59
+ | 参数名 | 类型 | 说明 |
60
+ |--------|------|------|
61
+ | success | boolean | 是否成功 |
62
+ | message | string | 响应消息 |
63
+ | data | object | 评论数据 |
64
+ | billing | object | 计费信息 |
65
+ | billing.service_fee | float | 服务费 |
66
+ | billing.total_fee | float | 总费用 |
67
+ | billing.discount_applied | boolean | 是否应用折扣 |
68
+ | billing.balance_before | float | 扣费前余额 |
69
+ | billing.balance_after | float | 扣费后余额 |
70
+ | billing.currency | string | 货币类型(CNY) |
71
+ | billing.transaction_id | string | 交易ID |
72
+
73
+ ## 注意事项
74
+ 1. 支持抖音分享链接和完整视频URL
75
+ 2. max_count默认获取50条评论
76
+ 3. 集成计费系统,按次收费
77
+
78
+ # 获取用户视频_同步
79
+
80
+ ## 接口信息
81
+ - **URL**: `http://plugin.aiconductor.fun/api/douyin_user_videos`
82
+ - **方法**: `POST` / `GET`
83
+ - **Content-Type**: `application/json`
84
+
85
+ ## 功能说明
86
+ 根据抖音用户主页URL获取该用户发布的视频列表,支持分页获取和时间限制,按页计费(0.03元/页)。
87
+
88
+ ## 请求参数
89
+
90
+ | 参数名 | 类型 | 必填 | 默认值 | 说明 |
91
+ |--------|------|------|--------|------|
92
+ | home_page | string | 是 | - | 抖音用户主页URL |
93
+ | api_key | string | 是 | - | 计费系统API密钥 |
94
+ | max_seconds | integer | 否 | 50 | 最长运行时间(秒),最大180 |
95
+ | max_pages | integer | 否 | 1 | 最大页数 |
96
+
97
+ ## 响应参数
98
+
99
+ | 参数名 | 类型 | 说明 |
100
+ |--------|------|------|
101
+ | success | boolean | 是否成功 |
102
+ | message | string | 响应消息 |
103
+ | data | object | 视频数据 |
104
+ | data.aweme_list | array | 视频列表 |
105
+ | data.aweme_list[].author | object | 作者信息(nickname、signature) |
106
+ | data.aweme_list[].create_time | integer | 创建时间戳 |
107
+ | data.aweme_list[].create_time_formatted | string | 格式化创建时间 |
108
+ | data.aweme_list[].home_page | string | 用户主页URL |
109
+ | data.aweme_list[].desc | string | 视频描述 |
110
+ | data.aweme_list[].share_url | string | 分享链接 |
111
+ | data.aweme_list[].duration | string | 视频时长(MM:SS) |
112
+ | data.aweme_list[].statistics | object | 统计数据(点赞、收藏、评论、分享) |
113
+ | billing | object | 计费信息 |
114
+ | billing.service_fee | float | 每页服务费 |
115
+ | billing.total_fee | float | 总费用 |
116
+ | billing.discount_applied | boolean | 是否应用折扣 |
117
+ | billing.pages_charged | integer | 计费页数 |
118
+ | billing.balance_before | float | 扣费前余额 |
119
+ | billing.balance_after | float | 扣费后余额 |
120
+ | billing.currency | string | 货币类型(CNY) |
121
+ | billing.transaction_ids | array | 交易ID列表 |
122
+
123
+ ## 注意事项
124
+ 1. 支持抖音短链接和长链接,自动解析
125
+ 2. max_seconds最大值为180秒
126
+ 3. 按页收费,每成功获取一页数据扣费一次
127
+ 4. 达到时间限制或页数限制时会自动停止翻页
128
+
129
+ # 获取用户详情
130
+
131
+ ## 接口信息
132
+ - **URL**: `http://plugin.aiconductor.fun/api/douyin_user_detail`
133
+ - **方法**: `POST` / `GET`
134
+ - **Content-Type**: `application/json`
135
+
136
+ ## 功能说明
137
+ 根据抖音用户主页URL获取用户详细信息,包括基本信息、统计数据、直播状态等。自动解析短链接并提取sec_user_id,按次计费(0.03元/秒)。
138
+
139
+ ## 请求参数
140
+
141
+ | 参数名 | 类型 | 必填 | 默认值 | 说明 |
142
+ |--------|------|------|--------|------|
143
+ | home_page | string | 是 | - | 抖音用户主页URL |
144
+ | api_key | string | 是 | - | 计费系统API密钥 |
145
+
146
+ ## 响应参数
147
+
148
+ | 参数名 | 类型 | 说明 |
149
+ |--------|------|------|
150
+ | success | boolean | 是否成功 |
151
+ | message | string | 响应消息 |
152
+ | data | object | 用户详情数据 |
153
+ | data.identity | object | 身份信息(uid、sec_user_id、unique_id、nickname) |
154
+ | data.profile | object | 资料信息(签名、头像、性别、年龄等) |
155
+ | data.stats | object | 统计数据(关注数、粉丝数、作品数等) |
156
+ | data.live | object | 直播信息(直播状态、房间ID) |
157
+ | billing | object | 计费信息 |
158
+ | billing.service_fee | float | 服务费 |
159
+ | billing.total_fee | float | 总费用 |
160
+ | billing.discount_applied | boolean | 是否应用折扣 |
161
+ | billing.balance_before | float | 扣费前余额 |
162
+ | billing.balance_after | float | 扣费后余额 |
163
+ | billing.currency | string | 货币类型(CNY) |
164
+ | billing.transaction_id | string | 交易ID |
165
+
166
+ ## 注意事项
167
+ 1. 支持抖音短链接和长链接,自动解析
168
+ 2. 返回分类后的用户信息(身份、资料、统计、直播)
169
+ 3. 集成计费系统,按次收费
170
+
171
+ # 抖音用户搜索
172
+
173
+ ## 接口信息
174
+ - **URL**: `http://plugin.aiconductor.fun/api/douyin_user_search`
175
+ - **方法**: `POST` / `GET`
176
+ - **Content-Type**: `application/json`
177
+
178
+ ## 功能说明
179
+ 根据关键词搜索抖音用户,支持通过粉丝数量和用户类型进行筛选查询,支持自动翻页,按页计费(0.15元/页)。
180
+
181
+ ## 请求参数
182
+
183
+ | 参数名 | 类型 | 必填 | 默认值 | 说明 |
184
+ |--------|------|------|--------|------|
185
+ | keyword | string | 是 | - | 搜索关键词 |
186
+ | api_key | string | 是 | - | 计费系统API密钥 |
187
+ | douyin_user_fans | string | 否 | "" | 粉丝数量筛选(见下方说明) |
188
+ | douyin_user_type | string | 否 | "" | 用户类型筛选(见下方说明) |
189
+ | max_pages | integer | 否 | 1 | 最大页数,0表示不限制 |
190
+ | max_seconds | integer | 否 | 50 | 最长运行时间(秒),最大180 |
191
+
192
+ ### 粉丝数量筛选 (douyin_user_fans)
193
+
194
+ | 值 | 说明 |
195
+ |----|------|
196
+ | "" | 不限 |
197
+ | "0_1k" | 1000以下 |
198
+ | "1k_1w" | 1000到1万 |
199
+ | "1w_10w" | 1万到10万 |
200
+ | "10w_100w" | 10万到100万 |
201
+ | "100w_" | 100万以上 |
202
+
203
+ ### 用户类型筛选 (douyin_user_type)
204
+
205
+ | 值 | 说明 |
206
+ |----|------|
207
+ | "" | 不限 |
208
+ | "common_user" | 普通用户 |
209
+ | "enterprise_user" | 企业认证用户 |
210
+ | "personal_user" | 个人认证用户 |
211
+
212
+ ## 响应参数
213
+
214
+ | 参数名 | 类型 | 说明 |
215
+ |--------|------|------|
216
+ | success | boolean | 是否成功 |
217
+ | message | string | 响应消息 |
218
+ | data | object | 搜索数据 |
219
+ | data.user_list | array | 用户列表 |
220
+ | data.user_list[].uid | string | 用户ID |
221
+ | data.user_list[].nickname | string | 用户昵称 |
222
+ | data.user_list[].gender | integer | 性别(0=未知,1=男,2=女) |
223
+ | data.user_list[].unique_id | string | 抖音号 |
224
+ | data.user_list[].sec_uid | string | 安全UID |
225
+ | data.user_list[].signature | string | 个性签名 |
226
+ | data.user_list[].follower_count | integer | 粉丝数量 |
227
+ | data.user_list[].avatar_url | string | 头像URL |
228
+ | data.user_list[].follow_status | integer | 是否已关注 |
229
+ | data.user_list[].live_status | integer | 是否正在直播 |
230
+ | data.user_list[].versatile_display | string | 抖音号展示文案 |
231
+ | data.user_list[].account_cert_info | object | 账号认证信息 |
232
+ | data.cursor | integer | 下一页游标 |
233
+ | data.has_more | integer | 是否还有更多数据(1=有,0=无) |
234
+ | billing | object | 计费信息 |
235
+ | billing.service_fee | float | 每页服务费 |
236
+ | billing.total_fee | float | 总费用 |
237
+ | billing.discount_applied | boolean | 是否应用折扣 |
238
+ | billing.pages_charged | integer | 计费页数 |
239
+ | billing.balance_before | float | 扣费前余额 |
240
+ | billing.balance_after | float | 扣费后余额 |
241
+ | billing.currency | string | 货币类型(CNY) |
242
+ | billing.transaction_ids | array | 交易ID列表 |
243
+
244
+ ## 注意事项
245
+ 1. 自动翻页,无需传入cursor和search_id
246
+ 2. max_seconds最大值为50秒
247
+ 3. 按页收费,每成功获取一页数据扣费一次
248
+ 4. 达到时间限制或页数限制时会自动停止翻页
249
+
250
+ # 抖音视频搜索
251
+
252
+ ## 接口信息
253
+ - **URL**: `http://plugin.aiconductor.fun/api/douyin_video_search`
254
+ - **方法**: `POST` / `GET`
255
+ - **Content-Type**: `application/json`
256
+ - **鉴权**: 请求头 `Authorization: Bearer <api_key>`(兼容 body/query 中 `api_key` 参数)
257
+
258
+ ## 功能说明
259
+ 根据关键词搜索抖音视频,支持排序方式、发布时间、视频时长、内容类型等筛选条件,支持自动翻页,按页计费(0.15元/页)。
260
+
261
+ ## 请求参数
262
+
263
+ | 参数名 | 类型 | 必填 | 默认值 | 说明 |
264
+ |--------|------|------|--------|------|
265
+ | keyword | string | 是 | - | 搜索关键词 |
266
+ | sort_type | string | 否 | "0" | 排序方式(见下方说明) |
267
+ | publish_time | string | 否 | "0" | 发布时间筛选(见下方说明) |
268
+ | filter_duration | string | 否 | "0" | 视频时长筛选(见下方说明) |
269
+ | content_type | string | 否 | "0" | 内容类型筛选(见下方说明) |
270
+ | max_pages | integer | 否 | 1 | 最大页数,0表示不限制 |
271
+ | max_seconds | integer | 否 | 50 | 最长运行时间(秒),最大50 |
272
+
273
+ ### 排序方式 (sort_type)
274
+
275
+ | 值 | 说明 |
276
+ |----|------|
277
+ | "0" | 综合排序 |
278
+ | "1" | 最多点赞 |
279
+ | "2" | 最新发布 |
280
+
281
+ ### 发布时间筛选 (publish_time)
282
+
283
+ | 值 | 说明 |
284
+ |----|------|
285
+ | "0" | 不限 |
286
+ | "1" | 最近一天 |
287
+ | "7" | 最近一周 |
288
+ | "180" | 最近半年 |
289
+
290
+ ### 视频时长筛选 (filter_duration)
291
+
292
+ | 值 | 说明 |
293
+ |----|------|
294
+ | "0" | 不限 |
295
+ | "0-1" | 1分钟以内 |
296
+ | "1-5" | 1-5分钟 |
297
+ | "5-10000" | 5分钟以上 |
298
+
299
+ ### 内容类型筛选 (content_type)
300
+
301
+ | 值 | 说明 |
302
+ |----|------|
303
+ | "0" | 不限 |
304
+ | "1" | 视频 |
305
+ | "2" | 图片 |
306
+ | "3" | 文章 |
307
+
308
+ ## 响应参数
309
+
310
+ | 参数名 | 类型 | 说明 |
311
+ |--------|------|------|
312
+ | success | boolean | 是否成功 |
313
+ | message | string | 响应消息 |
314
+ | data | object | 搜索数据 |
315
+ | data.video_list | array | 视频列表 |
316
+ | data.video_list[].aweme_id | string | 视频ID |
317
+ | data.video_list[].desc | string | 视频描述 |
318
+ | data.video_list[].create_time | integer | 创建时间戳 |
319
+ | data.video_list[].share_url | string | 分享链接 |
320
+ | data.video_list[].author | object | 作者信息 |
321
+ | data.video_list[].author.uid | string | 用户ID |
322
+ | data.video_list[].author.short_id | string | 短ID |
323
+ | data.video_list[].author.nickname | string | 用户昵称 |
324
+ | data.video_list[].author.sec_uid | string | 安全UID |
325
+ | data.video_list[].author.unique_id | string | 抖音号 |
326
+ | data.video_list[].author.signature | string | 个性签名 |
327
+ | data.video_list[].author.follower_count | integer | 粉丝数量 |
328
+ | data.video_list[].author.total_favorited | integer | 获赞总数 |
329
+ | data.video_list[].author.is_verified | boolean | 是否认证 |
330
+ | data.video_list[].author.verification_type | integer | 认证类型 |
331
+ | data.video_list[].author.enterprise_verify_reason | string | 企业认证原因 |
332
+ | data.video_list[].author.custom_verify | string | 自定义认证 |
333
+ | data.video_list[].author.region | string | 地区 |
334
+ | data.video_list[].author.avatar_url | string | 头像URL |
335
+ | data.video_list[].music | object | 音乐信息 |
336
+ | data.video_list[].music.id | integer | 音乐ID |
337
+ | data.video_list[].music.title | string | 音乐标题 |
338
+ | data.video_list[].music.author | string | 音乐作者 |
339
+ | data.video_list[].music.duration | integer | 音乐时长(秒) |
340
+ | data.video_list[].music.play_url | string | 音乐播放地址 |
341
+ | data.video_list[].video | object | 视频信息 |
342
+ | data.video_list[].video.duration | integer | 视频时长(毫秒) |
343
+ | data.video_list[].video.ratio | string | 视频清晰度 |
344
+ | data.video_list[].video.width | integer | 视频宽度 |
345
+ | data.video_list[].video.height | integer | 视频高度 |
346
+ | data.video_list[].video.play_url | string | 视频播放地址 |
347
+ | data.video_list[].video.cover_url | string | 封面URL |
348
+ | data.video_list[].video.dynamic_cover_url | string | 动态封面URL |
349
+ | data.video_list[].video.origin_cover_url | string | 原始封面URL |
350
+ | data.video_list[].statistics | object | 互动统计数据 |
351
+ | data.video_list[].statistics.aweme_id | string | 视频ID |
352
+ | data.video_list[].statistics.comment_count | integer | 评论数 |
353
+ | data.video_list[].statistics.digg_count | integer | 点赞数 |
354
+ | data.video_list[].statistics.download_count | integer | 下载数 |
355
+ | data.video_list[].statistics.play_count | integer | 播放数 |
356
+ | data.video_list[].statistics.share_count | integer | 分享数 |
357
+ | data.video_list[].statistics.forward_count | integer | 转发数 |
358
+ | data.video_list[].statistics.collect_count | integer | 收藏数 |
359
+ | data.video_list[].video_tag | array | 视频标签列表 |
360
+ | data.video_list[].video_tag[].tag_id | integer | 标签ID |
361
+ | data.video_list[].video_tag[].tag_name | string | 标签名称 |
362
+ | data.video_list[].video_tag[].level | integer | 标签级别 |
363
+ | data.cursor | integer | 下一页游标 |
364
+ | data.has_more | integer | 是否还有更多数据(1=有,0=无) |
365
+ | billing | object | 计费信息 |
366
+ | billing.service_fee | float | 每页服务费 |
367
+ | billing.total_fee | float | 总费用 |
368
+ | billing.discount_applied | boolean | 是否应用折扣 |
369
+ | billing.pages_charged | integer | 计费页数 |
370
+ | billing.balance_before | float | 扣费前余额 |
371
+ | billing.balance_after | float | 扣费后余额 |
372
+ | billing.currency | string | 货币类型(CNY) |
373
+ | billing.transaction_ids | array | 交易ID列表 |
374
+
375
+ ## 注意事项
376
+ 1. 自动翻页,无需手动配置
377
+ 2. max_seconds最大值为50秒
378
+ 3. 按页收费,每成功获取一页数据扣费一次
379
+ 4. 达到时间限制或页数限制时会自动停止翻页
380
+
381
+ # 作品链接转文字_v2
382
+
383
+ ## 接口信息
384
+ - **URL**: `http://plugin.aiconductor.fun/api/douyin_link_to_text_v2`
385
+ - **方法**: `POST` / `GET`
386
+ - **Content-Type**: `application/json`
387
+
388
+ ## 功能说明
389
+ 将抖音视频链接中的语音转换为文字(增强版)。支持两套语音识别策略,自动检测转写准确度,按量计费(视频下载0.01元/次,文本转写0.0003元/秒)。
390
+
391
+ ## 请求参数
392
+
393
+ | 参数名 | 类型 | 必填 | 默认值 | 说明 |
394
+ |--------|------|------|--------|------|
395
+ | api_key | string | 是 | - | 计费系统API密钥 |
396
+ | video_url | string | 是 | - | 抖音视频URL(支持分享链接) |
397
+
398
+ ## 响应参数
399
+
400
+ | 参数名 | 类型 | 说明 |
401
+ |--------|------|------|
402
+ | success | boolean | 是否成功 |
403
+ | message | string | 响应消息 |
404
+ | text | string | 转写文本结果 |
405
+ | billing | object | 计费信息 |
406
+ | billing.service_fee | float | 服务费 |
407
+ | billing.total_fee | float | 总费用 |
408
+ | billing.discount_applied | boolean | 是否应用折扣 |
409
+ | billing.balance_before | float | 扣费前余额 |
410
+ | billing.balance_after | float | 扣费后余额 |
411
+ | billing.currency | string | 货币类型(CNY) |
412
+ | billing.transaction_id | string | 交易ID |
413
+
414
+ ## 注意事项
415
+ 1. 支持抖音分享链接和完整视频URL
416
+ 2. 支持两套语音识别策略,成功率更高
417
+ 3. 自动检测转写准确度,确保结果质量
package/CODEBUDDY.md ADDED
@@ -0,0 +1,79 @@
1
+ # CODEBUDDY.md This file provides guidance to CodeBuddy when working with code in this repository.
2
+
3
+ ## Commands
4
+
5
+ ### 本地运行(开发调试)
6
+
7
+ ```bash
8
+ node bin/aiconductor-cli.js --help # 查看所有命令
9
+ node bin/aiconductor-cli.js html publish --help # 查看某命令选项
10
+ node bin/aiconductor-cli.js search web "关键词" -n 3 -f text # 运行网页搜索(text 输出)
11
+ node bin/aiconductor-cli.js search image "关键词" -n 2 # 运行图片搜索(JSON 输出)
12
+ ```
13
+
14
+ ### 发布到 npm
15
+
16
+ ```bash
17
+ npm version patch|minor|major # 升版本号(新增功能升 minor,修复升 patch)
18
+ npm pkg fix # 修复 package.json 格式问题
19
+ npm publish # 发布(如开启 2FA 需加 --otp=<验证码>)
20
+ ```
21
+
22
+ ### 验证流程
23
+
24
+ 发布前必须对本轮新增/修改的每个命令执行真实调用,确认 exit code 0 且输出格式正确。用 `node bin/aiconductor-cli.js <command> --help` 检查选项完整性。
25
+
26
+ ## Architecture
27
+
28
+ ### 分层设计
29
+
30
+ 项目采用三层分离架构,每层职责明确:
31
+
32
+ ```
33
+ bin/aiconductor-cli.js ← 入口层:commander 注册子命令树,不含业务逻辑
34
+ src/api.js ← API 层:HTTP 请求封装 + API Key 解析
35
+ src/commands/<topic>.js ← 命令层:参数组装、调用 API、格式化输出
36
+ ```
37
+
38
+ **`bin/aiconductor-cli.js`** 是纯聚合入口,用 commander 的 `.command().addCommand()` 结构将各 topic 模块注册为子命令树。action 回调只做参数传递,不含业务逻辑。新增命令分类只需在此文件引入并注册。
39
+
40
+ **`src/api.js`** 是所有外部 API 调用的底层封装:
41
+ - `resolveApiKey(optionKey)` — 三级优先级查找 API Key:`-k` 参数 > `AICONDUCTOR_API_KEY` 环境变量 > 当前目录 `.env` 文件
42
+ - `httpPost(host, path, body, headers)` — 通用 HTTP POST 封装,支持自定义 headers,统一处理 HTTP 错误(`type: "http_error"`)和网络错误(`type: "network_error"`)
43
+ - 每个外部 API 对应一个独立函数(如 `publishHtml`、`webSearch`),函数内过滤 `billing` 字段后返回
44
+ - 新增外部 API 渠道只改此文件
45
+
46
+ **`src/commands/<topic>.js`** 是命令编排层,每个文件对应一个命令分组:
47
+ - 接收业务参数 + options 对象
48
+ - 调用 `resolveApiKey` 解析 Key(失败则 exit 1)
49
+ - 组装参数调用 `api.js` 中的函数
50
+ - 通过 `outputResult` / `outputError` 统一处理输出
51
+ - 成功 `process.exit(0)`,失败 `process.exit(1)`
52
+
53
+ ### 命令文件命名规则
54
+
55
+ `src/commands/` 下的文件名与 API 端点名一一对应:
56
+ - `/api/html_publish` → `html_publish.js`
57
+ - `/api/aic_web_search` → `web_search.js`
58
+
59
+ 新增命令时遵循同样规则:文件名 = API 端点名(snake_case)。
60
+
61
+ ### 输出格式约定
62
+
63
+ 所有命令支持 `-f json|text` 两种输出格式:
64
+ - **JSON(默认)**:成功输出 `{ success: true, data: {...} }`,失败输出 `{ success: false, error: "..." }`,便于 AI Agent 解析
65
+ - **text**:人类可读格式,成功以 `✓` 开头,失败以 `✗` 开头
66
+
67
+ ### API Key 鉴权
68
+
69
+ 不同 API 使用不同鉴权方式:
70
+ - `html_publish`:api_key 放入请求 body
71
+ - `aic_web_search`:使用 `Authorization: Bearer <api_key>` 请求头
72
+
73
+ ### 计费信息过滤
74
+
75
+ 所有 API 函数在返回前通过解构 `const { billing, ...safe } = resp` 过滤掉计费字段,不暴露给终端用户。
76
+
77
+ ### Skills
78
+
79
+ `.codebuddy/skills/` 下存放 CodeBuddy Skill 文档(SKILL.md),描述 CLI 命令的使用方法供 AI Agent 调用。每个 Skill 对应一个 CLI 命令分组,目录名使用 kebab-case。此目录已被 `.npmignore` 排除,不会发布到 npm。
package/README.md ADDED
@@ -0,0 +1,108 @@
1
+ # AIConductor CLI
2
+
3
+ AIConductor 平台命令行工具箱,提供 HTML 发布和网页/图片搜索功能。
4
+
5
+ ## 安装
6
+
7
+ ```bash
8
+ npm install -g aiconductor-cli
9
+ ```
10
+
11
+ **Node.js >= 16.0.0** 环境。
12
+
13
+ ## 配置 API Key
14
+
15
+ 前往 [https://dev.aiconductor.fun/register](https://dev.aiconductor.fun/register) 注册获取 API Key,然后配置环境变量:
16
+
17
+ **macOS / Linux:**
18
+ ```bash
19
+ echo 'export AICONDUCTOR_API_KEY="your-api-key"' >> ~/.zshrc && source ~/.zshrc
20
+ ```
21
+
22
+ **Windows PowerShell:**
23
+ ```powershell
24
+ [Environment]::SetEnvironmentVariable("AICONDUCTOR_API_KEY", "your-api-key", "User")
25
+ ```
26
+
27
+ > 也可在项目目录创建 `.env` 文件写入 `AICONDUCTOR_API_KEY=your-api-key`。
28
+
29
+ ## 命令
30
+
31
+ ### 发布 HTML 网页
32
+
33
+ ```bash
34
+ aiconductor-cli html publish <HTML代码或文件路径> -t <英文文件名>
35
+ ```
36
+
37
+ | 选项 | 说明 |
38
+ |------|------|
39
+ | `-t, --title` | 英文文件名(必填,小写+连字符) |
40
+ | `-k, --api-key` | API Key(可选,默认读取环境变量) |
41
+ | `-f, --format` | 输出格式:`json`(默认)或 `text` |
42
+
43
+ 示例:
44
+ ```bash
45
+ aiconductor-cli html publish index.html -t my-page
46
+ aiconductor-cli html publish "<h1>Hello</h1>" -t hello-page -f text
47
+ ```
48
+
49
+ ### 网页搜索
50
+
51
+ ```bash
52
+ aiconductor-cli search web <关键词> [选项]
53
+ ```
54
+
55
+ | 选项 | 说明 | 默认值 |
56
+ |------|------|--------|
57
+ | `-n, --count` | 返回条数(最多50) | 5 |
58
+ | `--time-range` | 时间范围:`OneDay`/`OneWeek`/`OneMonth`/`OneYear` | 无 |
59
+ | `--no-summary` | 不返回内容摘要 | 默认返回 |
60
+ | `--content-format` | 内容格式:`Text`/`Markdown` | Text |
61
+ | `--auth-level` | 权威过滤:0=不过滤,1=优先权威来源 | 0 |
62
+ | `--query-rewrite` | 开启 Query 改写 | 关闭 |
63
+
64
+ 示例:
65
+ ```bash
66
+ aiconductor-cli search web "北京今日天气" -n 3 --auth-level 1
67
+ ```
68
+
69
+ ### 图片搜索
70
+
71
+ ```bash
72
+ aiconductor-cli search image <关键词> [选项]
73
+ ```
74
+
75
+ | 选项 | 说明 | 默认值 |
76
+ |------|------|--------|
77
+ | `-n, --count` | 返回条数(最多5) | 5 |
78
+ | `--time-range` | 时间范围 | 无 |
79
+
80
+ 示例:
81
+ ```bash
82
+ aiconductor-cli search image "长城风景" -n 3 -f text
83
+ ```
84
+
85
+ ### 通用选项
86
+
87
+ 所有命令支持:
88
+ - `-k, --api-key <key>` — API Key(优先级:参数 > 环境变量 > .env 文件)
89
+ - `-f, --format <json|text>` — 输出格式,默认 JSON
90
+
91
+ ## 计费
92
+
93
+ - HTML 发布:0.01 元/次
94
+ - 网页/图片搜索:0.03 元/次
95
+ - 搜索失败不扣费,仅成功时计费
96
+
97
+ ## 开发
98
+
99
+ ```bash
100
+ git clone https://github.com/huyi9531/aiconductor-cli.git
101
+ cd aiconductor-cli
102
+ npm install
103
+ node bin/aiconductor-cli.js --help # 本地运行
104
+ ```
105
+
106
+ ## License
107
+
108
+ MIT
@@ -2,7 +2,17 @@
2
2
 
3
3
  const { Command } = require("commander");
4
4
  const { publishCommand } = require("../src/commands/html_publish");
5
+ const { publishCommand: webpagePublishCommand } = require("../src/commands/md_publish");
5
6
  const { searchCommand } = require("../src/commands/web_search");
7
+ const {
8
+ videoInfoCommand,
9
+ videoCommentsCommand,
10
+ userVideosCommand,
11
+ userDetailCommand,
12
+ userSearchCommand,
13
+ videoSearchCommand,
14
+ linkToTextCommand,
15
+ } = require("../src/commands/douyin");
6
16
 
7
17
  const program = new Command();
8
18
 
@@ -27,6 +37,24 @@ htmlCmd
27
37
  publishCommand(html, options);
28
38
  });
29
39
 
40
+ // webpage 子命令组(统一发布 Markdown/HTML 为在线网页)
41
+ const webpageCmd = program
42
+ .command("webpage")
43
+ .description("网页发布(支持 Markdown 和 HTML)");
44
+
45
+ webpageCmd
46
+ .command("publish")
47
+ .description("将 Markdown 或 HTML 发布为在线网页(自动识别类型)")
48
+ .argument("<input>", "Markdown/HTML 文本字符串或文件路径")
49
+ .option("-k, --api-key <key>", "AIConductor API Key(也可通过 AICONDUCTOR_API_KEY 环境变量或 .env 文件提供)")
50
+ .option("-t, --title <title>", "文件名(不含 .html 后缀)")
51
+ .option("--type <type>", "强制指定类型:md 或 html(默认自动识别)")
52
+ .option("--copy-button", "添加代码复制按钮(仅 Markdown 类型生效)")
53
+ .option("-f, --format <format>", "输出格式:json 或 text", "json")
54
+ .action((input, options) => {
55
+ webpagePublishCommand(input, options);
56
+ });
57
+
30
58
  // search 子命令组
31
59
  const searchCmd = program
32
60
  .command("search")
@@ -60,6 +88,82 @@ searchCmd
60
88
  searchCommand(query, "image", options);
61
89
  });
62
90
 
91
+ // 未知命令处理
92
+
93
+ // douyin 子命令组
94
+ const douyinCmd = program
95
+ .command("douyin")
96
+ .description("抖音数据工具箱");
97
+
98
+ douyinCmd
99
+ .command("video-info")
100
+ .description("获取抖音作品详情(0.01元/次)")
101
+ .argument("<share_url>", "抖音视频分享链接")
102
+ .option("-k, --api-key <key>", "AIConductor API Key")
103
+ .option("-f, --format <format>", "输出格式:json 或 text", "json")
104
+ .action((shareUrl, options) => { videoInfoCommand(shareUrl, options); });
105
+
106
+ douyinCmd
107
+ .command("video-comments")
108
+ .description("获取抖音作品评论(0.01元/次)")
109
+ .argument("<video_url>", "抖音视频URL")
110
+ .option("-k, --api-key <key>", "AIConductor API Key")
111
+ .option("--max-count <n>", "获取评论数量", "50")
112
+ .option("-f, --format <format>", "输出格式:json 或 text", "json")
113
+ .action((videoUrl, options) => { videoCommentsCommand(videoUrl, options); });
114
+
115
+ douyinCmd
116
+ .command("user-videos")
117
+ .description("获取抖音用户视频列表(0.03元/页)")
118
+ .argument("<home_page>", "抖音用户主页URL")
119
+ .option("-k, --api-key <key>", "AIConductor API Key")
120
+ .option("--max-pages <n>", "最大页数", "1")
121
+ .option("--max-seconds <n>", "最长运行时间(秒),最大180", "50")
122
+ .option("-f, --format <format>", "输出格式:json 或 text", "json")
123
+ .action((homePage, options) => { userVideosCommand(homePage, options); });
124
+
125
+ douyinCmd
126
+ .command("user-detail")
127
+ .description("获取抖音用户详情(0.03元/次)")
128
+ .argument("<home_page>", "抖音用户主页URL")
129
+ .option("-k, --api-key <key>", "AIConductor API Key")
130
+ .option("-f, --format <format>", "输出格式:json 或 text", "json")
131
+ .action((homePage, options) => { userDetailCommand(homePage, options); });
132
+
133
+ douyinCmd
134
+ .command("user-search")
135
+ .description("搜索抖音用户(0.15元/页)")
136
+ .argument("<keyword>", "搜索关键词")
137
+ .option("-k, --api-key <key>", "AIConductor API Key")
138
+ .option("--max-pages <n>", "最大页数,0=不限制", "1")
139
+ .option("--max-seconds <n>", "最长运行时间(秒),最大180", "50")
140
+ .option("--fans-range <range>", "粉丝数量筛选:0_1k/1k_1w/1w_10w/10w_100w/100w_")
141
+ .option("--user-type <type>", "用户类型筛选:common_user/enterprise_user/personal_user")
142
+ .option("-f, --format <format>", "输出格式:json 或 text", "json")
143
+ .action((keyword, options) => { userSearchCommand(keyword, options); });
144
+
145
+ douyinCmd
146
+ .command("video-search")
147
+ .description("搜索抖音视频(0.15元/页)")
148
+ .argument("<keyword>", "搜索关键词")
149
+ .option("-k, --api-key <key>", "AIConductor API Key")
150
+ .option("--max-pages <n>", "最大页数,0=不限制", "1")
151
+ .option("--max-seconds <n>", "最长运行时间(秒),最大50", "50")
152
+ .option("--sort-type <type>", "排序方式:0=综合,1=最多点赞,2=最新发布", "0")
153
+ .option("--publish-time <time>", "发布时间:0=不限,1=一天,7=一周,180=半年", "0")
154
+ .option("--filter-duration <dur>", "视频时长:0=不限,0-1=1分钟内,1-5=1-5分钟,5-10000=5分钟以上", "0")
155
+ .option("--content-type <type>", "内容类型:0=不限,1=视频,2=图片,3=文章", "0")
156
+ .option("-f, --format <format>", "输出格式:json 或 text", "json")
157
+ .action((keyword, options) => { videoSearchCommand(keyword, options); });
158
+
159
+ douyinCmd
160
+ .command("link-to-text")
161
+ .description("抖音作品链接转文字(0.01元/次 + 0.0003元/秒)")
162
+ .argument("<video_url>", "抖音视频URL")
163
+ .option("-k, --api-key <key>", "AIConductor API Key")
164
+ .option("-f, --format <format>", "输出格式:json 或 text", "json")
165
+ .action((videoUrl, options) => { linkToTextCommand(videoUrl, options); });
166
+
63
167
  // 未知命令处理
64
168
  program.on("command:*", () => {
65
169
  console.error(`未知命令: ${program.args.join(" ")}`);
package/package.json CHANGED
@@ -1,25 +1,25 @@
1
- {
2
- "name": "aiconductor-cli",
3
- "version": "1.1.0",
4
- "description": "AIConductor CLI 工具箱",
5
- "keywords": [
6
- "aiconductor",
7
- "html",
8
- "publish",
9
- "deploy",
10
- "cli"
11
- ],
12
- "author": "",
13
- "license": "MIT",
14
- "main": "src/index.js",
15
- "bin": {
16
- "aiconductor-cli": "bin/aiconductor-cli.js"
17
- },
18
- "engines": {
19
- "node": ">=16.0.0"
20
- },
21
- "scripts": {},
22
- "dependencies": {
23
- "commander": "^12.1.0"
24
- }
25
- }
1
+ {
2
+ "name": "aiconductor-cli",
3
+ "version": "1.2.1",
4
+ "description": "AIConductor CLI 工具箱",
5
+ "keywords": [
6
+ "aiconductor",
7
+ "html",
8
+ "publish",
9
+ "deploy",
10
+ "cli"
11
+ ],
12
+ "author": "",
13
+ "license": "MIT",
14
+ "main": "src/index.js",
15
+ "bin": {
16
+ "aiconductor-cli": "bin/aiconductor-cli.js"
17
+ },
18
+ "engines": {
19
+ "node": ">=16.0.0"
20
+ },
21
+ "scripts": {},
22
+ "dependencies": {
23
+ "commander": "^12.1.0"
24
+ }
25
+ }
package/src/api.js CHANGED
@@ -4,7 +4,15 @@ const path = require("path");
4
4
 
5
5
  const API_HOST = "plugin.aiconductor.fun";
6
6
  const API_PATH = "/api/html_publish";
7
+ const MD_TO_HTML_API_PATH = "/api/md_to_html";
7
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";
8
16
 
9
17
  /**
10
18
  * 解析 API Key,三级优先级:命令行参数 > 系统环境变量 > .env 文件
@@ -137,4 +145,155 @@ async function webSearch(apiKey, params) {
137
145
  return safe;
138
146
  }
139
147
 
140
- module.exports = { resolveApiKey, publishHtml, webSearch };
148
+ /**
149
+ * Markdown 转 HTML 并发布
150
+ * @param {string} apiKey
151
+ * @param {string} mdText
152
+ * @param {string} [filename]
153
+ * @param {boolean} [addCopyButton=true]
154
+ * @returns {Promise<{success: boolean, message: string, file_url: string, online_url: string}>}
155
+ */
156
+ async function publishMdToHtml(apiKey, mdText, filename, addCopyButton = false) {
157
+ const reqBody = {
158
+ api_key: apiKey,
159
+ md_text: mdText,
160
+ add_copy_button: addCopyButton,
161
+ };
162
+ if (filename) reqBody.filename = filename;
163
+
164
+ const resp = await httpPost(API_HOST, MD_TO_HTML_API_PATH, reqBody);
165
+
166
+ const { billing, ...safe } = resp;
167
+ return safe;
168
+ }
169
+
170
+ /**
171
+ * 获取抖音作品详情
172
+ * @param {string} apiKey
173
+ * @param {string} shareUrl
174
+ * @returns {Promise<object>}
175
+ */
176
+ async function douyinVideoInfo(apiKey, shareUrl) {
177
+ const resp = await httpPost(API_HOST, DOUYIN_VIDEO_INFO_PATH, {
178
+ api_key: apiKey,
179
+ share_url: shareUrl,
180
+ });
181
+ const { billing, ...safe } = resp;
182
+ return safe;
183
+ }
184
+
185
+ /**
186
+ * 获取抖音作品评论
187
+ * @param {string} apiKey
188
+ * @param {string} videoUrl
189
+ * @param {number} [maxCount=50]
190
+ * @returns {Promise<object>}
191
+ */
192
+ async function douyinVideoComments(apiKey, videoUrl, maxCount) {
193
+ const body = { api_key: apiKey, video_url: videoUrl };
194
+ if (maxCount !== undefined && maxCount !== null) body.max_count = maxCount;
195
+ const resp = await httpPost(API_HOST, DOUYIN_VIDEO_COMMENTS_PATH, body);
196
+ const { billing, ...safe } = resp;
197
+ return safe;
198
+ }
199
+
200
+ /**
201
+ * 获取抖音用户视频列表
202
+ * @param {string} apiKey
203
+ * @param {string} homePage
204
+ * @param {number} [maxSeconds=50]
205
+ * @param {number} [maxPages=1]
206
+ * @returns {Promise<object>}
207
+ */
208
+ async function douyinUserVideos(apiKey, homePage, maxSeconds, maxPages) {
209
+ const body = { api_key: apiKey, home_page: homePage };
210
+ if (maxSeconds !== undefined && maxSeconds !== null) body.max_seconds = maxSeconds;
211
+ if (maxPages !== undefined && maxPages !== null) body.max_pages = maxPages;
212
+ const resp = await httpPost(API_HOST, DOUYIN_USER_VIDEOS_PATH, body);
213
+ const { billing, ...safe } = resp;
214
+ return safe;
215
+ }
216
+
217
+ /**
218
+ * 获取抖音用户详情
219
+ * @param {string} apiKey
220
+ * @param {string} homePage
221
+ * @returns {Promise<object>}
222
+ */
223
+ async function douyinUserDetail(apiKey, homePage) {
224
+ const resp = await httpPost(API_HOST, DOUYIN_USER_DETAIL_PATH, {
225
+ api_key: apiKey,
226
+ home_page: homePage,
227
+ });
228
+ const { billing, ...safe } = resp;
229
+ return safe;
230
+ }
231
+
232
+ /**
233
+ * 搜索抖音用户
234
+ * @param {string} apiKey
235
+ * @param {string} keyword
236
+ * @param {object} [opts]
237
+ * @returns {Promise<object>}
238
+ */
239
+ async function douyinUserSearch(apiKey, keyword, opts = {}) {
240
+ const body = { api_key: apiKey, keyword };
241
+ if (opts.maxPages !== undefined && opts.maxPages !== null) body.max_pages = opts.maxPages;
242
+ if (opts.maxSeconds !== undefined && opts.maxSeconds !== null) body.max_seconds = opts.maxSeconds;
243
+ if (opts.fansRange) body.douyin_user_fans = opts.fansRange;
244
+ if (opts.userType) body.douyin_user_type = opts.userType;
245
+ const resp = await httpPost(API_HOST, DOUYIN_USER_SEARCH_PATH, body);
246
+ const { billing, ...safe } = resp;
247
+ return safe;
248
+ }
249
+
250
+ /**
251
+ * 搜索抖音视频
252
+ * @param {string} apiKey
253
+ * @param {string} keyword
254
+ * @param {object} [opts]
255
+ * @returns {Promise<object>}
256
+ */
257
+ async function douyinVideoSearch(apiKey, keyword, opts = {}) {
258
+ const body = { keyword };
259
+ if (opts.sortType !== undefined) body.sort_type = opts.sortType;
260
+ if (opts.publishTime !== undefined) body.publish_time = opts.publishTime;
261
+ if (opts.filterDuration !== undefined) body.filter_duration = opts.filterDuration;
262
+ if (opts.contentType !== undefined) body.content_type = opts.contentType;
263
+ if (opts.maxPages !== undefined && opts.maxPages !== null) body.max_pages = opts.maxPages;
264
+ if (opts.maxSeconds !== undefined && opts.maxSeconds !== null) body.max_seconds = opts.maxSeconds;
265
+ const resp = await httpPost(API_HOST, DOUYIN_VIDEO_SEARCH_PATH, body, {
266
+ Authorization: `Bearer ${apiKey}`,
267
+ });
268
+ const { billing, ...safe } = resp;
269
+ return safe;
270
+ }
271
+
272
+ /**
273
+ * 抖音作品链接转文字
274
+ * @param {string} apiKey
275
+ * @param {string} videoUrl
276
+ * @returns {Promise<object>}
277
+ */
278
+ async function douyinLinkToText(apiKey, videoUrl) {
279
+ const resp = await httpPost(API_HOST, DOUYIN_LINK_TO_TEXT_PATH, {
280
+ api_key: apiKey,
281
+ video_url: videoUrl,
282
+ });
283
+ const { billing, ...safe } = resp;
284
+ return safe;
285
+ }
286
+
287
+ module.exports = {
288
+ resolveApiKey,
289
+ publishHtml,
290
+ publishMdToHtml,
291
+ webSearch,
292
+ douyinVideoInfo,
293
+ douyinVideoComments,
294
+ douyinUserVideos,
295
+ douyinUserDetail,
296
+ douyinUserSearch,
297
+ douyinVideoSearch,
298
+ douyinLinkToText,
299
+ };
@@ -0,0 +1,151 @@
1
+ const {
2
+ resolveApiKey,
3
+ douyinVideoInfo,
4
+ douyinVideoComments,
5
+ douyinUserVideos,
6
+ douyinUserDetail,
7
+ douyinUserSearch,
8
+ douyinVideoSearch,
9
+ douyinLinkToText,
10
+ } = require("../api");
11
+
12
+ function outputResult(data, format) {
13
+ if (format === "json") {
14
+ process.stdout.write(JSON.stringify({ success: true, data }, null, 2) + "\n");
15
+ } else {
16
+ process.stdout.write("\u2713 \u64cd\u4f5c\u6210\u529f\n");
17
+ process.stdout.write(JSON.stringify(data, null, 2) + "\n");
18
+ }
19
+ }
20
+
21
+ function outputError(err, format) {
22
+ if (format === "json") {
23
+ process.stderr.write(JSON.stringify({ success: false, error: err.message || String(err) }, null, 2) + "\n");
24
+ } else {
25
+ process.stderr.write("\u2717 \u9519\u8bef: " + (err.message || err) + "\n");
26
+ }
27
+ }
28
+
29
+ function resolveKey(optionKey, format) {
30
+ const key = resolveApiKey(optionKey);
31
+ if (!key) {
32
+ outputError(
33
+ { message: "\u7f3a\u5c11 API Key\u3002\u8bf7\u901a\u8fc7 -k \u53c2\u6570\u3001AICONDUCTOR_API_KEY \u73af\u5883\u53d8\u91cf\u6216 .env \u6587\u4ef6\u63d0\u4f9b\u3002" },
34
+ format
35
+ );
36
+ process.exit(1);
37
+ }
38
+ return key;
39
+ }
40
+
41
+ function handleError(err, format) {
42
+ if (err.type === "http_error") {
43
+ outputError(
44
+ { message: "API \u8fd4\u56de\u9519\u8bef (HTTP " + err.statusCode + "): " + (err.body?.message || JSON.stringify(err.body)) },
45
+ format
46
+ );
47
+ } else if (err.type === "network_error") {
48
+ outputError({ message: "\u7f51\u7edc\u9519\u8bef: " + err.message }, format);
49
+ } else {
50
+ outputError({ message: "\u672a\u77e5\u9519\u8bef: " + (err.message || err) }, format);
51
+ }
52
+ }
53
+
54
+ async function videoInfoCommand(shareUrl, options = {}) {
55
+ const format = options.format || "json";
56
+ const key = resolveKey(options.apiKey, format);
57
+ try {
58
+ const result = await douyinVideoInfo(key, shareUrl);
59
+ outputResult(result, format);
60
+ process.exit(0);
61
+ } catch (err) { handleError(err, format); process.exit(1); }
62
+ }
63
+
64
+ async function videoCommentsCommand(videoUrl, options = {}) {
65
+ const format = options.format || "json";
66
+ const key = resolveKey(options.apiKey, format);
67
+ try {
68
+ const maxCount = options.maxCount ? parseInt(options.maxCount, 10) : undefined;
69
+ const result = await douyinVideoComments(key, videoUrl, maxCount);
70
+ outputResult(result, format);
71
+ process.exit(0);
72
+ } catch (err) { handleError(err, format); process.exit(1); }
73
+ }
74
+
75
+ async function userVideosCommand(homePage, options = {}) {
76
+ const format = options.format || "json";
77
+ const key = resolveKey(options.apiKey, format);
78
+ try {
79
+ const maxSeconds = options.maxSeconds ? parseInt(options.maxSeconds, 10) : undefined;
80
+ const maxPages = options.maxPages ? parseInt(options.maxPages, 10) : undefined;
81
+ const result = await douyinUserVideos(key, homePage, maxSeconds, maxPages);
82
+ outputResult(result, format);
83
+ process.exit(0);
84
+ } catch (err) { handleError(err, format); process.exit(1); }
85
+ }
86
+
87
+ async function userDetailCommand(homePage, options = {}) {
88
+ const format = options.format || "json";
89
+ const key = resolveKey(options.apiKey, format);
90
+ try {
91
+ const result = await douyinUserDetail(key, homePage);
92
+ outputResult(result, format);
93
+ process.exit(0);
94
+ } catch (err) { handleError(err, format); process.exit(1); }
95
+ }
96
+
97
+ async function userSearchCommand(keyword, options = {}) {
98
+ const format = options.format || "json";
99
+ const key = resolveKey(options.apiKey, format);
100
+ try {
101
+ const maxPages = options.maxPages ? parseInt(options.maxPages, 10) : undefined;
102
+ const maxSeconds = options.maxSeconds ? parseInt(options.maxSeconds, 10) : undefined;
103
+ const result = await douyinUserSearch(key, keyword, {
104
+ maxPages,
105
+ maxSeconds,
106
+ fansRange: options.fansRange,
107
+ userType: options.userType,
108
+ });
109
+ outputResult(result, format);
110
+ process.exit(0);
111
+ } catch (err) { handleError(err, format); process.exit(1); }
112
+ }
113
+
114
+ async function videoSearchCommand(keyword, options = {}) {
115
+ const format = options.format || "json";
116
+ const key = resolveKey(options.apiKey, format);
117
+ try {
118
+ const maxPages = options.maxPages ? parseInt(options.maxPages, 10) : undefined;
119
+ const maxSeconds = options.maxSeconds ? parseInt(options.maxSeconds, 10) : undefined;
120
+ const result = await douyinVideoSearch(key, keyword, {
121
+ maxPages,
122
+ maxSeconds,
123
+ sortType: options.sortType,
124
+ publishTime: options.publishTime,
125
+ filterDuration: options.filterDuration,
126
+ contentType: options.contentType,
127
+ });
128
+ outputResult(result, format);
129
+ process.exit(0);
130
+ } catch (err) { handleError(err, format); process.exit(1); }
131
+ }
132
+
133
+ async function linkToTextCommand(videoUrl, options = {}) {
134
+ const format = options.format || "json";
135
+ const key = resolveKey(options.apiKey, format);
136
+ try {
137
+ const result = await douyinLinkToText(key, videoUrl);
138
+ outputResult(result, format);
139
+ process.exit(0);
140
+ } catch (err) { handleError(err, format); process.exit(1); }
141
+ }
142
+
143
+ module.exports = {
144
+ videoInfoCommand,
145
+ videoCommentsCommand,
146
+ userVideosCommand,
147
+ userDetailCommand,
148
+ userSearchCommand,
149
+ videoSearchCommand,
150
+ linkToTextCommand,
151
+ };
@@ -0,0 +1,118 @@
1
+ const fs = require("fs");
2
+ const path = require("path");
3
+ const { resolveApiKey, publishHtml, publishMdToHtml } = require("../api");
4
+
5
+ /**
6
+ * 解析输入内容:如果是已存在的文件路径则读取,否则当作原始字符串
7
+ * @param {string} input
8
+ * @returns {{ content: string, filePath: string|null }}
9
+ */
10
+ function resolveInput(input) {
11
+ const trimmed = input.trim();
12
+ try {
13
+ const filePath = path.resolve(trimmed);
14
+ if (fs.existsSync(filePath) && fs.statSync(filePath).isFile()) {
15
+ return { content: fs.readFileSync(filePath, "utf-8"), filePath };
16
+ }
17
+ } catch (_) {}
18
+ return { content: trimmed, filePath: null };
19
+ }
20
+
21
+ /**
22
+ * 自动检测内容类型:Markdown 或 HTML
23
+ * @param {string} content
24
+ * @param {string|null} filePath
25
+ * @returns {"md"|"html"}
26
+ */
27
+ function detectType(content, filePath) {
28
+ // 优先用文件扩展名判断
29
+ if (filePath) {
30
+ const ext = path.extname(filePath).toLowerCase();
31
+ if (ext === ".md" || ext === ".markdown") return "md";
32
+ if (ext === ".html" || ext === ".htm") return "html";
33
+ }
34
+ // 内容特征判断
35
+ const trimmed = content.trim();
36
+ if (trimmed.startsWith("<!DOCTYPE") || trimmed.startsWith("<html") || trimmed.startsWith("<head")) {
37
+ return "html";
38
+ }
39
+ if (trimmed.startsWith("<") && /<\w+[^>]*>/.test(trimmed.substring(0, 200))) {
40
+ return "html";
41
+ }
42
+ // 默认 Markdown
43
+ return "md";
44
+ }
45
+
46
+ function outputResult(data, format) {
47
+ if (format === "json") {
48
+ process.stdout.write(JSON.stringify({ success: true, data }, null, 2) + "\n");
49
+ } else {
50
+ process.stdout.write(`✓ 发布成功\n`);
51
+ process.stdout.write(` URL: ${data.online_url}\n`);
52
+ if (data.message) {
53
+ process.stdout.write(` 信息: ${data.message}\n`);
54
+ }
55
+ }
56
+ }
57
+
58
+ function outputError(err, format) {
59
+ if (format === "json") {
60
+ process.stderr.write(JSON.stringify({ success: false, error: err.message || String(err) }, null, 2) + "\n");
61
+ } else {
62
+ process.stderr.write(`✗ 错误: ${err.message || err}\n`);
63
+ }
64
+ }
65
+
66
+ /**
67
+ * webpage publish 命令 — 统一发布 Markdown 或 HTML 为在线网页
68
+ * @param {string} input - Markdown/HTML 代码或文件路径
69
+ * @param {{ apiKey?: string, format?: string, title?: string, copyButton?: boolean, type?: string }} options
70
+ */
71
+ async function publishCommand(input, options = {}) {
72
+ const format = options.format || "json";
73
+ const resolvedKey = resolveApiKey(options.apiKey);
74
+
75
+ if (!resolvedKey) {
76
+ outputError(
77
+ { message: "缺少 API Key。请通过 -k 参数、AICONDUCTOR_API_KEY 环境变量或 .env 文件提供。" },
78
+ format
79
+ );
80
+ process.exit(1);
81
+ }
82
+
83
+ try {
84
+ const { content, filePath } = resolveInput(input);
85
+ // 确定类型:--type 显式指定优先,否则自动检测
86
+ let type = options.type;
87
+ if (!type) {
88
+ type = detectType(content, filePath);
89
+ } else if (type !== "md" && type !== "html") {
90
+ outputError({ message: `无效的 --type 值: "${type}",可选: md 或 html` }, format);
91
+ process.exit(1);
92
+ }
93
+
94
+ let result;
95
+ if (type === "html") {
96
+ result = await publishHtml(resolvedKey, content, options.title);
97
+ } else {
98
+ const addCopyButton = options.copyButton === true;
99
+ result = await publishMdToHtml(resolvedKey, content, options.title, addCopyButton);
100
+ }
101
+ outputResult(result, format);
102
+ process.exit(0);
103
+ } catch (err) {
104
+ if (err.type === "http_error") {
105
+ outputError(
106
+ { message: `API 返回错误 (HTTP ${err.statusCode}): ${err.body?.message || JSON.stringify(err.body)}` },
107
+ format
108
+ );
109
+ } else if (err.type === "network_error") {
110
+ outputError({ message: `网络错误: ${err.message}` }, format);
111
+ } else {
112
+ outputError({ message: `未知错误: ${err.message || err}` }, format);
113
+ }
114
+ process.exit(1);
115
+ }
116
+ }
117
+
118
+ module.exports = { publishCommand };