@world-engines/project-host 0.1.0-alpha.0

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 (218) hide show
  1. package/LICENSE +46 -0
  2. package/assets/gui/WELogo-title.svg +11 -0
  3. package/assets/gui/app.js +1326 -0
  4. package/assets/gui/index.html +118 -0
  5. package/assets/gui/local-preview-asset-urls.js +61 -0
  6. package/assets/gui/local-preview-bridge.js +393 -0
  7. package/assets/gui/spatial-template-panel.js +233 -0
  8. package/assets/gui/styles.css +115 -0
  9. package/assets/gui/terrain-render.js +120 -0
  10. package/assets/gui/workspace-status.css +17 -0
  11. package/assets/gui/workspace-status.js +255 -0
  12. package/assets/gui/world-map-globe.js +187 -0
  13. package/assets/gui/world-map-worker.js +62 -0
  14. package/assets/gui/world-map.css +30 -0
  15. package/assets/gui/world-map.js +2466 -0
  16. package/dist/account-model-proxy.d.ts +21 -0
  17. package/dist/account-model-proxy.js +470 -0
  18. package/dist/authoring-browser-runtime.d.ts +7 -0
  19. package/dist/authoring-browser-runtime.js +110 -0
  20. package/dist/authoring-ingestion.d.ts +38 -0
  21. package/dist/authoring-ingestion.js +410 -0
  22. package/dist/authoring-model-connection.d.ts +166 -0
  23. package/dist/authoring-model-connection.js +757 -0
  24. package/dist/chinese-lexical.d.ts +152 -0
  25. package/dist/chinese-lexical.js +555 -0
  26. package/dist/chromium-preview-browser-driver.d.ts +43 -0
  27. package/dist/chromium-preview-browser-driver.js +575 -0
  28. package/dist/cli/download.d.ts +12 -0
  29. package/dist/cli/download.js +50 -0
  30. package/dist/cli/dump.d.ts +32 -0
  31. package/dist/cli/dump.js +91 -0
  32. package/dist/cli/ingest.d.ts +49 -0
  33. package/dist/cli/ingest.js +225 -0
  34. package/dist/cli/review.d.ts +10 -0
  35. package/dist/cli/review.js +44 -0
  36. package/dist/cli/upload.d.ts +57 -0
  37. package/dist/cli/upload.js +112 -0
  38. package/dist/cli-command.d.ts +60 -0
  39. package/dist/cli-command.js +613 -0
  40. package/dist/cli.d.ts +2 -0
  41. package/dist/cli.js +182 -0
  42. package/dist/desktop-authoring-runtime.d.ts +88 -0
  43. package/dist/desktop-authoring-runtime.js +1602 -0
  44. package/dist/desktop-sidecar.d.ts +11 -0
  45. package/dist/desktop-sidecar.js +143 -0
  46. package/dist/document-dump.d.ts +113 -0
  47. package/dist/document-dump.js +431 -0
  48. package/dist/document-graph-extractor.d.ts +125 -0
  49. package/dist/document-graph-extractor.js +588 -0
  50. package/dist/document-ingestion-journal.d.ts +68 -0
  51. package/dist/document-ingestion-journal.js +471 -0
  52. package/dist/document-ingestion.d.ts +183 -0
  53. package/dist/document-ingestion.js +976 -0
  54. package/dist/document-query-path-proof.d.ts +30 -0
  55. package/dist/document-query-path-proof.js +34 -0
  56. package/dist/document-query-path-renderer.d.ts +50 -0
  57. package/dist/document-query-path-renderer.js +75 -0
  58. package/dist/document-query-proof-paths.d.ts +22 -0
  59. package/dist/document-query-proof-paths.js +71 -0
  60. package/dist/document-query.d.ts +97 -0
  61. package/dist/document-query.js +1106 -0
  62. package/dist/document-relation-role.d.ts +14 -0
  63. package/dist/document-relation-role.js +86 -0
  64. package/dist/document-source.d.ts +107 -0
  65. package/dist/document-source.js +255 -0
  66. package/dist/embedding-cache.d.ts +35 -0
  67. package/dist/embedding-cache.js +317 -0
  68. package/dist/frozen-review-local-pipeline.d.ts +88 -0
  69. package/dist/frozen-review-local-pipeline.js +609 -0
  70. package/dist/frozen-review-source-capture.d.ts +15 -0
  71. package/dist/frozen-review-source-capture.js +551 -0
  72. package/dist/frozen-review-wasm-encryptor.d.ts +16 -0
  73. package/dist/frozen-review-wasm-encryptor.js +72 -0
  74. package/dist/frozen-review.d.ts +131 -0
  75. package/dist/frozen-review.js +802 -0
  76. package/dist/graph-traversal.d.ts +23 -0
  77. package/dist/graph-traversal.js +100 -0
  78. package/dist/gui/graph-layout-worker.js +1 -0
  79. package/dist/gui/graph-layout.wasm +0 -0
  80. package/dist/gui/graph.css +1 -0
  81. package/dist/gui/graph.js +76 -0
  82. package/dist/gui/plane-generation.js +112 -0
  83. package/dist/gui/plane-generator.js +310 -0
  84. package/dist/gui/plane-generator_bg.wasm +0 -0
  85. package/dist/gui/procedural-city.js +595 -0
  86. package/dist/gui/road-generation.js +129 -0
  87. package/dist/gui/road-parcels.js +1584 -0
  88. package/dist/gui/score.css +1 -0
  89. package/dist/gui/score.js +54 -0
  90. package/dist/gui/spatial-runtime.js +1231 -0
  91. package/dist/gui/spatial-templates.js +200 -0
  92. package/dist/gui/trigger.css +1 -0
  93. package/dist/gui/trigger.js +142 -0
  94. package/dist/harness-callback-transport.d.ts +17 -0
  95. package/dist/harness-callback-transport.js +128 -0
  96. package/dist/harness-document-ingestion.d.ts +77 -0
  97. package/dist/harness-document-ingestion.js +343 -0
  98. package/dist/index.d.ts +43 -0
  99. package/dist/index.js +43 -0
  100. package/dist/ingestion-source-reconciliation.d.ts +88 -0
  101. package/dist/ingestion-source-reconciliation.js +253 -0
  102. package/dist/ladybug-runtime.d.ts +117 -0
  103. package/dist/ladybug-runtime.js +886 -0
  104. package/dist/local-account-embedding.d.ts +34 -0
  105. package/dist/local-account-embedding.js +294 -0
  106. package/dist/local-gallery.d.ts +35 -0
  107. package/dist/local-gallery.js +227 -0
  108. package/dist/local-gui-auth.d.ts +96 -0
  109. package/dist/local-gui-auth.js +703 -0
  110. package/dist/local-gui-contract.d.ts +53 -0
  111. package/dist/local-gui-contract.js +5 -0
  112. package/dist/local-gui-image-assets.d.ts +17 -0
  113. package/dist/local-gui-image-assets.js +96 -0
  114. package/dist/local-gui-preview-client.d.ts +3 -0
  115. package/dist/local-gui-preview-client.js +111 -0
  116. package/dist/local-gui-score.d.ts +72 -0
  117. package/dist/local-gui-score.js +184 -0
  118. package/dist/local-gui-server.d.ts +10 -0
  119. package/dist/local-gui-server.js +987 -0
  120. package/dist/local-gui-version.d.ts +17 -0
  121. package/dist/local-gui-version.js +147 -0
  122. package/dist/local-gui-workspace.d.ts +14 -0
  123. package/dist/local-gui-workspace.js +171 -0
  124. package/dist/local-model-settings.d.ts +28 -0
  125. package/dist/local-model-settings.js +124 -0
  126. package/dist/local-preview-assets.d.ts +28 -0
  127. package/dist/local-preview-assets.js +124 -0
  128. package/dist/local-preview-bound-resources.d.ts +12 -0
  129. package/dist/local-preview-bound-resources.js +80 -0
  130. package/dist/local-preview-message-store.d.ts +63 -0
  131. package/dist/local-preview-message-store.js +214 -0
  132. package/dist/local-preview-player-read-models.d.ts +47 -0
  133. package/dist/local-preview-player-read-models.js +90 -0
  134. package/dist/local-preview-projections.d.ts +61 -0
  135. package/dist/local-preview-projections.js +117 -0
  136. package/dist/local-preview-runtime.d.ts +105 -0
  137. package/dist/local-preview-runtime.js +553 -0
  138. package/dist/local-preview-session.d.ts +73 -0
  139. package/dist/local-preview-session.js +395 -0
  140. package/dist/local-preview-source.d.ts +40 -0
  141. package/dist/local-preview-source.js +192 -0
  142. package/dist/local-preview-view-events.d.ts +101 -0
  143. package/dist/local-preview-view-events.js +179 -0
  144. package/dist/local-preview-view-state-sqlite.d.ts +24 -0
  145. package/dist/local-preview-view-state-sqlite.js +263 -0
  146. package/dist/local-preview-view-state.d.ts +93 -0
  147. package/dist/local-preview-view-state.js +108 -0
  148. package/dist/local-preview-vite-config.d.ts +6 -0
  149. package/dist/local-preview-vite-config.js +122 -0
  150. package/dist/local-preview-world-effects.d.ts +9 -0
  151. package/dist/local-preview-world-effects.js +55 -0
  152. package/dist/local-preview-worldline-store.d.ts +47 -0
  153. package/dist/local-preview-worldline-store.js +282 -0
  154. package/dist/local-preview-worldlines.d.ts +95 -0
  155. package/dist/local-preview-worldlines.js +136 -0
  156. package/dist/mcp-async-dispatcher.d.ts +3 -0
  157. package/dist/mcp-async-dispatcher.js +129 -0
  158. package/dist/mcp-line-queue.d.ts +14 -0
  159. package/dist/mcp-line-queue.js +184 -0
  160. package/dist/mcp-pipe-internal.d.ts +6 -0
  161. package/dist/mcp-pipe-internal.js +151 -0
  162. package/dist/mcp-pipe-response.d.ts +14 -0
  163. package/dist/mcp-pipe-response.js +50 -0
  164. package/dist/mcp-stdio-response.d.ts +12 -0
  165. package/dist/mcp-stdio-response.js +27 -0
  166. package/dist/mcp.d.ts +27 -0
  167. package/dist/mcp.js +194 -0
  168. package/dist/prepared-embedding-cache.d.ts +38 -0
  169. package/dist/prepared-embedding-cache.js +238 -0
  170. package/dist/preview-browser-adapter.d.ts +53 -0
  171. package/dist/preview-browser-adapter.js +171 -0
  172. package/dist/preview-runtime.d.ts +67 -0
  173. package/dist/preview-runtime.js +460 -0
  174. package/dist/project-auth-transport.d.ts +27 -0
  175. package/dist/project-auth-transport.js +121 -0
  176. package/dist/project-auth.d.ts +51 -0
  177. package/dist/project-auth.js +108 -0
  178. package/dist/project-git-capture-recovery.d.ts +16 -0
  179. package/dist/project-git-capture-recovery.js +347 -0
  180. package/dist/project-git-cli.d.ts +5 -0
  181. package/dist/project-git-cli.js +40 -0
  182. package/dist/project-git-scenario-source.d.ts +10 -0
  183. package/dist/project-git-scenario-source.js +240 -0
  184. package/dist/project-git-view-source.d.ts +13 -0
  185. package/dist/project-git-view-source.js +134 -0
  186. package/dist/project-git.d.ts +97 -0
  187. package/dist/project-git.js +656 -0
  188. package/dist/project-host.d.ts +59 -0
  189. package/dist/project-host.js +471 -0
  190. package/dist/project-writer-lease.d.ts +18 -0
  191. package/dist/project-writer-lease.js +430 -0
  192. package/dist/public-error.d.ts +7 -0
  193. package/dist/public-error.js +61 -0
  194. package/dist/retrieval-benchmark.d.ts +46 -0
  195. package/dist/retrieval-benchmark.js +58 -0
  196. package/dist/safe-project-path.d.ts +8 -0
  197. package/dist/safe-project-path.js +93 -0
  198. package/dist/scenario-writer.d.ts +32 -0
  199. package/dist/scenario-writer.js +191 -0
  200. package/dist/transfer/download.d.ts +159 -0
  201. package/dist/transfer/download.js +598 -0
  202. package/dist/transfer/transport.d.ts +26 -0
  203. package/dist/transfer/transport.js +1 -0
  204. package/dist/transfer/upload.d.ts +162 -0
  205. package/dist/transfer/upload.js +354 -0
  206. package/dist/transfer-types.d.ts +95 -0
  207. package/dist/transfer-types.js +1 -0
  208. package/dist/transfer.d.ts +18 -0
  209. package/dist/transfer.js +273 -0
  210. package/dist/trigger-runtime.d.ts +121 -0
  211. package/dist/trigger-runtime.js +153 -0
  212. package/dist/vendor/wasm-core/asset-hashes.json +5 -0
  213. package/dist/vendor/wasm-core/wasm_core.d.ts +490 -0
  214. package/dist/vendor/wasm-core/wasm_core.js +1 -0
  215. package/dist/vendor/wasm-core/wasm_core_bg.wasm +0 -0
  216. package/dist/view-watcher.d.ts +9 -0
  217. package/dist/view-watcher.js +34 -0
  218. package/package.json +110 -0
