@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,1602 @@
|
|
|
1
|
+
import { AuthoringBridgeError, } from "@world-engines/authoring-bridge";
|
|
2
|
+
import { decodeScenarioPartWesp, encodeWesp, sha256Hex } from "@world-engines/project-format";
|
|
3
|
+
import { canonicalJson, projectNeutralArtifactEmbeddingSources } from "@world-engines/scenario-review-snapshot";
|
|
4
|
+
import { normalizeEntityProps, serializeEntityProps } from "@world-engines/authoring-ui/entity-props";
|
|
5
|
+
import { decodeSpatialWorldProtobuf, encodeSpatialWorldProtobuf, } from "@world-engines/spatial-authoring";
|
|
6
|
+
import { validatePlaneGeneration } from "@world-engines/spatial-authoring/plane-generation";
|
|
7
|
+
import { validateRoadGeneration } from "@world-engines/spatial-authoring/road-generation";
|
|
8
|
+
import { SPATIAL_TEMPLATE_PRESETS, validateSpatialTemplateConfiguration, validateSpatialTemplateLibrary, } from "@world-engines/spatial-authoring/spatial-templates";
|
|
9
|
+
import { PROCEDURAL_GENERATOR_IDENTITY, PROCEDURAL_WORLD_PROFILE, validateProceduralWorld, } from "@world-engines/spatial-authoring/procedural-city";
|
|
10
|
+
import { LadybugProjectRuntime, embedLadybugQuery } from "./ladybug-runtime.js";
|
|
11
|
+
import { ProjectTriggerRuntime } from "./trigger-runtime.js";
|
|
12
|
+
import { reduceTmwSceneV1, tmwSceneOperationFromEffect } from "@world-engines/tmw/scene-state";
|
|
13
|
+
import { initialTmwSceneFromMetadata, validateTmwSceneSourceReferencesV1 } from "@world-engines/tmw/scene-author-source";
|
|
14
|
+
import { assertSafeExistingProjectPath, ensureSafeProjectDirectory } from "./safe-project-path.js";
|
|
15
|
+
function canonicalEmbeddingGraph(graph) {
|
|
16
|
+
return {
|
|
17
|
+
entities: graph.entities.map((candidate) => ({
|
|
18
|
+
...candidate,
|
|
19
|
+
entity_id: typeof candidate.entity_id === "string" ? candidate.entity_id : candidate.id,
|
|
20
|
+
})),
|
|
21
|
+
relations: graph.relations.map((relation) => {
|
|
22
|
+
const src = typeof relation.from_id === "string" ? relation.from_id : relation.src;
|
|
23
|
+
const dst = typeof relation.to_id === "string" ? relation.to_id : relation.dst;
|
|
24
|
+
const kind = relation.kind;
|
|
25
|
+
return {
|
|
26
|
+
...relation,
|
|
27
|
+
relation_id: typeof relation.relation_id === "string" ? relation.relation_id
|
|
28
|
+
: `physical:${String(src)}:${String(kind)}:${String(dst)}`,
|
|
29
|
+
from_id: src,
|
|
30
|
+
to_id: dst,
|
|
31
|
+
};
|
|
32
|
+
}),
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* 本地 prepare 与正式 review reader 共用的 corpus 文本 seam。
|
|
37
|
+
*/
|
|
38
|
+
export function buildAuthoringRecallCorpus(graph) {
|
|
39
|
+
return projectNeutralArtifactEmbeddingSources(canonicalEmbeddingGraph(graph));
|
|
40
|
+
}
|
|
41
|
+
const LADYBUG_RECALL_PROJECTION_FORMAT = "projection-v2-scope-batch-digest";
|
|
42
|
+
const WORLD_GET_MAX_LIMIT = 200;
|
|
43
|
+
const WORLD_GET_MAX_RELATIONS_PER_PAGE = 2_000;
|
|
44
|
+
const textEncoder = new TextEncoder();
|
|
45
|
+
const textDecoder = new TextDecoder("utf-8", { fatal: true });
|
|
46
|
+
function fail(message, code = "E_VALIDATION_FAILED") {
|
|
47
|
+
throw new AuthoringBridgeError(code, message);
|
|
48
|
+
}
|
|
49
|
+
function record(value, label) {
|
|
50
|
+
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
51
|
+
fail(`${label} 必须是 object`);
|
|
52
|
+
return value;
|
|
53
|
+
}
|
|
54
|
+
function exactKeys(value, expected, label) {
|
|
55
|
+
const actual = Object.keys(value).sort();
|
|
56
|
+
const canonical = [...expected].sort();
|
|
57
|
+
if (actual.length !== canonical.length || actual.some((key, index) => key !== canonical[index])) {
|
|
58
|
+
fail(`${label} 字段必须严格为:${canonical.join(", ")}`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
function stringValue(value, label) {
|
|
62
|
+
if (typeof value !== "string" || value.trim().length === 0)
|
|
63
|
+
fail(`${label} 必须是非空 string`);
|
|
64
|
+
return value;
|
|
65
|
+
}
|
|
66
|
+
function canonicalBase64(value, label) {
|
|
67
|
+
if (typeof value !== "string" || !/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/u.test(value))
|
|
68
|
+
fail(`${label} 必须是 canonical base64`);
|
|
69
|
+
const decoded = Buffer.from(value, "base64");
|
|
70
|
+
if (decoded.toString("base64") !== value)
|
|
71
|
+
fail(`${label} 必须是 canonical base64`);
|
|
72
|
+
return value;
|
|
73
|
+
}
|
|
74
|
+
function resourceRefsSection(bytes) {
|
|
75
|
+
if (bytes.byteLength === 0)
|
|
76
|
+
return [];
|
|
77
|
+
let value;
|
|
78
|
+
try {
|
|
79
|
+
value = JSON.parse(textDecoder.decode(bytes));
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
return fail("resource_refs 不是合法 JSON section");
|
|
83
|
+
}
|
|
84
|
+
if (!Array.isArray(value))
|
|
85
|
+
fail("resource_refs 必须是 array");
|
|
86
|
+
const seen = new Set();
|
|
87
|
+
return value.map((candidate, index) => {
|
|
88
|
+
const ref = record(candidate, `resource_refs[${index}]`);
|
|
89
|
+
exactKeys(ref, ["path", "media_type", "bytes_base64"], `resource_refs[${index}]`);
|
|
90
|
+
const path = stringValue(ref.path, `resource_refs[${index}].path`);
|
|
91
|
+
if (seen.has(path))
|
|
92
|
+
fail("resource_refs.path 不能重复");
|
|
93
|
+
seen.add(path);
|
|
94
|
+
return {
|
|
95
|
+
path,
|
|
96
|
+
media_type: stringValue(ref.media_type, `resource_refs[${index}].media_type`),
|
|
97
|
+
bytes_base64: canonicalBase64(ref.bytes_base64, `resource_refs[${index}].bytes_base64`),
|
|
98
|
+
};
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
function worldSearchScopeId(value) {
|
|
102
|
+
if (value === undefined)
|
|
103
|
+
return undefined;
|
|
104
|
+
const filters = record(value, "world_search.filters");
|
|
105
|
+
const keys = Object.keys(filters);
|
|
106
|
+
if (keys.length !== 1 || keys[0] !== "scope_id") {
|
|
107
|
+
fail("world_search.filters 当前只支持 scope_id 精确过滤");
|
|
108
|
+
}
|
|
109
|
+
const scopeId = filters.scope_id;
|
|
110
|
+
if (typeof scopeId !== "string" || !/^[^\0\r\n]{1,512}$/u.test(scopeId)) {
|
|
111
|
+
fail("world_search.filters.scope_id 无效");
|
|
112
|
+
}
|
|
113
|
+
return scopeId;
|
|
114
|
+
}
|
|
115
|
+
function jsonSection(bytes, fallback, label) {
|
|
116
|
+
if (bytes.byteLength === 0)
|
|
117
|
+
return fallback;
|
|
118
|
+
try {
|
|
119
|
+
return record(JSON.parse(textDecoder.decode(bytes)), label);
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
return fail(`${label} 不是合法 JSON section`);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
function stableJson(value) {
|
|
126
|
+
return textEncoder.encode(JSON.stringify(value));
|
|
127
|
+
}
|
|
128
|
+
function stableStructuralJson(value) {
|
|
129
|
+
const normalize = (candidate) => {
|
|
130
|
+
if (Array.isArray(candidate))
|
|
131
|
+
return candidate.map(normalize);
|
|
132
|
+
if (candidate === null || typeof candidate !== "object")
|
|
133
|
+
return candidate;
|
|
134
|
+
return Object.fromEntries(Object.entries(candidate)
|
|
135
|
+
.filter(([, entry]) => entry !== undefined)
|
|
136
|
+
.sort(([left], [right]) => compareCodepoint(left, right))
|
|
137
|
+
.map(([key, entry]) => [key, normalize(entry)]));
|
|
138
|
+
};
|
|
139
|
+
return JSON.stringify(normalize(value));
|
|
140
|
+
}
|
|
141
|
+
/** locale 与 UTF-16 surrogate 顺序都不能参与稳定分页;这里显式按 Unicode code point 排序。 */
|
|
142
|
+
function compareCodepoint(left, right) {
|
|
143
|
+
const leftPoints = Array.from(left, (value) => value.codePointAt(0));
|
|
144
|
+
const rightPoints = Array.from(right, (value) => value.codePointAt(0));
|
|
145
|
+
const length = Math.min(leftPoints.length, rightPoints.length);
|
|
146
|
+
for (let index = 0; index < length; index += 1) {
|
|
147
|
+
if (leftPoints[index] !== rightPoints[index])
|
|
148
|
+
return leftPoints[index] - rightPoints[index];
|
|
149
|
+
}
|
|
150
|
+
return leftPoints.length - rightPoints.length;
|
|
151
|
+
}
|
|
152
|
+
function compareRelation(left, right) {
|
|
153
|
+
return compareCodepoint(left.src, right.src)
|
|
154
|
+
|| compareCodepoint(left.dst, right.dst)
|
|
155
|
+
|| compareCodepoint(left.kind, right.kind)
|
|
156
|
+
|| compareCodepoint(JSON.stringify(left.props), JSON.stringify(right.props));
|
|
157
|
+
}
|
|
158
|
+
function encodeWorldGetCursor(readback, afterId) {
|
|
159
|
+
const cursor = {
|
|
160
|
+
v: 1,
|
|
161
|
+
project_id: readback.project_id,
|
|
162
|
+
source_sha256: readback.source_sha256,
|
|
163
|
+
project_revision: readback.project_revision,
|
|
164
|
+
after_id: afterId,
|
|
165
|
+
};
|
|
166
|
+
return Buffer.from(JSON.stringify(cursor), "utf8").toString("base64url");
|
|
167
|
+
}
|
|
168
|
+
function decodeWorldGetCursor(encoded) {
|
|
169
|
+
if (encoded.length > 16_384 || !/^[A-Za-z0-9_-]+$/u.test(encoded))
|
|
170
|
+
fail("world_get cursor 无效", "E_SCHEMA_INVALID");
|
|
171
|
+
let value;
|
|
172
|
+
try {
|
|
173
|
+
const bytes = Buffer.from(encoded, "base64url");
|
|
174
|
+
if (bytes.toString("base64url") !== encoded)
|
|
175
|
+
fail("world_get cursor 无效", "E_SCHEMA_INVALID");
|
|
176
|
+
value = JSON.parse(textDecoder.decode(bytes));
|
|
177
|
+
}
|
|
178
|
+
catch {
|
|
179
|
+
return fail("world_get cursor 无效", "E_SCHEMA_INVALID");
|
|
180
|
+
}
|
|
181
|
+
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
182
|
+
fail("world_get cursor 无效", "E_SCHEMA_INVALID");
|
|
183
|
+
const cursor = value;
|
|
184
|
+
const keys = Object.keys(cursor).sort();
|
|
185
|
+
if (keys.join("\0") !== ["after_id", "project_id", "project_revision", "source_sha256", "v"].join("\0")
|
|
186
|
+
|| cursor.v !== 1
|
|
187
|
+
|| typeof cursor.project_id !== "string" || cursor.project_id.length === 0
|
|
188
|
+
|| typeof cursor.source_sha256 !== "string" || !/^[a-f0-9]{64}$/u.test(cursor.source_sha256)
|
|
189
|
+
|| !Number.isSafeInteger(cursor.project_revision) || cursor.project_revision < 0
|
|
190
|
+
|| typeof cursor.after_id !== "string" || cursor.after_id.trim().length === 0) {
|
|
191
|
+
fail("world_get cursor 无效", "E_SCHEMA_INVALID");
|
|
192
|
+
}
|
|
193
|
+
return cursor;
|
|
194
|
+
}
|
|
195
|
+
function readVarint(bytes, start) {
|
|
196
|
+
let value = 0;
|
|
197
|
+
let shift = 0;
|
|
198
|
+
for (let offset = start; offset < bytes.length && offset < start + 10; offset += 1) {
|
|
199
|
+
const byte = bytes[offset];
|
|
200
|
+
value += (byte & 0x7f) * 2 ** shift;
|
|
201
|
+
if ((byte & 0x80) === 0)
|
|
202
|
+
return { value, next: offset + 1 };
|
|
203
|
+
shift += 7;
|
|
204
|
+
}
|
|
205
|
+
return fail("Scenario protobuf varint 无效");
|
|
206
|
+
}
|
|
207
|
+
function varint(value) {
|
|
208
|
+
if (!Number.isSafeInteger(value) || value < 0)
|
|
209
|
+
fail("protobuf varint 必须为非负安全整数");
|
|
210
|
+
const out = [];
|
|
211
|
+
do {
|
|
212
|
+
const byte = value % 128;
|
|
213
|
+
value = Math.floor(value / 128);
|
|
214
|
+
out.push(value === 0 ? byte : byte | 0x80);
|
|
215
|
+
} while (value !== 0);
|
|
216
|
+
return new Uint8Array(out);
|
|
217
|
+
}
|
|
218
|
+
function concat(parts) { const out = new Uint8Array(parts.reduce((size, part) => size + part.length, 0)); let offset = 0; for (const part of parts) {
|
|
219
|
+
out.set(part, offset);
|
|
220
|
+
offset += part.length;
|
|
221
|
+
} return out; }
|
|
222
|
+
function fields(bytes) {
|
|
223
|
+
const out = new Map();
|
|
224
|
+
let offset = 0;
|
|
225
|
+
while (offset < bytes.length) {
|
|
226
|
+
const tag = readVarint(bytes, offset);
|
|
227
|
+
offset = tag.next;
|
|
228
|
+
const field = Math.floor(tag.value / 8);
|
|
229
|
+
const wire = tag.value % 8;
|
|
230
|
+
if (field <= 0)
|
|
231
|
+
fail("Scenario protobuf field 无效");
|
|
232
|
+
if (wire === 0) {
|
|
233
|
+
offset = readVarint(bytes, offset).next;
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
if (wire !== 2)
|
|
237
|
+
fail("Scenario protobuf 包含不支持字段");
|
|
238
|
+
const length = readVarint(bytes, offset);
|
|
239
|
+
offset = length.next;
|
|
240
|
+
const end = offset + length.value;
|
|
241
|
+
if (end > bytes.length)
|
|
242
|
+
fail("Scenario protobuf length 被截断");
|
|
243
|
+
const values = out.get(field) ?? [];
|
|
244
|
+
values.push(bytes.slice(offset, end));
|
|
245
|
+
out.set(field, values);
|
|
246
|
+
offset = end;
|
|
247
|
+
}
|
|
248
|
+
return out;
|
|
249
|
+
}
|
|
250
|
+
function first(map, field) { return map.get(field)?.[0] ?? new Uint8Array(); }
|
|
251
|
+
function decodeScenarioSourceV1(bytes) {
|
|
252
|
+
// schema_version 是唯一 varint field;其余 ScenarioPart 字段都是 bytes / nested message。
|
|
253
|
+
let offset = 0;
|
|
254
|
+
let version = 0;
|
|
255
|
+
const sections = new Map();
|
|
256
|
+
while (offset < bytes.length) {
|
|
257
|
+
const tag = readVarint(bytes, offset);
|
|
258
|
+
offset = tag.next;
|
|
259
|
+
const field = Math.floor(tag.value / 8);
|
|
260
|
+
const wire = tag.value % 8;
|
|
261
|
+
if (field === 1 && wire === 0) {
|
|
262
|
+
const next = readVarint(bytes, offset);
|
|
263
|
+
version = next.value;
|
|
264
|
+
offset = next.next;
|
|
265
|
+
continue;
|
|
266
|
+
}
|
|
267
|
+
if (wire !== 2)
|
|
268
|
+
fail("Scenario protobuf 包含不支持字段");
|
|
269
|
+
const length = readVarint(bytes, offset);
|
|
270
|
+
offset = length.next;
|
|
271
|
+
const end = offset + length.value;
|
|
272
|
+
if (end > bytes.length)
|
|
273
|
+
fail("Scenario protobuf length 被截断");
|
|
274
|
+
const values = sections.get(field) ?? [];
|
|
275
|
+
values.push(bytes.slice(offset, end));
|
|
276
|
+
sections.set(field, values);
|
|
277
|
+
offset = end;
|
|
278
|
+
}
|
|
279
|
+
return { schema_version: version, metadata: first(sections, 2), logical_graph: first(sections, 3), trigger_semantics: first(sections, 4), layout: first(sections, 5), resource_refs: first(sections, 6), prompt_retrieval: first(sections, 7), spatial_world: sections.get(8)?.[0] };
|
|
280
|
+
}
|
|
281
|
+
function encodedField(field, value) { return concat([varint(field * 8 + 2), varint(value.length), value]); }
|
|
282
|
+
function encodeScenarioSourceV1(source) {
|
|
283
|
+
const fieldsToWrite = [[2, source.metadata], [3, source.logical_graph], [4, source.trigger_semantics], [5, source.layout], [6, source.resource_refs], [7, source.prompt_retrieval]];
|
|
284
|
+
if (source.spatial_world !== undefined)
|
|
285
|
+
fieldsToWrite.push([8, source.spatial_world]);
|
|
286
|
+
return concat([varint(8), varint(source.schema_version), ...fieldsToWrite.filter(([, value]) => value.length > 0).map(([field, value]) => encodedField(field, value))]);
|
|
287
|
+
}
|
|
288
|
+
/** 只替换一个已知 bytes field;其他已知/未知 protobuf field 均保持原始字节和顺序。 */
|
|
289
|
+
function replaceScenarioBytesField(input, wanted, replacement) {
|
|
290
|
+
const output = [];
|
|
291
|
+
let offset = 0;
|
|
292
|
+
let replaced = false;
|
|
293
|
+
while (offset < input.length) {
|
|
294
|
+
const start = offset;
|
|
295
|
+
const tag = readVarint(input, offset);
|
|
296
|
+
offset = tag.next;
|
|
297
|
+
const field = Math.floor(tag.value / 8), wire = tag.value % 8;
|
|
298
|
+
if (wire === 0)
|
|
299
|
+
offset = readVarint(input, offset).next;
|
|
300
|
+
else if (wire === 2) {
|
|
301
|
+
const length = readVarint(input, offset);
|
|
302
|
+
offset = length.next + length.value;
|
|
303
|
+
if (offset > input.length)
|
|
304
|
+
fail("Scenario protobuf length 被截断");
|
|
305
|
+
}
|
|
306
|
+
else
|
|
307
|
+
fail("Scenario protobuf 包含不支持字段");
|
|
308
|
+
if (field === wanted) {
|
|
309
|
+
if (wire !== 2 || replaced)
|
|
310
|
+
fail("Scenario resource_refs field 重复或 wire 无效");
|
|
311
|
+
replaced = true;
|
|
312
|
+
if (replacement.byteLength > 0)
|
|
313
|
+
output.push(encodedField(wanted, replacement));
|
|
314
|
+
}
|
|
315
|
+
else
|
|
316
|
+
output.push(input.slice(start, offset));
|
|
317
|
+
}
|
|
318
|
+
if (!replaced && replacement.byteLength > 0)
|
|
319
|
+
output.push(encodedField(wanted, replacement));
|
|
320
|
+
return concat(output);
|
|
321
|
+
}
|
|
322
|
+
function spatialIds(spatial) {
|
|
323
|
+
if (spatial === undefined)
|
|
324
|
+
return [];
|
|
325
|
+
const source = fields(spatial);
|
|
326
|
+
const result = [];
|
|
327
|
+
for (const field of [7, 8, 9, 10, 11])
|
|
328
|
+
for (const object of source.get(field) ?? []) {
|
|
329
|
+
const id = first(fields(object), 1);
|
|
330
|
+
if (id.length > 0)
|
|
331
|
+
result.push(textDecoder.decode(id));
|
|
332
|
+
}
|
|
333
|
+
return result;
|
|
334
|
+
}
|
|
335
|
+
const SPATIAL_KEY = /^[a-z][a-z0-9-]{0,63}$/;
|
|
336
|
+
function finiteCoordinate(value, label) {
|
|
337
|
+
if (typeof value !== "number" || !Number.isFinite(value))
|
|
338
|
+
fail(`${label} 必须是有限数`);
|
|
339
|
+
return value;
|
|
340
|
+
}
|
|
341
|
+
function controlledActorId(value, label) {
|
|
342
|
+
if (value === undefined || value === null)
|
|
343
|
+
return value;
|
|
344
|
+
if (typeof value !== "string" || !/^[^\0\r\n]{1,512}$/u.test(value))
|
|
345
|
+
fail(`${label} 必须是有效 entity id 或 null`);
|
|
346
|
+
return value;
|
|
347
|
+
}
|
|
348
|
+
function spatialKind(value, label) {
|
|
349
|
+
if (value === "location" || value === "portal" || value === "region")
|
|
350
|
+
return value;
|
|
351
|
+
return fail(`${label} 不支持的 spatial kind`);
|
|
352
|
+
}
|
|
353
|
+
function spatialId(kind, key) { return `spatial:${kind}:${key}`; }
|
|
354
|
+
function planeGeneration(value) {
|
|
355
|
+
try {
|
|
356
|
+
return validatePlaneGeneration(value);
|
|
357
|
+
}
|
|
358
|
+
catch (error) {
|
|
359
|
+
return fail(error instanceof Error ? error.message : "PlaneGenerationV1 无效");
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
function roadGeneration(value) {
|
|
363
|
+
try {
|
|
364
|
+
return validateRoadGeneration(value);
|
|
365
|
+
}
|
|
366
|
+
catch (error) {
|
|
367
|
+
return fail(error instanceof Error ? error.message : "RoadGenerationV1 无效");
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
function proceduralWorld(value, widthMeters, heightMeters) {
|
|
371
|
+
try {
|
|
372
|
+
return validateProceduralWorld(value, { widthMeters, heightMeters });
|
|
373
|
+
}
|
|
374
|
+
catch (error) {
|
|
375
|
+
return fail(error instanceof Error ? error.message : "ProceduralWorldV1 无效");
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
function spatialTemplates(value) {
|
|
379
|
+
try {
|
|
380
|
+
return validateSpatialTemplateConfiguration(value);
|
|
381
|
+
}
|
|
382
|
+
catch (error) {
|
|
383
|
+
return fail(error instanceof Error ? error.message : "SpatialTemplateConfigurationV1 无效");
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
function spatialTemplateLibrary(value) {
|
|
387
|
+
try {
|
|
388
|
+
return validateSpatialTemplateLibrary(value);
|
|
389
|
+
}
|
|
390
|
+
catch (error) {
|
|
391
|
+
return fail(error instanceof Error ? error.message : "SpatialTemplateLibraryV1 无效");
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
function spatialTemplateKind(value) {
|
|
395
|
+
if (value === "city" || value === "parcel" || value === "building" || value === "interior")
|
|
396
|
+
return value;
|
|
397
|
+
return fail("template kind 必须是 city、parcel、building 或 interior");
|
|
398
|
+
}
|
|
399
|
+
function builtInSpatialTemplatePresets() {
|
|
400
|
+
return Object.values(SPATIAL_TEMPLATE_PRESETS);
|
|
401
|
+
}
|
|
402
|
+
function effectiveSpatialTemplatePresets(library) {
|
|
403
|
+
return [...builtInSpatialTemplatePresets(), ...(library?.presets ?? [])];
|
|
404
|
+
}
|
|
405
|
+
function emptyProceduralWorld(worldSeed) {
|
|
406
|
+
return proceduralWorld({
|
|
407
|
+
profile: PROCEDURAL_WORLD_PROFILE,
|
|
408
|
+
identity: PROCEDURAL_GENERATOR_IDENTITY,
|
|
409
|
+
worldSeed,
|
|
410
|
+
cities: [],
|
|
411
|
+
}, 1, 1);
|
|
412
|
+
}
|
|
413
|
+
const decodeSpatialWorld = decodeSpatialWorldProtobuf;
|
|
414
|
+
const encodeSpatialWorld = encodeSpatialWorldProtobuf;
|
|
415
|
+
function spatialWorldReadback(world) {
|
|
416
|
+
const copy = clone(world);
|
|
417
|
+
delete copy._wireBase64;
|
|
418
|
+
delete copy._topologyWireBase64;
|
|
419
|
+
delete copy._topologyWireKind;
|
|
420
|
+
for (const object of copy.objects) {
|
|
421
|
+
delete object._wireField;
|
|
422
|
+
delete object._wireBase64;
|
|
423
|
+
delete object._wireSignature;
|
|
424
|
+
delete object._authorSettings;
|
|
425
|
+
}
|
|
426
|
+
return copy;
|
|
427
|
+
}
|
|
428
|
+
function stableSourceModelJson(model) {
|
|
429
|
+
const { resource_refs_changed: _resourceRefsChanged, ...stable } = model;
|
|
430
|
+
return stableStructuralJson({ ...stable, spatial_world: model.spatial_world === undefined ? undefined : spatialWorldReadback(model.spatial_world) });
|
|
431
|
+
}
|
|
432
|
+
function clone(value) { return JSON.parse(JSON.stringify(value)); }
|
|
433
|
+
function canonicalProps(value, label) {
|
|
434
|
+
const props = record(value, label);
|
|
435
|
+
try {
|
|
436
|
+
const visit = (candidate) => {
|
|
437
|
+
if (Array.isArray(candidate)) {
|
|
438
|
+
candidate.forEach(visit);
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
if (typeof candidate !== "object" || candidate === null)
|
|
442
|
+
return;
|
|
443
|
+
const row = candidate;
|
|
444
|
+
if (row._system_prop_envelope_v1 === true) {
|
|
445
|
+
// 共享 codec 只用于结构/metadata 校验;返回原 wire,避免把旧 number/boolean/null 标量字符串化。
|
|
446
|
+
serializeEntityProps(normalizeEntityProps({ value: row }));
|
|
447
|
+
}
|
|
448
|
+
Object.values(row).forEach(visit);
|
|
449
|
+
};
|
|
450
|
+
visit(props);
|
|
451
|
+
return clone(props);
|
|
452
|
+
}
|
|
453
|
+
catch {
|
|
454
|
+
return fail(`${label} 不符合 canonical entity props contract`);
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
function canonicalViewAccess(value, label) {
|
|
458
|
+
if (value !== undefined && value !== "inherit" && value !== "deny" && value !== "read" && value !== "read_write")
|
|
459
|
+
fail(`${label} 无效`);
|
|
460
|
+
}
|
|
461
|
+
function validateEntityRow(row) {
|
|
462
|
+
if (!Array.isArray(row.name) || row.name.length === 0 || row.name.some((value) => typeof value !== "string" || value.length === 0))
|
|
463
|
+
fail("entity.name 必须是非空 string array");
|
|
464
|
+
canonicalViewAccess(row.view_access, "entity.view_access");
|
|
465
|
+
if (row.disabled !== undefined && typeof row.disabled !== "boolean")
|
|
466
|
+
fail("entity.disabled 必须是 boolean");
|
|
467
|
+
}
|
|
468
|
+
function validateRelationRow(row) {
|
|
469
|
+
canonicalViewAccess(row.view_access, "relation.view_access");
|
|
470
|
+
if (row.initial_available !== undefined && typeof row.initial_available !== "boolean")
|
|
471
|
+
fail("relation.initial_available 必须是 boolean");
|
|
472
|
+
for (const key of ["title", "content"])
|
|
473
|
+
if (row[key] !== undefined && typeof row[key] !== "string")
|
|
474
|
+
fail(`relation.${key} 必须是 string`);
|
|
475
|
+
}
|
|
476
|
+
function entityIdentity(row) {
|
|
477
|
+
return stringValue(row.entity_id ?? row.id, "entity.entity_id");
|
|
478
|
+
}
|
|
479
|
+
function relationIdentity(row) {
|
|
480
|
+
const src = stringValue(row.from_id ?? row.src, "relation.from_id");
|
|
481
|
+
const dst = stringValue(row.to_id ?? row.dst, "relation.to_id");
|
|
482
|
+
return { ...(row.relation_id === undefined ? {} : { relationId: stringValue(row.relation_id, "relation.relation_id") }), src, dst };
|
|
483
|
+
}
|
|
484
|
+
function persistedGraph(graph) {
|
|
485
|
+
const entities = graph.entities.map((entity) => {
|
|
486
|
+
const row = { ...entity };
|
|
487
|
+
if (typeof row.entity_id === "string")
|
|
488
|
+
delete row.id;
|
|
489
|
+
return row;
|
|
490
|
+
});
|
|
491
|
+
const relations = graph.relations.map((relation) => {
|
|
492
|
+
const row = { ...relation };
|
|
493
|
+
if (typeof row.from_id === "string")
|
|
494
|
+
delete row.src;
|
|
495
|
+
if (typeof row.to_id === "string")
|
|
496
|
+
delete row.dst;
|
|
497
|
+
return row;
|
|
498
|
+
});
|
|
499
|
+
return { ...graph, entities, relations };
|
|
500
|
+
}
|
|
501
|
+
function runtimeDefinitions(model) {
|
|
502
|
+
const current = model.retrieval.runtime_definitions;
|
|
503
|
+
if (current === undefined) {
|
|
504
|
+
const created = { triggers: [], trigger_variables_schema: [], cg_defs: [], achievement_defs: [], transaction_defs: [] };
|
|
505
|
+
model.retrieval.runtime_definitions = created;
|
|
506
|
+
return created;
|
|
507
|
+
}
|
|
508
|
+
return record(current, "prompt_retrieval.runtime_definitions");
|
|
509
|
+
}
|
|
510
|
+
function cgDefinitions(model) {
|
|
511
|
+
const definitions = runtimeDefinitions(model);
|
|
512
|
+
if (!Array.isArray(definitions.cg_defs))
|
|
513
|
+
fail("runtime_definitions.cg_defs 必须是 array");
|
|
514
|
+
return definitions.cg_defs;
|
|
515
|
+
}
|
|
516
|
+
function validateCgDefinition(model, value) {
|
|
517
|
+
const row = record(value, "cg definition");
|
|
518
|
+
const cg_id = stringValue(row.cg_id, "cg.cg_id");
|
|
519
|
+
const name = stringValue(row.name, "cg.name");
|
|
520
|
+
const asset_path = stringValue(row.asset_path, "cg.asset_path");
|
|
521
|
+
const anchor = record(row.anchor, "cg.anchor");
|
|
522
|
+
if (anchor.attr_path !== undefined)
|
|
523
|
+
fail("cg.anchor.attr_path 尚不受发布 runtime 支持;不能把子属性绑定降级为根 attr_key");
|
|
524
|
+
const kind = stringValue(anchor.kind, "cg.anchor.kind");
|
|
525
|
+
if (kind !== "node" && kind !== "edge" && kind !== "attr")
|
|
526
|
+
fail("cg.anchor.kind 无效");
|
|
527
|
+
const id = stringValue(anchor.id, "cg.anchor.id");
|
|
528
|
+
const attrKey = anchor.attr_key;
|
|
529
|
+
if ((kind === "attr") !== (typeof attrKey === "string" && attrKey.length > 0))
|
|
530
|
+
fail("cg.anchor.attr_key 与 kind 不匹配");
|
|
531
|
+
if (kind === "node" || kind === "attr") {
|
|
532
|
+
const entity = model.logical_graph.entities.find((item) => item.id === id);
|
|
533
|
+
if (entity === undefined || (kind === "attr" && !Object.hasOwn(entity.props, attrKey)))
|
|
534
|
+
fail("cg anchor entity/attr 不存在");
|
|
535
|
+
}
|
|
536
|
+
else if (!model.logical_graph.relations.some((item) => item.relation_id === id))
|
|
537
|
+
fail("cg anchor relation 不存在");
|
|
538
|
+
const unlock = row.unlock_trigger_id;
|
|
539
|
+
if (unlock !== undefined && (typeof unlock !== "string" || unlock.length === 0))
|
|
540
|
+
fail("cg.unlock_trigger_id 无效");
|
|
541
|
+
return { cg_id, name, asset_path, anchor: { kind, id, ...(kind === "attr" ? { attr_key: attrKey } : {}) }, ...(unlock === undefined ? {} : { unlock_trigger_id: unlock }) };
|
|
542
|
+
}
|
|
543
|
+
function sourceModel(source) {
|
|
544
|
+
const message = decodeScenarioSourceV1(source);
|
|
545
|
+
if (message.schema_version !== 1)
|
|
546
|
+
fail("ScenarioPartSourceV1 schema_version 必须为 1");
|
|
547
|
+
const metadata = jsonSection(message.metadata, {}, "metadata");
|
|
548
|
+
const graph = jsonSection(message.logical_graph, { entities: [], relations: [], history: [] }, "logical_graph");
|
|
549
|
+
const trigger = jsonSection(message.trigger_semantics, { triggers: [] }, "trigger_semantics");
|
|
550
|
+
const layout = jsonSection(message.layout, { interaction: {}, blueprint: {} }, "layout");
|
|
551
|
+
const retrieval = jsonSection(message.prompt_retrieval, {}, "prompt_retrieval");
|
|
552
|
+
const resourceRefs = resourceRefsSection(message.resource_refs);
|
|
553
|
+
const entities = Array.isArray(graph.entities) ? graph.entities.map((item) => {
|
|
554
|
+
const row = record(item, "logical_graph.entities[]");
|
|
555
|
+
return { ...row, id: entityIdentity(row), kind: stringValue(row.kind, "entity.kind"), props: record(row.props ?? {}, "entity.props") };
|
|
556
|
+
}) : fail("logical_graph.entities 必须是 array");
|
|
557
|
+
const relations = Array.isArray(graph.relations) ? graph.relations.map((item) => {
|
|
558
|
+
const row = record(item, "logical_graph.relations[]");
|
|
559
|
+
const identity = relationIdentity(row);
|
|
560
|
+
return { ...row, src: identity.src, dst: identity.dst, ...(identity.relationId === undefined ? {} : { relation_id: identity.relationId }), kind: stringValue(row.kind, "relation.kind"), props: record(row.props ?? {}, "relation.props") };
|
|
561
|
+
}) : fail("logical_graph.relations 必须是 array");
|
|
562
|
+
const triggers = trigger.triggers === undefined ? undefined : Array.isArray(trigger.triggers) ? trigger.triggers.map((item) => {
|
|
563
|
+
const row = record(item, "trigger_semantics.triggers[]");
|
|
564
|
+
const spatialRefs = row.spatial_refs === undefined ? undefined : (Array.isArray(row.spatial_refs) ? row.spatial_refs.map((ref) => stringValue(ref, "trigger.spatial_refs[]")) : fail("trigger.spatial_refs 必须是 array"));
|
|
565
|
+
return spatialRefs === undefined
|
|
566
|
+
? { ...row, id: stringValue(row.id, "trigger.id") }
|
|
567
|
+
: { ...row, id: stringValue(row.id, "trigger.id"), spatial_refs: spatialRefs };
|
|
568
|
+
}) : fail("trigger_semantics.triggers 必须是 array");
|
|
569
|
+
const history = Array.isArray(graph.history) ? graph.history.map((item) => record(item, "logical_graph.history[]")) : fail("logical_graph.history 必须是 array");
|
|
570
|
+
const spatialWorld = decodeSpatialWorld(message.spatial_world);
|
|
571
|
+
return { message, model: { metadata, logical_graph: { ...graph, entities, relations, history }, trigger_semantics: triggers === undefined ? trigger : { ...trigger, triggers }, layout: { interaction: record(layout.interaction ?? {}, "layout.interaction"), blueprint: record(layout.blueprint ?? {}, "layout.blueprint") }, retrieval, resource_refs: resourceRefs, resource_refs_changed: false, spatial_ids: spatialWorld?.objects.map((object) => object.id) ?? spatialIds(message.spatial_world), spatial_world: spatialWorld } };
|
|
572
|
+
}
|
|
573
|
+
function encodeModel(message, model) {
|
|
574
|
+
const next = {
|
|
575
|
+
...message,
|
|
576
|
+
metadata: stableJson(model.metadata),
|
|
577
|
+
logical_graph: stableJson(persistedGraph(model.logical_graph)),
|
|
578
|
+
trigger_semantics: stableJson(model.trigger_semantics),
|
|
579
|
+
layout: stableJson(model.layout),
|
|
580
|
+
...(model.resource_refs_changed ? { resource_refs: stableJson(model.resource_refs) } : {}),
|
|
581
|
+
prompt_retrieval: stableJson(model.retrieval),
|
|
582
|
+
spatial_world: encodeSpatialWorld(model.spatial_world),
|
|
583
|
+
};
|
|
584
|
+
return encodeWesp(encodeScenarioSourceV1(next));
|
|
585
|
+
}
|
|
586
|
+
function mergePatch(previous, patch) {
|
|
587
|
+
return { ...previous, ...patch };
|
|
588
|
+
}
|
|
589
|
+
/** history 只保留可解释 mutation 摘要,避免把 rawBase64/embedding/整份 source 再复制一遍。 */
|
|
590
|
+
function worldOperationHistorySummary(value) {
|
|
591
|
+
const operation = record(value, "world operation history");
|
|
592
|
+
const summary = {};
|
|
593
|
+
for (const key of ["type", "id", "kind", "src", "dst", "key", "label", "x", "y", "confirmed", "actorId"]) {
|
|
594
|
+
const field = operation[key];
|
|
595
|
+
if (field === null || typeof field === "string" || typeof field === "number" || typeof field === "boolean")
|
|
596
|
+
summary[key] = field;
|
|
597
|
+
}
|
|
598
|
+
if (typeof operation.props === "object" && operation.props !== null && !Array.isArray(operation.props)) {
|
|
599
|
+
const props = operation.props;
|
|
600
|
+
summary.changed_prop_ids = Object.keys(props).filter((key) => key !== "embedding" && key !== "rawBase64" && key !== "originalBytesBase64").sort();
|
|
601
|
+
const source = typeof props.source === "object" && props.source !== null && !Array.isArray(props.source)
|
|
602
|
+
? props.source
|
|
603
|
+
: props;
|
|
604
|
+
const sourceSummary = {};
|
|
605
|
+
for (const key of ["scopeId", "scope_id", "sourceId", "source_id", "sourceRevision", "source_revision", "sourceSha256", "source_sha256", "byte_sha256", "byteLength", "byte_length", "raw_byte_length", "documentId", "document_id", "operation_id"]) {
|
|
606
|
+
const field = source[key];
|
|
607
|
+
if (typeof field === "string" || typeof field === "number")
|
|
608
|
+
sourceSummary[key] = field;
|
|
609
|
+
}
|
|
610
|
+
if (Object.keys(sourceSummary).length > 0)
|
|
611
|
+
summary.source = sourceSummary;
|
|
612
|
+
}
|
|
613
|
+
return summary;
|
|
614
|
+
}
|
|
615
|
+
function validateSpatialReferences(model) {
|
|
616
|
+
try {
|
|
617
|
+
validateTmwSceneSourceReferencesV1(initialTmwSceneFromMetadata(model.metadata), sceneSourceReferences(model));
|
|
618
|
+
}
|
|
619
|
+
catch (error) {
|
|
620
|
+
fail(error instanceof Error ? error.message : "Scene source reference 无效");
|
|
621
|
+
}
|
|
622
|
+
const spatial = new Set(model.spatial_ids);
|
|
623
|
+
for (const trigger of model.trigger_semantics.triggers ?? []) {
|
|
624
|
+
for (const ref of trigger.spatial_refs ?? [])
|
|
625
|
+
if (!spatial.has(ref))
|
|
626
|
+
fail(`Trigger ${trigger.id} 引用了不存在的 spatial_id:${ref}`);
|
|
627
|
+
}
|
|
628
|
+
for (const entity of model.logical_graph.entities) {
|
|
629
|
+
const ref = entity.props.spatial_ref;
|
|
630
|
+
if (ref !== undefined && (typeof ref !== "string" || !spatial.has(ref)))
|
|
631
|
+
fail(`Ladybug ${entity.id} 引用了不存在的 spatial_ref`);
|
|
632
|
+
}
|
|
633
|
+
const actorId = model.spatial_world?.topology.controlledActorId;
|
|
634
|
+
if (actorId !== undefined && actorId !== null) {
|
|
635
|
+
const actor = model.logical_graph.entities.find((entity) => entity.id === actorId);
|
|
636
|
+
if (actor === undefined)
|
|
637
|
+
fail(`controlled actor 不存在:${actorId}`);
|
|
638
|
+
if (actor.kind !== "character" && actor.kind !== "npc")
|
|
639
|
+
fail(`controlled actor 必须是 character 或 npc:${actorId}`);
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
function sceneSourceReferences(model) {
|
|
643
|
+
const proceduralCityIds = model.spatial_world?.topology.procedural?.cities.map((city) => city.id) ?? [];
|
|
644
|
+
return {
|
|
645
|
+
entityNodeIds: model.logical_graph.entities.map((entity) => entity.id),
|
|
646
|
+
spatialWorld: model.spatial_world === undefined ? null : {
|
|
647
|
+
worldId: model.spatial_world.worldId, revision: model.spatial_world.spatialRevision,
|
|
648
|
+
spatialIds: [...model.spatial_ids, ...proceduralCityIds],
|
|
649
|
+
},
|
|
650
|
+
};
|
|
651
|
+
}
|
|
652
|
+
/** 空间写入只推进同一地图中仍存在目标的 version pin;绝不把删除或异世界引用迁移到别处。 */
|
|
653
|
+
function rebindSceneSpatialReference(reference, world, spatialIds) {
|
|
654
|
+
if (typeof reference !== "object" || reference === null || Array.isArray(reference))
|
|
655
|
+
return reference;
|
|
656
|
+
const value = reference;
|
|
657
|
+
if (value.worldId !== world.worldId || typeof value.spatialId !== "string" || !spatialIds.has(value.spatialId))
|
|
658
|
+
return reference;
|
|
659
|
+
return { worldId: world.worldId, sourceRevision: world.spatialRevision, spatialId: value.spatialId };
|
|
660
|
+
}
|
|
661
|
+
/** 同事务内更新已有 Scene 字面引用,避免地图版本已推进而 source 暂时不可读。 */
|
|
662
|
+
function rebindSceneSpatialReferencesAfterSpatialMutation(model) {
|
|
663
|
+
const world = model.spatial_world;
|
|
664
|
+
if (world === undefined)
|
|
665
|
+
return;
|
|
666
|
+
const spatialIds = new Set(sceneSourceReferences(model).spatialWorld?.spatialIds ?? []);
|
|
667
|
+
const scene = initialTmwSceneFromMetadata(model.metadata);
|
|
668
|
+
if (scene?.spatialRef !== null && scene?.spatialRef !== undefined) {
|
|
669
|
+
const spatialRef = rebindSceneSpatialReference(scene.spatialRef, world, spatialIds);
|
|
670
|
+
if (spatialRef !== scene.spatialRef)
|
|
671
|
+
model.metadata.initial_scene = { ...scene, spatialRef };
|
|
672
|
+
}
|
|
673
|
+
const document = model.trigger_semantics;
|
|
674
|
+
for (const effect of Array.isArray(document.effects) ? document.effects : []) {
|
|
675
|
+
if (typeof effect !== "object" || effect === null || Array.isArray(effect))
|
|
676
|
+
continue;
|
|
677
|
+
const node = effect;
|
|
678
|
+
if (node.capability_id !== "scene.begin" && node.capability_id !== "scene.location.set")
|
|
679
|
+
continue;
|
|
680
|
+
if (typeof node.arguments !== "object" || node.arguments === null || Array.isArray(node.arguments))
|
|
681
|
+
continue;
|
|
682
|
+
const argumentsValue = node.arguments;
|
|
683
|
+
const spatialRef = rebindSceneSpatialReference(argumentsValue.spatialRef, world, spatialIds);
|
|
684
|
+
if (spatialRef !== argumentsValue.spatialRef)
|
|
685
|
+
node.arguments = { ...argumentsValue, spatialRef };
|
|
686
|
+
}
|
|
687
|
+
for (const condition of Array.isArray(document.conditions) ? document.conditions : []) {
|
|
688
|
+
if (typeof condition !== "object" || condition === null || Array.isArray(condition))
|
|
689
|
+
continue;
|
|
690
|
+
const node = condition;
|
|
691
|
+
if (typeof node.expression !== "object" || node.expression === null || Array.isArray(node.expression))
|
|
692
|
+
continue;
|
|
693
|
+
const expression = node.expression;
|
|
694
|
+
if (expression.op !== "scene_field" || expression.field !== "location")
|
|
695
|
+
continue;
|
|
696
|
+
const value = rebindSceneSpatialReference(expression.value, world, spatialIds);
|
|
697
|
+
if (value !== expression.value)
|
|
698
|
+
node.expression = { ...expression, value };
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
/** 从同一 WESP 的 Ladybug/Trigger section 重建删除影响;不把 UI projection 当 authority。 */
|
|
702
|
+
function rebuildSpatialReferenceProjection(model) {
|
|
703
|
+
const world = model.spatial_world;
|
|
704
|
+
if (world === undefined)
|
|
705
|
+
return;
|
|
706
|
+
for (const object of world.objects)
|
|
707
|
+
object.references = [];
|
|
708
|
+
const byId = new Map(world.objects.map((object) => [object.id, object]));
|
|
709
|
+
const add = (spatialId, reference) => {
|
|
710
|
+
const target = byId.get(spatialId);
|
|
711
|
+
if (target !== undefined)
|
|
712
|
+
target.references.push(reference);
|
|
713
|
+
};
|
|
714
|
+
const scene = initialTmwSceneFromMetadata(model.metadata);
|
|
715
|
+
if (scene?.spatialRef !== null && scene?.spatialRef !== undefined) {
|
|
716
|
+
add(scene.spatialRef.spatialId, { ownerId: scene.sceneId, ownerKind: "scenario", fieldPath: "metadata.initial_scene.spatialRef" });
|
|
717
|
+
}
|
|
718
|
+
for (const entity of model.logical_graph.entities) {
|
|
719
|
+
if (typeof entity.props.spatial_ref === "string")
|
|
720
|
+
add(entity.props.spatial_ref, { ownerId: entity.id, ownerKind: "ladybug", fieldPath: "attributes.spatial_ref" });
|
|
721
|
+
}
|
|
722
|
+
for (const trigger of model.trigger_semantics.triggers ?? []) {
|
|
723
|
+
for (const spatialId of trigger.spatial_refs ?? [])
|
|
724
|
+
add(spatialId, { ownerId: trigger.id, ownerKind: "trigger", fieldPath: "spatial_refs" });
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
function removeSpatialReferences(model, spatialId) {
|
|
728
|
+
for (const entity of model.logical_graph.entities)
|
|
729
|
+
if (entity.props.spatial_ref === spatialId)
|
|
730
|
+
delete entity.props.spatial_ref;
|
|
731
|
+
for (const trigger of model.trigger_semantics.triggers ?? []) {
|
|
732
|
+
if (trigger.spatial_refs !== undefined)
|
|
733
|
+
trigger.spatial_refs = trigger.spatial_refs.filter((reference) => reference !== spatialId);
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
export class DesktopAuthoringRuntime {
|
|
737
|
+
root;
|
|
738
|
+
port;
|
|
739
|
+
capabilities;
|
|
740
|
+
adapter;
|
|
741
|
+
#triggerRuntime = new ProjectTriggerRuntime();
|
|
742
|
+
#recallRuntime;
|
|
743
|
+
#recallSourceSha256;
|
|
744
|
+
#recallGraphOnly = false;
|
|
745
|
+
#recallPreparedIdentity;
|
|
746
|
+
constructor(root, port, capabilities) {
|
|
747
|
+
this.root = root;
|
|
748
|
+
this.port = port;
|
|
749
|
+
this.capabilities = capabilities;
|
|
750
|
+
const handlers = {
|
|
751
|
+
world_schema: () => this.#worldSchema(),
|
|
752
|
+
spatial_template_library: () => this.#spatialTemplateLibrary(),
|
|
753
|
+
world_search: (ctx) => this.#worldSearch(ctx.call.input),
|
|
754
|
+
world_get: (ctx) => this.#worldGet(ctx.call.input),
|
|
755
|
+
world_traverse: (ctx) => this.#worldTraverse(ctx.call.input),
|
|
756
|
+
world_apply: (ctx) => this.#effect(ctx, "world", (model, input) => this.#applyWorld(model, input, ctx.effect_identity.operation_id)),
|
|
757
|
+
world_history: (ctx) => this.#history(ctx.call.input),
|
|
758
|
+
trigger_schema: async () => {
|
|
759
|
+
const { readback } = await this.#readModel();
|
|
760
|
+
return {
|
|
761
|
+
schema_version: 3,
|
|
762
|
+
document: "TMW TriggerDocumentV3",
|
|
763
|
+
authority: "scenario/source.wes#trigger_semantics",
|
|
764
|
+
project_id: readback.project_id,
|
|
765
|
+
project_revision: readback.project_revision,
|
|
766
|
+
source_sha256: readback.source_sha256,
|
|
767
|
+
empty_document: await this.#triggerRuntime.emptyDocument(),
|
|
768
|
+
...this.#triggerRuntime.authoringCapabilities(),
|
|
769
|
+
};
|
|
770
|
+
},
|
|
771
|
+
trigger_read: (ctx) => this.#triggerRead(ctx.call.input),
|
|
772
|
+
trigger_apply: (ctx) => this.#effect(ctx, "trigger", (model, input) => this.#applyTrigger(model, input)),
|
|
773
|
+
trigger_validate: (ctx) => this.#triggerValidate(ctx.call.input),
|
|
774
|
+
trigger_simulate: (ctx) => this.#triggerSimulate(ctx.call.input),
|
|
775
|
+
interaction_layout_read: () => this.#layoutRead("interaction"),
|
|
776
|
+
interaction_layout_apply: (ctx) => this.#effect(ctx, "layout", (model, input) => { model.layout.interaction = mergePatch(model.layout.interaction, record(input.patch, "patch")); }),
|
|
777
|
+
trigger_blueprint_layout_read: () => this.#layoutRead("blueprint"),
|
|
778
|
+
trigger_blueprint_layout_apply: (ctx) => this.#effect(ctx, "layout", (model, input) => { model.layout.blueprint = mergePatch(model.layout.blueprint, record(input.patch, "patch")); }),
|
|
779
|
+
retrieval_config_read: () => this.#readModel().then(({ model }) => clone(model.retrieval)),
|
|
780
|
+
retrieval_config_apply: (ctx) => this.#effect(ctx, "retrieval", (model, input) => { model.retrieval = mergePatch(model.retrieval, record(input.patch, "patch")); }),
|
|
781
|
+
retrieval_probe: (ctx) => this.#worldSearch({ query: ctx.call.input.query, mode: "hybrid", limit: ctx.call.input.top_k }),
|
|
782
|
+
};
|
|
783
|
+
const names = Object.keys(handlers);
|
|
784
|
+
if (capabilities.ask_user !== undefined) {
|
|
785
|
+
handlers.ask_user = (ctx) => capabilities.ask_user(ctx.call.input);
|
|
786
|
+
names.push("ask_user");
|
|
787
|
+
}
|
|
788
|
+
if (capabilities.preview_inspect !== undefined) {
|
|
789
|
+
handlers.preview_inspect = (ctx) => capabilities.preview_inspect(ctx.call.input);
|
|
790
|
+
names.push("preview_inspect");
|
|
791
|
+
}
|
|
792
|
+
if (capabilities.preview_interact !== undefined) {
|
|
793
|
+
handlers.preview_interact = (ctx) => capabilities.preview_interact(ctx.call.input);
|
|
794
|
+
names.push("preview_interact");
|
|
795
|
+
}
|
|
796
|
+
this.adapter = { capabilities: names, handlers };
|
|
797
|
+
}
|
|
798
|
+
static async open(root, port, capabilities = {}) {
|
|
799
|
+
const runtime = new DesktopAuthoringRuntime(root, port, capabilities);
|
|
800
|
+
await runtime.#hydrateFromSource();
|
|
801
|
+
return runtime;
|
|
802
|
+
}
|
|
803
|
+
async close() {
|
|
804
|
+
await this.#recallRuntime?.close();
|
|
805
|
+
}
|
|
806
|
+
/** 固定完整 Node/Edge 读回;派生 graph-only cache 不调用模型,不能成为第二 source authority。 */
|
|
807
|
+
async readGraphProjectionReadback(request = {}) {
|
|
808
|
+
const { runtime, model, readback } = await this.#ensureRecallRuntime(true);
|
|
809
|
+
const graph = await runtime.readGraphSnapshot(request);
|
|
810
|
+
const current = await this.port.readSourceReadback();
|
|
811
|
+
if (current.source_sha256 !== readback.source_sha256 || current.project_revision !== readback.project_revision) {
|
|
812
|
+
fail("图核对期间 source revision 已改变", "E_STALE_PROJECT_REVISION");
|
|
813
|
+
}
|
|
814
|
+
const nodeIds = new Set(graph.nodes.map((node) => node.id));
|
|
815
|
+
const edgeKeys = new Set(graph.edges.map((edge) => JSON.stringify([edge.src, edge.dst, edge.kind])));
|
|
816
|
+
return {
|
|
817
|
+
schema_version: 1,
|
|
818
|
+
source_sha256: readback.source_sha256,
|
|
819
|
+
project_revision: readback.project_revision,
|
|
820
|
+
graph,
|
|
821
|
+
authoring: {
|
|
822
|
+
metadata: clone(model.metadata),
|
|
823
|
+
runtime_definitions: clone(runtimeDefinitions(model)),
|
|
824
|
+
entities: clone(model.logical_graph.entities.filter((entity) => nodeIds.has(entity.id))),
|
|
825
|
+
relations: clone(model.logical_graph.relations.filter((relation) => edgeKeys.has(JSON.stringify([relation.src, relation.dst, relation.kind])))),
|
|
826
|
+
},
|
|
827
|
+
};
|
|
828
|
+
}
|
|
829
|
+
async #readModel() {
|
|
830
|
+
const readback = await this.port.readSourceReadback();
|
|
831
|
+
const payload = await decodeScenarioPartWesp(readback.source);
|
|
832
|
+
const parsed = sourceModel(payload);
|
|
833
|
+
rebuildSpatialReferenceProjection(parsed.model);
|
|
834
|
+
validateSpatialReferences(parsed.model);
|
|
835
|
+
return { readback, payload, ...parsed };
|
|
836
|
+
}
|
|
837
|
+
async #hydrateFromSource() {
|
|
838
|
+
await this.#readModel();
|
|
839
|
+
await this.#recallRuntime?.close();
|
|
840
|
+
this.#recallRuntime = undefined;
|
|
841
|
+
this.#recallSourceSha256 = undefined;
|
|
842
|
+
}
|
|
843
|
+
async #validateRecallVectors(model, readback, scopeId, port) {
|
|
844
|
+
const entities = model.logical_graph.entities.filter((entity) => entity.props.retrieval_excluded !== true
|
|
845
|
+
&& !["world_ingestion_batch", "world_ingestion_source_control", "world_document_ingestion_source_control"].includes(entity.kind)
|
|
846
|
+
&& (scopeId === undefined || entity.props.scope_id === scopeId));
|
|
847
|
+
const corpus = buildAuthoringRecallCorpus(model.logical_graph);
|
|
848
|
+
const requestedEntities = await Promise.all(corpus.filter((entry) => entities.some((entity) => entity.id === entry.entity_id)).map(async (entry) => Object.freeze({ ...entry, source_text_sha256: await sha256Hex(textEncoder.encode(entry.source_text)) })));
|
|
849
|
+
const prepared = await this.capabilities.get_prepared_embedding_snapshot?.({
|
|
850
|
+
project_id: readback.project_id, project_revision: readback.project_revision, source_sha256: readback.source_sha256,
|
|
851
|
+
...(scopeId === undefined ? {} : { scope_id: scopeId }),
|
|
852
|
+
entities: requestedEntities,
|
|
853
|
+
});
|
|
854
|
+
if (prepared == null) {
|
|
855
|
+
throw new AuthoringBridgeError("E_VALIDATION_FAILED", "semantic_recall_corpus_vectors_unverified;请先运行 review prepare", {
|
|
856
|
+
failure: "E_REVIEW_PREPARE_REQUIRED",
|
|
857
|
+
});
|
|
858
|
+
}
|
|
859
|
+
if (prepared.project_id !== readback.project_id || prepared.project_revision !== readback.project_revision
|
|
860
|
+
|| prepared.source_sha256 !== readback.source_sha256 || !prepared.scope_id || (scopeId !== undefined && scopeId !== prepared.scope_id)) {
|
|
861
|
+
return fail("semantic_recall_prepared_source_mismatch");
|
|
862
|
+
}
|
|
863
|
+
const { source, batch, materialization } = prepared.snapshot;
|
|
864
|
+
const descriptor = batch.descriptor;
|
|
865
|
+
if (!source.owner_id || !source.scenario_id || !Number.isSafeInteger(source.source_revision) || source.source_revision < 1
|
|
866
|
+
|| !/^[a-f0-9]{64}$/u.test(source.revision_digest) || descriptor.source_revision !== source.source_revision
|
|
867
|
+
|| descriptor.source_revision_digest !== source.revision_digest || !/^[a-f0-9]{64}$/u.test(descriptor.descriptor_sha256)
|
|
868
|
+
|| descriptor.provider !== port.providerId || descriptor.model !== port.modelId || descriptor.dimension !== port.dimensions
|
|
869
|
+
|| !Number.isSafeInteger(descriptor.dimension) || descriptor.dimension < 1)
|
|
870
|
+
return fail("semantic_recall_descriptor_mismatch");
|
|
871
|
+
const canonical = (value) => textEncoder.encode(canonicalJson(value));
|
|
872
|
+
const descriptorHash = await sha256Hex(canonical(descriptor));
|
|
873
|
+
if (descriptorHash !== await sha256Hex(canonical(materialization.descriptor)))
|
|
874
|
+
return fail("semantic_recall_descriptor_mismatch");
|
|
875
|
+
if (materialization.index_resource.path !== "__derived__/embedding-index-v1.json")
|
|
876
|
+
return fail("semantic_recall_index_invalid");
|
|
877
|
+
let index;
|
|
878
|
+
try {
|
|
879
|
+
index = record(JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(materialization.index_resource.bytes)), "embedding index");
|
|
880
|
+
}
|
|
881
|
+
catch {
|
|
882
|
+
return fail("semantic_recall_index_invalid");
|
|
883
|
+
}
|
|
884
|
+
if (index.schema_version !== 1 || !Array.isArray(index.entries)
|
|
885
|
+
|| ["provider", "model", "dimension", "descriptor_sha256", "source_revision", "source_revision_digest"].some((key) => index[key] !== descriptor[key])) {
|
|
886
|
+
return fail("semantic_recall_index_invalid");
|
|
887
|
+
}
|
|
888
|
+
if (entities.length === 0 || (scopeId !== undefined && entities.some((entity) => entity.props.scope_id !== prepared.scope_id))
|
|
889
|
+
|| batch.entries.length !== entities.length || index.entries.length !== entities.length
|
|
890
|
+
|| materialization.vector_by_entity_id.size !== entities.length)
|
|
891
|
+
return fail("semantic_recall_corpus_incomplete");
|
|
892
|
+
const entries = new Map(batch.entries.map((entry) => [entry.entity_id, entry]));
|
|
893
|
+
const requestedById = new Map(requestedEntities.map((entry) => [entry.entity_id, entry]));
|
|
894
|
+
const indexEntries = new Map(index.entries.map((value) => { const entry = record(value, "embedding index entry"); return [entry.entity_id, entry]; }));
|
|
895
|
+
if (entries.size !== entities.length || indexEntries.size !== entities.length)
|
|
896
|
+
return fail("semantic_recall_corpus_incomplete");
|
|
897
|
+
const vectors = new Map();
|
|
898
|
+
for (const entity of entities) {
|
|
899
|
+
const entry = entries.get(entity.id);
|
|
900
|
+
const indexed = indexEntries.get(entity.id);
|
|
901
|
+
const vector = materialization.vector_by_entity_id.get(entity.id);
|
|
902
|
+
const requested = requestedById.get(entity.id);
|
|
903
|
+
if (requested === undefined)
|
|
904
|
+
return fail("semantic_recall_vector_binding_mismatch");
|
|
905
|
+
const textHash = await sha256Hex(textEncoder.encode(requested.source_text));
|
|
906
|
+
if (entry === undefined || indexed === undefined || vector === undefined || entry.source_kind !== requested.source_kind
|
|
907
|
+
|| indexed.source_kind !== requested.source_kind || entry.source_text_sha256 !== textHash || indexed.source_text_sha256 !== textHash
|
|
908
|
+
|| vector.length !== descriptor.dimension || vector.some((value) => !Number.isFinite(value)) || !vector.some((value) => value !== 0)) {
|
|
909
|
+
return fail("semantic_recall_vector_binding_mismatch");
|
|
910
|
+
}
|
|
911
|
+
const vectorHash = await sha256Hex(canonical([...vector]));
|
|
912
|
+
if (vectorHash !== indexed.vector_sha256 || vectorHash !== await sha256Hex(canonical([...entry.vector])))
|
|
913
|
+
return fail("semantic_recall_vector_hash_mismatch");
|
|
914
|
+
vectors.set(entity.id, Object.freeze([...vector]));
|
|
915
|
+
}
|
|
916
|
+
return { sourceSha256: readback.source_sha256, projectRevision: readback.project_revision, scopeId: prepared.scope_id,
|
|
917
|
+
dimensions: descriptor.dimension, vectors,
|
|
918
|
+
cacheIdentity: await sha256Hex(canonical({ scope: prepared.scope_id, descriptor, indexHash: await sha256Hex(materialization.index_resource.bytes) })) };
|
|
919
|
+
}
|
|
920
|
+
async #ensureRecallRuntime(graphOnly = false, prepared) {
|
|
921
|
+
const { model, readback } = await this.#readModel();
|
|
922
|
+
if (prepared !== undefined && (prepared.sourceSha256 !== readback.source_sha256 || prepared.projectRevision !== readback.project_revision))
|
|
923
|
+
return fail("semantic_recall_prepared_source_mismatch");
|
|
924
|
+
if (this.#recallRuntime !== undefined && this.#recallSourceSha256 === readback.source_sha256 && this.#recallGraphOnly === graphOnly && this.#recallPreparedIdentity === prepared?.cacheIdentity) {
|
|
925
|
+
return { runtime: this.#recallRuntime, model, readback };
|
|
926
|
+
}
|
|
927
|
+
await this.#recallRuntime?.close();
|
|
928
|
+
this.#recallRuntime = undefined;
|
|
929
|
+
this.#recallSourceSha256 = undefined;
|
|
930
|
+
const projectedEntities = model.logical_graph.entities.filter((entity) => graphOnly || (entity.kind !== "world_ingestion_batch"
|
|
931
|
+
&& entity.kind !== "world_ingestion_source_control"
|
|
932
|
+
&& entity.kind !== "world_document_ingestion_source_control"));
|
|
933
|
+
const projectedIds = new Set(projectedEntities.map((entity) => entity.id));
|
|
934
|
+
// 图投影只验证实体/关系;不得为了它构造受 embedding 单条长度约束的文本语料。
|
|
935
|
+
const corpus = graphOnly
|
|
936
|
+
? new Map()
|
|
937
|
+
: new Map(buildAuthoringRecallCorpus(model.logical_graph).map((entry) => [entry.entity_id, entry.source_text]));
|
|
938
|
+
const dimensions = graphOnly ? 1 : prepared?.dimensions ?? 1;
|
|
939
|
+
const embeddingCacheIdentity = graphOnly ? "graph-only-complete-v1" : prepared?.cacheIdentity ?? "keyword-only";
|
|
940
|
+
const recallDirectory = await ensureSafeProjectDirectory(this.root, `.worldengine/projections/ladybug-recall/${LADYBUG_RECALL_PROJECTION_FORMAT}/${readback.source_sha256}/readonly-corpus-v1/${embeddingCacheIdentity}`);
|
|
941
|
+
const runtime = await LadybugProjectRuntime.open({
|
|
942
|
+
directory: recallDirectory,
|
|
943
|
+
dimensions,
|
|
944
|
+
});
|
|
945
|
+
try {
|
|
946
|
+
await assertSafeExistingProjectPath(this.root, recallDirectory);
|
|
947
|
+
await runtime.ingest({
|
|
948
|
+
schemaVersion: 1,
|
|
949
|
+
batchId: `wesp-${readback.source_sha256}`,
|
|
950
|
+
source: {
|
|
951
|
+
sourceId: "scenario/source.wes",
|
|
952
|
+
contentSha256: readback.source_sha256,
|
|
953
|
+
extractedAtMs: 0,
|
|
954
|
+
extractor: { harness: "worldengine-project-host", contractVersion: "wesp-logical-graph-v1" },
|
|
955
|
+
},
|
|
956
|
+
entities: projectedEntities.map((entity) => ({
|
|
957
|
+
id: entity.id,
|
|
958
|
+
kind: entity.kind,
|
|
959
|
+
text: graphOnly ? `${entity.id} ${entity.kind}` : corpus.get(entity.id) ?? `${entity.id} ${entity.kind}`,
|
|
960
|
+
props: entity.props,
|
|
961
|
+
...(!graphOnly && prepared?.vectors.has(entity.id)
|
|
962
|
+
? { embedding: prepared.vectors.get(entity.id) }
|
|
963
|
+
: {}),
|
|
964
|
+
})),
|
|
965
|
+
relations: model.logical_graph.relations.filter((relation) => projectedIds.has(relation.src) && projectedIds.has(relation.dst)),
|
|
966
|
+
});
|
|
967
|
+
await assertSafeExistingProjectPath(this.root, recallDirectory);
|
|
968
|
+
}
|
|
969
|
+
catch (error) {
|
|
970
|
+
await runtime.close();
|
|
971
|
+
if (error instanceof AuthoringBridgeError)
|
|
972
|
+
throw error;
|
|
973
|
+
return fail("Ladybug projection hydrate 失败");
|
|
974
|
+
}
|
|
975
|
+
this.#recallRuntime = runtime;
|
|
976
|
+
this.#recallSourceSha256 = readback.source_sha256;
|
|
977
|
+
this.#recallGraphOnly = graphOnly;
|
|
978
|
+
this.#recallPreparedIdentity = prepared?.cacheIdentity;
|
|
979
|
+
return { runtime, model, readback };
|
|
980
|
+
}
|
|
981
|
+
async #effect(ctx, section, mutate) {
|
|
982
|
+
const identity = ctx.effect_identity;
|
|
983
|
+
const { readback, payload, message, model } = await this.#readModel();
|
|
984
|
+
if (readback.project_revision !== identity.expected_project_revision)
|
|
985
|
+
fail(`期望 revision ${identity.expected_project_revision},当前 ${readback.project_revision}`, "E_STALE_PROJECT_REVISION");
|
|
986
|
+
const next = clone(model);
|
|
987
|
+
await mutate(next, ctx.call.input);
|
|
988
|
+
rebuildSpatialReferenceProjection(next);
|
|
989
|
+
validateSpatialReferences(next);
|
|
990
|
+
const operations = Array.isArray(ctx.call.input.operations) ? ctx.call.input.operations : [];
|
|
991
|
+
const resourceOnly = section === "world" && operations.length > 0 && operations.every((value) => {
|
|
992
|
+
const type = typeof value === "object" && value !== null && !Array.isArray(value) ? value.type : undefined;
|
|
993
|
+
return type === "bind_gallery_resource" || type === "unbind_resource_ref";
|
|
994
|
+
});
|
|
995
|
+
const source = resourceOnly
|
|
996
|
+
? next.resource_refs_changed
|
|
997
|
+
? await encodeWesp(replaceScenarioBytesField(payload, 6, stableJson(next.resource_refs)))
|
|
998
|
+
: readback.source
|
|
999
|
+
: await encodeModel(message, next);
|
|
1000
|
+
const receipt = await this.port.writeScenario({ ...identity, expected_source_sha256: readback.source_sha256, source });
|
|
1001
|
+
await this.#hydrateFromSource();
|
|
1002
|
+
const written = await this.#readModel();
|
|
1003
|
+
const sourceMatches = resourceOnly
|
|
1004
|
+
? stableStructuralJson(written.model.resource_refs) === stableStructuralJson(next.resource_refs)
|
|
1005
|
+
: stableSourceModelJson(written.model) === stableSourceModelJson(next);
|
|
1006
|
+
if (written.readback.project_revision !== receipt.next_project_revision
|
|
1007
|
+
|| written.model.logical_graph.entities.length !== next.logical_graph.entities.length
|
|
1008
|
+
|| !sourceMatches) {
|
|
1009
|
+
fail("WESP write readback 与 typed mutation 不一致", "E_OPERATION_UNCERTAIN");
|
|
1010
|
+
}
|
|
1011
|
+
return { ...receipt, changed_sections: receipt.changed_sections.length === 0 ? [] : [section], readback_verified: true };
|
|
1012
|
+
}
|
|
1013
|
+
async #applyWorld(model, input, operationId) {
|
|
1014
|
+
const operations = input.operations;
|
|
1015
|
+
if (!Array.isArray(operations))
|
|
1016
|
+
fail("operations 必须是 array");
|
|
1017
|
+
const resourceOperationCount = operations.filter((value) => {
|
|
1018
|
+
const type = typeof value === "object" && value !== null && !Array.isArray(value) ? value.type : undefined;
|
|
1019
|
+
return type === "bind_gallery_resource" || type === "unbind_resource_ref";
|
|
1020
|
+
}).length;
|
|
1021
|
+
if (resourceOperationCount > 0 && resourceOperationCount !== operations.length)
|
|
1022
|
+
fail("resource_refs 操作不能与其他 world operation 混合");
|
|
1023
|
+
let spatialMutated = false;
|
|
1024
|
+
for (const [operationIndex, value] of operations.entries()) {
|
|
1025
|
+
const op = record(value, "world operation");
|
|
1026
|
+
const type = stringValue(op.type, "world operation.type");
|
|
1027
|
+
if (type.startsWith("scene.")) {
|
|
1028
|
+
const { type: _type, ...argumentsValue } = op;
|
|
1029
|
+
try {
|
|
1030
|
+
const operation = tmwSceneOperationFromEffect(type, argumentsValue);
|
|
1031
|
+
if (operation === null)
|
|
1032
|
+
fail("无效 Scene operation");
|
|
1033
|
+
model.metadata.initial_scene = reduceTmwSceneV1(initialTmwSceneFromMetadata(model.metadata), operation, `${operationId}:${operationIndex}`);
|
|
1034
|
+
}
|
|
1035
|
+
catch (error) {
|
|
1036
|
+
fail(error instanceof Error ? error.message : "Scene operation 无效");
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
else if (type === "upsert_entity") {
|
|
1040
|
+
const id = stringValue(op.id, "entity.id");
|
|
1041
|
+
const index = model.logical_graph.entities.findIndex((item) => item.id === id);
|
|
1042
|
+
const entity = { ...(index === -1 ? {} : model.logical_graph.entities[index]), id, kind: stringValue(op.kind, "entity.kind"), props: record(op.props ?? {}, "entity.props") };
|
|
1043
|
+
if (index === -1)
|
|
1044
|
+
model.logical_graph.entities.push(entity);
|
|
1045
|
+
else
|
|
1046
|
+
model.logical_graph.entities[index] = entity;
|
|
1047
|
+
}
|
|
1048
|
+
else if (type === "delete_entity") {
|
|
1049
|
+
const id = stringValue(op.id, "entity.id");
|
|
1050
|
+
if (model.spatial_world?.topology.controlledActorId === id)
|
|
1051
|
+
fail(`必须先解除 controlled actor 绑定:${id}`);
|
|
1052
|
+
model.logical_graph.entities = model.logical_graph.entities.filter((item) => item.id !== id);
|
|
1053
|
+
model.logical_graph.relations = model.logical_graph.relations.filter((item) => item.src !== id && item.dst !== id);
|
|
1054
|
+
}
|
|
1055
|
+
else if (type === "upsert_relation") {
|
|
1056
|
+
const src = stringValue(op.src, "relation.src"), dst = stringValue(op.dst, "relation.dst"), kind = stringValue(op.kind, "relation.kind");
|
|
1057
|
+
const index = model.logical_graph.relations.findIndex((item) => item.src === src && item.dst === dst && item.kind === kind);
|
|
1058
|
+
const relation = { ...(index === -1 ? {} : model.logical_graph.relations[index]), src, dst, kind, props: record(op.props ?? {}, "relation.props") };
|
|
1059
|
+
if (!model.logical_graph.entities.some((item) => item.id === relation.src) || !model.logical_graph.entities.some((item) => item.id === relation.dst))
|
|
1060
|
+
fail("relation 两端实体必须存在");
|
|
1061
|
+
if (index === -1)
|
|
1062
|
+
model.logical_graph.relations.push(relation);
|
|
1063
|
+
else
|
|
1064
|
+
model.logical_graph.relations[index] = relation;
|
|
1065
|
+
}
|
|
1066
|
+
else if (type === "upsert_entity_row") {
|
|
1067
|
+
const row = record(op.entity, "entity row");
|
|
1068
|
+
validateEntityRow(row);
|
|
1069
|
+
const id = entityIdentity(row);
|
|
1070
|
+
const index = model.logical_graph.entities.findIndex((item) => item.id === id);
|
|
1071
|
+
const entity = { ...row, entity_id: id, id, kind: stringValue(row.kind, "entity.kind"), props: canonicalProps(row.props ?? {}, "entity.props") };
|
|
1072
|
+
if (index === -1)
|
|
1073
|
+
model.logical_graph.entities.push(entity);
|
|
1074
|
+
else
|
|
1075
|
+
model.logical_graph.entities[index] = entity;
|
|
1076
|
+
}
|
|
1077
|
+
else if (type === "upsert_relation_row") {
|
|
1078
|
+
const row = record(op.relation, "relation row");
|
|
1079
|
+
validateRelationRow(row);
|
|
1080
|
+
const identity = relationIdentity(row);
|
|
1081
|
+
if (identity.relationId === undefined)
|
|
1082
|
+
fail("relation row 缺少 relation_id");
|
|
1083
|
+
if (!model.logical_graph.entities.some((item) => item.id === identity.src) || !model.logical_graph.entities.some((item) => item.id === identity.dst))
|
|
1084
|
+
fail("relation 两端实体必须存在");
|
|
1085
|
+
const relation = { ...row, relation_id: identity.relationId, from_id: identity.src, to_id: identity.dst, src: identity.src, dst: identity.dst, kind: stringValue(row.kind, "relation.kind"), props: canonicalProps(row.props ?? {}, "relation.props") };
|
|
1086
|
+
const index = model.logical_graph.relations.findIndex((item) => item.relation_id === identity.relationId
|
|
1087
|
+
|| (item.relation_id === undefined && item.src === identity.src && item.dst === identity.dst && item.kind === relation.kind));
|
|
1088
|
+
if (index === -1)
|
|
1089
|
+
model.logical_graph.relations.push(relation);
|
|
1090
|
+
else
|
|
1091
|
+
model.logical_graph.relations[index] = relation;
|
|
1092
|
+
}
|
|
1093
|
+
else if (type === "delete_relation") {
|
|
1094
|
+
const src = stringValue(op.src, "relation.src"), dst = stringValue(op.dst, "relation.dst"), kind = stringValue(op.kind, "relation.kind");
|
|
1095
|
+
model.logical_graph.relations = model.logical_graph.relations.filter((item) => item.src !== src || item.dst !== dst || item.kind !== kind);
|
|
1096
|
+
}
|
|
1097
|
+
else if (type === "upsert_cg") {
|
|
1098
|
+
const cg = validateCgDefinition(model, op.cg ?? op);
|
|
1099
|
+
const definitions = runtimeDefinitions(model);
|
|
1100
|
+
const current = cgDefinitions(model);
|
|
1101
|
+
const index = current.findIndex((item) => item.cg_id === cg.cg_id);
|
|
1102
|
+
definitions.cg_defs = index === -1 ? [...current, cg] : current.map((item, offset) => offset === index ? cg : item);
|
|
1103
|
+
}
|
|
1104
|
+
else if (type === "delete_cg") {
|
|
1105
|
+
const cgId = stringValue(op.cg_id, "cg.cg_id");
|
|
1106
|
+
const definitions = runtimeDefinitions(model);
|
|
1107
|
+
definitions.cg_defs = cgDefinitions(model).filter((item) => item.cg_id !== cgId);
|
|
1108
|
+
}
|
|
1109
|
+
else if (type === "bind_gallery_resource") {
|
|
1110
|
+
exactKeys(op, ["type", "path"], "bind_gallery_resource");
|
|
1111
|
+
const path = stringValue(op.path, "bind_gallery_resource.path");
|
|
1112
|
+
const { readLocalGalleryImage } = await import("./local-gallery.js");
|
|
1113
|
+
const image = await readLocalGalleryImage(this.root, path);
|
|
1114
|
+
const next = {
|
|
1115
|
+
path: image.asset_path,
|
|
1116
|
+
media_type: image.mime,
|
|
1117
|
+
bytes_base64: Buffer.from(image.bytes).toString("base64"),
|
|
1118
|
+
};
|
|
1119
|
+
const index = model.resource_refs.findIndex((entry) => entry.path === next.path);
|
|
1120
|
+
if (index === -1) {
|
|
1121
|
+
model.resource_refs.push(next);
|
|
1122
|
+
model.resource_refs_changed = true;
|
|
1123
|
+
}
|
|
1124
|
+
else if (stableStructuralJson(model.resource_refs[index]) !== stableStructuralJson(next)) {
|
|
1125
|
+
fail("resource_refs 已存在同路径但内容不一致", "E_OPERATION_UNCERTAIN");
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
else if (type === "unbind_resource_ref") {
|
|
1129
|
+
exactKeys(op, ["type", "path"], "unbind_resource_ref");
|
|
1130
|
+
const path = stringValue(op.path, "unbind_resource_ref.path");
|
|
1131
|
+
const next = model.resource_refs.filter((entry) => entry.path !== path);
|
|
1132
|
+
if (next.length === model.resource_refs.length)
|
|
1133
|
+
fail("resource_refs 不存在该 path", "E_REFERENCE_MISSING");
|
|
1134
|
+
model.resource_refs = next;
|
|
1135
|
+
model.resource_refs_changed = true;
|
|
1136
|
+
}
|
|
1137
|
+
else if (type === "spatial_set_generation") {
|
|
1138
|
+
exactKeys(op, ["type", "generation", "confirmed"], "spatial_set_generation");
|
|
1139
|
+
if (typeof op.confirmed !== "boolean")
|
|
1140
|
+
fail("spatial_set_generation.confirmed 必须是 boolean");
|
|
1141
|
+
const generation = planeGeneration(op.generation);
|
|
1142
|
+
let world = model.spatial_world;
|
|
1143
|
+
if (world === undefined) {
|
|
1144
|
+
const procedural = proceduralWorld({
|
|
1145
|
+
profile: PROCEDURAL_WORLD_PROFILE,
|
|
1146
|
+
identity: PROCEDURAL_GENERATOR_IDENTITY,
|
|
1147
|
+
worldSeed: generation.seed,
|
|
1148
|
+
cities: [],
|
|
1149
|
+
}, generation.widthMeters, generation.heightMeters);
|
|
1150
|
+
world = { schemaVersion: 1, worldId: "world", spatialRevision: 0, topology: { kind: "freeform", width: generation.widthMeters, height: generation.heightMeters, generation, procedural }, objects: [] };
|
|
1151
|
+
model.spatial_world = world;
|
|
1152
|
+
model.spatial_ids = [];
|
|
1153
|
+
}
|
|
1154
|
+
else {
|
|
1155
|
+
if (world._wireBase64 !== undefined && world._topologyWireKind !== 2)
|
|
1156
|
+
fail("旧的非平面或未知 spatial topology 不能静默改写为平面地图");
|
|
1157
|
+
const shrinking = generation.widthMeters < world.topology.width || generation.heightMeters < world.topology.height;
|
|
1158
|
+
if (shrinking && world.objects.some((object) => object.x > generation.widthMeters || object.y > generation.heightMeters)) {
|
|
1159
|
+
fail("生成范围缩小会使现有 spatial object 超出边界");
|
|
1160
|
+
}
|
|
1161
|
+
if (world.topology.generation !== undefined
|
|
1162
|
+
&& stableStructuralJson(world.topology.generation) !== stableStructuralJson(generation)
|
|
1163
|
+
&& op.confirmed !== true)
|
|
1164
|
+
fail("替换现有平面生成参数必须显式确认");
|
|
1165
|
+
const previousProcedural = world.topology.procedural;
|
|
1166
|
+
if (previousProcedural !== undefined && previousProcedural.cities.length > 0
|
|
1167
|
+
&& world.topology.generation !== undefined
|
|
1168
|
+
&& stableStructuralJson(world.topology.generation) !== stableStructuralJson(generation)) {
|
|
1169
|
+
proceduralWorld(previousProcedural, generation.widthMeters, generation.heightMeters);
|
|
1170
|
+
fail("已有程序化城市时不能修改 generation;必须先迁移或清理城市");
|
|
1171
|
+
}
|
|
1172
|
+
const procedural = proceduralWorld({
|
|
1173
|
+
...(previousProcedural ?? emptyProceduralWorld(generation.seed)),
|
|
1174
|
+
worldSeed: generation.seed,
|
|
1175
|
+
}, generation.widthMeters, generation.heightMeters);
|
|
1176
|
+
world.topology = { ...world.topology, width: generation.widthMeters, height: generation.heightMeters, generation, procedural };
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
1179
|
+
else if (type === "spatial_set_road_generation") {
|
|
1180
|
+
exactKeys(op, ["type", "roadGeneration", "confirmed"], "spatial_set_road_generation");
|
|
1181
|
+
if (op.confirmed !== true)
|
|
1182
|
+
fail("保存道路设置必须显式确认");
|
|
1183
|
+
const world = model.spatial_world;
|
|
1184
|
+
if (world?.topology.generation === undefined)
|
|
1185
|
+
fail("保存道路设置必须先保存有效 generation");
|
|
1186
|
+
world.topology = { ...world.topology, roadGeneration: roadGeneration(op.roadGeneration) };
|
|
1187
|
+
}
|
|
1188
|
+
else if (type === "save_template_definition") {
|
|
1189
|
+
// 定义库与 active selection 分离:保存/更新定义绝不触碰生成器、objects 或已套用模板。
|
|
1190
|
+
exactKeys(op, ["type", "definition"], "save_template_definition");
|
|
1191
|
+
const world = model.spatial_world;
|
|
1192
|
+
if (world === undefined)
|
|
1193
|
+
fail("保存模板定义必须先保存有效 generation");
|
|
1194
|
+
const definition = record(op.definition, "template definition");
|
|
1195
|
+
const candidateDefinition = spatialTemplateLibrary({ version: 1, presets: [definition] }).presets[0];
|
|
1196
|
+
if (builtInSpatialTemplatePresets().some((preset) => preset.kind === candidateDefinition.kind && preset.id === candidateDefinition.id)) {
|
|
1197
|
+
fail("内置模板不可覆盖;请使用新的 kind/id 保存自定义模板定义");
|
|
1198
|
+
}
|
|
1199
|
+
const current = world.topology.templateLibrary ?? { version: 1, presets: [] };
|
|
1200
|
+
const existing = current.presets.filter((preset) => preset.kind === candidateDefinition.kind && preset.id === candidateDefinition.id);
|
|
1201
|
+
const highestRevision = existing.reduce((highest, preset) => Math.max(highest, preset.revision), 0);
|
|
1202
|
+
if (candidateDefinition.revision !== highestRevision + 1)
|
|
1203
|
+
fail("模板定义 revision 必须从 1 开始并对同 kind/id 连续递增");
|
|
1204
|
+
const library = spatialTemplateLibrary({ version: 1, presets: [...current.presets, candidateDefinition] });
|
|
1205
|
+
world.topology = { ...world.topology, templateLibrary: library };
|
|
1206
|
+
}
|
|
1207
|
+
else if (type === "apply_template_selection") {
|
|
1208
|
+
// 仅把经 library 校验的冻结快照写入 active configuration;不会创建、删除或搬移空间几何。
|
|
1209
|
+
exactKeys(op, ["type", "kind", "selection", "confirmedImpact"], "apply_template_selection");
|
|
1210
|
+
if (op.confirmedImpact !== true)
|
|
1211
|
+
fail("套用模板到现有地图必须显式确认影响");
|
|
1212
|
+
const world = model.spatial_world;
|
|
1213
|
+
if (world === undefined)
|
|
1214
|
+
fail("套用模板必须先保存有效 generation");
|
|
1215
|
+
const kind = spatialTemplateKind(op.kind);
|
|
1216
|
+
const candidate = spatialTemplates({ version: 1, [kind]: op.selection });
|
|
1217
|
+
const selection = candidate[kind];
|
|
1218
|
+
const matching = effectiveSpatialTemplatePresets(world.topology.templateLibrary).find((preset) => preset.kind === kind && preset.id === selection.presetId && preset.revision === selection.revision);
|
|
1219
|
+
if (matching === undefined || stableStructuralJson(matching.parameters) !== stableStructuralJson(selection.parameters)) {
|
|
1220
|
+
fail("模板选择必须精确匹配已发布或本世界保存的模板定义");
|
|
1221
|
+
}
|
|
1222
|
+
const previous = world.topology.templates;
|
|
1223
|
+
world.topology = { ...world.topology, templates: spatialTemplates({ ...(previous ?? { version: 1 }), [kind]: selection }) };
|
|
1224
|
+
}
|
|
1225
|
+
else if (type === "spatial_city_create") {
|
|
1226
|
+
exactKeys(op, ["type", "city"], "spatial_city_create");
|
|
1227
|
+
const world = model.spatial_world;
|
|
1228
|
+
if (world?.topology.generation === undefined)
|
|
1229
|
+
fail("创建程序化城市必须先保存有效 generation");
|
|
1230
|
+
const current = world.topology.procedural ?? emptyProceduralWorld(world.topology.generation.seed);
|
|
1231
|
+
const procedural = proceduralWorld({ ...current, cities: [...current.cities, op.city] }, world.topology.width, world.topology.height);
|
|
1232
|
+
world.topology = { ...world.topology, procedural };
|
|
1233
|
+
}
|
|
1234
|
+
else if (type === "spatial_set_controlled_actor") {
|
|
1235
|
+
exactKeys(op, ["type", "actorId"], "spatial_set_controlled_actor");
|
|
1236
|
+
const world = model.spatial_world;
|
|
1237
|
+
if (world?.topology.generation === undefined)
|
|
1238
|
+
fail("选择 controlled actor 必须先保存有效 generation");
|
|
1239
|
+
const actorId = controlledActorId(op.actorId, "spatial_set_controlled_actor.actorId");
|
|
1240
|
+
if (actorId === undefined)
|
|
1241
|
+
fail("spatial_set_controlled_actor.actorId 必须显式为 entity id 或 null");
|
|
1242
|
+
if (actorId !== null) {
|
|
1243
|
+
const actor = model.logical_graph.entities.find((entity) => entity.id === actorId);
|
|
1244
|
+
if (actor === undefined)
|
|
1245
|
+
fail(`controlled actor 不存在:${actorId}`);
|
|
1246
|
+
if (actor.kind !== "character" && actor.kind !== "npc")
|
|
1247
|
+
fail(`controlled actor 必须是 character 或 npc:${actorId}`);
|
|
1248
|
+
}
|
|
1249
|
+
world.topology = { ...world.topology, controlledActorId: actorId };
|
|
1250
|
+
}
|
|
1251
|
+
else if (type === "spatial_create") {
|
|
1252
|
+
const world = model.spatial_world;
|
|
1253
|
+
if (world === undefined)
|
|
1254
|
+
fail("spatial_world 尚未初始化");
|
|
1255
|
+
const key = stringValue(op.key, "spatial key");
|
|
1256
|
+
const kind = spatialKind(op.kind, "spatial kind");
|
|
1257
|
+
const label = stringValue(op.label, "spatial label");
|
|
1258
|
+
if (!SPATIAL_KEY.test(key))
|
|
1259
|
+
fail("spatial key 必须是稳定 kebab-case");
|
|
1260
|
+
const id = spatialId(kind, key);
|
|
1261
|
+
if (world.objects.some((object) => object.id === id))
|
|
1262
|
+
fail(`spatial object 已存在:${id}`);
|
|
1263
|
+
const x = finiteCoordinate(op.x, "spatial x");
|
|
1264
|
+
const y = finiteCoordinate(op.y, "spatial y");
|
|
1265
|
+
if (x < 0 || x > world.topology.width || y < 0 || y > world.topology.height)
|
|
1266
|
+
fail("spatial object 坐标超出 topology");
|
|
1267
|
+
world.objects.push({ id, key, kind, label, x, y, references: [] });
|
|
1268
|
+
model.spatial_ids = world.objects.map((object) => object.id);
|
|
1269
|
+
}
|
|
1270
|
+
else if (type === "spatial_update") {
|
|
1271
|
+
const world = model.spatial_world;
|
|
1272
|
+
if (world === undefined)
|
|
1273
|
+
fail("spatial_world 尚未初始化");
|
|
1274
|
+
const id = stringValue(op.id, "spatial id");
|
|
1275
|
+
const object = world.objects.find((entry) => entry.id === id);
|
|
1276
|
+
if (object === undefined)
|
|
1277
|
+
fail(`不存在 spatial object:${id}`);
|
|
1278
|
+
if (op.label !== undefined)
|
|
1279
|
+
object.label = stringValue(op.label, "spatial label");
|
|
1280
|
+
if (op.x !== undefined)
|
|
1281
|
+
object.x = finiteCoordinate(op.x, "spatial x");
|
|
1282
|
+
if (op.y !== undefined)
|
|
1283
|
+
object.y = finiteCoordinate(op.y, "spatial y");
|
|
1284
|
+
if (object.x < 0 || object.x > world.topology.width || object.y < 0 || object.y > world.topology.height)
|
|
1285
|
+
fail("spatial object 坐标超出 topology");
|
|
1286
|
+
}
|
|
1287
|
+
else if (type === "spatial_remove") {
|
|
1288
|
+
const world = model.spatial_world;
|
|
1289
|
+
if (world === undefined)
|
|
1290
|
+
fail("spatial_world 尚未初始化");
|
|
1291
|
+
const id = stringValue(op.id, "spatial id");
|
|
1292
|
+
const object = world.objects.find((entry) => entry.id === id);
|
|
1293
|
+
if (object === undefined)
|
|
1294
|
+
fail(`不存在 spatial object:${id}`);
|
|
1295
|
+
if (object.references.length > 0 && op.confirmed !== true)
|
|
1296
|
+
fail("存在空间引用影响,必须显式确认删除");
|
|
1297
|
+
if (op.confirmed === true)
|
|
1298
|
+
removeSpatialReferences(model, id);
|
|
1299
|
+
world.objects = world.objects.filter((entry) => entry.id !== id);
|
|
1300
|
+
model.spatial_ids = world.objects.map((entry) => entry.id);
|
|
1301
|
+
}
|
|
1302
|
+
else
|
|
1303
|
+
fail(`不支持的 world operation:${type}`);
|
|
1304
|
+
if ((type.startsWith("spatial_") || type === "save_template_definition" || type === "apply_template_selection") && model.spatial_world !== undefined) {
|
|
1305
|
+
model.spatial_world.spatialRevision += 1;
|
|
1306
|
+
spatialMutated = true;
|
|
1307
|
+
}
|
|
1308
|
+
}
|
|
1309
|
+
if (spatialMutated)
|
|
1310
|
+
rebindSceneSpatialReferencesAfterSpatialMutation(model);
|
|
1311
|
+
model.logical_graph.history.push({ at: Date.now(), domain: "world", operations: operations.map(worldOperationHistorySummary) });
|
|
1312
|
+
}
|
|
1313
|
+
async #applyTrigger(model, input) {
|
|
1314
|
+
const operations = input.operations;
|
|
1315
|
+
if (!Array.isArray(operations))
|
|
1316
|
+
fail("operations 必须是 array");
|
|
1317
|
+
for (const value of operations) {
|
|
1318
|
+
const op = record(value, "trigger operation");
|
|
1319
|
+
const type = stringValue(op.type, "trigger operation.type");
|
|
1320
|
+
if (type === "replace_document") {
|
|
1321
|
+
const document = record(op.document, "trigger document");
|
|
1322
|
+
const validation = await this.#triggerRuntime.validate(document);
|
|
1323
|
+
if (!validation.ok)
|
|
1324
|
+
fail(`TMW3 trigger document 无效:${validation.issues.join(",")}`);
|
|
1325
|
+
model.trigger_semantics = clone(document);
|
|
1326
|
+
}
|
|
1327
|
+
else if (type === "upsert") {
|
|
1328
|
+
if (model.trigger_semantics.version === 3)
|
|
1329
|
+
fail("TMW3 必须使用 replace_document,不能降级为 legacy trigger 格式");
|
|
1330
|
+
const triggers = model.trigger_semantics.triggers ?? [];
|
|
1331
|
+
const trigger = record(op.trigger, "trigger");
|
|
1332
|
+
const spatialRefs = trigger.spatial_refs === undefined ? undefined : (Array.isArray(trigger.spatial_refs) ? trigger.spatial_refs.map((ref) => stringValue(ref, "trigger.spatial_refs[]")) : fail("trigger.spatial_refs 必须是 array"));
|
|
1333
|
+
const next = spatialRefs === undefined
|
|
1334
|
+
? { ...trigger, id: stringValue(trigger.id, "trigger.id") }
|
|
1335
|
+
: { ...trigger, id: stringValue(trigger.id, "trigger.id"), spatial_refs: spatialRefs };
|
|
1336
|
+
const index = triggers.findIndex((item) => item.id === next.id);
|
|
1337
|
+
if (index === -1)
|
|
1338
|
+
triggers.push(next);
|
|
1339
|
+
else
|
|
1340
|
+
triggers[index] = next;
|
|
1341
|
+
model.trigger_semantics = { triggers };
|
|
1342
|
+
}
|
|
1343
|
+
else if (type === "delete") {
|
|
1344
|
+
if (model.trigger_semantics.version === 3)
|
|
1345
|
+
fail("TMW3 必须使用 replace_document,不能降级为 legacy trigger 格式");
|
|
1346
|
+
const id = stringValue(op.id, "trigger.id");
|
|
1347
|
+
model.trigger_semantics = { triggers: (model.trigger_semantics.triggers ?? []).filter((item) => item.id !== id) };
|
|
1348
|
+
}
|
|
1349
|
+
else
|
|
1350
|
+
fail(`不支持的 trigger operation:${type}`);
|
|
1351
|
+
}
|
|
1352
|
+
model.logical_graph.history.push({ at: Date.now(), domain: "trigger", operations: clone(operations) });
|
|
1353
|
+
}
|
|
1354
|
+
async #worldSearch(input) {
|
|
1355
|
+
const query = stringValue(input.query, "query");
|
|
1356
|
+
const requestedMode = stringValue(input.mode, "mode");
|
|
1357
|
+
const mode = requestedMode === "full_text" ? "keyword" : requestedMode;
|
|
1358
|
+
if (mode !== "keyword" && mode !== "semantic" && mode !== "hybrid")
|
|
1359
|
+
fail("world_search mode 无效");
|
|
1360
|
+
const limit = typeof input.limit === "number" ? input.limit : 20;
|
|
1361
|
+
const scopeId = worldSearchScopeId(input.filters);
|
|
1362
|
+
if (query === "spatial:") {
|
|
1363
|
+
if (scopeId !== undefined)
|
|
1364
|
+
fail("spatial: 查询不支持 scope_id filter");
|
|
1365
|
+
const { model, readback } = await this.#readModel();
|
|
1366
|
+
return { mode: requestedMode, results: [], spatial_world: model.spatial_world === undefined ? null : { ...spatialWorldReadback(model.spatial_world), revision: readback.project_revision } };
|
|
1367
|
+
}
|
|
1368
|
+
let prepared;
|
|
1369
|
+
let embeddingPort;
|
|
1370
|
+
if (mode !== "keyword") {
|
|
1371
|
+
embeddingPort = this.capabilities.resolve_ladybug_embedding_port === undefined
|
|
1372
|
+
? this.capabilities.ladybug_embedding_port
|
|
1373
|
+
: await this.capabilities.resolve_ladybug_embedding_port();
|
|
1374
|
+
if (embeddingPort === undefined)
|
|
1375
|
+
return fail("semantic_recall_query_provider_required", "E_AUTH_REQUIRED");
|
|
1376
|
+
const { model, readback } = await this.#readModel();
|
|
1377
|
+
prepared = await this.#validateRecallVectors(model, readback, scopeId, embeddingPort);
|
|
1378
|
+
}
|
|
1379
|
+
const { runtime, readback } = await this.#ensureRecallRuntime(false, prepared);
|
|
1380
|
+
let hits;
|
|
1381
|
+
// 唯一远端调用只编码本次query,发生失败时不能降级为keyword结果。
|
|
1382
|
+
const queryEmbedding = prepared === undefined ? undefined : await embedLadybugQuery(embeddingPort, query);
|
|
1383
|
+
try {
|
|
1384
|
+
hits = await runtime.recall({ queryText: query, mode, topK: limit, ...(scopeId === undefined ? {} : { scopeId }),
|
|
1385
|
+
...(queryEmbedding === undefined ? {} : { queryEmbedding }) });
|
|
1386
|
+
}
|
|
1387
|
+
catch (error) {
|
|
1388
|
+
if (error instanceof AuthoringBridgeError)
|
|
1389
|
+
throw error;
|
|
1390
|
+
return fail("Ladybug recall 需要有效的真实索引与 model provider");
|
|
1391
|
+
}
|
|
1392
|
+
if (prepared !== undefined) {
|
|
1393
|
+
const current = await this.port.readSourceReadback();
|
|
1394
|
+
if (current.source_sha256 !== readback.source_sha256 || current.project_revision !== readback.project_revision)
|
|
1395
|
+
return fail("semantic_recall_prepared_source_mismatch");
|
|
1396
|
+
}
|
|
1397
|
+
return {
|
|
1398
|
+
mode: requestedMode,
|
|
1399
|
+
results: hits.map((hit) => ({
|
|
1400
|
+
id: hit.id,
|
|
1401
|
+
kind: hit.kind,
|
|
1402
|
+
props: hit.props,
|
|
1403
|
+
score: hit.score,
|
|
1404
|
+
channels: hit.channels,
|
|
1405
|
+
sources: hit.sources,
|
|
1406
|
+
})),
|
|
1407
|
+
};
|
|
1408
|
+
}
|
|
1409
|
+
async #worldSchema() {
|
|
1410
|
+
const { model, readback } = await this.#readModel();
|
|
1411
|
+
return {
|
|
1412
|
+
schema_version: 1,
|
|
1413
|
+
project_id: readback.project_id,
|
|
1414
|
+
project_revision: readback.project_revision,
|
|
1415
|
+
source_sha256: readback.source_sha256,
|
|
1416
|
+
entities: ["id", "kind", "props"],
|
|
1417
|
+
relations: ["src", "dst", "kind", "props"],
|
|
1418
|
+
entity_listing: { tool: "world_get", order: "id_asc", max_limit: WORLD_GET_MAX_LIMIT, cursor: "source-bound-v1" },
|
|
1419
|
+
entity_kinds: [...new Set(model.logical_graph.entities.map((entity) => entity.kind))].sort(),
|
|
1420
|
+
relation_kinds: [...new Set(model.logical_graph.relations.map((relation) => relation.kind))].sort(),
|
|
1421
|
+
runtime_definitions: { cg_defs: { fields: ["cg_id", "name", "asset_path", "anchor", "unlock_trigger_id"], anchor_kinds: ["node", "edge", "attr"], attr_path_supported: false } },
|
|
1422
|
+
operations: { entity_row: "upsert_entity_row", relation_row: "upsert_relation_row", cg: ["upsert_cg", "delete_cg"], resource_refs: ["bind_gallery_resource", "unbind_resource_ref"] },
|
|
1423
|
+
spatial_templates: {
|
|
1424
|
+
authority: "scenario/source.wes#spatial_world.topology.templates",
|
|
1425
|
+
library_tool: "spatial_template_library",
|
|
1426
|
+
optional_for_legacy_worlds: true,
|
|
1427
|
+
operations: ["save_template_definition", "apply_template_selection"],
|
|
1428
|
+
apply_requires_confirmed_impact: true,
|
|
1429
|
+
persists: "active preset identity and frozen parameters only; no geometry",
|
|
1430
|
+
library_authority: "scenario/source.wes#spatial_world.topology.templateLibrary",
|
|
1431
|
+
},
|
|
1432
|
+
metadata: { keys: Object.keys(model.metadata).sort() },
|
|
1433
|
+
scene: {
|
|
1434
|
+
authority: "scenario/source.wes#metadata.initial_scene",
|
|
1435
|
+
runtime_authority: "RuntimeSavPayloadV3.scene",
|
|
1436
|
+
read_tool: "world_get", apply_tool: "world_apply", schema_version: 1,
|
|
1437
|
+
operations: ["scene.begin", "scene.end", "scene.present.add", "scene.present.remove", "scene.present.replace", "scene.time.set", "scene.location.set", "scene.note.add", "scene.note.remove"],
|
|
1438
|
+
note_lifetimes: ["once", "scene_end"], manuscript_text: "verbatim",
|
|
1439
|
+
},
|
|
1440
|
+
};
|
|
1441
|
+
}
|
|
1442
|
+
async #spatialTemplateLibrary() {
|
|
1443
|
+
const { model, readback } = await this.#readModel();
|
|
1444
|
+
return {
|
|
1445
|
+
version: 1,
|
|
1446
|
+
presets: clone(effectiveSpatialTemplatePresets(model.spatial_world?.topology.templateLibrary)),
|
|
1447
|
+
project_revision: readback.project_revision,
|
|
1448
|
+
source_sha256: readback.source_sha256,
|
|
1449
|
+
};
|
|
1450
|
+
}
|
|
1451
|
+
async #worldGet(input) {
|
|
1452
|
+
const { model, readback } = await this.#readModel();
|
|
1453
|
+
if (Array.isArray(input.ids)) {
|
|
1454
|
+
const ids = input.ids;
|
|
1455
|
+
const relations = input.include_relations === true ? model.logical_graph.relations : [];
|
|
1456
|
+
return {
|
|
1457
|
+
entities: model.logical_graph.entities.filter((entry) => ids.includes(entry.id)),
|
|
1458
|
+
relations: relations.filter((entry) => ids.includes(entry.src) || ids.includes(entry.dst)),
|
|
1459
|
+
metadata: clone(model.metadata),
|
|
1460
|
+
runtime_definitions: clone(runtimeDefinitions(model)),
|
|
1461
|
+
};
|
|
1462
|
+
}
|
|
1463
|
+
const limit = input.limit;
|
|
1464
|
+
if (!Number.isSafeInteger(limit) || limit < 1 || limit > WORLD_GET_MAX_LIMIT) {
|
|
1465
|
+
fail(`world_get list limit 必须在 1..${WORLD_GET_MAX_LIMIT}`, "E_SCHEMA_INVALID");
|
|
1466
|
+
}
|
|
1467
|
+
const entities = [...model.logical_graph.entities].sort((left, right) => compareCodepoint(left.id, right.id));
|
|
1468
|
+
if (new Set(entities.map((entity) => entity.id)).size !== entities.length) {
|
|
1469
|
+
fail("world_get list 要求 entity id 唯一");
|
|
1470
|
+
}
|
|
1471
|
+
let start = 0;
|
|
1472
|
+
if (input.cursor !== undefined) {
|
|
1473
|
+
const cursor = decodeWorldGetCursor(input.cursor);
|
|
1474
|
+
if (cursor.project_id !== readback.project_id)
|
|
1475
|
+
fail("world_get cursor 不属于当前 project");
|
|
1476
|
+
if (cursor.source_sha256 !== readback.source_sha256 || cursor.project_revision !== readback.project_revision) {
|
|
1477
|
+
fail("world_get cursor 对应的 source revision 已变化", "E_STALE_PROJECT_REVISION");
|
|
1478
|
+
}
|
|
1479
|
+
const anchor = entities.findIndex((entity) => entity.id === cursor.after_id);
|
|
1480
|
+
if (anchor === -1)
|
|
1481
|
+
fail("world_get cursor anchor 不存在", "E_SCHEMA_INVALID");
|
|
1482
|
+
start = anchor + 1;
|
|
1483
|
+
}
|
|
1484
|
+
const page = entities.slice(start, start + limit);
|
|
1485
|
+
const pageIds = new Set(page.map((entity) => entity.id));
|
|
1486
|
+
const relations = input.include_relations === true
|
|
1487
|
+
? model.logical_graph.relations
|
|
1488
|
+
.filter((relation) => pageIds.has(relation.src) || pageIds.has(relation.dst))
|
|
1489
|
+
.sort(compareRelation)
|
|
1490
|
+
: [];
|
|
1491
|
+
if (relations.length > WORLD_GET_MAX_RELATIONS_PER_PAGE) {
|
|
1492
|
+
throw new AuthoringBridgeError("E_VALIDATION_FAILED", `world_get 当前页相接 relations 超过 ${WORLD_GET_MAX_RELATIONS_PER_PAGE} 条;请缩小 entity page 或关闭 include_relations`, { failure: "E_WORLD_GET_RELATION_PAGE_TOO_LARGE", max_relations_per_page: WORLD_GET_MAX_RELATIONS_PER_PAGE, actual_relation_count: relations.length });
|
|
1493
|
+
}
|
|
1494
|
+
const hasMore = start + page.length < entities.length;
|
|
1495
|
+
return {
|
|
1496
|
+
entities: page,
|
|
1497
|
+
relations,
|
|
1498
|
+
next_cursor: hasMore && page.length > 0 ? encodeWorldGetCursor(readback, page[page.length - 1].id) : null,
|
|
1499
|
+
total_count: entities.length,
|
|
1500
|
+
order: "id_asc",
|
|
1501
|
+
project_revision: readback.project_revision,
|
|
1502
|
+
source_sha256: readback.source_sha256,
|
|
1503
|
+
metadata: clone(model.metadata),
|
|
1504
|
+
runtime_definitions: clone(runtimeDefinitions(model)),
|
|
1505
|
+
};
|
|
1506
|
+
}
|
|
1507
|
+
async #worldTraverse(input) {
|
|
1508
|
+
const startIds = input.start_ids;
|
|
1509
|
+
const direction = input.direction;
|
|
1510
|
+
const depth = input.depth;
|
|
1511
|
+
const limit = typeof input.limit === "number" ? input.limit : 100;
|
|
1512
|
+
const scopeId = input.scope_id;
|
|
1513
|
+
if (scopeId !== undefined && (typeof scopeId !== "string" || !/^[^\0\r\n]{1,512}$/u.test(scopeId))) {
|
|
1514
|
+
fail("world_traverse.scope_id 无效");
|
|
1515
|
+
}
|
|
1516
|
+
const entityKinds = new Set(Array.isArray(input.entity_kinds) ? input.entity_kinds : []);
|
|
1517
|
+
const relationKinds = Array.isArray(input.relation_kinds) ? input.relation_kinds : [];
|
|
1518
|
+
if (entityKinds.size > 64 || relationKinds.length > 64)
|
|
1519
|
+
fail("world_traverse kind filter 最多 64 项");
|
|
1520
|
+
const { runtime, model } = await this.#ensureRecallRuntime(true);
|
|
1521
|
+
let traversal;
|
|
1522
|
+
try {
|
|
1523
|
+
traversal = await runtime.traverse({
|
|
1524
|
+
startIds,
|
|
1525
|
+
...(scopeId === undefined ? {} : { scopeId }),
|
|
1526
|
+
direction,
|
|
1527
|
+
maxHops: depth,
|
|
1528
|
+
relationKinds,
|
|
1529
|
+
semanticOnly: input.semantic_only === true,
|
|
1530
|
+
maxPaths: limit,
|
|
1531
|
+
nodeBudget: Math.min(1_000, Math.max(limit, startIds.length)),
|
|
1532
|
+
});
|
|
1533
|
+
}
|
|
1534
|
+
catch (error) {
|
|
1535
|
+
if (error instanceof AuthoringBridgeError)
|
|
1536
|
+
throw error;
|
|
1537
|
+
return fail("Ladybug traverse 请求无效或 readback 不一致");
|
|
1538
|
+
}
|
|
1539
|
+
const visited = new Set(traversal.visitedNodeIds);
|
|
1540
|
+
const edgeKeys = new Set(traversal.paths.flatMap((path) => path.edges)
|
|
1541
|
+
.map((edge) => JSON.stringify([edge.src, edge.dst, edge.kind])));
|
|
1542
|
+
const entities = model.logical_graph.entities
|
|
1543
|
+
.filter((entry) => visited.has(entry.id) && (entityKinds.size === 0 || entityKinds.has(entry.kind)))
|
|
1544
|
+
.slice(0, limit);
|
|
1545
|
+
const relations = model.logical_graph.relations
|
|
1546
|
+
.filter((entry) => edgeKeys.has(JSON.stringify([entry.src, entry.dst, entry.kind])))
|
|
1547
|
+
.slice(0, limit);
|
|
1548
|
+
const evidenceByIdentity = new Map();
|
|
1549
|
+
for (const edge of traversal.paths.flatMap((path) => path.edges)) {
|
|
1550
|
+
for (const source of edge.sources)
|
|
1551
|
+
evidenceByIdentity.set(JSON.stringify(source), source);
|
|
1552
|
+
}
|
|
1553
|
+
return {
|
|
1554
|
+
entities,
|
|
1555
|
+
relations,
|
|
1556
|
+
paths: traversal.paths,
|
|
1557
|
+
evidence: [...evidenceByIdentity.values()],
|
|
1558
|
+
visited_node_ids: traversal.visitedNodeIds,
|
|
1559
|
+
truncated: traversal.truncated,
|
|
1560
|
+
};
|
|
1561
|
+
}
|
|
1562
|
+
async #history(input) { const { model } = await this.#readModel(); const ids = new Set(input.target_ids); const limit = typeof input.limit === "number" ? input.limit : 50; return { entries: model.logical_graph.history.filter((entry) => JSON.stringify(entry).split("\"").some((value) => ids.has(value))).slice(-limit) }; }
|
|
1563
|
+
async #triggerRead(input) {
|
|
1564
|
+
const { model, readback } = await this.#readModel();
|
|
1565
|
+
const ids = Array.isArray(input.trigger_ids) ? new Set(input.trigger_ids) : undefined;
|
|
1566
|
+
const legacy = model.trigger_semantics.triggers;
|
|
1567
|
+
return {
|
|
1568
|
+
project_id: readback.project_id,
|
|
1569
|
+
project_revision: readback.project_revision,
|
|
1570
|
+
source_sha256: readback.source_sha256,
|
|
1571
|
+
document: clone(model.trigger_semantics),
|
|
1572
|
+
...(legacy === undefined ? {} : { triggers: legacy.filter((entry) => ids === undefined || ids.has(entry.id)) }),
|
|
1573
|
+
};
|
|
1574
|
+
}
|
|
1575
|
+
async #triggerValidate(input) {
|
|
1576
|
+
const { model } = await this.#readModel();
|
|
1577
|
+
const candidate = input.candidate_document === undefined ? model.trigger_semantics : record(input.candidate_document, "candidate_document");
|
|
1578
|
+
return this.#triggerRuntime.validate(candidate);
|
|
1579
|
+
}
|
|
1580
|
+
async #triggerSimulate(input) {
|
|
1581
|
+
const { model } = await this.#readModel();
|
|
1582
|
+
const event = record(input.event, "event");
|
|
1583
|
+
const supplied = record(input.state ?? {}, "state");
|
|
1584
|
+
const state = {
|
|
1585
|
+
entities: Object.fromEntries(model.logical_graph.entities.map((entity) => [entity.id, clone(entity.props)])),
|
|
1586
|
+
...supplied,
|
|
1587
|
+
scene: supplied.scene === undefined ? initialTmwSceneFromMetadata(model.metadata) : supplied.scene,
|
|
1588
|
+
// 试玩可以修改候选 state,但不能伪造存在性与地图 revision 的 source authority。
|
|
1589
|
+
scene_source_references: sceneSourceReferences(model),
|
|
1590
|
+
};
|
|
1591
|
+
return this.#triggerRuntime.execute({
|
|
1592
|
+
document: model.trigger_semantics,
|
|
1593
|
+
event: {
|
|
1594
|
+
type: stringValue(event.type, "event.type"),
|
|
1595
|
+
...(event.id === undefined ? {} : { id: stringValue(event.id, "event.id") }),
|
|
1596
|
+
payload: record(event.payload ?? {}, "event.payload"),
|
|
1597
|
+
},
|
|
1598
|
+
state: state,
|
|
1599
|
+
});
|
|
1600
|
+
}
|
|
1601
|
+
async #layoutRead(kind) { const { model } = await this.#readModel(); return clone(model.layout[kind]); }
|
|
1602
|
+
}
|