@manturhub/cli 0.9.10-dev.20260810.6 → 0.9.10

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,26 +15,14 @@ manturhub login
15
15
  printf '%s' "$YOUR_MANTURHUB_KEY" | manturhub login --key-stdin
16
16
  ```
17
17
 
18
- 稳定版 `@manturhub/cli@latest` 默认连接生产站 `https://hub.mantur.ai`,生产 Key 必须在该站创建。
19
-
20
- 开发版 `@manturhub/cli@dev` 默认连接开发网关,无需设置环境变量:
21
-
22
- ```bash
23
- npm install -g @manturhub/cli@dev
24
- manturhub login
25
- manturhub ls
26
- ```
27
-
28
- `MANTURHUB_BASE` 可显式覆盖包的默认网关。开发网关是唯一允许使用远程 HTTP 的例外;生产站和其他远程部署必须使用 HTTPS。开发网关与生产站的 API Key 不通用。
29
- 算子列表和详情通过 `/api/openapi/v1/operators` 获取,只返回当前 Key 已授权的算子。
30
- 文件先通过 `POST /api/openapi/v1/files/upload` 提交 `files: [{ fileName, contentType, size }]`,从 `data.files[0]` 获取 `uploadUrl` 和 `downloadUrl`,再以 PUT 将文件流式上传到 `uploadUrl`;上传成功后命令输出 `downloadUrl`。API Key 只发送给 ManturHub 网关,不会发送给对象存储。
18
+ CLI 默认连接生产站 `https://hub.mantur.ai`,Key 也必须在该生产站创建。`hub.mantur.cn` 是独立测试环境,其 Key 不能用于生产站。
31
19
 
32
20
  需要 Node.js ≥ 18。也可免安装运行:`npx -y @manturhub/cli <命令>`。
33
21
 
34
22
  ## 快速开始
35
23
 
36
24
  ```bash
37
- # 发现与查看当前 Key 已授权的算子
25
+ # 发现与查看算子无需登录
38
26
  manturhub ls --cat text
39
27
  manturhub describe 电商文案生成
40
28
 
@@ -47,7 +35,7 @@ CLI 会在付费调用前按算子实时 schema 校验未知字段、必填项
47
35
 
48
36
  面向用户的列表、详情、报价、确认和结算统一显示中文算子名,也可直接用中文名调用。英文算子 ID 仅保留在 `--json` 机器输出和内部 API 请求中,供 Agent 与脚本稳定使用。
49
37
 
50
- 在 Agent 或脚本等非交互环境中,第一次运行只返回报价和 `confirmation_token`,不会调用或扣费;Agent 向用户确认后,使用提示中的 `--confirm <confirmation_token>` 执行。确认标识绑定算子、入参、价格版本和幂等 ID:内容或价格变化时拒绝调用,同一次调用重试时复用幂等 ID,避免重复扣费。
38
+ 在 Agent 或脚本等非交互环境中,第一次运行只返回报价和 `quote_id`,不会调用或扣费;Agent 向用户确认后,使用提示中的 `--confirm <quote_id>` 执行。报价 5 分钟内有效且只能使用一次。
51
39
 
52
40
  ## 主要命令
53
41
 
@@ -56,9 +44,9 @@ CLI 会在付费调用前按算子实时 schema 校验未知字段、必填项
56
44
  | `manturhub ls [--cat image\|video\|audio\|text\|data] [--json]` | 实时列出上线算子 |
57
45
  | `manturhub describe <中文算子名> [--json]` | 查看精确入参与异步属性 |
58
46
  | `manturhub quote <中文算子名> [--json]` | 查询 Java API 返回的实时计费公式 |
59
- | `manturhub run <中文算子名> --json '{}' [--confirm <confirmation_token>] [--no-wait]` | 调用算子;非交互确认传确认标识,异步任务默认轮询到终态 |
47
+ | `manturhub run <中文算子名> --json '{}' [--confirm <quote_id>] [--no-wait]` | 调用算子;非交互确认传报价 ID,异步任务默认轮询到终态 |
60
48
  | `manturhub run <中文算子名> --json-file params.json` | 从文件读取参数,适合长提示词和自动化 |
61
- | `manturhub upload <本地文件>` | 通过预签名 PUT 流式上传图片、音频或视频,并输出 `downloadUrl` |
49
+ | `manturhub upload <本地文件>` | 流式上传文档、图片、音频或视频并输出公网 URL |
62
50
  | `manturhub status <poll_url>` | 查询 `run --no-wait` 返回的异步任务 |
63
51
  | `manturhub balance [--json]` | 查询余额及美元等值(1 馒头 = $0.01 USD) |
64
52
  | `manturhub skill ls [--json]` / `skill add <slug> [--client codex]` | 浏览并从当前 ManturHub 站点安装业务 Skill |
@@ -69,6 +57,6 @@ CLI 会在付费调用前按算子实时 schema 校验未知字段、必填项
69
57
 
70
58
  机器消费输出可在支持的查询命令上加 `--json`。进度和提示写入 stderr,JSON 结果写入 stdout。
71
59
 
72
- 环境变量:`MANTURHUB_KEY`(优先于配置文件) · `MANTURHUB_BASE`(显式覆盖当前发布通道的默认网关)。
60
+ 环境变量:`MANTURHUB_KEY`(优先于配置文件) · `MANTURHUB_BASE`(测试或私有部署覆盖,默认 `https://hub.mantur.ai`)。
73
61
 
74
62
  ManturHub 算子广场:https://hub.mantur.ai
package/bin/cli.js CHANGED
@@ -1,14 +1,6 @@
1
1
  #!/usr/bin/env node
2
- import { getBaseUrl, isAllowedHttpUrl, saveConfig, loadConfig } from "../lib/config.js";
3
- import {
4
- apiFetch,
5
- BALANCE_PATH,
6
- fetchPublicCatalog,
7
- parseBalanceResponse,
8
- pollJob,
9
- PUBLIC_RECIPES_PATH,
10
- unwrapGatewayData,
11
- } from "../lib/api.js";
2
+ import { getBaseUrl, saveConfig, loadConfig } from "../lib/config.js";
3
+ import { apiFetch, pollJob } from "../lib/api.js";
12
4
  import { runInit } from "../lib/setup.js";
13
5
  import { skillLs, skillAdd, skillOutdated, skillUpdate } from "../lib/skill-install.js";
14
6
  import { suiteLs, suiteInstall } from "../lib/suite-install.js";
@@ -16,14 +8,19 @@ import { loginViaBrowser } from "../lib/login-link.js";
16
8
  import { maybeNotifyUpdate } from "../lib/update-check.js";
17
9
  import { maybeNotifySkillUpdates } from "../lib/skill-update-check.js";
18
10
  import { createReadStream, readFileSync, statSync } from "node:fs";
19
- import { createHash, randomUUID } from "node:crypto";
20
11
  import { fileURLToPath } from "node:url";
21
12
  import { dirname, join, basename, extname } from "node:path";
22
13
  import { parseDynamicParams, validateParams } from "../lib/params.js";
23
14
  import { confirmCharge, formatMantou, printBillingResult } from "../lib/billing-confirm.js";
24
15
 
