@manturhub/cli 0.9.10 → 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.
Files changed (3) hide show
  1. package/bin/cli.js +36 -11
  2. package/lib/api.js +48 -8
  3. package/package.json +1 -1
package/bin/cli.js CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import { getBaseUrl, saveConfig, loadConfig } from "../lib/config.js";
3
- import { apiFetch, pollJob } from "../lib/api.js";
3
+ import { apiFetch, classifyJobResponse, isFailedJobStatus, pollJob } from "../lib/api.js";
4
4
  import { runInit } from "../lib/setup.js";
5
5
  import { skillLs, skillAdd, skillOutdated, skillUpdate } from "../lib/skill-install.js";
6
6
  import { suiteLs, suiteInstall } from "../lib/suite-install.js";
@@ -456,17 +456,31 @@ async function main() {
456
456
  process.stderr.write(
457
457
  `⏳ 异步任务 ${r.json.job_id || ""} 已提交,轮询结果中(预计 ${r.json.estimated_seconds || "?"}s;加 --no-wait 可只拿 job_id)…\n`
458
458
  );
459
- const final = await pollJob(pollUrl, {
460
- initialBilling: r.json._billing,
461
- onTick: (j) =>
462
- process.stderr.write(
463
- ` ${j.status || "?"}${j.elapsed_ms ? " " + Math.round(j.elapsed_ms / 1000) + "s" : ""}\n`
464
- ),
465
- });
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
+ }
466
481
  console.log(JSON.stringify(final, null, 2));
467
482
  printBillingResult(final, process.stderr, operator.name);
468
- const st = final && final.status;
469
- if (st === "failed" || st === "error" || (final && final._timeout)) process.exit(1);
483
+ if (isFailedJobStatus(final.status) || final._timeout) process.exit(1);
470
484
  } else {
471
485
  console.log(JSON.stringify(r.json, null, 2));
472
486
  printBillingResult(r.json, process.stderr, operator.name);
@@ -538,9 +552,20 @@ async function main() {
538
552
  process.exit(1);
539
553
  }
540
554
  const r = await apiFetch(pu);
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
+ }
541
566
  console.log(JSON.stringify(r.json, null, 2));
542
567
  printBillingResult(r.json);
543
- if (!r.ok) process.exit(1);
568
+ if (state.failed) process.exit(1);
544
569
  break;
545
570
  }
546
571
 
package/lib/api.js CHANGED
@@ -62,14 +62,40 @@ export async function apiFetch(
62
62
  return { ok: res.ok, status: res.status, json };
63
63
  }
64
64
 
65
- // 异步算子:轮询 invoke 返回的 poll_url 直到任务出终态(succeeded/failed/…),返回最终 json。
66
- // 未知 shape(无 status 字段)按终态处理,直接返回让上层打印。
67
- const ACTIVE = new Set(["queued", "running", "pending", "processing", "in_progress"]);
65
+ // 异步算子只接受已知状态。HTTP 错误、非 JSON object、缺失 status 和未知状态
66
+ // 都必须失败关闭,不能因为“不在活动状态集合里”就被误判为成功终态。
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
+
68
89
  export function retainBilling(result, fallbackBilling) {
69
90
  if (!fallbackBilling || !result || typeof result !== "object" || Array.isArray(result)) return result;
70
91
  return result._billing ? result : { ...result, _billing: fallbackBilling };
71
92
  }
72
93
 
94
+ function attachBilling(error, billing) {
95
+ if (billing && error && typeof error === "object" && !error.billing) error.billing = billing;
96
+ return error;
97
+ }
98
+
73
99
  export async function pollJob(
74
100
  pollUrl,
75
101
  { intervalMs = 8000, maxMs = 1200000, onTick, initialBilling } = {}
@@ -78,14 +104,28 @@ export async function pollJob(
78
104
  let last = null;
79
105
  let lastBilling = initialBilling || null;
80
106
  while (Date.now() - start < maxMs) {
81
- const r = await apiFetch(pollUrl, { timeoutMs: 30000 });
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
+ }
82
119
  last = r.json;
83
120
  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) 返回
121
+ let state;
122
+ try {
123
+ state = classifyJobResponse(r.json);
124
+ } catch (error) {
125
+ throw attachBilling(error, lastBilling);
88
126
  }
127
+ if (onTick) onTick(r.json);
128
+ if (state.terminal) return retainBilling(r.json, lastBilling);
89
129
  await new Promise((res) => setTimeout(res, intervalMs));
90
130
  }
91
131
  return retainBilling(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@manturhub/cli",
3
- "version": "0.9.10",
3
+ "version": "0.9.11",
4
4
  "description": "ManturHub 算子广场 CLI:通过 REST 发现和调用 AI 算子、浏览配方,并安装 Skill 与 Agent 套件",
5
5
  "type": "module",
6
6
  "bin": {