@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.
- package/DEVELOPMENT.md +96 -9
- package/README.md +89 -38
- package/checksums.json +37 -32
- package/package.json +5 -1
- package/release-policy.json +10 -0
- package/release.json +13 -9
- package/runtime/business-client/README.md +13 -0
- package/runtime/business-client/package-lock.json +2 -2
- package/runtime/business-client/package.json +1 -1
- package/runtime/business-client/src/catalog.js +40 -17
- package/runtime/business-client/src/cli.js +150 -26
- package/runtime/business-client/src/config.js +6 -2
- package/runtime/business-client/src/http.js +88 -17
- package/scripts/lib.js +98 -4
- package/scripts/main.js +96 -11
- package/skills/feature-management/SKILL.md +187 -18
- package/skills/feature-management/assets/catalog-template/datasets/example_temperature_training.v1.json +16 -0
- package/skills/feature-management/assets/catalog-template/feature_sets/example_temperature_core.v1.json +1 -0
- package/skills/feature-management/assets/catalog-template/features/example_temperature_mean_5m.v1.json +9 -2
- package/skills/feature-management/assets/catalog-template/operator_package/tests/test_operator.py +51 -24
- package/skills/feature-management/assets/catalog-template/operators/example_temperature_features.v1.json +16 -1
- package/skills/feature-management/assets/catalog-template/parameters/example_temperature.v1.json +1 -0
- package/skills/feature-management/references/commands.md +66 -4
- package/skills/feature-management/references/contracts.md +43 -6
- package/skills/feature-management/references/operator-authoring.md +15 -7
- 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
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
let
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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
|
-
|
|
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
|
-
|
|
115
|
-
const rawName =
|
|
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
|
-
|
|
330
|
-
|
|
331
|
-
checks.push({
|
|
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({
|
|
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 [--
|
|
21
|
-
ml-platform upgrade [
|
|
22
|
-
ml-platform migrate [--
|
|
23
|
-
ml-platform status [--
|
|
24
|
-
ml-platform doctor [
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
163
|
+
printResult(command, result, options.summary);
|
|
83
164
|
return result.ok ? 0 : 1;
|
|
84
165
|
}
|
|
85
166
|
const result = doctor(options);
|
|
86
|
-
|
|
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.
|
|
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 };
|