@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.
- package/LICENSE +46 -0
- package/assets/gui/WELogo-title.svg +11 -0
- package/assets/gui/app.js +1326 -0
- package/assets/gui/index.html +118 -0
- package/assets/gui/local-preview-asset-urls.js +61 -0
- package/assets/gui/local-preview-bridge.js +393 -0
- package/assets/gui/spatial-template-panel.js +233 -0
- package/assets/gui/styles.css +115 -0
- package/assets/gui/terrain-render.js +120 -0
- package/assets/gui/workspace-status.css +17 -0
- package/assets/gui/workspace-status.js +255 -0
- package/assets/gui/world-map-globe.js +187 -0
- package/assets/gui/world-map-worker.js +62 -0
- package/assets/gui/world-map.css +30 -0
- package/assets/gui/world-map.js +2466 -0
- package/dist/account-model-proxy.d.ts +21 -0
- package/dist/account-model-proxy.js +470 -0
- package/dist/authoring-browser-runtime.d.ts +7 -0
- package/dist/authoring-browser-runtime.js +110 -0
- package/dist/authoring-ingestion.d.ts +38 -0
- package/dist/authoring-ingestion.js +410 -0
- package/dist/authoring-model-connection.d.ts +166 -0
- package/dist/authoring-model-connection.js +757 -0
- package/dist/chinese-lexical.d.ts +152 -0
- package/dist/chinese-lexical.js +555 -0
- package/dist/chromium-preview-browser-driver.d.ts +43 -0
- package/dist/chromium-preview-browser-driver.js +575 -0
- package/dist/cli/download.d.ts +12 -0
- package/dist/cli/download.js +50 -0
- package/dist/cli/dump.d.ts +32 -0
- package/dist/cli/dump.js +91 -0
- package/dist/cli/ingest.d.ts +49 -0
- package/dist/cli/ingest.js +225 -0
- package/dist/cli/review.d.ts +10 -0
- package/dist/cli/review.js +44 -0
- package/dist/cli/upload.d.ts +57 -0
- package/dist/cli/upload.js +112 -0
- package/dist/cli-command.d.ts +60 -0
- package/dist/cli-command.js +613 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +182 -0
- package/dist/desktop-authoring-runtime.d.ts +88 -0
- package/dist/desktop-authoring-runtime.js +1602 -0
- package/dist/desktop-sidecar.d.ts +11 -0
- package/dist/desktop-sidecar.js +143 -0
- package/dist/document-dump.d.ts +113 -0
- package/dist/document-dump.js +431 -0
- package/dist/document-graph-extractor.d.ts +125 -0
- package/dist/document-graph-extractor.js +588 -0
- package/dist/document-ingestion-journal.d.ts +68 -0
- package/dist/document-ingestion-journal.js +471 -0
- package/dist/document-ingestion.d.ts +183 -0
- package/dist/document-ingestion.js +976 -0
- package/dist/document-query-path-proof.d.ts +30 -0
- package/dist/document-query-path-proof.js +34 -0
- package/dist/document-query-path-renderer.d.ts +50 -0
- package/dist/document-query-path-renderer.js +75 -0
- package/dist/document-query-proof-paths.d.ts +22 -0
- package/dist/document-query-proof-paths.js +71 -0
- package/dist/document-query.d.ts +97 -0
- package/dist/document-query.js +1106 -0
- package/dist/document-relation-role.d.ts +14 -0
- package/dist/document-relation-role.js +86 -0
- package/dist/document-source.d.ts +107 -0
- package/dist/document-source.js +255 -0
- package/dist/embedding-cache.d.ts +35 -0
- package/dist/embedding-cache.js +317 -0
- package/dist/frozen-review-local-pipeline.d.ts +88 -0
- package/dist/frozen-review-local-pipeline.js +609 -0
- package/dist/frozen-review-source-capture.d.ts +15 -0
- package/dist/frozen-review-source-capture.js +551 -0
- package/dist/frozen-review-wasm-encryptor.d.ts +16 -0
- package/dist/frozen-review-wasm-encryptor.js +72 -0
- package/dist/frozen-review.d.ts +131 -0
- package/dist/frozen-review.js +802 -0
- package/dist/graph-traversal.d.ts +23 -0
- package/dist/graph-traversal.js +100 -0
- package/dist/gui/graph-layout-worker.js +1 -0
- package/dist/gui/graph-layout.wasm +0 -0
- package/dist/gui/graph.css +1 -0
- package/dist/gui/graph.js +76 -0
- package/dist/gui/plane-generation.js +112 -0
- package/dist/gui/plane-generator.js +310 -0
- package/dist/gui/plane-generator_bg.wasm +0 -0
- package/dist/gui/procedural-city.js +595 -0
- package/dist/gui/road-generation.js +129 -0
- package/dist/gui/road-parcels.js +1584 -0
- package/dist/gui/score.css +1 -0
- package/dist/gui/score.js +54 -0
- package/dist/gui/spatial-runtime.js +1231 -0
- package/dist/gui/spatial-templates.js +200 -0
- package/dist/gui/trigger.css +1 -0
- package/dist/gui/trigger.js +142 -0
- package/dist/harness-callback-transport.d.ts +17 -0
- package/dist/harness-callback-transport.js +128 -0
- package/dist/harness-document-ingestion.d.ts +77 -0
- package/dist/harness-document-ingestion.js +343 -0
- package/dist/index.d.ts +43 -0
- package/dist/index.js +43 -0
- package/dist/ingestion-source-reconciliation.d.ts +88 -0
- package/dist/ingestion-source-reconciliation.js +253 -0
- package/dist/ladybug-runtime.d.ts +117 -0
- package/dist/ladybug-runtime.js +886 -0
- package/dist/local-account-embedding.d.ts +34 -0
- package/dist/local-account-embedding.js +294 -0
- package/dist/local-gallery.d.ts +35 -0
- package/dist/local-gallery.js +227 -0
- package/dist/local-gui-auth.d.ts +96 -0
- package/dist/local-gui-auth.js +703 -0
- package/dist/local-gui-contract.d.ts +53 -0
- package/dist/local-gui-contract.js +5 -0
- package/dist/local-gui-image-assets.d.ts +17 -0
- package/dist/local-gui-image-assets.js +96 -0
- package/dist/local-gui-preview-client.d.ts +3 -0
- package/dist/local-gui-preview-client.js +111 -0
- package/dist/local-gui-score.d.ts +72 -0
- package/dist/local-gui-score.js +184 -0
- package/dist/local-gui-server.d.ts +10 -0
- package/dist/local-gui-server.js +987 -0
- package/dist/local-gui-version.d.ts +17 -0
- package/dist/local-gui-version.js +147 -0
- package/dist/local-gui-workspace.d.ts +14 -0
- package/dist/local-gui-workspace.js +171 -0
- package/dist/local-model-settings.d.ts +28 -0
- package/dist/local-model-settings.js +124 -0
- package/dist/local-preview-assets.d.ts +28 -0
- package/dist/local-preview-assets.js +124 -0
- package/dist/local-preview-bound-resources.d.ts +12 -0
- package/dist/local-preview-bound-resources.js +80 -0
- package/dist/local-preview-message-store.d.ts +63 -0
- package/dist/local-preview-message-store.js +214 -0
- package/dist/local-preview-player-read-models.d.ts +47 -0
- package/dist/local-preview-player-read-models.js +90 -0
- package/dist/local-preview-projections.d.ts +61 -0
- package/dist/local-preview-projections.js +117 -0
- package/dist/local-preview-runtime.d.ts +105 -0
- package/dist/local-preview-runtime.js +553 -0
- package/dist/local-preview-session.d.ts +73 -0
- package/dist/local-preview-session.js +395 -0
- package/dist/local-preview-source.d.ts +40 -0
- package/dist/local-preview-source.js +192 -0
- package/dist/local-preview-view-events.d.ts +101 -0
- package/dist/local-preview-view-events.js +179 -0
- package/dist/local-preview-view-state-sqlite.d.ts +24 -0
- package/dist/local-preview-view-state-sqlite.js +263 -0
- package/dist/local-preview-view-state.d.ts +93 -0
- package/dist/local-preview-view-state.js +108 -0
- package/dist/local-preview-vite-config.d.ts +6 -0
- package/dist/local-preview-vite-config.js +122 -0
- package/dist/local-preview-world-effects.d.ts +9 -0
- package/dist/local-preview-world-effects.js +55 -0
- package/dist/local-preview-worldline-store.d.ts +47 -0
- package/dist/local-preview-worldline-store.js +282 -0
- package/dist/local-preview-worldlines.d.ts +95 -0
- package/dist/local-preview-worldlines.js +136 -0
- package/dist/mcp-async-dispatcher.d.ts +3 -0
- package/dist/mcp-async-dispatcher.js +129 -0
- package/dist/mcp-line-queue.d.ts +14 -0
- package/dist/mcp-line-queue.js +184 -0
- package/dist/mcp-pipe-internal.d.ts +6 -0
- package/dist/mcp-pipe-internal.js +151 -0
- package/dist/mcp-pipe-response.d.ts +14 -0
- package/dist/mcp-pipe-response.js +50 -0
- package/dist/mcp-stdio-response.d.ts +12 -0
- package/dist/mcp-stdio-response.js +27 -0
- package/dist/mcp.d.ts +27 -0
- package/dist/mcp.js +194 -0
- package/dist/prepared-embedding-cache.d.ts +38 -0
- package/dist/prepared-embedding-cache.js +238 -0
- package/dist/preview-browser-adapter.d.ts +53 -0
- package/dist/preview-browser-adapter.js +171 -0
- package/dist/preview-runtime.d.ts +67 -0
- package/dist/preview-runtime.js +460 -0
- package/dist/project-auth-transport.d.ts +27 -0
- package/dist/project-auth-transport.js +121 -0
- package/dist/project-auth.d.ts +51 -0
- package/dist/project-auth.js +108 -0
- package/dist/project-git-capture-recovery.d.ts +16 -0
- package/dist/project-git-capture-recovery.js +347 -0
- package/dist/project-git-cli.d.ts +5 -0
- package/dist/project-git-cli.js +40 -0
- package/dist/project-git-scenario-source.d.ts +10 -0
- package/dist/project-git-scenario-source.js +240 -0
- package/dist/project-git-view-source.d.ts +13 -0
- package/dist/project-git-view-source.js +134 -0
- package/dist/project-git.d.ts +97 -0
- package/dist/project-git.js +656 -0
- package/dist/project-host.d.ts +59 -0
- package/dist/project-host.js +471 -0
- package/dist/project-writer-lease.d.ts +18 -0
- package/dist/project-writer-lease.js +430 -0
- package/dist/public-error.d.ts +7 -0
- package/dist/public-error.js +61 -0
- package/dist/retrieval-benchmark.d.ts +46 -0
- package/dist/retrieval-benchmark.js +58 -0
- package/dist/safe-project-path.d.ts +8 -0
- package/dist/safe-project-path.js +93 -0
- package/dist/scenario-writer.d.ts +32 -0
- package/dist/scenario-writer.js +191 -0
- package/dist/transfer/download.d.ts +159 -0
- package/dist/transfer/download.js +598 -0
- package/dist/transfer/transport.d.ts +26 -0
- package/dist/transfer/transport.js +1 -0
- package/dist/transfer/upload.d.ts +162 -0
- package/dist/transfer/upload.js +354 -0
- package/dist/transfer-types.d.ts +95 -0
- package/dist/transfer-types.js +1 -0
- package/dist/transfer.d.ts +18 -0
- package/dist/transfer.js +273 -0
- package/dist/trigger-runtime.d.ts +121 -0
- package/dist/trigger-runtime.js +153 -0
- package/dist/vendor/wasm-core/asset-hashes.json +5 -0
- package/dist/vendor/wasm-core/wasm_core.d.ts +490 -0
- package/dist/vendor/wasm-core/wasm_core.js +1 -0
- package/dist/vendor/wasm-core/wasm_core_bg.wasm +0 -0
- package/dist/view-watcher.d.ts +9 -0
- package/dist/view-watcher.js +34 -0
- package/package.json +110 -0
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 本地 Preview 的 View custom state read model。
|
|
3
|
+
*
|
|
4
|
+
* 这个模块不拥有文件或 SQLite。Runtime owner 必须把 snapshot/commit 接到同一
|
|
5
|
+
* runtime SAV envelope 的 projection[viewId],并复用 runtime 的 head CAS 与
|
|
6
|
+
* operation receipt;因此不会产生第二个 local save authority。
|
|
7
|
+
*/
|
|
8
|
+
const ID = /^[A-Za-z0-9._:-]{1,128}$/u;
|
|
9
|
+
const NAMESPACE = "chatplay.custom-state.v1";
|
|
10
|
+
const MAX_ENTRIES = 256;
|
|
11
|
+
const MAX_AUTHORIZED_USERS = 64;
|
|
12
|
+
/** 单个 view custom-state entry 的真实持久化上限,供 Host capability 投影复用。 */
|
|
13
|
+
export const LOCAL_PREVIEW_CUSTOM_STATE_MAX_BYTES = 64 * 1024;
|
|
14
|
+
function fail(message) { throw new Error(`local_preview_view_state:${message}`); }
|
|
15
|
+
function publicScope(scope) {
|
|
16
|
+
return Object.freeze({ captureId: scope.captureId, namespace: scope.namespace, saveId: scope.saveId, viewId: scope.viewId });
|
|
17
|
+
}
|
|
18
|
+
function clone(value) { return JSON.parse(JSON.stringify(value)); }
|
|
19
|
+
function validJson(value) {
|
|
20
|
+
if (value === null || typeof value === "boolean" || typeof value === "string")
|
|
21
|
+
return true;
|
|
22
|
+
if (typeof value === "number")
|
|
23
|
+
return Number.isFinite(value);
|
|
24
|
+
if (Array.isArray(value))
|
|
25
|
+
return value.every(validJson);
|
|
26
|
+
return typeof value === "object" && Object.getPrototypeOf(value) === Object.prototype && Object.values(value).every(validJson);
|
|
27
|
+
}
|
|
28
|
+
function assertScope(scope) {
|
|
29
|
+
if (!ID.test(scope.captureId) || scope.namespace !== NAMESPACE || !ID.test(scope.saveId) || !ID.test(scope.viewId))
|
|
30
|
+
fail("invalid_scope");
|
|
31
|
+
if ("principalId" in scope && scope.principalId !== null && !ID.test(scope.principalId))
|
|
32
|
+
fail("invalid_principal");
|
|
33
|
+
}
|
|
34
|
+
function assertEntries(entries) {
|
|
35
|
+
if (entries.length > MAX_ENTRIES || new Set(entries.map((entry) => entry.key)).size !== entries.length)
|
|
36
|
+
fail("invalid_entries");
|
|
37
|
+
for (const entry of entries) {
|
|
38
|
+
if (!ID.test(entry.key) || (entry.scope !== "public" && entry.scope !== "protected") || !Number.isSafeInteger(entry.revision) || entry.revision < 0)
|
|
39
|
+
fail("invalid_entry");
|
|
40
|
+
if (entry.authorizedUserIds.length > MAX_AUTHORIZED_USERS || new Set(entry.authorizedUserIds).size !== entry.authorizedUserIds.length || entry.authorizedUserIds.some((userId) => !ID.test(userId)))
|
|
41
|
+
fail("invalid_authorized_users");
|
|
42
|
+
if (entry.scope === "public" && entry.authorizedUserIds.length !== 0)
|
|
43
|
+
fail("public_authorized_users");
|
|
44
|
+
if (entry.scope === "protected" && entry.authorizedUserIds.length === 0)
|
|
45
|
+
fail("protected_without_authorized_user");
|
|
46
|
+
if (!validJson(entry.value) || new TextEncoder().encode(JSON.stringify(entry.value)).byteLength > LOCAL_PREVIEW_CUSTOM_STATE_MAX_BYTES)
|
|
47
|
+
fail("invalid_or_oversize_value");
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
function assertSnapshot(expected, snapshot) {
|
|
51
|
+
assertScope(snapshot.scope);
|
|
52
|
+
if (snapshot.scope.captureId !== expected.captureId || snapshot.scope.namespace !== expected.namespace || snapshot.scope.saveId !== expected.saveId || snapshot.scope.viewId !== expected.viewId || !Number.isSafeInteger(snapshot.generation) || snapshot.generation < 0)
|
|
53
|
+
fail("snapshot_scope_or_generation_mismatch");
|
|
54
|
+
assertRuntimeHead(snapshot.runtimeHead);
|
|
55
|
+
assertEntries(snapshot.entries);
|
|
56
|
+
}
|
|
57
|
+
function assertRuntimeHead(head) {
|
|
58
|
+
if (!ID.test(head.revisionId) || !Number.isSafeInteger(head.revisionSequence) || head.revisionSequence < 0 || (head.ledgerHead !== null && !ID.test(head.ledgerHead)))
|
|
59
|
+
fail("invalid_runtime_head");
|
|
60
|
+
}
|
|
61
|
+
function authorized(scope, entry) {
|
|
62
|
+
return entry.scope === "public" || (scope.principalId !== null && entry.authorizedUserIds.includes(scope.principalId));
|
|
63
|
+
}
|
|
64
|
+
function immutableEntries(entries) {
|
|
65
|
+
return Object.freeze(entries.map((entry) => Object.freeze({ ...entry, value: clone(entry.value), authorizedUserIds: Object.freeze([...entry.authorizedUserIds]) })));
|
|
66
|
+
}
|
|
67
|
+
/** Host-only facade. It deliberately contains no request-selected identity or iframe mutation API. */
|
|
68
|
+
export class LocalPreviewViewStateStore {
|
|
69
|
+
#scope;
|
|
70
|
+
#port;
|
|
71
|
+
constructor(scope, port) { this.#scope = Object.freeze({ ...scope }); this.#port = port; }
|
|
72
|
+
static async open(scope, port) {
|
|
73
|
+
assertScope(scope);
|
|
74
|
+
const publicViewScope = publicScope(scope);
|
|
75
|
+
const initialized = await port.initialize(publicViewScope);
|
|
76
|
+
assertSnapshot(publicViewScope, initialized);
|
|
77
|
+
return new LocalPreviewViewStateStore(scope, port);
|
|
78
|
+
}
|
|
79
|
+
async keys() { return (await this.#authorizedSnapshot()).entries.map((entry) => entry.key).sort(); }
|
|
80
|
+
/** 给 LocalPreviewReadOnlyProjection 的 customState reader 使用,结果已经按 Host principal 过滤。 */
|
|
81
|
+
async entries() { return (await this.#authorizedSnapshot()).entries; }
|
|
82
|
+
async get(key) { const entry = await this.getEntry(key); return entry === undefined ? undefined : clone(entry.value); }
|
|
83
|
+
async getEntry(key) {
|
|
84
|
+
if (!ID.test(key))
|
|
85
|
+
fail("invalid_key");
|
|
86
|
+
const entry = (await this.#authorizedSnapshot()).entries.find((candidate) => candidate.key === key);
|
|
87
|
+
return entry === undefined ? undefined : immutableEntries([entry])[0];
|
|
88
|
+
}
|
|
89
|
+
/** Runtime owner calls this after a formal action output; View RPC never gets this method. */
|
|
90
|
+
async commitRuntimeOutput(output) {
|
|
91
|
+
if (output.source !== "runtime-action-output" || !ID.test(output.operationId) || !Number.isSafeInteger(output.expectedGeneration) || output.expectedGeneration < 0)
|
|
92
|
+
fail("invalid_runtime_output");
|
|
93
|
+
assertRuntimeHead(output.expectedRuntimeHead);
|
|
94
|
+
assertEntries(output.entries);
|
|
95
|
+
const result = await this.#port.commitRuntimeOutput({ scope: publicScope(this.#scope), operationId: output.operationId, expectedRuntimeHead: Object.freeze({ ...output.expectedRuntimeHead }), expectedGeneration: output.expectedGeneration, entries: immutableEntries(output.entries) });
|
|
96
|
+
assertSnapshot(publicScope(this.#scope), result.snapshot);
|
|
97
|
+
if (new Set(result.changedKeys).size !== result.changedKeys.length || result.changedKeys.some((key) => !ID.test(key)))
|
|
98
|
+
fail("invalid_changed_keys");
|
|
99
|
+
return Object.freeze({ generation: result.snapshot.generation, changedKeys: Object.freeze([...result.changedKeys].sort()), entries: immutableEntries(result.snapshot.entries), replayed: result.replayed });
|
|
100
|
+
}
|
|
101
|
+
async #authorizedSnapshot() {
|
|
102
|
+
const scope = publicScope(this.#scope);
|
|
103
|
+
const snapshot = await this.#port.read(scope);
|
|
104
|
+
assertSnapshot(scope, snapshot);
|
|
105
|
+
return Object.freeze({ generation: snapshot.generation, entries: immutableEntries(snapshot.entries.filter((entry) => authorized(this.#scope, entry))) });
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
export const LOCAL_PREVIEW_VIEW_STATE_NAMESPACE = NAMESPACE;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export interface LocalPreviewViteConfig {
|
|
2
|
+
readonly path: string;
|
|
3
|
+
readonly basePath: string;
|
|
4
|
+
}
|
|
5
|
+
export declare function writeLocalPreviewViteConfig(projectRoot: string, viewRoot: string, port: number, parentOrigin?: string): Promise<LocalPreviewViteConfig>;
|
|
6
|
+
export declare function removeLocalPreviewViteConfig(projectRoot: string, path: string): Promise<void>;
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { randomBytes } from "node:crypto";
|
|
2
|
+
import { lstat, open, rm } from "node:fs/promises";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { pathToFileURL } from "node:url";
|
|
5
|
+
import { LOCAL_GUI_HOST, LOCAL_GUI_ORIGIN } from "./local-gui-contract.js";
|
|
6
|
+
import { assertSafeExistingProjectPath, ensureSafeProjectDirectory } from "./safe-project-path.js";
|
|
7
|
+
const CONFIG_NAMES = [
|
|
8
|
+
"vite.config.ts", "vite.config.mts", "vite.config.js", "vite.config.mjs",
|
|
9
|
+
"vite.config.cts", "vite.config.cjs",
|
|
10
|
+
];
|
|
11
|
+
const CONFIG_PREFIX = "preview-vite-";
|
|
12
|
+
function source(userConfigUrl, previewOrigin, parentOrigin, basePath) {
|
|
13
|
+
return `import userConfig from ${JSON.stringify(userConfigUrl)};
|
|
14
|
+
const PREVIEW_ORIGIN=${JSON.stringify(previewOrigin)};
|
|
15
|
+
const PARENT_ORIGIN=${JSON.stringify(parentOrigin)};
|
|
16
|
+
const BASE_PATH=${JSON.stringify(basePath)};
|
|
17
|
+
const EXPECTED_HOST=new URL(PREVIEW_ORIGIN).host;
|
|
18
|
+
const MODULE_DESTINATIONS=new Set(["script","worker","sharedworker","style","font"]);
|
|
19
|
+
const deny=(response)=>{response.statusCode=403;response.end("Forbidden");};
|
|
20
|
+
const securityPlugin={
|
|
21
|
+
name:"worldengine-local-preview-security",
|
|
22
|
+
enforce:"pre",
|
|
23
|
+
configureServer(server){
|
|
24
|
+
server.middlewares.use((request,response,next)=>{
|
|
25
|
+
let url;
|
|
26
|
+
try{url=new URL(request.url||"/",PREVIEW_ORIGIN);}catch{return deny(response);}
|
|
27
|
+
if(request.headers.host!==EXPECTED_HOST)return deny(response);
|
|
28
|
+
if(!url.pathname.startsWith(BASE_PATH))return deny(response);
|
|
29
|
+
const relativePath="/"+url.pathname.slice(BASE_PATH.length);
|
|
30
|
+
response.setHeader("Content-Security-Policy","frame-ancestors "+PARENT_ORIGIN);
|
|
31
|
+
response.setHeader("Cache-Control","no-store");
|
|
32
|
+
response.setHeader("X-Content-Type-Options","nosniff");
|
|
33
|
+
response.setHeader("Referrer-Policy","origin");
|
|
34
|
+
if(relativePath.startsWith("/__chatplay/"))return deny(response);
|
|
35
|
+
const origin=request.headers.origin;
|
|
36
|
+
if(origin===undefined){if(request.method!=="GET")return deny(response);return next();}
|
|
37
|
+
if(origin!=="null"||request.method!=="GET")return deny(response);
|
|
38
|
+
if(request.headers["sec-fetch-mode"]!=="cors"||!MODULE_DESTINATIONS.has(request.headers["sec-fetch-dest"]||""))return deny(response);
|
|
39
|
+
response.setHeader("Access-Control-Allow-Origin","null");
|
|
40
|
+
response.setHeader("Vary","Origin");
|
|
41
|
+
next();
|
|
42
|
+
});
|
|
43
|
+
server.httpServer?.prependListener("upgrade",(request,socket)=>{
|
|
44
|
+
const protocol=request.headers["sec-websocket-protocol"];
|
|
45
|
+
if(protocol!=="vite-hmr"&&protocol!=="vite-ping")return;
|
|
46
|
+
let pathname="";try{pathname=new URL(request.url||"/",PREVIEW_ORIGIN).pathname;}catch{}
|
|
47
|
+
if(request.headers.host!==EXPECTED_HOST||request.headers.origin!=="null"||pathname!==BASE_PATH)socket.destroy();
|
|
48
|
+
});
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
export default async(env)=>{
|
|
52
|
+
const resolved=typeof userConfig==="function"?await userConfig(env):await userConfig;
|
|
53
|
+
if(!resolved||typeof resolved!=="object")throw new Error("worldengine_preview_vite_config_invalid");
|
|
54
|
+
const hmr=resolved.server?.hmr;
|
|
55
|
+
return {...resolved,
|
|
56
|
+
base:BASE_PATH,
|
|
57
|
+
legacy:{...(resolved.legacy||{}),skipWebSocketTokenCheck:false},
|
|
58
|
+
server:{...(resolved.server||{}),cors:false,hmr:{...(hmr&&typeof hmr==="object"?hmr:{}),host:"localhost"}},
|
|
59
|
+
plugins:[securityPlugin,...(Array.isArray(resolved.plugins)?resolved.plugins:[])],
|
|
60
|
+
};
|
|
61
|
+
};
|
|
62
|
+
`;
|
|
63
|
+
}
|
|
64
|
+
export async function writeLocalPreviewViteConfig(projectRoot, viewRoot, port, parentOrigin = LOCAL_GUI_ORIGIN) {
|
|
65
|
+
let parent;
|
|
66
|
+
try {
|
|
67
|
+
parent = new URL(parentOrigin);
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
throw new Error("worldengine_preview_parent_origin_invalid");
|
|
71
|
+
}
|
|
72
|
+
if (parent.protocol !== "http:" || parent.hostname !== LOCAL_GUI_HOST || parent.port === "" || parent.pathname !== "/" || parent.search !== "" || parent.hash !== "" || parent.username !== "" || parent.password !== "") {
|
|
73
|
+
throw new Error("worldengine_preview_parent_origin_invalid");
|
|
74
|
+
}
|
|
75
|
+
const matches = [];
|
|
76
|
+
for (const name of CONFIG_NAMES) {
|
|
77
|
+
const candidate = join(viewRoot, name);
|
|
78
|
+
try {
|
|
79
|
+
const info = await lstat(candidate);
|
|
80
|
+
if (info.isSymbolicLink() || !info.isFile())
|
|
81
|
+
throw new Error("unsafe config");
|
|
82
|
+
matches.push(candidate);
|
|
83
|
+
}
|
|
84
|
+
catch (error) {
|
|
85
|
+
if (error.code !== "ENOENT")
|
|
86
|
+
throw error;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
if (matches.length !== 1)
|
|
90
|
+
throw new Error("worldengine_preview_vite_config_not_unique");
|
|
91
|
+
const directory = await ensureSafeProjectDirectory(projectRoot, ".worldengine");
|
|
92
|
+
const output = join(directory, `${CONFIG_PREFIX}${randomBytes(16).toString("hex")}.config.mjs`);
|
|
93
|
+
const basePath = `/_worldengine_preview/${randomBytes(16).toString("hex")}/`;
|
|
94
|
+
const handle = await open(output, "wx", 0o600);
|
|
95
|
+
try {
|
|
96
|
+
await handle.writeFile(source(pathToFileURL(matches[0]).href, `http://localhost:${port}`, parent.origin, basePath), "utf8");
|
|
97
|
+
await handle.sync();
|
|
98
|
+
}
|
|
99
|
+
finally {
|
|
100
|
+
await handle.close();
|
|
101
|
+
}
|
|
102
|
+
return Object.freeze({ path: output, basePath });
|
|
103
|
+
}
|
|
104
|
+
export async function removeLocalPreviewViteConfig(projectRoot, path) {
|
|
105
|
+
let canonical;
|
|
106
|
+
try {
|
|
107
|
+
canonical = await assertSafeExistingProjectPath(projectRoot, path);
|
|
108
|
+
}
|
|
109
|
+
catch (error) {
|
|
110
|
+
if (error.code === "ENOENT")
|
|
111
|
+
return;
|
|
112
|
+
throw error;
|
|
113
|
+
}
|
|
114
|
+
const name = canonical.slice(canonical.lastIndexOf("\\") + 1).slice(canonical.lastIndexOf("/") + 1);
|
|
115
|
+
if (!name.startsWith(CONFIG_PREFIX) || !/^[a-f0-9]{32}[.]config[.]mjs$/u.test(name.slice(CONFIG_PREFIX.length))) {
|
|
116
|
+
throw new Error("worldengine_preview_vite_config_owner_invalid");
|
|
117
|
+
}
|
|
118
|
+
const info = await lstat(canonical);
|
|
119
|
+
if (!info.isFile() || info.isSymbolicLink())
|
|
120
|
+
throw new Error("worldengine_preview_vite_config_owner_invalid");
|
|
121
|
+
await rm(canonical);
|
|
122
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type EffectPlan, type TmwJsonValue } from "@world-engines/tmw";
|
|
2
|
+
export interface LocalPreviewWorldProjection {
|
|
3
|
+
readonly entities: Readonly<Record<string, TmwJsonValue>>;
|
|
4
|
+
readonly emitted_events: ReadonlyArray<TmwJsonValue>;
|
|
5
|
+
readonly [key: string]: unknown;
|
|
6
|
+
}
|
|
7
|
+
export declare function createLocalPreviewWorldProjection(logicalGraph: unknown): LocalPreviewWorldProjection;
|
|
8
|
+
/** 纯 reducer:不写文件、不触发递归事件,只返回待 activation 一并提交的 projection。 */
|
|
9
|
+
export declare function reduceLocalPreviewWorldEffects(current: LocalPreviewWorldProjection, plans: ReadonlyArray<EffectPlan>, maxEvents?: number): LocalPreviewWorldProjection;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { canonicalTmwJsonValue } from "@world-engines/tmw";
|
|
2
|
+
function record(value, code) {
|
|
3
|
+
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
4
|
+
throw new Error(code);
|
|
5
|
+
return structuredClone(value);
|
|
6
|
+
}
|
|
7
|
+
export function createLocalPreviewWorldProjection(logicalGraph) {
|
|
8
|
+
const graph = typeof logicalGraph === "object" && logicalGraph !== null && !Array.isArray(logicalGraph) ? logicalGraph : {};
|
|
9
|
+
const rows = Array.isArray(graph.entities) ? graph.entities : [];
|
|
10
|
+
const entities = {};
|
|
11
|
+
for (const row of rows) {
|
|
12
|
+
if (typeof row !== "object" || row === null || Array.isArray(row))
|
|
13
|
+
throw new Error("local_preview_world_entity_invalid");
|
|
14
|
+
const candidate = row;
|
|
15
|
+
const id = candidate.entity_id ?? candidate.id;
|
|
16
|
+
if (typeof id !== "string" || id.length === 0)
|
|
17
|
+
throw new Error("local_preview_world_entity_id_invalid");
|
|
18
|
+
if (Object.hasOwn(entities, id))
|
|
19
|
+
throw new Error("local_preview_world_entity_id_duplicate");
|
|
20
|
+
entities[id] = canonicalTmwJsonValue({ ...candidate, entity_id: id }, "local_preview_world_entity");
|
|
21
|
+
}
|
|
22
|
+
return Object.freeze({ entities: Object.freeze(entities), emitted_events: Object.freeze([]), scenario_runtime_state: Object.freeze({ schema_version: 1, state_revision: "0", progress: 0, unlocked_resource_ids: Object.freeze([]) }), frozen_attribute_resource_refs: Object.freeze([]) });
|
|
23
|
+
}
|
|
24
|
+
/** 纯 reducer:不写文件、不触发递归事件,只返回待 activation 一并提交的 projection。 */
|
|
25
|
+
export function reduceLocalPreviewWorldEffects(current, plans, maxEvents = 64) {
|
|
26
|
+
const entities = record(current.entities, "local_preview_world_entities_invalid");
|
|
27
|
+
const emittedEvents = [...current.emitted_events];
|
|
28
|
+
for (const effect of plans.flatMap((plan) => plan.status === "ready" ? plan.effects : [])) {
|
|
29
|
+
if (effect.capabilityId === "world.set_attribute") {
|
|
30
|
+
if (effect.selector?.op !== "entity_id")
|
|
31
|
+
throw new Error("local_preview_world_entity_id_selector_required");
|
|
32
|
+
const args = record(effect.input, "local_preview_world_set_attribute_arguments_invalid");
|
|
33
|
+
if (!Array.isArray(args.path) || args.path.length === 0 || args.path.some((item) => typeof item !== "string" || item.length === 0))
|
|
34
|
+
throw new Error("local_preview_world_set_attribute_path_invalid");
|
|
35
|
+
const entity = record(entities[effect.selector.entity_id] ?? {}, "local_preview_world_entity_invalid");
|
|
36
|
+
let cursor = entity;
|
|
37
|
+
for (const segment of args.path.slice(0, -1)) {
|
|
38
|
+
const next = cursor[segment] === undefined ? {} : record(cursor[segment], "local_preview_world_attribute_path_invalid");
|
|
39
|
+
cursor[segment] = next;
|
|
40
|
+
cursor = next;
|
|
41
|
+
}
|
|
42
|
+
cursor[args.path.at(-1)] = canonicalTmwJsonValue(args.value ?? null, "local_preview_world_attribute_value");
|
|
43
|
+
entities[effect.selector.entity_id] = entity;
|
|
44
|
+
}
|
|
45
|
+
else if (effect.capabilityId === "world.emit_event") {
|
|
46
|
+
if (emittedEvents.length >= maxEvents)
|
|
47
|
+
throw new Error("local_preview_world_event_queue_full");
|
|
48
|
+
emittedEvents.push(canonicalTmwJsonValue(effect.input, "local_preview_world_emitted_event"));
|
|
49
|
+
}
|
|
50
|
+
else if (!/^(variable\.|node\.|edge\.|object\.|scene\.)/u.test(effect.capabilityId)) {
|
|
51
|
+
throw new Error(`local_preview_effect_capability_unsupported:${effect.capabilityId}`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return Object.freeze({ ...structuredClone(current), entities: Object.freeze(entities), emitted_events: Object.freeze(emittedEvents) });
|
|
55
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { DatabaseSync } from "node:sqlite";
|
|
2
|
+
import { type RuntimeSavPayloadV3 } from "@world-engines/tmw";
|
|
3
|
+
import type { LocalPreviewWorldlineActivateResult, LocalPreviewWorldlineForkResult, LocalPreviewWorldlineListResult, LocalPreviewWorldlineRuntime, LocalPreviewWorldlineScope, LocalPreviewWorldlineSnapshot } from "./local-preview-worldlines.js";
|
|
4
|
+
export interface LocalPreviewActiveWorldlineMessages {
|
|
5
|
+
readonly activeWorldlineId: string;
|
|
6
|
+
readonly catalogRevision: number;
|
|
7
|
+
/** Only IDs sealed by a successful writer checkpoint; message bodies live in LocalPreviewMessageStore. */
|
|
8
|
+
readonly orderedMessageIds: readonly string[];
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* The SQLite-backed half of local Preview worldlines.
|
|
12
|
+
*
|
|
13
|
+
* It deliberately receives the Runtime's already-open activation.sqlite
|
|
14
|
+
* connection. There is therefore one CAS boundary for SAV, activation head,
|
|
15
|
+
* message checkpoints and worldline catalog; this is not a JSONL mirror.
|
|
16
|
+
*/
|
|
17
|
+
export declare class LocalPreviewWorldlineStore implements LocalPreviewWorldlineRuntime {
|
|
18
|
+
#private;
|
|
19
|
+
constructor(input: Readonly<{
|
|
20
|
+
database: DatabaseSync;
|
|
21
|
+
scope: LocalPreviewWorldlineScope;
|
|
22
|
+
}>);
|
|
23
|
+
snapshotWorldlines(): Promise<LocalPreviewWorldlineSnapshot>;
|
|
24
|
+
listWorldlines(scope: LocalPreviewWorldlineScope): Promise<LocalPreviewWorldlineListResult>;
|
|
25
|
+
/**
|
|
26
|
+
* Returns one SQLite snapshot of the active catalog row and its authoritative
|
|
27
|
+
* committed-message order. Accepted user input that has not reached a writer
|
|
28
|
+
* checkpoint is intentionally absent and must not be promoted here.
|
|
29
|
+
*/
|
|
30
|
+
readActiveWorldlineMessages(): Promise<LocalPreviewActiveWorldlineMessages>;
|
|
31
|
+
forkWorldlineAtMessage(scope: LocalPreviewWorldlineScope, input: ForkInput): Promise<LocalPreviewWorldlineForkResult>;
|
|
32
|
+
activateWorldline(scope: LocalPreviewWorldlineScope, input: ActivateInput): Promise<LocalPreviewWorldlineActivateResult>;
|
|
33
|
+
/**
|
|
34
|
+
* Called only by the Runtime's genuine message/SAV commit while that caller
|
|
35
|
+
* already owns activation.sqlite's transaction. It does not infer a
|
|
36
|
+
* checkpoint from JSONL or synthesize one for an uncommitted assistant turn.
|
|
37
|
+
*/
|
|
38
|
+
recordCommittedMessageCheckpointInTransaction(input: Readonly<{
|
|
39
|
+
messageId: string;
|
|
40
|
+
orderedMessageIds: readonly string[];
|
|
41
|
+
sav: RuntimeSavPayloadV3;
|
|
42
|
+
createdAtMs: number;
|
|
43
|
+
}>): void;
|
|
44
|
+
}
|
|
45
|
+
type ForkInput = Parameters<LocalPreviewWorldlineRuntime["forkWorldlineAtMessage"]>[1];
|
|
46
|
+
type ActivateInput = Parameters<LocalPreviewWorldlineRuntime["activateWorldline"]>[1];
|
|
47
|
+
export {};
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { AuthoringBridgeError } from "@world-engines/authoring-bridge";
|
|
3
|
+
import { canonicalRuntimeSavPayload, canonicalTmwJsonValue } from "@world-engines/tmw";
|
|
4
|
+
/**
|
|
5
|
+
* The SQLite-backed half of local Preview worldlines.
|
|
6
|
+
*
|
|
7
|
+
* It deliberately receives the Runtime's already-open activation.sqlite
|
|
8
|
+
* connection. There is therefore one CAS boundary for SAV, activation head,
|
|
9
|
+
* message checkpoints and worldline catalog; this is not a JSONL mirror.
|
|
10
|
+
*/
|
|
11
|
+
export class LocalPreviewWorldlineStore {
|
|
12
|
+
#db;
|
|
13
|
+
#scope;
|
|
14
|
+
#scopeKey;
|
|
15
|
+
#savBinding;
|
|
16
|
+
constructor(input) {
|
|
17
|
+
assertScope(input.scope);
|
|
18
|
+
this.#db = input.database;
|
|
19
|
+
this.#scope = Object.freeze({ ...input.scope });
|
|
20
|
+
this.#scopeKey = scopeKey(this.#scope);
|
|
21
|
+
this.#db.exec([
|
|
22
|
+
"CREATE TABLE IF NOT EXISTS local_preview_worldline_schema (schema_name TEXT PRIMARY KEY, version INTEGER NOT NULL);",
|
|
23
|
+
"CREATE TABLE IF NOT EXISTS local_preview_worldline_catalog_v2 (scope_key TEXT PRIMARY KEY, active_worldline_id TEXT NOT NULL, root_worldline_id TEXT NOT NULL, revision INTEGER NOT NULL);",
|
|
24
|
+
"CREATE TABLE IF NOT EXISTS local_preview_worldline_v2 (scope_key TEXT NOT NULL, worldline_id TEXT NOT NULL, title TEXT NOT NULL, parent_worldline_id TEXT, fork_anchor_op_id TEXT, head_op_id TEXT, sav_json TEXT NOT NULL, projection_json TEXT NOT NULL, projection_digest TEXT NOT NULL, message_ids_json TEXT NOT NULL, created_at_ms INTEGER NOT NULL, updated_at_ms INTEGER NOT NULL, revision INTEGER NOT NULL, PRIMARY KEY(scope_key,worldline_id));",
|
|
25
|
+
"CREATE TABLE IF NOT EXISTS local_preview_worldline_checkpoint_v2 (scope_key TEXT NOT NULL, message_id TEXT NOT NULL, worldline_id TEXT NOT NULL, sav_json TEXT NOT NULL, projection_json TEXT NOT NULL, projection_digest TEXT NOT NULL, message_ids_json TEXT NOT NULL, checkpoint_digest TEXT NOT NULL, created_at_ms INTEGER NOT NULL, PRIMARY KEY(scope_key,message_id));",
|
|
26
|
+
"CREATE TABLE IF NOT EXISTS local_preview_worldline_request_v2 (scope_key TEXT NOT NULL, idempotency_key TEXT NOT NULL, digest TEXT NOT NULL, result_json TEXT NOT NULL, PRIMARY KEY(scope_key,idempotency_key));",
|
|
27
|
+
].join(""));
|
|
28
|
+
const schema = this.#db.prepare("SELECT version FROM local_preview_worldline_schema WHERE schema_name='local_preview_worldline'").get();
|
|
29
|
+
if (schema === undefined)
|
|
30
|
+
this.#db.prepare("INSERT INTO local_preview_worldline_schema(schema_name,version) VALUES ('local_preview_worldline',2)").run();
|
|
31
|
+
else if (schema.version !== 2)
|
|
32
|
+
fail("E_OPERATION_UNCERTAIN", "local Preview worldline schema 版本不兼容;拒绝静默丢弃既有分支");
|
|
33
|
+
const initialSav = readSav(this.#db);
|
|
34
|
+
this.#savBinding = Object.freeze({ epoch: initialSav.epoch, runtimeGeneration: initialSav.runtimeGeneration, runtimeContractHash: initialSav.runtimeContractHash, artifactDigest: initialSav.artifactDigest, sourceRevision: initialSav.sourceRevision });
|
|
35
|
+
this.#initializeRoot();
|
|
36
|
+
}
|
|
37
|
+
snapshotWorldlines() {
|
|
38
|
+
const catalog = this.#catalog();
|
|
39
|
+
return Promise.resolve(Object.freeze({ scope: this.#scope, activeWorldlineId: catalog.active, rootWorldlineId: catalog.root, catalogRevision: catalog.revision }));
|
|
40
|
+
}
|
|
41
|
+
listWorldlines(scope) {
|
|
42
|
+
this.#assertScope(scope);
|
|
43
|
+
const catalog = this.#catalog();
|
|
44
|
+
const rows = this.#db.prepare("SELECT worldline_id,title,parent_worldline_id,fork_anchor_op_id,head_op_id,created_at_ms,updated_at_ms,revision FROM local_preview_worldline_v2 WHERE scope_key=? ORDER BY created_at_ms,worldline_id").all(this.#scopeKey);
|
|
45
|
+
const worldlines = rows.map((row) => summary(row));
|
|
46
|
+
return Promise.resolve(Object.freeze({ activeWorldlineId: catalog.active, rootWorldlineId: catalog.root, worldlineCount: worldlines.length, worldlines: Object.freeze(worldlines) }));
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Returns one SQLite snapshot of the active catalog row and its authoritative
|
|
50
|
+
* committed-message order. Accepted user input that has not reached a writer
|
|
51
|
+
* checkpoint is intentionally absent and must not be promoted here.
|
|
52
|
+
*/
|
|
53
|
+
readActiveWorldlineMessages() {
|
|
54
|
+
return Promise.resolve(this.#inReadTransaction(() => {
|
|
55
|
+
const catalog = this.#catalog();
|
|
56
|
+
const line = this.#line(catalog.active);
|
|
57
|
+
return Object.freeze({ activeWorldlineId: catalog.active, catalogRevision: catalog.revision, orderedMessageIds: Object.freeze([...parseIds(line.message_ids_json, "active worldline message ids")]) });
|
|
58
|
+
}));
|
|
59
|
+
}
|
|
60
|
+
async forkWorldlineAtMessage(scope, input) {
|
|
61
|
+
this.#assertScope(scope);
|
|
62
|
+
assertId(input.messageId, "messageId");
|
|
63
|
+
assertId(input.expectedActiveWorldlineId, "expectedActiveWorldlineId");
|
|
64
|
+
assertId(input.idempotencyKey, "idempotencyKey");
|
|
65
|
+
assertTime(input.createdAtMs, "createdAtMs");
|
|
66
|
+
if (input.requestedTitle !== null)
|
|
67
|
+
assertTitle(input.requestedTitle);
|
|
68
|
+
const digest = sha256(JSON.stringify({ scopeKey: this.#scopeKey, kind: "fork", messageId: input.messageId, title: input.requestedTitle }));
|
|
69
|
+
return this.#inTransaction(() => {
|
|
70
|
+
const previous = this.#request(input.idempotencyKey, digest);
|
|
71
|
+
if (previous !== null)
|
|
72
|
+
return previous;
|
|
73
|
+
const catalog = this.#requireCas(input.expectedCatalogRevision, input.expectedActiveWorldlineId);
|
|
74
|
+
const checkpoint = this.#db.prepare("SELECT worldline_id,sav_json,projection_json,message_ids_json FROM local_preview_worldline_checkpoint_v2 WHERE scope_key=? AND message_id=?").get(this.#scopeKey, input.messageId);
|
|
75
|
+
if (checkpoint === undefined)
|
|
76
|
+
fail("E_VALIDATION_FAILED", "只能从真实已提交消息分叉");
|
|
77
|
+
const source = this.#line(catalog.active);
|
|
78
|
+
const checkpointMessageIds = parseIds(checkpoint.message_ids_json, "checkpoint message ids");
|
|
79
|
+
const sourceMessageIds = parseIds(source.message_ids_json, "worldline message ids");
|
|
80
|
+
if (!hasPrefix(sourceMessageIds, checkpointMessageIds))
|
|
81
|
+
fail("E_OPERATION_UNCERTAIN", "worldline checkpoint 不再是当前已提交消息前缀");
|
|
82
|
+
const childId = `line.${sha256(`${this.#scopeKey}:${input.idempotencyKey}`).slice(0, 32)}`;
|
|
83
|
+
const existingChild = this.#tryLine(childId);
|
|
84
|
+
if (existingChild !== null)
|
|
85
|
+
fail("E_OPERATION_ID_CONFLICT", "worldline idempotency target 已被不同请求占用");
|
|
86
|
+
const childSav = withWorldline(parseSav(checkpoint.sav_json, this.#savBinding), childId, this.#savBinding);
|
|
87
|
+
const now = input.createdAtMs;
|
|
88
|
+
const child = Object.freeze({
|
|
89
|
+
worldlineId: childId, title: input.requestedTitle ?? `分支 ${checkpointMessageIds.length}`,
|
|
90
|
+
parentWorldlineId: catalog.active, forkAnchorOpId: childSav.ledgerHead, headOpId: childSav.ledgerHead,
|
|
91
|
+
sav: childSav, projectionJson: checkpoint.projection_json, messageIds: checkpointMessageIds, createdAtMs: now, updatedAtMs: now, revision: 1,
|
|
92
|
+
});
|
|
93
|
+
this.#insertLine(child);
|
|
94
|
+
this.#writeCatalog({ active: childId, root: catalog.root, revision: catalog.revision + 1 });
|
|
95
|
+
writeSavState(this.#db, childSav, checkpoint.projection_json);
|
|
96
|
+
const result = Object.freeze({ worldlineId: childId, activeWorldlineId: childId, anchorMessageId: input.messageId, worldlineCount: this.#lineCount(), removedMessageIds: Object.freeze(sourceMessageIds.slice(checkpointMessageIds.length)), deletedEntities: 0, deletedRelations: 0 });
|
|
97
|
+
this.#writeRequest(input.idempotencyKey, digest, result);
|
|
98
|
+
return result;
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
async activateWorldline(scope, input) {
|
|
102
|
+
this.#assertScope(scope);
|
|
103
|
+
assertId(input.worldlineId, "worldlineId");
|
|
104
|
+
assertId(input.expectedActiveWorldlineId, "expectedActiveWorldlineId");
|
|
105
|
+
assertId(input.idempotencyKey, "idempotencyKey");
|
|
106
|
+
assertTime(input.createdAtMs, "createdAtMs");
|
|
107
|
+
const digest = sha256(JSON.stringify({ scopeKey: this.#scopeKey, kind: "activate", worldlineId: input.worldlineId }));
|
|
108
|
+
return this.#inTransaction(() => {
|
|
109
|
+
const previous = this.#request(input.idempotencyKey, digest);
|
|
110
|
+
if (previous !== null)
|
|
111
|
+
return previous;
|
|
112
|
+
const catalog = this.#requireCas(input.expectedCatalogRevision, input.expectedActiveWorldlineId);
|
|
113
|
+
const line = this.#line(input.worldlineId);
|
|
114
|
+
const sav = withWorldline(parseSav(line.sav_json, this.#savBinding), input.worldlineId, this.#savBinding);
|
|
115
|
+
if (catalog.active !== input.worldlineId)
|
|
116
|
+
this.#writeCatalog({ active: input.worldlineId, root: catalog.root, revision: catalog.revision + 1 });
|
|
117
|
+
writeSavState(this.#db, sav, line.projection_json);
|
|
118
|
+
const result = Object.freeze({ worldlineId: input.worldlineId, activeWorldlineId: input.worldlineId, worldlineCount: this.#lineCount(), messageCount: parseIds(line.message_ids_json, "worldline message ids").length });
|
|
119
|
+
this.#writeRequest(input.idempotencyKey, digest, result);
|
|
120
|
+
return result;
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Called only by the Runtime's genuine message/SAV commit while that caller
|
|
125
|
+
* already owns activation.sqlite's transaction. It does not infer a
|
|
126
|
+
* checkpoint from JSONL or synthesize one for an uncommitted assistant turn.
|
|
127
|
+
*/
|
|
128
|
+
recordCommittedMessageCheckpointInTransaction(input) {
|
|
129
|
+
if (!isTransaction(this.#db))
|
|
130
|
+
fail("E_OPERATION_UNCERTAIN", "message checkpoint 必须与真实 SAV commit 处于同一 SQLite transaction");
|
|
131
|
+
assertId(input.messageId, "messageId");
|
|
132
|
+
assertMessageIds(input.orderedMessageIds);
|
|
133
|
+
if (input.orderedMessageIds.at(-1) !== input.messageId)
|
|
134
|
+
fail("E_VALIDATION_FAILED", "message checkpoint 必须锚定最新已提交消息");
|
|
135
|
+
assertTime(input.createdAtMs, "checkpoint createdAtMs");
|
|
136
|
+
const catalog = this.#catalog();
|
|
137
|
+
const line = this.#line(catalog.active);
|
|
138
|
+
const ids = Object.freeze([...input.orderedMessageIds]);
|
|
139
|
+
const previous = parseIds(line.message_ids_json, "worldline message ids");
|
|
140
|
+
if (!hasPrefix(ids, previous))
|
|
141
|
+
fail("E_OPERATION_UNCERTAIN", "message checkpoint 不能重写既有提交历史");
|
|
142
|
+
const sav = withWorldline(input.sav, catalog.active, this.#savBinding);
|
|
143
|
+
const savJson = JSON.stringify(sav);
|
|
144
|
+
const projectionJson = readProjectionJson(this.#db);
|
|
145
|
+
const idsJson = JSON.stringify(ids);
|
|
146
|
+
const projectionDigest = sha256(projectionJson);
|
|
147
|
+
const checkpointDigest = sha256(JSON.stringify({ messageId: input.messageId, ids, savJson, projectionDigest }));
|
|
148
|
+
const existing = this.#db.prepare("SELECT checkpoint_digest FROM local_preview_worldline_checkpoint_v2 WHERE scope_key=? AND message_id=?").get(this.#scopeKey, input.messageId);
|
|
149
|
+
if (existing !== undefined) {
|
|
150
|
+
if (existing.checkpoint_digest !== checkpointDigest)
|
|
151
|
+
fail("E_OPERATION_ID_CONFLICT", "已提交消息绑定不同 checkpoint");
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
this.#db.prepare("INSERT INTO local_preview_worldline_checkpoint_v2(scope_key,message_id,worldline_id,sav_json,projection_json,projection_digest,message_ids_json,checkpoint_digest,created_at_ms) VALUES (?,?,?,?,?,?,?,?,?)").run(this.#scopeKey, input.messageId, catalog.active, savJson, projectionJson, projectionDigest, idsJson, checkpointDigest, input.createdAtMs);
|
|
155
|
+
this.#db.prepare("UPDATE local_preview_worldline_v2 SET sav_json=?, projection_json=?, projection_digest=?, message_ids_json=?, head_op_id=?, updated_at_ms=?, revision=revision+1 WHERE scope_key=? AND worldline_id=?").run(savJson, projectionJson, projectionDigest, idsJson, sav.ledgerHead, input.createdAtMs, this.#scopeKey, catalog.active);
|
|
156
|
+
}
|
|
157
|
+
#initializeRoot() {
|
|
158
|
+
if (this.#db.prepare("SELECT 1 FROM local_preview_worldline_catalog_v2 WHERE scope_key=?").get(this.#scopeKey) !== undefined)
|
|
159
|
+
return;
|
|
160
|
+
this.#inTransaction(() => {
|
|
161
|
+
if (this.#db.prepare("SELECT 1 FROM local_preview_worldline_catalog_v2 WHERE scope_key=?").get(this.#scopeKey) !== undefined)
|
|
162
|
+
return;
|
|
163
|
+
const sav = readSav(this.#db, this.#savBinding);
|
|
164
|
+
const rootId = sav.activeTimeline.worldlineId;
|
|
165
|
+
assertId(rootId, "root worldline id");
|
|
166
|
+
const now = 0;
|
|
167
|
+
this.#db.prepare("INSERT INTO local_preview_worldline_catalog_v2(scope_key,active_worldline_id,root_worldline_id,revision) VALUES (?,?,?,1)").run(this.#scopeKey, rootId, rootId);
|
|
168
|
+
const projectionJson = readProjectionJson(this.#db);
|
|
169
|
+
this.#insertLine({ worldlineId: rootId, title: "主线", parentWorldlineId: null, forkAnchorOpId: null, headOpId: sav.ledgerHead, sav: withWorldline(sav, rootId, this.#savBinding), projectionJson, messageIds: Object.freeze([]), createdAtMs: now, updatedAtMs: now, revision: 1 });
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
#assertScope(scope) { if (scopeKey(scope) !== this.#scopeKey)
|
|
173
|
+
fail("E_OPERATION_UNCERTAIN", "worldline scope 不属于当前 Runtime capture"); }
|
|
174
|
+
#catalog() { const row = this.#db.prepare("SELECT active_worldline_id,root_worldline_id,revision FROM local_preview_worldline_catalog_v2 WHERE scope_key=?").get(this.#scopeKey); if (row === undefined)
|
|
175
|
+
fail("E_OPERATION_UNCERTAIN", "worldline catalog 缺失"); return Object.freeze({ active: row.active_worldline_id, root: row.root_worldline_id, revision: row.revision }); }
|
|
176
|
+
#requireCas(revision, active) { const catalog = this.#catalog(); if (!Number.isSafeInteger(revision) || revision < 1 || catalog.revision !== revision || catalog.active !== active)
|
|
177
|
+
fail("E_OPERATION_UNCERTAIN", "worldline catalog 已改变,请重新读取"); return catalog; }
|
|
178
|
+
#line(id) { const found = this.#tryLine(id); if (found === null)
|
|
179
|
+
fail("E_VALIDATION_FAILED", "worldline 不存在"); return found; }
|
|
180
|
+
#tryLine(id) { const row = this.#db.prepare("SELECT worldline_id,title,parent_worldline_id,fork_anchor_op_id,head_op_id,sav_json,projection_json,projection_digest,message_ids_json,created_at_ms,updated_at_ms,revision FROM local_preview_worldline_v2 WHERE scope_key=? AND worldline_id=?").get(this.#scopeKey, id); return row === undefined ? null : row; }
|
|
181
|
+
#lineCount() { return this.#db.prepare("SELECT COUNT(*) AS n FROM local_preview_worldline_v2 WHERE scope_key=?").get(this.#scopeKey).n; }
|
|
182
|
+
#insertLine(line) { const projectionDigest = sha256(line.projectionJson); this.#db.prepare("INSERT INTO local_preview_worldline_v2(scope_key,worldline_id,title,parent_worldline_id,fork_anchor_op_id,head_op_id,sav_json,projection_json,projection_digest,message_ids_json,created_at_ms,updated_at_ms,revision) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)").run(this.#scopeKey, line.worldlineId, line.title, line.parentWorldlineId, line.forkAnchorOpId, line.headOpId, JSON.stringify(line.sav), line.projectionJson, projectionDigest, JSON.stringify(line.messageIds), line.createdAtMs, line.updatedAtMs, line.revision); }
|
|
183
|
+
#writeCatalog(catalog) { this.#db.prepare("UPDATE local_preview_worldline_catalog_v2 SET active_worldline_id=?,root_worldline_id=?,revision=? WHERE scope_key=?").run(catalog.active, catalog.root, catalog.revision, this.#scopeKey); }
|
|
184
|
+
#request(key, digest) { const row = this.#db.prepare("SELECT digest,result_json FROM local_preview_worldline_request_v2 WHERE scope_key=? AND idempotency_key=?").get(this.#scopeKey, key); if (row === undefined)
|
|
185
|
+
return null; if (row.digest !== digest)
|
|
186
|
+
fail("E_OPERATION_ID_CONFLICT", "worldline request idempotency key 已绑定不同请求"); return JSON.parse(row.result_json); }
|
|
187
|
+
#writeRequest(key, digest, result) { this.#db.prepare("INSERT INTO local_preview_worldline_request_v2(scope_key,idempotency_key,digest,result_json) VALUES (?,?,?,?)").run(this.#scopeKey, key, digest, JSON.stringify(result)); }
|
|
188
|
+
#inReadTransaction(work) { if (isTransaction(this.#db))
|
|
189
|
+
return work(); this.#db.exec("BEGIN"); try {
|
|
190
|
+
const result = work();
|
|
191
|
+
this.#db.exec("COMMIT");
|
|
192
|
+
return result;
|
|
193
|
+
}
|
|
194
|
+
catch (error) {
|
|
195
|
+
try {
|
|
196
|
+
this.#db.exec("ROLLBACK");
|
|
197
|
+
}
|
|
198
|
+
catch { }
|
|
199
|
+
throw error;
|
|
200
|
+
} }
|
|
201
|
+
#inTransaction(work) { if (isTransaction(this.#db))
|
|
202
|
+
fail("E_OPERATION_UNCERTAIN", "worldline public operation 不能嵌套 activation transaction"); this.#db.exec("BEGIN IMMEDIATE"); try {
|
|
203
|
+
const result = work();
|
|
204
|
+
this.#db.exec("COMMIT");
|
|
205
|
+
return result;
|
|
206
|
+
}
|
|
207
|
+
catch (error) {
|
|
208
|
+
try {
|
|
209
|
+
this.#db.exec("ROLLBACK");
|
|
210
|
+
}
|
|
211
|
+
catch { }
|
|
212
|
+
throw error;
|
|
213
|
+
} }
|
|
214
|
+
}
|
|
215
|
+
function summary(row) { return Object.freeze({ worldlineId: row.worldline_id, title: row.title, parentWorldlineId: row.parent_worldline_id, forkAnchorOpId: row.fork_anchor_op_id, headOpId: row.head_op_id, durableStatus: "durable", createdAtMs: row.created_at_ms, updatedAtMs: row.updated_at_ms, revision: row.revision }); }
|
|
216
|
+
function readSav(db, binding) { const row = db.prepare("SELECT value FROM local_preview_state WHERE key='sav'").get(); if (row === undefined)
|
|
217
|
+
fail("E_OPERATION_UNCERTAIN", "activation.sqlite 缺少 capture-bound SAV"); return parseSav(row.value, binding); }
|
|
218
|
+
function readProjectionJson(db) { const row = db.prepare("SELECT value FROM local_preview_state WHERE key='projection'").get(); if (row === undefined)
|
|
219
|
+
fail("E_OPERATION_UNCERTAIN", "activation.sqlite 缺少完整 world projection"); try {
|
|
220
|
+
const value = JSON.parse(row.value);
|
|
221
|
+
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
222
|
+
throw new Error("invalid");
|
|
223
|
+
canonicalTmwJsonValue(value, "local_preview_worldline_projection");
|
|
224
|
+
return row.value;
|
|
225
|
+
}
|
|
226
|
+
catch {
|
|
227
|
+
fail("E_OPERATION_UNCERTAIN", "worldline projection snapshot 损坏");
|
|
228
|
+
} }
|
|
229
|
+
function writeSavState(db, sav, projectionJson) { readProjectionValue(projectionJson); const state = Object.freeze({ head: Object.freeze({ revisionId: sav.activeTimeline.revisionId, revisionSequence: sav.activeTimeline.revisionSequence, ledgerHead: sav.ledgerHead }), sav, ledger: sav.eventLedger, narrative: sav.narrative, intents: sav.durableIntents, presentations: sav.presentationIntents }); for (const [key, value] of Object.entries(state))
|
|
230
|
+
db.prepare("INSERT OR REPLACE INTO local_preview_state(key,value) VALUES (?,?)").run(key, JSON.stringify(value)); db.prepare("INSERT OR REPLACE INTO local_preview_state(key,value) VALUES ('projection',?)").run(projectionJson); }
|
|
231
|
+
function withWorldline(sav, worldlineId, binding) { const canonical = canonicalRuntimeSavPayload(Object.freeze({ ...structuredClone(sav), activeTimeline: Object.freeze({ ...sav.activeTimeline, worldlineId, active: true }) })); assertBinding(canonical, binding); return canonical; }
|
|
232
|
+
function parseSav(value, binding) { try {
|
|
233
|
+
const sav = canonicalRuntimeSavPayload(JSON.parse(value));
|
|
234
|
+
if (binding !== undefined)
|
|
235
|
+
assertBinding(sav, binding);
|
|
236
|
+
return sav;
|
|
237
|
+
}
|
|
238
|
+
catch {
|
|
239
|
+
fail("E_OPERATION_UNCERTAIN", "worldline SAV snapshot 损坏或 runtime binding 不匹配");
|
|
240
|
+
} }
|
|
241
|
+
function readProjectionValue(value) { try {
|
|
242
|
+
const projection = JSON.parse(value);
|
|
243
|
+
if (typeof projection !== "object" || projection === null || Array.isArray(projection))
|
|
244
|
+
throw new Error("invalid");
|
|
245
|
+
canonicalTmwJsonValue(projection, "local_preview_worldline_projection");
|
|
246
|
+
}
|
|
247
|
+
catch {
|
|
248
|
+
fail("E_OPERATION_UNCERTAIN", "worldline projection snapshot 损坏");
|
|
249
|
+
} }
|
|
250
|
+
function assertBinding(sav, binding) { if (sav.epoch !== binding.epoch || sav.runtimeGeneration !== binding.runtimeGeneration || sav.runtimeContractHash !== binding.runtimeContractHash || sav.artifactDigest !== binding.artifactDigest || sav.sourceRevision !== binding.sourceRevision)
|
|
251
|
+
fail("E_OPERATION_UNCERTAIN", "worldline SAV runtime binding 不匹配"); }
|
|
252
|
+
function parseIds(value, label) { try {
|
|
253
|
+
const ids = JSON.parse(value);
|
|
254
|
+
if (!Array.isArray(ids))
|
|
255
|
+
throw new Error("invalid");
|
|
256
|
+
assertMessageIds(ids);
|
|
257
|
+
return Object.freeze([...ids]);
|
|
258
|
+
}
|
|
259
|
+
catch (error) {
|
|
260
|
+
if (error instanceof AuthoringBridgeError)
|
|
261
|
+
throw error;
|
|
262
|
+
fail("E_OPERATION_UNCERTAIN", `${label} 损坏`);
|
|
263
|
+
} }
|
|
264
|
+
function assertMessageIds(value) { const known = new Set(); for (const id of value) {
|
|
265
|
+
assertId(id, "message id");
|
|
266
|
+
if (known.has(id))
|
|
267
|
+
fail("E_VALIDATION_FAILED", "message checkpoint 不可有重复 id");
|
|
268
|
+
known.add(id);
|
|
269
|
+
} }
|
|
270
|
+
function hasPrefix(full, prefix) { return prefix.length <= full.length && prefix.every((id, index) => full[index] === id); }
|
|
271
|
+
function assertScope(scope) { if (scope.authority !== "worldengine.local-preview-worldlines/v1" || !scope.authorNamespace || !scope.projectId || !scope.scenarioId || !scope.saveId || !/^[a-f0-9]{64}$/u.test(scope.captureSha256))
|
|
272
|
+
fail("E_OPERATION_UNCERTAIN", "worldline scope 无效"); }
|
|
273
|
+
function scopeKey(scope) { assertScope(scope); return sha256(JSON.stringify([scope.authority, scope.authorNamespace, scope.projectId, scope.scenarioId, scope.saveId, scope.captureSha256])); }
|
|
274
|
+
function assertId(value, name) { if (typeof value !== "string" || !/^[A-Za-z0-9._:-]{1,256}$/u.test(value))
|
|
275
|
+
fail("E_VALIDATION_FAILED", `${name} 无效`); }
|
|
276
|
+
function assertTitle(value) { if (typeof value !== "string" || value.trim() !== value || value.length === 0 || value.length > 160)
|
|
277
|
+
fail("E_VALIDATION_FAILED", "worldline title 无效"); }
|
|
278
|
+
function assertTime(value, label) { if (typeof value !== "number" || !Number.isFinite(value) || !Number.isSafeInteger(value) || value < 0)
|
|
279
|
+
fail("E_VALIDATION_FAILED", `${label} 无效`); }
|
|
280
|
+
function sha256(value) { return createHash("sha256").update(value).digest("hex"); }
|
|
281
|
+
function isTransaction(db) { return db.isTransaction === true; }
|
|
282
|
+
function fail(code, message) { throw new AuthoringBridgeError(code, message); }
|