@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,17 @@
1
+ export type LocalGuiVersionState = "checking" | "up_to_date" | "update_available" | "unpublished" | "error";
2
+ export interface LocalGuiVersionStatus {
3
+ readonly status: LocalGuiVersionState;
4
+ readonly current: string;
5
+ readonly latest?: string;
6
+ }
7
+ export interface LocalGuiVersionChecker {
8
+ /** 同步读取当前进程内缓存;不会因 HTTP 请求再次访问 registry。 */
9
+ get(): LocalGuiVersionStatus;
10
+ /** 每个 checker 最多执行一次异步检查,后续调用复用同一结果。 */
11
+ check(): Promise<LocalGuiVersionStatus>;
12
+ }
13
+ export interface LocalGuiVersionOptions {
14
+ readonly fetch?: typeof globalThis.fetch;
15
+ readonly timeoutMs?: number;
16
+ }
17
+ export declare function createLocalGuiVersionChecker(options?: LocalGuiVersionOptions): LocalGuiVersionChecker;
@@ -0,0 +1,147 @@
1
+ import { readFileSync } from "node:fs";
2
+ const PACKAGE_NAME = "@world-engines/project-host";
3
+ const REGISTRY_LATEST_URL = "https://registry.npmjs.org/@world-engines%2fproject-host/latest";
4
+ const DEFAULT_TIMEOUT_MS = 5_000;
5
+ const MAX_METADATA_BYTES = 64 * 1024;
6
+ const SEMVER_PATTERN = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[A-Za-z-][0-9A-Za-z-]*)(?:\.(?:0|[1-9]\d*|\d*[A-Za-z-][0-9A-Za-z-]*))*))?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/u;
7
+ function parseSemver(value) {
8
+ if (typeof value !== "string")
9
+ return undefined;
10
+ const match = SEMVER_PATTERN.exec(value);
11
+ if (match === null)
12
+ return undefined;
13
+ return {
14
+ core: [BigInt(match[1]), BigInt(match[2]), BigInt(match[3])],
15
+ prerelease: match[4]?.split(".") ?? [],
16
+ };
17
+ }
18
+ function compareSemver(leftValue, rightValue) {
19
+ const left = parseSemver(leftValue);
20
+ const right = parseSemver(rightValue);
21
+ if (left === undefined || right === undefined)
22
+ throw new Error("local_gui_version_semver_invalid");
23
+ for (let index = 0; index < 3; index += 1) {
24
+ const difference = left.core[index] - right.core[index];
25
+ if (difference !== 0n)
26
+ return difference < 0n ? -1 : 1;
27
+ }
28
+ if (left.prerelease.length === 0 || right.prerelease.length === 0) {
29
+ return left.prerelease.length === right.prerelease.length ? 0 : left.prerelease.length === 0 ? 1 : -1;
30
+ }
31
+ const length = Math.max(left.prerelease.length, right.prerelease.length);
32
+ for (let index = 0; index < length; index += 1) {
33
+ const a = left.prerelease[index];
34
+ const b = right.prerelease[index];
35
+ if (a === undefined || b === undefined)
36
+ return a === b ? 0 : a === undefined ? -1 : 1;
37
+ if (a === b)
38
+ continue;
39
+ const aNumeric = /^\d+$/u.test(a);
40
+ const bNumeric = /^\d+$/u.test(b);
41
+ if (aNumeric && bNumeric)
42
+ return BigInt(a) < BigInt(b) ? -1 : 1;
43
+ if (aNumeric !== bNumeric)
44
+ return aNumeric ? -1 : 1;
45
+ return a < b ? -1 : 1;
46
+ }
47
+ return 0;
48
+ }
49
+ function installedVersion() {
50
+ let value;
51
+ try {
52
+ value = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
53
+ }
54
+ catch {
55
+ throw new Error("local_gui_installed_package_unavailable");
56
+ }
57
+ if (typeof value !== "object" || value === null || Array.isArray(value))
58
+ throw new Error("local_gui_installed_package_invalid");
59
+ const manifest = value;
60
+ if (manifest.name !== PACKAGE_NAME || parseSemver(manifest.version) === undefined)
61
+ throw new Error("local_gui_installed_package_invalid");
62
+ return manifest.version;
63
+ }
64
+ async function boundedMetadata(response) {
65
+ const declared = Number(response.headers.get("content-length"));
66
+ if (Number.isFinite(declared) && declared > MAX_METADATA_BYTES)
67
+ throw new Error("local_gui_version_metadata_too_large");
68
+ if (response.body === null)
69
+ throw new Error("local_gui_version_metadata_invalid");
70
+ const reader = response.body.getReader();
71
+ const chunks = [];
72
+ let total = 0;
73
+ while (true) {
74
+ const item = await reader.read();
75
+ if (item.done)
76
+ break;
77
+ total += item.value.byteLength;
78
+ if (!Number.isSafeInteger(total) || total > MAX_METADATA_BYTES) {
79
+ await reader.cancel().catch(() => undefined);
80
+ throw new Error("local_gui_version_metadata_too_large");
81
+ }
82
+ chunks.push(item.value);
83
+ }
84
+ const bytes = new Uint8Array(total);
85
+ let offset = 0;
86
+ for (const chunk of chunks) {
87
+ bytes.set(chunk, offset);
88
+ offset += chunk.byteLength;
89
+ }
90
+ let value;
91
+ try {
92
+ value = JSON.parse(new TextDecoder().decode(bytes));
93
+ }
94
+ catch {
95
+ throw new Error("local_gui_version_metadata_invalid");
96
+ }
97
+ if (typeof value !== "object" || value === null || Array.isArray(value))
98
+ throw new Error("local_gui_version_metadata_invalid");
99
+ return value;
100
+ }
101
+ export function createLocalGuiVersionChecker(options = {}) {
102
+ const request = options.fetch ?? globalThis.fetch;
103
+ const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
104
+ if (typeof request !== "function" || !Number.isSafeInteger(timeoutMs) || timeoutMs <= 0 || timeoutMs > 30_000) {
105
+ throw new Error("local_gui_version_options_invalid");
106
+ }
107
+ const current = installedVersion();
108
+ let cached = Object.freeze({ status: "checking", current });
109
+ let checkPromise;
110
+ const check = async () => {
111
+ if (checkPromise !== undefined)
112
+ return checkPromise;
113
+ checkPromise = (async () => {
114
+ const controller = new AbortController();
115
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
116
+ try {
117
+ const response = await request(REGISTRY_LATEST_URL, {
118
+ method: "GET",
119
+ redirect: "error",
120
+ headers: { accept: "application/json" },
121
+ signal: controller.signal,
122
+ });
123
+ if (response.status === 404)
124
+ return Object.freeze({ status: "unpublished", current });
125
+ if (response.status !== 200)
126
+ return Object.freeze({ status: "error", current });
127
+ const metadata = await boundedMetadata(response);
128
+ if (metadata.name !== PACKAGE_NAME || typeof metadata.version !== "string" || parseSemver(metadata.version) === undefined) {
129
+ return Object.freeze({ status: "error", current });
130
+ }
131
+ const latest = metadata.version;
132
+ return Object.freeze({ status: compareSemver(current, latest) < 0 ? "update_available" : "up_to_date", current, latest });
133
+ }
134
+ catch {
135
+ return Object.freeze({ status: "error", current });
136
+ }
137
+ finally {
138
+ clearTimeout(timer);
139
+ }
140
+ })().then((result) => {
141
+ cached = result;
142
+ return result;
143
+ });
144
+ return checkPromise;
145
+ };
146
+ return Object.freeze({ get: () => cached, check });
147
+ }
@@ -0,0 +1,14 @@
1
+ export type LocalGuiFileArea = "view" | "docs";
2
+ export interface LocalGuiFileEntry {
3
+ readonly path: string;
4
+ readonly size: number;
5
+ readonly sha256: string;
6
+ }
7
+ export interface LocalGuiFileReadback extends LocalGuiFileEntry {
8
+ readonly content: string;
9
+ }
10
+ export declare const LOCAL_GUI_MAX_FILE_BYTES: number;
11
+ export declare function readLocalGuiFile(projectRoot: string, area: LocalGuiFileArea, path: string): Promise<LocalGuiFileReadback>;
12
+ export declare function listLocalGuiFiles(projectRoot: string, area: LocalGuiFileArea): Promise<readonly LocalGuiFileEntry[]>;
13
+ /** 同一进程内所有 GUI 对同一文件串行 CAS;外部编辑器仍由 rename 前二次摘要检查检测。 */
14
+ export declare function writeLocalGuiViewFile(projectRoot: string, path: string, content: string, expectedSha256: string): Promise<LocalGuiFileReadback>;
@@ -0,0 +1,171 @@
1
+ import { randomBytes } from "node:crypto";
2
+ import { lstat, open, readFile, readdir, rename, rm, stat } from "node:fs/promises";
3
+ import { extname, isAbsolute, join, relative, resolve, sep } from "node:path";
4
+ import { AuthoringBridgeError } from "@world-engines/authoring-bridge";
5
+ import { sha256Hex } from "@world-engines/project-format";
6
+ import { assertSafeExistingProjectPath, assertSafeProjectParent } from "./safe-project-path.js";
7
+ export const LOCAL_GUI_MAX_FILE_BYTES = 2 * 1024 * 1024;
8
+ const MAX_LISTED_FILES = 4_096;
9
+ const EXCLUDED_DIRECTORY_NAMES = new Set([".git", ".vite", ".worldengine", "coverage", "dist", "node_modules"]);
10
+ const TEXT_EXTENSIONS = new Set([
11
+ ".cjs", ".css", ".graphql", ".html", ".js", ".json", ".jsx", ".md", ".mjs",
12
+ ".scss", ".svg", ".toml", ".ts", ".tsx", ".txt", ".xml", ".yaml", ".yml",
13
+ ]);
14
+ const SENSITIVE_NAMES = new Set([
15
+ ".dev.vars", ".envrc", ".git-credentials", ".netrc", ".npmrc", ".pypirc", ".yarnrc", ".yarnrc.yml",
16
+ "auth.json", "credentials", "credentials.json", "private-key.json", "secrets.json", "secrets.yaml", "secrets.yml",
17
+ "service-account.json",
18
+ ]);
19
+ const WINDOWS_DEVICE_NAME = /^(?:con|prn|aux|nul|com[1-9]|lpt[1-9])(?:\.|$)/i;
20
+ function isSensitiveName(name) {
21
+ const lower = name.toLowerCase();
22
+ return lower === ".env" || lower.startsWith(".env.") || SENSITIVE_NAMES.has(lower)
23
+ || /(?:^|[._-])(?:credentials?|private[-_]?key|secrets?|service[-_]?account)(?:[._-]|$)/i.test(lower);
24
+ }
25
+ function assertSafeSegment(part) {
26
+ if (part === "" || part === "." || part === ".." || part.includes(":")) {
27
+ throw new AuthoringBridgeError("E_PATH_OUTSIDE_PROJECT", "文件路径无效");
28
+ }
29
+ if (part.endsWith(".") || part.endsWith(" ") || WINDOWS_DEVICE_NAME.test(part)) {
30
+ throw new AuthoringBridgeError("E_PATH_OUTSIDE_PROJECT", "文件路径无效");
31
+ }
32
+ if (isSensitiveName(part))
33
+ throw new AuthoringBridgeError("E_REMOTE_FORBIDDEN", "拒绝访问敏感配置文件");
34
+ }
35
+ function normalizeRelativePath(path) {
36
+ if (typeof path !== "string" || path === "" || isAbsolute(path) || path.includes("\0")) {
37
+ throw new AuthoringBridgeError("E_PATH_OUTSIDE_PROJECT", "文件路径必须是项目相对路径");
38
+ }
39
+ const parts = path.split(/[\\/]+/);
40
+ for (const part of parts)
41
+ assertSafeSegment(part);
42
+ if (parts.slice(0, -1).some((part) => EXCLUDED_DIRECTORY_NAMES.has(part.toLowerCase()))) {
43
+ throw new AuthoringBridgeError("E_REMOTE_FORBIDDEN", "拒绝访问生成目录或依赖目录");
44
+ }
45
+ if (!TEXT_EXTENSIONS.has(extname(parts.at(-1) ?? "").toLowerCase())) {
46
+ throw new AuthoringBridgeError("E_VALIDATION_FAILED", "只允许文本源码与文档文件");
47
+ }
48
+ return parts.join("/");
49
+ }
50
+ function areaDirectory(projectRoot, area) {
51
+ if (area !== "view" && area !== "docs") {
52
+ throw new AuthoringBridgeError("E_SCHEMA_INVALID", "未知文件区域");
53
+ }
54
+ return join(projectRoot, area);
55
+ }
56
+ function within(root, candidate) {
57
+ const rel = relative(root, candidate);
58
+ return rel === "" || (rel !== ".." && !rel.startsWith(`..${sep}`) && !isAbsolute(rel));
59
+ }
60
+ async function readRegularUtf8(projectRoot, root, path) {
61
+ const normalized = normalizeRelativePath(path);
62
+ const absolute = await assertSafeExistingProjectPath(projectRoot, join(root, ...normalized.split("/")));
63
+ if (!within(root, absolute))
64
+ throw new AuthoringBridgeError("E_PATH_OUTSIDE_PROJECT", "文件路径越界");
65
+ const info = await stat(absolute);
66
+ if (!info.isFile() || info.size > LOCAL_GUI_MAX_FILE_BYTES) {
67
+ throw new AuthoringBridgeError("E_VALIDATION_FAILED", "文件不是可编辑的普通文本文件");
68
+ }
69
+ const bytes = new Uint8Array(await readFile(absolute));
70
+ const content = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
71
+ return { path: normalized, size: bytes.byteLength, sha256: await sha256Hex(bytes), content };
72
+ }
73
+ export async function readLocalGuiFile(projectRoot, area, path) {
74
+ const root = await assertSafeExistingProjectPath(projectRoot, areaDirectory(projectRoot, area));
75
+ return readRegularUtf8(projectRoot, root, path);
76
+ }
77
+ export async function listLocalGuiFiles(projectRoot, area) {
78
+ const root = await assertSafeExistingProjectPath(projectRoot, areaDirectory(projectRoot, area));
79
+ const files = [];
80
+ async function walk(directory, prefix) {
81
+ const canonical = await assertSafeExistingProjectPath(projectRoot, directory);
82
+ if (!within(root, canonical))
83
+ throw new AuthoringBridgeError("E_PATH_OUTSIDE_PROJECT", "文件目录越界");
84
+ for (const entry of await readdir(canonical, { withFileTypes: true })) {
85
+ if (files.length >= MAX_LISTED_FILES)
86
+ throw new AuthoringBridgeError("E_VALIDATION_FAILED", "文件数量超过本地 GUI 上限");
87
+ const path = prefix === "" ? entry.name : `${prefix}/${entry.name}`;
88
+ const absolute = join(canonical, entry.name);
89
+ const info = await lstat(absolute);
90
+ if (info.isSymbolicLink())
91
+ throw new AuthoringBridgeError("E_PATH_OUTSIDE_PROJECT", "拒绝链接文件");
92
+ if (info.isDirectory()) {
93
+ if (!EXCLUDED_DIRECTORY_NAMES.has(entry.name.toLowerCase()))
94
+ await walk(absolute, path);
95
+ }
96
+ else if (info.isFile() && info.size <= LOCAL_GUI_MAX_FILE_BYTES && !isSensitiveName(entry.name) && TEXT_EXTENSIONS.has(extname(entry.name).toLowerCase())) {
97
+ const bytes = new Uint8Array(await readFile(await assertSafeExistingProjectPath(projectRoot, absolute)));
98
+ files.push({ path, size: bytes.byteLength, sha256: await sha256Hex(bytes) });
99
+ }
100
+ }
101
+ }
102
+ await walk(root, "");
103
+ return files.sort((left, right) => left.path.localeCompare(right.path));
104
+ }
105
+ async function writeLocalGuiViewFileUnlocked(projectRoot, path, content, expectedSha256) {
106
+ if (typeof content !== "string" || Buffer.byteLength(content, "utf8") > LOCAL_GUI_MAX_FILE_BYTES) {
107
+ throw new AuthoringBridgeError("E_VALIDATION_FAILED", "文件内容超过本地 GUI 上限");
108
+ }
109
+ if (!/^[a-f0-9]{64}$/.test(expectedSha256)) {
110
+ throw new AuthoringBridgeError("E_SCHEMA_INVALID", "expected_sha256 无效");
111
+ }
112
+ const root = await assertSafeExistingProjectPath(projectRoot, join(projectRoot, "view"));
113
+ const normalized = normalizeRelativePath(path);
114
+ const target = await assertSafeExistingProjectPath(projectRoot, join(root, ...normalized.split("/")));
115
+ if (!within(root, target))
116
+ throw new AuthoringBridgeError("E_PATH_OUTSIDE_PROJECT", "文件路径越界");
117
+ const before = await readRegularUtf8(projectRoot, root, normalized);
118
+ if (before.sha256 !== expectedSha256) {
119
+ const error = new Error("View 文件已变化");
120
+ error.code = "E_FILE_CHANGED";
121
+ throw error;
122
+ }
123
+ const bytes = new TextEncoder().encode(content);
124
+ const temp = join(resolve(target, ".."), `.worldengine-gui-${randomBytes(16).toString("hex")}${extname(target)}.tmp`);
125
+ await assertSafeProjectParent(projectRoot, temp);
126
+ try {
127
+ const handle = await open(temp, "wx");
128
+ try {
129
+ await handle.writeFile(bytes);
130
+ await handle.sync();
131
+ }
132
+ finally {
133
+ await handle.close();
134
+ }
135
+ const immediatelyBeforeRename = await readRegularUtf8(projectRoot, root, normalized);
136
+ if (immediatelyBeforeRename.sha256 !== expectedSha256) {
137
+ const error = new Error("View 文件已变化");
138
+ error.code = "E_FILE_CHANGED";
139
+ throw error;
140
+ }
141
+ await assertSafeProjectParent(projectRoot, target);
142
+ await rename(temp, target);
143
+ }
144
+ finally {
145
+ await rm(temp, { force: true });
146
+ }
147
+ const readback = await readRegularUtf8(projectRoot, root, normalized);
148
+ const writtenSha256 = await sha256Hex(bytes);
149
+ if (readback.sha256 !== writtenSha256 || readback.content !== content) {
150
+ throw new AuthoringBridgeError("E_OPERATION_UNCERTAIN", "View 文件写入回读不一致");
151
+ }
152
+ return readback;
153
+ }
154
+ const VIEW_WRITE_TAILS = new Map();
155
+ /** 同一进程内所有 GUI 对同一文件串行 CAS;外部编辑器仍由 rename 前二次摘要检查检测。 */
156
+ export async function writeLocalGuiViewFile(projectRoot, path, content, expectedSha256) {
157
+ const key = `${resolve(projectRoot).toLowerCase()}\0${path.replaceAll("\\", "/").toLowerCase()}`;
158
+ const previous = VIEW_WRITE_TAILS.get(key) ?? Promise.resolve();
159
+ let release;
160
+ const current = new Promise((resolvePromise) => { release = resolvePromise; });
161
+ VIEW_WRITE_TAILS.set(key, current);
162
+ await previous;
163
+ try {
164
+ return await writeLocalGuiViewFileUnlocked(projectRoot, path, content, expectedSha256);
165
+ }
166
+ finally {
167
+ release();
168
+ if (VIEW_WRITE_TAILS.get(key) === current)
169
+ VIEW_WRITE_TAILS.delete(key);
170
+ }
171
+ }
@@ -0,0 +1,28 @@
1
+ import { type LocalGuiFileReadback } from "./local-gui-workspace.js";
2
+ export interface LocalModelSettings {
3
+ readonly schema_version: 1;
4
+ readonly model: string;
5
+ }
6
+ export interface LocalModelSettingsReadback extends LocalGuiFileReadback {
7
+ readonly settings: LocalModelSettings;
8
+ }
9
+ export interface LocalAccountChatCatalog {
10
+ readonly accountIdentity: string;
11
+ readonly catalogDigest: string;
12
+ readonly models: readonly Readonly<{
13
+ model: string;
14
+ max_output_tokens: number;
15
+ }>[];
16
+ }
17
+ export declare function parseLocalModelSettings(value: unknown): LocalModelSettings;
18
+ export declare function readLocalModelSettings(projectRoot: string): Promise<LocalModelSettingsReadback>;
19
+ export declare function writeLocalModelSettings(projectRoot: string, input: Readonly<{
20
+ model: unknown;
21
+ expected_sha256: unknown;
22
+ }>): Promise<LocalModelSettingsReadback>;
23
+ /** 仅 Node Host 调用固定 11450 proxy;返回的 account identity 只用于服务端 CAS,绝不交给 GUI/iframe。 */
24
+ export declare function fetchLocalAccountChatCatalog(input: Readonly<{
25
+ projectRoot: string;
26
+ projectId: string;
27
+ fetcher?: typeof globalThis.fetch;
28
+ }>): Promise<LocalAccountChatCatalog>;
@@ -0,0 +1,124 @@
1
+ import { AuthoringBridgeError } from "@world-engines/authoring-bridge";
2
+ import { readLocalGuiFile, writeLocalGuiViewFile } from "./local-gui-workspace.js";
3
+ import { LOCAL_ACCOUNT_MODEL_PROXY_ORIGIN, resolveLocalAccountProjectBinding } from "./account-model-proxy.js";
4
+ const PREVIEW_CONFIG_PATH = "preview.config.json";
5
+ const MAX_CATALOG_BYTES = 64 * 1024;
6
+ const MODEL = /^[A-Za-z0-9][A-Za-z0-9._:/-]{0,255}$/u;
7
+ function fail(code, message) {
8
+ throw new AuthoringBridgeError(code, message);
9
+ }
10
+ function object(value) {
11
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? value : undefined;
12
+ }
13
+ async function boundedJson(response, limit) {
14
+ const declared = Number(response.headers.get("content-length"));
15
+ if (Number.isFinite(declared) && declared > limit)
16
+ throw new AuthoringBridgeError("E_VALIDATION_FAILED", "账户模型目录响应过大");
17
+ if (response.body === null)
18
+ throw new AuthoringBridgeError("E_VALIDATION_FAILED", "账户模型目录响应无效");
19
+ const reader = response.body.getReader();
20
+ const chunks = [];
21
+ let size = 0;
22
+ for (;;) {
23
+ const next = await reader.read();
24
+ if (next.done)
25
+ break;
26
+ size += next.value.byteLength;
27
+ if (size > limit) {
28
+ await reader.cancel();
29
+ throw new AuthoringBridgeError("E_VALIDATION_FAILED", "账户模型目录响应过大");
30
+ }
31
+ chunks.push(next.value);
32
+ }
33
+ const bytes = new Uint8Array(size);
34
+ let offset = 0;
35
+ for (const chunk of chunks) {
36
+ bytes.set(chunk, offset);
37
+ offset += chunk.byteLength;
38
+ }
39
+ try {
40
+ const value = object(JSON.parse(new TextDecoder().decode(bytes)));
41
+ if (value !== undefined)
42
+ return value;
43
+ }
44
+ catch { /* fail closed below */ }
45
+ throw new AuthoringBridgeError("E_VALIDATION_FAILED", "账户模型目录响应无效");
46
+ }
47
+ export function parseLocalModelSettings(value) {
48
+ const config = object(value);
49
+ if (config === undefined || Object.keys(config).sort().join("\0") !== "model\0schema_version"
50
+ || config.schema_version !== 1 || typeof config.model !== "string" || !MODEL.test(config.model)) {
51
+ fail("E_PREVIEW_MODEL_CONFIG_REQUIRED", "view/preview.config.json 无效");
52
+ }
53
+ return Object.freeze({ schema_version: 1, model: config.model });
54
+ }
55
+ export async function readLocalModelSettings(projectRoot) {
56
+ const file = await readLocalGuiFile(projectRoot, "view", PREVIEW_CONFIG_PATH);
57
+ let value;
58
+ try {
59
+ value = JSON.parse(file.content);
60
+ }
61
+ catch {
62
+ fail("E_PREVIEW_MODEL_CONFIG_REQUIRED", "view/preview.config.json 缺失或无效");
63
+ }
64
+ return Object.freeze({ ...file, settings: parseLocalModelSettings(value) });
65
+ }
66
+ export async function writeLocalModelSettings(projectRoot, input) {
67
+ if (typeof input.model !== "string" || !MODEL.test(input.model) || typeof input.expected_sha256 !== "string") {
68
+ fail("E_SCHEMA_INVALID", "模型设置字段无效");
69
+ }
70
+ const settings = Object.freeze({ schema_version: 1, model: input.model });
71
+ const file = await writeLocalGuiViewFile(projectRoot, PREVIEW_CONFIG_PATH, `${JSON.stringify(settings)}\n`, input.expected_sha256);
72
+ return Object.freeze({ ...file, settings });
73
+ }
74
+ /** 仅 Node Host 调用固定 11450 proxy;返回的 account identity 只用于服务端 CAS,绝不交给 GUI/iframe。 */
75
+ export async function fetchLocalAccountChatCatalog(input) {
76
+ const binding = await resolveLocalAccountProjectBinding(input.projectRoot, input.projectId);
77
+ const proxy = new URL(LOCAL_ACCOUNT_MODEL_PROXY_ORIGIN);
78
+ const fetcher = input.fetcher ?? globalThis.fetch;
79
+ const headers = { Host: proxy.host, "x-worldengine-project-id": input.projectId, "x-worldengine-project-root-sha256": binding.rootIdentity };
80
+ let sessionResponse;
81
+ try {
82
+ sessionResponse = await fetcher(`${proxy.origin}/v1/session`, { headers, signal: AbortSignal.timeout(10_000) });
83
+ }
84
+ catch {
85
+ throw new AuthoringBridgeError("E_OPERATION_UNCERTAIN", "账户模型代理暂时不可用");
86
+ }
87
+ if (sessionResponse.status === 401)
88
+ throw new AuthoringBridgeError("E_AUTH_REQUIRED", "请先登录账户");
89
+ if (!sessionResponse.ok)
90
+ throw new AuthoringBridgeError("E_OPERATION_UNCERTAIN", "账户模型代理暂时不可用");
91
+ const session = await boundedJson(sessionResponse, MAX_CATALOG_BYTES);
92
+ const identity = session.account_identity;
93
+ if (session.project_id !== input.projectId || typeof identity !== "string" || !/^[a-f0-9]{64}$/u.test(identity))
94
+ throw new AuthoringBridgeError("E_VALIDATION_FAILED", "账户模型 session 无效");
95
+ let catalogResponse;
96
+ try {
97
+ catalogResponse = await fetcher(`${proxy.origin}/v1/chat/catalog`, { headers: { ...headers, "x-worldengine-account-identity": identity }, signal: AbortSignal.timeout(10_000) });
98
+ }
99
+ catch {
100
+ throw new AuthoringBridgeError("E_OPERATION_UNCERTAIN", "账户模型目录暂时不可用");
101
+ }
102
+ if (catalogResponse.status === 401)
103
+ throw new AuthoringBridgeError("E_AUTH_REQUIRED", "请先登录账户");
104
+ if (catalogResponse.status === 409)
105
+ throw new AuthoringBridgeError("E_ACCOUNT_CHANGED", "账户已变化,请重新读取模型目录");
106
+ if (!catalogResponse.ok)
107
+ throw new AuthoringBridgeError("E_OPERATION_UNCERTAIN", "账户模型目录暂时不可用");
108
+ const catalog = await boundedJson(catalogResponse, MAX_CATALOG_BYTES);
109
+ if (catalog.project_id !== input.projectId || typeof catalog.principal_id !== "string" || catalog.principal_id.length === 0 || typeof catalog.catalog_digest !== "string" || !/^[a-f0-9]{64}$/u.test(catalog.catalog_digest) || !Array.isArray(catalog.models))
110
+ throw new AuthoringBridgeError("E_VALIDATION_FAILED", "账户模型目录响应无效");
111
+ const seen = new Set();
112
+ const models = catalog.models.flatMap((entry) => {
113
+ const item = object(entry);
114
+ const model = item?.model;
115
+ const max = item?.max_output_tokens;
116
+ if (typeof model !== "string" || !MODEL.test(model) || seen.has(model) || typeof max !== "number" || !Number.isSafeInteger(max) || max < 1 || max > 1_000_000)
117
+ return [];
118
+ seen.add(model);
119
+ return [Object.freeze({ model, max_output_tokens: max })];
120
+ });
121
+ if (models.length !== catalog.models.length)
122
+ throw new AuthoringBridgeError("E_VALIDATION_FAILED", "账户模型目录包含无效条目");
123
+ return Object.freeze({ accountIdentity: identity, catalogDigest: catalog.catalog_digest, models: Object.freeze(models) });
124
+ }
@@ -0,0 +1,28 @@
1
+ export interface LocalPreviewFrozenAsset {
2
+ readonly id: string;
3
+ readonly filename: string;
4
+ readonly mime: string;
5
+ readonly size: number;
6
+ /** 同次双读绑定的 manifest snapshot 时间,不是作者原始媒体创建时间。 */
7
+ readonly createdTs: number;
8
+ readonly source: "capture";
9
+ /** Host 用于拒绝旧 catalog entry;不应转发给不可信 iframe。 */
10
+ readonly sha256: string;
11
+ }
12
+ export interface LocalPreviewResolvedAssetBytes {
13
+ readonly id: string;
14
+ readonly mime: string;
15
+ readonly sha256: string;
16
+ readonly bytes: Uint8Array;
17
+ }
18
+ export interface LocalPreviewFrozenAssetResolver {
19
+ /** 仅返回媒体元数据,不返回作者路径或源 base64。 */
20
+ list(): readonly LocalPreviewFrozenAsset[];
21
+ /** 仅供 Host/父页面消费,调用方必须传回同次 list 得到的 hash fence。 */
22
+ resolveBytes(asset: Readonly<Pick<LocalPreviewFrozenAsset, "id" | "sha256">>): LocalPreviewResolvedAssetBytes;
23
+ }
24
+ /**
25
+ * 从 LocalPreviewRuntime.readProjectionSnapshot 的冻结 resource_refs 建立 Host-only catalog。
26
+ * 不读取作者文件系统路径,也不接受 URI;所有媒体字节都来自同一 immutable capture。
27
+ */
28
+ export declare function localPreviewAssetsFromRuntimeSnapshot(snapshot: unknown): LocalPreviewFrozenAssetResolver;
@@ -0,0 +1,124 @@
1
+ import { createHash } from "node:crypto";
2
+ import { AuthoringBridgeError } from "@world-engines/authoring-bridge";
3
+ const SHA256 = /^[a-f0-9]{64}$/u;
4
+ const BASE64 = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/u;
5
+ const SAFE_MEDIA_TYPES = new Set([
6
+ "image/avif",
7
+ "image/gif",
8
+ "image/jpeg",
9
+ "image/png",
10
+ "image/webp",
11
+ "audio/aac",
12
+ "audio/flac",
13
+ "audio/mpeg",
14
+ "audio/mp4",
15
+ "audio/ogg",
16
+ "audio/wav",
17
+ "audio/webm",
18
+ "video/mp4",
19
+ "video/ogg",
20
+ "video/webm",
21
+ ]);
22
+ function fail(message) {
23
+ throw new AuthoringBridgeError("E_OPERATION_UNCERTAIN", message);
24
+ }
25
+ function record(value, message) {
26
+ if (typeof value !== "object" || value === null || Array.isArray(value))
27
+ fail(message);
28
+ return value;
29
+ }
30
+ function sha256(bytes) {
31
+ return createHash("sha256").update(bytes).digest("hex");
32
+ }
33
+ function resourcePath(value, seen) {
34
+ if (typeof value !== "string" || value.length === 0 || value.length > 1024 || value.startsWith("/")
35
+ || value.includes("\\") || value.split("/").some((segment) => segment === "" || segment === "." || segment === "..")
36
+ || seen.has(value)) {
37
+ fail("本地 Preview capture resource path 无效或重复");
38
+ }
39
+ seen.add(value);
40
+ return value;
41
+ }
42
+ function mediaType(value) {
43
+ if (typeof value !== "string" || !SAFE_MEDIA_TYPES.has(value)) {
44
+ fail("本地 Preview capture resource media_type 不受支持");
45
+ }
46
+ return value;
47
+ }
48
+ function canonicalBase64(value) {
49
+ if (typeof value !== "string" || !BASE64.test(value))
50
+ fail("本地 Preview capture resource base64 无效");
51
+ const bytes = new Uint8Array(Buffer.from(value, "base64"));
52
+ if (Buffer.from(bytes).toString("base64") !== value)
53
+ fail("本地 Preview capture resource base64 非 canonical");
54
+ return bytes;
55
+ }
56
+ function assetId(captureSha256, path) {
57
+ return `local-preview-asset:v1:${captureSha256}:${sha256(path).slice(0, 32)}`;
58
+ }
59
+ /**
60
+ * 从 LocalPreviewRuntime.readProjectionSnapshot 的冻结 resource_refs 建立 Host-only catalog。
61
+ * 不读取作者文件系统路径,也不接受 URI;所有媒体字节都来自同一 immutable capture。
62
+ */
63
+ export function localPreviewAssetsFromRuntimeSnapshot(snapshot) {
64
+ const root = record(snapshot, "本地 Preview asset snapshot 无效");
65
+ if (root.kind !== "worldengine.local-preview-projection/v1" || root.development_preview !== true) {
66
+ fail("本地 Preview asset snapshot identity 无效");
67
+ }
68
+ const identity = record(root.capture_identity, "本地 Preview asset capture identity 缺失");
69
+ const captureSha256 = identity.capture_sha256;
70
+ if (typeof captureSha256 !== "string" || !SHA256.test(captureSha256))
71
+ fail("本地 Preview asset capture hash 无效");
72
+ const snapshotUpdatedAtMs = identity.snapshot_updated_at_ms;
73
+ if (typeof snapshotUpdatedAtMs !== "number" || !Number.isSafeInteger(snapshotUpdatedAtMs) || snapshotUpdatedAtMs < 0) {
74
+ fail("本地 Preview asset snapshot timestamp 无效");
75
+ }
76
+ const projection = record(root.source_projection, "本地 Preview asset source projection 缺失");
77
+ const refs = projection.resource_refs;
78
+ if (!Array.isArray(refs))
79
+ fail("本地 Preview asset resource_refs 必须为数组");
80
+ const paths = new Set();
81
+ const ids = new Set();
82
+ const captured = refs.map((candidate) => {
83
+ const ref = record(candidate, "本地 Preview capture resource 无效");
84
+ if (Object.keys(ref).some((key) => key !== "path" && key !== "media_type" && key !== "bytes_base64")) {
85
+ fail("本地 Preview capture resource 包含未知字段");
86
+ }
87
+ const path = resourcePath(ref.path, paths);
88
+ const mime = mediaType(ref.media_type);
89
+ const bytes = canonicalBase64(ref.bytes_base64);
90
+ const id = assetId(captureSha256, path);
91
+ if (ids.has(id))
92
+ fail("本地 Preview capture asset id 冲突");
93
+ ids.add(id);
94
+ const metadata = Object.freeze({
95
+ id,
96
+ filename: path.slice(path.lastIndexOf("/") + 1),
97
+ mime,
98
+ size: bytes.byteLength,
99
+ createdTs: snapshotUpdatedAtMs,
100
+ source: "capture",
101
+ sha256: sha256(bytes),
102
+ });
103
+ return Object.freeze({ metadata, bytes });
104
+ });
105
+ const byId = new Map(captured.map((entry) => [entry.metadata.id, entry]));
106
+ const metadata = Object.freeze(captured.map((entry) => entry.metadata));
107
+ return Object.freeze({
108
+ list: () => metadata,
109
+ resolveBytes(asset) {
110
+ const entry = byId.get(asset.id);
111
+ if (entry === undefined)
112
+ fail("本地 Preview capture asset 不存在或不属于当前 capture");
113
+ if (!SHA256.test(asset.sha256) || asset.sha256 !== entry.metadata.sha256 || sha256(entry.bytes) !== entry.metadata.sha256) {
114
+ fail("本地 Preview capture asset hash 已过期或不一致");
115
+ }
116
+ return Object.freeze({
117
+ id: entry.metadata.id,
118
+ mime: entry.metadata.mime,
119
+ sha256: entry.metadata.sha256,
120
+ bytes: entry.bytes.slice(),
121
+ });
122
+ },
123
+ });
124
+ }