@pippit-dev/cli 1.0.18 → 1.0.20
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 +18 -0
- package/checksums.txt +6 -6
- package/cmd/canvas/canvas.go +8 -2
- package/cmd/canvas/canvas_test.go +56 -0
- package/cmd/get_credit_balance.go +37 -0
- package/cmd/get_credit_balance_test.go +73 -0
- package/cmd/root.go +2 -1
- package/cmd/root_test.go +9 -0
- package/cmd/short_drama_test.go +1 -0
- package/dist/checksums.txt +6 -0
- package/dist/xyq-canvas-command-runtime.cjs +16 -0
- package/dist/xyq-canvas-command-runtime.cjs.LEGAL.txt +599 -0
- package/dist/xyq-canvas-command-runtime.cjs.sha256 +2 -0
- package/internal/canvas/apply.go +104 -28
- package/internal/canvas/canvas_test.go +113 -0
- package/internal/common/get_credit_balance.go +61 -0
- package/internal/common/get_credit_balance_test.go +75 -0
- package/internal/config/config.go +11 -8
- package/internal/config/config_test.go +3 -0
- package/package.json +9 -2
- package/scripts/canvas-command.js +881 -0
- package/scripts/run.js +21 -4
|
@@ -0,0 +1,881 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const fs = require("fs");
|
|
4
|
+
const http = require("http");
|
|
5
|
+
const https = require("https");
|
|
6
|
+
const os = require("os");
|
|
7
|
+
const path = require("path");
|
|
8
|
+
const { spawn } = require("child_process");
|
|
9
|
+
const { createHash, randomBytes } = require("crypto");
|
|
10
|
+
const { pathToFileURL } = require("url");
|
|
11
|
+
|
|
12
|
+
const DEFAULT_SDK_MODULE = path.join(
|
|
13
|
+
__dirname,
|
|
14
|
+
"..",
|
|
15
|
+
"dist",
|
|
16
|
+
"xyq-canvas-command-runtime.cjs"
|
|
17
|
+
);
|
|
18
|
+
const MAX_INPUT_BYTES = 64 * 1024 * 1024;
|
|
19
|
+
const PERSISTENCE_VERSION = 1;
|
|
20
|
+
const OPTIONAL_SCHEMA = Symbol("optional canvas command schema");
|
|
21
|
+
const CHECKPOINT_COMMANDS = new Set([
|
|
22
|
+
"compare_checkpoint",
|
|
23
|
+
"create_checkpoint",
|
|
24
|
+
"list_checkpoints",
|
|
25
|
+
"restore_checkpoint",
|
|
26
|
+
]);
|
|
27
|
+
const CANVAS_COMMAND_PERMISSIONS = [
|
|
28
|
+
"canvas.read",
|
|
29
|
+
"canvas.write",
|
|
30
|
+
"canvas.patch",
|
|
31
|
+
"canvas.asset.read",
|
|
32
|
+
"canvas.asset.write",
|
|
33
|
+
"canvas.checkpoint.create",
|
|
34
|
+
"canvas.checkpoint.restore",
|
|
35
|
+
"canvas.permission.read",
|
|
36
|
+
];
|
|
37
|
+
|
|
38
|
+
const COMMAND_HELP = `用法:
|
|
39
|
+
pippit-tool-cli canvas command list
|
|
40
|
+
pippit-tool-cli canvas command describe <command>
|
|
41
|
+
pippit-tool-cli canvas command run <command> --canvas-id <id> [--input <JSON> | --file <path|->]
|
|
42
|
+
`;
|
|
43
|
+
|
|
44
|
+
function isCanvasCommand(args) {
|
|
45
|
+
return args[0] === "canvas" && args[1] === "command";
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function parseCanvasCommandArgs(args) {
|
|
49
|
+
if (!isCanvasCommand(args)) throw new Error("仅支持 canvas command 命令");
|
|
50
|
+
const values = args.slice(2);
|
|
51
|
+
if (!values.length || values[0] === "--help" || values[0] === "-h") {
|
|
52
|
+
return { action: "help" };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const action = values[0];
|
|
56
|
+
if (!new Set(["list", "describe", "run"]).has(action)) {
|
|
57
|
+
throw new Error(`未知的 canvas command 子命令:${action}`);
|
|
58
|
+
}
|
|
59
|
+
let canvasId = "";
|
|
60
|
+
let commandName = "";
|
|
61
|
+
let filePath = "";
|
|
62
|
+
let input = "";
|
|
63
|
+
for (let index = 1; index < values.length; index += 1) {
|
|
64
|
+
const value = values[index];
|
|
65
|
+
if (value === "--help" || value === "-h") return { action: "help" };
|
|
66
|
+
if (value === "--canvas-id" || value === "--file" || value === "--input") {
|
|
67
|
+
const next = values[index + 1];
|
|
68
|
+
if (next === undefined || next.startsWith("--")) {
|
|
69
|
+
throw new Error(`参数 ${value} 缺少取值`);
|
|
70
|
+
}
|
|
71
|
+
if (value === "--canvas-id") canvasId = next.trim();
|
|
72
|
+
if (value === "--file") filePath = next;
|
|
73
|
+
if (value === "--input") input = next;
|
|
74
|
+
index += 1;
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
if (value.startsWith("--canvas-id=")) {
|
|
78
|
+
canvasId = value.slice("--canvas-id=".length).trim();
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
if (value.startsWith("--file=")) {
|
|
82
|
+
filePath = value.slice("--file=".length);
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
if (value.startsWith("--input=")) {
|
|
86
|
+
input = value.slice("--input=".length);
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
if (value.startsWith("-")) throw new Error(`未知参数:${value}`);
|
|
90
|
+
if (commandName) throw new Error(`多余的位置参数:${value}`);
|
|
91
|
+
commandName = value;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (action === "list") {
|
|
95
|
+
if (commandName || canvasId || filePath || input) throw new Error("canvas command list 不接受额外参数");
|
|
96
|
+
return { action };
|
|
97
|
+
}
|
|
98
|
+
if (!commandName) throw new Error(`canvas command ${action} 缺少 command 名称`);
|
|
99
|
+
if (action === "describe") {
|
|
100
|
+
if (canvasId || filePath || input) throw new Error("canvas command describe 不接受运行参数");
|
|
101
|
+
return { action, commandName };
|
|
102
|
+
}
|
|
103
|
+
if (!canvasId) throw new Error("canvas command run 缺少必填参数 --canvas-id");
|
|
104
|
+
if (input && filePath) throw new Error("--input 和 --file 不能同时使用");
|
|
105
|
+
return { action, canvasId, commandName, filePath, input };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function spawnProcess(invocation, args, options = {}) {
|
|
109
|
+
return new Promise((resolve, reject) => {
|
|
110
|
+
if (!invocation || !invocation.command) {
|
|
111
|
+
reject(new Error("原生 CLI 执行入口缺失"));
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
const child = spawn(invocation.command, [...(invocation.prefixArgs || []), ...args], {
|
|
115
|
+
cwd: options.cwd,
|
|
116
|
+
shell: false,
|
|
117
|
+
stdio: options.interactive ? "inherit" : ["pipe", "pipe", "pipe"],
|
|
118
|
+
windowsHide: true,
|
|
119
|
+
});
|
|
120
|
+
if (options.interactive) {
|
|
121
|
+
child.once("error", reject);
|
|
122
|
+
child.once("close", (exitCode, signal) => resolve({ exitCode, signal }));
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
let stdout = "";
|
|
126
|
+
let stderr = "";
|
|
127
|
+
child.stdout.setEncoding("utf8");
|
|
128
|
+
child.stderr.setEncoding("utf8");
|
|
129
|
+
child.stdout.on("data", (chunk) => {
|
|
130
|
+
stdout += chunk;
|
|
131
|
+
});
|
|
132
|
+
child.stderr.on("data", (chunk) => {
|
|
133
|
+
stderr += chunk;
|
|
134
|
+
});
|
|
135
|
+
child.once("error", reject);
|
|
136
|
+
child.once("close", (exitCode, signal) => resolve({ exitCode, signal, stderr, stdout }));
|
|
137
|
+
child.stdin.end(options.input);
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
class NativeCommandError extends Error {
|
|
142
|
+
constructor(args, result) {
|
|
143
|
+
const detail = result.stderr && result.stderr.trim();
|
|
144
|
+
const suffix = detail
|
|
145
|
+
? `:${detail}`
|
|
146
|
+
: result.signal
|
|
147
|
+
? `(信号 ${result.signal})`
|
|
148
|
+
: `(退出码 ${result.exitCode ?? "unknown"})`;
|
|
149
|
+
super(`原生 CLI 命令执行失败:${args.join(" ")}${suffix}`);
|
|
150
|
+
this.name = "NativeCommandError";
|
|
151
|
+
this.exitCode = result.exitCode;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function isAuthenticationFailure(error) {
|
|
156
|
+
return error instanceof NativeCommandError &&
|
|
157
|
+
(/\bHTTP\s+(401|403)\b/i.test(error.message) || /\bret\s*=\s*["']?1015["']?/i.test(error.message));
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
class NativeCanvasClient {
|
|
161
|
+
constructor(invocation, options = {}) {
|
|
162
|
+
this.invocation = invocation;
|
|
163
|
+
this.cwd = options.cwd;
|
|
164
|
+
this.stderr = options.stderr || process.stderr;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
async runJSON(args, input) {
|
|
168
|
+
const result = await spawnProcess(this.invocation, args, {
|
|
169
|
+
cwd: this.cwd,
|
|
170
|
+
input: input === undefined ? undefined : JSON.stringify(input),
|
|
171
|
+
});
|
|
172
|
+
if (result.exitCode !== 0) throw new NativeCommandError(args, result);
|
|
173
|
+
try {
|
|
174
|
+
return JSON.parse(result.stdout);
|
|
175
|
+
} catch (error) {
|
|
176
|
+
throw new Error(`原生 CLI 未返回有效 JSON(${args.join(" ")}):${error.message}`);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
async ensureAuthenticated() {
|
|
181
|
+
let status;
|
|
182
|
+
try {
|
|
183
|
+
status = await this.runJSON(["status"]);
|
|
184
|
+
} catch (_) {
|
|
185
|
+
status = undefined;
|
|
186
|
+
}
|
|
187
|
+
if (status?.logged_in === true) return status;
|
|
188
|
+
this.stderr.write("小云雀 CLI 尚未登录,正在打开网页授权…\n");
|
|
189
|
+
const result = await spawnProcess(this.invocation, ["login"], {
|
|
190
|
+
cwd: this.cwd,
|
|
191
|
+
interactive: true,
|
|
192
|
+
});
|
|
193
|
+
if (result.exitCode !== 0) throw new NativeCommandError(["login"], result);
|
|
194
|
+
status = await this.runJSON(["status"]);
|
|
195
|
+
if (status?.logged_in !== true) {
|
|
196
|
+
throw new Error("网页授权完成后仍未检测到有效的小云雀 CLI 登录态");
|
|
197
|
+
}
|
|
198
|
+
return status;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
async ensureCanvasAccess(canvasId) {
|
|
202
|
+
let status = await this.ensureAuthenticated();
|
|
203
|
+
try {
|
|
204
|
+
await this.getAssets([canvasId]);
|
|
205
|
+
return status;
|
|
206
|
+
} catch (error) {
|
|
207
|
+
if (!isAuthenticationFailure(error)) throw error;
|
|
208
|
+
}
|
|
209
|
+
this.stderr.write("小云雀 CLI 登录态已失效,正在重新打开网页授权…\n");
|
|
210
|
+
const result = await spawnProcess(this.invocation, ["login", "--force"], {
|
|
211
|
+
cwd: this.cwd,
|
|
212
|
+
interactive: true,
|
|
213
|
+
});
|
|
214
|
+
if (result.exitCode !== 0) throw new NativeCommandError(["login", "--force"], result);
|
|
215
|
+
status = await this.runJSON(["status"]);
|
|
216
|
+
if (status?.logged_in !== true) throw new Error("重新授权后仍未检测到有效的小云雀 CLI 登录态");
|
|
217
|
+
await this.getAssets([canvasId]);
|
|
218
|
+
return status;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
allocateAssetIds(count) {
|
|
222
|
+
return this.runJSON(["canvas", "allocate", "--count", String(count)]).then((result) => {
|
|
223
|
+
if (!Array.isArray(result.asset_ids) || result.asset_ids.length !== count) {
|
|
224
|
+
throw new Error(`申请画布资产 ID 失败:期望 ${count} 个,实际返回 ${result.asset_ids?.length ?? 0} 个`);
|
|
225
|
+
}
|
|
226
|
+
return result.asset_ids;
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
apply(request) {
|
|
231
|
+
if (!Array.isArray(request?.transactions) || request.transactions.length !== 1) {
|
|
232
|
+
throw new Error("画布同步一次只允许提交一个 transaction");
|
|
233
|
+
}
|
|
234
|
+
return this.runJSON(["canvas", "apply", "--transport-result", "--file", "-"], request);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
async getAssets(assetIds) {
|
|
238
|
+
if (!Array.isArray(assetIds) || assetIds.length === 0) return [];
|
|
239
|
+
const assets = [];
|
|
240
|
+
for (let offset = 0; offset < assetIds.length; offset += 50) {
|
|
241
|
+
const args = ["canvas", "get"];
|
|
242
|
+
for (const assetId of assetIds.slice(offset, offset + 50)) {
|
|
243
|
+
args.push("--asset-id", String(assetId));
|
|
244
|
+
}
|
|
245
|
+
const result = await this.runJSON(args);
|
|
246
|
+
if (!Array.isArray(result.assets)) throw new Error("画布资产查询结果缺少 assets");
|
|
247
|
+
assets.push(...result.assets);
|
|
248
|
+
}
|
|
249
|
+
return assets;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
async getAssetsAllowMissing(assetIds) {
|
|
253
|
+
try {
|
|
254
|
+
return await this.getAssets(assetIds);
|
|
255
|
+
} catch (error) {
|
|
256
|
+
if (!(error instanceof NativeCommandError) || !error.message.includes("did not return requested assets")) {
|
|
257
|
+
throw error;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
const assets = [];
|
|
261
|
+
for (const assetId of assetIds) {
|
|
262
|
+
try {
|
|
263
|
+
assets.push(...(await this.getAssets([assetId])));
|
|
264
|
+
} catch (error) {
|
|
265
|
+
if (!(error instanceof NativeCommandError) || !error.message.includes("did not return requested assets")) {
|
|
266
|
+
throw error;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
return assets;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function defaultStateDirectory() {
|
|
275
|
+
if (process.platform === "win32") {
|
|
276
|
+
return path.join(process.env.LOCALAPPDATA || path.join(os.homedir(), "AppData", "Local"), "pippit-tool-cli", "canvas-command");
|
|
277
|
+
}
|
|
278
|
+
if (process.platform === "darwin") {
|
|
279
|
+
return path.join(os.homedir(), "Library", "Application Support", "pippit-tool-cli", "canvas-command");
|
|
280
|
+
}
|
|
281
|
+
return path.join(process.env.XDG_STATE_HOME || path.join(os.homedir(), ".local", "state"), "pippit-tool-cli", "canvas-command");
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function emptyPersistenceState() {
|
|
285
|
+
return { clientId: null, outbound: [], snapshot: null, version: PERSISTENCE_VERSION };
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function canvasStateKey(credentialScope, canvasId) {
|
|
289
|
+
return createHash("sha256")
|
|
290
|
+
.update(JSON.stringify([String(credentialScope), String(canvasId)]))
|
|
291
|
+
.digest("hex");
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function validatePersistenceState(value, statePath) {
|
|
295
|
+
if (
|
|
296
|
+
!value || value.version !== PERSISTENCE_VERSION ||
|
|
297
|
+
(value.clientId !== null && typeof value.clientId !== "string") ||
|
|
298
|
+
!Array.isArray(value.outbound) ||
|
|
299
|
+
(value.snapshot !== null && typeof value.snapshot !== "object")
|
|
300
|
+
) {
|
|
301
|
+
throw new Error(`画布 command 本地恢复状态损坏:${statePath}`);
|
|
302
|
+
}
|
|
303
|
+
return value;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
async function atomicWriteJSON(statePath, value) {
|
|
307
|
+
const directory = path.dirname(statePath);
|
|
308
|
+
await fs.promises.mkdir(directory, { mode: 0o700, recursive: true });
|
|
309
|
+
const temporary = `${statePath}.${process.pid}.${randomBytes(8).toString("hex")}.tmp`;
|
|
310
|
+
let handle;
|
|
311
|
+
try {
|
|
312
|
+
handle = await fs.promises.open(temporary, "wx", 0o600);
|
|
313
|
+
await handle.writeFile(`${JSON.stringify(value)}\n`, "utf8");
|
|
314
|
+
await handle.sync();
|
|
315
|
+
await handle.close();
|
|
316
|
+
handle = undefined;
|
|
317
|
+
await fs.promises.rename(temporary, statePath);
|
|
318
|
+
} finally {
|
|
319
|
+
await handle?.close().catch(() => {});
|
|
320
|
+
await fs.promises.unlink(temporary).catch(() => {});
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function createFilePersistence({ canvasId, credentialScope, stateDirectory = defaultStateDirectory() }) {
|
|
325
|
+
if (!String(credentialScope || "").trim()) throw new Error("持久化画布状态缺少 credential_scope");
|
|
326
|
+
const stateKey = canvasStateKey(credentialScope, canvasId);
|
|
327
|
+
const statePath = path.join(stateDirectory, `${stateKey}.json`);
|
|
328
|
+
let tail = Promise.resolve();
|
|
329
|
+
const enqueue = (operation) => {
|
|
330
|
+
const result = tail.then(operation, operation);
|
|
331
|
+
tail = result.catch(() => {});
|
|
332
|
+
return result;
|
|
333
|
+
};
|
|
334
|
+
const load = async () => {
|
|
335
|
+
try {
|
|
336
|
+
return validatePersistenceState(JSON.parse(await fs.promises.readFile(statePath, "utf8")), statePath);
|
|
337
|
+
} catch (error) {
|
|
338
|
+
if (error?.code === "ENOENT") return emptyPersistenceState();
|
|
339
|
+
if (error instanceof SyntaxError) throw new Error(`画布 command 本地恢复状态损坏:${statePath}`);
|
|
340
|
+
throw error;
|
|
341
|
+
}
|
|
342
|
+
};
|
|
343
|
+
const read = (selector) => enqueue(async () => selector(await load()));
|
|
344
|
+
const update = (mutate) => enqueue(async () => {
|
|
345
|
+
const state = await load();
|
|
346
|
+
mutate(state);
|
|
347
|
+
await atomicWriteJSON(statePath, state);
|
|
348
|
+
});
|
|
349
|
+
return {
|
|
350
|
+
appendOutbound: (envelope) => update((state) => state.outbound.push(envelope)),
|
|
351
|
+
clear: () => update((state) => Object.assign(state, emptyPersistenceState())),
|
|
352
|
+
loadClientId: () => read((state) => state.clientId),
|
|
353
|
+
loadOutbound: () => read((state) => state.outbound),
|
|
354
|
+
loadSnapshot: () => read((state) => state.snapshot),
|
|
355
|
+
removeOutbound: (txId) => update((state) => {
|
|
356
|
+
state.outbound = state.outbound.filter((envelope) => envelope.txId !== txId);
|
|
357
|
+
}),
|
|
358
|
+
replaceOutbound: (envelopes) => update((state) => {
|
|
359
|
+
state.outbound = [...envelopes];
|
|
360
|
+
}),
|
|
361
|
+
replaceOutboundPartition: (active, capturedTxIds) => update((state) => {
|
|
362
|
+
const replaced = new Set([...capturedTxIds, ...active.map((envelope) => envelope.txId)]);
|
|
363
|
+
state.outbound = [...active, ...state.outbound.filter((envelope) => !replaced.has(envelope.txId))];
|
|
364
|
+
}),
|
|
365
|
+
saveClientId: (clientId) => update((state) => {
|
|
366
|
+
state.clientId = clientId;
|
|
367
|
+
}),
|
|
368
|
+
saveSnapshot: (snapshot) => update((state) => {
|
|
369
|
+
state.snapshot = snapshot;
|
|
370
|
+
}),
|
|
371
|
+
quarantine: () => enqueue(async () => {
|
|
372
|
+
const quarantinePath = `${statePath}.ambiguous.${Date.now()}.${randomBytes(6).toString("hex")}.json`;
|
|
373
|
+
try {
|
|
374
|
+
await fs.promises.rename(statePath, quarantinePath);
|
|
375
|
+
return quarantinePath;
|
|
376
|
+
} catch (error) {
|
|
377
|
+
if (error?.code === "ENOENT") return "";
|
|
378
|
+
throw error;
|
|
379
|
+
}
|
|
380
|
+
}),
|
|
381
|
+
statePath,
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function createFileCheckpointStore({ canvasId, credentialScope, stateDirectory = defaultStateDirectory() }) {
|
|
386
|
+
if (!String(credentialScope || "").trim()) throw new Error("持久化 checkpoint 缺少 credential_scope");
|
|
387
|
+
const statePath = path.join(stateDirectory, `${canvasStateKey(credentialScope, canvasId)}.checkpoints.json`);
|
|
388
|
+
let tail = Promise.resolve();
|
|
389
|
+
const enqueue = (operation) => {
|
|
390
|
+
const result = tail.then(operation, operation);
|
|
391
|
+
tail = result.catch(() => {});
|
|
392
|
+
return result;
|
|
393
|
+
};
|
|
394
|
+
const load = async () => {
|
|
395
|
+
try {
|
|
396
|
+
const value = JSON.parse(await fs.promises.readFile(statePath, "utf8"));
|
|
397
|
+
if (value?.version !== PERSISTENCE_VERSION || !Array.isArray(value.checkpoints)) {
|
|
398
|
+
throw new Error("invalid");
|
|
399
|
+
}
|
|
400
|
+
return value;
|
|
401
|
+
} catch (error) {
|
|
402
|
+
if (error?.code === "ENOENT") return { checkpoints: [], version: PERSISTENCE_VERSION };
|
|
403
|
+
throw new Error(`画布 command 本地 checkpoint 状态损坏:${statePath}`);
|
|
404
|
+
}
|
|
405
|
+
};
|
|
406
|
+
const copy = (value) => JSON.parse(JSON.stringify(value));
|
|
407
|
+
return {
|
|
408
|
+
create: (checkpoint) => enqueue(async () => {
|
|
409
|
+
const state = await load();
|
|
410
|
+
state.checkpoints = state.checkpoints.filter((value) => value.checkpointId !== checkpoint.checkpointId);
|
|
411
|
+
state.checkpoints.push(copy(checkpoint));
|
|
412
|
+
await atomicWriteJSON(statePath, state);
|
|
413
|
+
}),
|
|
414
|
+
get: (checkpointId) => enqueue(async () => {
|
|
415
|
+
const checkpoint = (await load()).checkpoints.find((value) => value.checkpointId === checkpointId);
|
|
416
|
+
return checkpoint ? copy(checkpoint) : undefined;
|
|
417
|
+
}),
|
|
418
|
+
list: (targetCanvasId) => enqueue(async () => copy(
|
|
419
|
+
(await load()).checkpoints.filter((value) => value.canvasId === targetCanvasId)
|
|
420
|
+
)),
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
function createMemoryPersistence() {
|
|
425
|
+
let state = emptyPersistenceState();
|
|
426
|
+
const copy = (value) => JSON.parse(JSON.stringify(value));
|
|
427
|
+
return {
|
|
428
|
+
async appendOutbound(envelope) { state.outbound.push(copy(envelope)); },
|
|
429
|
+
async clear() { state = emptyPersistenceState(); },
|
|
430
|
+
async loadClientId() { return state.clientId; },
|
|
431
|
+
async loadOutbound() { return copy(state.outbound); },
|
|
432
|
+
async loadSnapshot() { return copy(state.snapshot); },
|
|
433
|
+
async removeOutbound(txId) { state.outbound = state.outbound.filter((value) => value.txId !== txId); },
|
|
434
|
+
async replaceOutbound(envelopes) { state.outbound = copy(envelopes); },
|
|
435
|
+
async replaceOutboundPartition(active, capturedTxIds) {
|
|
436
|
+
const replaced = new Set([...capturedTxIds, ...active.map((value) => value.txId)]);
|
|
437
|
+
state.outbound = [...copy(active), ...state.outbound.filter((value) => !replaced.has(value.txId))];
|
|
438
|
+
},
|
|
439
|
+
async saveClientId(clientId) { state.clientId = clientId; },
|
|
440
|
+
async saveSnapshot(snapshot) { state.snapshot = copy(snapshot); },
|
|
441
|
+
async quarantine() { return ""; },
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
function requestFetch(url, init = {}, redirects = 0) {
|
|
446
|
+
return new Promise((resolve, reject) => {
|
|
447
|
+
const parsed = new URL(url);
|
|
448
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
449
|
+
reject(new Error(`不支持的文本资产下载协议:${parsed.protocol}`));
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
452
|
+
const request = (parsed.protocol === "https:" ? https : http).get(parsed, (response) => {
|
|
453
|
+
if (response.statusCode >= 300 && response.statusCode < 400 && response.headers.location) {
|
|
454
|
+
response.resume();
|
|
455
|
+
if (redirects >= 3) return reject(new Error("文本资产下载重定向次数过多"));
|
|
456
|
+
return resolve(requestFetch(new URL(response.headers.location, parsed).toString(), init, redirects + 1));
|
|
457
|
+
}
|
|
458
|
+
const chunks = [];
|
|
459
|
+
let bytes = 0;
|
|
460
|
+
response.on("data", (chunk) => {
|
|
461
|
+
bytes += chunk.length;
|
|
462
|
+
if (bytes > MAX_INPUT_BYTES) {
|
|
463
|
+
request.destroy(new Error("文本资产超过 64 MiB 限制"));
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
466
|
+
chunks.push(chunk);
|
|
467
|
+
});
|
|
468
|
+
response.on("end", () => {
|
|
469
|
+
const body = Buffer.concat(chunks).toString("utf8");
|
|
470
|
+
resolve({
|
|
471
|
+
ok: response.statusCode >= 200 && response.statusCode < 300,
|
|
472
|
+
status: response.statusCode,
|
|
473
|
+
statusText: response.statusMessage || "",
|
|
474
|
+
text: async () => body,
|
|
475
|
+
});
|
|
476
|
+
});
|
|
477
|
+
});
|
|
478
|
+
if (init.signal) {
|
|
479
|
+
if (init.signal.aborted) request.destroy(new Error("文本资产下载已取消"));
|
|
480
|
+
else init.signal.addEventListener("abort", () => request.destroy(new Error("文本资产下载已取消")), { once: true });
|
|
481
|
+
}
|
|
482
|
+
request.setTimeout(30_000, () => request.destroy(new Error("文本资产下载超时")));
|
|
483
|
+
request.once("error", reject);
|
|
484
|
+
});
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
function createCanvasAssetRuntime({ nativeClient, sdk }) {
|
|
488
|
+
const serviceTransport = sdk.createPippitAssetServiceTransport({
|
|
489
|
+
assetQuery: async (request) => ({
|
|
490
|
+
data: { Assets: await nativeClient.getAssetsAllowMissing(request.pippit_asset_ids) },
|
|
491
|
+
ret: 0,
|
|
492
|
+
}),
|
|
493
|
+
mutation: {
|
|
494
|
+
batchGeneratePippitAssetIds: async ({ count = 0 }) => ({
|
|
495
|
+
data: { ids: await nativeClient.allocateAssetIds(count) },
|
|
496
|
+
ret: 0,
|
|
497
|
+
}),
|
|
498
|
+
batchPatchAsset: (request) => nativeClient.apply(request),
|
|
499
|
+
},
|
|
500
|
+
});
|
|
501
|
+
const assetRuntime = sdk.createPippitAssetRuntime({
|
|
502
|
+
storage: false,
|
|
503
|
+
sync: { batchPatchAsset: (request) => nativeClient.apply(request) },
|
|
504
|
+
transport: serviceTransport,
|
|
505
|
+
});
|
|
506
|
+
const loader = sdk.createPippitAssetSdkCanvasLoader({ client: assetRuntime.client });
|
|
507
|
+
return { assetRuntime, loader };
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
function createCanvasTransportFactory({ assetRuntime, loader }) {
|
|
511
|
+
return ({ canvasId, clientId }) => {
|
|
512
|
+
const transport = assetRuntime.createSyncTransport({
|
|
513
|
+
clientId,
|
|
514
|
+
loader,
|
|
515
|
+
scopeAssetId: canvasId,
|
|
516
|
+
});
|
|
517
|
+
return {
|
|
518
|
+
assetVersions: transport.assetVersions,
|
|
519
|
+
clientId: transport.clientId,
|
|
520
|
+
close: transport.close.bind(transport),
|
|
521
|
+
commit: transport.commit.bind(transport),
|
|
522
|
+
fetchAssets: transport.fetchAssets.bind(transport),
|
|
523
|
+
fetchSnapshot: async (options) => {
|
|
524
|
+
const snapshot = await transport.fetchSnapshot(options);
|
|
525
|
+
return { document: snapshot.state };
|
|
526
|
+
},
|
|
527
|
+
getConnectionState: transport.getConnectionState.bind(transport),
|
|
528
|
+
onConnectionChange: transport.onConnectionChange.bind(transport),
|
|
529
|
+
onInvalidate: transport.onInvalidate.bind(transport),
|
|
530
|
+
syncAssetSubscriptions: transport.syncAssetSubscriptions.bind(transport),
|
|
531
|
+
};
|
|
532
|
+
};
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
function createSchemaValue(json, optional = false) {
|
|
536
|
+
const schema = { ...json };
|
|
537
|
+
Object.defineProperty(schema, OPTIONAL_SCHEMA, { value: optional });
|
|
538
|
+
Object.defineProperties(schema, {
|
|
539
|
+
describe: {
|
|
540
|
+
value(description) {
|
|
541
|
+
return createSchemaValue({ ...json, description }, optional);
|
|
542
|
+
},
|
|
543
|
+
},
|
|
544
|
+
optional: {
|
|
545
|
+
value() {
|
|
546
|
+
return createSchemaValue(json, true);
|
|
547
|
+
},
|
|
548
|
+
},
|
|
549
|
+
});
|
|
550
|
+
return schema;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
function createSchemaFactory() {
|
|
554
|
+
return {
|
|
555
|
+
array: (item) => createSchemaValue({ items: schemaToJSON(item), type: "array" }),
|
|
556
|
+
boolean: () => createSchemaValue({ type: "boolean" }),
|
|
557
|
+
number: () => createSchemaValue({ type: "number" }),
|
|
558
|
+
string: () => createSchemaValue({ type: "string" }),
|
|
559
|
+
unknown: () => createSchemaValue({}),
|
|
560
|
+
};
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
function schemaToJSON(schema) {
|
|
564
|
+
return Object.fromEntries(Object.entries(schema || {}).filter(([, value]) => typeof value !== "function"));
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
function definitionToJSON(name, definition) {
|
|
568
|
+
const properties = {};
|
|
569
|
+
const required = [];
|
|
570
|
+
for (const [argument, schema] of Object.entries(definition.args || {})) {
|
|
571
|
+
properties[argument] = schemaToJSON(schema);
|
|
572
|
+
if (!schema[OPTIONAL_SCHEMA]) required.push(argument);
|
|
573
|
+
}
|
|
574
|
+
return {
|
|
575
|
+
description: definition.description,
|
|
576
|
+
input_schema: {
|
|
577
|
+
properties,
|
|
578
|
+
...(required.length ? { required } : {}),
|
|
579
|
+
type: "object",
|
|
580
|
+
},
|
|
581
|
+
name,
|
|
582
|
+
};
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
function createDefinitions(sdk, runtime, allocateNodeId) {
|
|
586
|
+
const definitions = sdk.createXyqCanvasOpencodeToolDefinitions({
|
|
587
|
+
allocateNodeId,
|
|
588
|
+
runtime,
|
|
589
|
+
schema: createSchemaFactory(),
|
|
590
|
+
});
|
|
591
|
+
for (const [name, definition] of Object.entries(definitions || {})) {
|
|
592
|
+
if (!definition || typeof definition.description !== "string" || typeof definition.execute !== "function") {
|
|
593
|
+
throw new Error(`画布 SDK 返回了无效的 command 定义:${name}`);
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
return definitions;
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
function ensureNodeRuntimeGlobals() {
|
|
600
|
+
if (typeof globalThis.structuredClone !== "function") {
|
|
601
|
+
const { deserialize, serialize } = require("v8");
|
|
602
|
+
Object.defineProperty(globalThis, "structuredClone", {
|
|
603
|
+
configurable: true,
|
|
604
|
+
value: (value) => deserialize(serialize(value)),
|
|
605
|
+
writable: true,
|
|
606
|
+
});
|
|
607
|
+
}
|
|
608
|
+
if (typeof globalThis.fetch !== "function") {
|
|
609
|
+
Object.defineProperty(globalThis, "fetch", {
|
|
610
|
+
configurable: true,
|
|
611
|
+
value: requestFetch,
|
|
612
|
+
writable: true,
|
|
613
|
+
});
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
async function loadSdk(options) {
|
|
618
|
+
ensureNodeRuntimeGlobals();
|
|
619
|
+
if (options.sdk) return options.sdk;
|
|
620
|
+
if (!fs.existsSync(DEFAULT_SDK_MODULE)) {
|
|
621
|
+
throw new Error("当前 CLI 安装包缺少画布 command 运行时,请更新到包含该能力的正式版本");
|
|
622
|
+
}
|
|
623
|
+
const imported = await import(pathToFileURL(DEFAULT_SDK_MODULE).href);
|
|
624
|
+
return imported.default && typeof imported.default === "object"
|
|
625
|
+
? { ...imported.default, ...imported }
|
|
626
|
+
: imported;
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
function assertSdkFunctions(sdk, names) {
|
|
630
|
+
for (const name of names) {
|
|
631
|
+
if (typeof sdk[name] !== "function") throw new Error(`画布 SDK 产物缺少 ${name}`);
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
function parseInputJSON(parsed, cwd) {
|
|
636
|
+
let payload = parsed.input;
|
|
637
|
+
if (parsed.filePath) {
|
|
638
|
+
if (parsed.filePath === "-") {
|
|
639
|
+
payload = fs.readFileSync(0, { encoding: "utf8" });
|
|
640
|
+
} else {
|
|
641
|
+
const inputPath = path.resolve(cwd, parsed.filePath);
|
|
642
|
+
const stat = fs.statSync(inputPath);
|
|
643
|
+
if (!stat.isFile()) throw new Error(`command 输入不是普通文件:${inputPath}`);
|
|
644
|
+
if (stat.size > MAX_INPUT_BYTES) throw new Error("command 输入超过 64 MiB 限制");
|
|
645
|
+
payload = fs.readFileSync(inputPath, "utf8");
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
if (!payload) return {};
|
|
649
|
+
if (Buffer.byteLength(payload) > MAX_INPUT_BYTES) throw new Error("command 输入超过 64 MiB 限制");
|
|
650
|
+
let value;
|
|
651
|
+
try {
|
|
652
|
+
value = JSON.parse(payload);
|
|
653
|
+
} catch (error) {
|
|
654
|
+
throw new Error(`command 输入不是有效 JSON:${error.message}`);
|
|
655
|
+
}
|
|
656
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
657
|
+
throw new Error("command 输入必须是 JSON object");
|
|
658
|
+
}
|
|
659
|
+
return value;
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
function writeJSON(stream, value) {
|
|
663
|
+
stream.write(`${JSON.stringify(value, null, 2)}\n`);
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
function decorateCatalogEntry(sdk, entry) {
|
|
667
|
+
const output = { ...entry };
|
|
668
|
+
if (entry.name === "apply_mutations") {
|
|
669
|
+
output.mutation_definitions = sdk.XYQ_CANVAS_OPENCODE_MUTATION_DEFINITIONS;
|
|
670
|
+
}
|
|
671
|
+
if (entry.name === "apply_mutations" || entry.name === "invoke_command") {
|
|
672
|
+
output.registered_commands = sdk.XYQ_CANVAS_REGISTERED_COMMAND_DEFINITIONS;
|
|
673
|
+
}
|
|
674
|
+
return output;
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
function createPublicCatalog(sdk, definitions) {
|
|
678
|
+
const entries = [];
|
|
679
|
+
const routes = new Map();
|
|
680
|
+
const append = (entry, route) => {
|
|
681
|
+
if (routes.has(entry.name)) return;
|
|
682
|
+
entries.push(decorateCatalogEntry(sdk, entry));
|
|
683
|
+
routes.set(entry.name, route);
|
|
684
|
+
};
|
|
685
|
+
for (const [name, definition] of Object.entries(definitions)) {
|
|
686
|
+
append(definitionToJSON(name, definition), { kind: "tool", name });
|
|
687
|
+
}
|
|
688
|
+
for (const definition of sdk.XYQ_CANVAS_OPENCODE_MUTATION_DEFINITIONS) {
|
|
689
|
+
append({
|
|
690
|
+
description: definition.description,
|
|
691
|
+
input_schema: { description: definition.input, type: "object" },
|
|
692
|
+
name: definition.kind,
|
|
693
|
+
}, { kind: "mutation", name: definition.kind });
|
|
694
|
+
}
|
|
695
|
+
for (const definition of sdk.XYQ_CANVAS_REGISTERED_COMMAND_DEFINITIONS) {
|
|
696
|
+
append({
|
|
697
|
+
description: definition.description,
|
|
698
|
+
input_schema: { description: definition.input, type: "object" },
|
|
699
|
+
name: definition.name,
|
|
700
|
+
}, { kind: "registered", name: definition.name });
|
|
701
|
+
}
|
|
702
|
+
return { entries, routes };
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
function validatePublicCommandInput(route, input) {
|
|
706
|
+
if (
|
|
707
|
+
route.kind === "tool" &&
|
|
708
|
+
route.name === "apply_mutations" &&
|
|
709
|
+
Array.isArray(input.mutations) &&
|
|
710
|
+
input.mutations.length > 1 &&
|
|
711
|
+
input.mutations.some((mutation) => mutation?.kind === "invoke_command")
|
|
712
|
+
) {
|
|
713
|
+
throw new Error("已注册业务 command 不能与其他 mutation 放在同一原子批次中,请单独执行");
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
function executePublicCommand(route, definitions, input) {
|
|
718
|
+
if (route.kind === "tool") {
|
|
719
|
+
return definitions[route.name].execute(
|
|
720
|
+
route.name === "apply_mutations" ? { ...input, atomic: true } : input
|
|
721
|
+
);
|
|
722
|
+
}
|
|
723
|
+
const mutation = route.kind === "mutation"
|
|
724
|
+
? { ...input, kind: route.name }
|
|
725
|
+
: { args: [input], kind: "invoke_command", name: route.name };
|
|
726
|
+
return definitions.apply_mutations.execute({
|
|
727
|
+
atomic: true,
|
|
728
|
+
intent: `canvas command ${route.name}`,
|
|
729
|
+
mutations: [mutation],
|
|
730
|
+
});
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
async function runCanvasCommand(args, options = {}) {
|
|
734
|
+
const parsed = parseCanvasCommandArgs(args);
|
|
735
|
+
const stdout = options.stdout || process.stdout;
|
|
736
|
+
const stderr = options.stderr || process.stderr;
|
|
737
|
+
if (parsed.action === "help") {
|
|
738
|
+
stdout.write(COMMAND_HELP);
|
|
739
|
+
return 0;
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
const sdk = await loadSdk(options);
|
|
743
|
+
assertSdkFunctions(sdk, ["createXyqCanvasOpencodeToolDefinitions"]);
|
|
744
|
+
if (!Array.isArray(sdk.XYQ_CANVAS_OPENCODE_MUTATION_DEFINITIONS)) {
|
|
745
|
+
throw new Error("画布 SDK 产物缺少 mutation command 目录");
|
|
746
|
+
}
|
|
747
|
+
if (!Array.isArray(sdk.XYQ_CANVAS_REGISTERED_COMMAND_DEFINITIONS)) {
|
|
748
|
+
throw new Error("画布 SDK 产物缺少已注册业务 command 目录");
|
|
749
|
+
}
|
|
750
|
+
const catalogDefinitions = createDefinitions(
|
|
751
|
+
sdk,
|
|
752
|
+
{ permissions: [], store: {} },
|
|
753
|
+
async () => ""
|
|
754
|
+
);
|
|
755
|
+
const { entries: catalog, routes } = createPublicCatalog(sdk, catalogDefinitions);
|
|
756
|
+
if (parsed.action === "list") {
|
|
757
|
+
writeJSON(stdout, { commands: catalog });
|
|
758
|
+
return 0;
|
|
759
|
+
}
|
|
760
|
+
const catalogEntry = catalog.find((entry) => entry.name === parsed.commandName);
|
|
761
|
+
if (!catalogEntry) throw new Error(`未知的画布 command:${parsed.commandName}`);
|
|
762
|
+
if (parsed.action === "describe") {
|
|
763
|
+
writeJSON(stdout, catalogEntry);
|
|
764
|
+
return 0;
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
assertSdkFunctions(sdk, [
|
|
768
|
+
"createPippitAssetRuntime",
|
|
769
|
+
"createPippitAssetSdkCanvasLoader",
|
|
770
|
+
"createPippitAssetServiceTransport",
|
|
771
|
+
"createXyqCanvasCommandRuntime",
|
|
772
|
+
]);
|
|
773
|
+
const cwd = options.cwd || process.cwd();
|
|
774
|
+
const input = parseInputJSON(parsed, cwd);
|
|
775
|
+
const route = routes.get(parsed.commandName);
|
|
776
|
+
validatePublicCommandInput(route, input);
|
|
777
|
+
const nativeClient = new NativeCanvasClient(options.nativeInvocation, { cwd, stderr });
|
|
778
|
+
const authStatus = await nativeClient.ensureCanvasAccess(parsed.canvasId);
|
|
779
|
+
const requiresDurableCheckpoint = CHECKPOINT_COMMANDS.has(parsed.commandName) ||
|
|
780
|
+
(parsed.commandName === "apply_mutations" && input.checkpointBefore === true);
|
|
781
|
+
if (requiresDurableCheckpoint && !authStatus.credential_scope) {
|
|
782
|
+
throw new Error("checkpoint command 需要通过网页登录,以便安全隔离跨进程状态");
|
|
783
|
+
}
|
|
784
|
+
const { assetRuntime, loader } = createCanvasAssetRuntime({ nativeClient, sdk });
|
|
785
|
+
const checkpointStore = authStatus.credential_scope
|
|
786
|
+
? createFileCheckpointStore({
|
|
787
|
+
canvasId: parsed.canvasId,
|
|
788
|
+
credentialScope: authStatus.credential_scope,
|
|
789
|
+
stateDirectory: options.stateDirectory,
|
|
790
|
+
})
|
|
791
|
+
: undefined;
|
|
792
|
+
|
|
793
|
+
let executionError;
|
|
794
|
+
let saveError;
|
|
795
|
+
let serializedResult;
|
|
796
|
+
const allocatedAssetIds = [];
|
|
797
|
+
let standalone;
|
|
798
|
+
const persistence = authStatus.credential_scope
|
|
799
|
+
? createFilePersistence({
|
|
800
|
+
canvasId: parsed.canvasId,
|
|
801
|
+
credentialScope: authStatus.credential_scope,
|
|
802
|
+
stateDirectory: options.stateDirectory,
|
|
803
|
+
})
|
|
804
|
+
: createMemoryPersistence();
|
|
805
|
+
try {
|
|
806
|
+
standalone = sdk.createXyqCanvasCommandRuntime({
|
|
807
|
+
canvasId: parsed.canvasId,
|
|
808
|
+
persistence,
|
|
809
|
+
sync: { flush: { maxAttempts: 1, maxBatchSize: 1 } },
|
|
810
|
+
transportFactory: createCanvasTransportFactory({ assetRuntime, loader }),
|
|
811
|
+
});
|
|
812
|
+
if (!standalone?.canvas || !standalone.store || !standalone.commands || !standalone.runtime) {
|
|
813
|
+
throw new Error("画布 SDK 返回了不完整的 command 运行时");
|
|
814
|
+
}
|
|
815
|
+
standalone.canvas.start();
|
|
816
|
+
await standalone.canvas.whenReady();
|
|
817
|
+
const definitions = createDefinitions(
|
|
818
|
+
sdk,
|
|
819
|
+
{ checkpoints: checkpointStore, permissions: CANVAS_COMMAND_PERMISSIONS, store: standalone.store },
|
|
820
|
+
async () => {
|
|
821
|
+
const [assetId] = await assetRuntime.client.ids.allocate(1);
|
|
822
|
+
allocatedAssetIds.push(assetId);
|
|
823
|
+
return assetId;
|
|
824
|
+
}
|
|
825
|
+
);
|
|
826
|
+
try {
|
|
827
|
+
serializedResult = await executePublicCommand(route, definitions, input);
|
|
828
|
+
} catch (error) {
|
|
829
|
+
executionError = error;
|
|
830
|
+
}
|
|
831
|
+
if (!executionError) {
|
|
832
|
+
try {
|
|
833
|
+
standalone.canvas.flush();
|
|
834
|
+
await standalone.canvas.waitUntilSaved();
|
|
835
|
+
} catch (error) {
|
|
836
|
+
saveError = error;
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
} finally {
|
|
840
|
+
standalone?.canvas?.dispose();
|
|
841
|
+
assetRuntime.dispose();
|
|
842
|
+
}
|
|
843
|
+
if (executionError) {
|
|
844
|
+
const quarantinePath = await persistence.quarantine();
|
|
845
|
+
const suffix = quarantinePath ? `;未确认事务已隔离到 ${quarantinePath}` : "";
|
|
846
|
+
throw new Error(`${executionError.message}${suffix}。请先查询画布状态,不要直接重跑该 command。`);
|
|
847
|
+
}
|
|
848
|
+
if (saveError) {
|
|
849
|
+
const quarantinePath = await persistence.quarantine();
|
|
850
|
+
const suffix = quarantinePath ? `;未确认事务已隔离到 ${quarantinePath}` : "";
|
|
851
|
+
throw new Error(`${saveError.message}${suffix}。请先查询画布状态,不要直接重跑该 command。`);
|
|
852
|
+
}
|
|
853
|
+
if (typeof serializedResult !== "string") throw new Error("画布 command 未返回 JSON 字符串");
|
|
854
|
+
let result;
|
|
855
|
+
try {
|
|
856
|
+
result = JSON.parse(serializedResult);
|
|
857
|
+
} catch (error) {
|
|
858
|
+
throw new Error(`画布 command 返回了无效 JSON:${error.message}`);
|
|
859
|
+
}
|
|
860
|
+
if (result?.ok === true && allocatedAssetIds.length) {
|
|
861
|
+
result = { ...result, allocated_asset_ids: allocatedAssetIds };
|
|
862
|
+
}
|
|
863
|
+
writeJSON(stdout, result);
|
|
864
|
+
return result?.ok === false ? 1 : 0;
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
module.exports = {
|
|
868
|
+
COMMAND_HELP,
|
|
869
|
+
NativeCanvasClient,
|
|
870
|
+
createCanvasAssetRuntime,
|
|
871
|
+
createCanvasTransportFactory,
|
|
872
|
+
createFileCheckpointStore,
|
|
873
|
+
createFilePersistence,
|
|
874
|
+
createMemoryPersistence,
|
|
875
|
+
createSchemaFactory,
|
|
876
|
+
definitionToJSON,
|
|
877
|
+
ensureNodeRuntimeGlobals,
|
|
878
|
+
isCanvasCommand,
|
|
879
|
+
parseCanvasCommandArgs,
|
|
880
|
+
runCanvasCommand,
|
|
881
|
+
};
|