ability-cli 0.1.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/README.md ADDED
@@ -0,0 +1,323 @@
1
+ # ability-cli
2
+
3
+ ## 1. 项目简介
4
+
5
+ **ability-cli** 是面向原子能力平台的命令行工具,用于在终端中搜索能力、查看详情、执行调用,以及直接访问底层 HTTP 接口。
6
+
7
+ 采用**双层设计**:
8
+
9
+ - **上层工作流命令**:面向日常使用的 `search`、`inspect`、`exec`,用自然语言或能力 ID 完成「找能力 → 看 schema → 调能力」的闭环。
10
+ - **下层接口直通 `raw`**:按路径映射网关 REST 接口,便于联调、脚本化与排障。
11
+
12
+ ---
13
+
14
+ ## 2. 安装
15
+
16
+ ```bash
17
+ # 克隆仓库
18
+ git clone <repo-url>
19
+ cd ability-cli
20
+
21
+ # 安装依赖
22
+ pnpm install
23
+
24
+ # 构建
25
+ pnpm build
26
+
27
+ # 全局链接(可选,Windows 推荐)
28
+ npm link
29
+ ```
30
+
31
+ 构建完成后,可通过 `ability-cli`(或 `node dist/index.js`)运行。若使用全局链接,请确保 `pnpm` 的全局 bin 目录在 `PATH` 中。
32
+
33
+ ---
34
+
35
+ ## 3. 快速上手
36
+
37
+ ```bash
38
+ # 配置(首次使用)
39
+ ability-cli config set --app-id <your-app-id> --app-secret <your-secret>
40
+
41
+ # 搜索能力
42
+ ability-cli search "帮我导航回家" --device-id abc123
43
+
44
+ # 查看能力详情
45
+ ability-cli inspect --ability-id 20001
46
+
47
+ # 生成参数模板
48
+ ability-cli inspect --ability-id 20001 --gen-template > params.json
49
+
50
+ # 执行能力
51
+ ability-cli exec --ability-id 20001 --params params.json --device-id abc123
52
+
53
+ # 健康检查
54
+ ability-cli doctor
55
+ ```
56
+
57
+ ---
58
+
59
+ ## 4. 命令参考
60
+
61
+ ### 全局选项
62
+
63
+ 下列选项可写在任意子命令**之前**,作用于本次整条命令(例如 `ability-cli --env prod search "查询天气"`)。
64
+
65
+ | 选项 | 说明 |
66
+ |------|------|
67
+ | `--env <env>` | 环境:`test` / `prod`,决定使用配置文件中哪一套 `profiles` |
68
+ | `--app-id <id>` | `zy-app-id` |
69
+ | `--app-secret <secret>` | `appSecret` |
70
+ | `--auth-token <token>` | `Authorization` 请求头 |
71
+ | `--base-url <url>` | 网关根地址 |
72
+ | `--magic-sign` | 测试环境跳过验签(`zy-sign` 固定为测试用魔数) |
73
+ | `-v, --verbose` | 详细输出(请求 URL、头、体与响应等) |
74
+
75
+ ### 上层命令
76
+
77
+ #### `search [query]`
78
+
79
+ 搜索能力(自然语言或配合筛选条件,底层调用能力列表接口)。
80
+
81
+ | 选项 | 说明 |
82
+ |------|------|
83
+ | `[query]` | 自然语言检索,映射为查询参数 `nl` |
84
+ | `--device-id <id>` | 设备 ID;未指定时使用配置文件 `defaults.deviceId` |
85
+ | `--app-name <name>` | 应用名称 |
86
+ | `--type <type>` | 能力二级分类 |
87
+ | `--capability-type <ct>` | 能力三级分类 |
88
+ | `--json` | 仅输出 JSON(`data` 部分) |
89
+
90
+ #### `inspect`
91
+
92
+ 查看能力详情与 `inputSchema`。
93
+
94
+ | 选项 | 说明 |
95
+ |------|------|
96
+ | `--ability-id <id>` | **必填**,能力 ID |
97
+ | `--gen-template` | 根据 `inputSchema` 生成参数模板 JSON 并输出 |
98
+ | `--json` | 仅输出 JSON(`data` 部分) |
99
+
100
+ #### `exec`
101
+
102
+ 执行能力(异步 `call-tool`)。
103
+
104
+ | 选项 | 说明 |
105
+ |------|------|
106
+ | `--ability-id <id>` | **必填**,能力 ID |
107
+ | `--params <json>` | 参数:JSON 字符串,或指向 JSON 文件的路径 |
108
+ | `--device-id <id>` | 设备 ID;默认可来自 `defaults.deviceId` |
109
+ | `--session-id <id>` | `sessionId` |
110
+ | `--tool-call-id <id>` | `toolCallId` |
111
+ | `--task-id <id>` | `taskId` |
112
+ | `--message-id <id>` | `messageId` |
113
+ | `--trace-id <id>` | `traceId`;未指定时自动生成 |
114
+ | `--timeout <ms>` | 超时(毫秒),默认 `30000` |
115
+ | `--json` | 以 JSON 展示返回 `data` |
116
+
117
+ ### 下层命令:`raw`
118
+
119
+ `raw` 子命令与网关路径一一对应,适合精确控制请求参数。
120
+
121
+ #### `raw ability-list`
122
+
123
+ GET 能力列表。
124
+
125
+ | 选项 | 说明 |
126
+ |------|------|
127
+ | `--device-id <id>` | 设备 ID |
128
+ | `--app-name <name>` | 应用名称 |
129
+ | `--type <type>` | 能力二级分类 |
130
+ | `--capability-type <ct>` | 能力三级分类 |
131
+ | `--nl <text>` | 自然语言筛选 |
132
+ | `--json` | JSON 输出 |
133
+
134
+ #### `raw ability-record`
135
+
136
+ POST 能力上报。
137
+
138
+ | 选项 | 说明 |
139
+ |------|------|
140
+ | `--file <path>` | **必填**,请求体 JSON 文件路径 |
141
+ | `--json` | JSON 输出 |
142
+
143
+ #### `raw app-info`
144
+
145
+ GET 应用信息。
146
+
147
+ | 选项 | 说明 |
148
+ |------|------|
149
+ | `--package-name <pkg>` | **必填**,应用包名 |
150
+ | `--json` | JSON 输出 |
151
+
152
+ #### `raw ability-info`
153
+
154
+ GET 能力详情。
155
+
156
+ | 选项 | 说明 |
157
+ |------|------|
158
+ | `--ability-id <id>` | **必填**,能力 ID |
159
+ | `--json` | JSON 输出 |
160
+
161
+ #### `raw call-tool`
162
+
163
+ POST 执行能力。
164
+
165
+ | 选项 | 说明 |
166
+ |------|------|
167
+ | `--file <path>` | **必填**,请求体 JSON 文件路径 |
168
+ | `--json` | JSON 输出 |
169
+
170
+ #### `raw category-list`
171
+
172
+ GET 全量分类。
173
+
174
+ | 选项 | 说明 |
175
+ |------|------|
176
+ | `--json` | JSON 输出 |
177
+
178
+ ### 工具命令
179
+
180
+ #### `config set`
181
+
182
+ 写入 `~/.ability-cli/config.json`。
183
+
184
+ | 选项 | 说明 |
185
+ |------|------|
186
+ | `--env <env>` | 目标环境;影响写入哪一段 `profiles`,并更新当前 `env` |
187
+ | `--app-id <id>` | `zy-app-id` |
188
+ | `--app-secret <secret>` | `appSecret` |
189
+ | `--auth-token <token>` | Authorization token |
190
+ | `--base-url <url>` | 网关地址 |
191
+ | `--device-id <id>` | 默认设备 ID(`defaults.deviceId`) |
192
+ | `--device-model <model>` | 默认设备型号 |
193
+ | `--os-version <ver>` | 默认系统版本 |
194
+
195
+ #### `config show`
196
+
197
+ 打印当前完整配置(JSON)。
198
+
199
+ #### `sign debug`
200
+
201
+ 打印签名用原串与 `zy-sign`,便于对齐服务端验签逻辑。
202
+
203
+ | 选项 | 说明 |
204
+ |------|------|
205
+ | `--app-id <id>` | 覆盖 `zy-app-id`(默认取自当前上下文) |
206
+ | `--app-secret <secret>` | 覆盖密钥 |
207
+ | `--nonce <nonce>` | 固定 nonce |
208
+ | `--timestamp <ts>` | 固定时间戳 |
209
+
210
+ #### `doctor`
211
+
212
+ 检查配置文件路径、当前环境、`zy-app-id` / `appSecret` / `baseUrl` 是否已配置,并请求 `{baseUrl}/time` 做网关连通性探测。
213
+
214
+ ---
215
+
216
+ ## 5. 配置
217
+
218
+ ### 配置文件
219
+
220
+ 默认路径:**`~/.ability-cli/config.json`**。
221
+
222
+ 首次运行若文件不存在,会使用内置默认:当前环境 `test`,`test` / `prod` 各有一套 `baseUrl`(测试/生产网关),`zy-appId`、`appSecret` 等为空;`defaults` 中可存默认设备信息等。
223
+
224
+ 通过 `ability-cli config set` 写入后,会按 `env` 更新对应 `profiles[env]` 下的 `baseUrl`、`zyAppId`、`appSecret`、`authToken` 等。
225
+
226
+ ### 环境变量
227
+
228
+ | 变量 | 作用 |
229
+ |------|------|
230
+ | `ABILITY_CLI_ENV` | 选用 `profiles` 的环境键(如 `test` / `prod`) |
231
+ | `ABILITY_CLI_APP_ID` | 覆盖 `zy-app-id` |
232
+ | `ABILITY_CLI_APP_SECRET` | 覆盖 `appSecret` |
233
+ | `ABILITY_CLI_AUTH_TOKEN` | 覆盖 Authorization |
234
+ | `ABILITY_CLI_BASE_URL` | 覆盖网关根地址 |
235
+
236
+ ### 优先级
237
+
238
+ 对 **appId / appSecret / authToken / baseUrl** 等请求上下文:**命令行全局选项 > 对应环境变量 > 配置文件当前 profile > 内置默认值**。
239
+
240
+ 当前激活的 **profile(test 或 prod)** 由 **`ABILITY_CLI_ENV`(若设置)** 与配置文件中的 **`env`** 字段共同决定(环境变量优先)。
241
+
242
+ ---
243
+
244
+ ## 6. 签名机制
245
+
246
+ 请求头中的 `zy-sign` 使用 **HMAC-SHA256** 计算:
247
+
248
+ 1. 拼接**原串**(字符串直接相连,无分隔符):
249
+ `zy-app-id` + `zy-nonce` + `zy-timestamp` + `appSecret`
250
+ 2. 以 **`appSecret` 作为 HMAC 密钥**,对原串做 UTF-8 编码后的 HMAC-SHA256,结果取 **hex** 字符串,作为 `zy-sign`。
251
+
252
+ 同时会带上 `zy-app-id`、`zy-nonce`、`zy-timestamp`。
253
+
254
+ 在**测试环境**下,若使用全局选项 **`--magic-sign`**,则跳过上述计算,`zy-sign` 使用固定测试值(便于跳过网关验签联调)。
255
+
256
+ ---
257
+
258
+ ## 7. 输出格式
259
+
260
+ - **默认**:表格(如能力列表)或带颜色的分段文本(如能力详情、成功/错误提示)。
261
+ - **`--json`**:标准响应输出 `data`;非标准响应则输出原始响应 JSON,便于排障。
262
+ - **`-v` / `--verbose`**:将请求与响应细节输出到 stderr,不影响主结果结构。
263
+
264
+ ---
265
+
266
+ ## 8. 退出码
267
+
268
+ | 退出码 | 含义 |
269
+ |--------|------|
270
+ | 0 | 成功 |
271
+ | 1 | 参数错误或未捕获的运行时错误 |
272
+ | 2 | 网络错误(如连接被拒绝、DNS 失败等) |
273
+ | 3 | 签名/认证失败 |
274
+ | 4 | 业务错误(接口返回 `code !== 200`) |
275
+
276
+ ---
277
+
278
+ ## 9. 开发
279
+
280
+ ```bash
281
+ pnpm install
282
+ pnpm dev # watch 模式构建
283
+ pnpm build # 生产构建
284
+ pnpm test # 运行测试(Vitest)
285
+ ```
286
+
287
+ ---
288
+
289
+ ## 10. 发布
290
+
291
+ ### 首次发布
292
+
293
+ 1. 在 `package.json` 中把占位符替换为真实地址:
294
+ - `repository.url`
295
+ - `homepage`
296
+ - `bugs.url`
297
+ 2. 登录 npm:
298
+
299
+ ```bash
300
+ npm login
301
+ ```
302
+
303
+ 3. 本地预检:
304
+
305
+ ```bash
306
+ pnpm build
307
+ pnpm test
308
+ npm pack
309
+ ```
310
+
311
+ 4. 发布(公有包):
312
+
313
+ ```bash
314
+ npm publish --access public
315
+ ```
316
+
317
+ ### 版本升级发布
318
+
319
+ ```bash
320
+ npm version patch # 或 minor / major
321
+ git push --follow-tags
322
+ npm publish --access public
323
+ ```
@@ -0,0 +1 @@
1
+ #!/usr/bin/env node
package/dist/index.js ADDED
@@ -0,0 +1,470 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { Command } from "commander";
5
+
6
+ // src/config.ts
7
+ import fs from "fs";
8
+ import path from "path";
9
+ import os from "os";
10
+ var DEFAULT_CONFIG = {
11
+ env: "prod",
12
+ profiles: {
13
+ test: {
14
+ baseUrl: "https://biz-gw.zyqltest.com",
15
+ zyAppId: "",
16
+ appSecret: "",
17
+ authToken: ""
18
+ },
19
+ prod: {
20
+ baseUrl: "https://biz-gw.zyql.com",
21
+ zyAppId: "",
22
+ appSecret: "",
23
+ authToken: ""
24
+ }
25
+ },
26
+ defaults: {
27
+ deviceId: "",
28
+ deviceModel: "",
29
+ osVersion: "",
30
+ timeout: 30
31
+ }
32
+ };
33
+ function getDefaultConfigPath() {
34
+ return path.join(os.homedir(), ".ability-cli", "config.json");
35
+ }
36
+ function loadConfig(configPath) {
37
+ const p = configPath ?? getDefaultConfigPath();
38
+ if (!fs.existsSync(p)) return structuredClone(DEFAULT_CONFIG);
39
+ const raw = fs.readFileSync(p, "utf-8");
40
+ return { ...structuredClone(DEFAULT_CONFIG), ...JSON.parse(raw) };
41
+ }
42
+ function saveConfig(configPath, config) {
43
+ const dir = path.dirname(configPath);
44
+ fs.mkdirSync(dir, { recursive: true });
45
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2), "utf-8");
46
+ }
47
+ function getActiveProfile(config) {
48
+ const env = process.env.ABILITY_CLI_ENV ?? config.env;
49
+ return config.profiles[env] ?? config.profiles.prod;
50
+ }
51
+ function resolveOption(flag, envVar, configVal, defaultVal) {
52
+ return flag ?? envVar ?? configVal ?? defaultVal;
53
+ }
54
+
55
+ // src/output.ts
56
+ import chalk from "chalk";
57
+ import Table from "cli-table3";
58
+ function printJson(data) {
59
+ console.log(JSON.stringify(data, null, 2));
60
+ }
61
+ function printSuccess(msg) {
62
+ console.log(chalk.green("\u2714 ") + msg);
63
+ }
64
+ function printError(msg) {
65
+ console.error(chalk.red("\u2716 ") + msg);
66
+ }
67
+ function printAbilityTable(abilities) {
68
+ const table = new Table({
69
+ head: [
70
+ chalk.cyan("ID"),
71
+ chalk.cyan("\u80FD\u529B\u540D\u79F0"),
72
+ chalk.cyan("\u5E94\u7528"),
73
+ chalk.cyan("\u63CF\u8FF0"),
74
+ chalk.cyan("\u8C03\u7528\u65B9\u5F0F"),
75
+ chalk.cyan("\u6743\u9650")
76
+ ],
77
+ colWidths: [8, 20, 12, 30, 8, 6],
78
+ wordWrap: true
79
+ });
80
+ for (const a of abilities) {
81
+ table.push([
82
+ a.abilityId ?? "",
83
+ a.name ?? "",
84
+ a.appName ?? "",
85
+ a.description ?? "",
86
+ a.callingMethod ?? "",
87
+ a.permission ?? ""
88
+ ]);
89
+ }
90
+ console.log(table.toString());
91
+ }
92
+ function printAbilityDetail(a) {
93
+ console.log(chalk.bold.cyan(`
94
+ \u2500\u2500 \u80FD\u529B\u8BE6\u60C5 \u2500\u2500
95
+ `));
96
+ console.log(` ${chalk.gray("ID:")} ${a.abilityId}`);
97
+ console.log(` ${chalk.gray("\u540D\u79F0:")} ${a.name}`);
98
+ console.log(` ${chalk.gray("\u5E94\u7528:")} ${a.appName} (${a.packageName})`);
99
+ console.log(` ${chalk.gray("\u63CF\u8FF0:")} ${a.description}`);
100
+ console.log(` ${chalk.gray("\u8C03\u7528\u65B9\u5F0F:")} ${a.callingMethod}`);
101
+ console.log(` ${chalk.gray("\u8D85\u65F6:")} ${a.timeout}s`);
102
+ console.log(` ${chalk.gray("\u6743\u9650:")} ${a.permission}`);
103
+ console.log(` ${chalk.gray("\u8BBE\u5907\u7C7B\u578B:")} ${a.deviceType}`);
104
+ if (a.inputSchema) {
105
+ console.log(chalk.bold.cyan(`
106
+ \u2500\u2500 inputSchema \u2500\u2500
107
+ `));
108
+ console.log(JSON.stringify(a.inputSchema, null, 2));
109
+ }
110
+ }
111
+ function handleApiResponse(res, jsonMode, formatter) {
112
+ const hasStandardEnvelope = res && typeof res === "object" && Object.prototype.hasOwnProperty.call(res, "code");
113
+ if (hasStandardEnvelope && res.code !== 200) {
114
+ const msg = res.msg ?? res.message ?? JSON.stringify(res);
115
+ printError(`\u63A5\u53E3\u8FD4\u56DE\u9519\u8BEF: [${res.code}] ${msg}`);
116
+ process.exit(4);
117
+ }
118
+ const payload = hasStandardEnvelope ? res.data : res;
119
+ if (jsonMode) {
120
+ printJson(payload);
121
+ } else {
122
+ formatter(payload);
123
+ }
124
+ }
125
+
126
+ // src/commands/config.ts
127
+ function registerConfigCommand(program2) {
128
+ const cfg = program2.command("config").description("\u7BA1\u7406 CLI \u914D\u7F6E");
129
+ cfg.command("set").description("\u8BBE\u7F6E\u914D\u7F6E\u9879").option("--env <env>", "\u73AF\u5883 (test/prod)").option("--app-id <id>", "zy-app-id").option("--app-secret <secret>", "appSecret").option("--auth-token <token>", "Authorization token").option("--base-url <url>", "\u7F51\u5173\u5730\u5740").option("--device-id <id>", "\u9ED8\u8BA4\u8BBE\u5907ID").option("--device-model <model>", "\u9ED8\u8BA4\u8BBE\u5907\u578B\u53F7").option("--os-version <ver>", "\u9ED8\u8BA4\u7CFB\u7EDF\u7248\u672C").action((opts) => {
130
+ const configPath = getDefaultConfigPath();
131
+ const config = loadConfig(configPath);
132
+ const env = opts.env ?? config.env;
133
+ config.env = env;
134
+ if (!config.profiles[env]) {
135
+ config.profiles[env] = { baseUrl: "", zyAppId: "", appSecret: "", authToken: "" };
136
+ }
137
+ const p = config.profiles[env];
138
+ if (opts.appId) p.zyAppId = opts.appId;
139
+ if (opts.appSecret) p.appSecret = opts.appSecret;
140
+ if (opts.authToken) p.authToken = opts.authToken;
141
+ if (opts.baseUrl) p.baseUrl = opts.baseUrl;
142
+ if (opts.deviceId) config.defaults.deviceId = opts.deviceId;
143
+ if (opts.deviceModel) config.defaults.deviceModel = opts.deviceModel;
144
+ if (opts.osVersion) config.defaults.osVersion = opts.osVersion;
145
+ saveConfig(configPath, config);
146
+ printSuccess(`\u914D\u7F6E\u5DF2\u4FDD\u5B58\u5230 ${configPath}`);
147
+ });
148
+ cfg.command("show").description("\u67E5\u770B\u5F53\u524D\u914D\u7F6E").action(() => {
149
+ const config = loadConfig();
150
+ printJson(config);
151
+ });
152
+ }
153
+
154
+ // src/context.ts
155
+ function buildContext(opts) {
156
+ const config = loadConfig();
157
+ if (opts.env) config.env = opts.env;
158
+ const profile = getActiveProfile(config);
159
+ profile.zyAppId = resolveOption(opts.appId, process.env.ABILITY_CLI_APP_ID, profile.zyAppId, "");
160
+ profile.appSecret = resolveOption(opts.appSecret, process.env.ABILITY_CLI_APP_SECRET, profile.appSecret, "");
161
+ profile.authToken = resolveOption(opts.authToken, process.env.ABILITY_CLI_AUTH_TOKEN, profile.authToken, "");
162
+ profile.baseUrl = resolveOption(opts.baseUrl, process.env.ABILITY_CLI_BASE_URL, profile.baseUrl, "https://biz-gw.zyql.com");
163
+ return {
164
+ profile,
165
+ signOpts: { magicSign: opts.magicSign },
166
+ verbose: opts.verbose
167
+ };
168
+ }
169
+ function getDefaults() {
170
+ return loadConfig().defaults;
171
+ }
172
+
173
+ // src/signer.ts
174
+ import { createHmac, randomBytes } from "crypto";
175
+ function sign(message, secret) {
176
+ return createHmac("sha256", secret).update(message, "utf8").digest("hex");
177
+ }
178
+ function buildSignHeaders(appId, appSecret, opts = {}) {
179
+ const nonce = opts.nonce ?? randomBytes(8).toString("hex");
180
+ const timestamp = opts.timestamp ?? Date.now().toString();
181
+ const zySign = opts.magicSign ? "123456" : sign(`${appId}${nonce}${timestamp}${appSecret}`, appSecret);
182
+ return {
183
+ "zy-app-id": appId,
184
+ "zy-nonce": nonce,
185
+ "zy-timestamp": timestamp,
186
+ "zy-sign": zySign
187
+ };
188
+ }
189
+
190
+ // src/http-client.ts
191
+ function buildUrl(base, path2, params) {
192
+ const url = new URL(path2, base);
193
+ if (params) {
194
+ for (const [k, v] of Object.entries(params)) {
195
+ if (v !== void 0 && v !== "") url.searchParams.set(k, v);
196
+ }
197
+ }
198
+ return url.toString();
199
+ }
200
+ function buildRequestHeaders(signHeaders, authToken) {
201
+ const headers = { ...signHeaders };
202
+ if (authToken) headers["Authorization"] = authToken;
203
+ return headers;
204
+ }
205
+ async function apiGet(ctx, path2, params) {
206
+ const signHeaders = buildSignHeaders(ctx.profile.zyAppId, ctx.profile.appSecret, ctx.signOpts);
207
+ const headers = buildRequestHeaders(signHeaders, ctx.profile.authToken);
208
+ const url = buildUrl(ctx.profile.baseUrl, path2, params);
209
+ if (ctx.verbose) {
210
+ console.error(`[verbose] GET ${url}`);
211
+ console.error(`[verbose] Headers: ${JSON.stringify(headers, null, 2)}`);
212
+ }
213
+ const res = await fetch(url, { headers });
214
+ const body = await res.json();
215
+ if (ctx.verbose) console.error(`[verbose] Response: ${JSON.stringify(body, null, 2)}`);
216
+ return body;
217
+ }
218
+ async function apiPost(ctx, path2, data) {
219
+ const signHeaders = buildSignHeaders(ctx.profile.zyAppId, ctx.profile.appSecret, ctx.signOpts);
220
+ const headers = buildRequestHeaders(signHeaders, ctx.profile.authToken);
221
+ headers["Content-Type"] = "application/json";
222
+ const url = buildUrl(ctx.profile.baseUrl, path2);
223
+ if (ctx.verbose) {
224
+ console.error(`[verbose] POST ${url}`);
225
+ console.error(`[verbose] Headers: ${JSON.stringify(headers, null, 2)}`);
226
+ console.error(`[verbose] Body: ${JSON.stringify(data, null, 2)}`);
227
+ }
228
+ const res = await fetch(url, {
229
+ method: "POST",
230
+ headers,
231
+ body: JSON.stringify(data)
232
+ });
233
+ const body = await res.json();
234
+ if (ctx.verbose) console.error(`[verbose] Response: ${JSON.stringify(body, null, 2)}`);
235
+ return body;
236
+ }
237
+
238
+ // src/commands/raw.ts
239
+ import fs2 from "fs";
240
+ function registerRawCommand(program2) {
241
+ const raw = program2.command("raw").description("\u539F\u59CB\u63A5\u53E3\u76F4\u901A\uFF08\u8054\u8C03/\u6392\u969C\uFF09");
242
+ raw.command("ability-list").description("GET \u80FD\u529B\u5217\u8868").option("--device-id <id>", "\u8BBE\u5907ID").option("--app-name <name>", "\u5E94\u7528\u540D\u79F0").option("--category <category>", "\u4E09\u7EA7\u5206\u7C7B").option("--natural-lang <text>", "\u81EA\u7136\u8BED\u8A00\u7B5B\u9009").option("--json", "JSON \u8F93\u51FA").action(async (opts) => {
243
+ const ctx = buildContext(program2.opts());
244
+ const params = {};
245
+ if (opts.deviceId) params.deviceId = opts.deviceId;
246
+ if (opts.appName) params.appName = opts.appName;
247
+ if (opts.category) params.category = opts.category;
248
+ if (opts.naturalLang) params.naturalLang = opts.naturalLang;
249
+ const res = await apiGet(ctx, "/ability-service/api/v1/ability/list", params);
250
+ handleApiResponse(res, opts.json, (data) => printAbilityTable(data));
251
+ });
252
+ raw.command("ability-record").description("POST \u80FD\u529B\u4E0A\u62A5").requiredOption("--file <path>", "JSON \u6587\u4EF6\u8DEF\u5F84").option("--json", "JSON \u8F93\u51FA").action(async (opts) => {
253
+ const ctx = buildContext(program2.opts());
254
+ const data = JSON.parse(fs2.readFileSync(opts.file, "utf-8"));
255
+ const res = await apiPost(ctx, "/ability-service/api/v1/ability/record", data);
256
+ handleApiResponse(res, opts.json, () => printSuccess("\u80FD\u529B\u4E0A\u62A5\u6210\u529F"));
257
+ });
258
+ raw.command("app-info").description("GET \u5E94\u7528\u4FE1\u606F").requiredOption("--package-name <pkg>", "\u5E94\u7528\u5305\u540D").option("--json", "JSON \u8F93\u51FA").action(async (opts) => {
259
+ const ctx = buildContext(program2.opts());
260
+ const res = await apiGet(ctx, "/ability-service/api/v1/ability/appInfo", { packageName: opts.packageName });
261
+ handleApiResponse(res, opts.json, (data) => printJson(data));
262
+ });
263
+ raw.command("ability-info").description("GET \u80FD\u529B\u8BE6\u60C5").requiredOption("--ability-id <id>", "\u80FD\u529BID").option("--json", "JSON \u8F93\u51FA").action(async (opts) => {
264
+ const ctx = buildContext(program2.opts());
265
+ const res = await apiGet(ctx, "/ability-service/api/v1/ability/abilityInfo", { abilityId: opts.abilityId });
266
+ handleApiResponse(res, opts.json, (data) => printJson(data));
267
+ });
268
+ raw.command("call-tool").description("POST \u6267\u884C\u80FD\u529B").requiredOption("--file <path>", "JSON \u6587\u4EF6\u8DEF\u5F84").option("--json", "JSON \u8F93\u51FA").action(async (opts) => {
269
+ const ctx = buildContext(program2.opts());
270
+ const data = JSON.parse(fs2.readFileSync(opts.file, "utf-8"));
271
+ const res = await apiPost(ctx, "/ability-service/api/mcp/async/call-tool", data);
272
+ handleApiResponse(res, opts.json, (data2) => printJson(data2));
273
+ });
274
+ raw.command("category-list").description("GET \u5168\u91CF\u5206\u7C7B").option("--json", "JSON \u8F93\u51FA").action(async (opts) => {
275
+ const ctx = buildContext(program2.opts());
276
+ const res = await apiGet(ctx, "/ability-service/api/v1/ability/category");
277
+ handleApiResponse(res, opts.json, (data) => printJson(data));
278
+ });
279
+ }
280
+
281
+ // src/commands/search.ts
282
+ function registerSearchCommand(program2) {
283
+ program2.command("search [query]").description("\u641C\u7D22\u80FD\u529B\uFF08\u652F\u6301\u81EA\u7136\u8BED\u8A00\uFF09").option("--device-id <id>", "\u8BBE\u5907ID").option("--app-name <name>", "\u5E94\u7528\u540D\u79F0").option("--category <category>", "\u4E09\u7EA7\u5206\u7C7B").option("--natural-lang <text>", "\u81EA\u7136\u8BED\u8A00\uFF08\u4F18\u5148\u4E8E query\uFF09").option("--json", "JSON \u8F93\u51FA").action(async (query, opts) => {
284
+ const ctx = buildContext(program2.opts());
285
+ const defaults = getDefaults();
286
+ const params = {};
287
+ const deviceId = opts.deviceId ?? defaults.deviceId;
288
+ if (deviceId) params.deviceId = deviceId;
289
+ const naturalLang = opts.naturalLang ?? query;
290
+ if (naturalLang) params.naturalLang = naturalLang;
291
+ if (opts.appName) params.appName = opts.appName;
292
+ if (opts.category) params.category = opts.category;
293
+ const res = await apiGet(ctx, "/ability-service/api/v1/ability/list", params);
294
+ handleApiResponse(res, opts.json, (data) => {
295
+ const list = Array.isArray(data) ? data : [];
296
+ if (list.length === 0) {
297
+ console.log("\u672A\u627E\u5230\u5339\u914D\u7684\u80FD\u529B");
298
+ return;
299
+ }
300
+ printAbilityTable(list);
301
+ });
302
+ });
303
+ }
304
+
305
+ // src/commands/inspect.ts
306
+ function generateTemplate(schema) {
307
+ if (!schema || !schema.properties) return {};
308
+ const result = {};
309
+ for (const [key, prop] of Object.entries(schema.properties)) {
310
+ switch (prop.type) {
311
+ case "string":
312
+ result[key] = prop.default ?? "";
313
+ break;
314
+ case "number":
315
+ result[key] = prop.default ?? 0;
316
+ break;
317
+ case "bool":
318
+ result[key] = prop.default ?? false;
319
+ break;
320
+ case "array":
321
+ result[key] = prop.items ? [generateTemplate(prop.items)] : [];
322
+ break;
323
+ case "object":
324
+ result[key] = generateTemplate(prop);
325
+ break;
326
+ default:
327
+ result[key] = null;
328
+ }
329
+ }
330
+ return result;
331
+ }
332
+ function registerInspectCommand(program2) {
333
+ program2.command("inspect").description("\u67E5\u770B\u80FD\u529B\u8BE6\u60C5\u4E0E inputSchema").requiredOption("--ability-id <id>", "\u80FD\u529BID").option("--gen-template", "\u751F\u6210\u53C2\u6570\u6A21\u677F JSON").option("--json", "JSON \u8F93\u51FA").action(async (opts) => {
334
+ const ctx = buildContext(program2.opts());
335
+ const res = await apiGet(ctx, "/ability-service/api/v1/ability/abilityInfo", { abilityId: opts.abilityId });
336
+ handleApiResponse(res, opts.json, (data) => {
337
+ if (opts.genTemplate && data.inputSchema) {
338
+ printJson(generateTemplate(data.inputSchema));
339
+ } else {
340
+ printAbilityDetail(data);
341
+ }
342
+ });
343
+ });
344
+ }
345
+
346
+ // src/commands/exec.ts
347
+ import { randomUUID } from "crypto";
348
+ import fs3 from "fs";
349
+ function registerExecCommand(program2) {
350
+ program2.command("exec").description("\u6267\u884C\u80FD\u529B").requiredOption("--ability-id <id>", "\u80FD\u529BID").option("--params <json>", "\u53C2\u6570 JSON \u5B57\u7B26\u4E32\u6216\u6587\u4EF6\u8DEF\u5F84").option("--device-id <id>", "\u8BBE\u5907ID").option("--session-id <id>", "sessionId").option("--tool-call-id <id>", "toolCallId").option("--task-id <id>", "taskId").option("--message-id <id>", "messageId").option("--trace-id <id>", "traceId").option("--timeout <ms>", "\u8D85\u65F6\u65F6\u95F4(\u6BEB\u79D2)", "30000").option("--json", "JSON \u8F93\u51FA").action(async (opts) => {
351
+ const ctx = buildContext(program2.opts());
352
+ const defaults = getDefaults();
353
+ let params = {};
354
+ if (opts.params) {
355
+ if (fs3.existsSync(opts.params)) {
356
+ params = JSON.parse(fs3.readFileSync(opts.params, "utf-8"));
357
+ } else {
358
+ params = JSON.parse(opts.params);
359
+ }
360
+ }
361
+ const body = {
362
+ abilityId: Number(opts.abilityId),
363
+ traceId: opts.traceId ?? randomUUID().replace(/-/g, ""),
364
+ sessionId: opts.sessionId ?? "",
365
+ toolCallId: opts.toolCallId ?? "",
366
+ taskId: opts.taskId ?? "",
367
+ messageId: opts.messageId ?? "",
368
+ deviceId: opts.deviceId ?? defaults.deviceId ?? "",
369
+ timeoutMillis: Number(opts.timeout),
370
+ params
371
+ };
372
+ const res = await apiPost(ctx, "/ability-service/api/mcp/async/call-tool", body);
373
+ handleApiResponse(res, opts.json, (data) => {
374
+ printSuccess("\u80FD\u529B\u6267\u884C\u5B8C\u6210");
375
+ printJson(data);
376
+ });
377
+ });
378
+ }
379
+
380
+ // src/commands/sign.ts
381
+ import chalk2 from "chalk";
382
+ function registerSignCommand(program2) {
383
+ const signCmd = program2.command("sign").description("\u7B7E\u540D\u5DE5\u5177");
384
+ signCmd.command("debug").description("\u8C03\u8BD5\u7B7E\u540D\uFF08\u6253\u5370\u539F\u4E32\u548C\u7ED3\u679C\uFF09").option("--app-id <id>", "zy-app-id").option("--app-secret <secret>", "appSecret").option("--nonce <nonce>", "\u6307\u5B9A nonce").option("--timestamp <ts>", "\u6307\u5B9A timestamp").action((opts) => {
385
+ const ctx = buildContext(program2.opts());
386
+ const appId = opts.appId ?? ctx.profile.zyAppId;
387
+ const secret = opts.appSecret ?? ctx.profile.appSecret;
388
+ const headers = buildSignHeaders(appId, secret, {
389
+ nonce: opts.nonce,
390
+ timestamp: opts.timestamp
391
+ });
392
+ const rawStr = `${appId}${headers["zy-nonce"]}${headers["zy-timestamp"]}${secret}`;
393
+ console.log(chalk2.bold.cyan("\n\u2500\u2500 \u7B7E\u540D\u8C03\u8BD5 \u2500\u2500\n"));
394
+ console.log(` ${chalk2.gray("appId:")} ${appId}`);
395
+ console.log(` ${chalk2.gray("nonce:")} ${headers["zy-nonce"]}`);
396
+ console.log(` ${chalk2.gray("timestamp:")} ${headers["zy-timestamp"]}`);
397
+ console.log(` ${chalk2.gray("\u539F\u4E32:")} ${rawStr}`);
398
+ console.log(` ${chalk2.gray("zy-sign:")} ${headers["zy-sign"]}`);
399
+ });
400
+ }
401
+
402
+ // src/commands/doctor.ts
403
+ import chalk3 from "chalk";
404
+ function registerDoctorCommand(program2) {
405
+ program2.command("doctor").description("\u68C0\u67E5\u914D\u7F6E\u5B8C\u6574\u6027\u548C\u7F51\u5173\u8FDE\u901A\u6027").action(async () => {
406
+ const config = loadConfig();
407
+ const profile = getActiveProfile(config);
408
+ let ok = true;
409
+ console.log(chalk3.bold.cyan("\n\u2500\u2500 CLI \u5065\u5EB7\u68C0\u67E5 \u2500\u2500\n"));
410
+ const checks = [
411
+ ["\u914D\u7F6E\u6587\u4EF6", getDefaultConfigPath(), true],
412
+ ["\u5F53\u524D\u73AF\u5883", config.env, true],
413
+ ["zy-app-id", profile.zyAppId || "(\u7A7A)", !!profile.zyAppId],
414
+ ["appSecret", profile.appSecret ? "******" : "(\u7A7A)", !!profile.appSecret],
415
+ ["baseUrl", profile.baseUrl || "(\u7A7A)", !!profile.baseUrl]
416
+ ];
417
+ for (const [label, val, pass] of checks) {
418
+ const icon = pass ? chalk3.green("\u2714") : chalk3.red("\u2716");
419
+ console.log(` ${icon} ${chalk3.gray(label + ":")} ${val}`);
420
+ if (!pass) ok = false;
421
+ }
422
+ try {
423
+ const res = await fetch(`${profile.baseUrl}/time`);
424
+ const body = await res.json();
425
+ if (body.code === 200) {
426
+ console.log(` ${chalk3.green("\u2714")} ${chalk3.gray("\u7F51\u5173\u8FDE\u901A:")} ${body.data}`);
427
+ const serverTs = Number(String(body.data).slice(0, -1) + "0");
428
+ const drift = Math.abs(Date.now() - serverTs);
429
+ if (drift > 3e4) {
430
+ console.log(` ${chalk3.yellow("\u26A0")} ${chalk3.gray("\u65F6\u949F\u504F\u5DEE:")} ${drift}ms\uFF08\u5EFA\u8BAE\u4F7F\u7528 --sync-time\uFF09`);
431
+ }
432
+ } else {
433
+ console.log(` ${chalk3.red("\u2716")} ${chalk3.gray("\u7F51\u5173\u8FDE\u901A:")} \u8FD4\u56DE code=${body.code}`);
434
+ ok = false;
435
+ }
436
+ } catch (e) {
437
+ console.log(` ${chalk3.red("\u2716")} ${chalk3.gray("\u7F51\u5173\u8FDE\u901A:")} ${e.message}`);
438
+ ok = false;
439
+ }
440
+ console.log();
441
+ if (ok) {
442
+ console.log(chalk3.green(" \u4E00\u5207\u6B63\u5E38\uFF0C\u53EF\u4EE5\u5F00\u59CB\u4F7F\u7528\uFF01"));
443
+ } else {
444
+ console.log(chalk3.yellow(" \u5B58\u5728\u95EE\u9898\uFF0C\u8BF7\u68C0\u67E5\u4E0A\u65B9\u6807\u8BB0 \u2716 \u7684\u9879\u76EE"));
445
+ }
446
+ });
447
+ }
448
+
449
+ // src/index.ts
450
+ var program = new Command();
451
+ program.name("ability-cli").description("\u539F\u5B50\u80FD\u529B\u5E73\u53F0 CLI \u5DE5\u5177").version("0.1.0").option("--env <env>", "\u73AF\u5883 (test/prod)").option("--app-id <id>", "zy-app-id").option("--app-secret <secret>", "appSecret").option("--auth-token <token>", "Authorization").option("--base-url <url>", "\u7F51\u5173\u5730\u5740").option("--magic-sign", "\u6D4B\u8BD5\u73AF\u5883\u8DF3\u8FC7\u9A8C\u7B7E").option("-v, --verbose", "\u8BE6\u7EC6\u8F93\u51FA");
452
+ registerConfigCommand(program);
453
+ registerRawCommand(program);
454
+ registerSearchCommand(program);
455
+ registerInspectCommand(program);
456
+ registerExecCommand(program);
457
+ registerSignCommand(program);
458
+ registerDoctorCommand(program);
459
+ process.on("unhandledRejection", (err) => {
460
+ if (err?.cause?.code === "ECONNREFUSED" || err?.cause?.code === "ENOTFOUND") {
461
+ printError(`\u7F51\u7EDC\u9519\u8BEF: ${err.message}`);
462
+ process.exit(2);
463
+ }
464
+ printError(err?.message ?? "\u672A\u77E5\u9519\u8BEF");
465
+ process.exit(1);
466
+ });
467
+ program.parseAsync().catch((err) => {
468
+ printError(err.message);
469
+ process.exit(1);
470
+ });
package/package.json ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "ability-cli",
3
+ "version": "0.1.0",
4
+ "description": "原子能力平台 CLI 工具",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "<repo-url>"
8
+ },
9
+ "homepage": "<repo-url>",
10
+ "bugs": {
11
+ "url": "<repo-url>/issues"
12
+ },
13
+ "type": "module",
14
+ "bin": {
15
+ "ability-cli": "./dist/index.js"
16
+ },
17
+ "files": [
18
+ "dist",
19
+ "README.md",
20
+ "LICENSE"
21
+ ],
22
+ "scripts": {
23
+ "build": "tsup src/index.ts --format esm --dts --clean",
24
+ "dev": "tsup src/index.ts --format esm --watch",
25
+ "test": "vitest run",
26
+ "test:watch": "vitest",
27
+ "prepublishOnly": "pnpm build && pnpm test"
28
+ },
29
+ "keywords": [],
30
+ "author": "",
31
+ "license": "ISC",
32
+ "packageManager": "pnpm@10.28.2",
33
+ "dependencies": {
34
+ "chalk": "^5.6.2",
35
+ "cli-table3": "^0.6.5",
36
+ "commander": "^14.0.3"
37
+ },
38
+ "pnpm": {
39
+ "onlyBuiltDependencies": ["esbuild"]
40
+ },
41
+ "devDependencies": {
42
+ "@types/node": "^25.5.2",
43
+ "tsup": "^8.5.1",
44
+ "typescript": "^6.0.2",
45
+ "vitest": "^4.1.3"
46
+ }
47
+ }