@manturhub/cli 0.9.10-dev.20260810.6 → 0.9.11
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 +6 -18
- package/bin/cli.js +159 -367
- package/lib/api.js +52 -74
- package/lib/config.js +8 -27
- package/lib/download.js +2 -2
- package/lib/login-link.js +13 -22
- package/lib/skill-install.js +19 -19
- package/lib/skill-update-fetch.js +9 -11
- package/lib/suite-install.js +21 -15
- package/package.json +2 -4
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
|
-
|
|
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
|
-
#
|
|
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 或脚本等非交互环境中,第一次运行只返回报价和 `
|
|
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 <
|
|
47
|
+
| `manturhub run <中文算子名> --json '{}' [--confirm <quote_id>] [--no-wait]` | 调用算子;非交互确认传报价 ID,异步任务默认轮询到终态 |
|
|
60
48
|
| `manturhub run <中文算子名> --json-file params.json` | 从文件读取参数,适合长提示词和自动化 |
|
|
61
|
-
| `manturhub upload <本地文件>` |
|
|
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,
|
|
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, classifyJobResponse, isFailedJobStatus, 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
|
|
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
|
-
|
|
244
|
-
|
|
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
|
-
|
|
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]
|
|
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 <
|
|
262
|
-
使用非交互试算返回的确认标识调用
|
|
138
|
+
manturhub run <中文算子名> ... --confirm <quote_id> 使用非交互试算返回的报价确认 ID 调用
|
|
263
139
|
manturhub run <中文算子名> ... --no-wait 提交异步任务后立即返回 poll_url,不自动轮询
|
|
264
|
-
manturhub upload <本地文件>
|
|
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(
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
384
|
-
|
|
385
|
-
|
|
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
|
-
|
|
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(
|
|
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 =
|
|
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
|
|
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(
|
|
334
|
+
const floor = Number(r.json.floor);
|
|
476
335
|
console.log(
|
|
477
336
|
JSON.stringify(
|
|
478
|
-
Number.isFinite(floor) ? { ...
|
|
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
|
-
|
|
485
|
-
|
|
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,127 +400,90 @@ async function main() {
|
|
|
547
400
|
console.error(`${error.message},已停止调用避免误扣费`);
|
|
548
401
|
process.exit(1);
|
|
549
402
|
}
|
|
550
|
-
const
|
|
551
|
-
const schema =
|
|
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
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
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
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
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);
|
|
610
441
|
}
|
|
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
|
-
|
|
445
|
+
`/api/v1/operators/${encodeURIComponent(operatorId)}/invoke`,
|
|
635
446
|
{
|
|
636
447
|
method: "POST",
|
|
637
|
-
body
|
|
448
|
+
body,
|
|
638
449
|
timeoutMs: 120000,
|
|
639
|
-
headers: { "
|
|
450
|
+
headers: quoteId ? { "X-Mantur-Quote-Id": quoteId } : {},
|
|
640
451
|
}
|
|
641
452
|
);
|
|
642
|
-
|
|
643
|
-
const
|
|
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
|
-
`⏳ 异步任务 ${
|
|
457
|
+
`⏳ 异步任务 ${r.json.job_id || ""} 已提交,轮询结果中(预计 ${r.json.estimated_seconds || "?"}s;加 --no-wait 可只拿 job_id)…\n`
|
|
656
458
|
);
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
459
|
+
let final;
|
|
460
|
+
try {
|
|
461
|
+
final = await pollJob(pollUrl, {
|
|
462
|
+
initialBilling: r.json._billing,
|
|
463
|
+
onTick: (j) =>
|
|
464
|
+
process.stderr.write(
|
|
465
|
+
` ${j.status}${j.elapsed_ms ? " " + Math.round(j.elapsed_ms / 1000) + "s" : ""}\n`
|
|
466
|
+
),
|
|
467
|
+
});
|
|
468
|
+
} catch (error) {
|
|
469
|
+
console.error(
|
|
470
|
+
`异步任务 ${r.json.job_id || ""} 已提交,但状态查询失败:${error.message}`
|
|
471
|
+
);
|
|
472
|
+
if (error.billing) {
|
|
473
|
+
printBillingResult({ _billing: error.billing }, process.stderr, operator.name);
|
|
474
|
+
}
|
|
475
|
+
console.error("请勿重新调用算子,以免重复生成和重复扣费。");
|
|
476
|
+
if (r.json.job_id) console.error(`任务 ID:${r.json.job_id}`);
|
|
477
|
+
console.error(`轮询地址:${pollUrl}`);
|
|
478
|
+
console.error("请稍后使用 `manturhub status <poll_url>` 继续查询。");
|
|
479
|
+
process.exit(1);
|
|
480
|
+
}
|
|
664
481
|
console.log(JSON.stringify(final, null, 2));
|
|
665
482
|
printBillingResult(final, process.stderr, operator.name);
|
|
666
|
-
|
|
667
|
-
if (st === "failed" || st === "error" || final?._timeout) process.exit(1);
|
|
483
|
+
if (isFailedJobStatus(final.status) || final._timeout) process.exit(1);
|
|
668
484
|
} else {
|
|
669
|
-
console.log(JSON.stringify(
|
|
670
|
-
printBillingResult(
|
|
485
|
+
console.log(JSON.stringify(r.json, null, 2));
|
|
486
|
+
printBillingResult(r.json, process.stderr, operator.name);
|
|
671
487
|
if (!r.ok) process.exit(1);
|
|
672
488
|
}
|
|
673
489
|
break;
|
|
@@ -676,7 +492,7 @@ async function main() {
|
|
|
676
492
|
case "upload": {
|
|
677
493
|
const file = args[1];
|
|
678
494
|
if (!file || file.startsWith("--")) {
|
|
679
|
-
console.error("用法: manturhub upload <本地文件> (
|
|
495
|
+
console.error("用法: manturhub upload <本地文件> (文档/图片/音频/视频 → 公网 URL)");
|
|
680
496
|
process.exit(1);
|
|
681
497
|
}
|
|
682
498
|
try {
|
|
@@ -688,7 +504,7 @@ async function main() {
|
|
|
688
504
|
const mime = mimeFromFile(file);
|
|
689
505
|
if (!mime) {
|
|
690
506
|
console.error(
|
|
691
|
-
`不支持的文件类型: ${file}\n
|
|
507
|
+
`不支持的文件类型: ${file}\n支持文档/图片/音频/视频(md/txt/json/csv/pdf/docx/xls/xlsx/png/jpg/mp3/mp4…)`
|
|
692
508
|
);
|
|
693
509
|
process.exit(1);
|
|
694
510
|
}
|
|
@@ -700,35 +516,26 @@ async function main() {
|
|
|
700
516
|
console.error(`读不到文件: ${file}(${e.message})`);
|
|
701
517
|
process.exit(1);
|
|
702
518
|
}
|
|
703
|
-
const
|
|
519
|
+
const p = await apiFetch("/api/v1/uploads/presign", {
|
|
704
520
|
method: "POST",
|
|
705
|
-
body: {
|
|
706
|
-
files: [
|
|
707
|
-
{
|
|
708
|
-
fileName: basename(file),
|
|
709
|
-
contentType: mime,
|
|
710
|
-
size: stat.size,
|
|
711
|
-
},
|
|
712
|
-
],
|
|
713
|
-
},
|
|
521
|
+
body: { filename: basename(file), size: stat.size, mime },
|
|
714
522
|
});
|
|
715
|
-
if (!
|
|
716
|
-
console.error(
|
|
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");
|
|
523
|
+
if (!p.ok || !p.json || !p.json.put_url) {
|
|
524
|
+
console.error(`presign 失败(HTTP ${p.status}): ${JSON.stringify(p.json)}`);
|
|
723
525
|
process.exit(1);
|
|
724
526
|
}
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
527
|
+
const put = await fetch(p.json.put_url, {
|
|
528
|
+
method: "PUT",
|
|
529
|
+
headers: { "Content-Type": mime, "Content-Length": String(stat.size) },
|
|
530
|
+
body: createReadStream(file),
|
|
531
|
+
duplex: "half",
|
|
532
|
+
signal: AbortSignal.timeout(10 * 60 * 1000),
|
|
533
|
+
});
|
|
534
|
+
if (!put.ok) {
|
|
535
|
+
console.error(`上传到存储失败(HTTP ${put.status})`);
|
|
729
536
|
process.exit(1);
|
|
730
537
|
}
|
|
731
|
-
console.log(
|
|
538
|
+
console.log(p.json.access_url); // 公网 URL,直接喂给算子
|
|
732
539
|
break;
|
|
733
540
|
}
|
|
734
541
|
|
|
@@ -745,10 +552,20 @@ async function main() {
|
|
|
745
552
|
process.exit(1);
|
|
746
553
|
}
|
|
747
554
|
const r = await apiFetch(pu);
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
555
|
+
if (!r.ok) {
|
|
556
|
+
console.error(`查询失败(HTTP ${r.status})`);
|
|
557
|
+
process.exit(1);
|
|
558
|
+
}
|
|
559
|
+
let state;
|
|
560
|
+
try {
|
|
561
|
+
state = classifyJobResponse(r.json);
|
|
562
|
+
} catch (error) {
|
|
563
|
+
console.error(`任务状态响应无效:${error.message}`);
|
|
564
|
+
process.exit(1);
|
|
565
|
+
}
|
|
566
|
+
console.log(JSON.stringify(r.json, null, 2));
|
|
567
|
+
printBillingResult(r.json);
|
|
568
|
+
if (state.failed) process.exit(1);
|
|
752
569
|
break;
|
|
753
570
|
}
|
|
754
571
|
|
|
@@ -759,23 +576,15 @@ async function main() {
|
|
|
759
576
|
console.error(error.message);
|
|
760
577
|
process.exit(1);
|
|
761
578
|
}
|
|
762
|
-
const r = await apiFetch(
|
|
579
|
+
const r = await apiFetch("/api/v1/me");
|
|
763
580
|
if (!r.ok) {
|
|
764
581
|
console.error(`查询失败(HTTP ${r.status})`);
|
|
765
582
|
process.exit(1);
|
|
766
583
|
}
|
|
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
584
|
console.log(
|
|
776
585
|
hasFlag("json")
|
|
777
|
-
? JSON.stringify({ ...
|
|
778
|
-
: `余额: ${usdFor(
|
|
586
|
+
? JSON.stringify({ ...r.json, balance_usd: Number(r.json.balance) * 0.01 }, null, 2)
|
|
587
|
+
: `余额: ${usdFor(r.json.balance)}(${r.json.balance ?? "-"} 馒头) 账号: ${r.json.email || "-"}`
|
|
779
588
|
);
|
|
780
589
|
break;
|
|
781
590
|
}
|
|
@@ -866,41 +675,30 @@ async function main() {
|
|
|
866
675
|
console.error(error.message);
|
|
867
676
|
process.exit(1);
|
|
868
677
|
}
|
|
869
|
-
const r = await apiFetch(
|
|
870
|
-
`${PUBLIC_RECIPES_PATH}/${encodeURIComponent(slug)}`,
|
|
871
|
-
{ auth: "optional" }
|
|
872
|
-
);
|
|
678
|
+
const r = await apiFetch(`/api/v1/recipes/${encodeURIComponent(slug)}`, { auth: "optional" });
|
|
873
679
|
if (!r.ok) {
|
|
874
680
|
console.error(`获取失败(HTTP ${r.status}): ${slug}`);
|
|
875
681
|
process.exit(1);
|
|
876
682
|
}
|
|
877
|
-
|
|
878
|
-
try {
|
|
879
|
-
d = unwrapGatewayData(r.json) || {};
|
|
880
|
-
} catch (error) {
|
|
881
|
-
console.error(`获取失败: ${error.message}`);
|
|
882
|
-
process.exit(1);
|
|
883
|
-
}
|
|
683
|
+
const d = r.json;
|
|
884
684
|
if (args.includes("--json")) {
|
|
885
685
|
console.log(JSON.stringify(d, null, 2));
|
|
886
686
|
break;
|
|
887
687
|
}
|
|
888
|
-
|
|
889
|
-
console.log(
|
|
890
|
-
if (d.
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
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));
|
|
688
|
+
console.log(`\n${d.title} [${d.cat}] · 复刻 ${d.cost_estimate}`);
|
|
689
|
+
console.log(`${d.summary}\n`);
|
|
690
|
+
if (d.sample_url) {
|
|
691
|
+
const su = d.sample_url.startsWith("http")
|
|
692
|
+
? d.sample_url
|
|
693
|
+
: `${getBaseUrl()}${d.sample_url}`;
|
|
694
|
+
console.log(`效果样片: ${su}`);
|
|
903
695
|
}
|
|
696
|
+
console.log(`配方页: ${getBaseUrl()}/recipes/${d.slug}\n`);
|
|
697
|
+
if (d.prompt_template) console.log(`提示词模板:\n${d.prompt_template}\n`);
|
|
698
|
+
console.log("结构化参数(把 {占位符} 换成用户内容):");
|
|
699
|
+
console.log(JSON.stringify(d.params_json || {}, null, 2));
|
|
700
|
+
console.log("\n安全执行:将每步 params 写入 JSON 文件,再运行 `manturhub run <算子ID> --json-file <文件>`。");
|
|
701
|
+
if (d.sample_text) console.log(`\n效果节选:\n${d.sample_text}`);
|
|
904
702
|
break;
|
|
905
703
|
}
|
|
906
704
|
// manturhub recipe [ls|search] [关键词] [--cat video|image|script]
|
|
@@ -918,20 +716,16 @@ async function main() {
|
|
|
918
716
|
console.error(`未知配方分类: ${cat}(可选: video | image | script)`);
|
|
919
717
|
process.exit(1);
|
|
920
718
|
}
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
} catch (error) {
|
|
925
|
-
console.error(`配方列表获取失败(${error.message})`);
|
|
719
|
+
const r = await apiFetch(`/api/v1/recipes${cat ? `?cat=${encodeURIComponent(cat)}` : ""}`, { auth: "optional" });
|
|
720
|
+
if (!r.ok) {
|
|
721
|
+
console.error(`配方列表获取失败(HTTP ${r.status})`);
|
|
926
722
|
process.exit(1);
|
|
927
723
|
}
|
|
928
|
-
|
|
724
|
+
let list = r.json.recipes || [];
|
|
929
725
|
if (kwArg) {
|
|
930
726
|
const k = kwArg.toLowerCase();
|
|
931
727
|
list = list.filter((x) =>
|
|
932
|
-
`${x.
|
|
933
|
-
.toLowerCase()
|
|
934
|
-
.includes(k)
|
|
728
|
+
`${x.title}${x.summary}${(x.tags || []).join(",")}`.toLowerCase().includes(k)
|
|
935
729
|
);
|
|
936
730
|
}
|
|
937
731
|
if (hasFlag("json")) {
|
|
@@ -940,9 +734,7 @@ async function main() {
|
|
|
940
734
|
}
|
|
941
735
|
console.log(`ManturHub 配方(${list.length} 个):\n`);
|
|
942
736
|
for (const x of list) {
|
|
943
|
-
console.log(
|
|
944
|
-
` ${String(x.code || "").padEnd(32)} [${x.category || "其他"}] ${x.name || ""}`
|
|
945
|
-
);
|
|
737
|
+
console.log(` ${x.slug.padEnd(32)} [${x.cat}] ${x.title} · 复刻 ${x.cost_estimate}`);
|
|
946
738
|
}
|
|
947
739
|
console.log(
|
|
948
740
|
`\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
|
|
24
|
+
...(body ? { "Content-Type": "application/json" } : {}),
|
|
85
25
|
...headers,
|
|
86
26
|
},
|
|
87
|
-
body:
|
|
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) => {
|
|
@@ -123,14 +62,40 @@ export async function apiFetch(
|
|
|
123
62
|
return { ok: res.ok, status: res.status, json };
|
|
124
63
|
}
|
|
125
64
|
|
|
126
|
-
//
|
|
127
|
-
//
|
|
65
|
+
// 异步算子只接受已知状态。HTTP 错误、非 JSON object、缺失 status 和未知状态
|
|
66
|
+
// 都必须失败关闭,不能因为“不在活动状态集合里”就被误判为成功终态。
|
|
128
67
|
const ACTIVE = new Set(["created", "queued", "running", "pending", "processing", "in_progress"]);
|
|
68
|
+
const SUCCEEDED = new Set(["succeeded", "success", "completed", "done"]);
|
|
69
|
+
const FAILED = new Set(["failed", "error", "cancelled", "canceled"]);
|
|
70
|
+
|
|
71
|
+
export function classifyJobResponse(result) {
|
|
72
|
+
if (!result || typeof result !== "object" || Array.isArray(result)) {
|
|
73
|
+
throw new Error("异步任务状态响应必须是 JSON object");
|
|
74
|
+
}
|
|
75
|
+
if (typeof result.status !== "string" || !result.status.trim()) {
|
|
76
|
+
throw new Error("异步任务状态响应缺少合法的 status");
|
|
77
|
+
}
|
|
78
|
+
const status = result.status.trim().toLowerCase();
|
|
79
|
+
if (ACTIVE.has(status)) return { status, terminal: false, failed: false };
|
|
80
|
+
if (SUCCEEDED.has(status)) return { status, terminal: true, failed: false };
|
|
81
|
+
if (FAILED.has(status)) return { status, terminal: true, failed: true };
|
|
82
|
+
throw new Error(`异步任务返回未知状态: ${result.status}`);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function isFailedJobStatus(status) {
|
|
86
|
+
return FAILED.has(String(status || "").trim().toLowerCase());
|
|
87
|
+
}
|
|
88
|
+
|
|
129
89
|
export function retainBilling(result, fallbackBilling) {
|
|
130
90
|
if (!fallbackBilling || !result || typeof result !== "object" || Array.isArray(result)) return result;
|
|
131
91
|
return result._billing ? result : { ...result, _billing: fallbackBilling };
|
|
132
92
|
}
|
|
133
93
|
|
|
94
|
+
function attachBilling(error, billing) {
|
|
95
|
+
if (billing && error && typeof error === "object" && !error.billing) error.billing = billing;
|
|
96
|
+
return error;
|
|
97
|
+
}
|
|
98
|
+
|
|
134
99
|
export async function pollJob(
|
|
135
100
|
pollUrl,
|
|
136
101
|
{ intervalMs = 8000, maxMs = 1200000, onTick, initialBilling } = {}
|
|
@@ -139,15 +104,28 @@ export async function pollJob(
|
|
|
139
104
|
let last = null;
|
|
140
105
|
let lastBilling = initialBilling || null;
|
|
141
106
|
while (Date.now() - start < maxMs) {
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
if (!r.ok
|
|
149
|
-
|
|
107
|
+
let r;
|
|
108
|
+
try {
|
|
109
|
+
r = await apiFetch(pollUrl, { timeoutMs: 30000 });
|
|
110
|
+
} catch (error) {
|
|
111
|
+
throw attachBilling(error, lastBilling);
|
|
112
|
+
}
|
|
113
|
+
if (!r.ok) {
|
|
114
|
+
throw attachBilling(
|
|
115
|
+
new Error(`异步任务状态查询失败(HTTP ${r.status})`),
|
|
116
|
+
lastBilling
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
last = r.json;
|
|
120
|
+
if (r.json?._billing) lastBilling = r.json._billing;
|
|
121
|
+
let state;
|
|
122
|
+
try {
|
|
123
|
+
state = classifyJobResponse(r.json);
|
|
124
|
+
} catch (error) {
|
|
125
|
+
throw attachBilling(error, lastBilling);
|
|
150
126
|
}
|
|
127
|
+
if (onTick) onTick(r.json);
|
|
128
|
+
if (state.terminal) return retainBilling(r.json, lastBilling);
|
|
151
129
|
await new Promise((res) => setTimeout(res, intervalMs));
|
|
152
130
|
}
|
|
153
131
|
return retainBilling(
|
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
|
-
|
|
15
|
-
|
|
16
|
-
const
|
|
17
|
-
|
|
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
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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 && !
|
|
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
|
-
|
|
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
|
|
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
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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
|
|
111
|
-
|
|
102
|
+
`\n ✓ 授权成功,Key 已导入 ~/.manturhub/config.json。账号: ${
|
|
103
|
+
me.json.email || "-"
|
|
104
|
+
} 余额: ${usd}(${me.json.balance ?? "-"} 馒头)\n`
|
|
112
105
|
);
|
|
113
106
|
} else {
|
|
114
|
-
|
|
115
|
-
|
|
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
|
}
|
package/lib/skill-install.js
CHANGED
|
@@ -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
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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(
|
|
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 =
|
|
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
|
|
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
|
|
107
|
-
|
|
108
|
-
|
|
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: {
|
|
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
|
|
32
|
-
|
|
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.
|
|
36
|
-
name: item.name || item.
|
|
33
|
+
slug: item.slug,
|
|
34
|
+
name: item.name || item.slug,
|
|
37
35
|
version: item.version,
|
|
38
36
|
kind: item.kind || "skill",
|
|
39
37
|
}));
|
package/lib/suite-install.js
CHANGED
|
@@ -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 {
|
|
6
|
+
import { apiFetch } from "./api.js";
|
|
7
7
|
import { assertSecureDownloadUrl, downloadResponseToFile } from "./download.js";
|
|
8
8
|
|
|
9
|
-
|
|
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
|
|
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
|
-
|
|
23
|
-
.
|
|
24
|
-
.
|
|
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
|
|
56
|
-
|
|
57
|
-
|
|
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.
|
|
3
|
+
"version": "0.9.11",
|
|
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",
|