@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,162 @@
|
|
|
1
|
+
import { ScenarioPartSourceV1 } from "@world-engines/protocol-ts/v1/scenario_part_source";
|
|
2
|
+
import { type NeutralUploadLogicalObjectRefV2 } from "@world-engines/project-format";
|
|
3
|
+
import { type FrozenReviewTargetBinding } from "../frozen-review-local-pipeline.js";
|
|
4
|
+
import type { ProjectTransferTransport } from "./transport.js";
|
|
5
|
+
export type UploadPartKind = "scenario" | "view";
|
|
6
|
+
export interface UploadTarget {
|
|
7
|
+
readonly client_target_key: string;
|
|
8
|
+
readonly scenario_id?: string;
|
|
9
|
+
readonly view_id?: string;
|
|
10
|
+
readonly creates_scenario: boolean;
|
|
11
|
+
readonly creates_view: boolean;
|
|
12
|
+
readonly scenario_title?: string;
|
|
13
|
+
readonly view_title?: string;
|
|
14
|
+
readonly copy_on_bind: boolean;
|
|
15
|
+
readonly scenario_head?: string;
|
|
16
|
+
readonly view_digest?: string;
|
|
17
|
+
readonly binding_revision?: number;
|
|
18
|
+
}
|
|
19
|
+
export interface UploadSnapshot {
|
|
20
|
+
readonly snapshot_id: string;
|
|
21
|
+
readonly snapshot_digest: string;
|
|
22
|
+
readonly updated_at_ms: number;
|
|
23
|
+
}
|
|
24
|
+
export interface UploadObject {
|
|
25
|
+
readonly part_kind: UploadPartKind;
|
|
26
|
+
readonly sha256: string;
|
|
27
|
+
readonly byte_length: number;
|
|
28
|
+
readonly media_type: string;
|
|
29
|
+
}
|
|
30
|
+
export interface RemoteUploadPlan extends UploadSnapshot {
|
|
31
|
+
readonly operation_id: string;
|
|
32
|
+
readonly plan_id: string;
|
|
33
|
+
readonly plan_digest: string;
|
|
34
|
+
readonly expires_at_ms: number;
|
|
35
|
+
readonly targets: readonly UploadTarget[];
|
|
36
|
+
readonly objects: readonly UploadObject[];
|
|
37
|
+
readonly logical_objects?: readonly NeutralUploadLogicalObjectRefV2[];
|
|
38
|
+
}
|
|
39
|
+
export interface RemoteUploadOperation {
|
|
40
|
+
readonly operation_id: string;
|
|
41
|
+
readonly plan_id: string;
|
|
42
|
+
readonly plan_digest: string;
|
|
43
|
+
readonly state: string;
|
|
44
|
+
readonly targets: readonly Readonly<Record<string, unknown>>[];
|
|
45
|
+
}
|
|
46
|
+
export interface BindFrozenReviewUploadTargetOptions {
|
|
47
|
+
/** 多 target operation 必须显式提供;单 target 可省略。 */
|
|
48
|
+
readonly client_target_key?: string;
|
|
49
|
+
}
|
|
50
|
+
export interface UploadPlanRequest {
|
|
51
|
+
readonly operation_id: string;
|
|
52
|
+
readonly expires_at_ms: number;
|
|
53
|
+
readonly targets: readonly UploadTarget[];
|
|
54
|
+
}
|
|
55
|
+
export interface CapturedUploadObject extends UploadObject {
|
|
56
|
+
readonly bytes: Uint8Array;
|
|
57
|
+
}
|
|
58
|
+
export interface CapturedUploadSnapshot {
|
|
59
|
+
readonly snapshot: UploadSnapshot;
|
|
60
|
+
readonly objects: readonly CapturedUploadObject[];
|
|
61
|
+
}
|
|
62
|
+
export interface UploadPlanWireTarget {
|
|
63
|
+
readonly clientTargetKey: string;
|
|
64
|
+
readonly scenarioMode: "existing" | "create";
|
|
65
|
+
readonly scenarioId?: string;
|
|
66
|
+
readonly scenarioTitle?: string;
|
|
67
|
+
readonly viewMode: "existing" | "create";
|
|
68
|
+
readonly viewId?: string;
|
|
69
|
+
readonly viewTitle?: string;
|
|
70
|
+
readonly createsScenario: boolean;
|
|
71
|
+
readonly createsView: boolean;
|
|
72
|
+
readonly copyOnBind: boolean;
|
|
73
|
+
readonly scenarioHead?: string;
|
|
74
|
+
readonly viewDigest?: string;
|
|
75
|
+
readonly bindingRevision?: number;
|
|
76
|
+
}
|
|
77
|
+
/** CLI/GUI 与 Edge UploadPlanBody 共用的唯一 client target builder。 */
|
|
78
|
+
export declare function buildUploadPlanTarget(target: UploadTarget): UploadPlanWireTarget;
|
|
79
|
+
/** 生成未压缩 SVP v1;flags=0 是 SVP codec 支持的合法形式,避免 CLI 依赖 WebAssembly brotli。 */
|
|
80
|
+
/** Desktop private sidecar 与 CLI 共用的真实冻结快照;只返回当前项目 authority bytes。 */
|
|
81
|
+
export declare function captureUploadSnapshot(root: string): Promise<CapturedUploadSnapshot>;
|
|
82
|
+
/** 与 Edge wesp::scenario_sections 共用 golden 语义的 Node 映射。 */
|
|
83
|
+
export declare function mapScenarioPartToNeutralSections(source: ScenarioPartSourceV1, view: Uint8Array, localWesp: Uint8Array): readonly [{
|
|
84
|
+
readonly section_key: string;
|
|
85
|
+
readonly media_type: "application/json";
|
|
86
|
+
readonly canonical_encoding: "canonical-json-v1";
|
|
87
|
+
readonly payload: Uint8Array;
|
|
88
|
+
}, {
|
|
89
|
+
readonly section_key: string;
|
|
90
|
+
readonly media_type: "application/json";
|
|
91
|
+
readonly canonical_encoding: "canonical-json-v1";
|
|
92
|
+
readonly payload: Uint8Array;
|
|
93
|
+
}, {
|
|
94
|
+
readonly section_key: string;
|
|
95
|
+
readonly media_type: "application/json";
|
|
96
|
+
readonly canonical_encoding: "canonical-json-v1";
|
|
97
|
+
readonly payload: Uint8Array;
|
|
98
|
+
}, {
|
|
99
|
+
readonly section_key: string;
|
|
100
|
+
readonly media_type: "application/json";
|
|
101
|
+
readonly canonical_encoding: "canonical-json-v1";
|
|
102
|
+
readonly payload: Uint8Array;
|
|
103
|
+
}, {
|
|
104
|
+
readonly section_key: "view_project";
|
|
105
|
+
readonly media_type: "application/vnd.worldengine.svp";
|
|
106
|
+
readonly canonical_encoding: "svp-v1";
|
|
107
|
+
readonly payload: Uint8Array;
|
|
108
|
+
}, {
|
|
109
|
+
readonly section_key: string;
|
|
110
|
+
readonly media_type: "application/json";
|
|
111
|
+
readonly canonical_encoding: "canonical-json-v1";
|
|
112
|
+
readonly payload: Uint8Array;
|
|
113
|
+
}, {
|
|
114
|
+
readonly section_key: string;
|
|
115
|
+
readonly media_type: "application/json";
|
|
116
|
+
readonly canonical_encoding: "canonical-json-v1";
|
|
117
|
+
readonly payload: Uint8Array;
|
|
118
|
+
}, {
|
|
119
|
+
readonly section_key: string;
|
|
120
|
+
readonly media_type: "application/json";
|
|
121
|
+
readonly canonical_encoding: "canonical-json-v1";
|
|
122
|
+
readonly payload: Uint8Array;
|
|
123
|
+
}, {
|
|
124
|
+
readonly section_key: string;
|
|
125
|
+
readonly media_type: "application/json";
|
|
126
|
+
readonly canonical_encoding: "canonical-json-v1";
|
|
127
|
+
readonly payload: Uint8Array;
|
|
128
|
+
}, {
|
|
129
|
+
readonly section_key: string;
|
|
130
|
+
readonly media_type: "application/json";
|
|
131
|
+
readonly canonical_encoding: "canonical-json-v1";
|
|
132
|
+
readonly payload: Uint8Array;
|
|
133
|
+
}, {
|
|
134
|
+
readonly section_key: string;
|
|
135
|
+
readonly media_type: "application/json";
|
|
136
|
+
readonly canonical_encoding: "canonical-json-v1";
|
|
137
|
+
readonly payload: Uint8Array;
|
|
138
|
+
}, {
|
|
139
|
+
readonly section_key: "local_project_wesp";
|
|
140
|
+
readonly media_type: "application/vnd.worldengine.scenario-part+wesp";
|
|
141
|
+
readonly canonical_encoding: "wesp-v1";
|
|
142
|
+
readonly payload: Uint8Array;
|
|
143
|
+
}, {
|
|
144
|
+
readonly section_key: "spatial_world";
|
|
145
|
+
readonly media_type: "application/x-protobuf";
|
|
146
|
+
readonly canonical_encoding: "chat.v1.SpatialWorldSourceV1";
|
|
147
|
+
readonly payload: any;
|
|
148
|
+
}];
|
|
149
|
+
/** GUI/CLI 共用的远端 fanout coordinator;所有恢复始终使用同一 operation_id。 */
|
|
150
|
+
export declare class RemoteUploadCoordinator {
|
|
151
|
+
#private;
|
|
152
|
+
private constructor();
|
|
153
|
+
static open(root: string, projectId: string, transport: ProjectTransferTransport | undefined): Promise<RemoteUploadCoordinator>;
|
|
154
|
+
plan(request: UploadPlanRequest): Promise<RemoteUploadPlan>;
|
|
155
|
+
confirm(plan: RemoteUploadPlan, planDigest: string): Promise<RemoteUploadOperation>;
|
|
156
|
+
operation(operationId: string, reconcile?: boolean): Promise<RemoteUploadOperation>;
|
|
157
|
+
/** 仅消费 terminal authoritative upload witness;不从本地 WESP 或 plan 推导 Neutral identity。 */
|
|
158
|
+
bindFrozenReviewTarget(operationId: string, options?: BindFrozenReviewUploadTargetOptions): Promise<FrozenReviewTargetBinding>;
|
|
159
|
+
/** retry 只能复用既有 operation identity,服务端负责从该 operation 的冻结计划恢复。 */
|
|
160
|
+
retry(operationId: string): Promise<RemoteUploadOperation>;
|
|
161
|
+
undo(operationId: string): Promise<RemoteUploadOperation>;
|
|
162
|
+
}
|
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { lstat, readFile, readdir } from "node:fs/promises";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { ScenarioPartSourceV1, SpatialWorldSourceV1 } from "@world-engines/protocol-ts/v1/scenario_part_source";
|
|
5
|
+
import { ScenarioSourceManifest as ScenarioSourceManifestCodec } from "@world-engines/protocol-ts/v1/neutral_scenario_source";
|
|
6
|
+
import { canonicalScenarioSourceSectionBytes, createNeutralScenarioSource } from "@world-engines/scenario-author-source";
|
|
7
|
+
import { AuthoringBridgeError } from "@world-engines/authoring-bridge";
|
|
8
|
+
import { assertNeutralUploadPlanLimits, decodeScenarioPartWesp, encodeNeutralUploadLogicalObject } from "@world-engines/project-format";
|
|
9
|
+
import { assertSafeExistingProjectPath } from "../safe-project-path.js";
|
|
10
|
+
import { writeFrozenReviewTargetBinding } from "../frozen-review-local-pipeline.js";
|
|
11
|
+
function digest(bytes) { return createHash("sha256").update(bytes).digest("hex"); }
|
|
12
|
+
function createSha256() { const hash = createHash("sha256"); return { update(bytes) { hash.update(bytes); return this; }, digestHex() { return hash.digest("hex"); } }; }
|
|
13
|
+
function jsonDigest(value) { return digest(new TextEncoder().encode(JSON.stringify(value))); }
|
|
14
|
+
function asObject(value, message) { if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
15
|
+
throw new AuthoringBridgeError("E_SCHEMA_INVALID", message); return value; }
|
|
16
|
+
function asString(value, message) { if (typeof value !== "string" || value === "")
|
|
17
|
+
throw new AuthoringBridgeError("E_SCHEMA_INVALID", message); return value; }
|
|
18
|
+
function asNumber(value, message) { if (typeof value !== "number" || !Number.isSafeInteger(value))
|
|
19
|
+
throw new AuthoringBridgeError("E_SCHEMA_INVALID", message); return value; }
|
|
20
|
+
function remoteError(status) { if (status === 401 || status === 403)
|
|
21
|
+
throw new AuthoringBridgeError("E_AUTH_REQUIRED", "远端 Upload 请求未获授权"); throw new AuthoringBridgeError("E_VALIDATION_FAILED", `远端 Upload 请求失败:HTTP ${status}`); }
|
|
22
|
+
function assertTarget(target) {
|
|
23
|
+
buildUploadPlanTarget(target);
|
|
24
|
+
for (const hash of [target.scenario_head, target.view_digest])
|
|
25
|
+
if (hash !== undefined && !/^[a-f0-9]{64}$/.test(hash))
|
|
26
|
+
throw new AuthoringBridgeError("E_SCHEMA_INVALID", "upload target digest 无效");
|
|
27
|
+
}
|
|
28
|
+
/** CLI/GUI 与 Edge UploadPlanBody 共用的唯一 client target builder。 */
|
|
29
|
+
export function buildUploadPlanTarget(target) {
|
|
30
|
+
const scenarioMode = target.creates_scenario ? "create" : "existing";
|
|
31
|
+
const viewMode = target.creates_view ? "create" : "existing";
|
|
32
|
+
if (!target.client_target_key || (scenarioMode === "create" ? (target.scenario_id !== undefined || !target.scenario_title?.trim()) : !target.scenario_id)
|
|
33
|
+
|| (viewMode === "create" ? (target.view_id !== undefined || !target.view_title?.trim()) : !target.view_id)
|
|
34
|
+
|| (target.copy_on_bind && (viewMode !== "create" || !target.view_title?.trim())))
|
|
35
|
+
throw new AuthoringBridgeError("E_SCHEMA_INVALID", "upload target 与 Edge mode/title contract 不匹配");
|
|
36
|
+
return { clientTargetKey: target.client_target_key, scenarioMode, ...(target.scenario_id === undefined ? {} : { scenarioId: target.scenario_id }), ...(target.scenario_title === undefined ? {} : { scenarioTitle: target.scenario_title }), viewMode, ...(target.view_id === undefined ? {} : { viewId: target.view_id }), ...(target.view_title === undefined ? {} : { viewTitle: target.view_title }), createsScenario: target.creates_scenario, createsView: target.creates_view, copyOnBind: target.copy_on_bind, ...(target.scenario_head === undefined ? {} : { scenarioHead: target.scenario_head }), ...(target.view_digest === undefined ? {} : { viewDigest: target.view_digest }), ...(target.binding_revision === undefined ? {} : { bindingRevision: target.binding_revision }) };
|
|
37
|
+
}
|
|
38
|
+
async function files(root, relative = "") {
|
|
39
|
+
const directory = join(root, "view", ...relative.split("/").filter(Boolean));
|
|
40
|
+
const entries = await readdir(await assertSafeExistingProjectPath(root, directory), { withFileTypes: true });
|
|
41
|
+
const result = [];
|
|
42
|
+
for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
|
|
43
|
+
if (["node_modules", "dist", ".vite"].includes(entry.name))
|
|
44
|
+
continue;
|
|
45
|
+
const next = relative === "" ? entry.name : `${relative}/${entry.name}`;
|
|
46
|
+
const absolute = join(root, "view", ...next.split("/"));
|
|
47
|
+
const info = await lstat(absolute);
|
|
48
|
+
if (info.isSymbolicLink())
|
|
49
|
+
throw new AuthoringBridgeError("E_PATH_OUTSIDE_PROJECT", `拒绝 View 符号链接:${absolute}`);
|
|
50
|
+
if (info.isDirectory())
|
|
51
|
+
result.push(...await files(root, next));
|
|
52
|
+
else if (info.isFile())
|
|
53
|
+
result.push({ path: next, content_base64: Buffer.from(await readFile(await assertSafeExistingProjectPath(root, absolute))).toString("base64") });
|
|
54
|
+
}
|
|
55
|
+
return result;
|
|
56
|
+
}
|
|
57
|
+
/** 生成未压缩 SVP v1;flags=0 是 SVP codec 支持的合法形式,避免 CLI 依赖 WebAssembly brotli。 */
|
|
58
|
+
/** Desktop private sidecar 与 CLI 共用的真实冻结快照;只返回当前项目 authority bytes。 */
|
|
59
|
+
export async function captureUploadSnapshot(root) {
|
|
60
|
+
const manifest = asObject(JSON.parse(await readFile(join(root, "worldengine.project.json"), "utf8")), "manifest 无效");
|
|
61
|
+
const snapshot = asObject(manifest.snapshot, "manifest snapshot 无效");
|
|
62
|
+
const scenario = new Uint8Array(await readFile(await assertSafeExistingProjectPath(root, join(root, "scenario", "source.wes"))));
|
|
63
|
+
await decodeScenarioPartWesp(scenario);
|
|
64
|
+
const payload = new TextEncoder().encode(JSON.stringify({ files: await files(root) }));
|
|
65
|
+
const view = new Uint8Array(17 + payload.length);
|
|
66
|
+
view.set([0x53, 0x56, 0x50, 0x01, 0x00]);
|
|
67
|
+
const header = new DataView(view.buffer);
|
|
68
|
+
header.setBigUint64(5, BigInt(asNumber(manifest.updated_at_ms, "manifest updated_at_ms 无效")), true);
|
|
69
|
+
header.setUint32(13, payload.length, true);
|
|
70
|
+
view.set(payload, 17);
|
|
71
|
+
const objects = [
|
|
72
|
+
{ part_kind: "scenario", sha256: digest(scenario), byte_length: scenario.length, media_type: "application/vnd.worldengine.wesp", bytes: scenario },
|
|
73
|
+
{ part_kind: "view", sha256: digest(view), byte_length: view.length, media_type: "application/vnd.worldengine.svp", bytes: view },
|
|
74
|
+
];
|
|
75
|
+
return { snapshot: { snapshot_id: asString(snapshot.snapshot_id, "snapshot_id 无效"), snapshot_digest: asString(snapshot.canonical_digest, "snapshot digest 无效"), updated_at_ms: asNumber(manifest.updated_at_ms, "manifest updated_at_ms 无效") }, objects };
|
|
76
|
+
}
|
|
77
|
+
async function* one(bytes) { yield bytes; }
|
|
78
|
+
function json(bytes, fallback, field) {
|
|
79
|
+
if (bytes.byteLength === 0)
|
|
80
|
+
return fallback;
|
|
81
|
+
try {
|
|
82
|
+
return JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
throw new AuthoringBridgeError("E_SCHEMA_INVALID", `${field} 不是有效 JSON`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
function record(value, field) {
|
|
89
|
+
return asObject(value, `${field} 必须是 object`);
|
|
90
|
+
}
|
|
91
|
+
function canonicalJson(value) {
|
|
92
|
+
const encode = (input) => {
|
|
93
|
+
if (input === null)
|
|
94
|
+
return "null";
|
|
95
|
+
if (typeof input === "boolean" || typeof input === "string")
|
|
96
|
+
return JSON.stringify(input);
|
|
97
|
+
if (typeof input === "number") {
|
|
98
|
+
if (!Number.isFinite(input))
|
|
99
|
+
throw new AuthoringBridgeError("E_SCHEMA_INVALID", "canonical JSON number 无效");
|
|
100
|
+
return JSON.stringify(Object.is(input, -0) ? 0 : input);
|
|
101
|
+
}
|
|
102
|
+
if (Array.isArray(input))
|
|
103
|
+
return `[${input.map(encode).join(",")}]`;
|
|
104
|
+
if (typeof input === "object")
|
|
105
|
+
return `{${Object.entries(input).sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0).map(([key, child]) => `${JSON.stringify(key)}:${encode(child)}`).join(",")}}`;
|
|
106
|
+
throw new AuthoringBridgeError("E_SCHEMA_INVALID", "canonical JSON value 无效");
|
|
107
|
+
};
|
|
108
|
+
return new TextEncoder().encode(encode(value));
|
|
109
|
+
}
|
|
110
|
+
function jsonSection(section_key, payload) {
|
|
111
|
+
return { section_key, media_type: "application/json", canonical_encoding: "canonical-json-v1", payload: canonicalJson(payload) };
|
|
112
|
+
}
|
|
113
|
+
/** 与 Edge wesp::scenario_sections 共用 golden 语义的 Node 映射。 */
|
|
114
|
+
export function mapScenarioPartToNeutralSections(source, view, localWesp) {
|
|
115
|
+
const wespSha = digest(localWesp);
|
|
116
|
+
const metadataOriginal = json(source.metadata, {}, "metadata");
|
|
117
|
+
const metadataRoot = record(metadataOriginal, "metadata");
|
|
118
|
+
const alreadyNeutral = ["metadata", "protagonist", "system_prompts", "source_metadata"].some((key) => key in metadataRoot);
|
|
119
|
+
const sourceMetadata = metadataRoot.source_metadata === undefined ? {} : { ...record(metadataRoot.source_metadata, "metadata.source_metadata") };
|
|
120
|
+
sourceMetadata.local_project_wesp_byte_length = localWesp.byteLength;
|
|
121
|
+
sourceMetadata.local_project_wesp_sha256 = wespSha;
|
|
122
|
+
const metadata = {
|
|
123
|
+
metadata: alreadyNeutral ? (metadataRoot.metadata ?? null) : Object.keys(metadataRoot).length === 0 ? null : metadataOriginal,
|
|
124
|
+
protagonist: metadataRoot.protagonist ?? null,
|
|
125
|
+
source_metadata: sourceMetadata,
|
|
126
|
+
system_prompts: metadataRoot.system_prompts ?? [],
|
|
127
|
+
};
|
|
128
|
+
const graphRoot = record(json(source.logical_graph, { entities: [], history: [], relations: [] }, "logical_graph"), "logical_graph");
|
|
129
|
+
if (!Array.isArray(graphRoot.entities) || !Array.isArray(graphRoot.relations) || !Array.isArray(graphRoot.history ?? []))
|
|
130
|
+
throw new AuthoringBridgeError("E_SCHEMA_INVALID", "logical_graph 无效");
|
|
131
|
+
const entities = graphRoot.entities.map((value) => { const entity = { ...record(value, "logical_graph entity") }; const id = entity.entity_id ?? entity.id; if (typeof id !== "string" || id.length === 0)
|
|
132
|
+
throw new AuthoringBridgeError("E_SCHEMA_INVALID", "logical_graph entity id 无效"); delete entity.id; entity.entity_id = id; return entity; });
|
|
133
|
+
const relationIds = new Set();
|
|
134
|
+
const relations = graphRoot.relations.map((value) => {
|
|
135
|
+
const relation = { ...record(value, "logical_graph relation") };
|
|
136
|
+
const from = relation.from_id ?? relation.src;
|
|
137
|
+
const to = relation.to_id ?? relation.dst;
|
|
138
|
+
const kind = relation.kind;
|
|
139
|
+
if (typeof from !== "string" || from.length === 0 || typeof to !== "string" || to.length === 0 || typeof kind !== "string" || kind.length === 0)
|
|
140
|
+
throw new AuthoringBridgeError("E_SCHEMA_INVALID", "logical_graph relation identity 无效");
|
|
141
|
+
const relationId = relation.relation_id ?? relation.id ?? `relation.local.${digest(canonicalJson({ from_id: from, kind, to_id: to })).slice(0, 32)}`;
|
|
142
|
+
if (typeof relationId !== "string" || relationId.length === 0 || relationIds.has(relationId))
|
|
143
|
+
throw new AuthoringBridgeError("E_SCHEMA_INVALID", "logical_graph relation_id 无效或重复");
|
|
144
|
+
relationIds.add(relationId);
|
|
145
|
+
delete relation.src;
|
|
146
|
+
delete relation.dst;
|
|
147
|
+
delete relation.id;
|
|
148
|
+
relation.from_id = from;
|
|
149
|
+
relation.to_id = to;
|
|
150
|
+
relation.relation_id = relationId;
|
|
151
|
+
return relation;
|
|
152
|
+
});
|
|
153
|
+
const triggerOriginal = json(source.trigger_semantics, { triggers: [] }, "trigger_semantics");
|
|
154
|
+
const triggerRoot = record(triggerOriginal, "trigger_semantics");
|
|
155
|
+
const triggerKeys = ["conditions", "edges", "effects", "events", "presentations"];
|
|
156
|
+
const trigger = triggerKeys.every((key) => Array.isArray(triggerRoot[key]))
|
|
157
|
+
? {
|
|
158
|
+
...Object.fromEntries(triggerKeys.map((key) => [key, triggerRoot[key]])),
|
|
159
|
+
...(triggerRoot.value_graph === undefined ? {} : { value_graph: triggerRoot.value_graph }),
|
|
160
|
+
}
|
|
161
|
+
: Object.keys(triggerRoot).length === 1 && Array.isArray(triggerRoot.triggers) && triggerRoot.triggers.length === 0
|
|
162
|
+
? { conditions: [], edges: [], effects: [], events: [], presentations: [] }
|
|
163
|
+
: (() => { throw new AuthoringBridgeError("E_SCHEMA_INVALID", "trigger_semantics 不受支持"); })();
|
|
164
|
+
const layoutOriginal = json(source.layout, { blueprint: {}, interaction: {} }, "layout");
|
|
165
|
+
const layoutRoot = record(layoutOriginal, "layout");
|
|
166
|
+
const interaction = layoutRoot.interaction === undefined ? undefined : record(layoutRoot.interaction, "layout.interaction");
|
|
167
|
+
const layout = {
|
|
168
|
+
view_interaction_mobile: layoutRoot.view_interaction_mobile ?? interaction?.mobile ?? null,
|
|
169
|
+
view_interaction_pc: layoutRoot.view_interaction_pc ?? interaction?.pc ?? (interaction !== undefined && Object.keys(interaction).length > 0 ? interaction : null),
|
|
170
|
+
};
|
|
171
|
+
const resourceOriginal = json(source.resource_refs, [], "resource_refs");
|
|
172
|
+
const resources = Array.isArray(resourceOriginal) ? resourceOriginal : record(resourceOriginal, "resource_refs").resources;
|
|
173
|
+
if (!Array.isArray(resources))
|
|
174
|
+
throw new AuthoringBridgeError("E_SCHEMA_INVALID", "resource_refs 无效");
|
|
175
|
+
const retrievalOriginal = json(source.prompt_retrieval, {}, "prompt_retrieval");
|
|
176
|
+
const retrievalRoot = record(retrievalOriginal, "prompt_retrieval");
|
|
177
|
+
const remoteRetrieval = typeof retrievalRoot.retrieval === "object" && retrievalRoot.retrieval !== null && !Array.isArray(retrievalRoot.retrieval) ? retrievalRoot.retrieval : undefined;
|
|
178
|
+
const runtime = typeof retrievalRoot.runtime_definitions === "object" && retrievalRoot.runtime_definitions !== null && !Array.isArray(retrievalRoot.runtime_definitions) ? retrievalRoot.runtime_definitions : undefined;
|
|
179
|
+
const lookup = (key) => remoteRetrieval?.[key] ?? retrievalRoot[key];
|
|
180
|
+
const retrievalConfig = lookup("retrieval_config") ?? (Object.keys(retrievalRoot).length === 0 || ["retrieval", "music_candidates", "runtime_definitions"].some((key) => key in retrievalRoot) ? null : retrievalOriginal);
|
|
181
|
+
const promptRetrieval = { music_candidates: retrievalRoot.music_candidates ?? [], retrieval: { attr_mutex_keys: lookup("attr_mutex_keys") ?? [], play_config: lookup("play_config") ?? null, retrieval_config: retrievalConfig, style_guide_selections: lookup("style_guide_selections") ?? [], world_time: lookup("world_time") ?? null }, runtime_definitions: { achievement_defs: runtime?.achievement_defs ?? [], cg_defs: runtime?.cg_defs ?? [], transaction_defs: runtime?.transaction_defs ?? [], trigger_variables_schema: runtime?.trigger_variables_schema ?? [], triggers: runtime?.triggers ?? [] } };
|
|
182
|
+
const protobuf = localWesp.subarray(45);
|
|
183
|
+
return [
|
|
184
|
+
jsonSection("metadata", metadata),
|
|
185
|
+
jsonSection("logical_graph", { entities, relations }),
|
|
186
|
+
jsonSection("trigger_semantics", trigger),
|
|
187
|
+
jsonSection("layout", layout),
|
|
188
|
+
{ section_key: "view_project", media_type: "application/vnd.worldengine.svp", canonical_encoding: "svp-v1", payload: view },
|
|
189
|
+
jsonSection("resource_refs", resources),
|
|
190
|
+
jsonSection("prompt_retrieval", promptRetrieval),
|
|
191
|
+
jsonSection("local_graph_history", graphRoot.history ?? []),
|
|
192
|
+
jsonSection("local_project_layout", layoutOriginal),
|
|
193
|
+
jsonSection("local_project_trigger_semantics", triggerOriginal),
|
|
194
|
+
jsonSection("local_project_provenance", { schema_version: 1, wesp_byte_length: localWesp.byteLength, wesp_payload_byte_length: protobuf.byteLength, wesp_payload_sha256: digest(protobuf), wesp_sha256: wespSha }),
|
|
195
|
+
{ section_key: "local_project_wesp", media_type: "application/vnd.worldengine.scenario-part+wesp", canonical_encoding: "wesp-v1", payload: localWesp },
|
|
196
|
+
{ section_key: "spatial_world", media_type: "application/x-protobuf", canonical_encoding: "chat.v1.SpatialWorldSourceV1", payload: source.spatial_world === undefined ? new Uint8Array() : SpatialWorldSourceV1.encode(source.spatial_world).finish() },
|
|
197
|
+
];
|
|
198
|
+
}
|
|
199
|
+
/** 生成 v2 Neutral logical objects;WESP 只在 local_project_wesp section payload 中逐字节保存一次。 */
|
|
200
|
+
async function captureNeutralUploadInputs(root) {
|
|
201
|
+
const captured = await captureUploadSnapshot(root);
|
|
202
|
+
const scenarioObject = captured.objects.find((object) => object.part_kind === "scenario");
|
|
203
|
+
const viewObject = captured.objects.find((object) => object.part_kind === "view");
|
|
204
|
+
const manifest = asObject(JSON.parse(await readFile(join(root, "worldengine.project.json"), "utf8")), "manifest 无效");
|
|
205
|
+
const scenarioManifest = asObject(manifest.scenario, "manifest scenario 无效");
|
|
206
|
+
const scenarioId = asString(scenarioManifest.local_scenario_id, "local scenario id 无效");
|
|
207
|
+
const protobuf = await decodeScenarioPartWesp(scenarioObject.bytes);
|
|
208
|
+
const sections = mapScenarioPartToNeutralSections(ScenarioPartSourceV1.decode(protobuf), viewObject.bytes, scenarioObject.bytes);
|
|
209
|
+
const neutral = await createNeutralScenarioSource(scenarioId, sections);
|
|
210
|
+
const inputs = [{
|
|
211
|
+
object_id: "manifest",
|
|
212
|
+
object_kind: "manifest",
|
|
213
|
+
bytes: one(ScenarioSourceManifestCodec.encode(neutral.manifest).finish()),
|
|
214
|
+
}];
|
|
215
|
+
for (const section of neutral.sections) {
|
|
216
|
+
if (section.section_key === "local_project_wesp" || section.section_key === "view_project")
|
|
217
|
+
continue;
|
|
218
|
+
inputs.push({
|
|
219
|
+
object_id: `section:${section.section_key}`,
|
|
220
|
+
object_kind: "section",
|
|
221
|
+
section_key: section.section_key,
|
|
222
|
+
media_type: section.media_type,
|
|
223
|
+
canonical_encoding: section.canonical_encoding,
|
|
224
|
+
bytes: one(canonicalScenarioSourceSectionBytes(section)),
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
const viewSection = neutral.sections.find((section) => section.section_key === "view_project");
|
|
228
|
+
inputs.push({ object_id: "view", object_kind: "view", section_key: viewSection.section_key, media_type: viewSection.media_type, canonical_encoding: viewSection.canonical_encoding, bytes: one(canonicalScenarioSourceSectionBytes(viewSection)) });
|
|
229
|
+
const localWespSection = neutral.sections.find((section) => section.section_key === "local_project_wesp");
|
|
230
|
+
inputs.push({
|
|
231
|
+
object_id: "local_project_wesp",
|
|
232
|
+
object_kind: "local_project_wesp",
|
|
233
|
+
section_key: "local_project_wesp",
|
|
234
|
+
media_type: "application/vnd.worldengine.scenario-part+wesp",
|
|
235
|
+
canonical_encoding: "wesp-v1",
|
|
236
|
+
bytes: one(canonicalScenarioSourceSectionBytes(localWespSection)),
|
|
237
|
+
});
|
|
238
|
+
return { snapshot: captured.snapshot, legacy_objects: captured.objects.map(({ bytes: _bytes, ...object }) => object), inputs };
|
|
239
|
+
}
|
|
240
|
+
function parsePlan(value, request, snapshot, objects) {
|
|
241
|
+
const body = asObject(value, "远端 upload plan 无效");
|
|
242
|
+
const plan_id = asString(body.planId, "远端 upload plan 缺少 planId");
|
|
243
|
+
const plan_digest = asString(body.planDigest, "远端 upload plan 缺少 planDigest");
|
|
244
|
+
const expires_at_ms = asNumber(body.expiresAtMs, "远端 upload plan 缺少 expiresAtMs");
|
|
245
|
+
if (!/^[a-f0-9]{64}$/.test(plan_digest) || expires_at_ms <= Date.now())
|
|
246
|
+
throw new AuthoringBridgeError("E_CONFIRMATION_REQUIRED", "远端 upload plan 已过期或 digest 无效");
|
|
247
|
+
return { operation_id: request.operation_id, plan_id, plan_digest, expires_at_ms, targets: request.targets, objects, ...snapshot };
|
|
248
|
+
}
|
|
249
|
+
function parseOperation(value) {
|
|
250
|
+
const body = asObject(value, "远端 upload operation 无效");
|
|
251
|
+
const targets = body.targets;
|
|
252
|
+
if (!Array.isArray(targets))
|
|
253
|
+
throw new AuthoringBridgeError("E_SCHEMA_INVALID", "远端 upload operation 缺少 targets");
|
|
254
|
+
return { operation_id: asString(body.operationId, "远端 upload operation 缺少 operationId"), plan_id: asString(body.planId, "远端 upload operation 缺少 planId"), plan_digest: asString(body.planDigest, "远端 upload operation 缺少 planDigest"), state: asString(body.state, "远端 upload operation 缺少 state"), targets: targets.map((target) => asObject(target, "远端 upload target 无效")) };
|
|
255
|
+
}
|
|
256
|
+
function sha256(value, message) {
|
|
257
|
+
if (typeof value !== "string" || !/^[a-f0-9]{64}$/u.test(value))
|
|
258
|
+
throw new AuthoringBridgeError("E_OPERATION_UNCERTAIN", message);
|
|
259
|
+
return value;
|
|
260
|
+
}
|
|
261
|
+
function terminalReviewTarget(operation, requestedKey) {
|
|
262
|
+
if (operation.state !== "succeeded")
|
|
263
|
+
throw new AuthoringBridgeError("E_OPERATION_UNCERTAIN", "upload 尚未整体成功,不能建立 review target binding");
|
|
264
|
+
let selected;
|
|
265
|
+
if (requestedKey === undefined) {
|
|
266
|
+
if (operation.targets.length !== 1)
|
|
267
|
+
throw new AuthoringBridgeError("E_SCHEMA_INVALID", "多 target upload 必须显式选择 client_target_key");
|
|
268
|
+
selected = operation.targets[0];
|
|
269
|
+
}
|
|
270
|
+
else {
|
|
271
|
+
const matches = operation.targets.filter((target) => target.clientTargetKey === requestedKey);
|
|
272
|
+
if (matches.length !== 1)
|
|
273
|
+
throw new AuthoringBridgeError("E_SCHEMA_INVALID", "review target client_target_key 不属于该 upload operation");
|
|
274
|
+
selected = matches[0];
|
|
275
|
+
}
|
|
276
|
+
if (selected.state !== "succeeded")
|
|
277
|
+
throw new AuthoringBridgeError("E_OPERATION_UNCERTAIN", "所选 upload target 尚未成功");
|
|
278
|
+
return Object.freeze({
|
|
279
|
+
schema_version: 1,
|
|
280
|
+
project_id: "",
|
|
281
|
+
scenario_id: asString(selected.scenarioId, "terminal upload target 缺少 scenarioId"),
|
|
282
|
+
source_digest: sha256(selected.scenarioSourceDigestReadback, "terminal upload target 缺少 canonical source digest readback"),
|
|
283
|
+
source_head_digest: sha256(selected.scenarioHeadReadback, "terminal upload target 缺少 scenario head readback"),
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
/** GUI/CLI 共用的远端 fanout coordinator;所有恢复始终使用同一 operation_id。 */
|
|
287
|
+
export class RemoteUploadCoordinator {
|
|
288
|
+
#root;
|
|
289
|
+
#projectId;
|
|
290
|
+
#transport;
|
|
291
|
+
constructor(root, projectId, transport) { this.#root = root; this.#projectId = projectId; this.#transport = transport; }
|
|
292
|
+
static async open(root, projectId, transport) { if (!projectId)
|
|
293
|
+
throw new AuthoringBridgeError("E_SCHEMA_INVALID", "upload 缺少 project_id"); return new RemoteUploadCoordinator(await assertSafeExistingProjectPath(root, root), projectId, transport); }
|
|
294
|
+
#transportOrThrow() { if (!this.#transport)
|
|
295
|
+
throw new AuthoringBridgeError("E_AUTH_REQUIRED", "CLI 没有 ProjectAuthBroker 受控 transport"); return this.#transport; }
|
|
296
|
+
async plan(request) {
|
|
297
|
+
if (!request.operation_id || !Number.isSafeInteger(request.expires_at_ms) || request.expires_at_ms <= Date.now() || request.targets.length === 0 || new Set(request.targets.map((target) => target.client_target_key)).size !== request.targets.length)
|
|
298
|
+
throw new AuthoringBridgeError("E_SCHEMA_INVALID", "upload plan 请求无效");
|
|
299
|
+
request.targets.forEach(assertTarget);
|
|
300
|
+
const captured = await captureNeutralUploadInputs(this.#root);
|
|
301
|
+
const transport = this.#transportOrThrow();
|
|
302
|
+
const logicalObjects = [];
|
|
303
|
+
for (const input of captured.inputs)
|
|
304
|
+
logicalObjects.push(await encodeNeutralUploadLogicalObject(input, async ({ ref, bytes }) => {
|
|
305
|
+
const path = `/v1/creator/project-transfer/staging/${ref.encoded_sha256}`;
|
|
306
|
+
const put = await transport.request({ project_id: this.#projectId, operation_id: request.operation_id, method: "PUT", path, body: bytes, body_size: ref.encoded_byte_length, body_digest: ref.encoded_sha256, raw_body_size: ref.raw_byte_length, raw_body_digest: ref.raw_sha256, body_codec: ref.codec });
|
|
307
|
+
if (put.status < 200 || put.status >= 300)
|
|
308
|
+
remoteError(put.status);
|
|
309
|
+
const head = await transport.request({ project_id: this.#projectId, operation_id: request.operation_id, method: "HEAD", path });
|
|
310
|
+
if (head.status < 200 || head.status >= 300 || head.readback === undefined || head.readback.sha256 !== ref.encoded_sha256 || head.readback.byte_length !== ref.encoded_byte_length)
|
|
311
|
+
throw new AuthoringBridgeError("E_VALIDATION_FAILED", "Upload v2 chunk HEAD readback 不匹配");
|
|
312
|
+
}, { codec: "gzip-member-v1", create_sha256: createSha256 }));
|
|
313
|
+
assertNeutralUploadPlanLimits(logicalObjects);
|
|
314
|
+
const body = { schemaVersion: 2, planId: `upload-${request.operation_id}`, snapshotId: captured.snapshot.snapshot_id, snapshotDigest: captured.snapshot.snapshot_digest, expiresAtMs: request.expires_at_ms, parts: ["scenario", "view"], targets: request.targets.map(buildUploadPlanTarget), objects: logicalObjects.map((object) => ({ objectId: object.object_id, objectKind: object.object_kind, ...(object.section_key === undefined ? {} : { sectionKey: object.section_key }), ...(object.media_type === undefined ? {} : { mediaType: object.media_type }), ...(object.canonical_encoding === undefined ? {} : { canonicalEncoding: object.canonical_encoding }), rawByteLength: object.raw_byte_length, rawSha256: object.raw_sha256, chunks: object.chunks.map((chunk) => ({ index: chunk.index, rawOffset: chunk.raw_offset, rawByteLength: chunk.raw_byte_length, rawSha256: chunk.raw_sha256, codec: chunk.codec, encodedByteLength: chunk.encoded_byte_length, encodedSha256: chunk.encoded_sha256 })) })) };
|
|
315
|
+
const encodedBody = new TextEncoder().encode(JSON.stringify(body));
|
|
316
|
+
if (encodedBody.byteLength > 256 * 1024)
|
|
317
|
+
throw new AuthoringBridgeError("E_SCHEMA_INVALID", "Upload v2 descriptor 超过 256 KiB");
|
|
318
|
+
const response = await transport.request({ project_id: this.#projectId, operation_id: request.operation_id, method: "POST", path: "/v1/creator/project-transfer/upload-plans", body, body_digest: digest(encodedBody), body_size: encodedBody.byteLength });
|
|
319
|
+
if (response.status < 200 || response.status >= 300)
|
|
320
|
+
remoteError(response.status);
|
|
321
|
+
return { ...parsePlan(response.body, request, captured.snapshot, captured.legacy_objects), logical_objects: logicalObjects };
|
|
322
|
+
}
|
|
323
|
+
async confirm(plan, planDigest) {
|
|
324
|
+
if (planDigest !== plan.plan_digest || plan.expires_at_ms <= Date.now())
|
|
325
|
+
throw new AuthoringBridgeError("E_CONFIRMATION_REQUIRED", "--confirm-plan 与当前 upload plan 不匹配或已过期");
|
|
326
|
+
const body = { operationId: plan.operation_id, planId: plan.plan_id, planDigest: plan.plan_digest, confirmation: "force_overwrite", targets: plan.targets.map((target) => ({ clientTargetKey: target.client_target_key, scenarioId: target.scenario_id, viewId: target.view_id })) };
|
|
327
|
+
const response = await this.#transportOrThrow().request({ project_id: this.#projectId, operation_id: plan.operation_id, method: "POST", path: "/v1/creator/project-transfer/upload/confirm", body, body_digest: jsonDigest(body) });
|
|
328
|
+
if (response.status < 200 || response.status >= 300)
|
|
329
|
+
remoteError(response.status);
|
|
330
|
+
const operation = parseOperation(response.body);
|
|
331
|
+
if (operation.operation_id !== plan.operation_id || operation.plan_id !== plan.plan_id || operation.plan_digest !== plan.plan_digest)
|
|
332
|
+
throw new AuthoringBridgeError("E_OPERATION_UNCERTAIN", "confirm 返回的 operation 未绑定冻结 plan");
|
|
333
|
+
return operation;
|
|
334
|
+
}
|
|
335
|
+
async operation(operationId, reconcile = false) { if (!operationId)
|
|
336
|
+
throw new AuthoringBridgeError("E_SCHEMA_INVALID", "upload operation_id 不能为空"); const response = await this.#transportOrThrow().request({ project_id: this.#projectId, operation_id: operationId, method: reconcile ? "POST" : "GET", path: `/v1/creator/project-transfer/operations/${encodeURIComponent(operationId)}${reconcile ? "/reconcile" : ""}` }); if (response.status < 200 || response.status >= 300)
|
|
337
|
+
remoteError(response.status); const operation = parseOperation(response.body); if (operation.operation_id !== operationId)
|
|
338
|
+
throw new AuthoringBridgeError("E_OPERATION_ID_CONFLICT", "operation readback 返回了不同 operation_id"); return operation; }
|
|
339
|
+
/** 仅消费 terminal authoritative upload witness;不从本地 WESP 或 plan 推导 Neutral identity。 */
|
|
340
|
+
async bindFrozenReviewTarget(operationId, options = {}) {
|
|
341
|
+
const operation = await this.operation(operationId);
|
|
342
|
+
const selected = terminalReviewTarget(operation, options.client_target_key);
|
|
343
|
+
return await writeFrozenReviewTargetBinding(this.#root, Object.freeze({ ...selected, project_id: this.#projectId }));
|
|
344
|
+
}
|
|
345
|
+
/** retry 只能复用既有 operation identity,服务端负责从该 operation 的冻结计划恢复。 */
|
|
346
|
+
async retry(operationId) { if (!operationId)
|
|
347
|
+
throw new AuthoringBridgeError("E_SCHEMA_INVALID", "upload operation_id 不能为空"); const response = await this.#transportOrThrow().request({ project_id: this.#projectId, operation_id: operationId, method: "POST", path: `/v1/creator/project-transfer/operations/${encodeURIComponent(operationId)}/retry` }); if (response.status < 200 || response.status >= 300)
|
|
348
|
+
remoteError(response.status); const operation = parseOperation(response.body); if (operation.operation_id !== operationId)
|
|
349
|
+
throw new AuthoringBridgeError("E_OPERATION_ID_CONFLICT", "retry 返回了不同 operation_id"); return operation; }
|
|
350
|
+
async undo(operationId) { if (!operationId)
|
|
351
|
+
throw new AuthoringBridgeError("E_SCHEMA_INVALID", "upload operation_id 不能为空"); const response = await this.#transportOrThrow().request({ project_id: this.#projectId, operation_id: operationId, method: "POST", path: `/v1/creator/project-transfer/operations/${encodeURIComponent(operationId)}/undo` }); if (response.status < 200 || response.status >= 300)
|
|
352
|
+
remoteError(response.status); const operation = parseOperation(response.body); if (operation.operation_id !== operationId)
|
|
353
|
+
throw new AuthoringBridgeError("E_OPERATION_ID_CONFLICT", "undo 返回了不同 operation_id"); return operation; }
|
|
354
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import type { AuthoringEffectIdentity } from "@world-engines/authoring-bridge";
|
|
2
|
+
/** 本地盘点项只允许引用项目根目录内的相对路径。 */
|
|
3
|
+
export interface ProjectTransferInventoryEntry {
|
|
4
|
+
readonly project_relative_path: string;
|
|
5
|
+
readonly sha256: string;
|
|
6
|
+
readonly bytes: number;
|
|
7
|
+
}
|
|
8
|
+
export interface ProjectTransferInventory {
|
|
9
|
+
readonly schema_version: 1;
|
|
10
|
+
readonly project_id: string;
|
|
11
|
+
readonly entries: readonly ProjectTransferInventoryEntry[];
|
|
12
|
+
}
|
|
13
|
+
/** 从当前 canonical 项目根重新计算的本地 authority 盘点。 */
|
|
14
|
+
export interface ProjectTransferLocalInventory extends ProjectTransferInventory {
|
|
15
|
+
readonly project_revision: number;
|
|
16
|
+
}
|
|
17
|
+
export interface ProjectTransferRequest extends AuthoringEffectIdentity {
|
|
18
|
+
readonly inventory: ProjectTransferInventory;
|
|
19
|
+
}
|
|
20
|
+
export interface ProjectTransferPlan {
|
|
21
|
+
readonly schema_version: 1;
|
|
22
|
+
readonly operation_id: string;
|
|
23
|
+
readonly request_digest: string;
|
|
24
|
+
readonly plan_digest: string;
|
|
25
|
+
readonly inventory: ProjectTransferInventory;
|
|
26
|
+
/** 仅供本地恢复使用,始终位于项目根目录内。 */
|
|
27
|
+
readonly staging_path: string;
|
|
28
|
+
/** 执行前的不可变本地计划副本,始终位于项目根目录内。 */
|
|
29
|
+
readonly preimage_path: string;
|
|
30
|
+
}
|
|
31
|
+
export type ProjectTransferAvailability = {
|
|
32
|
+
readonly state: "signed_out";
|
|
33
|
+
readonly detail: string;
|
|
34
|
+
} | {
|
|
35
|
+
readonly state: "unavailable";
|
|
36
|
+
readonly detail: string;
|
|
37
|
+
} | {
|
|
38
|
+
readonly state: "ready";
|
|
39
|
+
readonly detail: string;
|
|
40
|
+
};
|
|
41
|
+
/**
|
|
42
|
+
* 同步侧边栏、CLI 与 MCP 共用的只读本地投影。
|
|
43
|
+
* auth_state 为 unavailable 时表示当前进程没有能够证明登录态的 adapter,不能伪装成 ready。
|
|
44
|
+
*/
|
|
45
|
+
export interface ProjectTransferSyncStatus {
|
|
46
|
+
readonly schema_version: 1;
|
|
47
|
+
readonly project_id: string;
|
|
48
|
+
readonly project_revision: number;
|
|
49
|
+
readonly inventory_digest: string;
|
|
50
|
+
readonly inventory_entry_count: number;
|
|
51
|
+
readonly pending_durable_ledger_count: number;
|
|
52
|
+
readonly auth_state: "unavailable" | "signed_out" | "authenticated";
|
|
53
|
+
readonly remote_adapter: ProjectTransferAvailability;
|
|
54
|
+
}
|
|
55
|
+
export interface ProjectTransferConfirmation {
|
|
56
|
+
readonly operation_id: string;
|
|
57
|
+
readonly plan_digest: string;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* 远端传输只能由显式注入、负责真实身份与网络 I/O 的 adapter 执行。
|
|
61
|
+
* Project host 本身不包含 MCP 或网络 fallback。
|
|
62
|
+
*/
|
|
63
|
+
export interface ProjectTransferRemoteAdapter {
|
|
64
|
+
authState(): Promise<"signed_out" | "authenticated">;
|
|
65
|
+
/** execute 必须以 operation_id 幂等,并返回已由远端权威读取验证的 receipt。 */
|
|
66
|
+
execute(plan: ProjectTransferPlan): Promise<ProjectTransferRemoteReceipt>;
|
|
67
|
+
/**
|
|
68
|
+
* 这是 execute 前及崩溃重试时唯一允许的权威读回。
|
|
69
|
+
* only state:not_found 允许随后执行;网络、认证或未知状态必须 reject,不能降级为 not_found。
|
|
70
|
+
*/
|
|
71
|
+
reconcile(operation_id: string): Promise<ProjectTransferRemoteReconcileResult>;
|
|
72
|
+
}
|
|
73
|
+
export interface ProjectTransferRemoteReceipt {
|
|
74
|
+
readonly operation_id: string;
|
|
75
|
+
readonly remote_transfer_id: string;
|
|
76
|
+
readonly readback_verified: boolean;
|
|
77
|
+
}
|
|
78
|
+
export type ProjectTransferRemoteReconcileResult = {
|
|
79
|
+
readonly state: "not_found";
|
|
80
|
+
} | {
|
|
81
|
+
readonly state: "completed";
|
|
82
|
+
readonly receipt: ProjectTransferRemoteReceipt;
|
|
83
|
+
};
|
|
84
|
+
export type ProjectTransferReconcileResult = {
|
|
85
|
+
readonly state: "signed_out" | "unavailable";
|
|
86
|
+
readonly detail: string;
|
|
87
|
+
} | {
|
|
88
|
+
readonly state: "not_found";
|
|
89
|
+
} | {
|
|
90
|
+
readonly state: "completed";
|
|
91
|
+
readonly receipt: ProjectTransferRemoteReceipt;
|
|
92
|
+
};
|
|
93
|
+
export interface ProjectTransferCoordinatorOptions {
|
|
94
|
+
readonly remote_adapter?: ProjectTransferRemoteAdapter;
|
|
95
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { ProjectTransferAvailability, ProjectTransferConfirmation, ProjectTransferCoordinatorOptions, ProjectTransferLocalInventory, ProjectTransferPlan, ProjectTransferReconcileResult, ProjectTransferRemoteReceipt, ProjectTransferRequest, ProjectTransferSyncStatus } from "./transfer-types.js";
|
|
2
|
+
/**
|
|
3
|
+
* 只枚举 manifest、Scenario Part 与 View Part;toolchain、认证态、缓存、日志、staging 和 operation ledger 不属于可传输 authority。
|
|
4
|
+
*/
|
|
5
|
+
export declare function buildLocalProjectTransferInventory(root: string, expectedProjectRevision?: number): Promise<ProjectTransferLocalInventory>;
|
|
6
|
+
/** 本地 Project 与真实远端 adapter 之间的 fail-closed 边界;不包含 MCP 或网络 fallback。 */
|
|
7
|
+
export declare class ProjectTransferCoordinator {
|
|
8
|
+
#private;
|
|
9
|
+
readonly root: string;
|
|
10
|
+
private constructor();
|
|
11
|
+
static open(root: string, options?: ProjectTransferCoordinatorOptions): Promise<ProjectTransferCoordinator>;
|
|
12
|
+
availability(): Promise<ProjectTransferAvailability>;
|
|
13
|
+
inventory(): Promise<ProjectTransferLocalInventory>;
|
|
14
|
+
status(): Promise<ProjectTransferSyncStatus>;
|
|
15
|
+
prepare(request: ProjectTransferRequest): Promise<ProjectTransferPlan>;
|
|
16
|
+
execute(plan: ProjectTransferPlan, confirmation: ProjectTransferConfirmation | undefined): Promise<ProjectTransferRemoteReceipt>;
|
|
17
|
+
reconcile(operationId: string): Promise<ProjectTransferReconcileResult>;
|
|
18
|
+
}
|