@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,34 @@
|
|
|
1
|
+
import { AuthoringModelConnectionError, type AuthoringEmbeddingPort } from "./authoring-model-connection.js";
|
|
2
|
+
import { LOCAL_ACCOUNT_MODEL_PROXY_ORIGIN } from "./account-model-proxy.js";
|
|
3
|
+
export interface LocalAccountEmbeddingPort extends AuthoringEmbeddingPort {
|
|
4
|
+
readonly origin: typeof LOCAL_ACCOUNT_MODEL_PROXY_ORIGIN | string;
|
|
5
|
+
readonly accountIdentity: string;
|
|
6
|
+
readonly descriptorSha256: string;
|
|
7
|
+
readonly descriptorSha256ByInputKind: Readonly<{
|
|
8
|
+
document: string;
|
|
9
|
+
query: string;
|
|
10
|
+
}>;
|
|
11
|
+
/** 任何可能使用本地向量 cache 的入口必须先调用;即使全命中也验证当前登录账户。 */
|
|
12
|
+
assertCurrentAccount(options?: Readonly<{
|
|
13
|
+
signal?: AbortSignal;
|
|
14
|
+
}>): Promise<void>;
|
|
15
|
+
}
|
|
16
|
+
export interface CreateLocalAccountEmbeddingPortOptions {
|
|
17
|
+
readonly projectRoot: string;
|
|
18
|
+
readonly projectId: string;
|
|
19
|
+
readonly fetch?: typeof globalThis.fetch;
|
|
20
|
+
}
|
|
21
|
+
export interface LocalAccountEmbeddingTerminalReceipt {
|
|
22
|
+
readonly operationId: string;
|
|
23
|
+
readonly status: "authorized_no_response" | "settled_no_response" | "failed_no_response" | "reconcile_required";
|
|
24
|
+
readonly ledgerStatus: "authorized" | "dispatched" | "provider_succeeded" | "settled" | "failed" | "ambiguous" | "reconcile_required";
|
|
25
|
+
readonly retryable: false;
|
|
26
|
+
readonly retryPolicy: "do_not_change_idempotency_key";
|
|
27
|
+
}
|
|
28
|
+
/** 已持久化但无向量响应时,调用方只能保留同一 key 进行 reconcile,绝不生成新计费操作。 */
|
|
29
|
+
export declare class LocalAccountEmbeddingTerminalError extends AuthoringModelConnectionError {
|
|
30
|
+
readonly receipt: LocalAccountEmbeddingTerminalReceipt;
|
|
31
|
+
readonly name = "LocalAccountEmbeddingTerminalError";
|
|
32
|
+
constructor(receipt: LocalAccountEmbeddingTerminalReceipt);
|
|
33
|
+
}
|
|
34
|
+
export declare function createLocalAccountEmbeddingPort(options: CreateLocalAccountEmbeddingPortOptions): Promise<LocalAccountEmbeddingPort>;
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { AuthoringModelConnectionError, } from "./authoring-model-connection.js";
|
|
3
|
+
import { LOCAL_ACCOUNT_MODEL_PROXY_HOST, LOCAL_ACCOUNT_MODEL_PROXY_ORIGIN, LOCAL_ACCOUNT_MODEL_PROXY_PORT, resolveLocalAccountProjectBinding, } from "./account-model-proxy.js";
|
|
4
|
+
const SHA256_PATTERN = /^[a-f0-9]{64}$/u;
|
|
5
|
+
const MAX_TEXTS_HARD_LIMIT = 128;
|
|
6
|
+
const MAX_RESPONSE_BYTES = 16 * 1024 * 1024;
|
|
7
|
+
const REQUEST_TIMEOUT_MS = 30_000;
|
|
8
|
+
/** 已持久化但无向量响应时,调用方只能保留同一 key 进行 reconcile,绝不生成新计费操作。 */
|
|
9
|
+
export class LocalAccountEmbeddingTerminalError extends AuthoringModelConnectionError {
|
|
10
|
+
receipt;
|
|
11
|
+
name = "LocalAccountEmbeddingTerminalError";
|
|
12
|
+
constructor(receipt) {
|
|
13
|
+
super("provider_rejected", false, 409);
|
|
14
|
+
this.receipt = receipt;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
function record(value) {
|
|
18
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) ? value : undefined;
|
|
19
|
+
}
|
|
20
|
+
function connectionError(status) {
|
|
21
|
+
if (status === 401 || status === 403)
|
|
22
|
+
return new AuthoringModelConnectionError("embedding_credentials_missing", false, status);
|
|
23
|
+
return new AuthoringModelConnectionError("provider_rejected", status === 429 || (status !== undefined && status >= 500), status);
|
|
24
|
+
}
|
|
25
|
+
function terminalReceipt(value) {
|
|
26
|
+
const error = record(value.error);
|
|
27
|
+
const operationId = value.operation_id;
|
|
28
|
+
const status = value.status;
|
|
29
|
+
const ledgerStatus = value.ledger_status;
|
|
30
|
+
const terminalStatuses = new Set(["authorized_no_response", "settled_no_response", "failed_no_response", "reconcile_required"]);
|
|
31
|
+
const ledgerStatuses = new Set(["authorized", "dispatched", "provider_succeeded", "settled", "failed", "ambiguous", "reconcile_required"]);
|
|
32
|
+
if (error?.code !== "E_OPERATION_UNCERTAIN"
|
|
33
|
+
|| typeof operationId !== "string" || !/^[A-Za-z0-9._:-]{1,256}$/u.test(operationId)
|
|
34
|
+
|| typeof status !== "string" || !terminalStatuses.has(status)
|
|
35
|
+
|| typeof ledgerStatus !== "string" || !ledgerStatuses.has(ledgerStatus)
|
|
36
|
+
|| value.retryable !== false || value.retry_policy !== "do_not_change_idempotency_key")
|
|
37
|
+
return undefined;
|
|
38
|
+
return Object.freeze({
|
|
39
|
+
operationId,
|
|
40
|
+
status: status,
|
|
41
|
+
ledgerStatus: ledgerStatus,
|
|
42
|
+
retryable: false,
|
|
43
|
+
retryPolicy: "do_not_change_idempotency_key",
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
function canonicalProxyOrigin(value) {
|
|
47
|
+
let url;
|
|
48
|
+
try {
|
|
49
|
+
url = new URL(value);
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
throw new AuthoringModelConnectionError("invalid_configuration", false);
|
|
53
|
+
}
|
|
54
|
+
if (url.protocol !== "http:" || url.hostname !== LOCAL_ACCOUNT_MODEL_PROXY_HOST || url.port === ""
|
|
55
|
+
|| url.username !== "" || url.password !== "" || (url.pathname !== "" && url.pathname !== "/")
|
|
56
|
+
|| url.search !== "" || url.hash !== "") {
|
|
57
|
+
throw new AuthoringModelConnectionError("invalid_configuration", false);
|
|
58
|
+
}
|
|
59
|
+
return url.origin;
|
|
60
|
+
}
|
|
61
|
+
async function responseJson(response) {
|
|
62
|
+
if (!response.ok) {
|
|
63
|
+
if (response.status === 409) {
|
|
64
|
+
const declared = Number(response.headers.get("content-length"));
|
|
65
|
+
if (!Number.isFinite(declared) || declared <= MAX_RESPONSE_BYTES) {
|
|
66
|
+
try {
|
|
67
|
+
const body = await response.text();
|
|
68
|
+
if (Buffer.byteLength(body, "utf8") <= MAX_RESPONSE_BYTES) {
|
|
69
|
+
const parsed = record(JSON.parse(body));
|
|
70
|
+
const receipt = parsed === undefined ? undefined : terminalReceipt(parsed);
|
|
71
|
+
if (receipt !== undefined)
|
|
72
|
+
throw new LocalAccountEmbeddingTerminalError(receipt);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
catch (error) {
|
|
76
|
+
if (error instanceof LocalAccountEmbeddingTerminalError)
|
|
77
|
+
throw error;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
await response.body?.cancel().catch(() => undefined);
|
|
82
|
+
throw connectionError(response.status);
|
|
83
|
+
}
|
|
84
|
+
const declared = Number(response.headers.get("content-length"));
|
|
85
|
+
if (Number.isFinite(declared) && declared > MAX_RESPONSE_BYTES) {
|
|
86
|
+
await response.body?.cancel().catch(() => undefined);
|
|
87
|
+
throw new AuthoringModelConnectionError("provider_response_invalid", false, response.status);
|
|
88
|
+
}
|
|
89
|
+
const reader = response.body?.getReader();
|
|
90
|
+
const chunks = [];
|
|
91
|
+
let total = 0;
|
|
92
|
+
try {
|
|
93
|
+
if (reader !== undefined) {
|
|
94
|
+
while (true) {
|
|
95
|
+
const item = await reader.read();
|
|
96
|
+
if (item.done)
|
|
97
|
+
break;
|
|
98
|
+
total += item.value.byteLength;
|
|
99
|
+
if (total > MAX_RESPONSE_BYTES) {
|
|
100
|
+
await reader.cancel().catch(() => undefined);
|
|
101
|
+
throw new AuthoringModelConnectionError("provider_response_invalid", false, response.status);
|
|
102
|
+
}
|
|
103
|
+
chunks.push(item.value);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
catch (error) {
|
|
108
|
+
if (error instanceof AuthoringModelConnectionError)
|
|
109
|
+
throw error;
|
|
110
|
+
throw new AuthoringModelConnectionError("transport_failed", true, response.status);
|
|
111
|
+
}
|
|
112
|
+
const bytes = new Uint8Array(total);
|
|
113
|
+
let offset = 0;
|
|
114
|
+
for (const chunk of chunks) {
|
|
115
|
+
bytes.set(chunk, offset);
|
|
116
|
+
offset += chunk.byteLength;
|
|
117
|
+
}
|
|
118
|
+
let value;
|
|
119
|
+
try {
|
|
120
|
+
value = JSON.parse(new TextDecoder().decode(bytes));
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
throw new AuthoringModelConnectionError("provider_response_invalid", false, response.status);
|
|
124
|
+
}
|
|
125
|
+
const parsed = record(value);
|
|
126
|
+
if (parsed === undefined)
|
|
127
|
+
throw new AuthoringModelConnectionError("provider_response_invalid", false, response.status);
|
|
128
|
+
return parsed;
|
|
129
|
+
}
|
|
130
|
+
async function fetchJson(request, url, init, externalSignal) {
|
|
131
|
+
const controller = new AbortController();
|
|
132
|
+
let timedOut = false;
|
|
133
|
+
const abortFromExternal = () => controller.abort(externalSignal?.reason);
|
|
134
|
+
if (externalSignal?.aborted)
|
|
135
|
+
abortFromExternal();
|
|
136
|
+
else
|
|
137
|
+
externalSignal?.addEventListener("abort", abortFromExternal, { once: true });
|
|
138
|
+
const timer = setTimeout(() => { timedOut = true; controller.abort(); }, REQUEST_TIMEOUT_MS);
|
|
139
|
+
try {
|
|
140
|
+
const response = await request(url, { ...init, signal: controller.signal });
|
|
141
|
+
return await responseJson(response);
|
|
142
|
+
}
|
|
143
|
+
catch (error) {
|
|
144
|
+
if (externalSignal?.aborted)
|
|
145
|
+
throw new AuthoringModelConnectionError("aborted", false);
|
|
146
|
+
if (timedOut)
|
|
147
|
+
throw new AuthoringModelConnectionError("timeout", true);
|
|
148
|
+
if (error instanceof AuthoringModelConnectionError)
|
|
149
|
+
throw error;
|
|
150
|
+
throw new AuthoringModelConnectionError("transport_failed", true);
|
|
151
|
+
}
|
|
152
|
+
finally {
|
|
153
|
+
clearTimeout(timer);
|
|
154
|
+
externalSignal?.removeEventListener("abort", abortFromExternal);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
function projectHeaders(projectId, rootIdentity, expectedAccountIdentity) {
|
|
158
|
+
const headers = new Headers({
|
|
159
|
+
accept: "application/json",
|
|
160
|
+
"x-worldengine-project-id": projectId,
|
|
161
|
+
"x-worldengine-project-root-sha256": rootIdentity,
|
|
162
|
+
});
|
|
163
|
+
if (expectedAccountIdentity !== undefined)
|
|
164
|
+
headers.set("x-worldengine-account-identity", expectedAccountIdentity);
|
|
165
|
+
return headers;
|
|
166
|
+
}
|
|
167
|
+
function embeddingIdempotencyKey(projectId, accountIdentity, descriptorSha256, inputKind, texts) {
|
|
168
|
+
const hash = createHash("sha256")
|
|
169
|
+
.update("worldengine-local-account-embedding/v1\0")
|
|
170
|
+
.update(projectId)
|
|
171
|
+
.update("\0")
|
|
172
|
+
.update(accountIdentity)
|
|
173
|
+
.update("\0")
|
|
174
|
+
.update(descriptorSha256)
|
|
175
|
+
.update(`\0${inputKind}\0`);
|
|
176
|
+
for (const text of texts) {
|
|
177
|
+
const bytes = Buffer.from(text, "utf8");
|
|
178
|
+
hash.update(String(bytes.byteLength)).update(":").update(createHash("sha256").update(bytes).digest("hex")).update("\0");
|
|
179
|
+
}
|
|
180
|
+
return `embedding:${hash.digest("hex")}`;
|
|
181
|
+
}
|
|
182
|
+
export async function createLocalAccountEmbeddingPort(options) {
|
|
183
|
+
const binding = await resolveLocalAccountProjectBinding(options.projectRoot, options.projectId);
|
|
184
|
+
const origin = canonicalProxyOrigin(LOCAL_ACCOUNT_MODEL_PROXY_ORIGIN);
|
|
185
|
+
if (origin !== `http://${LOCAL_ACCOUNT_MODEL_PROXY_HOST}:${LOCAL_ACCOUNT_MODEL_PROXY_PORT}`) {
|
|
186
|
+
throw new AuthoringModelConnectionError("invalid_configuration", false);
|
|
187
|
+
}
|
|
188
|
+
const request = options.fetch ?? globalThis.fetch;
|
|
189
|
+
if (typeof request !== "function")
|
|
190
|
+
throw new AuthoringModelConnectionError("invalid_configuration", false);
|
|
191
|
+
const get = async (path, signal, expectedAccountIdentity) => {
|
|
192
|
+
const init = {
|
|
193
|
+
method: "GET",
|
|
194
|
+
redirect: "manual",
|
|
195
|
+
headers: projectHeaders(options.projectId, binding.rootIdentity, expectedAccountIdentity),
|
|
196
|
+
};
|
|
197
|
+
return fetchJson(request, `${origin}${path}`, init, signal);
|
|
198
|
+
};
|
|
199
|
+
const initialSession = await get("/v1/session");
|
|
200
|
+
const accountIdentity = initialSession.account_identity;
|
|
201
|
+
if (initialSession.project_id !== options.projectId || typeof accountIdentity !== "string" || !SHA256_PATTERN.test(accountIdentity)) {
|
|
202
|
+
throw new AuthoringModelConnectionError("provider_response_invalid", false);
|
|
203
|
+
}
|
|
204
|
+
const descriptor = await get("/v1/embedding/descriptor", undefined, accountIdentity);
|
|
205
|
+
const provider = descriptor.provider;
|
|
206
|
+
const model = descriptor.model;
|
|
207
|
+
const dimensions = descriptor.dimensions;
|
|
208
|
+
const maxInputs = descriptor.max_inputs;
|
|
209
|
+
const maxInputChars = descriptor.max_input_chars;
|
|
210
|
+
const descriptorSha256 = descriptor.descriptor_sha256;
|
|
211
|
+
const inputKinds = record(descriptor.input_kinds);
|
|
212
|
+
const documentKind = record(inputKinds?.document);
|
|
213
|
+
const queryKind = record(inputKinds?.query);
|
|
214
|
+
if (descriptor.project_id !== options.projectId || descriptor.account_identity !== accountIdentity
|
|
215
|
+
|| typeof provider !== "string" || !/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/u.test(provider)
|
|
216
|
+
|| typeof model !== "string" || !/^[A-Za-z0-9][A-Za-z0-9._:/-]{0,255}$/u.test(model)
|
|
217
|
+
|| !Number.isSafeInteger(dimensions) || dimensions < 1 || dimensions > 65_536
|
|
218
|
+
|| !Number.isSafeInteger(maxInputs) || maxInputs < 1 || maxInputs > MAX_TEXTS_HARD_LIMIT
|
|
219
|
+
|| !Number.isSafeInteger(maxInputChars) || maxInputChars < 1 || maxInputChars > 1_000_000
|
|
220
|
+
|| typeof descriptorSha256 !== "string" || !SHA256_PATTERN.test(descriptorSha256)
|
|
221
|
+
|| documentKind?.descriptor_sha256 !== descriptorSha256
|
|
222
|
+
|| typeof queryKind?.descriptor_sha256 !== "string" || !SHA256_PATTERN.test(queryKind.descriptor_sha256)) {
|
|
223
|
+
throw new AuthoringModelConnectionError("provider_response_invalid", false);
|
|
224
|
+
}
|
|
225
|
+
const descriptorSha256ByInputKind = Object.freeze({ document: descriptorSha256, query: queryKind.descriptor_sha256 });
|
|
226
|
+
const assertCurrentAccount = async (assertOptions) => {
|
|
227
|
+
const current = await get("/v1/session", assertOptions?.signal);
|
|
228
|
+
if (current.project_id !== options.projectId || current.account_identity !== accountIdentity) {
|
|
229
|
+
throw new AuthoringModelConnectionError("embedding_credentials_missing", false, 409);
|
|
230
|
+
}
|
|
231
|
+
const refreshed = await get("/v1/embedding/descriptor", assertOptions?.signal, accountIdentity);
|
|
232
|
+
if (refreshed.project_id !== options.projectId || refreshed.account_identity !== accountIdentity
|
|
233
|
+
|| refreshed.provider !== provider || refreshed.model !== model || refreshed.dimensions !== dimensions
|
|
234
|
+
|| refreshed.max_inputs !== maxInputs || refreshed.max_input_chars !== maxInputChars
|
|
235
|
+
|| refreshed.descriptor_sha256 !== descriptorSha256
|
|
236
|
+
|| record(record(refreshed.input_kinds)?.document)?.descriptor_sha256 !== descriptorSha256ByInputKind.document
|
|
237
|
+
|| record(record(refreshed.input_kinds)?.query)?.descriptor_sha256 !== descriptorSha256ByInputKind.query) {
|
|
238
|
+
throw new AuthoringModelConnectionError("provider_model_mismatch", false, 409);
|
|
239
|
+
}
|
|
240
|
+
};
|
|
241
|
+
return Object.freeze({
|
|
242
|
+
origin,
|
|
243
|
+
providerId: `worldengine-account/${provider}/${accountIdentity}`,
|
|
244
|
+
modelId: model,
|
|
245
|
+
dimensions: dimensions,
|
|
246
|
+
accountIdentity,
|
|
247
|
+
descriptorSha256,
|
|
248
|
+
descriptorSha256ByInputKind,
|
|
249
|
+
assertCurrentAccount,
|
|
250
|
+
async embed(texts, embedOptions) {
|
|
251
|
+
if (!Array.isArray(texts) || texts.length === 0 || texts.length > MAX_TEXTS_HARD_LIMIT
|
|
252
|
+
|| texts.some((text) => typeof text !== "string" || text.length === 0 || [...text].length > maxInputChars)) {
|
|
253
|
+
throw new AuthoringModelConnectionError("invalid_request", false);
|
|
254
|
+
}
|
|
255
|
+
await assertCurrentAccount(embedOptions);
|
|
256
|
+
const inputKind = embedOptions?.inputKind ?? "document";
|
|
257
|
+
const complete = [];
|
|
258
|
+
for (let offset = 0; offset < texts.length; offset += maxInputs) {
|
|
259
|
+
const batch = texts.slice(offset, offset + maxInputs);
|
|
260
|
+
const body = JSON.stringify({ model, input: batch, dimensions, encoding_format: "float", input_kind: inputKind });
|
|
261
|
+
const postHeaders = projectHeaders(options.projectId, binding.rootIdentity, accountIdentity);
|
|
262
|
+
postHeaders.set("content-type", "application/json");
|
|
263
|
+
postHeaders.set("idempotency-key", embeddingIdempotencyKey(options.projectId, accountIdentity, descriptorSha256ByInputKind[inputKind], inputKind, batch));
|
|
264
|
+
const result = await fetchJson(request, `${origin}/v1/embeddings`, {
|
|
265
|
+
method: "POST", redirect: "manual", headers: postHeaders, body,
|
|
266
|
+
}, embedOptions?.signal);
|
|
267
|
+
const usage = record(result.usage);
|
|
268
|
+
if (result.model !== model || result.provider !== provider || result.dimensions !== dimensions || result.input_kind !== inputKind
|
|
269
|
+
|| typeof result.operation_id !== "string" || result.operation_id.length === 0 || result.operation_id.length > 256
|
|
270
|
+
|| !Number.isSafeInteger(usage?.prompt_tokens) || usage?.prompt_tokens < 0
|
|
271
|
+
|| !Number.isSafeInteger(usage?.total_tokens) || usage?.total_tokens < usage?.prompt_tokens
|
|
272
|
+
|| !Array.isArray(result.data) || result.data.length !== batch.length) {
|
|
273
|
+
throw new AuthoringModelConnectionError("provider_response_invalid", false);
|
|
274
|
+
}
|
|
275
|
+
const ordered = new Array(batch.length);
|
|
276
|
+
for (const itemValue of result.data) {
|
|
277
|
+
const item = record(itemValue);
|
|
278
|
+
const index = item?.index;
|
|
279
|
+
const vector = item?.embedding;
|
|
280
|
+
if (!Number.isSafeInteger(index) || index < 0 || index >= batch.length
|
|
281
|
+
|| ordered[index] !== undefined || !Array.isArray(vector) || vector.length !== dimensions
|
|
282
|
+
|| vector.some((entry) => typeof entry !== "number" || !Number.isFinite(entry))) {
|
|
283
|
+
throw new AuthoringModelConnectionError("provider_response_invalid", false);
|
|
284
|
+
}
|
|
285
|
+
ordered[index] = Object.freeze([...vector]);
|
|
286
|
+
}
|
|
287
|
+
if (ordered.some((vector) => vector === undefined))
|
|
288
|
+
throw new AuthoringModelConnectionError("provider_response_invalid", false);
|
|
289
|
+
complete.push(...ordered);
|
|
290
|
+
}
|
|
291
|
+
return Object.freeze(complete);
|
|
292
|
+
},
|
|
293
|
+
});
|
|
294
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { LocalProjectHost } from "./project-host.js";
|
|
2
|
+
export declare const LOCAL_GALLERY_MAX_IMPORT_BYTES: number;
|
|
3
|
+
export declare const LOCAL_GALLERY_MAX_FILES = 2048;
|
|
4
|
+
export type LocalGalleryArea = "scenario" | "view";
|
|
5
|
+
export type LocalGalleryOwnerKind = "entity" | "relation";
|
|
6
|
+
export interface LocalGalleryImage {
|
|
7
|
+
readonly path: string;
|
|
8
|
+
readonly asset_path: string;
|
|
9
|
+
readonly mime: "image/png" | "image/jpeg" | "image/webp" | "image/gif" | "image/avif";
|
|
10
|
+
readonly size: number;
|
|
11
|
+
readonly sha256: string;
|
|
12
|
+
readonly owner_kind?: LocalGalleryOwnerKind;
|
|
13
|
+
readonly owner_id?: string;
|
|
14
|
+
}
|
|
15
|
+
export interface LocalGalleryImportRequest {
|
|
16
|
+
readonly operation_id: string;
|
|
17
|
+
readonly expected_project_revision: number;
|
|
18
|
+
readonly area: LocalGalleryArea;
|
|
19
|
+
readonly owner_kind?: LocalGalleryOwnerKind;
|
|
20
|
+
readonly owner_id?: string;
|
|
21
|
+
readonly filename: string;
|
|
22
|
+
readonly content_base64: string;
|
|
23
|
+
}
|
|
24
|
+
export type LocalGalleryImportResult = LocalGalleryImage & Readonly<{
|
|
25
|
+
operation_id: string;
|
|
26
|
+
previous_project_revision: number;
|
|
27
|
+
next_project_revision: number;
|
|
28
|
+
project_snapshot_digest: string;
|
|
29
|
+
readback_verified: boolean;
|
|
30
|
+
}>;
|
|
31
|
+
export declare function listLocalGalleryImages(root: string, area: LocalGalleryArea, ownerKind?: LocalGalleryOwnerKind, ownerId?: string): Promise<readonly LocalGalleryImage[]>;
|
|
32
|
+
export declare function readLocalGalleryImage(root: string, path: string): Promise<LocalGalleryImage & {
|
|
33
|
+
bytes: Uint8Array;
|
|
34
|
+
}>;
|
|
35
|
+
export declare function importLocalGalleryImage(host: LocalProjectHost, request: LocalGalleryImportRequest): Promise<LocalGalleryImportResult>;
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
import { open, readFile, readdir, rename, stat, writeFile } from "node:fs/promises";
|
|
2
|
+
import { extname, join } from "node:path";
|
|
3
|
+
import { AuthoringBridgeError } from "@world-engines/authoring-bridge";
|
|
4
|
+
import { decodeGalleryScenarioEntityDirectory, galleryScenarioAssetPath, galleryViewAssetPath, sha256Hex, validateLocalAuthorProjectManifest, } from "@world-engines/project-format";
|
|
5
|
+
import { LOCAL_GUI_MAX_IMAGE_ASSET_BYTES, localGuiImageBytesMatchMime, localGuiImageMimeForPath, } from "./local-gui-image-assets.js";
|
|
6
|
+
import { acquireProjectWriterLease } from "./project-writer-lease.js";
|
|
7
|
+
import { assertSafeExistingProjectPath, assertSafeProjectParent, ensureSafeProjectDirectory, safeProjectPath } from "./safe-project-path.js";
|
|
8
|
+
export const LOCAL_GALLERY_MAX_IMPORT_BYTES = 2 * 1024 * 1024;
|
|
9
|
+
export const LOCAL_GALLERY_MAX_FILES = 2_048;
|
|
10
|
+
function fail(code, message) {
|
|
11
|
+
throw new AuthoringBridgeError(code, message);
|
|
12
|
+
}
|
|
13
|
+
async function manifest(root) {
|
|
14
|
+
const path = await assertSafeExistingProjectPath(root, join(root, "worldengine.project.json"));
|
|
15
|
+
return validateLocalAuthorProjectManifest(JSON.parse(await readFile(path, "utf8")));
|
|
16
|
+
}
|
|
17
|
+
async function assertV2(root) {
|
|
18
|
+
if ((await manifest(root)).schema_version !== 2)
|
|
19
|
+
fail("E_PROJECT_MIGRATION_REQUIRED", "Gallery 需要 project manifest schema v2");
|
|
20
|
+
}
|
|
21
|
+
function decodeBase64(value) {
|
|
22
|
+
if (typeof value !== "string" || value.length === 0 || value.length > Math.ceil(LOCAL_GALLERY_MAX_IMPORT_BYTES / 3) * 4 + 4 || !/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test(value)) {
|
|
23
|
+
fail("E_SCHEMA_INVALID", "content_base64 无效或过大");
|
|
24
|
+
}
|
|
25
|
+
const bytes = new Uint8Array(Buffer.from(value, "base64"));
|
|
26
|
+
if (bytes.byteLength === 0 || bytes.byteLength > LOCAL_GALLERY_MAX_IMPORT_BYTES || Buffer.from(bytes).toString("base64") !== value) {
|
|
27
|
+
fail("E_VALIDATION_FAILED", "图片内容无效或过大");
|
|
28
|
+
}
|
|
29
|
+
return bytes;
|
|
30
|
+
}
|
|
31
|
+
function canonicalExtension(filename) {
|
|
32
|
+
if (typeof filename !== "string" || filename.includes("/") || filename.includes("\\"))
|
|
33
|
+
fail("E_SCHEMA_INVALID", "filename 无效");
|
|
34
|
+
const extension = extname(filename).toLowerCase();
|
|
35
|
+
if (localGuiImageMimeForPath(`image${extension}`) === undefined)
|
|
36
|
+
fail("E_VALIDATION_FAILED", "图片扩展名不受支持");
|
|
37
|
+
return extension === ".jpeg" ? "jpg" : extension.slice(1);
|
|
38
|
+
}
|
|
39
|
+
function scenarioKind(kind) {
|
|
40
|
+
return kind === "entity" ? "node" : "edge";
|
|
41
|
+
}
|
|
42
|
+
async function assertOwner(host, ownerKind, ownerId) {
|
|
43
|
+
const graph = await host.readGraphProjectionReadback();
|
|
44
|
+
const exists = ownerKind === "entity"
|
|
45
|
+
? graph.authoring.entities.some((row) => row.entity_id === ownerId || row.id === ownerId)
|
|
46
|
+
: graph.authoring.relations.some((row) => row.relation_id === ownerId);
|
|
47
|
+
if (!exists)
|
|
48
|
+
fail("E_REFERENCE_MISSING", "Gallery owner 不存在");
|
|
49
|
+
}
|
|
50
|
+
async function imageReadback(root, path, owner) {
|
|
51
|
+
const absolute = await assertSafeExistingProjectPath(root, join(root, ...path.split("/")));
|
|
52
|
+
const info = await stat(absolute);
|
|
53
|
+
const mime = localGuiImageMimeForPath(path);
|
|
54
|
+
if (!info.isFile() || mime === undefined || info.size <= 0 || info.size > LOCAL_GUI_MAX_IMAGE_ASSET_BYTES)
|
|
55
|
+
fail("E_VALIDATION_FAILED", "Gallery 图片无效");
|
|
56
|
+
const bytes = new Uint8Array(await readFile(absolute));
|
|
57
|
+
if (bytes.byteLength > LOCAL_GUI_MAX_IMAGE_ASSET_BYTES || !localGuiImageBytesMatchMime(bytes, mime))
|
|
58
|
+
fail("E_VALIDATION_FAILED", "Gallery 图片扩展名与内容不一致");
|
|
59
|
+
const digest = await sha256Hex(bytes);
|
|
60
|
+
return { path, asset_path: path, mime, size: bytes.byteLength, sha256: digest, bytes, ...(owner === undefined ? {} : { owner_kind: owner.kind, owner_id: owner.id }) };
|
|
61
|
+
}
|
|
62
|
+
export async function listLocalGalleryImages(root, area, ownerKind, ownerId) {
|
|
63
|
+
await assertV2(root);
|
|
64
|
+
if (area !== "scenario" && area !== "view")
|
|
65
|
+
fail("E_SCHEMA_INVALID", "Gallery area 无效");
|
|
66
|
+
if ((area === "scenario") !== (ownerKind !== undefined && ownerId !== undefined))
|
|
67
|
+
fail("E_SCHEMA_INVALID", "Scenario Gallery 必须提供 owner");
|
|
68
|
+
const base = area === "view"
|
|
69
|
+
? "gallery/view"
|
|
70
|
+
: `gallery/scenario/${galleryScenarioAssetPath(scenarioKind(ownerKind), ownerId, "placeholder.png").split("/")[2]}`;
|
|
71
|
+
let directory;
|
|
72
|
+
try {
|
|
73
|
+
directory = await assertSafeExistingProjectPath(root, join(root, ...base.split("/")));
|
|
74
|
+
}
|
|
75
|
+
catch (error) {
|
|
76
|
+
if (area === "scenario" && error.code === "ENOENT")
|
|
77
|
+
return [];
|
|
78
|
+
throw error;
|
|
79
|
+
}
|
|
80
|
+
const result = [];
|
|
81
|
+
for (const entry of (await readdir(directory, { withFileTypes: true })).sort((a, b) => a.name.localeCompare(b.name))) {
|
|
82
|
+
if (result.length >= LOCAL_GALLERY_MAX_FILES)
|
|
83
|
+
fail("E_VALIDATION_FAILED", "Gallery 图片数量超过上限");
|
|
84
|
+
if (!entry.isFile() || entry.isSymbolicLink())
|
|
85
|
+
continue;
|
|
86
|
+
const path = `${base}/${entry.name}`;
|
|
87
|
+
try {
|
|
88
|
+
const { bytes: _bytes, ...image } = await imageReadback(root, path, ownerKind === undefined ? undefined : { kind: ownerKind, id: ownerId });
|
|
89
|
+
void _bytes;
|
|
90
|
+
if (`${image.sha256}.${canonicalExtension(entry.name)}` !== entry.name)
|
|
91
|
+
continue;
|
|
92
|
+
result.push(image);
|
|
93
|
+
}
|
|
94
|
+
catch (error) {
|
|
95
|
+
if (error instanceof AuthoringBridgeError && error.code === "E_VALIDATION_FAILED")
|
|
96
|
+
continue;
|
|
97
|
+
throw error;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return result;
|
|
101
|
+
}
|
|
102
|
+
export async function readLocalGalleryImage(root, path) {
|
|
103
|
+
await assertV2(root);
|
|
104
|
+
const parts = path.split("/");
|
|
105
|
+
if (parts[0] !== "gallery" || !["scenario", "view"].includes(parts[1] ?? ""))
|
|
106
|
+
fail("E_PATH_OUTSIDE_PROJECT", "Gallery path 无效");
|
|
107
|
+
if (parts[1] === "view" && parts.length === 3) {
|
|
108
|
+
const image = await imageReadback(root, path);
|
|
109
|
+
if (galleryViewAssetPath(image.sha256, canonicalExtension(parts[2])) !== path)
|
|
110
|
+
fail("E_PATH_OUTSIDE_PROJECT", "Gallery path 非 canonical");
|
|
111
|
+
return image;
|
|
112
|
+
}
|
|
113
|
+
if (parts[1] === "scenario" && parts.length === 4) {
|
|
114
|
+
const owner = decodeGalleryScenarioEntityDirectory(parts[2]);
|
|
115
|
+
const image = await imageReadback(root, path, { kind: owner.kind === "node" ? "entity" : "relation", id: owner.id });
|
|
116
|
+
if (galleryScenarioAssetPath(owner.kind, owner.id, `${image.sha256}.${canonicalExtension(parts[3])}`) !== path)
|
|
117
|
+
fail("E_PATH_OUTSIDE_PROJECT", "Gallery path 非 canonical");
|
|
118
|
+
return image;
|
|
119
|
+
}
|
|
120
|
+
fail("E_PATH_OUTSIDE_PROJECT", "Gallery path 无效");
|
|
121
|
+
}
|
|
122
|
+
async function atomicLedger(root, path, value) {
|
|
123
|
+
const staging = await ensureSafeProjectDirectory(root, ".worldengine/staging");
|
|
124
|
+
const bytes = new TextEncoder().encode(`${JSON.stringify(value)}\n`);
|
|
125
|
+
const temporary = join(staging, `gallery-ledger-${await sha256Hex(bytes)}.tmp`);
|
|
126
|
+
const handle = await open(temporary, "wx");
|
|
127
|
+
try {
|
|
128
|
+
await handle.writeFile(bytes);
|
|
129
|
+
await handle.sync();
|
|
130
|
+
}
|
|
131
|
+
finally {
|
|
132
|
+
await handle.close();
|
|
133
|
+
}
|
|
134
|
+
await assertSafeProjectParent(root, path);
|
|
135
|
+
await rename(temporary, path);
|
|
136
|
+
}
|
|
137
|
+
export async function importLocalGalleryImage(host, request) {
|
|
138
|
+
await assertV2(host.root);
|
|
139
|
+
if (request.area !== "scenario" && request.area !== "view")
|
|
140
|
+
fail("E_SCHEMA_INVALID", "Gallery area 无效");
|
|
141
|
+
if (!Number.isSafeInteger(request.expected_project_revision) || request.expected_project_revision < 0 || typeof request.operation_id !== "string" || request.operation_id.length === 0)
|
|
142
|
+
fail("E_SCHEMA_INVALID", "Gallery effect identity 无效");
|
|
143
|
+
if ((request.area === "scenario") !== (request.owner_kind !== undefined && request.owner_id !== undefined))
|
|
144
|
+
fail("E_SCHEMA_INVALID", "Scenario Gallery 必须提供 owner");
|
|
145
|
+
if (request.owner_kind !== undefined && request.owner_kind !== "entity" && request.owner_kind !== "relation")
|
|
146
|
+
fail("E_SCHEMA_INVALID", "Gallery owner_kind 无效");
|
|
147
|
+
if (request.owner_id !== undefined && request.owner_id.length === 0)
|
|
148
|
+
fail("E_SCHEMA_INVALID", "Gallery owner_id 无效");
|
|
149
|
+
const bytes = decodeBase64(request.content_base64);
|
|
150
|
+
const extension = canonicalExtension(request.filename);
|
|
151
|
+
const mime = localGuiImageMimeForPath(`image.${extension}`);
|
|
152
|
+
if (!localGuiImageBytesMatchMime(bytes, mime))
|
|
153
|
+
fail("E_VALIDATION_FAILED", "图片扩展名与内容不一致");
|
|
154
|
+
const contentSha256 = await sha256Hex(bytes);
|
|
155
|
+
const targetPath = request.area === "view"
|
|
156
|
+
? galleryViewAssetPath(contentSha256, extension)
|
|
157
|
+
: galleryScenarioAssetPath(scenarioKind(request.owner_kind), request.owner_id, `${contentSha256}.${extension}`);
|
|
158
|
+
const requestDigest = await sha256Hex(new TextEncoder().encode(JSON.stringify({ ...request, content_base64: contentSha256 })));
|
|
159
|
+
await ensureSafeProjectDirectory(host.root, ".worldengine/operations");
|
|
160
|
+
const ledgerKey = await sha256Hex(new TextEncoder().encode(request.operation_id));
|
|
161
|
+
const ledgerPath = await safeProjectPath(host.root, `.worldengine/operations/gallery-${ledgerKey}.json`);
|
|
162
|
+
let ledger;
|
|
163
|
+
try {
|
|
164
|
+
ledger = JSON.parse(await readFile(await assertSafeExistingProjectPath(host.root, ledgerPath), "utf8"));
|
|
165
|
+
}
|
|
166
|
+
catch (error) {
|
|
167
|
+
if (error.code !== "ENOENT")
|
|
168
|
+
throw error;
|
|
169
|
+
}
|
|
170
|
+
if (ledger !== undefined) {
|
|
171
|
+
if (ledger.request_digest !== requestDigest || ledger.target_path !== targetPath)
|
|
172
|
+
fail("E_OPERATION_ID_CONFLICT", "Gallery operation_id 已绑定其他请求");
|
|
173
|
+
if (ledger.state === "completed" && ledger.receipt !== undefined)
|
|
174
|
+
return ledger.receipt;
|
|
175
|
+
}
|
|
176
|
+
const lease = await acquireProjectWriterLease(host.root, { entrypoint: "gallery-import", operation_id: request.operation_id });
|
|
177
|
+
try {
|
|
178
|
+
const before = await host.inspect();
|
|
179
|
+
if (before.project_revision !== request.expected_project_revision)
|
|
180
|
+
fail("E_STALE_PROJECT_REVISION", "Gallery import project revision 已变化");
|
|
181
|
+
if (request.area === "scenario")
|
|
182
|
+
await assertOwner(host, request.owner_kind, request.owner_id);
|
|
183
|
+
const targetDirectory = targetPath.split("/").slice(0, -1).join("/");
|
|
184
|
+
await ensureSafeProjectDirectory(host.root, targetDirectory);
|
|
185
|
+
const target = await safeProjectPath(host.root, targetPath);
|
|
186
|
+
try {
|
|
187
|
+
const existing = new Uint8Array(await readFile(await assertSafeExistingProjectPath(host.root, target)));
|
|
188
|
+
if (await sha256Hex(existing) !== contentSha256)
|
|
189
|
+
fail("E_OPERATION_ID_CONFLICT", "Gallery target 内容冲突");
|
|
190
|
+
}
|
|
191
|
+
catch (error) {
|
|
192
|
+
if (error.code !== "ENOENT")
|
|
193
|
+
throw error;
|
|
194
|
+
const handle = await open(target, "wx");
|
|
195
|
+
try {
|
|
196
|
+
await handle.writeFile(bytes);
|
|
197
|
+
await handle.sync();
|
|
198
|
+
}
|
|
199
|
+
finally {
|
|
200
|
+
await handle.close();
|
|
201
|
+
}
|
|
202
|
+
const readback = new Uint8Array(await readFile(await assertSafeExistingProjectPath(host.root, target)));
|
|
203
|
+
if (await sha256Hex(readback) !== contentSha256)
|
|
204
|
+
fail("E_OPERATION_UNCERTAIN", "Gallery import 回读不一致");
|
|
205
|
+
}
|
|
206
|
+
if (ledger === undefined) {
|
|
207
|
+
ledger = { schema_version: 1, request_digest: requestDigest, target_path: targetPath, state: "prepared" };
|
|
208
|
+
await writeFile(ledgerPath, `${JSON.stringify(ledger)}\n`, { flag: "wx" });
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
finally {
|
|
212
|
+
await lease.release();
|
|
213
|
+
}
|
|
214
|
+
const snapshot = await host.snapshot({ operation_id: `gallery_snapshot_${ledgerKey}`, expected_project_revision: request.expected_project_revision });
|
|
215
|
+
const image = await readLocalGalleryImage(host.root, targetPath);
|
|
216
|
+
const receipt = {
|
|
217
|
+
path: image.path, asset_path: image.asset_path, mime: image.mime, size: image.size, sha256: image.sha256,
|
|
218
|
+
...(image.owner_kind === undefined ? {} : { owner_kind: image.owner_kind, owner_id: image.owner_id }),
|
|
219
|
+
operation_id: request.operation_id,
|
|
220
|
+
previous_project_revision: snapshot.previous_project_revision,
|
|
221
|
+
next_project_revision: snapshot.next_project_revision,
|
|
222
|
+
project_snapshot_digest: snapshot.project_snapshot_digest,
|
|
223
|
+
readback_verified: snapshot.readback_verified && image.sha256 === contentSha256,
|
|
224
|
+
};
|
|
225
|
+
await atomicLedger(host.root, ledgerPath, { ...ledger, state: "completed", receipt });
|
|
226
|
+
return receipt;
|
|
227
|
+
}
|