@zhuoyuezs/ml-platform 0.1.1 → 0.1.4

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 (26) hide show
  1. package/DEVELOPMENT.md +96 -9
  2. package/README.md +89 -38
  3. package/checksums.json +37 -32
  4. package/package.json +5 -1
  5. package/release-policy.json +10 -0
  6. package/release.json +13 -9
  7. package/runtime/business-client/README.md +13 -0
  8. package/runtime/business-client/package-lock.json +2 -2
  9. package/runtime/business-client/package.json +1 -1
  10. package/runtime/business-client/src/catalog.js +40 -17
  11. package/runtime/business-client/src/cli.js +150 -26
  12. package/runtime/business-client/src/config.js +6 -2
  13. package/runtime/business-client/src/http.js +88 -17
  14. package/scripts/lib.js +98 -4
  15. package/scripts/main.js +96 -11
  16. package/skills/feature-management/SKILL.md +187 -18
  17. package/skills/feature-management/assets/catalog-template/datasets/example_temperature_training.v1.json +16 -0
  18. package/skills/feature-management/assets/catalog-template/feature_sets/example_temperature_core.v1.json +1 -0
  19. package/skills/feature-management/assets/catalog-template/features/example_temperature_mean_5m.v1.json +9 -2
  20. package/skills/feature-management/assets/catalog-template/operator_package/tests/test_operator.py +51 -24
  21. package/skills/feature-management/assets/catalog-template/operators/example_temperature_features.v1.json +16 -1
  22. package/skills/feature-management/assets/catalog-template/parameters/example_temperature.v1.json +1 -0
  23. package/skills/feature-management/references/commands.md +66 -4
  24. package/skills/feature-management/references/contracts.md +43 -6
  25. package/skills/feature-management/references/operator-authoring.md +15 -7
  26. package/skills/feature-management/references/platform-capability-guide.md +44 -0
