@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,395 @@
1
+ import { createHash, randomUUID } from "node:crypto";
2
+ import { open, readFile } from "node:fs/promises";
3
+ import { AuthoringBridgeError } from "@world-engines/authoring-bridge";
4
+ import { canonicalAccountProjectRootIdentityBytes } from "@world-engines/project-format";
5
+ import { LOCAL_ACCOUNT_MODEL_PROXY_ORIGIN } from "./account-model-proxy.js";
6
+ import { LocalPreviewWorldlineFacade } from "./local-preview-worldlines.js";
7
+ import { ensureSafeProjectDirectory, safeProjectPath } from "./safe-project-path.js";
8
+ const MAX_TEXT = 64 * 1024;
9
+ const MAX_MESSAGES = 200;
10
+ const ID = /^[A-Za-z0-9._:-]{1,128}$/u;
11
+ const MODEL = /^[A-Za-z0-9][A-Za-z0-9._:/-]{0,255}$/u;
12
+ function fail(code, message) { throw new AuthoringBridgeError(code, message); }
13
+ function record(value) { return typeof value === "object" && value !== null && !Array.isArray(value) ? value : undefined; }
14
+ function messageId(prefix) { return `${prefix}:${randomUUID()}`; }
15
+ function eventDigest(value) { return createHash("sha256").update(value).digest("hex"); }
16
+ async function defaultModelClient(request) {
17
+ const proxy = new URL(LOCAL_ACCOUNT_MODEL_PROXY_ORIGIN);
18
+ const sessionHeaders = { Host: proxy.host, "x-worldengine-project-id": request.projectId, "x-worldengine-project-root-sha256": request.rootIdentity };
19
+ const session = await fetch(`${proxy.origin}/v1/session`, { headers: sessionHeaders, signal: AbortSignal.timeout(60_000) });
20
+ const sessionBody = record(await session.json().catch(() => undefined));
21
+ const accountIdentity = sessionBody?.account_identity;
22
+ if (!session.ok || typeof accountIdentity !== "string")
23
+ fail(session.status === 401 ? "E_AUTH_REQUIRED" : "E_OPERATION_UNCERTAIN", "账户模型代理不可用");
24
+ if (request.accountIdentity !== null && request.accountIdentity !== accountIdentity)
25
+ fail("E_ACCOUNT_CHANGED", "Preview session 已绑定其他账户;请新建本地 Preview session");
26
+ await request.bindAccount(accountIdentity);
27
+ const response = await fetch(`${proxy.origin}/v1/chat/completions`, {
28
+ method: "POST",
29
+ headers: { ...sessionHeaders, "x-worldengine-account-identity": accountIdentity, "content-type": "application/json", "idempotency-key": request.idempotencyKey },
30
+ body: JSON.stringify({ model: request.model, messages: request.messages, stream: false }), signal: AbortSignal.timeout(60_000),
31
+ });
32
+ const body = record(await response.json().catch(() => undefined));
33
+ const choices = Array.isArray(body?.choices) ? body.choices : [];
34
+ const choice = record(choices[0]);
35
+ const content = record(choice?.message)?.content;
36
+ if (!response.ok || typeof content !== "string" || content.length === 0 || content.length > MAX_TEXT) {
37
+ const code = record(body?.error)?.code;
38
+ fail(typeof code === "string" ? code : response.status === 401 ? "E_AUTH_REQUIRED" : "E_OPERATION_UNCERTAIN", "模型回合未完成");
39
+ }
40
+ return { content };
41
+ }
42
+ /** 项目内、host-owned 的开发 Preview 消息会话;它不是正式 Player save authority。 */
43
+ export class LocalPreviewSession {
44
+ #root;
45
+ #projectId;
46
+ #rootIdentity;
47
+ #modelClient;
48
+ #readOnlyProjection;
49
+ #runtime;
50
+ #hostNotificationSnapshot;
51
+ #now;
52
+ #listeners = new Set();
53
+ #revisionWaiters = new Set();
54
+ #messages = [];
55
+ #turns = new Map();
56
+ #accountIdentity = null;
57
+ #loaded = false;
58
+ #busy = false;
59
+ #sending = false;
60
+ #revision = 0;
61
+ #writerCommitted;
62
+ #failedViewDeliveries = new Map();
63
+ constructor(root, projectId, modelClient, now, readOnlyProjection, runtime, hostNotificationSnapshot, writerCommitted) {
64
+ this.#root = root;
65
+ this.#projectId = projectId;
66
+ this.#modelClient = modelClient;
67
+ this.#now = now;
68
+ this.#readOnlyProjection = readOnlyProjection;
69
+ this.#runtime = runtime;
70
+ this.#hostNotificationSnapshot = hostNotificationSnapshot;
71
+ this.#writerCommitted = writerCommitted;
72
+ this.#rootIdentity = createHash("sha256").update(canonicalAccountProjectRootIdentityBytes(root, process.platform === "win32" ? "win32" : "posix")).digest("hex");
73
+ }
74
+ static async open(input) {
75
+ const session = new LocalPreviewSession(input.root, input.projectId, input.modelClient ?? defaultModelClient, input.now ?? Date.now, input.readOnlyProjection, input.runtime, input.hostNotificationSnapshot, input.writerCommitted);
76
+ await session.#load();
77
+ return session;
78
+ }
79
+ onNotification(listener) { this.#listeners.add(listener); return () => this.#listeners.delete(listener); }
80
+ inspect() {
81
+ return Object.freeze({ development_preview: true, save_id: this.saveId, revision: this.#revision, pending: [...this.#turns.values()].filter((turn) => turn.state === "queued" || turn.state === "running").length, account_bound: this.#accountIdentity !== null, turns: [...this.#turns.values()].slice(-MAX_MESSAGES).map((turn) => Object.freeze({ turn_id: turn.id, state: turn.state, ...(turn.error_code === undefined ? {} : { error_code: turn.error_code }) })) });
82
+ }
83
+ async inspectForHost() {
84
+ let inspected = this.inspect();
85
+ if (this.#runtime !== undefined) {
86
+ const snapshot = await (await this.#runtime()).worldlineRuntime().snapshotWorldlines();
87
+ if (snapshot !== null)
88
+ inspected = Object.freeze({ ...inspected, worldline_save_id: snapshot.scope.saveId, worldline_author_namespace: snapshot.scope.authorNamespace });
89
+ }
90
+ if (this.#hostNotificationSnapshot !== undefined)
91
+ inspected = Object.freeze({ ...inspected, notification_snapshot: await this.#hostNotificationSnapshot() });
92
+ return inspected;
93
+ }
94
+ async waitForRevision(after, timeoutMs) {
95
+ if (!Number.isSafeInteger(after) || after < 0 || !Number.isSafeInteger(timeoutMs) || timeoutMs < 0 || timeoutMs > 15_000)
96
+ fail("EINVAL", "revision wait 参数无效");
97
+ if (this.#revision > after || timeoutMs === 0)
98
+ return await this.inspectForHost();
99
+ await new Promise((resolvePromise) => { const done = () => { this.#revisionWaiters.delete(done); clearTimeout(timer); resolvePromise(); }; const timer = setTimeout(done, timeoutMs); this.#revisionWaiters.add(done); });
100
+ return await this.inspectForHost();
101
+ }
102
+ get saveId() { return `local-preview:${this.#projectId}:${eventDigest(this.#rootIdentity).slice(0, 16)}`; }
103
+ pendingViewDeliveryFailures() { return Object.freeze([...this.#failedViewDeliveries].map(([operation_id, capture_sha256]) => Object.freeze({ operation_id, capture_sha256 }))); }
104
+ async recordViewDeliveryFailure(input) { if (!ID.test(input.operation_id) || !/^[a-f0-9]{64}$/u.test(input.capture_sha256))
105
+ fail("EINVAL", "View delivery failure identity 无效"); await this.#append({ v: 1, type: "view_delivery_failed", operation_id: input.operation_id, capture_sha256: input.capture_sha256 }); }
106
+ async resolveViewDeliveryFailure(operationId) { if (!ID.test(operationId) || !this.#failedViewDeliveries.has(operationId))
107
+ return; await this.#append({ v: 1, type: "view_delivery_resolved", operation_id: operationId }); }
108
+ async handle(method, params, worldlineContext) {
109
+ if (method === "chat.listMessages")
110
+ return await this.#list(params);
111
+ if (method === "chat.sendUserMessage")
112
+ return this.#send(params);
113
+ if (method === "preview.status")
114
+ return this.inspect();
115
+ if (method === "preview.capabilities") {
116
+ if (params === null || typeof params !== "object" || Array.isArray(params) || Object.keys(params).length !== 0)
117
+ fail("EINVAL", "参数无效");
118
+ return (await this.#projection()).capabilities();
119
+ }
120
+ if (method === "worldline.list" || method === "worldline.fork" || method === "worldline.activate") {
121
+ if (worldlineContext === undefined)
122
+ fail("E_CAPABILITY_UNAVAILABLE", "本地 Preview worldline 缺少 host request identity");
123
+ const result = await new LocalPreviewWorldlineFacade((await this.#writerRuntime()).worldlineRuntime()).handle(method, params, worldlineContext);
124
+ if (method === "worldline.fork" || method === "worldline.activate")
125
+ this.#touch();
126
+ return result;
127
+ }
128
+ if (["scenario.getMeta", "scenario.getProgress", "scenario.getVariable", "scenario.getUnlock", "scenario.getOwnedResources", "state.get", "state.getEntry", "state.keys", "asset.list", "asset.getUrl"].includes(method)) {
129
+ return await (await this.#projection()).handle(method, params);
130
+ }
131
+ fail("EPERM", "本地 Preview 未授予该能力");
132
+ }
133
+ async #projection() {
134
+ if (this.#readOnlyProjection === undefined)
135
+ fail("E_CAPABILITY_UNAVAILABLE", "本地 Preview 只读 Runtime projection 尚未就绪");
136
+ return typeof this.#readOnlyProjection === "function" ? await this.#readOnlyProjection() : this.#readOnlyProjection;
137
+ }
138
+ async #load() {
139
+ if (this.#loaded)
140
+ return;
141
+ const path = await safeProjectPath(this.#root, ".worldengine/local-preview-session.v1.jsonl");
142
+ let source;
143
+ try {
144
+ source = await readFile(path, "utf8");
145
+ }
146
+ catch (error) {
147
+ if (error.code === "ENOENT") {
148
+ this.#loaded = true;
149
+ return;
150
+ }
151
+ throw error;
152
+ }
153
+ if (source !== "" && !source.endsWith("\n"))
154
+ fail("E_PREVIEW_SESSION_LOG_CORRUPT", "Preview session 日志尾部不完整");
155
+ for (const line of source.split("\n").filter(Boolean)) {
156
+ let item;
157
+ try {
158
+ item = record(JSON.parse(line)) ?? fail("E_PREVIEW_SESSION_LOG_CORRUPT", "Preview session 日志无效");
159
+ }
160
+ catch (error) {
161
+ if (error instanceof AuthoringBridgeError)
162
+ throw error;
163
+ fail("E_PREVIEW_SESSION_LOG_CORRUPT", "Preview session 日志无效");
164
+ }
165
+ this.#apply(item, false);
166
+ }
167
+ this.#loaded = true;
168
+ const interrupted = [...this.#turns.values()].filter((turn) => turn.state === "queued" || turn.state === "running");
169
+ for (const turn of interrupted)
170
+ await this.#append({ v: 1, type: "turn", ...turn, state: "failed", error_code: "E_OPERATION_UNCERTAIN" });
171
+ }
172
+ #apply(item, notify) {
173
+ const type = item.type;
174
+ if (type === "session") {
175
+ if (item.project_id !== this.#projectId || item.save_id !== this.saveId)
176
+ fail("E_PREVIEW_SESSION_LOG_CORRUPT", "Preview session identity 不匹配");
177
+ return;
178
+ }
179
+ if (type === "account") {
180
+ if (typeof item.account_identity !== "string" || (this.#accountIdentity !== null && this.#accountIdentity !== item.account_identity))
181
+ fail("E_PREVIEW_SESSION_LOG_CORRUPT", "Preview session account 无效");
182
+ this.#accountIdentity = item.account_identity;
183
+ return;
184
+ }
185
+ if (type === "model_result") {
186
+ if (typeof item.turn_id !== "string" || typeof item.message_id !== "string" || typeof item.writer_receipt_digest !== "string" || !/^[a-f0-9]{64}$/u.test(item.writer_receipt_digest))
187
+ fail("E_PREVIEW_SESSION_LOG_CORRUPT", "Preview model receipt 无效");
188
+ return;
189
+ }
190
+ if (type === "view_delivery_failed") {
191
+ if (typeof item.operation_id !== "string" || !ID.test(item.operation_id) || typeof item.capture_sha256 !== "string" || !/^[a-f0-9]{64}$/u.test(item.capture_sha256))
192
+ fail("E_PREVIEW_SESSION_LOG_CORRUPT", "View delivery failure receipt 无效");
193
+ this.#failedViewDeliveries.set(item.operation_id, item.capture_sha256);
194
+ this.#revision += 1;
195
+ return;
196
+ }
197
+ if (type === "view_delivery_resolved") {
198
+ if (typeof item.operation_id !== "string" || !ID.test(item.operation_id))
199
+ fail("E_PREVIEW_SESSION_LOG_CORRUPT", "View delivery recovery receipt 无效");
200
+ this.#failedViewDeliveries.delete(item.operation_id);
201
+ this.#revision += 1;
202
+ return;
203
+ }
204
+ if (type === "accepted_user") {
205
+ if (typeof item.id !== "string" || typeof item.request_id !== "string" || typeof item.user_message_id !== "string" || !Number.isSafeInteger(item.ts))
206
+ fail("E_PREVIEW_SESSION_LOG_CORRUPT", "Preview accepted user receipt 无效");
207
+ this.#turns.set(item.id, { id: item.id, request_id: item.request_id, user_message_id: item.user_message_id, state: "queued" });
208
+ this.#revision += 1;
209
+ if (notify)
210
+ this.#notify({ topic: "turn.changed", data: { turnId: item.id, state: "queued" } });
211
+ return;
212
+ }
213
+ if (type === "turn_created") {
214
+ if (typeof item.id !== "string" || typeof item.request_id !== "string" || typeof item.user_message_id !== "string" || typeof item.text !== "string" || !Number.isSafeInteger(item.ts))
215
+ fail("E_PREVIEW_SESSION_LOG_CORRUPT", "Preview session turn 无效");
216
+ const message = Object.freeze({ id: item.user_message_id, saveId: this.saveId, role: "user", text: item.text, ts: item.ts, meta: Object.freeze({ turnId: item.id, source: "local-preview" }) });
217
+ this.#messages.push(message);
218
+ this.#turns.set(item.id, { id: item.id, request_id: item.request_id, user_message_id: item.user_message_id, state: "queued" });
219
+ this.#revision += 1;
220
+ if (notify) {
221
+ this.#notify({ topic: "message.added", data: message });
222
+ this.#notify({ topic: "turn.changed", data: { turnId: item.id, state: "queued" } });
223
+ }
224
+ return;
225
+ }
226
+ if (type === "assistant_committed") {
227
+ if (typeof item.id !== "string" || typeof item.turn_id !== "string" || !Number.isSafeInteger(item.ts))
228
+ fail("E_PREVIEW_SESSION_LOG_CORRUPT", "Preview committed message 无效");
229
+ this.#messages.push(Object.freeze({ id: item.id, saveId: this.saveId, role: "assistant", ts: item.ts, meta: Object.freeze({ turnId: item.turn_id, source: "local-preview" }) }));
230
+ this.#revision += 1;
231
+ if (notify)
232
+ this.#notify({ topic: "message.added", data: { id: item.id, role: "assistant" } });
233
+ return;
234
+ }
235
+ if (type === "message") {
236
+ if (typeof item.id !== "string" || (item.role !== "user" && item.role !== "assistant") || typeof item.text !== "string" || typeof item.turn_id !== "string" || !Number.isSafeInteger(item.ts))
237
+ fail("E_PREVIEW_SESSION_LOG_CORRUPT", "Preview session message 无效");
238
+ // 旧 JSONL assistant body 没有 Runtime SQLite receipt,不能被升格为 checkpoint 或当前消息 authority。
239
+ if (item.role === "assistant")
240
+ return;
241
+ const message = Object.freeze({ id: item.id, saveId: this.saveId, role: item.role, text: item.text, ts: item.ts, meta: Object.freeze({ turnId: item.turn_id, source: "local-preview" }) });
242
+ this.#messages.push(message);
243
+ this.#revision += 1;
244
+ if (notify)
245
+ this.#notify({ topic: "message.added", data: message });
246
+ return;
247
+ }
248
+ if (type === "turn") {
249
+ if (typeof item.id !== "string" || typeof item.request_id !== "string" || typeof item.user_message_id !== "string" || !["queued", "running", "completed", "failed"].includes(String(item.state)))
250
+ fail("E_PREVIEW_SESSION_LOG_CORRUPT", "Preview session turn 无效");
251
+ const turn = { id: item.id, request_id: item.request_id, user_message_id: item.user_message_id, state: item.state, ...(typeof item.error_code === "string" ? { error_code: item.error_code } : {}) };
252
+ this.#turns.set(turn.id, turn);
253
+ this.#revision += 1;
254
+ if (notify)
255
+ this.#notify({ topic: "turn.changed", data: { turnId: turn.id, state: turn.state, ...(turn.error_code === undefined ? {} : { errorCode: turn.error_code }) } });
256
+ return;
257
+ }
258
+ fail("E_PREVIEW_SESSION_LOG_CORRUPT", "Preview session event 无效");
259
+ }
260
+ async #append(item) {
261
+ await ensureSafeProjectDirectory(this.#root, ".worldengine");
262
+ const path = await safeProjectPath(this.#root, ".worldengine/local-preview-session.v1.jsonl");
263
+ const handle = await open(path, "a", 0o600);
264
+ try {
265
+ await handle.writeFile(`${JSON.stringify(item)}\n`, "utf8");
266
+ await handle.sync();
267
+ }
268
+ finally {
269
+ await handle.close();
270
+ }
271
+ this.#apply(item, true);
272
+ }
273
+ #notify(notification) { for (const listener of this.#listeners) {
274
+ try {
275
+ listener(notification);
276
+ }
277
+ catch { /* observer failures cannot alter the persisted turn. */ }
278
+ } for (const wake of [...this.#revisionWaiters])
279
+ wake(); }
280
+ #touch() { this.#revision += 1; for (const wake of [...this.#revisionWaiters])
281
+ wake(); }
282
+ async #list(value) {
283
+ const params = record(value) ?? fail("EINVAL", "参数无效");
284
+ const limit = params.limit === undefined ? MAX_MESSAGES : params.limit;
285
+ if (!Number.isSafeInteger(limit) || limit < 1 || limit > MAX_MESSAGES)
286
+ fail("EINVAL", "limit 无效");
287
+ const after = params.after;
288
+ const before = params.before;
289
+ if ((after !== undefined && typeof after !== "string") || (before !== undefined && typeof before !== "string"))
290
+ fail("EINVAL", "cursor 无效");
291
+ if (this.#runtime !== undefined) {
292
+ const messages = await (await this.#runtime()).readCommittedMessages();
293
+ const afterIndex = typeof after === "string" ? messages.findIndex((message) => message.messageId === after) : -1;
294
+ const beforeIndex = typeof before === "string" ? messages.findIndex((message) => message.messageId === before) : messages.length;
295
+ const start = after === undefined ? 0 : afterIndex < 0 ? messages.length : afterIndex + 1;
296
+ const end = before === undefined ? messages.length : beforeIndex < 0 ? 0 : beforeIndex;
297
+ return messages.slice(start, Math.max(start, end)).slice(-limit).map((message) => Object.freeze({ id: message.messageId, saveId: this.saveId, role: message.role, text: message.text, ts: 0, meta: Object.freeze({ turnId: message.turnId, source: "local-preview" }) }));
298
+ }
299
+ const afterIndex = typeof after === "string" ? this.#messages.findIndex((message) => message.id === after) : -1;
300
+ const beforeIndex = typeof before === "string" ? this.#messages.findIndex((message) => message.id === before) : this.#messages.length;
301
+ const start = after === undefined ? 0 : afterIndex < 0 ? this.#messages.length : afterIndex + 1;
302
+ const end = before === undefined ? this.#messages.length : beforeIndex < 0 ? 0 : beforeIndex;
303
+ return this.#messages.slice(start, Math.max(start, end)).slice(-limit);
304
+ }
305
+ async #send(value) {
306
+ const params = record(value) ?? fail("EINVAL", "参数无效");
307
+ const text = params.text;
308
+ const requestId = params.requestId;
309
+ if (typeof text !== "string" || text.trim().length === 0 || text.length > MAX_TEXT || typeof requestId !== "string" || !ID.test(requestId))
310
+ fail("EINVAL", "消息或 requestId 无效");
311
+ const existing = [...this.#turns.values()].find((turn) => turn.request_id === requestId);
312
+ if (existing !== undefined)
313
+ return { messageId: existing.user_message_id, turnId: existing.id, state: existing.state === "completed" || existing.state === "failed" ? existing.state : "queued" };
314
+ if (this.#sending || this.#busy || [...this.#turns.values()].some((turn) => turn.state === "queued" || turn.state === "running"))
315
+ fail("E_PREVIEW_TURN_PENDING", "上一个 Preview 回合仍在进行");
316
+ this.#sending = true;
317
+ try {
318
+ const turnId = messageId("local-turn");
319
+ const userId = messageId("local-message");
320
+ const ts = this.#now();
321
+ const runtime = await this.#writerRuntime();
322
+ await runtime.appendAcceptedUserMessage({ operation_id: `preview.user.${turnId}`, turn_id: turnId, message_id: userId, text });
323
+ await this.#append({ v: 1, type: "accepted_user", id: turnId, request_id: requestId, user_message_id: userId, ts });
324
+ void this.#run(turnId);
325
+ return { messageId: userId, turnId, state: "queued" };
326
+ }
327
+ finally {
328
+ this.#sending = false;
329
+ }
330
+ }
331
+ async #run(turnId) {
332
+ const turn = this.#turns.get(turnId);
333
+ if (turn === undefined || this.#busy)
334
+ return;
335
+ this.#busy = true;
336
+ try {
337
+ await this.#append({ v: 1, type: "turn", ...turn, state: "running" });
338
+ const runtime = await this.#writerRuntime();
339
+ const preparation = await runtime.prepareWriterTurn({ operation_id: `preview.writer.${turn.id}`, turn_id: turn.id });
340
+ const model = await this.#readModel();
341
+ const messages = await this.#writerMessages(runtime, preparation);
342
+ const result = await this.#modelClient({ projectId: this.#projectId, rootIdentity: this.#rootIdentity, accountIdentity: this.#accountIdentity, bindAccount: async (accountIdentity) => { if (this.#accountIdentity === null)
343
+ await this.#append({ v: 1, type: "account", account_identity: accountIdentity });
344
+ else if (this.#accountIdentity !== accountIdentity)
345
+ fail("E_ACCOUNT_CHANGED", "Preview session 已绑定其他账户;请新建本地 Preview session"); }, model, idempotencyKey: `preview:${eventDigest(`${this.saveId}:${turn.request_id}`).slice(0, 56)}`, messages });
346
+ const assistantId = messageId("local-message");
347
+ const ordered = [...(await runtime.readCommittedMessages()).map((message) => message.messageId), assistantId];
348
+ const writerReceiptDigest = eventDigest(`${turn.id}\0${result.content}\0${preparation.identity}`);
349
+ await this.#append({ v: 1, type: "model_result", turn_id: turn.id, message_id: assistantId, writer_receipt_digest: writerReceiptDigest });
350
+ await runtime.commitSuccessfulWriterResult({ prepare_identity: preparation.identity, text: result.content, message_id: assistantId, ordered_message_ids: ordered, writer_receipt_digest: writerReceiptDigest });
351
+ if (runtime.readCommittedWriterMessage(assistantId)?.text !== result.content)
352
+ fail("E_OPERATION_UNCERTAIN", "Preview Runtime writer commit 读回不一致");
353
+ await this.#append({ v: 1, type: "assistant_committed", id: assistantId, ts: this.#now(), turn_id: turnId });
354
+ await this.#append({ v: 1, type: "turn", ...turn, state: "completed" });
355
+ try {
356
+ await this.#writerCommitted?.({ turn_id: turnId });
357
+ }
358
+ catch { /* 已持久 writer 不能因 View delivery 调度失败变成失败回合。 */ }
359
+ }
360
+ catch (error) {
361
+ const code = error instanceof AuthoringBridgeError ? error.code : "E_OPERATION_UNCERTAIN";
362
+ try {
363
+ await this.#append({ v: 1, type: "turn", ...turn, state: "failed", error_code: code });
364
+ }
365
+ catch { /* retained queued/running log is intentionally uncertain and never auto-retried. */ }
366
+ }
367
+ finally {
368
+ this.#busy = false;
369
+ }
370
+ }
371
+ async #readModel() {
372
+ const path = await safeProjectPath(this.#root, "view/preview.config.json");
373
+ let value;
374
+ try {
375
+ value = JSON.parse(await readFile(path, "utf8"));
376
+ }
377
+ catch {
378
+ fail("E_PREVIEW_MODEL_CONFIG_REQUIRED", "view/preview.config.json 缺失或无效");
379
+ }
380
+ const config = record(value);
381
+ if (config === undefined || Object.keys(config).sort().join("\0") !== "model\0schema_version" || config.schema_version !== 1 || typeof config.model !== "string" || !MODEL.test(config.model))
382
+ fail("E_PREVIEW_MODEL_CONFIG_REQUIRED", "view/preview.config.json 无效");
383
+ return config.model;
384
+ }
385
+ async #writerRuntime() { if (this.#runtime === undefined)
386
+ fail("E_CAPABILITY_UNAVAILABLE", "本地 Preview writer Runtime 尚未就绪"); return await this.#runtime(); }
387
+ async #writerMessages(runtime, preparation) {
388
+ const snapshot = runtime.readProjectionSnapshot();
389
+ const context = JSON.stringify({ development_preview: true, scene: preparation.scene, manuscript_notes: preparation.scene?.manuscriptNotes ?? [], source_metadata: snapshot.source_projection?.metadata ?? null });
390
+ if (context.length > MAX_TEXT)
391
+ fail("E_VALIDATION_FAILED", "Preview Runtime 上下文过大");
392
+ const history = await runtime.readCommittedMessages();
393
+ return Object.freeze([{ role: "system", content: `当前本地开发 Preview Runtime 上下文:${context}` }, ...history.map((message) => ({ role: message.role, content: message.text }))]);
394
+ }
395
+ }
@@ -0,0 +1,40 @@
1
+ import { type NeutralScenarioSource, type ScenarioSourceManifest, type ScenarioSourceSection } from "@world-engines/scenario-author-source";
2
+ import { type CapturedUploadSnapshot } from "./transfer/upload.js";
3
+ export interface LocalPreviewSourceCaptureIdentity {
4
+ readonly kind: "worldengine.local-preview-source/v1";
5
+ readonly project_id: string;
6
+ readonly local_scenario_id: string;
7
+ readonly local_view_id: string;
8
+ readonly project_revision: number;
9
+ readonly snapshot_id: string;
10
+ readonly snapshot_digest: string;
11
+ /** 本地 manifest 绑定的 snapshot 更新时间;不是作者资产的创建时间。 */
12
+ readonly snapshot_updated_at_ms: number;
13
+ readonly scenario_sha256: string;
14
+ readonly view_source_tree_sha256: string;
15
+ readonly view_package_lock_sha256: string;
16
+ readonly neutral_source_sha256: string;
17
+ readonly neutral_manifest_sha256: string;
18
+ /** 本地 identity 的内容哈希;不是远端 head、lease 或发布回执。 */
19
+ readonly capture_sha256: string;
20
+ }
21
+ /**
22
+ * 仅供本地 Player 编译前的 immutable input 使用。
23
+ * 它明确不满足 NeutralSourceCaptureWitness:没有 owner、source revision 或远端 head。
24
+ */
25
+ export interface LocalPreviewSourceCapture {
26
+ readonly identity: LocalPreviewSourceCaptureIdentity;
27
+ readonly source: NeutralScenarioSource;
28
+ readonly source_bytes: Uint8Array;
29
+ readonly manifest: ScenarioSourceManifest;
30
+ readonly sections: readonly ScenarioSourceSection[];
31
+ }
32
+ export interface CaptureLocalPreviewSourceOptions {
33
+ readonly project_root: string;
34
+ /** 仅测试 seam;生产调用不得替换真实的本地 upload snapshot reader。 */
35
+ readonly capture_upload_snapshot?: (root: string) => Promise<CapturedUploadSnapshot>;
36
+ }
37
+ /**
38
+ * 从一个已 snapshot 一致的本地 Project 捕获中性作者源。全程只读;没有 upload、embedding、head/lease 或发布副作用。
39
+ */
40
+ export declare function captureLocalPreviewSource(options: CaptureLocalPreviewSourceOptions): Promise<LocalPreviewSourceCapture>;
@@ -0,0 +1,192 @@
1
+ import { createHash } from "node:crypto";
2
+ import { readFile } from "node:fs/promises";
3
+ import { join, resolve } from "node:path";
4
+ import { canonicalNeutralScenarioSourceBytes, createNeutralScenarioSource, } from "@world-engines/scenario-author-source";
5
+ import { ScenarioPartSourceV1 } from "@world-engines/protocol-ts/v1/scenario_part_source";
6
+ import { computeCanonicalFileTreeDigest, computeCanonicalProjectDigest, decodeScenarioPartWesp, validateLocalAuthorProjectManifest, } from "@world-engines/project-format";
7
+ import { AuthoringBridgeError } from "@world-engines/authoring-bridge";
8
+ import { LocalProjectHost } from "./project-host.js";
9
+ import { assertSafeExistingProjectPath } from "./safe-project-path.js";
10
+ import { captureUploadSnapshot, mapScenarioPartToNeutralSections, } from "./transfer/upload.js";
11
+ const SHA256 = /^[a-f0-9]{64}$/u;
12
+ const SVP_HEADER_BYTES = 17;
13
+ function sha256(bytes) {
14
+ return createHash("sha256").update(bytes).digest("hex");
15
+ }
16
+ function bytesEqual(left, right) {
17
+ return left.byteLength === right.byteLength && Buffer.from(left).equals(Buffer.from(right));
18
+ }
19
+ function error(message) {
20
+ throw new AuthoringBridgeError("E_OPERATION_UNCERTAIN", message);
21
+ }
22
+ function manifestDigestHex(manifest) {
23
+ return Buffer.from(manifest.manifest_sha256).toString("hex");
24
+ }
25
+ function captureDigest(identity) {
26
+ const entries = Object.entries(identity).map(([key, value]) => `${key.length}:${key}${String(value).length}:${String(value)}`);
27
+ return sha256(new TextEncoder().encode(entries.join("|")));
28
+ }
29
+ function parseViewSnapshot(bytes) {
30
+ if (bytes.byteLength < SVP_HEADER_BYTES || bytes[0] !== 0x53 || bytes[1] !== 0x56
31
+ || bytes[2] !== 0x50 || bytes[3] !== 0x01 || bytes[4] !== 0) {
32
+ error("本地 View capture 不是未压缩 SVP v1");
33
+ }
34
+ const length = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength).getUint32(13, true);
35
+ if (length !== bytes.byteLength - SVP_HEADER_BYTES)
36
+ error("本地 View capture 长度无效");
37
+ let value;
38
+ try {
39
+ value = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes.subarray(SVP_HEADER_BYTES)));
40
+ }
41
+ catch {
42
+ error("本地 View capture JSON 无效");
43
+ }
44
+ if (typeof value !== "object" || value === null || Array.isArray(value) || !Array.isArray(value.files)) {
45
+ error("本地 View capture 缺少 files");
46
+ }
47
+ const seen = new Set();
48
+ return Object.freeze(value.files.map((entry) => {
49
+ if (typeof entry !== "object" || entry === null || Array.isArray(entry))
50
+ error("本地 View capture 文件无效");
51
+ const candidate = entry;
52
+ const path = candidate.path;
53
+ const content = candidate.content_base64;
54
+ if (typeof path !== "string" || path.length === 0 || path.includes("\\") || path.startsWith("/")
55
+ || path.split("/").some((segment) => segment === "" || segment === "." || segment === "..") || seen.has(path)) {
56
+ error("本地 View capture 文件路径无效");
57
+ }
58
+ if (typeof content !== "string" || !/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/u.test(content)) {
59
+ error("本地 View capture 文件内容无效");
60
+ }
61
+ const decoded = new Uint8Array(Buffer.from(content, "base64"));
62
+ if (Buffer.from(decoded).toString("base64") !== content)
63
+ error("本地 View capture base64 非 canonical");
64
+ seen.add(path);
65
+ return Object.freeze({ path, bytes: decoded });
66
+ }));
67
+ }
68
+ async function readBinding(root) {
69
+ const path = await assertSafeExistingProjectPath(root, join(root, "worldengine.project.json"));
70
+ const bytes = new Uint8Array(await readFile(path));
71
+ let manifest;
72
+ try {
73
+ manifest = validateLocalAuthorProjectManifest(JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes)));
74
+ }
75
+ catch (cause) {
76
+ throw new AuthoringBridgeError("E_VALIDATION_FAILED", "worldengine.project.json 无效", { cause: cause instanceof Error ? cause.message : String(cause) });
77
+ }
78
+ if (await computeCanonicalProjectDigest(manifest) !== manifest.snapshot.canonical_digest)
79
+ error("本地 Project manifest 未绑定有效 snapshot");
80
+ return Object.freeze({
81
+ manifest_bytes: bytes,
82
+ manifest,
83
+ project_id: manifest.project_id,
84
+ project_revision: manifest.project_revision,
85
+ snapshot_id: manifest.snapshot.snapshot_id,
86
+ snapshot_digest: manifest.snapshot.canonical_digest,
87
+ snapshot_updated_at_ms: manifest.updated_at_ms,
88
+ scenario_sha256: manifest.scenario.source_sha256,
89
+ view_source_tree_sha256: manifest.view.source_tree_sha256,
90
+ view_package_lock_sha256: manifest.view.package_lock_sha256,
91
+ });
92
+ }
93
+ function capturedObjects(snapshot) {
94
+ const scenario = snapshot.objects.filter((object) => object.part_kind === "scenario");
95
+ const view = snapshot.objects.filter((object) => object.part_kind === "view");
96
+ if (scenario.length !== 1 || view.length !== 1 || snapshot.objects.length !== 2)
97
+ error("本地 capture objects 不完整或不唯一");
98
+ for (const object of [scenario[0], view[0]]) {
99
+ if (!SHA256.test(object.sha256) || object.byte_length !== object.bytes.byteLength || sha256(object.bytes) !== object.sha256) {
100
+ error("本地 capture object digest 不一致");
101
+ }
102
+ }
103
+ return Object.freeze({ scenario: scenario[0].bytes.slice(), view: view[0].bytes.slice() });
104
+ }
105
+ function assertClean(status) {
106
+ if (status.scenario_integrity !== "clean" || status.view_state !== "clean") {
107
+ error("本地 Project 尚未 snapshot 一致,拒绝捕获 Player 输入");
108
+ }
109
+ }
110
+ /**
111
+ * 从一个已 snapshot 一致的本地 Project 捕获中性作者源。全程只读;没有 upload、embedding、head/lease 或发布副作用。
112
+ */
113
+ export async function captureLocalPreviewSource(options) {
114
+ const root = await assertSafeExistingProjectPath(resolve(options.project_root), resolve(options.project_root));
115
+ const host = await LocalProjectHost.open(root);
116
+ try {
117
+ const beforeStatus = await host.inspect();
118
+ assertClean(beforeStatus);
119
+ const beforeTransfer = await host.readTransferReadback();
120
+ const before = await readBinding(root);
121
+ if (before.project_id !== beforeStatus.project_id || before.project_revision !== beforeTransfer.project_revision
122
+ || before.snapshot_digest !== beforeTransfer.snapshot_digest || before.scenario_sha256 !== beforeTransfer.scenario_sha256
123
+ || before.view_source_tree_sha256 !== beforeTransfer.view_source_tree_sha256) {
124
+ error("本地 Project readback 与 manifest snapshot 不一致");
125
+ }
126
+ const snapshot = await (options.capture_upload_snapshot ?? captureUploadSnapshot)(root);
127
+ if (snapshot.snapshot.snapshot_id !== before.snapshot_id || snapshot.snapshot.snapshot_digest !== before.snapshot_digest
128
+ || snapshot.snapshot.updated_at_ms !== before.snapshot_updated_at_ms) {
129
+ error("本地 capture 未绑定当前 manifest snapshot");
130
+ }
131
+ const objects = capturedObjects(snapshot);
132
+ const files = parseViewSnapshot(objects.view);
133
+ const capturedViewTree = await computeCanonicalFileTreeDigest(files
134
+ .filter((file) => file.path !== "package-lock.json")
135
+ .map((file) => ({ path: file.path, bytes: file.bytes })));
136
+ const capturedPackageLock = files.find((file) => file.path === "package-lock.json");
137
+ const capturedPackageLockSha256 = sha256(capturedPackageLock?.bytes ?? new Uint8Array());
138
+ if (sha256(objects.scenario) !== before.scenario_sha256 || capturedViewTree !== before.view_source_tree_sha256
139
+ || capturedPackageLockSha256 !== before.view_package_lock_sha256) {
140
+ error("本地 capture bytes 与 manifest source/view digest 不一致");
141
+ }
142
+ const protobuf = await decodeScenarioPartWesp(objects.scenario);
143
+ const sections = mapScenarioPartToNeutralSections(ScenarioPartSourceV1.decode(protobuf), objects.view, objects.scenario);
144
+ const source = await createNeutralScenarioSource(before.manifest.scenario.local_scenario_id, sections);
145
+ const sourceBytes = canonicalNeutralScenarioSourceBytes(source).slice();
146
+ const neutralManifest = source.manifest;
147
+ if (neutralManifest === undefined)
148
+ error("本地 neutral source 缺少 manifest");
149
+ const neutralSourceSha256 = sha256(sourceBytes);
150
+ const neutralManifestSha256 = manifestDigestHex(neutralManifest);
151
+ const after = await readBinding(root);
152
+ const afterStatus = await host.inspect();
153
+ assertClean(afterStatus);
154
+ const afterTransfer = await host.readTransferReadback();
155
+ if (!bytesEqual(before.manifest_bytes, after.manifest_bytes)
156
+ || before.project_id !== after.project_id || before.project_revision !== after.project_revision
157
+ || before.snapshot_id !== after.snapshot_id || before.snapshot_digest !== after.snapshot_digest
158
+ || before.snapshot_updated_at_ms !== after.snapshot_updated_at_ms
159
+ || before.scenario_sha256 !== after.scenario_sha256 || before.view_source_tree_sha256 !== after.view_source_tree_sha256
160
+ || before.view_package_lock_sha256 !== after.view_package_lock_sha256
161
+ || afterTransfer.project_revision !== beforeTransfer.project_revision || afterTransfer.snapshot_digest !== beforeTransfer.snapshot_digest
162
+ || afterTransfer.scenario_sha256 !== beforeTransfer.scenario_sha256 || afterTransfer.view_source_tree_sha256 !== beforeTransfer.view_source_tree_sha256) {
163
+ error("本地 Project 在 Player input capture 期间发生变化");
164
+ }
165
+ const identityWithoutCapture = Object.freeze({
166
+ kind: "worldengine.local-preview-source/v1",
167
+ project_id: before.project_id,
168
+ local_scenario_id: before.manifest.scenario.local_scenario_id,
169
+ local_view_id: before.manifest.view.local_view_id,
170
+ project_revision: before.project_revision,
171
+ snapshot_id: before.snapshot_id,
172
+ snapshot_digest: before.snapshot_digest,
173
+ snapshot_updated_at_ms: before.snapshot_updated_at_ms,
174
+ scenario_sha256: before.scenario_sha256,
175
+ view_source_tree_sha256: before.view_source_tree_sha256,
176
+ view_package_lock_sha256: before.view_package_lock_sha256,
177
+ neutral_source_sha256: neutralSourceSha256,
178
+ neutral_manifest_sha256: neutralManifestSha256,
179
+ });
180
+ const identity = Object.freeze({ ...identityWithoutCapture, capture_sha256: captureDigest(identityWithoutCapture) });
181
+ return Object.freeze({
182
+ identity,
183
+ source,
184
+ source_bytes: sourceBytes,
185
+ manifest: neutralManifest,
186
+ sections: Object.freeze([...source.sections]),
187
+ });
188
+ }
189
+ finally {
190
+ await host.close();
191
+ }
192
+ }