@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,238 @@
1
+ import { createHash, randomUUID } from "node:crypto";
2
+ import { open, readFile, rename, rm } from "node:fs/promises";
3
+ import { basename, dirname, join } from "node:path";
4
+ import { canonicalJson } from "@world-engines/scenario-review-snapshot";
5
+ import { AuthoringBridgeError } from "@world-engines/authoring-bridge";
6
+ import { sha256Hex } from "@world-engines/project-format";
7
+ import { buildAuthoringRecallCorpus } from "./desktop-authoring-runtime.js";
8
+ import { createCachedEmbeddingPort, readCachedEmbeddingVectors } from "./embedding-cache.js";
9
+ import { assertSafeExistingProjectPath, assertSafeProjectParent, ensureSafeProjectDirectory, safeProjectPath } from "./safe-project-path.js";
10
+ const PREPARED_PATH = ".worldengine/cache/embeddings/prepared-v1/current.json";
11
+ const INDEX_PATH = "__derived__/embedding-index-v1.json";
12
+ const MEDIA_TYPE = "application/vnd.chat.embedding-index.v1+json";
13
+ function fail(message) {
14
+ throw new AuthoringBridgeError("E_VALIDATION_FAILED", `${message};请先运行 worldengine recall prepare 或 review prepare 生成当前 revision 的冻结向量`);
15
+ }
16
+ function prepareRequired(message) {
17
+ throw new AuthoringBridgeError("E_VALIDATION_FAILED", `${message};请先运行 worldengine recall prepare 或 review prepare 生成当前 revision 的冻结向量`, {
18
+ failure: "E_REVIEW_PREPARE_REQUIRED",
19
+ });
20
+ }
21
+ function sha256(value) {
22
+ return createHash("sha256").update(value, "utf8").digest("hex");
23
+ }
24
+ async function currentBinding(projectRoot, expectedProjectId) {
25
+ const manifest = JSON.parse(await readFile(await assertSafeExistingProjectPath(projectRoot, join(projectRoot, "worldengine.project.json")), "utf8"));
26
+ if (manifest.project_id !== expectedProjectId || !Number.isSafeInteger(manifest.project_revision) || manifest.project_revision < 0)
27
+ fail("prepared embedding project binding 无效");
28
+ const source = new Uint8Array(await readFile(await assertSafeExistingProjectPath(projectRoot, join(projectRoot, "scenario", "source.wes"))));
29
+ return { project_revision: manifest.project_revision, source_sha256: await sha256Hex(source) };
30
+ }
31
+ async function atomicWrite(projectRoot, path, text) {
32
+ await assertSafeProjectParent(projectRoot, path);
33
+ const temporary = join(dirname(path), `.${basename(path)}.${randomUUID()}.tmp`);
34
+ const handle = await open(temporary, "wx");
35
+ try {
36
+ await handle.writeFile(text, "utf8");
37
+ await handle.sync();
38
+ }
39
+ catch (error) {
40
+ await handle.close();
41
+ await rm(temporary, { force: true });
42
+ throw error;
43
+ }
44
+ await handle.close();
45
+ try {
46
+ await rename(temporary, path);
47
+ }
48
+ catch (error) {
49
+ await rm(temporary, { force: true });
50
+ throw error;
51
+ }
52
+ }
53
+ /** 仅由显式 frozen-review embed 步骤写入;普通 search 不会生成 corpus vector。 */
54
+ export function persistPreparedFrozenEmbeddingBatch(options) {
55
+ return async (request) => {
56
+ const before = await currentBinding(options.projectRoot, options.projectId);
57
+ const result = await options.runBatch(request);
58
+ const descriptorBase = {
59
+ provider: result.descriptor.provider,
60
+ model: result.descriptor.model,
61
+ dimension: result.descriptor.dimension,
62
+ source_revision: result.descriptor.source_revision,
63
+ source_revision_digest: result.descriptor.source_revision_digest,
64
+ };
65
+ if (result.descriptor.source_revision !== request.source.source_revision
66
+ || result.descriptor.source_revision_digest !== request.source.revision_digest
67
+ || result.descriptor.descriptor_sha256 !== sha256(canonicalJson(descriptorBase))
68
+ || result.entries.length !== request.entities.length
69
+ || new Set(result.entries.map((entry) => entry.entity_id)).size !== result.entries.length)
70
+ fail("prepared embedding batch 不完整或 identity 无效");
71
+ const expected = new Map(request.entities.map((entity) => [entity.entity_id, entity]));
72
+ for (const entry of result.entries) {
73
+ const source = expected.get(entry.entity_id);
74
+ if (source === undefined || source.source_kind !== entry.source_kind || source.source_text_sha256 !== entry.source_text_sha256
75
+ || entry.vector.length !== result.descriptor.dimension || entry.vector.some((value) => !Number.isFinite(value)))
76
+ fail("prepared embedding batch 绑定无效");
77
+ }
78
+ const binding = await currentBinding(options.projectRoot, options.projectId);
79
+ if (binding.project_revision !== before.project_revision || binding.source_sha256 !== before.source_sha256) {
80
+ fail("embedding 执行期间项目 source 已变化,拒绝持久化旧批次");
81
+ }
82
+ await ensureSafeProjectDirectory(options.projectRoot, ".worldengine/cache/embeddings/prepared-v1");
83
+ const path = await safeProjectPath(options.projectRoot, PREPARED_PATH);
84
+ const metadata = Object.freeze({
85
+ schema_version: 1,
86
+ project_id: options.projectId,
87
+ ...before,
88
+ source: Object.freeze({ ...request.source }),
89
+ descriptor: Object.freeze({ ...result.descriptor }),
90
+ entities: Object.freeze(request.entities.map((entity) => Object.freeze({ ...entity }))),
91
+ });
92
+ await atomicWrite(options.projectRoot, path, `${JSON.stringify(metadata)}\n`);
93
+ return result;
94
+ };
95
+ }
96
+ /**
97
+ * 只消费本地 WESP 的当前只读图投影并准备 corpus 向量;不依赖 review capture、远端
98
+ * source-ready 或结构化模型。写入前后的 revision/source CAS 仍由同一持久化模块检查。
99
+ */
100
+ export async function prepareLocalRecallSnapshot(host, embedding, options = {}) {
101
+ const assertCurrentAccount = options.assertCurrentAccount ?? embedding.assertCurrentAccount;
102
+ await assertCurrentAccount?.();
103
+ const before = await host.readSourceReadback();
104
+ if (before.project_revision < 1)
105
+ fail("本地 recall corpus 尚无可绑定的 source revision");
106
+ const projection = await host.readGraphProjectionReadback();
107
+ if (projection.project_revision !== before.project_revision || projection.source_sha256 !== before.source_sha256) {
108
+ fail("本地 recall 图投影与 source identity 不一致");
109
+ }
110
+ const corpus = buildAuthoringRecallCorpus(projection.authoring).filter((entry) => {
111
+ if (options.scopeId === undefined)
112
+ return true;
113
+ const entity = projection.authoring.entities.find((candidate) => (candidate.entity_id ?? candidate.id) === entry.entity_id);
114
+ const props = entity?.props;
115
+ return typeof props === "object" && props !== null && !Array.isArray(props)
116
+ && (props.scope_id ?? props.scopeId) === options.scopeId;
117
+ });
118
+ if (corpus.length === 0)
119
+ fail("本地 recall corpus 为空");
120
+ const cachedEmbedding = await createCachedEmbeddingPort({ projectRoot: host.root, embedding, origin: embedding.origin });
121
+ const entities = await Promise.all(corpus.map(async (entry) => Object.freeze({
122
+ ...entry,
123
+ source_text_sha256: await sha256Hex(new TextEncoder().encode(entry.source_text)),
124
+ })));
125
+ const runBatch = persistPreparedFrozenEmbeddingBatch({
126
+ projectRoot: host.root,
127
+ projectId: before.project_id,
128
+ runBatch: async (request) => {
129
+ const vectors = await cachedEmbedding.embed(request.entities.map((entity) => entity.source_text), options.signal === undefined ? undefined : { signal: options.signal });
130
+ await assertCurrentAccount?.();
131
+ if (vectors.length !== request.entities.length
132
+ || vectors.some((vector) => vector.length !== cachedEmbedding.dimensions || vector.some((value) => !Number.isFinite(value)))) {
133
+ fail("本地 recall embedding provider result 无效");
134
+ }
135
+ const descriptorBase = Object.freeze({
136
+ provider: cachedEmbedding.providerId,
137
+ model: cachedEmbedding.modelId,
138
+ dimension: cachedEmbedding.dimensions,
139
+ source_revision: request.source.source_revision,
140
+ source_revision_digest: request.source.revision_digest,
141
+ });
142
+ return Object.freeze({
143
+ descriptor: Object.freeze({ ...descriptorBase, descriptor_sha256: sha256(canonicalJson(descriptorBase)) }),
144
+ entries: Object.freeze(request.entities.map((entity, index) => Object.freeze({
145
+ entity_id: entity.entity_id,
146
+ source_kind: entity.source_kind,
147
+ source_text_sha256: entity.source_text_sha256,
148
+ vector: Object.freeze([...vectors[index]]),
149
+ }))),
150
+ });
151
+ },
152
+ });
153
+ await runBatch(Object.freeze({
154
+ source: Object.freeze({
155
+ owner_id: `local:${before.project_id}`,
156
+ scenario_id: before.project_id,
157
+ source_revision: before.project_revision,
158
+ revision_digest: before.source_sha256,
159
+ }),
160
+ entities: Object.freeze(entities),
161
+ ...(options.signal === undefined ? {} : { signal: options.signal }),
162
+ }));
163
+ const current = await host.readSourceReadback();
164
+ if (current.project_id !== before.project_id || current.project_revision !== before.project_revision
165
+ || current.source_sha256 !== before.source_sha256)
166
+ fail("本地 recall prepare 完成后 source 已变化");
167
+ await assertCurrentAccount?.();
168
+ const prepared = await createPreparedEmbeddingSnapshotReader({
169
+ projectRoot: host.root,
170
+ projectId: before.project_id,
171
+ embedding: cachedEmbedding,
172
+ origin: cachedEmbedding.origin,
173
+ ...(assertCurrentAccount === undefined ? {} : { assertCurrentAccount }),
174
+ })({
175
+ project_id: before.project_id,
176
+ project_revision: before.project_revision,
177
+ source_sha256: before.source_sha256,
178
+ ...(options.scopeId === undefined ? {} : { scope_id: options.scopeId }),
179
+ entities,
180
+ });
181
+ if (prepared === null)
182
+ fail("本地 recall snapshot 无法读回");
183
+ return prepared;
184
+ }
185
+ /** 从 review prepare 已写入的 metadata + embedding cache 只读重建 formal snapshot。 */
186
+ export function createPreparedEmbeddingSnapshotReader(options) {
187
+ return async (request) => {
188
+ const assertCurrentAccount = options.assertCurrentAccount ?? options.embedding.assertCurrentAccount;
189
+ await assertCurrentAccount?.();
190
+ let metadata;
191
+ try {
192
+ metadata = JSON.parse(await readFile(await assertSafeExistingProjectPath(options.projectRoot, join(options.projectRoot, ...PREPARED_PATH.split("/"))), "utf8"));
193
+ }
194
+ catch {
195
+ return prepareRequired("当前项目没有 prepared embedding snapshot");
196
+ }
197
+ if (metadata.schema_version !== 1 || metadata.project_id !== options.projectId || metadata.project_id !== request.project_id
198
+ || metadata.project_revision !== request.project_revision || metadata.source_sha256 !== request.source_sha256)
199
+ prepareRequired("prepared embedding snapshot 已过期");
200
+ const descriptor = metadata.descriptor;
201
+ if (descriptor.provider !== options.embedding.providerId || descriptor.model !== options.embedding.modelId || descriptor.dimension !== options.embedding.dimensions)
202
+ prepareRequired("prepared embedding provider identity 不匹配");
203
+ const available = new Map(metadata.entities.map((entity) => [entity.entity_id, entity]));
204
+ const selected = request.entities;
205
+ if (selected.length === 0)
206
+ prepareRequired("prepared embedding corpus 为空");
207
+ for (const entity of selected) {
208
+ const stored = available.get(entity.entity_id);
209
+ if (stored === undefined || stored.source_kind !== entity.source_kind || stored.source_text !== entity.source_text || stored.source_text_sha256 !== entity.source_text_sha256)
210
+ prepareRequired("prepared embedding corpus 与当前 source 不匹配");
211
+ }
212
+ const cachedVectors = await readCachedEmbeddingVectors({ projectRoot: options.projectRoot, embedding: options.embedding, origin: options.origin }, selected.map((entity) => entity.source_text_sha256));
213
+ if (cachedVectors === null)
214
+ prepareRequired("prepared embedding vector cache 不完整");
215
+ await assertCurrentAccount?.();
216
+ const entries = [];
217
+ for (const [index, entity] of selected.entries()) {
218
+ entries.push(Object.freeze({ entity_id: entity.entity_id, source_kind: entity.source_kind, source_text_sha256: entity.source_text_sha256, vector: cachedVectors[index] }));
219
+ }
220
+ const indexEntries = await Promise.all(entries.map(async (entry) => Object.freeze({
221
+ entity_id: entry.entity_id, source_kind: entry.source_kind, source_text_sha256: entry.source_text_sha256,
222
+ vector_sha256: await sha256Hex(new TextEncoder().encode(canonicalJson(entry.vector))),
223
+ })));
224
+ const vectors = new Map(entries.map((entry) => [entry.entity_id, entry.vector]));
225
+ return Object.freeze({
226
+ project_id: request.project_id, project_revision: request.project_revision, source_sha256: request.source_sha256,
227
+ scope_id: request.scope_id ?? "__all__",
228
+ snapshot: Object.freeze({
229
+ source: Object.freeze({ ...metadata.source }),
230
+ batch: Object.freeze({ descriptor: Object.freeze({ ...descriptor }), entries: Object.freeze(entries) }),
231
+ materialization: Object.freeze({ descriptor: Object.freeze({ ...descriptor }), vector_by_entity_id: vectors, index_resource: Object.freeze({
232
+ path: INDEX_PATH, media_type: MEDIA_TYPE,
233
+ bytes: new TextEncoder().encode(canonicalJson({ schema_version: 1, ...descriptor, entries: indexEntries })),
234
+ }) }),
235
+ }),
236
+ });
237
+ };
238
+ }
@@ -0,0 +1,53 @@
1
+ import type { DesktopAuthoringCapabilities } from "./desktop-authoring-runtime.js";
2
+ import type { PreviewSession } from "./preview-runtime.js";
3
+ export declare const MAX_PREVIEW_DOM_CHARS: number;
4
+ export declare const MAX_PREVIEW_TEXT_CHARS: number;
5
+ export declare const MAX_PREVIEW_CONSOLE_ENTRIES = 100;
6
+ export declare const MAX_PREVIEW_CONSOLE_CHARS: number;
7
+ export declare const MAX_PREVIEW_SCREENSHOT_BYTES: number;
8
+ export interface PreviewBrowserConsoleEntry {
9
+ readonly level: "debug" | "info" | "warning" | "error";
10
+ readonly text: string;
11
+ }
12
+ export interface PreviewBrowserScreenshot {
13
+ readonly bytes: Uint8Array;
14
+ readonly mimeType: "image/png" | "image/jpeg" | "image/webp";
15
+ readonly width?: number;
16
+ readonly height?: number;
17
+ }
18
+ export interface PreviewBrowserInspectCapture {
19
+ /** 浏览器实际看到的最终 URL;adapter 会重新做 session origin/base-path 绑定校验。 */
20
+ readonly url: string;
21
+ readonly ready: boolean;
22
+ readonly dom?: string;
23
+ readonly text?: string;
24
+ readonly console?: readonly PreviewBrowserConsoleEntry[];
25
+ readonly screenshot?: PreviewBrowserScreenshot;
26
+ }
27
+ export interface PreviewBrowserDriver {
28
+ inspect(request: Readonly<{
29
+ sessionId: string;
30
+ url: string;
31
+ selector?: string;
32
+ includeScreenshot: boolean;
33
+ includeConsole: boolean;
34
+ }>): Promise<PreviewBrowserInspectCapture>;
35
+ interact(request: Readonly<{
36
+ sessionId: string;
37
+ url: string;
38
+ action: "click" | "type" | "press" | "scroll" | "navigate";
39
+ target: string;
40
+ value?: string;
41
+ }>): Promise<Readonly<{
42
+ url: string;
43
+ }>>;
44
+ }
45
+ export interface PreviewBrowserAdapterOptions {
46
+ readonly inspectSession: () => PreviewSession | null;
47
+ readonly driver: PreviewBrowserDriver;
48
+ }
49
+ /**
50
+ * 把外部 harness 的真实浏览器端口收敛到 exact-21 Preview GUI hooks。
51
+ * 这里不实现浏览器,也不模拟 ChatPlay 握手或业务响应;driver 只能获得当前 session 的受控 URL。
52
+ */
53
+ export declare function createPreviewBrowserCapabilities(options: PreviewBrowserAdapterOptions): Pick<DesktopAuthoringCapabilities, "preview_inspect" | "preview_interact">;
@@ -0,0 +1,171 @@
1
+ import { createHash } from "node:crypto";
2
+ import { AuthoringBridgeError } from "@world-engines/authoring-bridge";
3
+ export const MAX_PREVIEW_DOM_CHARS = 256 * 1024;
4
+ export const MAX_PREVIEW_TEXT_CHARS = 64 * 1024;
5
+ export const MAX_PREVIEW_CONSOLE_ENTRIES = 100;
6
+ export const MAX_PREVIEW_CONSOLE_CHARS = 8 * 1024;
7
+ export const MAX_PREVIEW_SCREENSHOT_BYTES = 4 * 1024 * 1024;
8
+ function fail(code, message) {
9
+ throw new AuthoringBridgeError(code, message);
10
+ }
11
+ function validSession(options) {
12
+ const session = options.inspectSession();
13
+ if (session === null || session.state !== "running") {
14
+ return fail("E_CAPABILITY_UNAVAILABLE", "Preview 浏览器仅在已验证 running session 中可用");
15
+ }
16
+ let base;
17
+ try {
18
+ base = new URL(session.url);
19
+ }
20
+ catch {
21
+ return fail("E_CAPABILITY_UNAVAILABLE", "Preview session URL 无效");
22
+ }
23
+ if (base.protocol !== "http:"
24
+ || base.hostname !== "localhost"
25
+ || !/^\/_worldengine_preview\/[a-f0-9]{32}\/$/u.test(base.pathname)
26
+ || base.search !== ""
27
+ || base.hash !== "") {
28
+ return fail("E_CAPABILITY_UNAVAILABLE", "Preview session 未绑定到受控 loopback capability path");
29
+ }
30
+ return { session, base };
31
+ }
32
+ function boundedString(value, label, maxChars, allowEmpty = true) {
33
+ if (value === undefined)
34
+ return undefined;
35
+ if (typeof value !== "string" || (!allowEmpty && value.length === 0) || value.length > maxChars || /\0/u.test(value)) {
36
+ return fail("E_SCHEMA_INVALID", `${label} 无效或超过 ${maxChars} 字符`);
37
+ }
38
+ return value;
39
+ }
40
+ function assertBoundUrl(value, base, label) {
41
+ let url;
42
+ try {
43
+ url = new URL(value, base);
44
+ }
45
+ catch {
46
+ return fail("E_SCHEMA_INVALID", `${label} 不是有效 URL`);
47
+ }
48
+ if (url.origin !== base.origin || !url.pathname.startsWith(base.pathname)) {
49
+ return fail("E_SCHEMA_INVALID", `${label} 必须位于当前 Preview origin 与 capability path 内`);
50
+ }
51
+ return url;
52
+ }
53
+ function boundedOutput(value, maxChars) {
54
+ if (value === undefined)
55
+ return { truncated: false };
56
+ return value.length <= maxChars
57
+ ? { value, truncated: false }
58
+ : { value: value.slice(0, maxChars), truncated: true };
59
+ }
60
+ function boundedConsole(entries) {
61
+ if (entries === undefined)
62
+ return { entries: Object.freeze([]), truncated: false };
63
+ const selected = entries.slice(0, MAX_PREVIEW_CONSOLE_ENTRIES).map((entry) => {
64
+ const level = entry.level;
65
+ if (level !== "debug" && level !== "info" && level !== "warning" && level !== "error") {
66
+ return fail("E_SCHEMA_INVALID", "Preview console level 无效");
67
+ }
68
+ const text = boundedString(entry.text, "Preview console text", MAX_PREVIEW_CONSOLE_CHARS) ?? "";
69
+ return Object.freeze({ level, text });
70
+ });
71
+ return { entries: Object.freeze(selected), truncated: entries.length > selected.length };
72
+ }
73
+ function screenshotResult(value) {
74
+ if (value === undefined)
75
+ return undefined;
76
+ if (value.mimeType !== "image/png" && value.mimeType !== "image/jpeg" && value.mimeType !== "image/webp") {
77
+ return fail("E_SCHEMA_INVALID", "Preview screenshot MIME 无效");
78
+ }
79
+ if (value.bytes.byteLength === 0 || value.bytes.byteLength > MAX_PREVIEW_SCREENSHOT_BYTES) {
80
+ return fail("E_SCHEMA_INVALID", `Preview screenshot 必须在 1..${MAX_PREVIEW_SCREENSHOT_BYTES} bytes 内`);
81
+ }
82
+ const dimension = (candidate, label) => {
83
+ if (candidate === undefined)
84
+ return undefined;
85
+ if (!Number.isSafeInteger(candidate) || candidate <= 0 || candidate > 16_384) {
86
+ return fail("E_SCHEMA_INVALID", `${label} 无效`);
87
+ }
88
+ return candidate;
89
+ };
90
+ return Object.freeze({
91
+ mime_type: value.mimeType,
92
+ byte_length: value.bytes.byteLength,
93
+ sha256: createHash("sha256").update(value.bytes).digest("hex"),
94
+ ...(value.width === undefined ? {} : { width: dimension(value.width, "Preview screenshot width") }),
95
+ ...(value.height === undefined ? {} : { height: dimension(value.height, "Preview screenshot height") }),
96
+ data_base64: Buffer.from(value.bytes).toString("base64"),
97
+ });
98
+ }
99
+ /**
100
+ * 把外部 harness 的真实浏览器端口收敛到 exact-21 Preview GUI hooks。
101
+ * 这里不实现浏览器,也不模拟 ChatPlay 握手或业务响应;driver 只能获得当前 session 的受控 URL。
102
+ */
103
+ export function createPreviewBrowserCapabilities(options) {
104
+ return Object.freeze({
105
+ preview_inspect: async (input) => {
106
+ const { session, base } = validSession(options);
107
+ const selector = boundedString(input.selector, "preview_inspect.selector", 4_096);
108
+ const includeScreenshot = input.include_screenshot === true;
109
+ const includeConsole = input.include_console === true;
110
+ const capture = await options.driver.inspect({
111
+ sessionId: session.session_id,
112
+ url: base.href,
113
+ ...(selector === undefined || selector === "" ? {} : { selector }),
114
+ includeScreenshot,
115
+ includeConsole,
116
+ });
117
+ const capturedUrl = assertBoundUrl(capture.url, base, "Preview 浏览器回读 URL");
118
+ const dom = boundedOutput(capture.dom, MAX_PREVIEW_DOM_CHARS);
119
+ const text = boundedOutput(capture.text, MAX_PREVIEW_TEXT_CHARS);
120
+ const consoleOutput = includeConsole ? boundedConsole(capture.console) : boundedConsole(undefined);
121
+ const screenshot = includeScreenshot ? screenshotResult(capture.screenshot) : undefined;
122
+ if (includeScreenshot && screenshot === undefined) {
123
+ return fail("E_CAPABILITY_UNAVAILABLE", "浏览器未返回请求的 Preview screenshot");
124
+ }
125
+ return Object.freeze({
126
+ schema_version: 1,
127
+ preview: Object.freeze({
128
+ session_id: session.session_id,
129
+ state: session.state,
130
+ ready: capture.ready,
131
+ url: capturedUrl.href,
132
+ path: `${capturedUrl.pathname}${capturedUrl.search}${capturedUrl.hash}`,
133
+ }),
134
+ ...(selector === undefined || selector === "" ? {} : { selector }),
135
+ ...(dom.value === undefined ? {} : { dom: dom.value }),
136
+ ...(text.value === undefined ? {} : { text: text.value }),
137
+ output_truncated: dom.truncated || text.truncated || consoleOutput.truncated,
138
+ ...(includeConsole ? { console: consoleOutput.entries } : {}),
139
+ ...(screenshot === undefined ? {} : { screenshot }),
140
+ });
141
+ },
142
+ preview_interact: async (input) => {
143
+ const { session, base } = validSession(options);
144
+ const action = input.action;
145
+ if (action !== "click" && action !== "type" && action !== "press" && action !== "scroll" && action !== "navigate") {
146
+ return fail("E_SCHEMA_INVALID", "preview_interact.action 不在白名单中");
147
+ }
148
+ const rawTarget = boundedString(input.target, "preview_interact.target", 4_096, false);
149
+ const valueLimit = action === "type" ? 64 * 1024 : 512;
150
+ const value = boundedString(input.value, "preview_interact.value", valueLimit);
151
+ const target = action === "navigate"
152
+ ? assertBoundUrl(rawTarget, base, "Preview navigation target").href
153
+ : rawTarget;
154
+ const result = await options.driver.interact({
155
+ sessionId: session.session_id,
156
+ url: base.href,
157
+ action,
158
+ target,
159
+ ...(value === undefined ? {} : { value }),
160
+ });
161
+ const current = assertBoundUrl(result.url, base, "Preview 浏览器交互回读 URL");
162
+ return Object.freeze({
163
+ schema_version: 1,
164
+ session_id: session.session_id,
165
+ action,
166
+ url: current.href,
167
+ path: `${current.pathname}${current.search}${current.hash}`,
168
+ });
169
+ },
170
+ });
171
+ }
@@ -0,0 +1,67 @@
1
+ import type { Readable } from "node:stream";
2
+ /** 浏览器使用独立 hostname,避免 127.0.0.1 的 host-only GUI cookie 随 Preview 请求发送。 */
3
+ export declare const LOCAL_PREVIEW_HOST = "localhost";
4
+ /** Socket 始终绑定 IPv4 loopback,避免 localhost 的 IPv6 DNS 解析歧义。 */
5
+ export declare const LOCAL_PREVIEW_BIND_HOST = "127.0.0.1";
6
+ export declare const MAX_PREVIEW_OUTPUT_BYTES: number;
7
+ export type PreviewSessionState = "starting" | "running" | "stopped" | "failed";
8
+ export interface PreviewSession {
9
+ readonly session_id: string;
10
+ readonly project_root: string;
11
+ readonly cwd: string;
12
+ readonly url: string;
13
+ readonly port: number;
14
+ readonly state: PreviewSessionState;
15
+ readonly pid: number | null;
16
+ readonly started_at_ms: number;
17
+ readonly stopped_at_ms: number | null;
18
+ readonly exit_code: number | null;
19
+ readonly stdout: string;
20
+ readonly stderr: string;
21
+ readonly stdout_truncated: boolean;
22
+ readonly stderr_truncated: boolean;
23
+ }
24
+ export interface PreviewChildProcess {
25
+ readonly pid?: number | undefined;
26
+ readonly stdout: Readable;
27
+ readonly stderr: Readable;
28
+ once(event: "error", listener: (error: Error) => void): unknown;
29
+ once(event: "exit", listener: (code: number | null, signal: NodeJS.Signals | null) => void): unknown;
30
+ once(event: "close", listener: (code: number | null, signal: NodeJS.Signals | null) => void): unknown;
31
+ }
32
+ export interface PreviewRuntimeDependencies {
33
+ readonly now?: () => number;
34
+ readonly newSessionId?: () => string;
35
+ readonly spawn?: (command: string, args: readonly string[], cwd: string) => PreviewChildProcess;
36
+ readonly findListeningPids?: (port: number) => Promise<readonly number[]>;
37
+ readonly getProcessCommandLine?: (pid: number) => Promise<string | null>;
38
+ readonly isProcessInTree?: (rootPid: number, pid: number) => Promise<boolean>;
39
+ readonly terminateProcessTree?: (pid: number) => Promise<void>;
40
+ readonly reserveLoopbackPort?: () => Promise<ReservedLoopbackPort>;
41
+ }
42
+ export interface ReservedLoopbackPort {
43
+ readonly port: number;
44
+ release(): Promise<void>;
45
+ }
46
+ export declare class PreviewRuntimeError extends Error {
47
+ readonly code: "E_PATH_OUTSIDE_PROJECT" | "E_PREVIEW_START_FAILED" | "E_PROJECT_BUSY";
48
+ constructor(code: PreviewRuntimeError["code"], message: string);
49
+ }
50
+ export declare function previewChildEnvironment(base?: NodeJS.ProcessEnv, nodeExecutable?: string): NodeJS.ProcessEnv;
51
+ /** 跨进程只读 owner 探针;只返回 PID、listener 与 cwd 三者都能验证的 live ledger。 */
52
+ export declare function readLivePreviewSessionOwner(projectRoot: string): Promise<Readonly<{
53
+ session_id: string;
54
+ pid: number;
55
+ port: number;
56
+ project_root: string;
57
+ cwd: string;
58
+ url: string;
59
+ }> | null>;
60
+ /** ProjectHost 的本地 Vite Preview 生命周期;每个 session 独占一个内核分配的 loopback 端口。 */
61
+ export declare class LocalPreviewRuntime {
62
+ #private;
63
+ constructor(dependencies?: PreviewRuntimeDependencies);
64
+ start(projectRoot: string, parentOrigin?: string): Promise<PreviewSession>;
65
+ inspect(): PreviewSession | null;
66
+ stop(): Promise<PreviewSession | null>;
67
+ }