@@ -45,6 +45,7 @@ class PlatformApiClient {
45
45
 
46
46
  get(endpoint, params) { return this.request("GET", endpoint, { params }); }
47
47
  post(endpoint, payload, params) { return this.request("POST", endpoint, { payload, params }); }
48
+ put(endpoint, payload, params) { return this.request("PUT", endpoint, { payload, params }); }
48
49
  delete(endpoint, params) { return this.request("DELETE", endpoint, { params }); }
49
50
  uploadOperatorPackage(project, name, version, packagePath) {
50
51
  const resolved = path.resolve(expandHome(packagePath));
@@ -63,33 +64,95 @@ class PlatformApiClient {
63
64
  fs.mkdirSync(target, { recursive: true });
64
65
  const controller = new AbortController();
65
66
  const timer = setTimeout(() => controller.abort(), this.timeoutMs);
67
+ const temporaryDir = fs.mkdtempSync(path.join(os.tmpdir(), "data-platform-artifact-download-"));
68
+ const temporary = path.join(temporaryDir, "artifact.zip");
66
69
  let response;
67
70
  try {
68
71
  response = await fetch(this.url(`/dataset-artifacts/${encodeURIComponent(project)}/${encodeURIComponent(datasetId)}/${encodeURIComponent(manifestHash)}/download`), { headers: { Accept: "application/zip" }, signal: controller.signal });
69
- } catch (error) {
70
- throw new Error(`artifact download failed: ${error.message}`);
71
- } finally {
72
- clearTimeout(timer);
73
- }
74
- if (!response.ok) {
75
- let detail = response.statusText;
76
- try { detail = JSON.parse(await response.text())?.detail ?? detail; } catch (_) { /* use status text */ }
77
- throw new Error(`artifact download failed: HTTP ${response.status}: ${detail}`);
78
- }
79
- const temporary = path.join(fs.mkdtempSync(path.join(os.tmpdir(), "data-platform-artifact-download-")), "artifact.zip");
80
- try {
81
- fs.writeFileSync(temporary, Buffer.from(await response.arrayBuffer()));
72
+ if (!response.ok) {
73
+ let detail = response.statusText;
74
+ try { detail = JSON.parse(await abortable(response.text(), controller.signal))?.detail ?? detail; } catch (_) { /* use status text */ }
75
+ throw new Error(`artifact download failed: HTTP ${response.status}: ${detail}`);
76
+ }
77
+ const total = contentLength(response);
78
+ let lastReported = -1;
79
+ const report = (received, complete = false) => {
80
+ if ((complete && received !== lastReported) || received === 0 || received - lastReported >= 1024 * 1024) {
81
+ reportDownloadProgress(received, total);
82
+ lastReported = received;
83
+ }
84
+ };
85
+ report(0);
86
+ await writeResponseBody(response, temporary, controller.signal, report);
82
87
  const files = await extractArchive(temporary, target);
83
88
  return { dataset_id: datasetId, manifest_hash: manifestHash, output_dir: target, file_count: files.length, files };
84
89
  } catch (error) {
85
90
  fs.rmSync(target, { recursive: true, force: true });
86
- throw error;
91
+ if (error instanceof Error && error.message.startsWith("artifact download failed:")) throw error;
92
+ throw new Error(`artifact download failed: ${error.message}`);
87
93
  } finally {
88
- fs.rmSync(path.dirname(temporary), { recursive: true, force: true });
94
+ clearTimeout(timer);
95
+ fs.rmSync(temporaryDir, { recursive: true, force: true });
89
96
  }
90
97
  }
91
98
  }
92
99
 
100
+ function contentLength(response) {
101
+ const raw = response.headers?.get?.("content-length") ?? response.headers?.["content-length"];
102
+ const value = Number(raw);
103
+ return Number.isFinite(value) && value >= 0 ? value : null;
104
+ }
105
+
106
+ function reportDownloadProgress(received, total) {
107
+ const suffix = total == null ? "" : `/${total}`;
108
+ process.stderr.write(`artifact download: received ${received}${suffix} bytes\n`);
109
+ }
110
+
111
+ function abortable(value, signal) {
112
+ if (signal.aborted) return Promise.reject(new Error("This operation was aborted"));
113
+ return new Promise((resolve, reject) => {
114
+ const onAbort = () => { cleanup(); reject(new Error("This operation was aborted")); };
115
+ const cleanup = () => signal.removeEventListener("abort", onAbort);
116
+ signal.addEventListener("abort", onAbort, { once: true });
117
+ Promise.resolve(value).then((result) => { cleanup(); resolve(result); }, (error) => { cleanup(); reject(error); });
118
+ });
119
+ }
120
+
121
+ async function writeResponseBody(response, outputPath, signal, onProgress) {
122
+ const handle = fs.openSync(outputPath, "w");
123
+ let received = 0;
124
+ try {
125
+ const write = (value) => {
126
+ const chunk = Buffer.from(value);
127
+ fs.writeSync(handle, chunk);
128
+ received += chunk.length;
129
+ onProgress(received);
130
+ };
131
+ if (response.body?.getReader) {
132
+ const reader = response.body.getReader();
133
+ while (true) {
134
+ const item = await abortable(reader.read(), signal);
135
+ if (item.done) break;
136
+ write(item.value);
137
+ }
138
+ } else if (response.body?.[Symbol.asyncIterator]) {
139
+ const iterator = response.body[Symbol.asyncIterator]();
140
+ while (true) {
141
+ const item = await abortable(iterator.next(), signal);
142
+ if (item.done) break;
143
+ write(item.value);
144
+ }
145
+ } else if (typeof response.arrayBuffer === "function") {
146
+ write(await abortable(response.arrayBuffer(), signal));
147
+ } else {
148
+ throw new Error("artifact download response has no readable body");
149
+ }
150
+ onProgress(received, true);
151
+ } finally {
152
+ fs.closeSync(handle);
153
+ }
154
+ }
155
+
93
156
  async function extractArchive(archivePath, outputDir) {
94
157
  const archive = fs.readFileSync(archivePath);
95
158
  const end = findEndOfCentralDirectory(archive);
@@ -111,8 +174,8 @@ async function extractArchive(archivePath, outputDir) {
111
174
  if (next > archive.length || compressedSize === 0xffffffff || size === 0xffffffff || localOffset === 0xffffffff) throw new Error("artifact archive entry metadata is invalid");
112
175
  if (flags & 1) throw new Error("artifact archive contains an encrypted entry");
113
176
  if (![0, 8].includes(method)) throw new Error(`artifact archive compression method is unsupported: ${method}`);
114
- if (!(flags & 0x800)) throw new Error("artifact archive contains a non-UTF-8 filename");
115
- const rawName = archive.subarray(cursor + 46, cursor + 46 + nameLength).toString("utf8");
177
+ const rawNameBytes = archive.subarray(cursor + 46, cursor + 46 + nameLength);
178
+ const rawName = decodeZipFilename(rawNameBytes, flags);
116
179
  const name = rawName.replace(/\\/g, "/"); const parts = name.split("/"); const mode = (attributes >>> 16) & 0xffff;
117
180
  if (!name || name.startsWith("/") || /^[A-Za-z]:/.test(name) || parts.includes("..")) throw new Error(`artifact archive contains unsafe path: ${JSON.stringify(rawName)}`);
118
181
  if ((mode & 0o170000) === 0o120000) throw new Error(`artifact archive contains a symbolic link: ${JSON.stringify(rawName)}`);
@@ -131,6 +194,14 @@ async function extractArchive(archivePath, outputDir) {
131
194
  return files.sort();
132
195
  }
133
196
 
197
+ function decodeZipFilename(bytes, flags) {
198
+ if (flags & 0x800) return bytes.toString("utf8");
199
+ // ZIP producers commonly omit the UTF-8 flag for ASCII entry names. Keep
200
+ // those names interoperable while refusing ambiguous non-ASCII encodings.
201
+ if (bytes.some((value) => value > 0x7f)) throw new Error("artifact archive contains an unsupported non-UTF-8 filename");
202
+ return bytes.toString("ascii");
203
+ }
204
+
134
205
  function findEndOfCentralDirectory(archive) { const minimum = Math.max(0, archive.length - 65557); for (let offset = archive.length - 22; offset >= minimum; offset -= 1) if (archive.readUInt32LE(offset) === 0x06054b50 && offset + 22 + archive.readUInt16LE(offset + 20) === archive.length) return offset; throw new Error("artifact download is not a valid zip archive"); }
135
206
  function crc32(buffer) { let crc = 0xffffffff; for (const byte of buffer) { crc ^= byte; for (let bit = 0; bit < 8; bit += 1) crc = (crc >>> 1) ^ (0xedb88320 & -(crc & 1)); } return (crc ^ 0xffffffff) >>> 0; }
136
207
 
package/scripts/lib.js CHANGED
@@ -25,6 +25,12 @@ if (RELEASE_POLICY.schema_version !== "data_platform.ml_platform_release_policy/
25
25
  const FORBIDDEN_DIRECTORIES = new Set(RELEASE_POLICY.forbidden_directories);
26
26
  const FORBIDDEN_FILES = new Set(RELEASE_POLICY.forbidden_files);
27
27
  const FORBIDDEN_SUFFIXES = new Set(RELEASE_POLICY.forbidden_suffixes);
28
+ const FORBIDDEN_CONTENT_PATTERNS = (RELEASE_POLICY.forbidden_content_patterns || []).map((item) => {
29
+ if (!item || typeof item.name !== "string" || typeof item.pattern !== "string") {
30
+ throw new Error("release policy 包含无效内容规则");
31
+ }
32
+ return { name: item.name, pattern: new RegExp(item.pattern, "i") };
33
+ });
28
34
 
29
35
  function parseOptions(args) {
30
36
  const options = {
@@ -34,7 +40,9 @@ function parseOptions(args) {
34
40
  allowDowngrade: false,
35
41
  prewarm: true,
36
42
  backupUnmanaged: false,
43
+ summary: true,
37
44
  };
45
+ let outputFlag = null;
38
46
  const valueFlags = new Map([
39
47
  ["--agent", "agent"],
40
48
  ["--scope", "scope"],
@@ -59,6 +67,14 @@ function parseOptions(args) {
59
67
  options.prewarm = false;
60
68
  } else if (arg === "--backup-unmanaged") {
61
69
  options.backupUnmanaged = true;
70
+ } else if (arg === "--summary") {
71
+ if (outputFlag === "json") throw new Error("--summary 与 --json 不能同时使用");
72
+ outputFlag = "summary";
73
+ options.summary = true;
74
+ } else if (arg === "--json") {
75
+ if (outputFlag === "summary") throw new Error("--summary 与 --json 不能同时使用");
76
+ outputFlag = "json";
77
+ options.summary = false;
62
78
  } else {
63
79
  throw new Error(`未知参数: ${arg}`);
64
80
  }
@@ -130,6 +146,12 @@ function loadRelease(packageRoot = PACKAGE_ROOT) {
130
146
  throw new Error("release policy 摘要与 release manifest 不匹配");
131
147
  }
132
148
  if (!isSemver(release.release_version)) throw new Error("release_version 不是有效 SemVer");
149
+ const requirements = release.runtime_requirements;
150
+ if (!requirements || requirements.node !== ">=18"
151
+ || !Array.isArray(requirements.os)
152
+ || requirements.os.join(",") !== "darwin,linux") {
153
+ throw new Error("release manifest 缺少有效 runtime requirements");
154
+ }
133
155
  const skill = release.skills && release.skills[SKILL_NAME];
134
156
  if (!skill || skill.path !== `skills/${SKILL_NAME}` || !isSha256(skill.sha256)
135
157
  || !isSemver(skill.revision) || typeof skill.requires_cli !== "string") {
@@ -186,6 +208,13 @@ function verifyPackage(packageRoot = PACKAGE_ROOT) {
186
208
  assertAllowedClient(clientRoot);
187
209
  rejectForbiddenTree(skillRoot, "Skill");
188
210
  rejectForbiddenTree(clientRoot, "business client");
211
+ rejectForbiddenContent([
212
+ ...listFiles(skillRoot),
213
+ ...listFiles(clientRoot),
214
+ ...["README.md", "DEVELOPMENT.md"]
215
+ .map((name) => path.join(packageRoot, name))
216
+ .filter((file) => fs.existsSync(file)),
217
+ ], "release payload");
189
218
  if (fs.existsSync(path.join(skillRoot, "runtime")) || fs.existsSync(path.join(skillRoot, "scripts"))) {
190
219
  throw new Error("Skill 不得包含 CLI runtime 或 wrapper");
191
220
  }
@@ -326,11 +355,31 @@ function doctor(options = {}, packageRoot = PACKAGE_ROOT) {
326
355
  actual: matchingShim ?? pathMatches[0] ?? null,
327
356
  remediation: pathOk ? null : `将 ${paths.binDir} 加入 PATH,然后重启 Agent 会话`,
328
357
  });
329
- if (options.apiUrl) {
330
- const apiHealth = runApiHealth(paths.dispatcher, options.apiUrl);
331
- checks.push({ name: "platform_api", ok: apiHealth.ok, detail: apiHealth.detail });
358
+ const apiTarget = resolveEffectiveApiTarget(options, paths.dispatcher);
359
+ if (apiTarget.error) {
360
+ checks.push({
361
+ name: "platform_api",
362
+ ok: false,
363
+ source: apiTarget.source,
364
+ detail: apiTarget.error,
365
+ });
366
+ } else if (apiTarget.apiUrl) {
367
+ const apiHealth = runApiHealth(paths.dispatcher, apiTarget.apiUrl);
368
+ checks.push({
369
+ name: "platform_api",
370
+ ok: apiHealth.ok,
371
+ source: apiTarget.source,
372
+ api_url: apiTarget.apiUrl,
373
+ detail: apiHealth.detail,
374
+ });
332
375
  } else {
333
- checks.push({ name: "platform_api", ok: null, skipped: true, reason: "未提供 --api-url" });
376
+ checks.push({
377
+ name: "platform_api",
378
+ ok: null,
379
+ skipped: true,
380
+ source: null,
381
+ reason: "未通过 --api-url、ML_PLATFORM_API_URL 或 configure 配置 API 地址",
382
+ });
334
383
  }
335
384
  return {
336
385
  ok: checks.every((check) => check.ok !== false),
@@ -693,6 +742,38 @@ function runApiHealth(dispatcher, apiUrl) {
693
742
  return runCliRuntime(dispatcher, ["--api-url", apiUrl.replace(/\/+$/, ""), "health"], false);
694
743
  }
695
744
 
745
+ function resolveEffectiveApiTarget(
746
+ options,
747
+ dispatcher,
748
+ env = process.env,
749
+ runtime = runCliRuntime,
750
+ ) {
751
+ if (options.apiUrl && String(options.apiUrl).trim()) {
752
+ return { apiUrl: String(options.apiUrl).trim(), source: "argument" };
753
+ }
754
+ if (env.ML_PLATFORM_API_URL && String(env.ML_PLATFORM_API_URL).trim()) {
755
+ return { apiUrl: String(env.ML_PLATFORM_API_URL).trim(), source: "environment" };
756
+ }
757
+ const configured = runtime(dispatcher, ["show-config"], false);
758
+ if (!configured.ok) {
759
+ return {
760
+ apiUrl: null,
761
+ source: "saved_config",
762
+ error: `无法读取已保存的 API 配置: ${configured.detail || "未知错误"}`,
763
+ };
764
+ }
765
+ let payload;
766
+ try {
767
+ payload = JSON.parse(configured.detail);
768
+ } catch (_) {
769
+ return { apiUrl: null, source: "saved_config", error: "show-config 未返回有效 JSON" };
770
+ }
771
+ const apiUrl = payload && payload.effective && payload.effective.api_url;
772
+ return apiUrl
773
+ ? { apiUrl: String(apiUrl).trim(), source: "saved_config" }
774
+ : { apiUrl: null, source: null };
775
+ }
776
+
696
777
  function checkTree(checks, name, root, expected) {
697
778
  if (!expected || !fs.existsSync(root)) {
698
779
  checks.push({ name, ok: false, error: `缺少安装目录或预期摘要: ${root}` });
@@ -796,6 +877,18 @@ function rejectForbiddenTree(root, label) {
796
877
  visit(root);
797
878
  }
798
879
 
880
+ function rejectForbiddenContent(files, label) {
881
+ for (const file of files) {
882
+ if (!fs.statSync(file).isFile() || path.basename(file) === "release-policy.json") continue;
883
+ const text = fs.readFileSync(file, "utf8");
884
+ for (const rule of FORBIDDEN_CONTENT_PATTERNS) {
885
+ if (rule.pattern.test(text)) {
886
+ throw new Error(`${label} 包含禁止发布内容 ${rule.name}: ${file}`);
887
+ }
888
+ }
889
+ }
890
+ }
891
+
799
892
  function treeDigest(root) {
800
893
  const files = listFiles(root);
801
894
  const hash = crypto.createHash("sha256");
@@ -990,6 +1083,7 @@ module.exports = {
990
1083
  loadRelease,
991
1084
  migrate,
992
1085
  parseOptions,
1086
+ resolveEffectiveApiTarget,
993
1087
  resolveInstallPaths,
994
1088
  sha256File,
995
1089
  status,
package/scripts/main.js CHANGED
@@ -17,13 +17,29 @@ const BUSINESS_ENTRY = path.resolve(__dirname, "..", "runtime", "business-client
17
17
 
18
18
  function usage() {
19
19
  return `用法:
20
- ml-platform install [--agent codex|pi|custom] [--scope user|project] [--skills-dir PATH] [--project-dir PATH] [--state-dir PATH] [--bin-dir PATH] [--upgrade] [--allow-downgrade] [--backup-unmanaged]
21
- ml-platform upgrade [install options]
22
- ml-platform migrate [--agent codex|pi|custom] [--scope user|project] [--skills-dir PATH] [--project-dir PATH]
23
- ml-platform status [--agent codex|pi|custom] [--scope user|project] [--skills-dir PATH] [--project-dir PATH] [--state-dir PATH] [--bin-dir PATH]
24
- ml-platform doctor [status options] [--api-url URL]
20
+ ml-platform install [--scope user|project] [--json]
21
+ ml-platform upgrade [--scope user|project] [--json]
22
+ ml-platform migrate [--scope user|project] [--json]
23
+ ml-platform status [--scope user|project] [--json]
24
+ ml-platform doctor [--scope user|project] [--api-url URL] [--json]
25
25
  ml-platform <business-command> [args]
26
26
 
27
+ 默认安装:
28
+ Codex 用户级 Skill,持久 CLI,简洁输出
29
+
30
+ 项目级安装:
31
+ ml-platform install --scope project
32
+
33
+ 常用选项:
34
+ --scope user|project Skill 安装范围,默认 user
35
+ --project-dir PATH 项目根目录,project scope 默认使用当前目录
36
+ --json 输出供 Agent 和 CI 解析的 JSON
37
+ --backup-unmanaged 备份目标位置已有的非托管 Skill
38
+
39
+ 高级选项:
40
+ --agent codex|pi|custom, --skills-dir PATH, --state-dir PATH, --bin-dir PATH,
41
+ --allow-downgrade, --no-prewarm, --upgrade, --summary
42
+
27
43
  安装管理命令:
28
44
  install, upgrade, migrate, status, doctor
29
45
 
@@ -31,10 +47,70 @@ function usage() {
31
47
  `;
32
48
  }
33
49
 
50
+ function managementUsage(command) {
51
+ const signatures = {
52
+ install: "install [--scope user|project] [--project-dir PATH] [--json]",
53
+ upgrade: "upgrade [--scope user|project] [--project-dir PATH] [--json]",
54
+ migrate: "migrate [--scope user|project] [--project-dir PATH] [--json]",
55
+ status: "status [--scope user|project] [--project-dir PATH] [--json]",
56
+ doctor: "doctor [--scope user|project] [--project-dir PATH] [--api-url URL] [--json]",
57
+ };
58
+ return `用法: ml-platform ${signatures[command]}\n`;
59
+ }
60
+
34
61
  function printJson(payload) {
35
62
  process.stdout.write(`${JSON.stringify(payload, null, 2)}\n`);
36
63
  }
37
64
 
65
+ function formatSummary(command, payload) {
66
+ if (command === "status") {
67
+ const lines = [`ML Platform: ${payload.installed ? "已安装" : "未安装"}`];
68
+ if (payload.installed_release) lines.push(`Release: ${payload.installed_release}`);
69
+ if (payload.cli?.version) lines.push(`CLI: ${payload.cli.version} (${payload.cli.path})`);
70
+ if (payload.skill?.revision) lines.push(`Skill: ${payload.skill.name}@${payload.skill.revision} (${payload.skill.path})`);
71
+ if (payload.record_error) lines.push(`错误: ${payload.record_error}`);
72
+ return `${lines.join("\n")}\n`;
73
+ }
74
+ if (command === "doctor") {
75
+ if (!Array.isArray(payload.checks)) {
76
+ return `ML Platform 检查失败: ${payload.error || "未知错误"}\n`;
77
+ }
78
+ const failed = payload.checks.filter((check) => check.ok === false);
79
+ const passed = payload.checks.filter((check) => check.ok === true);
80
+ const skipped = payload.checks.filter((check) => check.skipped);
81
+ const lines = [
82
+ `ML Platform 检查: ${payload.ok ? "通过" : "失败"}`,
83
+ `Release: ${payload.release_version || "未知"} CLI: ${payload.cli_version || "未知"}`,
84
+ `检查项: ${passed.length} 通过, ${failed.length} 失败, ${skipped.length} 跳过`,
85
+ ];
86
+ const api = payload.checks.find((check) => check.name === "platform_api");
87
+ if (api?.api_url) lines.push(`API: ${api.api_url} (${api.source})`);
88
+ else if (api?.skipped) lines.push(`API: 未配置 (${api.reason})`);
89
+ for (const check of failed) lines.push(`失败: ${check.name} - ${check.error || check.detail || "检查未通过"}`);
90
+ return `${lines.join("\n")}\n`;
91
+ }
92
+ const actionLabels = {
93
+ installed: "安装完成",
94
+ upgraded: "升级完成",
95
+ unchanged: "已是当前版本",
96
+ migrated: "迁移完成",
97
+ };
98
+ const lines = [
99
+ `ML Platform: ${actionLabels[payload.action] || payload.action || (payload.ok ? "完成" : "失败")}`,
100
+ ];
101
+ if (payload.release_version) lines.push(`Release: ${payload.release_version} CLI: ${payload.cli_version}`);
102
+ if (payload.cli_shim) lines.push(`命令: ${payload.cli_shim}`);
103
+ if (payload.skill_root) lines.push(`Skill: ${payload.skill_root}`);
104
+ if (payload.path_ready === false && payload.path_setup) lines.push(`PATH: ${payload.path_setup}`);
105
+ if (payload.restart_agent_session) lines.push("请重启 Agent 会话以加载最新 Skill。");
106
+ return `${lines.join("\n")}\n`;
107
+ }
108
+
109
+ function printResult(command, payload, summary) {
110
+ if (summary) process.stdout.write(formatSummary(command, payload));
111
+ else printJson(payload);
112
+ }
113
+
38
114
  function loadBusinessCommands() {
39
115
  try {
40
116
  const { BUSINESS_COMMANDS } = require(BUSINESS_ENTRY);
@@ -66,24 +142,29 @@ function main(argv) {
66
142
  }
67
143
  if (!MANAGEMENT_COMMANDS.has(command)) return dispatchBusiness(argv);
68
144
 
145
+ if (rest.includes("--help") || rest.includes("-h")) {
146
+ process.stdout.write(managementUsage(command));
147
+ return 0;
148
+ }
149
+
69
150
  const options = parseOptions(rest);
70
151
  if (command === "install" || command === "upgrade") {
71
152
  if (command === "upgrade") options.upgrade = true;
72
- printJson(install(options));
153
+ printResult(command, install(options), options.summary);
73
154
  return 0;
74
155
  }
75
156
  if (command === "migrate") {
76
157
  const result = migrate(options);
77
- printJson(result);
158
+ printResult(command, result, options.summary);
78
159
  return result.ok ? 0 : 1;
79
160
  }
80
161
  if (command === "status") {
81
162
  const result = status(options);
82
- printJson(result);
163
+ printResult(command, result, options.summary);
83
164
  return result.ok ? 0 : 1;
84
165
  }
85
166
  const result = doctor(options);
86
- printJson(result);
167
+ printResult(command, result, options.summary);
87
168
  return result.ok ? 0 : 1;
88
169
  }
89
170
 
@@ -91,9 +172,13 @@ if (require.main === module) {
91
172
  try {
92
173
  process.exitCode = main(process.argv.slice(2));
93
174
  } catch (error) {
94
- process.stderr.write(`${JSON.stringify({ ok: false, error: error.message }, null, 2)}\n`);
175
+ if (process.argv.slice(2).includes("--json")) {
176
+ process.stderr.write(`${JSON.stringify({ ok: false, error: error.message }, null, 2)}\n`);
177
+ } else {
178
+ process.stderr.write(`ML Platform: 失败\n错误: ${error.message}\n`);
179
+ }
95
180
  process.exitCode = 2;
96
181
  }
97
182
  }
98
183
 
99
- module.exports = { BUSINESS_ENTRY, MANAGEMENT_COMMANDS, dispatchBusiness, main, usage };
184
+ module.exports = { BUSINESS_ENTRY, MANAGEMENT_COMMANDS, dispatchBusiness, formatSummary, main, managementUsage, usage };