@manturhub/cli 0.8.2 → 0.9.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/README.md CHANGED
@@ -15,6 +15,8 @@ manturhub login
15
15
  printf '%s' "$YOUR_MANTURHUB_KEY" | manturhub login --key-stdin
16
16
  ```
17
17
 
18
+ CLI 默认连接生产站 `https://hub.mantur.ai`,Key 也必须在该生产站创建。`hub.mantur.cn` 是独立测试环境,其 Key 不能用于生产站。
19
+
18
20
  需要 Node.js ≥ 18。也可免安装运行:`npx -y @manturhub/cli <命令>`。
19
21
 
20
22
  ## 快速开始
@@ -22,24 +24,28 @@ printf '%s' "$YOUR_MANTURHUB_KEY" | manturhub login --key-stdin
22
24
  ```bash
23
25
  # 发现与查看算子无需登录
24
26
  manturhub ls --cat text
25
- manturhub describe op.text.commerce-copy
27
+ manturhub describe 电商文案生成
26
28
 
27
29
  # 先查实时价格,再调用
28
- manturhub quote op.text.commerce-copy
29
- manturhub run op.text.commerce-copy --json '{"product_info":"便携榨汁杯,USB 充电,300ml","scene":"product_title","tone":"lively"}'
30
+ manturhub quote 电商文案生成
31
+ manturhub run 电商文案生成 --json '{"product_info":"便携榨汁杯,USB 充电,300ml","scene":"product_title","tone":"lively"}'
30
32
  ```
31
33
 
32
- CLI 会在付费调用前按算子实时 schema 校验未知字段、必填项、类型和枚举,校验失败不会发起 invoke
34
+ CLI 会在付费调用前按算子实时 schema 校验未知字段、必填项、类型和枚举,校验失败不会发起 invoke。校验通过后会显示本次预计消耗和计费依据,获得确认才调用;完成后显示实际消耗和退款。批量参数也按整批请求试算。
35
+
36
+ 面向用户的列表、详情、报价、确认和结算统一显示中文算子名,也可直接用中文名调用。英文算子 ID 仅保留在 `--json` 机器输出和内部 API 请求中,供 Agent 与脚本稳定使用。
37
+
38
+ 在 Agent 或脚本等非交互环境中,第一次运行只返回报价和 `quote_id`,不会调用或扣费;Agent 向用户确认后,使用提示中的 `--confirm <quote_id>` 执行。报价 5 分钟内有效且只能使用一次。
33
39
 
34
40
  ## 主要命令
35
41
 
36
42
  | 命令 | 说明 |
37
43
  |---|---|
38
44
  | `manturhub ls [--cat image\|video\|audio\|text\|data] [--json]` | 实时列出上线算子 |
39
- | `manturhub describe <算子ID> [--json]` | 查看精确入参与异步属性 |
40
- | `manturhub quote <算子ID> [--json]` | 查询 Java API 返回的实时计费公式 |
41
- | `manturhub run <算子ID> --json '{}'` | 调用算子;异步任务默认轮询到终态 |
42
- | `manturhub run <算子ID> --json-file params.json` | 从文件读取参数,适合长提示词和自动化 |
45
+ | `manturhub describe <中文算子名> [--json]` | 查看精确入参与异步属性 |
46
+ | `manturhub quote <中文算子名> [--json]` | 查询 Java API 返回的实时计费公式 |
47
+ | `manturhub run <中文算子名> --json '{}'` | 调用算子;异步任务默认轮询到终态 |
48
+ | `manturhub run <中文算子名> --json-file params.json` | 从文件读取参数,适合长提示词和自动化 |
43
49
  | `manturhub upload <本地文件>` | 流式上传图片、音频或视频并输出公网 URL |
44
50
  | `manturhub status <poll_url>` | 查询 `run --no-wait` 返回的异步任务 |
45
51
  | `manturhub balance [--json]` | 查询余额及美元等值(1 馒头 = $0.01 USD) |
package/bin/cli.js CHANGED
@@ -10,6 +10,7 @@ import { createReadStream, readFileSync, statSync } from "node:fs";
10
10
  import { fileURLToPath } from "node:url";
11
11
  import { dirname, join, basename, extname } from "node:path";
12
12
  import { parseDynamicParams, validateParams } from "../lib/params.js";
13
+ import { confirmCharge, formatMantou, printBillingResult } from "../lib/billing-confirm.js";
13
14
 
14
15
  // 本地文件 → MIME(presign 只接受 image/audio/video)
