@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,59 @@
|
|
|
1
|
+
import { type AuthoringBridgeAdapter, type AuthoringEffectIdentity, type AuthoringToolCall, type AuthoringToolName, type AuthoringToolReceiptV1 } from "@world-engines/authoring-bridge";
|
|
2
|
+
import { type ScenarioProjectionHydrator, type ScenarioSourceReadback, type ScenarioWriteRequest, type ScenarioWriterFaultInjector } from "./scenario-writer.js";
|
|
3
|
+
import { type DesktopAuthoringCapabilities, type GraphProjectionReadback } from "./desktop-authoring-runtime.js";
|
|
4
|
+
import type { LadybugGraphSnapshotRequest } from "./ladybug-runtime.js";
|
|
5
|
+
export interface ProjectStatus {
|
|
6
|
+
readonly project_id: string;
|
|
7
|
+
readonly project_revision: number;
|
|
8
|
+
readonly snapshot_id: string;
|
|
9
|
+
readonly snapshot_digest: string;
|
|
10
|
+
readonly scenario_integrity: "clean" | "dirty";
|
|
11
|
+
readonly view_state: "clean" | "dirty";
|
|
12
|
+
readonly root_dependencies_installed: boolean;
|
|
13
|
+
readonly view_dependencies_installed: boolean;
|
|
14
|
+
}
|
|
15
|
+
export interface DoctorCheck {
|
|
16
|
+
readonly name: string;
|
|
17
|
+
readonly ok: boolean;
|
|
18
|
+
readonly detail: string;
|
|
19
|
+
}
|
|
20
|
+
export interface ProjectDoctorReport {
|
|
21
|
+
readonly schema_version: 1;
|
|
22
|
+
readonly project_id: string;
|
|
23
|
+
readonly ok: boolean;
|
|
24
|
+
readonly checks: readonly DoctorCheck[];
|
|
25
|
+
}
|
|
26
|
+
export type SnapshotRequest = AuthoringEffectIdentity;
|
|
27
|
+
export interface LocalProjectHostOptions {
|
|
28
|
+
readonly authoring_adapter?: AuthoringBridgeAdapter;
|
|
29
|
+
/** Desktop 的真实 authoring handler;未提供 Browser store/IDB/localStorage fallback。 */
|
|
30
|
+
readonly desktop_authoring_capabilities?: DesktopAuthoringCapabilities;
|
|
31
|
+
readonly scenario_projection_hydrator?: ScenarioProjectionHydrator;
|
|
32
|
+
/** 只用于 crash recovery integration test,生产路径不得注入。 */
|
|
33
|
+
readonly scenario_writer_fault_injector?: ScenarioWriterFaultInjector;
|
|
34
|
+
}
|
|
35
|
+
export declare class LocalProjectHost {
|
|
36
|
+
#private;
|
|
37
|
+
readonly root: string;
|
|
38
|
+
private constructor();
|
|
39
|
+
static open(root: string, options?: LocalProjectHostOptions): Promise<LocalProjectHost>;
|
|
40
|
+
inspect(): Promise<ProjectStatus>;
|
|
41
|
+
/** exact-21 descriptor 始终稳定;三个外部 callback 的执行可用性由真实注入端口决定。 */
|
|
42
|
+
isToolAvailable(name: AuthoringToolName): boolean;
|
|
43
|
+
/** Download completion fence:从真实 source/view/manifest 重新计算,不消费 receipt 自报字段。 */
|
|
44
|
+
readTransferReadback(): Promise<Readonly<{
|
|
45
|
+
project_revision: number;
|
|
46
|
+
snapshot_digest: string;
|
|
47
|
+
scenario_sha256: string;
|
|
48
|
+
view_source_tree_sha256: string;
|
|
49
|
+
}>>;
|
|
50
|
+
/** Desktop adapter 只通过此窄端口调用已显式注入的 capability;没有 handler 即 fail-closed。 */
|
|
51
|
+
dispatchTool(call: AuthoringToolCall): Promise<unknown>;
|
|
52
|
+
readSourceReadback(): Promise<ScenarioSourceReadback>;
|
|
53
|
+
/** Node-only 完整投影核对,不扩展 exact-21,不注入 embedding/model capability。 */
|
|
54
|
+
readGraphProjectionReadback(request?: LadybugGraphSnapshotRequest): Promise<GraphProjectionReadback>;
|
|
55
|
+
writeScenario(request: ScenarioWriteRequest): Promise<AuthoringToolReceiptV1>;
|
|
56
|
+
doctor(): Promise<ProjectDoctorReport>;
|
|
57
|
+
snapshot(request: SnapshotRequest): Promise<AuthoringToolReceiptV1>;
|
|
58
|
+
close(): Promise<void>;
|
|
59
|
+
}
|
|
@@ -0,0 +1,471 @@
|
|
|
1
|
+
import { lstat, open, readFile, readdir, realpath, rename, stat, writeFile, } from "node:fs/promises";
|
|
2
|
+
import { join, resolve } from "node:path";
|
|
3
|
+
import { AuthoringBridgeError, createAuthoringBridgeDispatcher, validateAuthoringEffectIdentity, } from "@world-engines/authoring-bridge";
|
|
4
|
+
import { computeCanonicalFileTreeDigest, computeCanonicalProjectDigest, decodeScenarioPartWesp, sha256Hex, validateLocalAuthorProjectManifest, } from "@world-engines/project-format";
|
|
5
|
+
import { AGENT_KIT_ROOT_LINKS } from "@world-engines/agent-kit";
|
|
6
|
+
import { assertSafeExistingProjectPath, assertSafeProjectParent, ensureSafeProjectDirectory, safeProjectPath, } from "./safe-project-path.js";
|
|
7
|
+
import { acquireProjectWriterLease } from "./project-writer-lease.js";
|
|
8
|
+
import { ScenarioWriter, } from "./scenario-writer.js";
|
|
9
|
+
import { DesktopAuthoringRuntime } from "./desktop-authoring-runtime.js";
|
|
10
|
+
async function pathExists(path) {
|
|
11
|
+
try {
|
|
12
|
+
await stat(path);
|
|
13
|
+
return true;
|
|
14
|
+
}
|
|
15
|
+
catch (error) {
|
|
16
|
+
if (error.code === "ENOENT")
|
|
17
|
+
return false;
|
|
18
|
+
throw error;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
async function readManifest(root) {
|
|
22
|
+
try {
|
|
23
|
+
const manifestPath = await safeProjectPath(root, "worldengine.project.json");
|
|
24
|
+
return validateLocalAuthorProjectManifest(JSON.parse(await readFile(await assertSafeExistingProjectPath(root, manifestPath), "utf8")));
|
|
25
|
+
}
|
|
26
|
+
catch (error) {
|
|
27
|
+
if (error instanceof AuthoringBridgeError)
|
|
28
|
+
throw error;
|
|
29
|
+
throw new AuthoringBridgeError("E_VALIDATION_FAILED", "worldengine.project.json 无效", {
|
|
30
|
+
cause: error instanceof Error ? error.message : String(error),
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
async function listCanonicalFiles(projectRoot, contentRoot, excludedNames, directory = "") {
|
|
35
|
+
const absoluteDirectory = join(contentRoot, ...directory.split("/").filter(Boolean));
|
|
36
|
+
await assertSafeExistingProjectPath(projectRoot, absoluteDirectory);
|
|
37
|
+
const entries = await readdir(absoluteDirectory, { withFileTypes: true });
|
|
38
|
+
const files = [];
|
|
39
|
+
for (const entry of entries) {
|
|
40
|
+
if (excludedNames.has(entry.name))
|
|
41
|
+
continue;
|
|
42
|
+
const relativePath = directory === "" ? entry.name : `${directory}/${entry.name}`;
|
|
43
|
+
const absolutePath = join(absoluteDirectory, entry.name);
|
|
44
|
+
const info = await lstat(absolutePath);
|
|
45
|
+
if (info.isSymbolicLink()) {
|
|
46
|
+
throw new AuthoringBridgeError("E_PATH_OUTSIDE_PROJECT", `拒绝符号链接、junction 或 reparse point:${absolutePath}`);
|
|
47
|
+
}
|
|
48
|
+
else if (info.isDirectory()) {
|
|
49
|
+
files.push(...(await listCanonicalFiles(projectRoot, contentRoot, excludedNames, relativePath)));
|
|
50
|
+
}
|
|
51
|
+
else if (info.isFile()) {
|
|
52
|
+
const canonical = await assertSafeExistingProjectPath(projectRoot, absolutePath);
|
|
53
|
+
files.push({ path: relativePath, bytes: new Uint8Array(await readFile(canonical)) });
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return files;
|
|
57
|
+
}
|
|
58
|
+
async function fileDigestOrEmpty(path) {
|
|
59
|
+
try {
|
|
60
|
+
return sha256Hex(new Uint8Array(await readFile(path)));
|
|
61
|
+
}
|
|
62
|
+
catch (error) {
|
|
63
|
+
if (error.code === "ENOENT")
|
|
64
|
+
return sha256Hex(new Uint8Array(0));
|
|
65
|
+
throw error;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
async function computeProjectDigests(root, manifest) {
|
|
69
|
+
const scenarioPath = await assertSafeExistingProjectPath(root, join(root, "scenario", "source.wes"));
|
|
70
|
+
const scenarioBytes = new Uint8Array(await readFile(scenarioPath));
|
|
71
|
+
try {
|
|
72
|
+
await decodeScenarioPartWesp(scenarioBytes);
|
|
73
|
+
}
|
|
74
|
+
catch (error) {
|
|
75
|
+
throw new AuthoringBridgeError("E_VALIDATION_FAILED", "scenario/source.wes 无效", {
|
|
76
|
+
cause: error instanceof Error ? error.message : String(error),
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
// v2 的图片统一归 gallery,旧 assets 目录已不再是必需的作者内容。
|
|
80
|
+
const assetsRoot = manifest.schema_version === 1
|
|
81
|
+
? await assertSafeExistingProjectPath(root, join(root, "scenario", "assets"))
|
|
82
|
+
: undefined;
|
|
83
|
+
const viewRoot = await assertSafeExistingProjectPath(root, join(root, "view"));
|
|
84
|
+
const galleryRoot = manifest.schema_version === 2
|
|
85
|
+
? await assertSafeExistingProjectPath(root, join(root, "gallery"))
|
|
86
|
+
: undefined;
|
|
87
|
+
return {
|
|
88
|
+
scenario_source_sha256: await sha256Hex(scenarioBytes),
|
|
89
|
+
scenario_assets_digest: await computeCanonicalFileTreeDigest(assetsRoot === undefined ? [] : await listCanonicalFiles(root, assetsRoot, new Set())),
|
|
90
|
+
view_source_tree_sha256: await computeCanonicalFileTreeDigest(await listCanonicalFiles(root, viewRoot, new Set(["node_modules", "dist", ".vite", "package-lock.json"]))),
|
|
91
|
+
view_package_lock_sha256: await fileDigestOrEmpty(join(viewRoot, "package-lock.json")),
|
|
92
|
+
...(galleryRoot === undefined ? {} : {
|
|
93
|
+
gallery_content_tree_sha256: await computeCanonicalFileTreeDigest(await listCanonicalFiles(root, galleryRoot, new Set())),
|
|
94
|
+
}),
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
function scenarioMatches(manifest, digests) {
|
|
98
|
+
return (manifest.scenario.source_sha256 === digests.scenario_source_sha256 &&
|
|
99
|
+
manifest.scenario.assets_digest === digests.scenario_assets_digest);
|
|
100
|
+
}
|
|
101
|
+
function viewMatches(manifest, digests) {
|
|
102
|
+
return (manifest.view.source_tree_sha256 === digests.view_source_tree_sha256 &&
|
|
103
|
+
manifest.view.package_lock_sha256 === digests.view_package_lock_sha256);
|
|
104
|
+
}
|
|
105
|
+
function galleryMatches(manifest, digests) {
|
|
106
|
+
return manifest.schema_version === 1 || manifest.gallery.content_tree_sha256 === digests.gallery_content_tree_sha256;
|
|
107
|
+
}
|
|
108
|
+
async function atomicWriteJson(path, stagingRoot, value) {
|
|
109
|
+
const root = resolve(stagingRoot, "..", "..");
|
|
110
|
+
await assertSafeExistingProjectPath(root, stagingRoot);
|
|
111
|
+
const bytes = new TextEncoder().encode(`${JSON.stringify(value, null, 2)}\n`);
|
|
112
|
+
const temp = join(stagingRoot, `manifest-${await sha256Hex(bytes)}.tmp`);
|
|
113
|
+
await assertSafeProjectParent(root, temp);
|
|
114
|
+
const handle = await open(temp, "wx");
|
|
115
|
+
try {
|
|
116
|
+
await handle.writeFile(bytes);
|
|
117
|
+
await handle.sync();
|
|
118
|
+
}
|
|
119
|
+
finally {
|
|
120
|
+
await handle.close();
|
|
121
|
+
}
|
|
122
|
+
await assertSafeProjectParent(root, path);
|
|
123
|
+
await rename(temp, path);
|
|
124
|
+
await assertSafeExistingProjectPath(root, path);
|
|
125
|
+
}
|
|
126
|
+
function manifestEquals(left, right) {
|
|
127
|
+
return JSON.stringify(left) === JSON.stringify(right);
|
|
128
|
+
}
|
|
129
|
+
export class LocalProjectHost {
|
|
130
|
+
root;
|
|
131
|
+
#manifest;
|
|
132
|
+
#scenarioWriter;
|
|
133
|
+
#toolDispatcher;
|
|
134
|
+
#desktopAuthoringRuntime;
|
|
135
|
+
#desktopAuthoringRuntimeOpening;
|
|
136
|
+
#desktopDispatchTail = Promise.resolve();
|
|
137
|
+
#desktopAuthoringCapabilities;
|
|
138
|
+
#closed = false;
|
|
139
|
+
constructor(root, manifest, options) {
|
|
140
|
+
this.root = root;
|
|
141
|
+
this.#manifest = manifest;
|
|
142
|
+
this.#scenarioWriter = new ScenarioWriter(root, options.scenario_projection_hydrator, options.scenario_writer_fault_injector);
|
|
143
|
+
this.#desktopAuthoringCapabilities = options.desktop_authoring_capabilities;
|
|
144
|
+
this.#toolDispatcher = options.authoring_adapter === undefined ? undefined : createAuthoringBridgeDispatcher(options.authoring_adapter);
|
|
145
|
+
}
|
|
146
|
+
static async open(root, options = {}) {
|
|
147
|
+
const canonicalRoot = await realpath(resolve(root));
|
|
148
|
+
const manifest = await readManifest(canonicalRoot);
|
|
149
|
+
const digests = await computeProjectDigests(canonicalRoot, manifest);
|
|
150
|
+
void digests;
|
|
151
|
+
return new LocalProjectHost(canonicalRoot, manifest, options);
|
|
152
|
+
}
|
|
153
|
+
#assertOpen() {
|
|
154
|
+
if (this.#closed)
|
|
155
|
+
throw new AuthoringBridgeError("E_PROJECT_NOT_OPEN", "ProjectHost 已关闭");
|
|
156
|
+
}
|
|
157
|
+
async inspect() {
|
|
158
|
+
this.#assertOpen();
|
|
159
|
+
this.#manifest = await readManifest(this.root);
|
|
160
|
+
const digests = await computeProjectDigests(this.root, this.#manifest);
|
|
161
|
+
return {
|
|
162
|
+
project_id: this.#manifest.project_id,
|
|
163
|
+
project_revision: this.#manifest.project_revision,
|
|
164
|
+
snapshot_id: this.#manifest.snapshot.snapshot_id,
|
|
165
|
+
snapshot_digest: this.#manifest.snapshot.canonical_digest,
|
|
166
|
+
scenario_integrity: scenarioMatches(this.#manifest, digests) ? "clean" : "dirty",
|
|
167
|
+
view_state: viewMatches(this.#manifest, digests) ? "clean" : "dirty",
|
|
168
|
+
root_dependencies_installed: await pathExists(join(this.root, "node_modules")),
|
|
169
|
+
view_dependencies_installed: await pathExists(join(this.root, "view", "node_modules")),
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
/** exact-21 descriptor 始终稳定;三个外部 callback 的执行可用性由真实注入端口决定。 */
|
|
173
|
+
isToolAvailable(name) {
|
|
174
|
+
if (name === "ask_user" || name === "preview_inspect" || name === "preview_interact") {
|
|
175
|
+
return this.#desktopAuthoringCapabilities?.[name] !== undefined;
|
|
176
|
+
}
|
|
177
|
+
return true;
|
|
178
|
+
}
|
|
179
|
+
/** Download completion fence:从真实 source/view/manifest 重新计算,不消费 receipt 自报字段。 */
|
|
180
|
+
async readTransferReadback() {
|
|
181
|
+
this.#assertOpen();
|
|
182
|
+
this.#manifest = await readManifest(this.root);
|
|
183
|
+
const digests = await computeProjectDigests(this.root, this.#manifest);
|
|
184
|
+
if (!scenarioMatches(this.#manifest, digests) || !viewMatches(this.#manifest, digests))
|
|
185
|
+
throw new AuthoringBridgeError("E_OPERATION_UNCERTAIN", "ProjectHost transfer projection/digest readback 不一致");
|
|
186
|
+
return { project_revision: this.#manifest.project_revision, snapshot_digest: this.#manifest.snapshot.canonical_digest, scenario_sha256: digests.scenario_source_sha256, view_source_tree_sha256: digests.view_source_tree_sha256 };
|
|
187
|
+
}
|
|
188
|
+
/** Desktop adapter 只通过此窄端口调用已显式注入的 capability;没有 handler 即 fail-closed。 */
|
|
189
|
+
async dispatchTool(call) {
|
|
190
|
+
this.#assertOpen();
|
|
191
|
+
if (this.#toolDispatcher !== undefined && this.#desktopAuthoringRuntimeOpening === undefined) {
|
|
192
|
+
return this.#toolDispatcher.dispatch(call);
|
|
193
|
+
}
|
|
194
|
+
if (this.#desktopAuthoringRuntimeOpening === undefined) {
|
|
195
|
+
// 延迟建立 projection,使 writer 的 crash-recovery ledger 能先由相同 source writer 收敛。
|
|
196
|
+
// promise 本身也是 single-flight fence,避免并发 MCP 首帧打开两份 runtime 并泄漏一份。
|
|
197
|
+
this.#desktopAuthoringRuntimeOpening = DesktopAuthoringRuntime.open(this.root, {
|
|
198
|
+
readSourceReadback: () => this.readSourceReadback(),
|
|
199
|
+
writeScenario: (request) => this.writeScenario(request),
|
|
200
|
+
}, this.#desktopAuthoringCapabilities).then((runtime) => {
|
|
201
|
+
this.#desktopAuthoringRuntime = runtime;
|
|
202
|
+
this.#toolDispatcher = createAuthoringBridgeDispatcher(runtime.adapter);
|
|
203
|
+
return this.#toolDispatcher;
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
const dispatcher = await this.#desktopAuthoringRuntimeOpening;
|
|
207
|
+
const result = this.#desktopDispatchTail.then(() => dispatcher.dispatch(call));
|
|
208
|
+
this.#desktopDispatchTail = result.then(() => undefined, () => undefined);
|
|
209
|
+
return result;
|
|
210
|
+
}
|
|
211
|
+
async readSourceReadback() {
|
|
212
|
+
this.#assertOpen();
|
|
213
|
+
return this.#scenarioWriter.readSourceReadback();
|
|
214
|
+
}
|
|
215
|
+
/** Node-only 完整投影核对,不扩展 exact-21,不注入 embedding/model capability。 */
|
|
216
|
+
async readGraphProjectionReadback(request = {}) {
|
|
217
|
+
this.#assertOpen();
|
|
218
|
+
const result = this.#desktopDispatchTail.then(async () => {
|
|
219
|
+
const runtime = await DesktopAuthoringRuntime.open(this.root, {
|
|
220
|
+
readSourceReadback: () => this.readSourceReadback(),
|
|
221
|
+
writeScenario: (input) => this.writeScenario(input),
|
|
222
|
+
});
|
|
223
|
+
try {
|
|
224
|
+
return await runtime.readGraphProjectionReadback(request);
|
|
225
|
+
}
|
|
226
|
+
finally {
|
|
227
|
+
await runtime.close();
|
|
228
|
+
}
|
|
229
|
+
});
|
|
230
|
+
this.#desktopDispatchTail = result.then(() => undefined, () => undefined);
|
|
231
|
+
return result;
|
|
232
|
+
}
|
|
233
|
+
async writeScenario(request) {
|
|
234
|
+
this.#assertOpen();
|
|
235
|
+
const receipt = await this.#scenarioWriter.write(request);
|
|
236
|
+
this.#manifest = await readManifest(this.root);
|
|
237
|
+
return receipt;
|
|
238
|
+
}
|
|
239
|
+
async doctor() {
|
|
240
|
+
const status = await this.inspect();
|
|
241
|
+
const hardlinkChecks = await Promise.all(AGENT_KIT_ROOT_LINKS.filter(({ kind }) => kind === "hardlink").map(async (rootLink) => {
|
|
242
|
+
try {
|
|
243
|
+
const output = join(this.root, ...rootLink.path.split("/"));
|
|
244
|
+
const target = join(this.root, ...rootLink.target.split("/"));
|
|
245
|
+
return (await stat(output)).ino === (await stat(target)).ino;
|
|
246
|
+
}
|
|
247
|
+
catch {
|
|
248
|
+
return false;
|
|
249
|
+
}
|
|
250
|
+
}));
|
|
251
|
+
const junctionChecks = await Promise.all(AGENT_KIT_ROOT_LINKS.filter(({ kind }) => kind === "junction").map(async (rootLink) => {
|
|
252
|
+
try {
|
|
253
|
+
const output = join(this.root, ...rootLink.path.split("/"));
|
|
254
|
+
const target = await realpath(join(this.root, ...rootLink.target.split("/")));
|
|
255
|
+
return (await lstat(output)).isSymbolicLink() && (await realpath(output)) === target;
|
|
256
|
+
}
|
|
257
|
+
catch {
|
|
258
|
+
return false;
|
|
259
|
+
}
|
|
260
|
+
}));
|
|
261
|
+
const viewConfigPresent = await pathExists(join(this.root, "view", "package.json"));
|
|
262
|
+
const checks = [
|
|
263
|
+
{ name: "project_format", ok: status.scenario_integrity === "clean" && status.view_state === "clean", detail: status.view_state === "clean" ? "manifest/WESP/View digest 一致" : "View 尚未 snapshot" },
|
|
264
|
+
{ name: "manifest_wesp_hash", ok: status.scenario_integrity === "clean", detail: "manifest、WESP 与 scenario hash 一致" },
|
|
265
|
+
{ name: "agent_instruction_hardlinks", ok: hardlinkChecks.every(Boolean), detail: "instructions/configs/launcher 单真相源" },
|
|
266
|
+
{ name: "agent_skill_junctions", ok: junctionChecks.every(Boolean), detail: "agents/codex/claude skills 单真相源" },
|
|
267
|
+
{ name: "view_config", ok: viewConfigPresent, detail: "view package 配置存在" },
|
|
268
|
+
{ name: "root_node_modules", ok: status.root_dependencies_installed, detail: status.root_dependencies_installed ? "已安装" : "未安装" },
|
|
269
|
+
{ name: "view_node_modules", ok: status.view_dependencies_installed, detail: status.view_dependencies_installed ? "已安装" : "未安装" },
|
|
270
|
+
{ name: "projection_source_revision", ok: status.project_revision === this.#manifest.project_revision, detail: "projection 与 manifest revision 一致" },
|
|
271
|
+
];
|
|
272
|
+
return {
|
|
273
|
+
schema_version: 1,
|
|
274
|
+
project_id: status.project_id,
|
|
275
|
+
ok: checks.every(({ ok }) => ok),
|
|
276
|
+
checks,
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
async snapshot(request) {
|
|
280
|
+
this.#assertOpen();
|
|
281
|
+
validateAuthoringEffectIdentity(request);
|
|
282
|
+
const requestDigest = await sha256Hex(new TextEncoder().encode(JSON.stringify(request)));
|
|
283
|
+
const ledgerKey = await sha256Hex(new TextEncoder().encode(request.operation_id));
|
|
284
|
+
await ensureSafeProjectDirectory(this.root, ".worldengine/operations");
|
|
285
|
+
await ensureSafeProjectDirectory(this.root, ".worldengine/snapshots");
|
|
286
|
+
await ensureSafeProjectDirectory(this.root, ".worldengine/staging");
|
|
287
|
+
const ledgerPath = await safeProjectPath(this.root, `.worldengine/operations/snapshot-${ledgerKey}.json`);
|
|
288
|
+
const lease = await acquireProjectWriterLease(this.root, {
|
|
289
|
+
entrypoint: "snapshot",
|
|
290
|
+
operation_id: request.operation_id,
|
|
291
|
+
});
|
|
292
|
+
try {
|
|
293
|
+
this.#manifest = await readManifest(this.root);
|
|
294
|
+
let ledger;
|
|
295
|
+
try {
|
|
296
|
+
ledger = JSON.parse(await readFile(await assertSafeExistingProjectPath(this.root, ledgerPath), "utf8"));
|
|
297
|
+
}
|
|
298
|
+
catch (error) {
|
|
299
|
+
if (error.code !== "ENOENT")
|
|
300
|
+
throw error;
|
|
301
|
+
}
|
|
302
|
+
if (ledger !== undefined) {
|
|
303
|
+
if (ledger.request_digest !== requestDigest) {
|
|
304
|
+
throw new AuthoringBridgeError("E_OPERATION_ID_CONFLICT", "同一 snapshot operation_id 使用了不同请求");
|
|
305
|
+
}
|
|
306
|
+
// 旧版本会把没有 source 变化的请求写成 in-flight,再尝试创建一个
|
|
307
|
+
// canonical digest 相同的 snapshot 文件。该文件已经存在时会留下 only-metadata
|
|
308
|
+
// 不同的 next_manifest;当前 manifest 仍是其 preimage,因此可安全收敛为 no-op。
|
|
309
|
+
const legacyNoChangePreimage = manifestEquals(this.#manifest, ledger.preimage) &&
|
|
310
|
+
ledger.receipt.operation_id === request.operation_id &&
|
|
311
|
+
ledger.receipt.project_id === this.#manifest.project_id &&
|
|
312
|
+
ledger.receipt.previous_project_revision === this.#manifest.project_revision &&
|
|
313
|
+
ledger.receipt.next_project_revision === this.#manifest.project_revision &&
|
|
314
|
+
ledger.receipt.changed_sections.length === 0 &&
|
|
315
|
+
ledger.receipt.project_snapshot_digest === this.#manifest.snapshot.canonical_digest;
|
|
316
|
+
if (legacyNoChangePreimage) {
|
|
317
|
+
const completedLedger = {
|
|
318
|
+
...ledger,
|
|
319
|
+
state: "completed",
|
|
320
|
+
preimage: this.#manifest,
|
|
321
|
+
next_manifest: this.#manifest,
|
|
322
|
+
};
|
|
323
|
+
await atomicWriteJson(ledgerPath, join(this.root, ".worldengine", "staging"), completedLedger);
|
|
324
|
+
return completedLedger.receipt;
|
|
325
|
+
}
|
|
326
|
+
if (!manifestEquals(this.#manifest, ledger.preimage) && !manifestEquals(this.#manifest, ledger.next_manifest)) {
|
|
327
|
+
throw new AuthoringBridgeError("E_OPERATION_UNCERTAIN", "snapshot in-flight ledger 与当前 manifest 不一致");
|
|
328
|
+
}
|
|
329
|
+
if (manifestEquals(this.#manifest, ledger.next_manifest)) {
|
|
330
|
+
if (ledger.state !== "completed") {
|
|
331
|
+
await atomicWriteJson(ledgerPath, join(this.root, ".worldengine", "staging"), { ...ledger, state: "completed" });
|
|
332
|
+
}
|
|
333
|
+
this.#manifest = ledger.next_manifest;
|
|
334
|
+
return ledger.receipt;
|
|
335
|
+
}
|
|
336
|
+
const snapshotPath = await safeProjectPath(this.root, `.worldengine/snapshots/${ledger.next_manifest.snapshot.snapshot_id}.json`);
|
|
337
|
+
try {
|
|
338
|
+
const snapshot = JSON.parse(await readFile(await assertSafeExistingProjectPath(this.root, snapshotPath), "utf8"));
|
|
339
|
+
if (!manifestEquals(snapshot, ledger.next_manifest)) {
|
|
340
|
+
throw new AuthoringBridgeError("E_OPERATION_UNCERTAIN", "snapshot preimage 与 in-flight ledger 不一致");
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
catch (error) {
|
|
344
|
+
if (error.code !== "ENOENT")
|
|
345
|
+
throw error;
|
|
346
|
+
await assertSafeProjectParent(this.root, snapshotPath);
|
|
347
|
+
await writeFile(snapshotPath, `${JSON.stringify(ledger.next_manifest, null, 2)}\n`, { encoding: "utf8", flag: "wx" });
|
|
348
|
+
await assertSafeExistingProjectPath(this.root, snapshotPath);
|
|
349
|
+
}
|
|
350
|
+
await atomicWriteJson(join(this.root, "worldengine.project.json"), join(this.root, ".worldengine", "staging"), ledger.next_manifest);
|
|
351
|
+
this.#manifest = await readManifest(this.root);
|
|
352
|
+
if (!manifestEquals(this.#manifest, ledger.next_manifest)) {
|
|
353
|
+
throw new AuthoringBridgeError("E_OPERATION_UNCERTAIN", "snapshot manifest reconcile readback 不一致");
|
|
354
|
+
}
|
|
355
|
+
await atomicWriteJson(ledgerPath, join(this.root, ".worldengine", "staging"), { ...ledger, state: "completed" });
|
|
356
|
+
return ledger.receipt;
|
|
357
|
+
}
|
|
358
|
+
if (this.#manifest.project_revision !== request.expected_project_revision) {
|
|
359
|
+
throw new AuthoringBridgeError("E_STALE_PROJECT_REVISION", `期望 revision ${request.expected_project_revision},当前 ${this.#manifest.project_revision}`);
|
|
360
|
+
}
|
|
361
|
+
const digests = await computeProjectDigests(this.root, this.#manifest);
|
|
362
|
+
const changedSections = [];
|
|
363
|
+
if (this.#manifest.scenario.source_sha256 !== digests.scenario_source_sha256 ||
|
|
364
|
+
this.#manifest.scenario.assets_digest !== digests.scenario_assets_digest)
|
|
365
|
+
changedSections.push("scenario");
|
|
366
|
+
if (!viewMatches(this.#manifest, digests))
|
|
367
|
+
changedSections.push("view");
|
|
368
|
+
if (!galleryMatches(this.#manifest, digests))
|
|
369
|
+
changedSections.push("gallery");
|
|
370
|
+
const previousRevision = this.#manifest.project_revision;
|
|
371
|
+
if (changedSections.length === 0) {
|
|
372
|
+
const receipt = {
|
|
373
|
+
schema_version: 1,
|
|
374
|
+
operation_id: request.operation_id,
|
|
375
|
+
project_id: this.#manifest.project_id,
|
|
376
|
+
previous_project_revision: previousRevision,
|
|
377
|
+
next_project_revision: previousRevision,
|
|
378
|
+
changed_sections: [],
|
|
379
|
+
project_snapshot_digest: this.#manifest.snapshot.canonical_digest,
|
|
380
|
+
readback_verified: true,
|
|
381
|
+
};
|
|
382
|
+
ledger = {
|
|
383
|
+
schema_version: 1,
|
|
384
|
+
request_digest: requestDigest,
|
|
385
|
+
state: "in_flight",
|
|
386
|
+
preimage: this.#manifest,
|
|
387
|
+
next_manifest: this.#manifest,
|
|
388
|
+
receipt,
|
|
389
|
+
};
|
|
390
|
+
await assertSafeProjectParent(this.root, ledgerPath);
|
|
391
|
+
await writeFile(ledgerPath, `${JSON.stringify(ledger, null, 2)}\n`, { encoding: "utf8", flag: "wx" });
|
|
392
|
+
await assertSafeExistingProjectPath(this.root, ledgerPath);
|
|
393
|
+
await atomicWriteJson(ledgerPath, join(this.root, ".worldengine", "staging"), { ...ledger, state: "completed" });
|
|
394
|
+
return receipt;
|
|
395
|
+
}
|
|
396
|
+
const nextRevision = changedSections.length === 0 ? previousRevision : previousRevision + 1;
|
|
397
|
+
const next = {
|
|
398
|
+
...this.#manifest,
|
|
399
|
+
project_revision: nextRevision,
|
|
400
|
+
updated_at_ms: Date.now(),
|
|
401
|
+
scenario: {
|
|
402
|
+
...this.#manifest.scenario,
|
|
403
|
+
source_sha256: digests.scenario_source_sha256,
|
|
404
|
+
assets_digest: digests.scenario_assets_digest,
|
|
405
|
+
},
|
|
406
|
+
view: {
|
|
407
|
+
...this.#manifest.view,
|
|
408
|
+
source_tree_sha256: digests.view_source_tree_sha256,
|
|
409
|
+
package_lock_sha256: digests.view_package_lock_sha256,
|
|
410
|
+
},
|
|
411
|
+
...(this.#manifest.schema_version === 1 ? {} : {
|
|
412
|
+
gallery: {
|
|
413
|
+
...this.#manifest.gallery,
|
|
414
|
+
content_tree_sha256: digests.gallery_content_tree_sha256,
|
|
415
|
+
},
|
|
416
|
+
}),
|
|
417
|
+
snapshot: {
|
|
418
|
+
snapshot_id: "snapshot_pending",
|
|
419
|
+
canonical_digest: "0".repeat(64),
|
|
420
|
+
previous_snapshot_id: this.#manifest.snapshot.snapshot_id,
|
|
421
|
+
},
|
|
422
|
+
};
|
|
423
|
+
next.snapshot.canonical_digest = await computeCanonicalProjectDigest(next);
|
|
424
|
+
next.snapshot.snapshot_id = `snapshot_${next.snapshot.canonical_digest.slice(0, 24)}`;
|
|
425
|
+
const snapshotPath = join(this.root, ".worldengine", "snapshots", `${next.snapshot.snapshot_id}.json`);
|
|
426
|
+
const receipt = {
|
|
427
|
+
schema_version: 1,
|
|
428
|
+
operation_id: request.operation_id,
|
|
429
|
+
project_id: next.project_id,
|
|
430
|
+
previous_project_revision: previousRevision,
|
|
431
|
+
next_project_revision: nextRevision,
|
|
432
|
+
changed_sections: changedSections,
|
|
433
|
+
project_snapshot_digest: next.snapshot.canonical_digest,
|
|
434
|
+
readback_verified: true,
|
|
435
|
+
};
|
|
436
|
+
ledger = {
|
|
437
|
+
schema_version: 1,
|
|
438
|
+
request_digest: requestDigest,
|
|
439
|
+
state: "in_flight",
|
|
440
|
+
preimage: this.#manifest,
|
|
441
|
+
next_manifest: next,
|
|
442
|
+
receipt,
|
|
443
|
+
};
|
|
444
|
+
await assertSafeProjectParent(this.root, ledgerPath);
|
|
445
|
+
await writeFile(ledgerPath, `${JSON.stringify(ledger, null, 2)}\n`, { encoding: "utf8", flag: "wx" });
|
|
446
|
+
await assertSafeExistingProjectPath(this.root, ledgerPath);
|
|
447
|
+
await assertSafeProjectParent(this.root, snapshotPath);
|
|
448
|
+
await writeFile(snapshotPath, `${JSON.stringify(next, null, 2)}\n`, { encoding: "utf8", flag: "wx" });
|
|
449
|
+
await assertSafeExistingProjectPath(this.root, snapshotPath);
|
|
450
|
+
await atomicWriteJson(join(this.root, "worldengine.project.json"), join(this.root, ".worldengine", "staging"), next);
|
|
451
|
+
const readback = await readManifest(this.root);
|
|
452
|
+
const readbackVerified = readback.project_revision === nextRevision &&
|
|
453
|
+
readback.snapshot.canonical_digest === next.snapshot.canonical_digest;
|
|
454
|
+
if (!readbackVerified) {
|
|
455
|
+
throw new AuthoringBridgeError("E_OPERATION_UNCERTAIN", "manifest atomic write readback 不一致");
|
|
456
|
+
}
|
|
457
|
+
await atomicWriteJson(ledgerPath, join(this.root, ".worldengine", "staging"), { ...ledger, state: "completed" });
|
|
458
|
+
this.#manifest = next;
|
|
459
|
+
return receipt;
|
|
460
|
+
}
|
|
461
|
+
finally {
|
|
462
|
+
await lease.release();
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
async close() {
|
|
466
|
+
await this.#desktopDispatchTail;
|
|
467
|
+
await this.#desktopAuthoringRuntimeOpening?.catch(() => undefined);
|
|
468
|
+
await this.#desktopAuthoringRuntime?.close();
|
|
469
|
+
this.#closed = true;
|
|
470
|
+
}
|
|
471
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export interface ProjectWriterLeaseOwner {
|
|
2
|
+
readonly [key: string]: unknown;
|
|
3
|
+
}
|
|
4
|
+
export interface ProjectWriterLease {
|
|
5
|
+
readonly path: string;
|
|
6
|
+
readonly leaseId: string;
|
|
7
|
+
/** Windows 使用 volume/file-index;POSIX 使用 dev/inode。 */
|
|
8
|
+
readonly fileIdentity: string;
|
|
9
|
+
release(): Promise<void>;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* 获取 canonical project root 下的唯一 writer lease。
|
|
13
|
+
*
|
|
14
|
+
* 普通 acquire 绝不猜测或删除 stale lock;崩溃恢复必须先由独立的 owner/ledger
|
|
15
|
+
* 协议验证。Windows helper 持有不共享 DELETE 的 handle,并用同一 file identity
|
|
16
|
+
* 的 duplicate handle 完成删除,避免 path-based cleanup 误删后来者。
|
|
17
|
+
*/
|
|
18
|
+
export declare function acquireProjectWriterLease(root: string, owner?: ProjectWriterLeaseOwner): Promise<ProjectWriterLease>;
|