aiconductor-cli 1.1.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CODEBUDDY.md +79 -0
- package/README.md +108 -0
- package/bin/aiconductor-cli.js +19 -0
- package/package.json +1 -1
- package/src/api.js +24 -1
- package/src/commands/md_publish.js +118 -0
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
|
package/bin/aiconductor-cli.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
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");
|
|
6
7
|
|
|
7
8
|
const program = new Command();
|
|
@@ -27,6 +28,24 @@ htmlCmd
|
|
|
27
28
|
publishCommand(html, options);
|
|
28
29
|
});
|
|
29
30
|
|
|
31
|
+
// webpage 子命令组(统一发布 Markdown/HTML 为在线网页)
|
|
32
|
+
const webpageCmd = program
|
|
33
|
+
.command("webpage")
|
|
34
|
+
.description("网页发布(支持 Markdown 和 HTML)");
|
|
35
|
+
|
|
36
|
+
webpageCmd
|
|
37
|
+
.command("publish")
|
|
38
|
+
.description("将 Markdown 或 HTML 发布为在线网页(自动识别类型)")
|
|
39
|
+
.argument("<input>", "Markdown/HTML 文本字符串或文件路径")
|
|
40
|
+
.option("-k, --api-key <key>", "AIConductor API Key(也可通过 AICONDUCTOR_API_KEY 环境变量或 .env 文件提供)")
|
|
41
|
+
.option("-t, --title <title>", "文件名(不含 .html 后缀)")
|
|
42
|
+
.option("--type <type>", "强制指定类型:md 或 html(默认自动识别)")
|
|
43
|
+
.option("--copy-button", "添加代码复制按钮(仅 Markdown 类型生效)")
|
|
44
|
+
.option("-f, --format <format>", "输出格式:json 或 text", "json")
|
|
45
|
+
.action((input, options) => {
|
|
46
|
+
webpagePublishCommand(input, options);
|
|
47
|
+
});
|
|
48
|
+
|
|
30
49
|
// search 子命令组
|
|
31
50
|
const searchCmd = program
|
|
32
51
|
.command("search")
|
package/package.json
CHANGED
package/src/api.js
CHANGED
|
@@ -4,6 +4,7 @@ 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";
|
|
8
9
|
|
|
9
10
|
/**
|
|
@@ -137,4 +138,26 @@ async function webSearch(apiKey, params) {
|
|
|
137
138
|
return safe;
|
|
138
139
|
}
|
|
139
140
|
|
|
140
|
-
|
|
141
|
+
/**
|
|
142
|
+
* Markdown 转 HTML 并发布
|
|
143
|
+
* @param {string} apiKey
|
|
144
|
+
* @param {string} mdText
|
|
145
|
+
* @param {string} [filename]
|
|
146
|
+
* @param {boolean} [addCopyButton=true]
|
|
147
|
+
* @returns {Promise<{success: boolean, message: string, file_url: string, online_url: string}>}
|
|
148
|
+
*/
|
|
149
|
+
async function publishMdToHtml(apiKey, mdText, filename, addCopyButton = false) {
|
|
150
|
+
const reqBody = {
|
|
151
|
+
api_key: apiKey,
|
|
152
|
+
md_text: mdText,
|
|
153
|
+
add_copy_button: addCopyButton,
|
|
154
|
+
};
|
|
155
|
+
if (filename) reqBody.filename = filename;
|
|
156
|
+
|
|
157
|
+
const resp = await httpPost(API_HOST, MD_TO_HTML_API_PATH, reqBody);
|
|
158
|
+
|
|
159
|
+
const { billing, ...safe } = resp;
|
|
160
|
+
return safe;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
module.exports = { resolveApiKey, publishHtml, publishMdToHtml, webSearch };
|
|
@@ -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 };
|