15
16
  const MIME_BY_EXT = {
@@ -67,8 +68,8 @@ function assertFlags(tokens, { value = [], boolean = [] } = {}) {
67
68
  function assertRunControlFlags(tokens) {
68
69
  for (let i = 0; i < tokens.length; i++) {
69
70
  const token = tokens[i];
70
- if (token === "--no-wait" || token.startsWith("--json=") || token.startsWith("--json-file=")) continue;
71
- if (token === "--json" || token === "--json-file") {
71
+ if (token === "--no-wait" || token.startsWith("--json=") || token.startsWith("--json-file=") || token.startsWith("--confirm=")) continue;
72
+ if (token === "--json" || token === "--json-file" || token === "--confirm") {
72
73
  const value = tokens[i + 1];
73
74
  if (value === undefined || value.startsWith("--")) throw new Error(`参数 ${token} 缺少值`);
74
75
  i++;
@@ -83,6 +84,37 @@ function usdFor(dumplings) {
83
84
  return Number.isFinite(value) ? `$${(value * 0.01).toFixed(2)} USD` : "-";
84
85
  }
85
86
 
87
+ const CAT_LABELS = {
88
+ text: "文本",
89
+ image: "图片",
90
+ video: "视频",
91
+ audio: "音频",
92
+ data: "数据",
93
+ };
94
+
95
+ function catLabel(cat) {
96
+ return CAT_LABELS[cat] || cat || "其他";
97
+ }
98
+
99
+ // Human commands accept either the stable operator ID or its Chinese display name.
100
+ // Machine-facing --json responses keep IDs unchanged for automation compatibility.
101
+ async function resolveOperator(ref) {
102
+ const direct = await apiFetch(`/api/v1/operators/${encodeURIComponent(ref)}`, { auth: "optional" });
103
+ if (direct.ok) return direct.json.operator || direct.json;
104
+ if (direct.status !== 404) throw new Error(`算子信息获取失败(HTTP ${direct.status})`);
105
+
106
+ const listed = await apiFetch("/api/v1/operators?status=online", { auth: "optional" });
107
+ if (!listed.ok) throw new Error(`算子列表获取失败(HTTP ${listed.status})`);
108
+ const operators = listed.json.operators || listed.json || [];
109
+ const matches = operators.filter((item) => item?.name === ref);
110
+ if (matches.length === 0) throw new Error(`未找到算子“${ref}”`);
111
+ if (matches.length > 1) throw new Error(`算子名称“${ref}”不唯一,请联系平台处理`);
112
+
113
+ const detail = await apiFetch(`/api/v1/operators/${encodeURIComponent(matches[0].id)}`, { auth: "optional" });
114
+ if (!detail.ok) throw new Error(`算子信息获取失败(HTTP ${detail.status})`);
115
+ return detail.json.operator || detail.json;
116
+ }
117
+
86
118
  const HELP = `manturhub — ManturHub 算子广场 CLI v${VERSION}
87
119
 
88
120
  用法:
@@ -90,10 +122,10 @@ const HELP = `manturhub — ManturHub 算子广场 CLI v${VERSION}
90
122
  manturhub login --key sk-xxx 手动配置 API Key(存 ~/.manturhub/config.json)
91
123
  manturhub login --key-stdin 从 stdin 安全读取 API Key
92
124
  manturhub ls [--cat <分类>] [--json] 列出上线算子(无需登录)
93
- manturhub describe <算子ID> [--json] 查看算子入参字段(无需登录)
94
- manturhub quote <算子ID> 查询实时计费公式(不要使用 Skill 内的历史价格)
95
- manturhub run <算子ID> --json '{}' 调用算子(异步算子自动轮询到出结果;--no-wait 只拿 job_id)
96
- manturhub run <算子ID> --json-file x.json 从文件读参数(prompt 来自配方/用户时更安全)
125
+ manturhub describe <中文算子名> [--json] 查看算子入参字段(无需登录)
126
+ manturhub quote <中文算子名> 查询实时计费公式(不要使用 Skill 内的历史价格)
127
+ manturhub run <中文算子名> --json '{}' 试算并确认费用后调用(异步算子自动轮询到出结果)
128
+ manturhub run <中文算子名> --json-file x.json 从文件读参数(prompt 来自配方/用户时更安全)
97
129
  manturhub upload <本地文件> 上传图片/音频/视频 → 公网 URL(喂算子前先转换本地文件)
98
130
  manturhub status <poll_url> 查异步任务状态(配合 run --no-wait)
99
131
  manturhub balance 查询馒头余额
@@ -161,7 +193,14 @@ async function main() {
161
193
  `✓ Key 已验证并保存。账号: ${r.json.email || "-"} 余额: ${usdFor(r.json.balance)}(${r.json.balance ?? "-"} 馒头)`
162
194
  );
163
195
  } else {
164
- console.error(`Key 验证失败(HTTP ${r.status}),未修改本地配置。请确认 key 是否正确、是否已激活。`);
196
+ const base = getBaseUrl();
197
+ const productionHint = new URL(base).hostname === "hub.mantur.ai"
198
+ ? " 对外用户请在 https://hub.mantur.ai 创建生产 Key;hub.mantur.cn 的测试 Key 不能用于生产。"
199
+ : "";
200
+ console.error(
201
+ `Key 验证失败(HTTP ${r.status}),未修改本地配置。当前连接:${base}。` +
202
+ `Key 必须由这个站点创建,请确认 Key 是否正确、是否已激活。${productionHint}`
203
+ );
165
204
  process.exit(1);
166
205
  }
167
206
  break;
@@ -198,9 +237,9 @@ async function main() {
198
237
  }
199
238
  console.log(`ManturHub 上线算子(${ops.length} 个):\n`);
200
239
  for (const o of ops) {
201
- console.log(` ${o.id.padEnd(26)} ${o.name} [${o.cat}]`);
240
+ console.log(` ${o.name} [${catLabel(o.cat)}]`);
202
241
  }
203
- console.log(`\n用 \`manturhub describe <算子ID>\` 查入参,\`manturhub run <算子ID> --json '{...}'\` 调用`);
242
+ console.log(`\n用 \`manturhub describe <中文算子名>\` 查入参,\`manturhub run <中文算子名> --json '{...}'\` 调用`);
204
243
  break;
205
244
  }
206
245
 
@@ -217,17 +256,18 @@ async function main() {
217
256
  console.error(error.message);
218
257
  process.exit(1);
219
258
  }
220
- const r = await apiFetch(`/api/v1/operators/${encodeURIComponent(op)}`, { auth: "optional" });
221
- if (!r.ok) {
222
- console.error(`获取失败(HTTP ${r.status}): ${op}`);
259
+ let o;
260
+ try {
261
+ o = await resolveOperator(op);
262
+ } catch (error) {
263
+ console.error(error.message);
223
264
  process.exit(1);
224
265
  }
225
- const o = r.json.operator || r.json;
226
266
  if (hasFlag("json")) {
227
267
  console.log(JSON.stringify(o, null, 2));
228
268
  break;
229
269
  }
230
- console.log(`\n${o.id} ${o.name || ""} [${o.cat || "-"}] · ${o.status || "-"}`);
270
+ console.log(`\n${o.name || "未命名算子"} [${catLabel(o.cat)}] · ${o.status || "-"}`);
231
271
  if (o.description) console.log(o.description);
232
272
  const ps = o.params_schema || (o.meta && o.meta.params_schema);
233
273
  if (ps && Array.isArray(ps.fields) && ps.fields.length) {
@@ -239,9 +279,9 @@ async function main() {
239
279
  }
240
280
  if (ps.async) console.log(`\n异步算子:run 默认自动轮询到出结果(--no-wait 只拿 task_id)`);
241
281
  } else {
242
- console.log(`\n(该算子未声明入参 schema,详见 ${getBaseUrl()}/marketplace/${o.id})`);
282
+ console.log(`\n(该算子未声明入参 schema,请在 ManturHub 算子广场查看详情)`);
243
283
  }
244
- console.log(`\n调用: manturhub run ${o.id} --json '{...}'`);
284
+ console.log(`\n调用: manturhub run '${o.name}' --json '{...}'`);
245
285
  break;
246
286
  }
247
287
 
@@ -257,7 +297,14 @@ async function main() {
257
297
  console.error(error.message);
258
298
  process.exit(1);
259
299
  }
260
- const r = await apiFetch(`/api/v1/operators/${encodeURIComponent(op)}/quote`, { auth: "optional" });
300
+ let operator;
301
+ try {
302
+ operator = await resolveOperator(op);
303
+ } catch (error) {
304
+ console.error(error.message);
305
+ process.exit(1);
306
+ }
307
+ const r = await apiFetch(`/api/v1/operators/${encodeURIComponent(operator.id)}/quote`, { auth: "optional" });
261
308
  if (!r.ok) {
262
309
  console.error(`查询价格失败(HTTP ${r.status}): ${JSON.stringify(r.json)}`);
263
310
  process.exit(1);
@@ -272,7 +319,7 @@ async function main() {
272
319
  )
273
320
  );
274
321
  } else {
275
- console.log(`${r.json.operatorId || op}: ${r.json.formula || "详见算子页"}`);
322
+ console.log(`${operator.name}: ${r.json.formula || "详见算子页"}`);
276
323
  if (r.json.floor !== undefined) console.log(`最低扣费: ${usdFor(r.json.floor)}(${r.json.floor} 馒头)`);
277
324
  }
278
325
  break;
@@ -325,12 +372,14 @@ async function main() {
325
372
  process.exit(1);
326
373
  }
327
374
  }
328
- const detail = await apiFetch(`/api/v1/operators/${encodeURIComponent(op)}`, { auth: "optional" });
329
- if (!detail.ok) {
330
- console.error(`参数校验前无法读取算子 schema(HTTP ${detail.status}),已停止调用避免误扣费`);
375
+ let operator;
376
+ try {
377
+ operator = await resolveOperator(op);
378
+ } catch (error) {
379
+ console.error(`${error.message},已停止调用避免误扣费`);
331
380
  process.exit(1);
332
381
  }
333
- const operator = detail.json.operator || detail.json;
382
+ const operatorId = operator.id;
334
383
  const schema = operator.params_schema || operator.meta?.params_schema;
335
384
  try {
336
385
  body = validateParams(body, schema, { coerceStrings: !jsonArg && !jsonFile });
@@ -338,9 +387,47 @@ async function main() {
338
387
  console.error(`参数校验失败: ${error.message}`);
339
388
  process.exit(1);
340
389
  }
390
+ let quoteId = getFlag("confirm");
391
+ if (!quoteId) {
392
+ const quote = await apiFetch(`/api/v1/operators/${encodeURIComponent(operatorId)}/quote`, {
393
+ method: "POST",
394
+ body,
395
+ });
396
+ if (!quote.ok) {
397
+ console.error(`本次费用试算失败(HTTP ${quote.status}): ${JSON.stringify(quote.json)}`);
398
+ process.exit(1);
399
+ }
400
+ const estimated = Number(quote.json?.estimated_dumplings);
401
+ quoteId = quote.json?.quote_id;
402
+ if (Number.isFinite(estimated) && estimated > 0) {
403
+ if (process.stdin.isTTY && process.stderr.isTTY) {
404
+ if (!(await confirmCharge({ ...quote.json, operator_name: operator.name }))) {
405
+ console.error("已取消,未调用算子、未扣费。");
406
+ process.exit(2);
407
+ }
408
+ } else {
409
+ console.error(JSON.stringify({
410
+ error: "CONFIRMATION_REQUIRED",
411
+ operator_name: operator.name,
412
+ message: `本次预计消耗 ${formatMantou(estimated)},请先取得用户确认`,
413
+ estimated_dumplings: estimated,
414
+ balance: quote.json?.balance,
415
+ formula: quote.json?.formula,
416
+ quote_id: quoteId,
417
+ retry_with: `--confirm ${quoteId}`,
418
+ }, null, 2));
419
+ process.exit(3);
420
+ }
421
+ }
422
+ }
341
423
  const r = await apiFetch(
342
- `/api/v1/operators/${encodeURIComponent(op)}/invoke`,
343
- { method: "POST", body, timeoutMs: 120000 }
424
+ `/api/v1/operators/${encodeURIComponent(operatorId)}/invoke`,
425
+ {
426
+ method: "POST",
427
+ body,
428
+ timeoutMs: 120000,
429
+ headers: quoteId ? { "X-Mantur-Quote-Id": quoteId } : {},
430
+ }
344
431
  );
345
432
  // 异步算子(返回 poll_url)默认自动轮询到出结果;--no-wait 只拿 job_id。
346
433
  const pollUrl = r.ok && r.json && r.json.poll_url;
@@ -355,10 +442,12 @@ async function main() {
355
442
  ),
356
443
  });
357
444
  console.log(JSON.stringify(final, null, 2));
445
+ printBillingResult(final, process.stderr, operator.name);
358
446
  const st = final && final.status;
359
447
  if (st === "failed" || st === "error" || (final && final._timeout)) process.exit(1);
360
448
  } else {
361
449
  console.log(JSON.stringify(r.json, null, 2));
450
+ printBillingResult(r.json, process.stderr, operator.name);
362
451
  if (!r.ok) process.exit(1);
363
452
  }
364
453
  break;
package/lib/api.js CHANGED
@@ -3,7 +3,7 @@ import { getKey, getBaseUrl } from "./config.js";
3
3
  // Thin REST client against the ManturHub gateway. Public discovery calls may omit auth.
4
4
  export async function apiFetch(
5
5
  path,
6
- { method = "GET", body, key, auth = "required", timeoutMs = 30000 } = {}
6
+ { method = "GET", body, key, auth = "required", timeoutMs = 30000, headers = {} } = {}
7
7
  ) {
8
8
  const apiKey = key === undefined ? getKey() : key;
9
9
  if (auth === "required" && !apiKey) {
@@ -22,6 +22,7 @@ export async function apiFetch(
22
22
  headers: {
23
23
  ...(includeKey && apiKey ? { "x-api-key": apiKey } : {}),
24
24
  ...(body ? { "Content-Type": "application/json" } : {}),
25
+ ...headers,
25
26
  },
26
27
  body: body ? JSON.stringify(body) : undefined,
27
28
  signal: AbortSignal.timeout(timeoutMs),
@@ -0,0 +1,36 @@
1
+ import { createInterface } from "node:readline/promises";
2
+
3
+ export function formatMantou(value) {
4
+ const number = Number(value);
5
+ return Number.isFinite(number) ? `${number} 馒头($${(number * 0.01).toFixed(2)} USD)` : "未知";
6
+ }
7
+
8
+ export async function confirmCharge(quote, { input = process.stdin, output = process.stderr } = {}) {
9
+ if (quote.operator_name) output.write(`\n算子:${quote.operator_name}\n`);
10
+ output.write(`\n⚠️ 本次预计消耗:${formatMantou(quote.estimated_dumplings)}\n`);
11
+ if (quote.formula) output.write(`计费依据:${quote.formula}\n`);
12
+ if (Number.isFinite(Number(quote.balance))) output.write(`当前余额:${quote.balance} 馒头\n`);
13
+ const rl = createInterface({ input, output });
14
+ try {
15
+ const answer = (await rl.question("是否继续?[y/N] ")).trim().toLowerCase();
16
+ return answer === "y" || answer === "yes";
17
+ } finally {
18
+ rl.close();
19
+ }
20
+ }
21
+
22
+ export function printBillingResult(result, output = process.stderr, operatorName = null) {
23
+ const billing = result?._billing;
24
+ if (!billing) return;
25
+ const estimated = Number(billing.estimated_dumplings);
26
+ const charged = Number(billing.charged_dumplings);
27
+ const refunded = Number(billing.refunded_dumplings);
28
+ if (billing.final) {
29
+ if (operatorName) output.write(`\n算子:${operatorName}\n`);
30
+ output.write(`\n✓ 本次实际消耗:${formatMantou(charged)}\n`);
31
+ if (Number.isFinite(refunded) && refunded > 0) output.write(` 已退款:${formatMantou(refunded)}\n`);
32
+ if (Number.isFinite(estimated) && estimated !== charged) output.write(` 调用前预计:${formatMantou(estimated)}\n`);
33
+ } else {
34
+ output.write(`\n⏳ 已预扣:${formatMantou(charged)},任务完成后以最终结算为准\n`);
35
+ }
36
+ }
package/lib/params.js CHANGED
@@ -85,6 +85,14 @@ export function parseDynamicParams(tokens) {
85
85
  for (let i = 0; i < tokens.length; i++) {
86
86
  const token = tokens[i];
87
87
  if (token === "--no-wait") continue;
88
+ if (token === "--confirm") {
89
+ if (tokens[i + 1] === undefined || tokens[i + 1].startsWith("--")) {
90
+ throw new Error("参数 --confirm 缺少值");
91
+ }
92
+ i++;
93
+ continue;
94
+ }
95
+ if (token?.startsWith("--confirm=")) continue;
88
96
  if (!token?.startsWith("--")) throw new Error(`无法识别的参数: ${token}`);
89
97
  const equals = token.indexOf("=");
90
98
  if (equals > 2) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@manturhub/cli",
3
- "version": "0.8.2",
3
+ "version": "0.9.1",
4
4
  "description": "ManturHub 算子广场 CLI:通过 REST 发现和调用 AI 算子、浏览配方,并安装 Skill 与 Agent 套件",
5
5
  "type": "module",
6
6
  "bin": {