@@ -0,0 +1,575 @@
1
+ import { spawn } from "node:child_process";
2
+ import { mkdir, mkdtemp, rm } from "node:fs/promises";
3
+ import { createServer } from "node:net";
4
+ import { join } from "node:path";
5
+ import { AuthoringBridgeError } from "@world-engines/authoring-bridge";
6
+ import { createPreviewBrowserCapabilities } from "./preview-browser-adapter.js";
7
+ import { ensureSafeProjectDirectory } from "./safe-project-path.js";
8
+ const DEFAULT_BROWSER_PATHS = process.platform === "win32"
9
+ ? [
10
+ "C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe",
11
+ "C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe",
12
+ "C:\\Program Files\\Microsoft\\Edge\\Application\\msedge.exe",
13
+ "C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe",
14
+ ]
15
+ : ["/usr/bin/google-chrome", "/usr/bin/chromium", "/usr/bin/chromium-browser"];
16
+ const START_TIMEOUT_MS = 15_000;
17
+ const COMMAND_TIMEOUT_MS = 10_000;
18
+ const DEFAULT_RENDER_READY_TIMEOUT_MS = 10_000;
19
+ class CdpConnection {
20
+ #socket;
21
+ #pending = new Map();
22
+ #listeners = new Map();
23
+ #nextId = 0;
24
+ constructor(socket) {
25
+ this.#socket = socket;
26
+ socket.addEventListener("message", (event) => {
27
+ if (typeof event.data !== "string")
28
+ return;
29
+ let envelope;
30
+ try {
31
+ envelope = JSON.parse(event.data);
32
+ }
33
+ catch {
34
+ return;
35
+ }
36
+ if (envelope.id !== undefined) {
37
+ const pending = this.#pending.get(envelope.id);
38
+ if (pending === undefined)
39
+ return;
40
+ this.#pending.delete(envelope.id);
41
+ clearTimeout(pending.timer);
42
+ if (envelope.error !== undefined)
43
+ pending.reject(new Error(`CDP ${envelope.error.code ?? "error"}: ${envelope.error.message ?? "failed"}`));
44
+ else
45
+ pending.resolve(envelope.result ?? {});
46
+ return;
47
+ }
48
+ if (envelope.method !== undefined) {
49
+ for (const listener of this.#listeners.get(envelope.method) ?? [])
50
+ listener(envelope.params ?? {}, envelope.sessionId);
51
+ }
52
+ });
53
+ socket.addEventListener("close", () => this.#rejectAll(new Error("CDP connection closed")));
54
+ socket.addEventListener("error", () => this.#rejectAll(new Error("CDP connection failed")));
55
+ }
56
+ static async connect(url) {
57
+ const socket = new WebSocket(url);
58
+ await new Promise((resolve, reject) => {
59
+ const timer = setTimeout(() => reject(new Error("CDP websocket timeout")), COMMAND_TIMEOUT_MS);
60
+ socket.addEventListener("open", () => { clearTimeout(timer); resolve(); }, { once: true });
61
+ socket.addEventListener("error", () => { clearTimeout(timer); reject(new Error("CDP websocket failed")); }, { once: true });
62
+ });
63
+ return new CdpConnection(socket);
64
+ }
65
+ #rejectAll(error) {
66
+ for (const pending of this.#pending.values()) {
67
+ clearTimeout(pending.timer);
68
+ pending.reject(error);
69
+ }
70
+ this.#pending.clear();
71
+ }
72
+ command(method, params = {}, sessionId) {
73
+ const id = ++this.#nextId;
74
+ return new Promise((resolve, reject) => {
75
+ const timer = setTimeout(() => {
76
+ this.#pending.delete(id);
77
+ reject(new Error(`CDP command timeout: ${method}`));
78
+ }, COMMAND_TIMEOUT_MS);
79
+ this.#pending.set(id, { resolve, reject, timer });
80
+ this.#socket.send(JSON.stringify({ id, method, params, ...(sessionId === undefined ? {} : { sessionId }) }));
81
+ });
82
+ }
83
+ once(method, timeoutMs = COMMAND_TIMEOUT_MS) {
84
+ return new Promise((resolve, reject) => {
85
+ const listeners = this.#listeners.get(method) ?? new Set();
86
+ const timer = setTimeout(() => { listeners.delete(listener); reject(new Error(`CDP event timeout: ${method}`)); }, timeoutMs);
87
+ const listener = (params) => { clearTimeout(timer); listeners.delete(listener); resolve(params); };
88
+ listeners.add(listener);
89
+ this.#listeners.set(method, listeners);
90
+ });
91
+ }
92
+ on(method, listener) {
93
+ const listeners = this.#listeners.get(method) ?? new Set();
94
+ listeners.add(listener);
95
+ this.#listeners.set(method, listeners);
96
+ return () => listeners.delete(listener);
97
+ }
98
+ close() { this.#socket.close(); }
99
+ }
100
+ async function reservePort() {
101
+ const server = createServer();
102
+ await new Promise((resolve, reject) => {
103
+ server.once("error", reject);
104
+ server.listen(0, "127.0.0.1", resolve);
105
+ });
106
+ const address = server.address();
107
+ if (address === null || typeof address === "string")
108
+ throw new Error("browser port reservation failed");
109
+ await new Promise((resolve, reject) => server.close((error) => error === undefined ? resolve() : reject(error)));
110
+ return address.port;
111
+ }
112
+ async function existingBrowserPath(explicit) {
113
+ const { access } = await import("node:fs/promises");
114
+ for (const candidate of explicit === undefined ? DEFAULT_BROWSER_PATHS : [explicit]) {
115
+ try {
116
+ await access(candidate);
117
+ return candidate;
118
+ }
119
+ catch { /* try next installed browser */ }
120
+ }
121
+ throw new AuthoringBridgeError("E_CAPABILITY_UNAVAILABLE", "未找到可用的 Chrome/Edge 浏览器");
122
+ }
123
+ function cdpValue(result) {
124
+ const remote = result.result;
125
+ if (typeof remote !== "object" || remote === null)
126
+ return undefined;
127
+ const value = remote.value;
128
+ return value;
129
+ }
130
+ function jsString(value) { return JSON.stringify(value).replace(/</gu, "\\u003c"); }
131
+ function validatedCdpWebSocketUrl(value, port, kind = "page") {
132
+ if (value === undefined)
133
+ throw new Error("browser CDP target unavailable");
134
+ const url = new URL(value);
135
+ if (url.protocol !== "ws:"
136
+ || url.hostname !== "127.0.0.1"
137
+ || url.port !== String(port)
138
+ || !(kind === "page" ? /^\/devtools\/page\/[A-Za-z0-9_-]+$/u : /^\/devtools\/browser\/[A-Za-z0-9_-]+$/u).test(url.pathname)
139
+ || url.username !== ""
140
+ || url.password !== ""
141
+ || url.search !== ""
142
+ || url.hash !== "")
143
+ throw new Error("browser returned an untrusted CDP websocket endpoint");
144
+ return url.href;
145
+ }
146
+ async function stopOwnedBrowser(child) {
147
+ if (child.exitCode !== null)
148
+ return;
149
+ const exited = new Promise((resolve) => child.once("exit", () => resolve()));
150
+ child.kill();
151
+ await Promise.race([exited, new Promise((resolve) => setTimeout(resolve, 2_000))]);
152
+ if (child.exitCode === null && process.platform === "win32" && child.pid !== undefined) {
153
+ const killer = spawn("taskkill.exe", ["/PID", String(child.pid), "/T", "/F"], { stdio: "ignore", windowsHide: true });
154
+ await new Promise((resolve) => killer.once("exit", () => resolve()));
155
+ await Promise.race([exited, new Promise((resolve) => setTimeout(resolve, 2_000))]);
156
+ }
157
+ }
158
+ function findFrame(tree, previewUrl) {
159
+ if (typeof tree !== "object" || tree === null)
160
+ return null;
161
+ const record = tree;
162
+ const frame = record.frame;
163
+ if (typeof frame === "object" && frame !== null) {
164
+ const candidate = frame;
165
+ if (typeof candidate.id === "string" && typeof candidate.url === "string" && candidate.url.startsWith(previewUrl)) {
166
+ return { id: candidate.id, url: candidate.url };
167
+ }
168
+ }
169
+ if (Array.isArray(record.childFrames)) {
170
+ for (const child of record.childFrames) {
171
+ const found = findFrame(child, previewUrl);
172
+ if (found !== null)
173
+ return found;
174
+ }
175
+ }
176
+ return null;
177
+ }
178
+ /** 系统 Chrome/Edge 的真实 CDP driver;默认 headless,不弹窗,也不注入 cookie 或作者能力。 */
179
+ export class ChromiumPreviewBrowserDriver {
180
+ #options;
181
+ #session = null;
182
+ constructor(options = {}) { this.#options = options; }
183
+ async #open(sessionId, url) {
184
+ if (this.#session?.sessionId === sessionId)
185
+ return this.#session;
186
+ await this.close();
187
+ const executable = await existingBrowserPath(this.#options.executablePath);
188
+ if (this.#options.tempRoot === undefined) {
189
+ throw new AuthoringBridgeError("E_CAPABILITY_UNAVAILABLE", "Preview 浏览器缺少项目内 profile 根目录");
190
+ }
191
+ const tempRoot = this.#options.tempRoot;
192
+ await mkdir(tempRoot, { recursive: true });
193
+ const profilePath = await mkdtemp(join(tempRoot, "worldengine-preview-browser-"));
194
+ const port = await reservePort();
195
+ const topLevelUrl = this.#options.hostPageUrl ?? url;
196
+ const child = spawn(executable, [
197
+ `--remote-debugging-port=${port}`,
198
+ "--remote-debugging-address=127.0.0.1",
199
+ `--user-data-dir=${profilePath}`,
200
+ "--no-first-run",
201
+ "--no-default-browser-check",
202
+ "--disable-background-networking",
203
+ "--disable-component-update",
204
+ "--disable-sync",
205
+ "--disable-extensions",
206
+ "--disable-features=Translate,OptimizationHints,MediaRouter",
207
+ ...(this.#options.headless === false ? [] : ["--headless=new"]),
208
+ "about:blank",
209
+ ], { stdio: "ignore", windowsHide: true });
210
+ try {
211
+ const deadline = Date.now() + START_TIMEOUT_MS;
212
+ let target;
213
+ while (Date.now() < deadline) {
214
+ if (child.exitCode !== null)
215
+ throw new Error(`browser exited with ${child.exitCode}`);
216
+ try {
217
+ const response = await fetch(`http://127.0.0.1:${port}/json/version`);
218
+ if (response.ok) {
219
+ target = await response.json();
220
+ break;
221
+ }
222
+ }
223
+ catch { /* browser has not opened CDP yet */ }
224
+ await new Promise((resolve) => setTimeout(resolve, 50));
225
+ }
226
+ const cdp = await CdpConnection.connect(validatedCdpWebSocketUrl(target?.webSocketDebuggerUrl, port, "browser"));
227
+ const created = await cdp.command("Target.createTarget", { url: "about:blank" });
228
+ if (typeof created.targetId !== "string")
229
+ throw new Error("browser page target unavailable");
230
+ const attached = await cdp.command("Target.attachToTarget", { targetId: created.targetId, flatten: true });
231
+ if (typeof attached.sessionId !== "string")
232
+ throw new Error("browser page session unavailable");
233
+ const rootSessionId = attached.sessionId;
234
+ const consoleEntries = [];
235
+ const defaultContexts = new Map();
236
+ const childTargets = new Map();
237
+ cdp.on("Runtime.consoleAPICalled", (params) => {
238
+ const rawType = params.type;
239
+ const level = rawType === "error" ? "error" : rawType === "warning" ? "warning" : rawType === "debug" ? "debug" : "info";
240
+ const args = Array.isArray(params.args) ? params.args : [];
241
+ const text = args.map((entry) => {
242
+ if (typeof entry !== "object" || entry === null)
243
+ return String(entry);
244
+ const record = entry;
245
+ return typeof record.value === "string" ? record.value : typeof record.description === "string" ? record.description : JSON.stringify(record.value);
246
+ }).join(" ");
247
+ consoleEntries.push({ level, text });
248
+ if (consoleEntries.length > 200)
249
+ consoleEntries.splice(0, consoleEntries.length - 200);
250
+ });
251
+ cdp.on("Runtime.executionContextCreated", (params, sessionId) => {
252
+ if (typeof params.context !== "object" || params.context === null)
253
+ return;
254
+ const context = params.context;
255
+ const aux = context.auxData;
256
+ if (typeof context.id === "number" && typeof aux === "object" && aux !== null) {
257
+ const data = aux;
258
+ if (data.isDefault === true && typeof data.frameId === "string") {
259
+ defaultContexts.set(data.frameId, { contextId: context.id, ...(sessionId === undefined ? {} : { sessionId }) });
260
+ }
261
+ }
262
+ });
263
+ cdp.on("Runtime.executionContextDestroyed", (params) => {
264
+ if (typeof params.executionContextId !== "number")
265
+ return;
266
+ for (const [frameId, context] of defaultContexts)
267
+ if (context.contextId === params.executionContextId)
268
+ defaultContexts.delete(frameId);
269
+ });
270
+ cdp.on("Runtime.executionContextsCleared", () => defaultContexts.clear());
271
+ cdp.on("Target.attachedToTarget", (params) => {
272
+ if (typeof params.sessionId !== "string")
273
+ return;
274
+ const info = params.targetInfo;
275
+ if (typeof info === "object" && info !== null) {
276
+ const target = info;
277
+ if (typeof target.targetId === "string" && typeof target.url === "string") {
278
+ childTargets.set(params.sessionId, { targetId: target.targetId, url: target.url });
279
+ }
280
+ }
281
+ void cdp.command("Runtime.enable", {}, params.sessionId).catch(() => undefined);
282
+ });
283
+ await Promise.all([
284
+ cdp.command("Target.setAutoAttach", { autoAttach: true, waitForDebuggerOnStart: false, flatten: true }, rootSessionId),
285
+ cdp.command("Page.enable", {}, rootSessionId),
286
+ cdp.command("Runtime.enable", {}, rootSessionId),
287
+ ]);
288
+ const opened = { sessionId, process: child, profilePath, cdp, rootSessionId, console: consoleEntries, defaultContexts, childTargets, url: topLevelUrl };
289
+ this.#session = opened;
290
+ await this.#navigate(opened, topLevelUrl);
291
+ if (this.#options.hostPageUrl !== undefined) {
292
+ if (this.#options.expectedProjectId !== undefined) {
293
+ const status = await this.#evaluate(opened, `(async()=>{const r=await fetch("/api/status",{credentials:"same-origin",cache:"no-store"});return {status:r.status,body:await r.json()};})()`);
294
+ if (typeof status !== "object" || status === null)
295
+ throw new Error("GUI status unavailable");
296
+ const statusRecord = status;
297
+ const body = statusRecord.body;
298
+ const result = body?.result;
299
+ const project = result?.project;
300
+ const projectId = result?.project_id ?? project?.project_id;
301
+ if (statusRecord.status !== 200 || projectId !== this.#options.expectedProjectId)
302
+ throw new Error("GUI project identity mismatch");
303
+ }
304
+ const activate = this.#options.activatePreviewSelector ?? '[data-view="view"]';
305
+ await this.#evaluate(opened, `(()=>{const e=document.querySelector(${jsString(activate)});if(!e)throw new Error("preview activator missing");e.click();return true;})()`);
306
+ const frameSelector = this.#options.previewFrameSelector ?? "#preview-frame";
307
+ let frameBound = false;
308
+ const bindDeadline = Date.now() + COMMAND_TIMEOUT_MS;
309
+ while (Date.now() < bindDeadline) {
310
+ const src = await this.#evaluate(opened, `(()=>{const e=document.querySelector(${jsString(frameSelector)});return e&&e.src||"";})()`);
311
+ if (typeof src === "string" && src.startsWith(url)) {
312
+ frameBound = true;
313
+ break;
314
+ }
315
+ await new Promise((resolve) => setTimeout(resolve, 50));
316
+ }
317
+ if (!frameBound)
318
+ throw new Error("GUI Host did not bind the current Preview URL");
319
+ await this.#frameContext(opened, url);
320
+ }
321
+ return opened;
322
+ }
323
+ catch (error) {
324
+ await stopOwnedBrowser(child);
325
+ await rm(profilePath, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 });
326
+ throw new AuthoringBridgeError("E_CAPABILITY_UNAVAILABLE", `无法启动 Preview 浏览器: ${error instanceof Error ? error.message : String(error)}`);
327
+ }
328
+ }
329
+ async #navigate(session, url) {
330
+ const loaded = session.cdp.once("Page.loadEventFired").catch(() => ({}));
331
+ await session.cdp.command("Page.navigate", { url }, session.rootSessionId);
332
+ await loaded;
333
+ session.url = url;
334
+ }
335
+ async #frameContext(session, previewUrl) {
336
+ const deadline = Date.now() + COMMAND_TIMEOUT_MS;
337
+ let frameObserved = false;
338
+ while (Date.now() < deadline) {
339
+ const tree = await session.cdp.command("Page.getFrameTree", {}, session.rootSessionId);
340
+ const frame = findFrame(tree.frameTree, previewUrl);
341
+ let attached = frame === null
342
+ ? [...session.childTargets.entries()].find(([, target]) => target.url.startsWith(previewUrl))
343
+ : undefined;
344
+ if (frame === null && attached === undefined) {
345
+ const targets = await session.cdp.command("Target.getTargets");
346
+ const infos = Array.isArray(targets.targetInfos) ? targets.targetInfos : [];
347
+ const target = infos.find((entry) => {
348
+ if (typeof entry !== "object" || entry === null)
349
+ return false;
350
+ const info = entry;
351
+ return info.type === "iframe" && typeof info.url === "string" && info.url.startsWith(previewUrl);
352
+ });
353
+ if (target !== undefined && typeof target.targetId === "string" && typeof target.url === "string") {
354
+ frameObserved = true;
355
+ const existing = [...session.childTargets.entries()].find(([, value]) => value.targetId === target.targetId);
356
+ if (existing === undefined) {
357
+ const result = await session.cdp.command("Target.attachToTarget", { targetId: target.targetId, flatten: true });
358
+ if (typeof result.sessionId === "string") {
359
+ session.childTargets.set(result.sessionId, { targetId: target.targetId, url: target.url });
360
+ await session.cdp.command("Runtime.enable", {}, result.sessionId);
361
+ attached = [result.sessionId, { targetId: target.targetId, url: target.url }];
362
+ }
363
+ }
364
+ else
365
+ attached = existing;
366
+ }
367
+ }
368
+ const candidate = frame ?? (attached === undefined ? null : { id: attached[1].targetId, url: attached[1].url });
369
+ if (candidate !== null) {
370
+ frameObserved = true;
371
+ const attachedSessionId = attached?.[0];
372
+ const context = session.defaultContexts.get(candidate.id)
373
+ ?? (attachedSessionId === undefined ? undefined : [...session.defaultContexts.values()].find((entry) => entry.sessionId === attachedSessionId));
374
+ if (context !== undefined) {
375
+ session.frameId = candidate.id;
376
+ session.executionContextId = context.contextId;
377
+ return { frameId: candidate.id, contextId: context.contextId, ...(context.sessionId === undefined ? {} : { sessionId: context.sessionId }), url: candidate.url };
378
+ }
379
+ }
380
+ await new Promise((resolve) => setTimeout(resolve, 50));
381
+ }
382
+ throw new AuthoringBridgeError("E_CAPABILITY_UNAVAILABLE", frameObserved
383
+ ? `sandbox Preview frame 已加载但默认 execution context 不可用(contexts=${session.defaultContexts.size})`
384
+ : `真实 Host 未加载受控 sandbox Preview frame(contexts=${session.defaultContexts.size})`);
385
+ }
386
+ async #evaluate(session, expression, contextId, cdpSessionId) {
387
+ const result = await session.cdp.command("Runtime.evaluate", {
388
+ expression,
389
+ returnByValue: true,
390
+ awaitPromise: true,
391
+ ...(contextId === undefined ? {} : { contextId }),
392
+ }, cdpSessionId ?? session.rootSessionId);
393
+ const exception = result.exceptionDetails;
394
+ if (exception !== undefined)
395
+ throw new AuthoringBridgeError("E_SCHEMA_INVALID", "Preview 浏览器操作失败");
396
+ return cdpValue(result);
397
+ }
398
+ async #waitForRenderReady(session, frame) {
399
+ const timeoutMs = this.#options.renderReadyTimeoutMs ?? DEFAULT_RENDER_READY_TIMEOUT_MS;
400
+ if (!Number.isSafeInteger(timeoutMs) || timeoutMs <= 0 || timeoutMs > COMMAND_TIMEOUT_MS) {
401
+ throw new AuthoringBridgeError("E_SCHEMA_INVALID", "Preview render readiness timeout 无效");
402
+ }
403
+ const deadline = Date.now() + timeoutMs;
404
+ let reason = "chatplay-not-ready";
405
+ while (Date.now() < deadline) {
406
+ const remainingMs = deadline - Date.now();
407
+ const readiness = await this.#evaluate(session, `(async()=>{
408
+ const timeout=(ms)=>new Promise((resolve)=>setTimeout(()=>resolve(false),ms));
409
+ const nextPaint=()=>new Promise((resolve)=>{
410
+ let settled=false;
411
+ const finish=(value)=>{if(!settled){settled=true;clearTimeout(timer);resolve(value);}};
412
+ const timer=setTimeout(()=>finish(false),100);
413
+ requestAnimationFrame(()=>finish(true));
414
+ });
415
+ const visibleSignature=()=>Array.from(document.body.querySelectorAll("*")).flatMap((element)=>{
416
+ if (element instanceof HTMLScriptElement || element instanceof HTMLStyleElement || element instanceof HTMLLinkElement || element instanceof HTMLMetaElement) return [];
417
+ const style=getComputedStyle(element);
418
+ if (style.display==="none" || style.visibility!=="visible" || Number(style.opacity)===0) return [];
419
+ const rect=element.getBoundingClientRect();
420
+ if (rect.width<=0 || rect.height<=0) return [];
421
+ const directText=Array.from(element.childNodes).some((node)=>node.nodeType===Node.TEXT_NODE && (node.textContent||"").trim().length>0);
422
+ const paintedSurface=style.backgroundImage!=="none" || !/^rgba?\\(0,\\s*0,\\s*0,\\s*0\\)$/u.test(style.backgroundColor) || parseFloat(style.borderTopWidth)>0 || parseFloat(style.borderRightWidth)>0 || parseFloat(style.borderBottomWidth)>0 || parseFloat(style.borderLeftWidth)>0 || style.boxShadow!=="none";
423
+ const intrinsic=element instanceof HTMLCanvasElement || element instanceof HTMLImageElement || element instanceof HTMLVideoElement || element instanceof SVGElement;
424
+ return directText || paintedSurface || intrinsic ? [element.tagName+":"+Math.round(rect.width)+"x"+Math.round(rect.height)+":"+(directText?1:0)+":"+(paintedSurface?1:0)+":"+(intrinsic?1:0)] : [];
425
+ }).join("|");
426
+ const chatPlay=window.ChatPlay;
427
+ if (!chatPlay || !chatPlay.ready || typeof chatPlay.ready.then!=="function") return {ready:false,reason:"chatplay-unavailable"};
428
+ const sdkReady=await Promise.race([Promise.resolve(chatPlay.ready).then(()=>true,()=>false),timeout(Math.min(100,${Math.max(1, remainingMs)}))]);
429
+ if (sdkReady!==true) return {ready:false,reason:"chatplay-not-ready"};
430
+ const before=visibleSignature();
431
+ if (before==="") return {ready:false,reason:"renderable-view-missing"};
432
+ if (await nextPaint()!==true || await nextPaint()!==true) return {ready:false,reason:"paint-not-observed"};
433
+ const after=visibleSignature();
434
+ return {ready:before===after && after!=="",reason:"render-unstable"};
435
+ })()`, frame.contextId, frame.sessionId);
436
+ if (typeof readiness === "object" && readiness !== null) {
437
+ const record = readiness;
438
+ if (record.ready === true)
439
+ return;
440
+ if (typeof record.reason === "string")
441
+ reason = record.reason;
442
+ }
443
+ if (Date.now() < deadline)
444
+ await new Promise((resolve) => setTimeout(resolve, Math.min(50, deadline - Date.now())));
445
+ }
446
+ throw new AuthoringBridgeError("E_PREVIEW_NOT_READY", `Preview 未在限定时间内完成 ChatPlay ready 与首次可视渲染 (${reason})`);
447
+ }
448
+ async inspect(request) {
449
+ const session = await this.#open(request.sessionId, request.url);
450
+ const frame = this.#options.hostPageUrl === undefined ? undefined : await this.#frameContext(session, request.url);
451
+ if (frame !== undefined)
452
+ await this.#waitForRenderReady(session, frame);
453
+ const selector = request.selector ?? "body";
454
+ const snapshot = await this.#evaluate(session, `(()=>{const e=document.querySelector(${jsString(selector)});if(!e)return null;return {dom:e.outerHTML||"",text:e.textContent||"",url:location.href,ready:document.readyState==="complete"};})()`, frame?.contextId, frame?.sessionId);
455
+ if (snapshot === null || typeof snapshot !== "object")
456
+ throw new AuthoringBridgeError("E_SCHEMA_INVALID", "Preview selector 未命中");
457
+ const record = snapshot;
458
+ let screenshot;
459
+ if (request.includeScreenshot) {
460
+ let clip;
461
+ if (frame !== undefined) {
462
+ const frameSelector = this.#options.previewFrameSelector ?? "#preview-frame";
463
+ const bounds = await this.#evaluate(session, `(()=>{const e=document.querySelector(${jsString(frameSelector)});if(!e)return null;const r=e.getBoundingClientRect();return {x:r.x,y:r.y,width:r.width,height:r.height};})()`);
464
+ if (typeof bounds !== "object" || bounds === null)
465
+ throw new AuthoringBridgeError("E_CAPABILITY_UNAVAILABLE", "Preview iframe 截图区域不可用");
466
+ const box = bounds;
467
+ if (![box.x, box.y, box.width, box.height].every((value) => typeof value === "number" && Number.isFinite(value)) || box.width <= 0 || box.height <= 0) {
468
+ throw new AuthoringBridgeError("E_CAPABILITY_UNAVAILABLE", "Preview iframe 截图区域无效");
469
+ }
470
+ clip = { x: box.x, y: box.y, width: box.width, height: box.height, scale: 1 };
471
+ }
472
+ const result = await session.cdp.command("Page.captureScreenshot", { format: "png", fromSurface: true, captureBeyondViewport: false, ...(clip === undefined ? {} : { clip }) }, session.rootSessionId);
473
+ if (typeof result.data !== "string")
474
+ throw new Error("CDP screenshot missing");
475
+ screenshot = { bytes: Buffer.from(result.data, "base64"), mimeType: "image/png" };
476
+ }
477
+ return {
478
+ url: typeof record.url === "string" ? record.url : session.url,
479
+ ready: record.ready === true,
480
+ dom: typeof record.dom === "string" ? record.dom : "",
481
+ text: typeof record.text === "string" ? record.text : "",
482
+ ...(request.includeConsole ? { console: Object.freeze([...session.console]) } : {}),
483
+ ...(screenshot === undefined ? {} : { screenshot }),
484
+ };
485
+ }
486
+ async interact(request) {
487
+ const session = await this.#open(request.sessionId, request.url);
488
+ let frame = this.#options.hostPageUrl === undefined ? undefined : await this.#frameContext(session, request.url);
489
+ if (request.action === "navigate") {
490
+ if (frame === undefined)
491
+ await this.#navigate(session, request.target);
492
+ else {
493
+ session.defaultContexts.delete(frame.frameId);
494
+ delete session.frameId;
495
+ delete session.executionContextId;
496
+ await session.cdp.command("Page.navigate", { url: request.target, frameId: frame.frameId }, session.rootSessionId);
497
+ frame = await this.#frameContext(session, request.target);
498
+ }
499
+ }
500
+ else if (request.action === "click")
501
+ await this.#evaluate(session, `(()=>{const e=document.querySelector(${jsString(request.target)});if(!e)throw new Error("missing");e.click();return true;})()`, frame?.contextId, frame?.sessionId);
502
+ else if (request.action === "type")
503
+ await this.#evaluate(session, `(()=>{const e=document.querySelector(${jsString(request.target)});if(!e||!("value" in e))throw new Error("missing");e.focus();e.value=${jsString(request.value ?? "")};e.dispatchEvent(new Event("input",{bubbles:true}));e.dispatchEvent(new Event("change",{bubbles:true}));return true;})()`, frame?.contextId, frame?.sessionId);
504
+ else if (request.action === "scroll")
505
+ await this.#evaluate(session, `(()=>{const e=document.querySelector(${jsString(request.target)});if(!e)throw new Error("missing");e.scrollIntoView({block:"center",inline:"center"});return true;})()`, frame?.contextId, frame?.sessionId);
506
+ else {
507
+ const key = request.value ?? request.target;
508
+ if (!/^(?:Enter|Escape|Tab|ArrowUp|ArrowDown|ArrowLeft|ArrowRight|Backspace|Delete|Home|End|PageUp|PageDown| )$/u.test(key)) {
509
+ throw new AuthoringBridgeError("E_SCHEMA_INVALID", "Preview press key 不在白名单中");
510
+ }
511
+ await session.cdp.command("Input.dispatchKeyEvent", { type: "keyDown", key }, session.rootSessionId);
512
+ await session.cdp.command("Input.dispatchKeyEvent", { type: "keyUp", key }, session.rootSessionId);
513
+ }
514
+ const value = await this.#evaluate(session, "location.href", frame?.contextId, frame?.sessionId);
515
+ return { url: typeof value === "string" ? value : session.url };
516
+ }
517
+ /** 仅供开发 harness 验证正式高层 SDK;不暴露任意 RPC/eval。 */
518
+ async sendChatPlayUserMessage(sessionId, previewUrl, text) {
519
+ if (text.trim().length === 0 || text.length > 64 * 1024)
520
+ throw new AuthoringBridgeError("E_SCHEMA_INVALID", "ChatPlay message 无效");
521
+ const session = await this.#open(sessionId, previewUrl);
522
+ const frame = await this.#frameContext(session, previewUrl);
523
+ const value = await this.#evaluate(session, `(async()=>{await window.ChatPlay.ready;return window.ChatPlay.sendUserMessage(${jsString(text)});})()`, frame.contextId, frame.sessionId);
524
+ if (typeof value !== "object" || value === null)
525
+ throw new AuthoringBridgeError("E_OPERATION_UNCERTAIN", "ChatPlay send 回执无效");
526
+ const result = value;
527
+ if ((result.messageId !== undefined && typeof result.messageId !== "string") || (result.turnId !== undefined && typeof result.turnId !== "string")) {
528
+ throw new AuthoringBridgeError("E_OPERATION_UNCERTAIN", "ChatPlay send 回执字段无效");
529
+ }
530
+ return Object.freeze({
531
+ ...(typeof result.messageId === "string" ? { messageId: result.messageId } : {}),
532
+ ...(typeof result.turnId === "string" ? { turnId: result.turnId } : {}),
533
+ });
534
+ }
535
+ async listChatPlayMessages(sessionId, previewUrl) {
536
+ const session = await this.#open(sessionId, previewUrl);
537
+ const frame = await this.#frameContext(session, previewUrl);
538
+ const value = await this.#evaluate(session, "(async()=>{await window.ChatPlay.ready;return window.ChatPlay.listMessages({limit:200});})()", frame.contextId, frame.sessionId);
539
+ if (!Array.isArray(value) || value.some((entry) => typeof entry !== "object" || entry === null || Array.isArray(entry))) {
540
+ throw new AuthoringBridgeError("E_OPERATION_UNCERTAIN", "ChatPlay message list 无效");
541
+ }
542
+ return Object.freeze(value.map((entry) => Object.freeze({ ...entry })));
543
+ }
544
+ async close() {
545
+ const session = this.#session;
546
+ this.#session = null;
547
+ if (session === null)
548
+ return;
549
+ if (session.process.exitCode === null) {
550
+ const exited = new Promise((resolve) => session.process.once("exit", () => resolve()));
551
+ await session.cdp.command("Browser.close").catch(() => undefined);
552
+ await Promise.race([exited, new Promise((resolve) => setTimeout(resolve, 2_000))]);
553
+ if (session.process.exitCode === null)
554
+ await stopOwnedBrowser(session.process);
555
+ }
556
+ session.cdp.close();
557
+ await rm(session.profilePath, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 });
558
+ }
559
+ }
560
+ /** ProjectHost/CLI 的窄接线入口:CLI 只需把返回 capability 传入 LocalProjectHostOptions。 */
561
+ export async function createChromiumPreviewBrowserHarness(runtime, options = {}) {
562
+ const preview = runtime.inspect();
563
+ if (preview === null || preview.state !== "running")
564
+ throw new AuthoringBridgeError("E_PREVIEW_NOT_RUNNING", "Preview 浏览器需要 running session");
565
+ const cacheRoot = options.tempRoot ?? await ensureSafeProjectDirectory(preview.project_root, ".worldengine/cache/browser");
566
+ const driver = new ChromiumPreviewBrowserDriver({ ...options, tempRoot: cacheRoot });
567
+ return Object.freeze({
568
+ driver,
569
+ desktopAuthoringCapabilities: createPreviewBrowserCapabilities({
570
+ inspectSession: () => runtime.inspect(),
571
+ driver,
572
+ }),
573
+ close: () => driver.close(),
574
+ });
575
+ }
@@ -0,0 +1,12 @@
1
+ import { type DownloadRemoteRequest, type RemoteDownloadPlan } from "../transfer/download.js";
2
+ export interface DownloadCliInvocation {
3
+ readonly kind: "plan" | "confirm" | "operation" | "reconcile" | "restore";
4
+ readonly target_pair_id?: string;
5
+ readonly parts?: readonly ("scenario" | "view")[];
6
+ readonly plan?: RemoteDownloadPlan;
7
+ readonly plan_digest?: string;
8
+ readonly operation_id?: string;
9
+ }
10
+ /** 非交互 CLI 只能以当前 frozen plan digest 确认覆盖,不读取 cookie。 */
11
+ export declare function parseDownloadInvocation(args: readonly string[]): DownloadCliInvocation;
12
+ export declare function toDownloadRemoteRequest(projectId: string, invocation: DownloadCliInvocation): DownloadRemoteRequest;
@@ -0,0 +1,50 @@
1
+ import { AuthoringBridgeError } from "@world-engines/authoring-bridge";
2
+ import { parseRemoteDownloadPlan } from "../transfer/download.js";
3
+ function invalid(message) { throw new AuthoringBridgeError("E_SCHEMA_INVALID", message); }
4
+ function parseParts(value) {
5
+ if (value === undefined)
6
+ invalid("download plan 需要 --parts scenario|view|both");
7
+ const parts = value === "both" ? ["scenario", "view"] : value.split(",");
8
+ if (parts.length < 1 || parts.length > 2 || new Set(parts).size !== parts.length || parts.some((part) => part !== "scenario" && part !== "view"))
9
+ invalid("--parts 只接受 scenario、view 或 both");
10
+ return parts;
11
+ }
12
+ function parseJsonPlan(value) {
13
+ if (value === undefined)
14
+ invalid("download confirm 需要 --plan-json <frozen-plan-json>");
15
+ try {
16
+ return parseRemoteDownloadPlan(JSON.parse(value));
17
+ }
18
+ catch (error) {
19
+ if (error instanceof AuthoringBridgeError)
20
+ throw error;
21
+ invalid("--plan-json 必须是 JSON frozen plan");
22
+ }
23
+ }
24
+ /** 非交互 CLI 只能以当前 frozen plan digest 确认覆盖,不读取 cookie。 */
25
+ export function parseDownloadInvocation(args) {
26
+ const [command, ...rest] = args;
27
+ if (command === "plan") {
28
+ const [targetPairId, flag, parts, ...extra] = rest;
29
+ if (!targetPairId || flag !== "--parts" || extra.length !== 0)
30
+ invalid("download plan <target-pair-id> --parts scenario|view|both");
31
+ return { kind: "plan", target_pair_id: targetPairId, parts: parseParts(parts) };
32
+ }
33
+ if (command === "confirm") {
34
+ const [planFlag, planJson, confirmFlag, digest, ...extra] = rest;
35
+ if (planFlag !== "--plan-json" || confirmFlag !== "--confirm-plan" || !digest || extra.length !== 0)
36
+ invalid("download confirm --plan-json <frozen-plan-json> --confirm-plan <digest>");
37
+ return { kind: "confirm", plan: parseJsonPlan(planJson), plan_digest: digest };
38
+ }
39
+ if (command === "operation" || command === "reconcile" || command === "restore") {
40
+ if (rest.length !== 1 || !rest[0])
41
+ invalid(`download ${command} <operation-id>`);
42
+ return { kind: command, operation_id: rest[0] };
43
+ }
44
+ invalid("download 只接受 plan、confirm、operation、reconcile 或 restore");
45
+ }
46
+ export function toDownloadRemoteRequest(projectId, invocation) {
47
+ if (invocation.kind !== "plan")
48
+ invalid("只有 download plan 可创建远端请求");
49
+ return { project_id: projectId, target_pair_id: invocation.target_pair_id, parts: invocation.parts };
50
+ }