25
- // 本地文件 → MIME(presign 只接受 image/audio/video)
16
+ // 本地文件 → MIME(最终可用类型仍由上传服务端白名单决定)
26
17
  const MIME_BY_EXT = {
18
+ ".md": "text/markdown", ".markdown": "text/markdown", ".txt": "text/plain",
19
+ ".json": "application/json", ".csv": "text/csv",
20
+ ".pdf": "application/pdf",
21
+ ".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
22
+ ".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
23
+ ".xls": "application/vnd.ms-excel",
27
24
  ".png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".webp": "image/webp",
28
25
  ".gif": "image/gif", ".bmp": "image/bmp",
29
26
  ".mp3": "audio/mpeg", ".wav": "audio/wav", ".m4a": "audio/mp4", ".aac": "audio/aac",
@@ -35,38 +32,6 @@ function mimeFromFile(f) {
35
32
  return MIME_BY_EXT[extname(f).toLowerCase()] || null;
36
33
  }
37
34
 
38
- function assertSecureUploadUrl(value) {
39
- let url;
40
- try {
41
- url = new URL(value);
42
- } catch {
43
- throw new Error("上传接口返回的 uploadUrl 不合法");
44
- }
45
- if (url.protocol !== "https:" && !isAllowedHttpUrl(url)) {
46
- throw new Error("uploadUrl 必须使用 HTTPS");
47
- }
48
- return url.href;
49
- }
50
-
51
- async function putFileToUploadUrl(file, mime, size, uploadUrl) {
52
- const response = await fetch(assertSecureUploadUrl(uploadUrl), {
53
- method: "PUT",
54
- headers: {
55
- "Content-Type": mime,
56
- "Content-Length": String(size),
57
- },
58
- body: createReadStream(file),
59
- duplex: "half",
60
- redirect: "manual",
61
- signal: AbortSignal.timeout(10 * 60 * 1000),
62
- });
63
- if (!response.ok) {
64
- await response.body?.cancel();
65
- throw new Error(`文件上传失败(HTTP ${response.status})`);
66
- }
67
- await response.body?.cancel();
68
- }
69
-
70
35
  const VERSION = JSON.parse(
71
36
  readFileSync(join(dirname(fileURLToPath(import.meta.url)), "../package.json"), "utf8")
72
37
  ).version;
@@ -134,117 +99,29 @@ const CAT_LABELS = {
134
99
  data: "数据",
135
100
  };
136
101
 
137
- const OPERATORS_PATH = "/api/openapi/v1/operators";
138
- const FILE_UPLOAD_PATH = "/api/openapi/v1/files/upload";
139
-
140
102
  function catLabel(cat) {
141
103
  return CAT_LABELS[cat] || cat || "其他";
142
104
  }
143
105
 
144
- function operatorId(operator) {
145
- return operator?.apiCode || operator?.code || operator?.id;
146
- }
147
-
148
- function operatorCat(operator) {
149
- return operator?.category || operator?.cat;
150
- }
151
-
152
- function operatorParamsSchema(operator) {
153
- const declared = operator?.params_schema || operator?.meta?.params_schema;
154
- if (declared) return declared;
155
- const inputSchema = operator?.inputSchema;
156
- if (!inputSchema || inputSchema.type !== "object" || !inputSchema.properties) return null;
157
- const required = new Set(Array.isArray(inputSchema.required) ? inputSchema.required : []);
158
- return {
159
- fields: Object.entries(inputSchema.properties).map(([name, field]) => ({
160
- name,
161
- type: field.type,
162
- required: required.has(name),
163
- enum: field.enum,
164
- minLength: field.minLength,
165
- maxLength: field.maxLength,
166
- minimum: field.minimum,
167
- maximum: field.maximum,
168
- desc: field.description,
169
- })),
170
- };
171
- }
172
-
173
- function canonicalJson(value) {
174
- if (Array.isArray(value)) return `[${value.map(canonicalJson).join(",")}]`;
175
- if (value && typeof value === "object") {
176
- const entries = Object.keys(value)
177
- .sort()
178
- .map((key) => `${JSON.stringify(key)}:${canonicalJson(value[key])}`);
179
- return `{${entries.join(",")}}`;
180
- }
181
- return JSON.stringify(value);
182
- }
183
-
184
- function confirmationDigest(operator, input) {
185
- return createHash("sha256").update(canonicalJson({ operator, input })).digest("base64url");
186
- }
187
-
188
- function encodeConfirmationToken(priceVersionTag, idempotencyKey, operator, input) {
189
- const inputDigest = confirmationDigest(operator, input);
190
- return Buffer.from(
191
- JSON.stringify({ priceVersionTag, idempotencyKey, inputDigest })
192
- ).toString("base64url");
193
- }
194
-
195
- function decodeConfirmationToken(value) {
196
- try {
197
- const parsed = JSON.parse(Buffer.from(value, "base64url").toString("utf8"));
198
- if (
199
- typeof parsed.priceVersionTag !== "string"
200
- || !/^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i
201
- .test(parsed.idempotencyKey || "")
202
- || !/^[A-Za-z0-9_-]{43}$/.test(parsed.inputDigest || "")
203
- ) return null;
204
- return parsed;
205
- } catch {
206
- return null;
207
- }
208
- }
209
-
210
- async function listOperators() {
211
- const pageSize = 100;
212
- const first = await apiFetch(`${OPERATORS_PATH}?pageNum=1&pageSize=${pageSize}`);
213
- if (!first.ok) throw new Error(`算子列表获取失败(HTTP ${first.status})`);
214
- const firstPage = unwrapGatewayData(first.json) || {};
215
- let records = null;
216
- if (Array.isArray(firstPage.records)) records = [...firstPage.records];
217
- else if (Array.isArray(firstPage.operators)) records = [...firstPage.operators];
218
- else if (Array.isArray(firstPage)) records = [...firstPage];
219
- if (!records) throw new Error("算子列表响应缺少 records");
220
- const pages = Number(firstPage.pages);
221
- if (!Number.isFinite(pages) || pages <= 1) return records;
222
-
223
- for (let pageNum = 2; pageNum <= pages; pageNum++) {
224
- const response = await apiFetch(
225
- `${OPERATORS_PATH}?pageNum=${pageNum}&pageSize=${pageSize}`
226
- );
227
- if (!response.ok) throw new Error(`算子列表第 ${pageNum} 页获取失败(HTTP ${response.status})`);
228
- const page = unwrapGatewayData(response.json) || {};
229
- if (Array.isArray(page.records)) records.push(...page.records);
230
- }
231
- return records;
232
- }
233
-
234
- async function fetchOperator(id) {
235
- const response = await apiFetch(`${OPERATORS_PATH}/${encodeURIComponent(id)}`);
236
- if (!response.ok) throw new Error(`算子信息获取失败(HTTP ${response.status})`);
237
- return unwrapGatewayData(response.json);
238
- }
239
-
240
106
  // Human commands accept either the stable operator ID or its Chinese display name.
241
107
  // Machine-facing --json responses keep IDs unchanged for automation compatibility.
242
108
  async function resolveOperator(ref) {
243
- const operators = await listOperators();
244
- const matches = operators.filter((item) => item?.name === ref || operatorId(item) === ref);
109
+ if (String(ref).startsWith("op.")) {
110
+ const direct = await apiFetch(`/api/v1/operators/${encodeURIComponent(ref)}`, { auth: "optional" });
111
+ if (direct.ok) return direct.json.operator || direct.json;
112
+ if (direct.status !== 404) throw new Error(`算子信息获取失败(HTTP ${direct.status})`);
113
+ }
114
+
115
+ const listed = await apiFetch("/api/v1/operators?status=online", { auth: "optional" });
116
+ if (!listed.ok) throw new Error(`算子列表获取失败(HTTP ${listed.status})`);
117
+ const operators = listed.json.operators || listed.json || [];
118
+ const matches = operators.filter((item) => item?.name === ref || item?.id === ref);
245
119
  if (matches.length === 0) throw new Error(`未找到算子“${ref}”`);
246
120
  if (matches.length > 1) throw new Error(`算子名称“${ref}”不唯一,请联系平台处理`);
247
- return fetchOperator(operatorId(matches[0]));
121
+
122
+ const detail = await apiFetch(`/api/v1/operators/${encodeURIComponent(matches[0].id)}`, { auth: "optional" });
123
+ if (!detail.ok) throw new Error(`算子信息获取失败(HTTP ${detail.status})`);
124
+ return detail.json.operator || detail.json;
248
125
  }
249
126
 
250
127
  const HELP = `manturhub — ManturHub 算子广场 CLI v${VERSION}
@@ -253,15 +130,14 @@ const HELP = `manturhub — ManturHub 算子广场 CLI v${VERSION}
253
130
  manturhub login 浏览器授权登录(生成链接→登录创建 Key→自动导入,推荐)
254
131
  manturhub login --key sk-xxx 手动配置 API Key(存 ~/.manturhub/config.json)
255
132
  manturhub login --key-stdin 从 stdin 安全读取 API Key
256
- manturhub ls [--cat <分类>] [--json] 列出当前 Key 已授权的上线算子
257
- manturhub describe <中文算子名> [--json] 查看已授权算子的入参字段
133
+ manturhub ls [--cat <分类>] [--json] 列出上线算子(无需登录)
134
+ manturhub describe <中文算子名> [--json] 查看算子入参字段(无需登录)
258
135
  manturhub quote <中文算子名> 查询实时计费公式(不要使用 Skill 内的历史价格)
259
136
  manturhub run <中文算子名> --json '{}' 试算并确认费用后调用(异步算子自动轮询到出结果)
260
137
  manturhub run <中文算子名> --json-file x.json 从文件读参数(prompt 来自配方/用户时更安全)
261
- manturhub run <中文算子名> ... --confirm <confirmation_token>
262
- 使用非交互试算返回的确认标识调用
138
+ manturhub run <中文算子名> ... --confirm <quote_id> 使用非交互试算返回的报价确认 ID 调用
263
139
  manturhub run <中文算子名> ... --no-wait 提交异步任务后立即返回 poll_url,不自动轮询
264
- manturhub upload <本地文件> 上传图片/音频/视频 → 公网 URL(喂算子前先转换本地文件)
140
+ manturhub upload <本地文件> 上传文档/图片/音频/视频 → 公网 URL(喂算子前先转换本地文件)
265
141
  manturhub status <poll_url> 查异步任务状态(配合 run --no-wait)
266
142
  manturhub balance 查询馒头余额
267
143
  manturhub init [--force] 从线上安装核心 Skill + 写 Agent 引导
@@ -323,32 +199,21 @@ async function main() {
323
199
  await loginViaBrowser();
324
200
  break;
325
201
  }
326
- const r = await apiFetch(BALANCE_PATH, { key });
327
- let verifiedBalance;
328
- let verificationError;
202
+ const r = await apiFetch("/api/v1/me", { key });
329
203
  if (r.ok) {
330
- try {
331
- verifiedBalance = parseBalanceResponse(r.json);
332
- } catch (error) {
333
- verificationError = error.message;
334
- }
335
- }
336
- if (r.ok && verifiedBalance) {
337
204
  const cfg = loadConfig();
338
205
  cfg.key = key;
339
206
  saveConfig(cfg);
340
207
  console.log(
341
- `✓ Key 已验证并保存。余额: ${usdFor(verifiedBalance.totalBalance)}`
342
- + `(${verifiedBalance.totalBalance} 馒头)`
208
+ `✓ Key 已验证并保存。账号: ${r.json.email || "-"} 余额: ${usdFor(r.json.balance)}(${r.json.balance ?? "-"} 馒头)`
343
209
  );
344
210
  } else {
345
211
  const base = getBaseUrl();
346
212
  const productionHint = new URL(base).hostname === "hub.mantur.ai"
347
- ? " 对外用户请在 https://hub.mantur.ai 创建生产 Key;开发测试网关的 Key 不能用于生产。"
213
+ ? " 对外用户请在 https://hub.mantur.ai 创建生产 Key;hub.mantur.cn 的测试 Key 不能用于生产。"
348
214
  : "";
349
- const reason = r.ok ? verificationError : `HTTP ${r.status}`;
350
215
  console.error(
351
- `Key 验证失败(${reason}),未修改本地配置。当前连接:${base}。` +
216
+ `Key 验证失败(HTTP ${r.status}),未修改本地配置。当前连接:${base}。` +
352
217
  `Key 必须由这个站点创建,请确认 Key 是否正确、是否已激活。${productionHint}`
353
218
  );
354
219
  process.exit(1);
@@ -380,21 +245,20 @@ async function main() {
380
245
  console.error(`未知分类: ${cat}(可选: ${[...cats].join(" | ")})`);
381
246
  process.exit(1);
382
247
  }
383
- let ops;
384
- try {
385
- ops = await listOperators();
386
- } catch (error) {
387
- console.error(error.message);
248
+ const r = await apiFetch("/api/v1/operators?status=online", { auth: "optional" });
249
+ if (!r.ok) {
250
+ console.error(`列表获取失败(HTTP ${r.status})`);
388
251
  process.exit(1);
389
252
  }
390
- if (cat) ops = ops.filter((o) => operatorCat(o) === cat);
253
+ let ops = r.json.operators || r.json || [];
254
+ if (cat) ops = ops.filter((o) => o.cat === cat);
391
255
  if (hasFlag("json")) {
392
256
  console.log(JSON.stringify({ operators: ops }, null, 2));
393
257
  break;
394
258
  }
395
259
  console.log(`ManturHub 上线算子(${ops.length} 个):\n`);
396
260
  for (const o of ops) {
397
- console.log(` ${o.name} [${catLabel(operatorCat(o))}]`);
261
+ console.log(` ${o.name} [${catLabel(o.cat)}]`);
398
262
  }
399
263
  console.log(`\n用 \`manturhub describe <中文算子名>\` 查入参,\`manturhub run <中文算子名> --json '{...}'\` 调用`);
400
264
  break;
@@ -424,11 +288,9 @@ async function main() {
424
288
  console.log(JSON.stringify(o, null, 2));
425
289
  break;
426
290
  }
427
- console.log(
428
- `\n${o.name || "未命名算子"} [${catLabel(operatorCat(o))}] · ${o.status || "online"}`
429
- );
291
+ console.log(`\n${o.name || "未命名算子"} [${catLabel(o.cat)}] · ${o.status || "-"}`);
430
292
  if (o.description) console.log(o.description);
431
- const ps = operatorParamsSchema(o);
293
+ const ps = o.params_schema || (o.meta && o.meta.params_schema);
432
294
  if (ps && Array.isArray(ps.fields) && ps.fields.length) {
433
295
  console.log(`\n入参:`);
434
296
  for (const f of ps.fields) {
@@ -463,32 +325,23 @@ async function main() {
463
325
  console.error(error.message);
464
326
  process.exit(1);
465
327
  }
466
- const id = operatorId(operator);
467
- const quotePath = operator.quotePath || `${OPERATORS_PATH}/${encodeURIComponent(id)}/quote`;
468
- const r = await apiFetch(quotePath);
328
+ const r = await apiFetch(`/api/v1/operators/${encodeURIComponent(operator.id)}/quote`, { auth: "optional" });
469
329
  if (!r.ok) {
470
330
  console.error(`查询价格失败(HTTP ${r.status}): ${JSON.stringify(r.json)}`);
471
331
  process.exit(1);
472
332
  }
473
- const quoted = unwrapGatewayData(r.json) || {};
474
333
  if (args.includes("--json")) {
475
- const floor = Number(quoted.floor ?? quoted.estimatedPoints);
334
+ const floor = Number(r.json.floor);
476
335
  console.log(
477
336
  JSON.stringify(
478
- Number.isFinite(floor) ? { ...quoted, floor_usd: floor * 0.01 } : quoted,
337
+ Number.isFinite(floor) ? { ...r.json, floor_usd: floor * 0.01 } : r.json,
479
338
  null,
480
339
  2
481
340
  )
482
341
  );
483
342
  } else {
484
- const formulas = Array.isArray(quoted.items)
485
- ? quoted.items.map((item) => item.formula).filter(Boolean).join(";")
486
- : "";
487
- console.log(
488
- `${operator.name}: ${quoted.formula || formulas || operator.pricing?.displayText || "详见算子页"}`
489
- );
490
- const floor = quoted.floor ?? quoted.estimatedPoints;
491
- if (floor !== undefined) console.log(`最低扣费: ${usdFor(floor)}(${floor} 馒头)`);
343
+ console.log(`${operator.name}: ${r.json.formula || "详见算子页"}`);
344
+ if (r.json.floor !== undefined) console.log(`最低扣费: ${usdFor(r.json.floor)}(${r.json.floor} 馒头)`);
492
345
  }
493
346
  break;
494
347
  }
@@ -547,115 +400,64 @@ async function main() {
547
400
  console.error(`${error.message},已停止调用避免误扣费`);
548
401
  process.exit(1);
549
402
  }
550
- const resolvedOperatorId = operatorId(operator);
551
- const schema = operatorParamsSchema(operator);
403
+ const operatorId = operator.id;
404
+ const schema = operator.params_schema || operator.meta?.params_schema;
552
405
  try {
553
406
  body = validateParams(body, schema, { coerceStrings: !jsonArg && !jsonFile });
554
407
  } catch (error) {
555
408
  console.error(`参数校验失败: ${error.message}`);
556
409
  process.exit(1);
557
410
  }
558
- const confirmationToken = getFlag("confirm");
559
- const quotePath = operator.quotePath
560
- || `${OPERATORS_PATH}/${encodeURIComponent(resolvedOperatorId)}/quote`;
561
- const quoteResponse = await apiFetch(quotePath);
562
- if (!quoteResponse.ok) {
563
- console.error(
564
- `本次费用试算失败(HTTP ${quoteResponse.status}): ${JSON.stringify(quoteResponse.json)}`
565
- );
566
- process.exit(1);
567
- }
568
- const quote = unwrapGatewayData(quoteResponse.json) || {};
569
- const estimated = Number(quote.estimatedPoints);
570
- const priceVersionTag = quote.priceVersionTag;
571
- const formula = Array.isArray(quote.items)
572
- ? quote.items.map((item) => item.formula).filter(Boolean).join(";")
573
- : "";
574
- if (quote.billable !== false && !Number.isFinite(estimated)) {
575
- console.error("报价响应缺少有效的 estimatedPoints,已停止调用避免误扣费。");
576
- process.exit(1);
577
- }
578
- if (quote.enoughBalance === false) {
579
- console.error(`余额不足:当前 ${quote.balance ?? "-"} 馒头,预计需要 ${estimated} 馒头。`);
580
- process.exit(1);
581
- }
582
- let idempotencyKey = randomUUID();
583
- if (confirmationToken) {
584
- const confirmation = decodeConfirmationToken(confirmationToken);
585
- if (
586
- !confirmation
587
- || confirmation.priceVersionTag !== priceVersionTag
588
- || confirmation.inputDigest !== confirmationDigest(resolvedOperatorId, body)
589
- ) {
590
- console.error("报价已变化或确认标识无效,请重新执行 run 获取最新报价。");
411
+ let quoteId = getFlag("confirm");
412
+ if (!quoteId) {
413
+ const quote = await apiFetch(`/api/v1/operators/${encodeURIComponent(operatorId)}/quote`, {
414
+ method: "POST",
415
+ body,
416
+ });
417
+ if (!quote.ok) {
418
+ console.error(`本次费用试算失败(HTTP ${quote.status}): ${JSON.stringify(quote.json)}`);
591
419
  process.exit(1);
592
420
  }
593
- idempotencyKey = confirmation.idempotencyKey;
594
- }
595
- if (!confirmationToken && Number.isFinite(estimated) && estimated > 0) {
596
- if (process.stdin.isTTY && process.stderr.isTTY) {
597
- if (!(await confirmCharge({
598
- estimated_dumplings: estimated,
599
- balance: quote.balance,
600
- formula,
601
- operator_name: operator.name,
602
- }))) {
603
- console.error("已取消,未调用算子、未扣费。");
604
- process.exit(2);
421
+ const estimated = Number(quote.json?.estimated_dumplings);
422
+ quoteId = quote.json?.quote_id;
423
+ if (Number.isFinite(estimated) && estimated > 0) {
424
+ if (process.stdin.isTTY && process.stderr.isTTY) {
425
+ if (!(await confirmCharge({ ...quote.json, operator_name: operator.name }))) {
426
+ console.error("已取消,未调用算子、未扣费。");
427
+ process.exit(2);
428
+ }
429
+ } else {
430
+ console.error(JSON.stringify({
431
+ error: "CONFIRMATION_REQUIRED",
432
+ operator_name: operator.name,
433
+ message: `本次预计消耗 ${formatMantou(estimated)},请先取得用户确认`,
434
+ estimated_dumplings: estimated,
435
+ balance: quote.json?.balance,
436
+ formula: quote.json?.formula,
437
+ quote_id: quoteId,
438
+ retry_with: `--confirm ${quoteId}`,
439
+ }, null, 2));
440
+ process.exit(3);
605
441
  }
606
- } else {
607
- if (!priceVersionTag) {
608
- console.error("报价响应缺少 priceVersionTag,无法安全确认付费调用。");
609
- process.exit(1);
610
- }
611
- const encodedConfirmationToken = encodeConfirmationToken(
612
- priceVersionTag,
613
- idempotencyKey,
614
- resolvedOperatorId,
615
- body
616
- );
617
- console.error(JSON.stringify({
618
- error: "CONFIRMATION_REQUIRED",
619
- operator_name: operator.name,
620
- message: `本次预计消耗 ${formatMantou(estimated)},请先取得用户确认`,
621
- estimated_dumplings: estimated,
622
- balance: quote.balance,
623
- formula,
624
- price_version_tag: priceVersionTag,
625
- confirmation_token: encodedConfirmationToken,
626
- retry_with: `--confirm ${encodedConfirmationToken}`,
627
- }, null, 2));
628
- process.exit(3);
629
442
  }
630
443
  }
631
- const invokePath = operator.invokePath
632
- || `${OPERATORS_PATH}/${encodeURIComponent(resolvedOperatorId)}/invoke`;
633
444
  const r = await apiFetch(
634
- invokePath,
445
+ `/api/v1/operators/${encodeURIComponent(operatorId)}/invoke`,
635
446
  {
636
447
  method: "POST",
637
- body: { input: body },
448
+ body,
638
449
  timeoutMs: 120000,
639
- headers: { "Idempotency-Key": idempotencyKey },
450
+ headers: quoteId ? { "X-Mantur-Quote-Id": quoteId } : {},
640
451
  }
641
452
  );
642
- let invoked = r.ok ? unwrapGatewayData(r.json) : r.json;
643
- const invokedJobId = invoked?.jobId || invoked?.job_id;
644
- if (r.ok && invokedJobId) {
645
- // OpenAPI 作业必须通过当前网关查询,不能把 Agent Key 发送到响应中的其他域名。
646
- invoked = {
647
- ...invoked,
648
- pollUrl: `/api/openapi/v1/jobs/${encodeURIComponent(invokedJobId)}`,
649
- };
650
- }
651
- // 异步算子默认自动轮询到出结果;--no-wait 只返回提交结果。
652
- const pollUrl = r.ok && (invoked?.pollUrl || invoked?.poll_url);
453
+ // 异步算子(返回 poll_url)默认自动轮询到出结果;--no-wait 只拿 job_id。
454
+ const pollUrl = r.ok && r.json && r.json.poll_url;
653
455
  if (pollUrl && !args.includes("--no-wait")) {
654
456
  process.stderr.write(
655
- `⏳ 异步任务 ${invokedJobId || ""} 已提交,轮询结果中…\n`
457
+ `⏳ 异步任务 ${r.json.job_id || ""} 已提交,轮询结果中(预计 ${r.json.estimated_seconds || "?"}s;加 --no-wait 可只拿 job_id)…\n`
656
458
  );
657
459
  const final = await pollJob(pollUrl, {
658
- initialBilling: invoked._billing,
460
+ initialBilling: r.json._billing,
659
461
  onTick: (j) =>
660
462
  process.stderr.write(
661
463
  ` ${j.status || "?"}${j.elapsed_ms ? " " + Math.round(j.elapsed_ms / 1000) + "s" : ""}\n`
@@ -663,11 +465,11 @@ async function main() {
663
465
  });
664
466
  console.log(JSON.stringify(final, null, 2));
665
467
  printBillingResult(final, process.stderr, operator.name);
666
- const st = String(final?.status || "").toLowerCase();
667
- if (st === "failed" || st === "error" || final?._timeout) process.exit(1);
468
+ const st = final && final.status;
469
+ if (st === "failed" || st === "error" || (final && final._timeout)) process.exit(1);
668
470
  } else {
669
- console.log(JSON.stringify(invoked, null, 2));
670
- printBillingResult(invoked, process.stderr, operator.name);
471
+ console.log(JSON.stringify(r.json, null, 2));
472
+ printBillingResult(r.json, process.stderr, operator.name);
671
473
  if (!r.ok) process.exit(1);
672
474
  }
673
475
  break;
@@ -676,7 +478,7 @@ async function main() {
676
478
  case "upload": {
677
479
  const file = args[1];
678
480
  if (!file || file.startsWith("--")) {
679
- console.error("用法: manturhub upload <本地文件> (图片/音频/视频 → 公网 URL)");
481
+ console.error("用法: manturhub upload <本地文件> (文档/图片/音频/视频 → 公网 URL)");
680
482
  process.exit(1);
681
483
  }
682
484
  try {
@@ -688,7 +490,7 @@ async function main() {
688
490
  const mime = mimeFromFile(file);
689
491
  if (!mime) {
690
492
  console.error(
691
- `不支持的文件类型: ${file}\n仅支持图片/音频/视频(png/jpg/webp/gif/mp3/wav/m4a/mp4/mov/webm…)`
493
+ `不支持的文件类型: ${file}\n支持文档/图片/音频/视频(md/txt/json/csv/pdf/docx/xls/xlsx/png/jpg/mp3/mp4…)`
692
494
  );
693
495
  process.exit(1);
694
496
  }
@@ -700,35 +502,26 @@ async function main() {
700
502
  console.error(`读不到文件: ${file}(${e.message})`);
701
503
  process.exit(1);
702
504
  }
703
- const prepared = await apiFetch(FILE_UPLOAD_PATH, {
505
+ const p = await apiFetch("/api/v1/uploads/presign", {
704
506
  method: "POST",
705
- body: {
706
- files: [
707
- {
708
- fileName: basename(file),
709
- contentType: mime,
710
- size: stat.size,
711
- },
712
- ],
713
- },
507
+ body: { filename: basename(file), size: stat.size, mime },
714
508
  });
715
- if (!prepared.ok) {
716
- console.error(`获取上传地址失败(HTTP ${prepared.status}): ${JSON.stringify(prepared.json)}`);
717
- process.exit(1);
718
- }
719
- const result = unwrapGatewayData(prepared.json) || {};
720
- const upload = Array.isArray(result.files) ? result.files[0] : null;
721
- if (!upload?.uploadUrl || !upload?.downloadUrl) {
722
- console.error("上传接口响应的 files[0] 缺少 uploadUrl 或 downloadUrl");
509
+ if (!p.ok || !p.json || !p.json.put_url) {
510
+ console.error(`presign 失败(HTTP ${p.status}): ${JSON.stringify(p.json)}`);
723
511
  process.exit(1);
724
512
  }
725
- try {
726
- await putFileToUploadUrl(file, mime, stat.size, upload.uploadUrl);
727
- } catch (error) {
728
- console.error(error.message);
513
+ const put = await fetch(p.json.put_url, {
514
+ method: "PUT",
515
+ headers: { "Content-Type": mime, "Content-Length": String(stat.size) },
516
+ body: createReadStream(file),
517
+ duplex: "half",
518
+ signal: AbortSignal.timeout(10 * 60 * 1000),
519
+ });
520
+ if (!put.ok) {
521
+ console.error(`上传到存储失败(HTTP ${put.status})`);
729
522
  process.exit(1);
730
523
  }
731
- console.log(upload.downloadUrl); // 上传成功后输出可直接喂给算子的下载地址
524
+ console.log(p.json.access_url); // 公网 URL,直接喂给算子
732
525
  break;
733
526
  }
734
527
 
@@ -745,9 +538,8 @@ async function main() {
745
538
  process.exit(1);
746
539
  }
747
540
  const r = await apiFetch(pu);
748
- const status = r.ok ? unwrapGatewayData(r.json) : r.json;
749
- console.log(JSON.stringify(status, null, 2));
750
- printBillingResult(status);
541
+ console.log(JSON.stringify(r.json, null, 2));
542
+ printBillingResult(r.json);
751
543
  if (!r.ok) process.exit(1);
752
544
  break;
753
545
  }
@@ -759,23 +551,15 @@ async function main() {
759
551
  console.error(error.message);
760
552
  process.exit(1);
761
553
  }
762
- const r = await apiFetch(BALANCE_PATH);
554
+ const r = await apiFetch("/api/v1/me");
763
555
  if (!r.ok) {
764
556
  console.error(`查询失败(HTTP ${r.status})`);
765
557
  process.exit(1);
766
558
  }
767
- let parsedBalance;
768
- try {
769
- parsedBalance = parseBalanceResponse(r.json);
770
- } catch (error) {
771
- console.error(error.message);
772
- process.exit(1);
773
- }
774
- const { balance, totalBalance: balancePoints } = parsedBalance;
775
559
  console.log(
776
560
  hasFlag("json")
777
- ? JSON.stringify({ ...balance, balance_usd: balancePoints * 0.01 }, null, 2)
778
- : `余额: ${usdFor(balancePoints)}(${balancePoints} 馒头)`
561
+ ? JSON.stringify({ ...r.json, balance_usd: Number(r.json.balance) * 0.01 }, null, 2)
562
+ : `余额: ${usdFor(r.json.balance)}(${r.json.balance ?? "-"} 馒头) 账号: ${r.json.email || "-"}`
779
563
  );
780
564
  break;
781
565
  }
@@ -866,41 +650,30 @@ async function main() {
866
650
  console.error(error.message);
867
651
  process.exit(1);
868
652
  }
869
- const r = await apiFetch(
870
- `${PUBLIC_RECIPES_PATH}/${encodeURIComponent(slug)}`,
871
- { auth: "optional" }
872
- );
653
+ const r = await apiFetch(`/api/v1/recipes/${encodeURIComponent(slug)}`, { auth: "optional" });
873
654
  if (!r.ok) {
874
655
  console.error(`获取失败(HTTP ${r.status}): ${slug}`);
875
656
  process.exit(1);
876
657
  }
877
- let d;
878
- try {
879
- d = unwrapGatewayData(r.json) || {};
880
- } catch (error) {
881
- console.error(`获取失败: ${error.message}`);
882
- process.exit(1);
883
- }
658
+ const d = r.json;
884
659
  if (args.includes("--json")) {
885
660
  console.log(JSON.stringify(d, null, 2));
886
661
  break;
887
662
  }
888
- const recipeCode = d.code || slug;
889
- console.log(`\n${d.name || recipeCode} [${d.category || "其他"}]${d.version ? ` · ${d.version}` : ""}`);
890
- if (d.description) console.log(`${d.description}\n`);
891
- if (d.sampleUrl) {
892
- console.log(`效果样片: ${d.sampleUrl}`);
893
- }
894
- console.log(`配方页: ${getBaseUrl()}/recipes/${recipeCode}\n`);
895
- if (d.usage) console.log(`执行说明:\n${d.usage}\n`);
896
- if (d.inputSchema) {
897
- console.log("输入结构:");
898
- console.log(JSON.stringify(d.inputSchema, null, 2));
899
- }
900
- if (Array.isArray(d.steps) && d.steps.length) {
901
- console.log("\n执行步骤:");
902
- console.log(JSON.stringify(d.steps, null, 2));
663
+ console.log(`\n${d.title} [${d.cat}] · 复刻 ${d.cost_estimate}`);
664
+ console.log(`${d.summary}\n`);
665
+ if (d.sample_url) {
666
+ const su = d.sample_url.startsWith("http")
667
+ ? d.sample_url
668
+ : `${getBaseUrl()}${d.sample_url}`;
669
+ console.log(`效果样片: ${su}`);
903
670
  }
671
+ console.log(`配方页: ${getBaseUrl()}/recipes/${d.slug}\n`);
672
+ if (d.prompt_template) console.log(`提示词模板:\n${d.prompt_template}\n`);
673
+ console.log("结构化参数(把 {占位符} 换成用户内容):");
674
+ console.log(JSON.stringify(d.params_json || {}, null, 2));
675
+ console.log("\n安全执行:将每步 params 写入 JSON 文件,再运行 `manturhub run <算子ID> --json-file <文件>`。");
676
+ if (d.sample_text) console.log(`\n效果节选:\n${d.sample_text}`);
904
677
  break;
905
678
  }
906
679
  // manturhub recipe [ls|search] [关键词] [--cat video|image|script]
@@ -918,20 +691,16 @@ async function main() {
918
691
  console.error(`未知配方分类: ${cat}(可选: video | image | script)`);
919
692
  process.exit(1);
920
693
  }
921
- let list;
922
- try {
923
- list = await fetchPublicCatalog(PUBLIC_RECIPES_PATH);
924
- } catch (error) {
925
- console.error(`配方列表获取失败(${error.message})`);
694
+ const r = await apiFetch(`/api/v1/recipes${cat ? `?cat=${encodeURIComponent(cat)}` : ""}`, { auth: "optional" });
695
+ if (!r.ok) {
696
+ console.error(`配方列表获取失败(HTTP ${r.status})`);
926
697
  process.exit(1);
927
698
  }
928
- if (cat) list = list.filter((item) => item.category === cat);
699
+ let list = r.json.recipes || [];
929
700
  if (kwArg) {
930
701
  const k = kwArg.toLowerCase();
931
702
  list = list.filter((x) =>
932
- `${x.name || ""}${x.summary || ""}${(x.tags || []).join(",")}`
933
- .toLowerCase()
934
- .includes(k)
703
+ `${x.title}${x.summary}${(x.tags || []).join(",")}`.toLowerCase().includes(k)
935
704
  );
936
705
  }
937
706
  if (hasFlag("json")) {
@@ -940,9 +709,7 @@ async function main() {
940
709
  }
941
710
  console.log(`ManturHub 配方(${list.length} 个):\n`);
942
711
  for (const x of list) {
943
- console.log(
944
- ` ${String(x.code || "").padEnd(32)} [${x.category || "其他"}] ${x.name || ""}`
945
- );
712
+ console.log(` ${x.slug.padEnd(32)} [${x.cat}] ${x.title} · 复刻 ${x.cost_estimate}`);
946
713
  }
947
714
  console.log(
948
715
  `\n用 \`manturhub recipe get <配方ID>\` 看提示词模板与调用参数;挑选体验更好的网页版: ${getBaseUrl()}/recipes`
package/lib/api.js CHANGED
@@ -1,70 +1,10 @@
1
1
  import { getKey, getBaseUrl } from "./config.js";
2
2
 
3
- export const BALANCE_PATH = "/api/openapi/v1/credits/balance";
4
- export const PUBLIC_RECIPES_PATH = "/api/public/agent/v1/recipes";
5
- export const PUBLIC_SKILLS_PATH = "/api/public/agent/v1/skills";
6
-
7
- export function unwrapGatewayData(json) {
8
- if (json?.success === false || (typeof json?.code === "number" && json.code !== 0)) {
9
- throw new Error(json?.message || "ManturHub Gateway 请求失败");
10
- }
11
- return json && Object.hasOwn(json, "data") ? json.data : json;
12
- }
13
-
14
- export function parseBalanceResponse(json) {
15
- const balance = unwrapGatewayData(json);
16
- const totalBalance = Number(balance?.totalBalance);
17
- if (!Number.isFinite(totalBalance)) {
18
- throw new Error("余额接口响应缺少有效的 totalBalance");
19
- }
20
- return { balance, totalBalance };
21
- }
22
-
23
- export async function fetchPublicCatalog(path, { pageSize = 100 } = {}) {
24
- const fetchPage = async (pageNum) => {
25
- const response = await apiFetch(
26
- `${path}?pageNum=${pageNum}&pageSize=${pageSize}`,
27
- { auth: "optional" }
28
- );
29
- if (!response.ok) {
30
- const error = new Error(`HTTP ${response.status}`);
31
- error.status = response.status;
32
- throw error;
33
- }
34
- const page = unwrapGatewayData(response.json) || {};
35
- if (!Array.isArray(page.records)) {
36
- throw new Error("公共广场响应缺少 records");
37
- }
38
- return page;
39
- };
40
-
41
- const first = await fetchPage(1);
42
- const records = [...first.records];
43
- const pages = Number(first.pages);
44
- if (!Number.isFinite(pages) || pages <= 1) return records;
45
- for (let pageNum = 2; pageNum <= pages; pageNum++) {
46
- const page = await fetchPage(pageNum);
47
- records.push(...page.records);
48
- }
49
- return records;
50
- }
51
-
52
3
  // Thin REST client against the ManturHub gateway. Public discovery calls may omit auth.
53
4
  export async function apiFetch(
54
5
  path,
55
- {
56
- method = "GET",
57
- body,
58
- rawBody,
59
- key,
60
- auth = "required",
61
- timeoutMs = 30000,
62
- headers = {},
63
- } = {}
6
+ { method = "GET", body, key, auth = "required", timeoutMs = 30000, headers = {} } = {}
64
7
  ) {
65
- if (body !== undefined && rawBody !== undefined) {
66
- throw new Error("body 和 rawBody 不能同时使用");
67
- }
68
8
  const apiKey = key === undefined ? getKey() : key;
69
9
  if (auth === "required" && !apiKey) {
70
10
  throw new Error(
@@ -81,11 +21,10 @@ export async function apiFetch(
81
21
  method,
82
22
  headers: {
83
23
  ...(includeKey && apiKey ? { "x-api-key": apiKey } : {}),
84
- ...(body !== undefined ? { "Content-Type": "application/json" } : {}),
24
+ ...(body ? { "Content-Type": "application/json" } : {}),
85
25
  ...headers,
86
26
  },
87
- body: rawBody ?? (body !== undefined ? JSON.stringify(body) : undefined),
88
- ...(rawBody !== undefined ? { duplex: "half" } : {}),
27
+ body: body ? JSON.stringify(body) : undefined,
89
28
  signal: AbortSignal.timeout(timeoutMs),
90
29
  });
91
30
  const requestWithRetry = async (includeKey) => {
@@ -125,7 +64,7 @@ export async function apiFetch(
125
64
 
126
65
  // 异步算子:轮询 invoke 返回的 poll_url 直到任务出终态(succeeded/failed/…),返回最终 json。
127
66
  // 未知 shape(无 status 字段)按终态处理,直接返回让上层打印。
128
- const ACTIVE = new Set(["created", "queued", "running", "pending", "processing", "in_progress"]);
67
+ const ACTIVE = new Set(["queued", "running", "pending", "processing", "in_progress"]);
129
68
  export function retainBilling(result, fallbackBilling) {
130
69
  if (!fallbackBilling || !result || typeof result !== "object" || Array.isArray(result)) return result;
131
70
  return result._billing ? result : { ...result, _billing: fallbackBilling };
@@ -140,13 +79,12 @@ export async function pollJob(
140
79
  let lastBilling = initialBilling || null;
141
80
  while (Date.now() - start < maxMs) {
142
81
  const r = await apiFetch(pollUrl, { timeoutMs: 30000 });
143
- const response = r.json && Object.hasOwn(r.json, "data") ? r.json.data : r.json;
144
- last = response;
145
- if (response?._billing) lastBilling = response._billing;
146
- const s = response?.status;
147
- if (onTick) onTick(response);
148
- if (!r.ok || !s || !ACTIVE.has(String(s).toLowerCase())) {
149
- return retainBilling(response, lastBilling); // 终态(或报错/未知 shape)→ 返回
82
+ last = r.json;
83
+ if (r.json?._billing) lastBilling = r.json._billing;
84
+ const s = r.json && r.json.status;
85
+ if (onTick) onTick(r.json);
86
+ if (!r.ok || !s || !ACTIVE.has(s)) {
87
+ return retainBilling(r.json, lastBilling); // 终态(或报错/未知 shape) 返回
150
88
  }
151
89
  await new Promise((res) => setTimeout(res, intervalMs));
152
90
  }
package/lib/config.js CHANGED
@@ -11,33 +11,15 @@ import {
11
11
  // Local config lives at ~/.manturhub/config.json (chmod 600). Env vars win over it.
12
12
  const DIR = join(homedir(), ".manturhub");
13
13
  const FILE = join(DIR, "config.json");
14
- const DEVELOPMENT_HTTP_ORIGIN = "http://gateway-dev.guiyi.cn";
15
- const PRODUCTION_ORIGIN = "https://hub.mantur.ai";
16
- const PACKAGE_VERSION = JSON.parse(
17
- readFileSync(new URL("../package.json", import.meta.url), "utf8")
18
- ).version;
19
-
20
- export function defaultBaseForVersion(version) {
21
- return /-dev(?:\.|$)/.test(String(version)) ? DEVELOPMENT_HTTP_ORIGIN : PRODUCTION_ORIGIN;
22
- }
23
-
24
- // dev 预发布包默认连接开发网关,稳定包默认连接生产网关。MANTURHUB_BASE
25
- // 仍可显式覆盖;历史配置中保存过的官方默认地址不应覆盖当前发布通道。
26
- const DEFAULT_BASE = defaultBaseForVersion(PACKAGE_VERSION);
27
- const KNOWN_DEFAULT_BASES = new Set([
28
- DEVELOPMENT_HTTP_ORIGIN,
29
- PRODUCTION_ORIGIN,
14
+ // Published CLI defaults to production; test/private deployments override with
15
+ // MANTURHUB_BASE or config.json. All generated links must go through getBaseUrl().
16
+ const DEFAULT_BASE = "https://hub.mantur.ai";
17
+ const LEGACY_DEFAULT_BASES = new Set([
30
18
  "https://hub.mantur.cn",
31
19
  "https://manturhub.leisurecat.cloud",
32
20
  "https://api.ophub.com",
33
21
  ]);
34
22
 
35
- export function isAllowedHttpUrl(url) {
36
- const loopback =
37
- url.hostname === "localhost" || url.hostname === "127.0.0.1" || url.hostname === "[::1]";
38
- return url.protocol === "http:" && (loopback || url.origin === DEVELOPMENT_HTTP_ORIGIN);
39
- }
40
-
41
23
  export function loadConfig() {
42
24
  try {
43
25
  return JSON.parse(readFileSync(FILE, "utf8"));
@@ -67,10 +49,9 @@ function validateBaseUrl(value) {
67
49
  } catch {
68
50
  throw new Error(`ManturHub 网关地址不合法: ${value}`);
69
51
  }
70
- if (url.protocol !== "https:" && !isAllowedHttpUrl(url)) {
71
- throw new Error(
72
- "MANTURHUB_BASE 必须使用 HTTPS(仅本机地址和 http://gateway-dev.guiyi.cn 可使用 HTTP)"
73
- );
52
+ const loopback = url.hostname === "localhost" || url.hostname === "127.0.0.1" || url.hostname === "[::1]";
53
+ if (url.protocol !== "https:" && !(url.protocol === "http:" && loopback)) {
54
+ throw new Error("MANTURHUB_BASE 必须使用 HTTPS(仅 localhost/127.0.0.1/::1 可使用 HTTP)");
74
55
  }
75
56
  if (url.username || url.password) throw new Error("MANTURHUB_BASE 不得包含用户名或密码");
76
57
  return url.href.replace(/\/$/, "");
@@ -79,6 +60,6 @@ function validateBaseUrl(value) {
79
60
  export function getBaseUrl() {
80
61
  if (process.env.MANTURHUB_BASE) return validateBaseUrl(process.env.MANTURHUB_BASE);
81
62
  const storedBase = loadConfig().baseUrl?.replace(/\/$/, "");
82
- if (storedBase && !KNOWN_DEFAULT_BASES.has(storedBase)) return validateBaseUrl(storedBase);
63
+ if (storedBase && !LEGACY_DEFAULT_BASES.has(storedBase)) return validateBaseUrl(storedBase);
83
64
  return DEFAULT_BASE;
84
65
  }
package/lib/download.js CHANGED
@@ -1,7 +1,6 @@
1
1
  import { createWriteStream, rmSync } from "node:fs";
2
2
  import { Readable, Transform } from "node:stream";
3
3
  import { pipeline } from "node:stream/promises";
4
- import { isAllowedHttpUrl } from "./config.js";
5
4
 
6
5
  export const MAX_PACKAGE_BYTES = 100 * 1024 * 1024;
7
6
 
@@ -12,7 +11,8 @@ export function assertSecureDownloadUrl(value) {
12
11
  } catch {
13
12
  throw new Error("下载重定向地址不合法");
14
13
  }
15
- if (url.protocol !== "https:" && !isAllowedHttpUrl(url)) {
14
+ const loopback = url.hostname === "localhost" || url.hostname === "127.0.0.1" || url.hostname === "[::1]";
15
+ if (url.protocol !== "https:" && !(url.protocol === "http:" && loopback)) {
16
16
  throw new Error("安装包下载地址必须使用 HTTPS");
17
17
  }
18
18
  return url.href;
package/lib/login-link.js CHANGED
@@ -3,7 +3,7 @@
3
3
  // key 只经后端 poll(device_code)下发,绝不进浏览器 URL。
4
4
  import { spawn } from "node:child_process";
5
5
  import { getBaseUrl, loadConfig, saveConfig } from "./config.js";
6
- import { apiFetch, BALANCE_PATH, parseBalanceResponse } from "./api.js";
6
+ import { apiFetch } from "./api.js";
7
7
 
8
8
  const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
9
9
 
@@ -91,31 +91,22 @@ export async function loginViaBrowser() {
91
91
  process.exit(1);
92
92
  }
93
93
  if (poll.status === "ready" && poll.key) {
94
- const verification = await apiFetch(BALANCE_PATH, { key: poll.key });
95
- let verifiedBalance;
96
- let verificationError;
97
- if (verification.ok) {
98
- try {
99
- verifiedBalance = parseBalanceResponse(verification.json);
100
- } catch (error) {
101
- verificationError = error.message;
102
- }
103
- }
104
- if (verification.ok && verifiedBalance) {
105
- const cfg = loadConfig();
106
- cfg.key = poll.key;
107
- saveConfig(cfg);
108
- const usd = `$${(verifiedBalance.totalBalance * 0.01).toFixed(2)} USD`;
94
+ const cfg = loadConfig();
95
+ cfg.key = poll.key;
96
+ saveConfig(cfg);
97
+ const me = await apiFetch("/api/v1/me", { key: poll.key });
98
+ if (me.ok) {
99
+ const balance = Number(me.json.balance);
100
+ const usd = Number.isFinite(balance) ? `$${(balance * 0.01).toFixed(2)} USD` : "-";
109
101
  console.log(
110
- `\n ✓ 授权成功,Key 已验证并导入 ~/.manturhub/config.json。` +
111
- `余额: ${usd}(${verifiedBalance.totalBalance} 馒头)\n`
102
+ `\n ✓ 授权成功,Key 已导入 ~/.manturhub/config.json。账号: ${
103
+ me.json.email || "-"
104
+ } 余额: ${usd}(${me.json.balance ?? "-"} 馒头)\n`
112
105
  );
113
106
  } else {
114
- const reason = verification.ok ? verificationError : `HTTP ${verification.status}`;
115
- console.error(
116
- `\n Key 余额验证失败(${reason}),未修改本地配置。请重新运行 manturhub login。\n`
107
+ console.log(
108
+ `\n ✓ Key 已导入,但验证返回 HTTP ${me.status}(稍后可用 manturhub balance 再确认)。\n`
117
109
  );
118
- process.exit(1);
119
110
  }
120
111
  return;
121
112
  }
@@ -10,14 +10,9 @@ import {
10
10
  renameSync,
11
11
  rmSync,
12
12
  } from "node:fs";
13
- import { getBaseUrl } from "./config.js";
13
+ import { getKey, getBaseUrl } from "./config.js";
14
14
  import { extractZipSafely, validateSlug } from "./archive.js";
15
- import {
16
- apiFetch,
17
- fetchPublicCatalog,
18
- PUBLIC_SKILLS_PATH,
19
- unwrapGatewayData,
20
- } from "./api.js";
15
+ import { apiFetch } from "./api.js";
21
16
  import { assertSecureDownloadUrl, downloadResponseToFile } from "./download.js";
22
17
  import { displayClient, resolveSkillClient } from "./agent-target.js";
23
18
  import {
@@ -32,31 +27,36 @@ import {
32
27
  skillDestination,
33
28
  } from "./skill-state.js";
34
29
 
30
+ const CLI_SKILL_HEADERS = { "X-ManturHub-Client": "cli" };
31
+
35
32
  function fail(message) {
36
33
  console.error(message);
37
34
  process.exit(1);
38
35
  }
39
36
 
40
37
  async function fetchOnlineSkills() {
41
- const records = await fetchPublicCatalog(PUBLIC_SKILLS_PATH);
42
- return records
43
- .filter((item) => item.kind !== "suite")
44
- .map((item) => ({ ...item, slug: item.code }));
38
+ const response = await apiFetch("/api/v1/skills", {
39
+ auth: "optional",
40
+ headers: CLI_SKILL_HEADERS,
41
+ });
42
+ if (!response.ok) throw new Error(`Skill 列表获取失败(HTTP ${response.status})`);
43
+ return (response.json.skills || response.json || []).filter((item) => item.kind !== "suite");
45
44
  }
46
45
 
47
46
  async function fetchSkillDetail(slug) {
48
- const response = await apiFetch(`${PUBLIC_SKILLS_PATH}/${encodeURIComponent(slug)}`, {
47
+ const response = await apiFetch(`/api/v1/skills/${encodeURIComponent(slug)}`, {
49
48
  auth: "optional",
49
+ headers: CLI_SKILL_HEADERS,
50
50
  });
51
51
  if (response.status === 404) {
52
52
  throw new Error(`Skill 不存在: ${slug}(用 \`manturhub skill ls\` 看可用列表)`);
53
53
  }
54
54
  if (!response.ok) throw new Error(`Skill 信息获取失败(HTTP ${response.status})`);
55
- const detail = unwrapGatewayData(response.json) || {};
55
+ const detail = response.json.skill || response.json;
56
56
  if (!detail?.version) {
57
57
  throw new Error(`线上 Skill「${slug}」缺少 version,平台需先修正后才能安装`);
58
58
  }
59
- return { ...detail, slug: detail.code };
59
+ return detail;
60
60
  }
61
61
 
62
62
  function detectExistingClient(slug) {
@@ -103,13 +103,13 @@ function assertSafeToReplace(destinations, slug, state, baseUrl, force) {
103
103
  }
104
104
 
105
105
  async function downloadSkillBundle(slug, target) {
106
- const base = new URL(getBaseUrl());
107
- const bundlePath = `${PUBLIC_SKILLS_PATH}/${encodeURIComponent(slug)}/bundle`;
108
- const url = new URL(bundlePath, base.href.endsWith("/") ? base.href : `${base.href}/`);
109
- if (url.origin !== base.origin) {
110
- throw new Error(`Skill「${slug}」的下载地址指向非 ManturHub 地址`);
106
+ const key = getKey();
107
+ if (!key) {
108
+ throw new Error("下载 Skill API Key。运行 `manturhub login`,或设置环境变量 MANTURHUB_KEY。");
111
109
  }
110
+ const url = `${getBaseUrl()}/api/v1/skills/${encodeURIComponent(slug)}/download`;
112
111
  const response = await fetch(url, {
112
+ headers: { "x-api-key": key, ...CLI_SKILL_HEADERS },
113
113
  redirect: "manual",
114
114
  signal: AbortSignal.timeout(30000),
115
115
  });
@@ -2,7 +2,6 @@ import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
2
  import { homedir } from "node:os";
3
3
  import { join } from "node:path";
4
4
  import { getBaseUrl } from "./config.js";
5
- import { PUBLIC_SKILLS_PATH, unwrapGatewayData } from "./api.js";
6
5
 
7
6
  const DIR = join(homedir(), ".manturhub");
8
7
  const CACHE = join(DIR, "skill-update-check.json");
@@ -17,23 +16,22 @@ function readPrevious() {
17
16
 
18
17
  const baseUrl = getBaseUrl();
19
18
  try {
20
- const url = new URL(
21
- `${PUBLIC_SKILLS_PATH}?pageNum=1&pageSize=100`,
22
- `${baseUrl}/`
23
- );
19
+ const url = new URL("/api/v1/skills", `${baseUrl}/`);
24
20
  const response = await fetch(url, {
25
21
  signal: AbortSignal.timeout(8000),
26
- headers: { Accept: "application/json" },
22
+ headers: {
23
+ Accept: "application/json",
24
+ "X-ManturHub-Client": "cli",
25
+ },
27
26
  });
28
27
  mkdirSync(DIR, { recursive: true });
29
28
  if (response.ok) {
30
29
  const json = await response.json();
31
- const page = unwrapGatewayData(json) || {};
32
- const skills = (page.records || [])
33
- .filter((item) => item?.kind !== "suite" && item?.code && item?.version)
30
+ const skills = (json.skills || json || [])
31
+ .filter((item) => item?.kind !== "suite" && item?.slug && item?.version)
34
32
  .map((item) => ({
35
- slug: item.code,
36
- name: item.name || item.code,
33
+ slug: item.slug,
34
+ name: item.name || item.slug,
37
35
  version: item.version,
38
36
  kind: item.kind || "skill",
39
37
  }));
@@ -1,12 +1,14 @@
1
1
  import { join, resolve } from "node:path";
2
2
  import { tmpdir } from "node:os";
3
3
  import { mkdtempSync, rmSync } from "node:fs";
4
- import { getBaseUrl } from "./config.js";
4
+ import { getKey, getBaseUrl } from "./config.js";
5
5
  import { extractZipSafely, validateSlug } from "./archive.js";
6
- import { fetchPublicCatalog, PUBLIC_SKILLS_PATH } from "./api.js";
6
+ import { apiFetch } from "./api.js";
7
7
  import { assertSecureDownloadUrl, downloadResponseToFile } from "./download.js";
8
8
 
9
- // #456 Agent 套件(团队版 Skill):与 skill 共用公共 Skill 广场列表和固定 bundle 下载接口,
9
+ const CLI_SKILL_HEADERS = { "X-ManturHub-Client": "cli" };
10
+
11
+ // #456 Agent 套件(团队版 Skill):与 skill 共用 /api/v1/skills 元数据与下载端点,
10
12
  // 区别在安装形态——解压为当前目录下的工作目录(非用户级 skills)。API Key 继续由
11
13
  // CLI 从 ~/.manturhub/config.json 读取,绝不复制进可能被提交的项目目录。
12
14
 
@@ -14,14 +16,20 @@ import { assertSecureDownloadUrl, downloadResponseToFile } from "./download.js";
14
16
  export async function suiteLs({ json = false } = {}) {
15
17
  let res;
16
18
  try {
17
- res = await fetchPublicCatalog(PUBLIC_SKILLS_PATH);
19
+ res = await apiFetch("/api/v1/skills", {
20
+ auth: "optional",
21
+ headers: CLI_SKILL_HEADERS,
22
+ });
18
23
  } catch (e) {
19
24
  console.error(`套件列表获取失败: ${e.message}`);
20
25
  process.exit(1);
21
26
  }
22
- const suites = res
23
- .filter((item) => item.kind === "suite")
24
- .map((item) => ({ ...item, slug: item.code }));
27
+ if (!res.ok) {
28
+ console.error(`套件列表获取失败(HTTP ${res.status})`);
29
+ process.exit(1);
30
+ }
31
+ const data = res.json;
32
+ const suites = (data.skills || []).filter((s) => s.kind === "suite");
25
33
  if (json) {
26
34
  console.log(JSON.stringify({ suites }, null, 2));
27
35
  return;
@@ -52,20 +60,18 @@ export async function suiteInstall(slug, dirArg) {
52
60
  console.error(error.message);
53
61
  process.exit(1);
54
62
  }
55
- const base = new URL(getBaseUrl());
56
- const bundlePath = `${PUBLIC_SKILLS_PATH}/${encodeURIComponent(slug)}/bundle`;
57
- const url = new URL(
58
- bundlePath,
59
- base.href.endsWith("/") ? base.href : `${base.href}/`
60
- );
61
- if (url.origin !== base.origin) {
62
- console.error(`套件「${slug}」的下载地址指向非 ManturHub 地址`);
63
+ const key = getKey();
64
+ if (!key) {
65
+ console.error("下载套件需 API Key。运行 `manturhub login`,或设置环境变量 MANTURHUB_KEY。");
63
66
  process.exit(1);
64
67
  }
68
+
69
+ const url = `${getBaseUrl()}/api/v1/skills/${encodeURIComponent(slug)}/download`;
65
70
  // 手动处理 302:跟随预签名地址时不把 API Key 带去对象存储(同 skill add)。
66
71
  let res;
67
72
  try {
68
73
  res = await fetch(url, {
74
+ headers: { "x-api-key": key, ...CLI_SKILL_HEADERS },
69
75
  redirect: "manual",
70
76
  signal: AbortSignal.timeout(30000),
71
77
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@manturhub/cli",
3
- "version": "0.9.10-dev.20260810.6",
3
+ "version": "0.9.10",
4
4
  "description": "ManturHub 算子广场 CLI:通过 REST 发现和调用 AI 算子、浏览配方,并安装 Skill 与 Agent 套件",
5
5
  "type": "module",
6
6
  "bin": {
@@ -14,9 +14,7 @@
14
14
  },
15
15
  "scripts": {
16
16
  "test": "node --test",
17
- "prepack": "npm test",
18
- "release:dev": "npm version prerelease --preid=dev --no-git-tag-version && npm publish --access public --tag dev",
19
- "release:prod": "npm version patch --no-git-tag-version && npm publish --access public --tag latest"
17
+ "prepack": "npm test"
20
18
  },
21
19
  "files": [
22
20
  "bin",