@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,240 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { AuthoringBridgeError } from "@world-engines/authoring-bridge";
|
|
3
|
+
import { ScenarioPartSourceV1, SpatialWorldSourceV1 } from "@world-engines/protocol-ts/v1/scenario_part_source";
|
|
4
|
+
import { computeCanonicalFileTreeDigest, decodeScenarioPartWesp, encodeWesp, sha256Hex, } from "@world-engines/project-format";
|
|
5
|
+
import { LocalProjectHost } from "./project-host.js";
|
|
6
|
+
const MAP_LAYOUT_PATH = "layout.section.json";
|
|
7
|
+
const MAP_SPATIAL_PATH = "spatial-world.section.json";
|
|
8
|
+
const DATABASE_PATH = "scenario-graph.json";
|
|
9
|
+
const text = new TextEncoder();
|
|
10
|
+
const decoder = new TextDecoder("utf-8", { fatal: true });
|
|
11
|
+
async function assertCapture(expected) {
|
|
12
|
+
if (!Number.isSafeInteger(expected.project_revision) || expected.project_revision < 0 || typeof expected.source_sha256 !== "string" || !/^[a-f0-9]{64}$/.test(expected.source_sha256)) {
|
|
13
|
+
fail("expected Git source capture 无效");
|
|
14
|
+
}
|
|
15
|
+
const digest = await computeCanonicalFileTreeDigest(Object.entries(expected.files).map(([path, bytesValue]) => ({ path, bytes: bytesValue })));
|
|
16
|
+
if (digest !== expected.digest)
|
|
17
|
+
fail("expected Git source capture digest 不匹配");
|
|
18
|
+
}
|
|
19
|
+
function fail(message) {
|
|
20
|
+
throw new AuthoringBridgeError("E_SCHEMA_INVALID", message);
|
|
21
|
+
}
|
|
22
|
+
function canonicalJson(value) {
|
|
23
|
+
if (value === null || typeof value === "boolean" || typeof value === "string")
|
|
24
|
+
return JSON.stringify(value);
|
|
25
|
+
if (typeof value === "number") {
|
|
26
|
+
if (!Number.isFinite(value))
|
|
27
|
+
fail("Git source JSON number 无效");
|
|
28
|
+
return JSON.stringify(Object.is(value, -0) ? 0 : value);
|
|
29
|
+
}
|
|
30
|
+
if (Array.isArray(value))
|
|
31
|
+
return `[${value.map(canonicalJson).join(",")}]`;
|
|
32
|
+
if (typeof value === "object") {
|
|
33
|
+
return `{${Object.entries(value)
|
|
34
|
+
.filter(([, child]) => child !== undefined)
|
|
35
|
+
.sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0)
|
|
36
|
+
.map(([key, child]) => `${JSON.stringify(key)}:${canonicalJson(child)}`)
|
|
37
|
+
.join(",")}}`;
|
|
38
|
+
}
|
|
39
|
+
fail("Git source JSON value 无效");
|
|
40
|
+
}
|
|
41
|
+
function bytes(value) { return text.encode(canonicalJson(value)); }
|
|
42
|
+
function json(value, field) {
|
|
43
|
+
try {
|
|
44
|
+
return JSON.parse(decoder.decode(value));
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
return fail(`${field} 必须是 UTF-8 JSON`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
function record(value, field) {
|
|
51
|
+
if (value === null || typeof value !== "object" || Array.isArray(value))
|
|
52
|
+
fail(`${field} 必须是 object`);
|
|
53
|
+
return value;
|
|
54
|
+
}
|
|
55
|
+
function array(value, field) {
|
|
56
|
+
if (!Array.isArray(value))
|
|
57
|
+
fail(`${field} 必须是 array`);
|
|
58
|
+
return value;
|
|
59
|
+
}
|
|
60
|
+
function requiredString(value, field) {
|
|
61
|
+
if (typeof value !== "string" || value.length === 0)
|
|
62
|
+
fail(`${field} 无效`);
|
|
63
|
+
return value;
|
|
64
|
+
}
|
|
65
|
+
function sameKeys(value, keys, field) {
|
|
66
|
+
const actual = Object.keys(value);
|
|
67
|
+
if (actual.length !== keys.length || actual.some((key) => !keys.includes(key)))
|
|
68
|
+
fail(`${field} 字段无效`);
|
|
69
|
+
}
|
|
70
|
+
function base64(bytesValue) { return Buffer.from(bytesValue).toString("base64"); }
|
|
71
|
+
function base64Bytes(value, field) {
|
|
72
|
+
if (typeof value !== "string")
|
|
73
|
+
fail(`${field} 必须是 base64`);
|
|
74
|
+
const result = new Uint8Array(Buffer.from(value, "base64"));
|
|
75
|
+
if (base64(result).replace(/=+$/, "") !== value.replace(/=+$/, ""))
|
|
76
|
+
fail(`${field} 不是 canonical base64`);
|
|
77
|
+
return result;
|
|
78
|
+
}
|
|
79
|
+
function normalizeEntityForGit(value) {
|
|
80
|
+
const entity = { ...record(value, "logical_graph.entities[]") };
|
|
81
|
+
const id = requiredString(entity.entity_id ?? entity.id, "logical_graph entity id");
|
|
82
|
+
delete entity.id;
|
|
83
|
+
entity.entity_id = id;
|
|
84
|
+
return entity;
|
|
85
|
+
}
|
|
86
|
+
async function normalizeRelationForGit(value) {
|
|
87
|
+
const relation = { ...record(value, "logical_graph.relations[]") };
|
|
88
|
+
const from = requiredString(relation.from_id ?? relation.src, "logical_graph relation from_id");
|
|
89
|
+
const to = requiredString(relation.to_id ?? relation.dst, "logical_graph relation to_id");
|
|
90
|
+
const kind = requiredString(relation.kind, "logical_graph relation kind");
|
|
91
|
+
const relationId = relation.relation_id ?? relation.id
|
|
92
|
+
?? `relation.local.${(await sha256Hex(bytes({ from_id: from, kind, to_id: to }))).slice(0, 32)}`;
|
|
93
|
+
requiredString(relationId, "logical_graph relation_id");
|
|
94
|
+
delete relation.id;
|
|
95
|
+
delete relation.src;
|
|
96
|
+
delete relation.dst;
|
|
97
|
+
relation.from_id = from;
|
|
98
|
+
relation.to_id = to;
|
|
99
|
+
relation.kind = kind;
|
|
100
|
+
if (relationId !== undefined)
|
|
101
|
+
relation.relation_id = relationId;
|
|
102
|
+
return relation;
|
|
103
|
+
}
|
|
104
|
+
function normalizeEntityForLocal(value) {
|
|
105
|
+
const entity = { ...record(value, "scenario-graph.entities[]") };
|
|
106
|
+
const id = requiredString(entity.entity_id, "scenario-graph entity_id");
|
|
107
|
+
delete entity.entity_id;
|
|
108
|
+
entity.id = id;
|
|
109
|
+
return entity;
|
|
110
|
+
}
|
|
111
|
+
function normalizeRelationForLocal(value) {
|
|
112
|
+
const relation = { ...record(value, "scenario-graph.relations[]") };
|
|
113
|
+
const from = requiredString(relation.from_id, "scenario-graph from_id");
|
|
114
|
+
const to = requiredString(relation.to_id, "scenario-graph to_id");
|
|
115
|
+
requiredString(relation.kind, "scenario-graph kind");
|
|
116
|
+
if (relation.relation_id !== undefined)
|
|
117
|
+
requiredString(relation.relation_id, "scenario-graph relation_id");
|
|
118
|
+
delete relation.from_id;
|
|
119
|
+
delete relation.to_id;
|
|
120
|
+
relation.src = from;
|
|
121
|
+
relation.dst = to;
|
|
122
|
+
return relation;
|
|
123
|
+
}
|
|
124
|
+
async function graphSnapshot(projectRevision, graph) {
|
|
125
|
+
const entities = array(graph.entities ?? [], "logical_graph.entities").map(normalizeEntityForGit);
|
|
126
|
+
const relations = await Promise.all(array(graph.relations ?? [], "logical_graph.relations").map(normalizeRelationForGit));
|
|
127
|
+
const state_sha256 = await sha256Hex(bytes({
|
|
128
|
+
entities: [...entities].sort((left, right) => String(left.entity_id).localeCompare(String(right.entity_id))),
|
|
129
|
+
relations: [...relations].sort((left, right) => String(left.relation_id ?? "").localeCompare(String(right.relation_id ?? ""))),
|
|
130
|
+
}));
|
|
131
|
+
return { revision: projectRevision, state_sha256, entities, relations };
|
|
132
|
+
}
|
|
133
|
+
function localLayoutToGitPayload(source) {
|
|
134
|
+
const layout = source.byteLength === 0 ? {} : record(json(source, "layout"), "layout");
|
|
135
|
+
const interaction = layout.interaction === undefined ? undefined : record(layout.interaction, "layout.interaction");
|
|
136
|
+
return bytes({
|
|
137
|
+
view_interaction_mobile: layout.view_interaction_mobile ?? interaction?.mobile ?? null,
|
|
138
|
+
view_interaction_pc: layout.view_interaction_pc ?? interaction?.pc ?? (interaction !== undefined && Object.keys(interaction).length > 0 ? interaction : null),
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
function gitPayloadToLocalLayout(previousSource, payload) {
|
|
142
|
+
const previous = previousSource.byteLength === 0 ? {} : record(json(previousSource, "layout"), "layout");
|
|
143
|
+
const target = record(json(payload, "layout Git payload"), "layout Git payload");
|
|
144
|
+
sameKeys(target, ["view_interaction_mobile", "view_interaction_pc"], "layout Git payload");
|
|
145
|
+
return bytes({ ...previous, view_interaction_mobile: target.view_interaction_mobile, view_interaction_pc: target.view_interaction_pc });
|
|
146
|
+
}
|
|
147
|
+
function parseMap(files) {
|
|
148
|
+
const names = Object.keys(files);
|
|
149
|
+
if ((names.length !== 1 && names.length !== 2) || !(MAP_LAYOUT_PATH in files) || names.some((name) => name !== MAP_LAYOUT_PATH && name !== MAP_SPATIAL_PATH))
|
|
150
|
+
fail("map Git tree 只能包含 layout 与可选 spatial-world");
|
|
151
|
+
const layoutEnvelope = record(json(files[MAP_LAYOUT_PATH], MAP_LAYOUT_PATH), MAP_LAYOUT_PATH);
|
|
152
|
+
sameKeys(layoutEnvelope, ["section_key", "media_type", "canonical_encoding", "payload_base64"], MAP_LAYOUT_PATH);
|
|
153
|
+
if (layoutEnvelope.section_key !== "layout" || typeof layoutEnvelope.media_type !== "string" || layoutEnvelope.media_type.length === 0 || typeof layoutEnvelope.canonical_encoding !== "string" || layoutEnvelope.canonical_encoding.length === 0)
|
|
154
|
+
fail("layout section descriptor 无效");
|
|
155
|
+
const layout = base64Bytes(layoutEnvelope.payload_base64, "layout payload_base64");
|
|
156
|
+
// Web Git 支持 opaque layout descriptor;Local WESP 没有 descriptor slot,因此只接受可以无损投影到 local layout 的 JSON section。
|
|
157
|
+
if (layoutEnvelope.media_type !== "application/json")
|
|
158
|
+
fail("Local WESP 不支持非 JSON layout Git section");
|
|
159
|
+
record(json(layout, "layout payload"), "layout payload");
|
|
160
|
+
if (!(MAP_SPATIAL_PATH in files))
|
|
161
|
+
return { layout };
|
|
162
|
+
const spatialEnvelope = record(json(files[MAP_SPATIAL_PATH], MAP_SPATIAL_PATH), MAP_SPATIAL_PATH);
|
|
163
|
+
sameKeys(spatialEnvelope, ["section_key", "media_type", "canonical_encoding", "payload_base64"], MAP_SPATIAL_PATH);
|
|
164
|
+
if (spatialEnvelope.section_key !== "spatial_world" || spatialEnvelope.media_type !== "application/x-protobuf" || spatialEnvelope.canonical_encoding !== "chat.v1.SpatialWorldSourceV1")
|
|
165
|
+
fail("spatial_world section descriptor 不支持");
|
|
166
|
+
const spatial = base64Bytes(spatialEnvelope.payload_base64, "spatial_world payload_base64");
|
|
167
|
+
try {
|
|
168
|
+
SpatialWorldSourceV1.decode(spatial);
|
|
169
|
+
}
|
|
170
|
+
catch {
|
|
171
|
+
fail("spatial_world protobuf 无效");
|
|
172
|
+
}
|
|
173
|
+
return { layout, spatial };
|
|
174
|
+
}
|
|
175
|
+
async function parseGraph(files) {
|
|
176
|
+
if (Object.keys(files).length !== 1 || !(DATABASE_PATH in files))
|
|
177
|
+
fail("database Git tree 必须只包含 scenario-graph.json");
|
|
178
|
+
const value = record(json(files[DATABASE_PATH], DATABASE_PATH), DATABASE_PATH);
|
|
179
|
+
sameKeys(value, ["entities", "relations", "revision", "state_sha256"], DATABASE_PATH);
|
|
180
|
+
if (!Number.isSafeInteger(value.revision) || value.revision < 0 || typeof value.state_sha256 !== "string" || !/^[a-f0-9]{64}$/.test(value.state_sha256))
|
|
181
|
+
fail("scenario-graph metadata 无效");
|
|
182
|
+
const localShape = { entities: array(value.entities, "scenario-graph.entities").map(normalizeEntityForLocal), relations: array(value.relations, "scenario-graph.relations").map(normalizeRelationForLocal) };
|
|
183
|
+
const snapshot = await graphSnapshot(value.revision, localShape);
|
|
184
|
+
if (snapshot.state_sha256 !== value.state_sha256)
|
|
185
|
+
fail("scenario-graph state_sha256 不匹配");
|
|
186
|
+
return snapshot;
|
|
187
|
+
}
|
|
188
|
+
async function capture(root, kind) {
|
|
189
|
+
const host = await LocalProjectHost.open(root);
|
|
190
|
+
try {
|
|
191
|
+
const readback = await host.readSourceReadback();
|
|
192
|
+
const message = ScenarioPartSourceV1.decode(await decodeScenarioPartWesp(readback.source));
|
|
193
|
+
if (message.schema_version !== 1)
|
|
194
|
+
fail("ScenarioPartSourceV1 schema_version 不支持");
|
|
195
|
+
if (kind === "map") {
|
|
196
|
+
const files = {
|
|
197
|
+
[MAP_LAYOUT_PATH]: bytes({ section_key: "layout", media_type: "application/json", canonical_encoding: "canonical-json-v1", payload_base64: base64(localLayoutToGitPayload(message.layout)) }),
|
|
198
|
+
...(message.spatial_world === undefined ? {} : { [MAP_SPATIAL_PATH]: bytes({ section_key: "spatial_world", media_type: "application/x-protobuf", canonical_encoding: "chat.v1.SpatialWorldSourceV1", payload_base64: base64(SpatialWorldSourceV1.encode(message.spatial_world).finish()) }) }),
|
|
199
|
+
};
|
|
200
|
+
return { readback, message, files };
|
|
201
|
+
}
|
|
202
|
+
const snapshot = await graphSnapshot(readback.project_revision, message.logical_graph.byteLength === 0 ? {} : record(json(message.logical_graph, "logical_graph"), "logical_graph"));
|
|
203
|
+
return { readback, message, files: { [DATABASE_PATH]: bytes(snapshot) } };
|
|
204
|
+
}
|
|
205
|
+
finally {
|
|
206
|
+
await host.close();
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
export async function captureScenarioGitSource(root, kind) {
|
|
210
|
+
const current = await capture(root, kind);
|
|
211
|
+
return Object.freeze({ files: current.files, digest: await computeCanonicalFileTreeDigest(Object.entries(current.files).map(([path, bytesValue]) => ({ path, bytes: bytesValue }))), project_revision: current.readback.project_revision, source_sha256: current.readback.source_sha256 });
|
|
212
|
+
}
|
|
213
|
+
export async function applyScenarioGitSource(root, kind, files, expected) {
|
|
214
|
+
await assertCapture(expected);
|
|
215
|
+
const current = await capture(root, kind);
|
|
216
|
+
const currentDigest = await computeCanonicalFileTreeDigest(Object.entries(current.files).map(([path, bytesValue]) => ({ path, bytes: bytesValue })));
|
|
217
|
+
if (current.readback.project_revision !== expected.project_revision || current.readback.source_sha256 !== expected.source_sha256 || currentDigest !== expected.digest) {
|
|
218
|
+
throw new AuthoringBridgeError("E_STALE_PROJECT_REVISION", "project git apply 的本地 source 已改变");
|
|
219
|
+
}
|
|
220
|
+
const next = ScenarioPartSourceV1.decode(await decodeScenarioPartWesp(current.readback.source));
|
|
221
|
+
if (kind === "map") {
|
|
222
|
+
const incoming = parseMap(files);
|
|
223
|
+
next.layout = gitPayloadToLocalLayout(next.layout, incoming.layout);
|
|
224
|
+
if (incoming.spatial !== undefined)
|
|
225
|
+
next.spatial_world = SpatialWorldSourceV1.decode(incoming.spatial);
|
|
226
|
+
}
|
|
227
|
+
else {
|
|
228
|
+
const incoming = await parseGraph(files);
|
|
229
|
+
const previousGraph = next.logical_graph.byteLength === 0 ? {} : record(json(next.logical_graph, "logical_graph"), "logical_graph");
|
|
230
|
+
next.logical_graph = bytes({ ...previousGraph, entities: incoming.entities.map(normalizeEntityForLocal), relations: incoming.relations.map(normalizeRelationForLocal) });
|
|
231
|
+
}
|
|
232
|
+
const source = await encodeWesp(ScenarioPartSourceV1.encode(next).finish());
|
|
233
|
+
const host = await LocalProjectHost.open(root);
|
|
234
|
+
try {
|
|
235
|
+
await host.writeScenario({ operation_id: `project_git_${kind}_${randomUUID()}`, expected_project_revision: expected.project_revision, expected_source_sha256: expected.source_sha256, source });
|
|
236
|
+
}
|
|
237
|
+
finally {
|
|
238
|
+
await host.close();
|
|
239
|
+
}
|
|
240
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export interface ViewGitCapture {
|
|
2
|
+
readonly files: Readonly<Record<string, Uint8Array>>;
|
|
3
|
+
/** 完整可版本化视图树(包括 lockfile)的 canonical SHA-256。 */
|
|
4
|
+
readonly digest: string;
|
|
5
|
+
readonly project_revision: number;
|
|
6
|
+
readonly source_sha256: string;
|
|
7
|
+
}
|
|
8
|
+
export declare function captureViewGitSource(root: string): Promise<ViewGitCapture>;
|
|
9
|
+
/**
|
|
10
|
+
* 将已从 Git checkout 读出的完整视图 source tree 通过 Download Replace 的 writer lease
|
|
11
|
+
* 与 preimage/recovery 协议写入。调用方必须先完成仓库 ACL;这里不读取任何运行时 bundle。
|
|
12
|
+
*/
|
|
13
|
+
export declare function applyViewGitSource(root: string, files: Readonly<Record<string, Uint8Array>>, expected: ViewGitCapture): Promise<void>;
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
import { lstat, readFile, readdir } from "node:fs/promises";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { AuthoringBridgeError } from "@world-engines/authoring-bridge";
|
|
5
|
+
import { computeCanonicalFileTreeDigest } from "@world-engines/project-format";
|
|
6
|
+
import { LocalProjectHost } from "./project-host.js";
|
|
7
|
+
import { assertSafeExistingProjectPath } from "./safe-project-path.js";
|
|
8
|
+
import { DownloadReplaceCoordinator, isProjectGitViewExcludedName } from "./transfer/download.js";
|
|
9
|
+
const WINDOWS_DEVICE_NAME = /^(?:con|prn|aux|nul|com[1-9]|lpt[1-9])$/iu;
|
|
10
|
+
function sha256(bytes) {
|
|
11
|
+
return createHash("sha256").update(bytes).digest("hex");
|
|
12
|
+
}
|
|
13
|
+
function failPath(message) {
|
|
14
|
+
throw new AuthoringBridgeError("E_PATH_OUTSIDE_PROJECT", message);
|
|
15
|
+
}
|
|
16
|
+
/** Git tree 只承载作者源;不得把 hook、嵌套仓或构建产物写入本地项目。 */
|
|
17
|
+
function assertGitViewPath(path) {
|
|
18
|
+
if (typeof path !== "string" || path.length === 0 || path.includes("\\") || path.startsWith("/") || /^[A-Za-z]:/.test(path)) {
|
|
19
|
+
return failPath("视图 Git 文件路径必须是安全的 POSIX 相对路径");
|
|
20
|
+
}
|
|
21
|
+
const parts = path.split("/");
|
|
22
|
+
if (parts.some((part) => {
|
|
23
|
+
const deviceStem = part.split(".", 1)[0].trimEnd();
|
|
24
|
+
return part === "" || part === "." || part === ".."
|
|
25
|
+
|| isProjectGitViewExcludedName(part)
|
|
26
|
+
|| /[<>:"|?*\u0000-\u001f]/u.test(part)
|
|
27
|
+
|| /[. ]$/u.test(part)
|
|
28
|
+
|| WINDOWS_DEVICE_NAME.test(deviceStem);
|
|
29
|
+
})) {
|
|
30
|
+
return failPath("视图 Git tree 不能包含控制目录、生成目录或路径逃逸");
|
|
31
|
+
}
|
|
32
|
+
return path;
|
|
33
|
+
}
|
|
34
|
+
async function listViewSourceFiles(root, directory, prefix = "") {
|
|
35
|
+
const canonical = await assertSafeExistingProjectPath(root, directory);
|
|
36
|
+
const entries = await readdir(canonical, { withFileTypes: true });
|
|
37
|
+
const output = [];
|
|
38
|
+
for (const entry of entries.sort((left, right) => left.name.localeCompare(right.name))) {
|
|
39
|
+
if (isProjectGitViewExcludedName(entry.name))
|
|
40
|
+
continue;
|
|
41
|
+
const absolute = join(canonical, entry.name);
|
|
42
|
+
const info = await lstat(absolute);
|
|
43
|
+
if (info.isSymbolicLink())
|
|
44
|
+
failPath(`拒绝符号链接、junction 或 reparse point:${absolute}`);
|
|
45
|
+
const path = assertGitViewPath(prefix === "" ? entry.name : `${prefix}/${entry.name}`);
|
|
46
|
+
if (info.isDirectory())
|
|
47
|
+
output.push(...await listViewSourceFiles(root, absolute, path));
|
|
48
|
+
else if (info.isFile())
|
|
49
|
+
output.push({ path, bytes: new Uint8Array(await readFile(await assertSafeExistingProjectPath(root, absolute))) });
|
|
50
|
+
else
|
|
51
|
+
failPath(`视图 Git tree 只允许普通文件和目录:${absolute}`);
|
|
52
|
+
}
|
|
53
|
+
return output;
|
|
54
|
+
}
|
|
55
|
+
async function capture(root) {
|
|
56
|
+
const host = await LocalProjectHost.open(root);
|
|
57
|
+
try {
|
|
58
|
+
const source = await host.readSourceReadback();
|
|
59
|
+
const viewRoot = await assertSafeExistingProjectPath(root, join(root, "view"));
|
|
60
|
+
const files = Object.fromEntries((await listViewSourceFiles(root, viewRoot)).map((file) => [file.path, file.bytes]));
|
|
61
|
+
return { files, project_revision: source.project_revision, source_sha256: source.source_sha256 };
|
|
62
|
+
}
|
|
63
|
+
finally {
|
|
64
|
+
await host.close();
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
async function digestFiles(files) {
|
|
68
|
+
return computeCanonicalFileTreeDigest(Object.entries(files)
|
|
69
|
+
.map(([path, bytes]) => ({ path: assertGitViewPath(path), bytes }))
|
|
70
|
+
.sort((left, right) => left.path.localeCompare(right.path)));
|
|
71
|
+
}
|
|
72
|
+
function validatedIncoming(files) {
|
|
73
|
+
const output = [];
|
|
74
|
+
const windowsPaths = new Set();
|
|
75
|
+
for (const [path, bytes] of Object.entries(files)) {
|
|
76
|
+
assertGitViewPath(path);
|
|
77
|
+
const windowsPath = path.toLowerCase();
|
|
78
|
+
if (windowsPaths.has(windowsPath))
|
|
79
|
+
throw new AuthoringBridgeError("E_PATH_OUTSIDE_PROJECT", `视图 Git tree 包含 Windows 路径冲突:${path}`);
|
|
80
|
+
windowsPaths.add(windowsPath);
|
|
81
|
+
if (!(bytes instanceof Uint8Array))
|
|
82
|
+
throw new AuthoringBridgeError("E_SCHEMA_INVALID", `视图 Git 文件不是字节数组:${path}`);
|
|
83
|
+
output.push({ path, bytes: new Uint8Array(bytes) });
|
|
84
|
+
}
|
|
85
|
+
return output.sort((left, right) => left.path.localeCompare(right.path));
|
|
86
|
+
}
|
|
87
|
+
function planFor(expected, incoming) {
|
|
88
|
+
const identity = {
|
|
89
|
+
expected_project_revision: expected.project_revision,
|
|
90
|
+
expected_source_sha256: expected.source_sha256,
|
|
91
|
+
expected_view_digest: expected.digest,
|
|
92
|
+
files: incoming.map((file) => [file.path, sha256(file.bytes)]),
|
|
93
|
+
};
|
|
94
|
+
const planDigest = sha256(new TextEncoder().encode(JSON.stringify(identity)));
|
|
95
|
+
return {
|
|
96
|
+
schema_version: 1,
|
|
97
|
+
operation_id: `project_git_view_${randomUUID()}`,
|
|
98
|
+
plan_digest: planDigest,
|
|
99
|
+
expected_project_revision: expected.project_revision,
|
|
100
|
+
selection: "view",
|
|
101
|
+
strict_source_cas: {
|
|
102
|
+
schema_version: 1,
|
|
103
|
+
expected_source_sha256: expected.source_sha256,
|
|
104
|
+
expected_view_source_tree_sha256: expected.digest,
|
|
105
|
+
},
|
|
106
|
+
view: {
|
|
107
|
+
install_dependencies: false,
|
|
108
|
+
files: incoming.map((file) => ({ path: file.path, content_base64: Buffer.from(file.bytes).toString("base64"), sha256: sha256(file.bytes) })),
|
|
109
|
+
},
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
export async function captureViewGitSource(root) {
|
|
113
|
+
const current = await capture(root);
|
|
114
|
+
return Object.freeze({
|
|
115
|
+
files: current.files,
|
|
116
|
+
digest: await digestFiles(current.files),
|
|
117
|
+
project_revision: current.project_revision,
|
|
118
|
+
source_sha256: current.source_sha256,
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* 将已从 Git checkout 读出的完整视图 source tree 通过 Download Replace 的 writer lease
|
|
123
|
+
* 与 preimage/recovery 协议写入。调用方必须先完成仓库 ACL;这里不读取任何运行时 bundle。
|
|
124
|
+
*/
|
|
125
|
+
export async function applyViewGitSource(root, files, expected) {
|
|
126
|
+
const incoming = validatedIncoming(files);
|
|
127
|
+
const current = await captureViewGitSource(root);
|
|
128
|
+
if (current.project_revision !== expected.project_revision || current.source_sha256 !== expected.source_sha256 || current.digest !== expected.digest) {
|
|
129
|
+
throw new AuthoringBridgeError("E_STALE_PROJECT_REVISION", "project git apply 的本地视图 source 已改变");
|
|
130
|
+
}
|
|
131
|
+
const plan = planFor(expected, incoming);
|
|
132
|
+
const coordinator = await DownloadReplaceCoordinator.open(root);
|
|
133
|
+
await coordinator.apply(plan, { operation_id: plan.operation_id, plan_digest: plan.plan_digest });
|
|
134
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import type { ProjectTransferTransport } from "./transfer/transport.js";
|
|
2
|
+
export declare const PROJECT_GIT_MANIFEST_PATH = ".worldengine/project-git/manifest.json";
|
|
3
|
+
export declare const PROJECT_GIT_REPOSITORY_KINDS: readonly ["map", "database", "view"];
|
|
4
|
+
export type ProjectGitRepositoryKind = typeof PROJECT_GIT_REPOSITORY_KINDS[number];
|
|
5
|
+
export interface ProjectGitRepositoryRefV1 {
|
|
6
|
+
readonly kind: ProjectGitRepositoryKind;
|
|
7
|
+
readonly remote_path: string;
|
|
8
|
+
readonly commit: string | null;
|
|
9
|
+
}
|
|
10
|
+
export interface ProjectGitManifestBodyV1 {
|
|
11
|
+
readonly schema_version: 1;
|
|
12
|
+
readonly scenario_id: string;
|
|
13
|
+
readonly metadata: Readonly<{
|
|
14
|
+
title: string;
|
|
15
|
+
description: string;
|
|
16
|
+
}>;
|
|
17
|
+
readonly repositories: Readonly<Record<ProjectGitRepositoryKind, ProjectGitRepositoryRefV1>>;
|
|
18
|
+
}
|
|
19
|
+
/** Edge `GET /project-manifest` 的真实 response envelope。 */
|
|
20
|
+
export interface ProjectGitManifestV1 {
|
|
21
|
+
readonly scenario_id: string;
|
|
22
|
+
readonly revision: number;
|
|
23
|
+
readonly manifest: ProjectGitManifestBodyV1;
|
|
24
|
+
readonly updated_by?: string | null;
|
|
25
|
+
readonly updated_at?: number | null;
|
|
26
|
+
}
|
|
27
|
+
export interface ProjectGitPermissionsV1 {
|
|
28
|
+
readonly map_read: boolean;
|
|
29
|
+
readonly map_write: boolean;
|
|
30
|
+
readonly database_read: boolean;
|
|
31
|
+
readonly database_write: boolean;
|
|
32
|
+
readonly view_read: boolean;
|
|
33
|
+
readonly view_write: boolean;
|
|
34
|
+
readonly metadata_write: boolean;
|
|
35
|
+
}
|
|
36
|
+
export interface ProjectGitTransportRequest {
|
|
37
|
+
readonly project_id: string;
|
|
38
|
+
readonly method: "GET" | "POST" | "PUT";
|
|
39
|
+
readonly path: string;
|
|
40
|
+
readonly content_type?: "application/x-ndjson; profile=\"chat-artifact-v5-submit-stream-v1\"" | "application/x-git-upload-pack-request" | "application/x-git-receive-pack-request";
|
|
41
|
+
readonly body?: unknown | Uint8Array;
|
|
42
|
+
}
|
|
43
|
+
export interface ProjectGitTransportResponse {
|
|
44
|
+
readonly status: number;
|
|
45
|
+
readonly body: unknown;
|
|
46
|
+
}
|
|
47
|
+
export interface ProjectGitTransport {
|
|
48
|
+
request(request: ProjectGitTransportRequest): Promise<ProjectGitTransportResponse>;
|
|
49
|
+
}
|
|
50
|
+
export interface ProjectGitCommandResult {
|
|
51
|
+
readonly status: "cloned" | "fetched" | "pushed";
|
|
52
|
+
readonly repository: ProjectGitRepositoryKind;
|
|
53
|
+
readonly local_path: string;
|
|
54
|
+
readonly head: string;
|
|
55
|
+
}
|
|
56
|
+
export interface ProjectGitSourceResult {
|
|
57
|
+
readonly status: "captured" | "apply_prepared" | "applied" | "source_inspected";
|
|
58
|
+
readonly repository: ProjectGitRepositoryKind;
|
|
59
|
+
readonly local_path: string;
|
|
60
|
+
readonly project_revision: number;
|
|
61
|
+
readonly source_digest: string;
|
|
62
|
+
}
|
|
63
|
+
interface ProjectIdentity {
|
|
64
|
+
readonly project_id: string;
|
|
65
|
+
readonly scenario_id: string;
|
|
66
|
+
}
|
|
67
|
+
export declare function validateProjectGitManifest(value: unknown, expected?: ProjectIdentity): ProjectGitManifestV1;
|
|
68
|
+
export declare function projectGitTransport(transport: ProjectTransferTransport): ProjectGitTransport;
|
|
69
|
+
export declare class ProjectGitClient {
|
|
70
|
+
readonly projectRoot: string;
|
|
71
|
+
readonly identity: ProjectIdentity;
|
|
72
|
+
private readonly transport;
|
|
73
|
+
private constructor();
|
|
74
|
+
static open(projectRoot: string, transport: ProjectGitTransport | undefined): Promise<ProjectGitClient>;
|
|
75
|
+
readManifest(): Promise<ProjectGitManifestV1>;
|
|
76
|
+
private readAccess;
|
|
77
|
+
private captureSource;
|
|
78
|
+
/** Native 在命令后只读核对同一实际作者源,避免复制 map/database/view 摘要算法。 */
|
|
79
|
+
inspect(kindValue: string): Promise<ProjectGitSourceResult>;
|
|
80
|
+
private withSourceLease;
|
|
81
|
+
private assertCleanRepository;
|
|
82
|
+
private saveSourcePreimage;
|
|
83
|
+
/** 导出真实作者源,不触碰 index;作者仍使用原始 Git add/commit/merge。 */
|
|
84
|
+
capture(kindValue: string): Promise<ProjectGitSourceResult>;
|
|
85
|
+
/** 在 fetch/merge 之前固定本地预影;后续本地编辑必须导致 apply 拒绝。 */
|
|
86
|
+
prepareApply(kindValue: string): Promise<ProjectGitSourceResult>;
|
|
87
|
+
apply(kindValue: string): Promise<ProjectGitSourceResult>;
|
|
88
|
+
private sourceResult;
|
|
89
|
+
pullManifest(): Promise<ProjectGitManifestV1>;
|
|
90
|
+
pushManifest(): Promise<ProjectGitManifestV1>;
|
|
91
|
+
private pullManifestFromValue;
|
|
92
|
+
clone(kindValue: string): Promise<ProjectGitCommandResult>;
|
|
93
|
+
fetch(kindValue: string): Promise<ProjectGitCommandResult>;
|
|
94
|
+
push(kindValue: string): Promise<ProjectGitCommandResult>;
|
|
95
|
+
private result;
|
|
96
|
+
}
|
|
97
|
+
export {};
|