@senguoyun/dsh-arkme 0.1.31 → 0.1.33
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/README.md +24 -0
- package/lib/client.js +2459 -2307
- package/lib/client.js.map +1 -1
- package/lib/extension-profile-restart-helper.js +17 -5
- package/lib/index.js +3911 -104
- package/lib/persistent-extension.js +1 -1
- package/lib/{persistent-runtime-BJ8konXI.js → persistent-runtime-DUqdJv8K.js} +30114 -768
- package/lib/sdk.js +38 -0
- package/lib/types/arkme-service.d.ts +25 -10
- package/lib/types/arkme-service.d.ts.map +1 -1
- package/lib/types/client/ArkmeBillingSettings.d.ts.map +1 -1
- package/lib/types/client/ArkmeBotConversationSurface.d.ts.map +1 -1
- package/lib/types/client/ArkmeBotSettingsPanel.d.ts.map +1 -1
- package/lib/types/client/ArkmeExtensionRecoveryNotice.d.ts +9 -0
- package/lib/types/client/ArkmeExtensionRecoveryNotice.d.ts.map +1 -0
- package/lib/types/client/ArkmeFooterAction.d.ts +1 -7
- package/lib/types/client/ArkmeFooterAction.d.ts.map +1 -1
- package/lib/types/client/ArkmeFooterDropdown.d.ts.map +1 -1
- package/lib/types/client/ArkmeGroupChatControls.d.ts +17 -1
- package/lib/types/client/ArkmeGroupChatControls.d.ts.map +1 -1
- package/lib/types/client/ArkmeLinkText.d.ts +13 -0
- package/lib/types/client/ArkmeLinkText.d.ts.map +1 -0
- package/lib/types/client/ArkmeLongArticleDialog.d.ts.map +1 -1
- package/lib/types/client/ArkmeMarketplace.d.ts +2 -1
- package/lib/types/client/ArkmeMarketplace.d.ts.map +1 -1
- package/lib/types/client/ArkmePersistentShell.d.ts.map +1 -1
- package/lib/types/client/ArkmeProductNavigation.d.ts.map +1 -1
- package/lib/types/client/ArkmeQuickAdd.d.ts.map +1 -1
- package/lib/types/client/ArkmeRichContent.d.ts +4 -21
- package/lib/types/client/ArkmeRichContent.d.ts.map +1 -1
- package/lib/types/client/ArkmeRichText.d.ts +12 -0
- package/lib/types/client/ArkmeRichText.d.ts.map +1 -0
- package/lib/types/client/ArkmeSearchSurface.d.ts.map +1 -1
- package/lib/types/client/ArkmeSettingsSurface.d.ts +4 -19
- package/lib/types/client/ArkmeSettingsSurface.d.ts.map +1 -1
- package/lib/types/client/ArkmeSidebar.d.ts.map +1 -1
- package/lib/types/client/ArkmeUpdateSurfaces.d.ts +38 -5
- package/lib/types/client/ArkmeUpdateSurfaces.d.ts.map +1 -1
- package/lib/types/client/ArkmeVirtualWorkspace.d.ts.map +1 -1
- package/lib/types/client/api.d.ts +1 -1
- package/lib/types/client/api.d.ts.map +1 -1
- package/lib/types/client/bot-chat-directory-projection.d.ts +7 -0
- package/lib/types/client/bot-chat-directory-projection.d.ts.map +1 -0
- package/lib/types/client/desktop-quarantine-status.d.ts +3 -0
- package/lib/types/client/desktop-quarantine-status.d.ts.map +1 -0
- package/lib/types/client/index.d.ts +0 -2
- package/lib/types/client/index.d.ts.map +1 -1
- package/lib/types/client/link-metadata-client.d.ts +20 -0
- package/lib/types/client/link-metadata-client.d.ts.map +1 -0
- package/lib/types/client/realtime-client-events.d.ts +4 -1
- package/lib/types/client/realtime-client-events.d.ts.map +1 -1
- package/lib/types/client/redesign/contacts/AlphabeticalContactList.d.ts +3 -3
- package/lib/types/client/redesign/contacts/AlphabeticalContactList.d.ts.map +1 -1
- package/lib/types/client/redesign/contacts/ContactDirectorySurface.d.ts +3 -3
- package/lib/types/client/redesign/contacts/ContactDirectorySurface.d.ts.map +1 -1
- package/lib/types/client/related-recordings.d.ts +3 -0
- package/lib/types/client/related-recordings.d.ts.map +1 -1
- package/lib/types/client/text-link-parser.d.ts +10 -0
- package/lib/types/client/text-link-parser.d.ts.map +1 -0
- package/lib/types/client/text-link-tlds.d.ts +2 -0
- package/lib/types/client/text-link-tlds.d.ts.map +1 -0
- package/lib/types/client/ui-controller.d.ts.map +1 -1
- package/lib/types/client/update-presentation.d.ts +1 -1
- package/lib/types/client/update-presentation.d.ts.map +1 -1
- package/lib/types/client/update-ui-controller.d.ts +1 -1
- package/lib/types/client/update-ui-controller.d.ts.map +1 -1
- package/lib/types/dsh-host-capabilities.d.ts +10 -0
- package/lib/types/dsh-host-capabilities.d.ts.map +1 -0
- package/lib/types/dsh-remote/api-proxy-adapter.d.ts +250 -0
- package/lib/types/dsh-remote/api-proxy-adapter.d.ts.map +1 -0
- package/lib/types/dsh-remote/channel-manager.d.ts +45 -0
- package/lib/types/dsh-remote/channel-manager.d.ts.map +1 -0
- package/lib/types/dsh-remote/command-ledger.d.ts +60 -0
- package/lib/types/dsh-remote/command-ledger.d.ts.map +1 -0
- package/lib/types/dsh-remote/control-plane.d.ts +17 -0
- package/lib/types/dsh-remote/control-plane.d.ts.map +1 -0
- package/lib/types/dsh-remote/crypto.d.ts +11 -0
- package/lib/types/dsh-remote/crypto.d.ts.map +1 -0
- package/lib/types/dsh-remote/default-socket-factory.d.ts +8 -0
- package/lib/types/dsh-remote/default-socket-factory.d.ts.map +1 -0
- package/lib/types/dsh-remote/dsh-event-contract.d.ts +29 -0
- package/lib/types/dsh-remote/dsh-event-contract.d.ts.map +1 -0
- package/lib/types/dsh-remote/errors.d.ts +9 -0
- package/lib/types/dsh-remote/errors.d.ts.map +1 -0
- package/lib/types/dsh-remote/host.d.ts +85 -0
- package/lib/types/dsh-remote/host.d.ts.map +1 -0
- package/lib/types/dsh-remote/protocol-v1.d.ts +6 -0
- package/lib/types/dsh-remote/protocol-v1.d.ts.map +1 -0
- package/lib/types/dsh-remote/realtime-transport.d.ts +84 -0
- package/lib/types/dsh-remote/realtime-transport.d.ts.map +1 -0
- package/lib/types/dsh-remote/runtime-secret-broker.d.ts +28 -0
- package/lib/types/dsh-remote/runtime-secret-broker.d.ts.map +1 -0
- package/lib/types/dsh-remote/runtime-store.d.ts +39 -0
- package/lib/types/dsh-remote/runtime-store.d.ts.map +1 -0
- package/lib/types/dsh-remote/transport-fragment.d.ts +10 -0
- package/lib/types/dsh-remote/transport-fragment.d.ts.map +1 -0
- package/lib/types/dsh-remote/types.d.ts +164 -0
- package/lib/types/dsh-remote/types.d.ts.map +1 -0
- package/lib/types/extensions/desktop-quarantine.d.ts +79 -0
- package/lib/types/extensions/desktop-quarantine.d.ts.map +1 -0
- package/lib/types/extensions/profile-installer.d.ts +5 -0
- package/lib/types/extensions/profile-installer.d.ts.map +1 -1
- package/lib/types/extensions/profile-restart-helper.d.ts +3 -1
- package/lib/types/extensions/profile-restart-helper.d.ts.map +1 -1
- package/lib/types/harness-embed-route.d.ts +11 -1
- package/lib/types/harness-embed-route.d.ts.map +1 -1
- package/lib/types/host-api.d.ts +5 -1
- package/lib/types/host-api.d.ts.map +1 -1
- package/lib/types/index.d.ts +15 -0
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/keychain-store.d.ts +33 -0
- package/lib/types/keychain-store.d.ts.map +1 -1
- package/lib/types/link-metadata.d.ts +9 -0
- package/lib/types/link-metadata.d.ts.map +1 -0
- package/lib/types/managed-ai/adapter.d.ts +2 -0
- package/lib/types/managed-ai/adapter.d.ts.map +1 -1
- package/lib/types/sdk/index.d.ts +9 -2
- package/lib/types/sdk/index.d.ts.map +1 -1
- package/lib/types/services/bot-conversation-service.d.ts +59 -0
- package/lib/types/services/bot-conversation-service.d.ts.map +1 -0
- package/lib/types/services/bot-service.d.ts +11 -29
- package/lib/types/services/bot-service.d.ts.map +1 -1
- package/lib/types/services/chat-service.d.ts +30 -5
- package/lib/types/services/chat-service.d.ts.map +1 -1
- package/lib/types/services/contact-directory-service.d.ts.map +1 -1
- package/lib/types/services/link-metadata-service.d.ts +73 -0
- package/lib/types/services/link-metadata-service.d.ts.map +1 -0
- package/lib/types/services/related-recording-service.d.ts +1 -0
- package/lib/types/services/related-recording-service.d.ts.map +1 -1
- package/lib/types/services/service.d.ts +4 -2
- package/lib/types/services/service.d.ts.map +1 -1
- package/lib/types/tools/ports/bots.d.ts +1 -1
- package/lib/types/tools/ports/bots.d.ts.map +1 -1
- package/lib/types/types.d.ts +55 -21
- package/lib/types/types.d.ts.map +1 -1
- package/package.json +24 -23
package/lib/index.js
CHANGED
|
@@ -1,17 +1,18 @@
|
|
|
1
1
|
import { a as securePrivateDirectory, c as securePrivateFileSync, i as prepareProfilePackageManager, n as isArkmeProfilePackageName, o as securePrivateDirectorySync, r as localExtensionPnpmArgs, s as securePrivateFile, t as applyArkmeProfileBundlePolicy } from "./profile-bundle-policy-CvojZuTD.js";
|
|
2
|
-
import { $ as ARKME_RUNTIME_INSTANCE_ID, A as inspectNativeBundleArtifactV3, D as bundleSha256, E as arkmeSandboxEntryId, F as assertExtensionArtifactSize, G as validateVoiceprintPcm16Wav, I as canonicalExtensionJson, K as ARKME_DEFAULT_SHARE_WEBSITE, L as sha256Hex, M as packLocalNativeBundleDirectoryV3, N as readNativeBundleTarballV3, O as canonicalBundleJson, P as renderArkmeSandboxHostEntry, Q as ARKME_WORLD_PUBLISH_MAX_IMAGE_BYTES, R as unpackArkmeExtension, S as ARKME_BUNDLE_ARTIFACT_KIND, T as ArkmeBundleArtifactError, U as ARKME_EXTENSION_RUNTIME_UNAVAILABLE_MESSAGE, W as ArkmeService, Z as ARKME_WORLD_PUBLISH_MAX_IMAGES, _ as arkmeClientContentDigest, at as ArkmePluginError, b as verifyExtensionResolutionSignature, c as persistentExtensionBundleDirectory, d as readPersistentExtensionActivation, et as ARKME_TOOL_FILE_MAX_BYTES, f as refreshPersistentClientWrapper, g as activeProfileExtensionOwnerConflicts, h as renderArkmeBundleClientBundle, i as invokePersistentArkmeExtension, it as isArkmeBotAvatarRef, j as packBundleFiles, k as inspectBundleArtifact, l as persistentExtensionPackageIdentity, m as writePersistentExtensionActivation, nt as arkmeNormalizedFileMimeType, o as persistentArkmeExtensionRuntimeState, ot as ArkmeOutgoingCallError, p as repairPersistentExtensionInstallation, q as ARKME_PROVIDER_CONTRACT_VERSION, r as deactivatePersistentArkmeExtension, rt as arkmePickedFileKind, s as materializePersistentExtensionBundle, t as activatePersistentArkmeExtension, tt as arkmeCanInlineLocalFile, u as quarantinePersistentExtension, v as arkmeClientContentDigestFromSource, w as ARKME_NATIVE_BUNDLE_ARTIFACT_KIND, x as verifyNativeBundleResolutionSignature, y as verifyBundleResolutionSignature, z as ARKME_EXTENSION_FORMAT } from "./persistent-runtime-
|
|
2
|
+
import { $ as ARKME_RUNTIME_INSTANCE_ID, A as inspectNativeBundleArtifactV3, D as bundleSha256, E as arkmeSandboxEntryId, F as assertExtensionArtifactSize, G as validateVoiceprintPcm16Wav, I as canonicalExtensionJson, K as ARKME_DEFAULT_SHARE_WEBSITE, L as sha256Hex, M as packLocalNativeBundleDirectoryV3, N as readNativeBundleTarballV3, O as canonicalBundleJson, P as renderArkmeSandboxHostEntry, Q as ARKME_WORLD_PUBLISH_MAX_IMAGE_BYTES, R as unpackArkmeExtension, S as ARKME_BUNDLE_ARTIFACT_KIND, T as ArkmeBundleArtifactError, U as ARKME_EXTENSION_RUNTIME_UNAVAILABLE_MESSAGE, W as ArkmeService, Z as ARKME_WORLD_PUBLISH_MAX_IMAGES, _ as arkmeClientContentDigest, at as ArkmePluginError, b as verifyExtensionResolutionSignature, c as persistentExtensionBundleDirectory, d as readPersistentExtensionActivation, et as ARKME_TOOL_FILE_MAX_BYTES, f as refreshPersistentClientWrapper, g as activeProfileExtensionOwnerConflicts, h as renderArkmeBundleClientBundle, i as invokePersistentArkmeExtension, it as isArkmeBotAvatarRef, j as packBundleFiles, k as inspectBundleArtifact, l as persistentExtensionPackageIdentity, m as writePersistentExtensionActivation, nt as arkmeNormalizedFileMimeType, o as persistentArkmeExtensionRuntimeState, ot as ArkmeOutgoingCallError, p as repairPersistentExtensionInstallation, q as ARKME_PROVIDER_CONTRACT_VERSION, r as deactivatePersistentArkmeExtension, rt as arkmePickedFileKind, s as materializePersistentExtensionBundle, t as activatePersistentArkmeExtension, tt as arkmeCanInlineLocalFile, u as quarantinePersistentExtension, v as arkmeClientContentDigestFromSource, w as ARKME_NATIVE_BUNDLE_ARTIFACT_KIND, x as verifyNativeBundleResolutionSignature, y as verifyBundleResolutionSignature, z as ARKME_EXTENSION_FORMAT } from "./persistent-runtime-DUqdJv8K.js";
|
|
3
3
|
import { _ as parsePluginUpdateManifest, b as validatePluginUpdateServiceOrigin, d as writePluginUpdateLifecycleLog, f as PluginUpdateInstallStateStore, g as existingCachedPluginArtifactPath, h as downloadAndCachePluginArtifact, m as PluginUpdateArtifactError, n as buildTargetInstallArgs, o as reconcilePluginUpdaterProfilePolicy, p as ARKME_PLUGIN_PACKAGE_NAME$1, t as assertTargetArtifactIntegrity, u as detachManagedProfilePluginLink, v as pluginUpdateEndpointUrl, y as validatePluginUpdateArtifactOrigin } from "./plugin-updater-helper-BByw0ZyG.js";
|
|
4
4
|
import { i as invokeArkmeBundle, n as arkmeBundleActive, r as deactivateArkmeBundle } from "./bundle-runtime-BTzRlc6V.js";
|
|
5
5
|
import { t as ArkmeExtensionInstallStore } from "./install-store-C1tAM346.js";
|
|
6
|
-
import { homedir } from "node:os";
|
|
6
|
+
import { homedir, hostname } from "node:os";
|
|
7
7
|
import { chmodSync, closeSync, createReadStream, existsSync, mkdirSync, openSync, readFileSync, realpathSync, renameSync, rmSync, rmdirSync, statSync, writeFileSync } from "node:fs";
|
|
8
|
-
import { createHash, randomBytes, randomUUID } from "node:crypto";
|
|
8
|
+
import { createCipheriv, createDecipheriv, createHash, randomBytes, randomUUID } from "node:crypto";
|
|
9
9
|
import { dirname, isAbsolute, join, relative, resolve, sep, win32 } from "node:path";
|
|
10
10
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
11
11
|
import Schema from "@deepseek-ai/schemastery";
|
|
12
12
|
import { execFile, spawn } from "node:child_process";
|
|
13
|
-
import { chmod as chmod$1, mkdir, open, readFile, realpath, rename, stat, unlink, writeFile } from "node:fs/promises";
|
|
13
|
+
import { chmod as chmod$1, mkdir, open, readFile, readdir, realpath, rename, rm, stat, unlink, writeFile } from "node:fs/promises";
|
|
14
14
|
import { promisify } from "node:util";
|
|
15
|
+
import { createServer } from "node:http";
|
|
15
16
|
import { Readable } from "node:stream";
|
|
16
17
|
import semver from "semver";
|
|
17
18
|
import { defineTool } from "@deepseek-ai/dsh-tools";
|
|
@@ -21,9 +22,9 @@ import { getOrCreateAnonymousUserId } from "@deepseek-ai/dsh-anonymous-user-id";
|
|
|
21
22
|
import { LlmAdapter, LlmError, ProviderRequestId, createUserMessage } from "@deepseek-ai/dsh-llm";
|
|
22
23
|
import { DeepSeekAdapter, resolveAdapterOptions } from "@deepseek-ai/dsh-llm-deepseek";
|
|
23
24
|
import { Buffer as Buffer$1 } from "node:buffer";
|
|
24
|
-
import { createServer } from "node:http";
|
|
25
25
|
import sharp from "sharp";
|
|
26
26
|
import { AttachmentError, AttachmentId } from "@deepseek-ai/dsh-attachment";
|
|
27
|
+
import WebSocket from "ws";
|
|
27
28
|
//#region src/openclaw/cli-adapter.ts
|
|
28
29
|
const JOTMO_CHANNEL_PACKAGE = "@jotmo/openclaw-channel@0.1.12";
|
|
29
30
|
function assertSucceeded(result, operation) {
|
|
@@ -503,7 +504,7 @@ function registerDSHAgentInputRecordSync(ctx, service, options = {}) {
|
|
|
503
504
|
const PLUGIN_UPDATE_TERMINAL_STATE_TTL_MS = 10 * 6e4;
|
|
504
505
|
//#endregion
|
|
505
506
|
//#region src/plugin-update-state.ts
|
|
506
|
-
function emptyState$
|
|
507
|
+
function emptyState$2() {
|
|
507
508
|
return {
|
|
508
509
|
version: 1,
|
|
509
510
|
consecutiveFailures: 0
|
|
@@ -555,7 +556,7 @@ function normalizedNotice(value) {
|
|
|
555
556
|
}
|
|
556
557
|
function parseState$1(raw) {
|
|
557
558
|
const value = JSON.parse(raw);
|
|
558
|
-
if (value === null || typeof value !== "object") return emptyState$
|
|
559
|
+
if (value === null || typeof value !== "object") return emptyState$2();
|
|
559
560
|
const source = value;
|
|
560
561
|
const lastCheckedAtMillis = positiveNumber$1(source.lastCheckedAtMillis);
|
|
561
562
|
const lastSuccessfulCheckAtMillis = positiveNumber$1(source.lastSuccessfulCheckAtMillis);
|
|
@@ -590,14 +591,14 @@ var PluginUpdateStateStore = class {
|
|
|
590
591
|
this.onRecover = options.onRecover;
|
|
591
592
|
}
|
|
592
593
|
async snapshot() {
|
|
593
|
-
let result = emptyState$
|
|
594
|
+
let result = emptyState$2();
|
|
594
595
|
await this.serial(async () => {
|
|
595
596
|
result = cloneState(await this.load());
|
|
596
597
|
});
|
|
597
598
|
return result;
|
|
598
599
|
}
|
|
599
600
|
async update(mutator) {
|
|
600
|
-
let result = emptyState$
|
|
601
|
+
let result = emptyState$2();
|
|
601
602
|
await this.serial(async () => {
|
|
602
603
|
const state = await this.load();
|
|
603
604
|
mutator(state);
|
|
@@ -617,7 +618,7 @@ var PluginUpdateStateStore = class {
|
|
|
617
618
|
this.state = parseState$1(await readFile(this.path, "utf8"));
|
|
618
619
|
} catch (error) {
|
|
619
620
|
if (error.code !== "ENOENT") this.onRecover?.(error);
|
|
620
|
-
this.state = emptyState$
|
|
621
|
+
this.state = emptyState$2();
|
|
621
622
|
await this.write(this.state);
|
|
622
623
|
}
|
|
623
624
|
return this.state;
|
|
@@ -1393,6 +1394,69 @@ function effectiveExtensionPublisherRole(item) {
|
|
|
1393
1394
|
return item.source?.type === "github_repository" ? "importer" : "author";
|
|
1394
1395
|
}
|
|
1395
1396
|
//#endregion
|
|
1397
|
+
//#region src/dsh-remote/errors.ts
|
|
1398
|
+
var DshRemoteError = class extends Error {
|
|
1399
|
+
code;
|
|
1400
|
+
retryable;
|
|
1401
|
+
details;
|
|
1402
|
+
constructor(code, message, retryable = false, details = {}, options) {
|
|
1403
|
+
super(message, options);
|
|
1404
|
+
this.code = code;
|
|
1405
|
+
this.retryable = retryable;
|
|
1406
|
+
this.details = details;
|
|
1407
|
+
this.name = "DshRemoteError";
|
|
1408
|
+
}
|
|
1409
|
+
};
|
|
1410
|
+
function asDshRemoteError(error) {
|
|
1411
|
+
if (error instanceof DshRemoteError) return error;
|
|
1412
|
+
return new DshRemoteError("REMOTE_TRANSPORT_FAILED", error instanceof Error && error.message.trim() !== "" ? error.message : "远程连接暂时不可用", true, {}, { cause: error });
|
|
1413
|
+
}
|
|
1414
|
+
//#endregion
|
|
1415
|
+
//#region src/link-metadata.ts
|
|
1416
|
+
function linkUrl(rawUrl) {
|
|
1417
|
+
return rawUrl instanceof URL ? new URL(rawUrl.href) : new URL(rawUrl);
|
|
1418
|
+
}
|
|
1419
|
+
function arkmeKnownLinkMetadataFallback(rawUrl) {
|
|
1420
|
+
try {
|
|
1421
|
+
const url = linkUrl(rawUrl);
|
|
1422
|
+
const host = url.hostname.replace(/^www\./iu, "").toLowerCase();
|
|
1423
|
+
const parts = url.pathname.split("/").filter(Boolean);
|
|
1424
|
+
if (host === "github.com") {
|
|
1425
|
+
if (parts.length >= 4 && parts[2] === "pull") return {
|
|
1426
|
+
url: url.href,
|
|
1427
|
+
title: `Pull Request #${parts[3]} · ${parts[0]}/${parts[1]}`
|
|
1428
|
+
};
|
|
1429
|
+
if (parts.length >= 2) return {
|
|
1430
|
+
url: url.href,
|
|
1431
|
+
title: `${parts[0]}/${parts[1]}`
|
|
1432
|
+
};
|
|
1433
|
+
}
|
|
1434
|
+
if ((host === "codeup.aliyun.com" || host.endsWith(".codeup.aliyun.com")) && parts.includes("change")) {
|
|
1435
|
+
const changeIndex = parts.indexOf("change");
|
|
1436
|
+
const repository = changeIndex >= 2 ? parts[changeIndex - 1] : void 0;
|
|
1437
|
+
const changeNo = parts[changeIndex + 1];
|
|
1438
|
+
if (repository !== void 0 && changeNo !== void 0) return {
|
|
1439
|
+
url: url.href,
|
|
1440
|
+
title: `${repository} · Change #${changeNo}`
|
|
1441
|
+
};
|
|
1442
|
+
}
|
|
1443
|
+
} catch {}
|
|
1444
|
+
return null;
|
|
1445
|
+
}
|
|
1446
|
+
function arkmeRequiredLinkMetadataFallback(rawUrl) {
|
|
1447
|
+
const url = linkUrl(rawUrl);
|
|
1448
|
+
const siteName = url.hostname.replace(/^www\./iu, "");
|
|
1449
|
+
const known = arkmeKnownLinkMetadataFallback(url);
|
|
1450
|
+
return known === null ? {
|
|
1451
|
+
url: url.href,
|
|
1452
|
+
title: "分享链接",
|
|
1453
|
+
siteName
|
|
1454
|
+
} : {
|
|
1455
|
+
...known,
|
|
1456
|
+
siteName
|
|
1457
|
+
};
|
|
1458
|
+
}
|
|
1459
|
+
//#endregion
|
|
1396
1460
|
//#region src/host-api.ts
|
|
1397
1461
|
const MAX_REQUEST_BYTES = 128 * 1024;
|
|
1398
1462
|
function isLoopback$5(address) {
|
|
@@ -1809,6 +1873,7 @@ function createArkmeHostApi(service, options) {
|
|
|
1809
1873
|
}
|
|
1810
1874
|
const request = await readRequest(req);
|
|
1811
1875
|
const params = request.params ?? {};
|
|
1876
|
+
if (request.operation === "link.metadata" && origin === void 0) throw new ArkmePluginError("origin-required", "网址名称解析必须从当前 DSH 页面发起", false, 403);
|
|
1812
1877
|
if ([
|
|
1813
1878
|
"user.arkme-id.set",
|
|
1814
1879
|
"extensions.delete",
|
|
@@ -1827,15 +1892,18 @@ function createArkmeHostApi(service, options) {
|
|
|
1827
1892
|
"extensions.client.failure",
|
|
1828
1893
|
"extensions.persistent.invoke",
|
|
1829
1894
|
"extensions.bundle.invoke",
|
|
1830
|
-
"extensions.mine.publish"
|
|
1895
|
+
"extensions.mine.publish",
|
|
1896
|
+
"extensions.quarantine.dismiss",
|
|
1897
|
+
"extensions.quarantine.reenable",
|
|
1898
|
+
"remote.renameDesktop"
|
|
1831
1899
|
].includes(request.operation) && origin === void 0) throw new ArkmePluginError("origin-required", "扩展变更必须从当前 DSH 页面发起", false, 403);
|
|
1832
1900
|
writeJson$4(res, 200, {
|
|
1833
1901
|
ok: true,
|
|
1834
|
-
value: await dispatchArkmeHostOperation(service, request.operation, params, options.updateManager, options.extensionManager?.(), options.extensionInstallTasks?.(), options.ownedExtensionInventory?.(), controller.signal)
|
|
1902
|
+
value: await dispatchArkmeHostOperation(service, request.operation, params, options.updateManager, options.extensionManager?.(), options.extensionInstallTasks?.(), options.ownedExtensionInventory?.(), controller.signal, options.remoteHost?.(), options.desktopQuarantine)
|
|
1835
1903
|
});
|
|
1836
1904
|
} catch (error) {
|
|
1837
1905
|
if (controller.signal.aborted && res.destroyed) return;
|
|
1838
|
-
const known = error instanceof ArkmePluginError ? error : error instanceof ArkmePluginUpdateError ? new ArkmePluginError(error.code, error.message, error.retryable, error.retryable ? 503 : 409) : error instanceof ArkmeOutgoingCallError ? new ArkmePluginError(error.code, error.message, error.retryable, error.code === "call-active" ? 409 : 400) : new ArkmePluginError("internal-error", "Arkme 插件处理失败", true, 500, { cause: error });
|
|
1906
|
+
const known = error instanceof ArkmePluginError ? error : error instanceof ArkmePluginUpdateError ? new ArkmePluginError(error.code, error.message, error.retryable, error.retryable ? 503 : 409) : error instanceof ArkmeOutgoingCallError ? new ArkmePluginError(error.code, error.message, error.retryable, error.code === "call-active" ? 409 : 400) : error instanceof DshRemoteError ? new ArkmePluginError(error.code, error.message, error.retryable, error.retryable ? 503 : 409) : new ArkmePluginError("internal-error", "Arkme 插件处理失败", true, 500, { cause: error });
|
|
1839
1907
|
writeJson$4(res, known.httpStatus, {
|
|
1840
1908
|
ok: false,
|
|
1841
1909
|
error: {
|
|
@@ -1849,12 +1917,17 @@ function createArkmeHostApi(service, options) {
|
|
|
1849
1917
|
}
|
|
1850
1918
|
};
|
|
1851
1919
|
}
|
|
1852
|
-
async function dispatchArkmeHostOperation(service, operation, params, updateManager, extensionManager, extensionInstallTasks, ownedExtensionInventory, requestSignal) {
|
|
1920
|
+
async function dispatchArkmeHostOperation(service, operation, params, updateManager, extensionManager, extensionInstallTasks, ownedExtensionInventory, requestSignal, remoteHost, desktopQuarantine) {
|
|
1853
1921
|
switch (operation) {
|
|
1854
1922
|
case "provider.capabilities": return service.providerCapabilities();
|
|
1855
1923
|
case "provider.instance": return { instanceId: ARKME_RUNTIME_INSTANCE_ID };
|
|
1856
1924
|
case "provider.state": return await service.providerState();
|
|
1857
1925
|
case "chat.realtime.state": return service.chatRealtimeState();
|
|
1926
|
+
case "link.metadata": return await service.resolveLinkMetadata(stringParam(params, "url"), requestSignal === void 0 ? {} : { signal: requestSignal });
|
|
1927
|
+
case "source.link-metadata.resolve": {
|
|
1928
|
+
const url = stringParam(params, "url");
|
|
1929
|
+
return await service.resolveLinkMetadata(url, requestSignal === void 0 ? {} : { signal: requestSignal }) ?? arkmeRequiredLinkMetadataFallback(url);
|
|
1930
|
+
}
|
|
1858
1931
|
case "plugin.update.status": return await requireUpdateManager(updateManager).status();
|
|
1859
1932
|
case "plugin.update.check": return await requireUpdateManager(updateManager).check({ manual: true });
|
|
1860
1933
|
case "plugin.update.install": return await requireUpdateManager(updateManager).install();
|
|
@@ -1875,6 +1948,8 @@ async function dispatchArkmeHostOperation(service, operation, params, updateMana
|
|
|
1875
1948
|
case "auth.phone.send": return await service.sendPhoneCode(stringParam(params, "phone"), captchaParam(params));
|
|
1876
1949
|
case "auth.phone.verify": return await service.verifyPhoneCode(stringParam(params, "phone"), stringParam(params, "code"));
|
|
1877
1950
|
case "auth.logout": return await service.logout();
|
|
1951
|
+
case "remote.getStatus": return requireRemoteHost(remoteHost).getStatus();
|
|
1952
|
+
case "remote.renameDesktop": return await requireRemoteHost(remoteHost).renameDesktop(stringParam(params, "displayName"));
|
|
1878
1953
|
case "billing.quota": return await service.billingQuota();
|
|
1879
1954
|
case "billing.products": return await service.billingProducts();
|
|
1880
1955
|
case "billing.order.create": return await service.createBillingOrder({
|
|
@@ -1921,8 +1996,10 @@ async function dispatchArkmeHostOperation(service, operation, params, updateMana
|
|
|
1921
1996
|
case "directory.group.open-chat": return await service.openDirectoryGroupChat(stringParam(params, "sourceRef").trim(), requestSignal);
|
|
1922
1997
|
case "directory.bot.open-chat": return await service.openBotChat(stringParam(params, "botRef").trim(), requestSignal === void 0 ? {} : { signal: requestSignal });
|
|
1923
1998
|
case "bots.private-chat.open": return await service.openBotPrivateChat(stringParam(params, "botRef").trim(), requestSignal === void 0 ? {} : { signal: requestSignal });
|
|
1999
|
+
case "bots.private-chat.refresh": return await service.refreshBotPrivateChat(stringParam(params, "botRef").trim(), requestSignal === void 0 ? {} : { signal: requestSignal });
|
|
1924
2000
|
case "bots.private-chat.directory": return await service.listBotPrivateChatDirectory(requestSignal === void 0 ? {} : { signal: requestSignal });
|
|
1925
2001
|
case "bots.private-chat.send": return await service.sendBotPrivateChatMessage(stringParam(params, "botRef").trim(), stringParam(params, "content"), requestSignal === void 0 ? {} : { signal: requestSignal });
|
|
2002
|
+
case "bots.private-chat.mark-read": return await service.markBotPrivateChatRead(stringParam(params, "botRef").trim(), numberParam(params, "sequence", 0), requestSignal === void 0 ? {} : { signal: requestSignal });
|
|
1926
2003
|
case "unmarked-speakers.options": return await service.unmarkedSpeakerOptions(stringParam(params, "candidateRef").trim());
|
|
1927
2004
|
case "unmarked-speakers.retry-inference": return await service.retryUnmarkedSpeakerInference(stringParam(params, "candidateRef").trim());
|
|
1928
2005
|
case "unmarked-speakers.segments": {
|
|
@@ -2162,7 +2239,7 @@ async function dispatchArkmeHostOperation(service, operation, params, updateMana
|
|
|
2162
2239
|
case "source.message-copy-link": return await service.copySourceMessageLink(stringParam(params, "sourceRef"), messageActionRefsParam(params), requestSignal === void 0 ? {} : { signal: requestSignal });
|
|
2163
2240
|
case "source.message-copy-link.resolve": return await service.resolveMessageCopyLink(stringParam(params, "sid"), requestSignal === void 0 ? {} : { signal: requestSignal });
|
|
2164
2241
|
case "source.message-copy-link.extend": return await service.extendMessageCopyLink(stringParam(params, "sid"), numberParam(params, "itemIndex", 0), stringParam(params, "textContent"), stringParam(params, "recordUid"), requestSignal === void 0 ? {} : { signal: requestSignal });
|
|
2165
|
-
case "source.
|
|
2242
|
+
case "source.shared-recording-detail": return await service.sharedRecordingDetail(stringParam(params, "detailRef"), requestSignal === void 0 ? {} : { signal: requestSignal });
|
|
2166
2243
|
case "source.forward-messages": return await service.forwardSourceMessages(stringParam(params, "sourceRef"), messageActionRefsParam(params), {
|
|
2167
2244
|
...stringParam(params, "targetSourceRef") === "" ? {} : { targetSourceRef: stringParam(params, "targetSourceRef") },
|
|
2168
2245
|
...stringParam(params, "recordUid") === "" ? {} : { recordUid: stringParam(params, "recordUid") },
|
|
@@ -2376,6 +2453,10 @@ async function dispatchArkmeHostOperation(service, operation, params, updateMana
|
|
|
2376
2453
|
case "extensions.share.resolve": return (await enrichExtensionAuthors(service, [await requireExtensionManager(extensionManager).resolveSharedCatalogDetail(stringParam(params, "shareRef"))]))[0];
|
|
2377
2454
|
case "extensions.delete": return await requireOwnedExtensionInventory(ownedExtensionInventory).delete({ extensionId: stringParam(params, "extensionId") });
|
|
2378
2455
|
case "extensions.installed-list": return requireExtensionManager(extensionManager).listInstalled();
|
|
2456
|
+
case "extensions.quarantine.status": return await requireDesktopQuarantine(desktopQuarantine).status();
|
|
2457
|
+
case "extensions.quarantine.dismiss": return await requireDesktopQuarantine(desktopQuarantine).dismiss(stringParam(params, "packageName"));
|
|
2458
|
+
case "extensions.quarantine.reenable": return await requireDesktopQuarantine(desktopQuarantine).reenable(stringParam(params, "packageName"));
|
|
2459
|
+
case "extensions.quarantine.health": return await requireDesktopQuarantine(desktopQuarantine).health(stringParam(params, "packageName"));
|
|
2379
2460
|
case "extensions.mine.list": return await requireOwnedExtensionInventory(ownedExtensionInventory).list({ ...stringParam(params, "currentSessionId").trim() === "" ? {} : { currentSessionId: stringParam(params, "currentSessionId").trim() } });
|
|
2380
2461
|
case "extensions.mine.publish": return await requireOwnedExtensionInventory(ownedExtensionInventory).publish({
|
|
2381
2462
|
ownedRef: stringParam(params, "ownedRef"),
|
|
@@ -2444,6 +2525,10 @@ function requireUpdateManager(updateManager) {
|
|
|
2444
2525
|
if (updateManager === void 0) throw new ArkmePluginError("plugin-update-unavailable", "插件更新检查暂不可用", true, 503);
|
|
2445
2526
|
return updateManager;
|
|
2446
2527
|
}
|
|
2528
|
+
function requireRemoteHost(host) {
|
|
2529
|
+
if (host === void 0) throw new ArkmePluginError("CAPABILITY_UNSUPPORTED", "当前 DSH 未加载远控 Host", false, 503);
|
|
2530
|
+
return host;
|
|
2531
|
+
}
|
|
2447
2532
|
function requireExtensionInstallTasks(tasks) {
|
|
2448
2533
|
if (tasks === void 0) throw new ArkmePluginError("extension-runtime-unavailable", "当前 DSH 未加载扩展安装运行时", false, 503);
|
|
2449
2534
|
return tasks;
|
|
@@ -2452,6 +2537,10 @@ function requireExtensionManager(manager) {
|
|
|
2452
2537
|
if (manager === void 0) throw new ArkmePluginError("extension-runtime-unavailable", "当前 DSH 未加载 Dynamic Cordis Runner,市集不可用", false, 503);
|
|
2453
2538
|
return manager;
|
|
2454
2539
|
}
|
|
2540
|
+
function requireDesktopQuarantine(quarantine) {
|
|
2541
|
+
if (quarantine === void 0) throw new ArkmePluginError("extension-runtime-unavailable", "桌面扩展恢复状态尚未就绪", false, 503);
|
|
2542
|
+
return quarantine;
|
|
2543
|
+
}
|
|
2455
2544
|
function requireOwnedExtensionInventory(inventory) {
|
|
2456
2545
|
if (inventory === void 0) throw new ArkmePluginError("extension-runtime-unavailable", "扩展运行时尚未就绪", true, 503);
|
|
2457
2546
|
return inventory;
|
|
@@ -2471,19 +2560,71 @@ function extensionEditableVisibilityParam(params) {
|
|
|
2471
2560
|
return value;
|
|
2472
2561
|
}
|
|
2473
2562
|
//#endregion
|
|
2563
|
+
//#region src/dsh-host-capabilities.ts
|
|
2564
|
+
function isRecord(value) {
|
|
2565
|
+
return typeof value === "object" && value !== null;
|
|
2566
|
+
}
|
|
2567
|
+
function sessionController(value) {
|
|
2568
|
+
if (!isRecord(value) || typeof value.openWorkspacePath !== "function") return void 0;
|
|
2569
|
+
return value;
|
|
2570
|
+
}
|
|
2571
|
+
function apiProxy(value) {
|
|
2572
|
+
if (!isRecord(value) || !isRecord(value.host) || typeof value.host.openPath !== "function") return void 0;
|
|
2573
|
+
return value;
|
|
2574
|
+
}
|
|
2575
|
+
/**
|
|
2576
|
+
* Open one Arkme-owned, already-validated local path through the current DSH
|
|
2577
|
+
* Host capability. The service shape is detected at call time so one plugin
|
|
2578
|
+
* package works with both the Typert Remote and legacy ApiProxy generations.
|
|
2579
|
+
*/
|
|
2580
|
+
async function openDshHostPath(ctx, path, signal) {
|
|
2581
|
+
signal.throwIfAborted();
|
|
2582
|
+
const current = sessionController(ctx.get("sessionController"));
|
|
2583
|
+
if (current !== void 0) {
|
|
2584
|
+
const response = await current.openWorkspacePath({ path }, signal);
|
|
2585
|
+
if (!isRecord(response) || response.opened !== true) throw new Error("DSH sessionController.openWorkspacePath 返回了无效响应");
|
|
2586
|
+
return;
|
|
2587
|
+
}
|
|
2588
|
+
const legacy = apiProxy(ctx.get("apiProxy"));
|
|
2589
|
+
if (legacy === void 0) throw new Error("当前 DSH 宿主未提供本机文件打开能力");
|
|
2590
|
+
const response = await legacy.host.openPath({
|
|
2591
|
+
rpcId: randomUUID(),
|
|
2592
|
+
payload: { path }
|
|
2593
|
+
}, signal);
|
|
2594
|
+
if (!isRecord(response) || !isRecord(response.result)) throw new Error("DSH apiProxy.host.openPath 返回了无效响应");
|
|
2595
|
+
if (response.result.ok === false) {
|
|
2596
|
+
const error = response.result.error;
|
|
2597
|
+
throw new Error(isRecord(error) && typeof error.message === "string" ? error.message : "DSH apiProxy.host.openPath 调用失败");
|
|
2598
|
+
}
|
|
2599
|
+
if (response.result.ok !== true || !isRecord(response.result.value) || response.result.value.opened !== true) throw new Error("DSH apiProxy.host.openPath 返回了无效响应");
|
|
2600
|
+
}
|
|
2601
|
+
//#endregion
|
|
2474
2602
|
//#region src/harness-embed-contract.ts
|
|
2475
2603
|
const ARKME_HARNESS_EMBED_PATH = "/arkme-self/harness-frame";
|
|
2476
2604
|
//#endregion
|
|
2477
2605
|
//#region src/harness-embed-route.ts
|
|
2478
2606
|
const ARKME_PLUGIN_PACKAGE_NAME = "@senguoyun/dsh-arkme";
|
|
2479
2607
|
const DSH_CLIENT_HMR_PACKAGE_NAME = "@deepseek-ai/dsh-client-hmr";
|
|
2480
|
-
const
|
|
2608
|
+
const REQUIRED_BOOT_PACKAGE_GROUPS = [{
|
|
2609
|
+
capability: "client module loader",
|
|
2610
|
+
alternatives: ["@deepseek-ai/dsh-client-modules"]
|
|
2611
|
+
}, {
|
|
2612
|
+
capability: "client connection runtime",
|
|
2613
|
+
alternatives: ["@deepseek-ai/dsh-client-runtime", "@deepseek-ai/dsh-client-connection"]
|
|
2614
|
+
}];
|
|
2615
|
+
/** Forward only the same-origin browser session needed by authenticated DSH roots. */
|
|
2616
|
+
function dshRootDocumentHeaders(request) {
|
|
2617
|
+
const headers = { Accept: "text/html" };
|
|
2618
|
+
if (request.headers.cookie !== void 0) headers.Cookie = request.headers.cookie;
|
|
2619
|
+
return headers;
|
|
2620
|
+
}
|
|
2481
2621
|
function shortHash(input) {
|
|
2482
2622
|
return createHash("sha1").update(input).digest("hex").slice(0, 12);
|
|
2483
2623
|
}
|
|
2484
|
-
function
|
|
2485
|
-
return
|
|
2624
|
+
function serializedBootGraph(graph) {
|
|
2625
|
+
return JSON.stringify(graph).replaceAll("<", "\\u003c");
|
|
2486
2626
|
}
|
|
2627
|
+
const BOOT_ASSIGNMENT_RENDERERS = [(graph) => `window.__DSH_BOOT__ = ${serializedBootGraph(graph)}`, (graph) => `globalThis["__DSH_BOOT__"] = ${serializedBootGraph(graph)}`];
|
|
2487
2628
|
function referencesPackage(specifier, packageName) {
|
|
2488
2629
|
return specifier === packageName || specifier.startsWith(`${packageName}/`);
|
|
2489
2630
|
}
|
|
@@ -2493,10 +2634,17 @@ function dependencyRemovedBy(entry, removedPackageNames) {
|
|
|
2493
2634
|
if (removed !== void 0) return removed;
|
|
2494
2635
|
}
|
|
2495
2636
|
}
|
|
2496
|
-
function
|
|
2497
|
-
|
|
2637
|
+
function requiredBootPackages(entries) {
|
|
2638
|
+
const packageNames = new Set(entries.map((entry) => entry.id));
|
|
2639
|
+
const required = /* @__PURE__ */ new Set();
|
|
2640
|
+
for (const group of REQUIRED_BOOT_PACKAGE_GROUPS) {
|
|
2641
|
+
const available = group.alternatives.filter((packageName) => packageNames.has(packageName));
|
|
2642
|
+
if (available.length === 0) throw new Error(`harness boot graph: required ${group.capability} is missing; expected one of ${group.alternatives.join(", ")}`);
|
|
2643
|
+
for (const packageName of available) required.add(packageName);
|
|
2644
|
+
}
|
|
2645
|
+
return required;
|
|
2498
2646
|
}
|
|
2499
|
-
function completeRemovedPackageClosure(entries, removedPackageNames) {
|
|
2647
|
+
function completeRemovedPackageClosure(entries, removedPackageNames, requiredPackageNames) {
|
|
2500
2648
|
let changed = true;
|
|
2501
2649
|
while (changed) {
|
|
2502
2650
|
changed = false;
|
|
@@ -2504,7 +2652,7 @@ function completeRemovedPackageClosure(entries, removedPackageNames) {
|
|
|
2504
2652
|
if (removedPackageNames.has(entry.id)) continue;
|
|
2505
2653
|
const removed = dependencyRemovedBy(entry, removedPackageNames);
|
|
2506
2654
|
if (removed === void 0) continue;
|
|
2507
|
-
if (
|
|
2655
|
+
if (requiredPackageNames.has(entry.id)) throw new Error(`harness boot graph: required package ${entry.id} depends on removed package ${removed}`);
|
|
2508
2656
|
removedPackageNames.add(entry.id);
|
|
2509
2657
|
changed = true;
|
|
2510
2658
|
}
|
|
@@ -2516,32 +2664,67 @@ function assertNoRemovedDependencies(entries, removedPackageNames) {
|
|
|
2516
2664
|
if (removed !== void 0) throw new Error(`harness boot graph: kept package ${entry.id} depends on removed package ${removed}`);
|
|
2517
2665
|
}
|
|
2518
2666
|
}
|
|
2667
|
+
function projectBootBatches(batches, keptPackageNames) {
|
|
2668
|
+
if (batches === void 0) return void 0;
|
|
2669
|
+
const projected = batches.flatMap((batch) => {
|
|
2670
|
+
const entries = batch.entries.filter((packageName) => keptPackageNames.has(packageName));
|
|
2671
|
+
return entries.length === 0 ? [] : [{
|
|
2672
|
+
...batch,
|
|
2673
|
+
entries
|
|
2674
|
+
}];
|
|
2675
|
+
});
|
|
2676
|
+
const covered = /* @__PURE__ */ new Set();
|
|
2677
|
+
for (const batch of projected) for (const packageName of batch.entries) {
|
|
2678
|
+
if (covered.has(packageName)) throw new Error(`harness boot graph: package ${packageName} belongs to more than one projected batch`);
|
|
2679
|
+
covered.add(packageName);
|
|
2680
|
+
}
|
|
2681
|
+
for (const packageName of keptPackageNames) if (!covered.has(packageName)) throw new Error(`harness boot graph: package ${packageName} belongs to no projected batch`);
|
|
2682
|
+
return projected;
|
|
2683
|
+
}
|
|
2519
2684
|
/**
|
|
2520
2685
|
* Build the iframe-only DSH graph without any Arkme-managed extension clients.
|
|
2521
2686
|
* Host halves stay process-owned and untouched; only this document's browser
|
|
2522
2687
|
* boot graph is projected.
|
|
2523
2688
|
*/
|
|
2524
2689
|
function projectHarnessBootGraph(graph, installedPackageNames) {
|
|
2690
|
+
const requiredPackageNames = requiredBootPackages(graph.entries);
|
|
2525
2691
|
const removedPackageNames = /* @__PURE__ */ new Set([
|
|
2526
2692
|
ARKME_PLUGIN_PACKAGE_NAME,
|
|
2527
2693
|
DSH_CLIENT_HMR_PACKAGE_NAME,
|
|
2528
2694
|
...installedPackageNames.map((name) => name.trim()).filter(Boolean)
|
|
2529
2695
|
]);
|
|
2530
|
-
completeRemovedPackageClosure(graph.entries, removedPackageNames);
|
|
2696
|
+
completeRemovedPackageClosure(graph.entries, removedPackageNames, requiredPackageNames);
|
|
2531
2697
|
const entries = graph.entries.filter((entry) => !removedPackageNames.has(entry.id));
|
|
2532
|
-
for (const required of
|
|
2698
|
+
for (const required of requiredPackageNames) if (!entries.some((entry) => entry.id === required)) throw new Error(`harness boot graph: required package ${required} was removed`);
|
|
2533
2699
|
assertNoRemovedDependencies(entries, removedPackageNames);
|
|
2700
|
+
const batches = projectBootBatches(graph.batches, new Set(entries.map((entry) => entry.id)));
|
|
2534
2701
|
return {
|
|
2535
|
-
rev: shortHash(JSON.stringify(entries
|
|
2536
|
-
|
|
2702
|
+
rev: shortHash(JSON.stringify(batches === void 0 ? entries : {
|
|
2703
|
+
entries,
|
|
2704
|
+
batches
|
|
2705
|
+
})),
|
|
2706
|
+
entries,
|
|
2707
|
+
...batches === void 0 ? {} : { batches }
|
|
2537
2708
|
};
|
|
2538
2709
|
}
|
|
2539
2710
|
/** Replace exactly the graph DSH injected into the current root document. */
|
|
2540
2711
|
function replaceHarnessBootGraph(html, fullGraph, projectedGraph) {
|
|
2541
|
-
const
|
|
2542
|
-
const
|
|
2543
|
-
|
|
2544
|
-
|
|
2712
|
+
const matches = [];
|
|
2713
|
+
for (const render of BOOT_ASSIGNMENT_RENDERERS) {
|
|
2714
|
+
const assignment = render(fullGraph);
|
|
2715
|
+
let at = html.indexOf(assignment);
|
|
2716
|
+
while (at !== -1) {
|
|
2717
|
+
matches.push({
|
|
2718
|
+
at,
|
|
2719
|
+
assignment,
|
|
2720
|
+
render
|
|
2721
|
+
});
|
|
2722
|
+
at = html.indexOf(assignment, at + assignment.length);
|
|
2723
|
+
}
|
|
2724
|
+
}
|
|
2725
|
+
if (matches.length !== 1) throw new Error("harness boot graph: expected exactly one current DSH boot assignment");
|
|
2726
|
+
const { at, assignment, render } = matches[0];
|
|
2727
|
+
return `${html.slice(0, at)}${render(projectedGraph)}${html.slice(at + assignment.length)}`;
|
|
2545
2728
|
}
|
|
2546
2729
|
function createHarnessEmbedRouteHandler(options) {
|
|
2547
2730
|
return async (request, response) => {
|
|
@@ -2556,7 +2739,7 @@ function createHarnessEmbedRouteHandler(options) {
|
|
|
2556
2739
|
try {
|
|
2557
2740
|
const fullGraph = options.getGraph();
|
|
2558
2741
|
const projectedGraph = projectHarnessBootGraph(fullGraph, options.installedPackageNames());
|
|
2559
|
-
const html = replaceHarnessBootGraph(await options.readRootHtml(), fullGraph, projectedGraph);
|
|
2742
|
+
const html = replaceHarnessBootGraph(await options.readRootHtml(request), fullGraph, projectedGraph);
|
|
2560
2743
|
const body = Buffer.from(html);
|
|
2561
2744
|
response.writeHead(200, {
|
|
2562
2745
|
"Content-Type": "text/html; charset=utf-8",
|
|
@@ -3268,6 +3451,220 @@ var ArkmeKeychainStore = class {
|
|
|
3268
3451
|
if (process.platform !== "darwin") throw new Error("当前版本只支持使用 macOS Keychain 保存 Arkme 登录凭据");
|
|
3269
3452
|
}
|
|
3270
3453
|
};
|
|
3454
|
+
const MACOS_KEYCHAIN_JXA_SCRIPT = String.raw`
|
|
3455
|
+
ObjC.import('Foundation')
|
|
3456
|
+
ObjC.import('Security')
|
|
3457
|
+
function run(argv) {
|
|
3458
|
+
if (argv.length !== 2) throw new Error('invalid Keychain coordinates')
|
|
3459
|
+
const query = $.NSMutableDictionary.dictionary
|
|
3460
|
+
query.setObjectForKey($("genp"), $("class"))
|
|
3461
|
+
query.setObjectForKey($(argv[0]), $("acct"))
|
|
3462
|
+
query.setObjectForKey($(argv[1]), $("svce"))
|
|
3463
|
+
const payload = $.NSFileHandle.fileHandleWithStandardInput.readDataToEndOfFile
|
|
3464
|
+
const update = $.NSMutableDictionary.dictionary
|
|
3465
|
+
update.setObjectForKey(payload, $("v_Data"))
|
|
3466
|
+
let status = Number($.SecItemUpdate(query, update))
|
|
3467
|
+
if (status === -25300) {
|
|
3468
|
+
query.setObjectForKey(payload, $("v_Data"))
|
|
3469
|
+
status = Number($.SecItemAdd(query, null))
|
|
3470
|
+
}
|
|
3471
|
+
if (status !== 0) throw new Error('Keychain write failed with status ' + String(status))
|
|
3472
|
+
}
|
|
3473
|
+
`;
|
|
3474
|
+
const MACOS_KEYCHAIN_READ_JXA_SCRIPT = String.raw`
|
|
3475
|
+
ObjC.import('Foundation')
|
|
3476
|
+
ObjC.import('Security')
|
|
3477
|
+
function run(argv) {
|
|
3478
|
+
if (argv.length !== 2) throw new Error('invalid Keychain coordinates')
|
|
3479
|
+
const query = $.NSMutableDictionary.dictionary
|
|
3480
|
+
query.setObjectForKey($("genp"), $("class"))
|
|
3481
|
+
query.setObjectForKey($(argv[0]), $("acct"))
|
|
3482
|
+
query.setObjectForKey($(argv[1]), $("svce"))
|
|
3483
|
+
query.setObjectForKey($.NSNumber.numberWithBool(true), $("r_Data"))
|
|
3484
|
+
query.setObjectForKey($("m_LimitOne"), $("m_Limit"))
|
|
3485
|
+
const result = Ref()
|
|
3486
|
+
const status = Number($.SecItemCopyMatching(query, result))
|
|
3487
|
+
if (status === -25300) return JSON.stringify({ found: false })
|
|
3488
|
+
if (status !== 0) throw new Error('Keychain read failed with status ' + String(status))
|
|
3489
|
+
const data = ObjC.castRefToObject(result[0])
|
|
3490
|
+
const encoded = data.base64EncodedStringWithOptions(0)
|
|
3491
|
+
return JSON.stringify({ found: true, payload: ObjC.unwrap(encoded) })
|
|
3492
|
+
}
|
|
3493
|
+
`;
|
|
3494
|
+
const MACOS_KEYCHAIN_DELETE_JXA_SCRIPT = String.raw`
|
|
3495
|
+
ObjC.import('Foundation')
|
|
3496
|
+
ObjC.import('Security')
|
|
3497
|
+
function run(argv) {
|
|
3498
|
+
if (argv.length !== 2) throw new Error('invalid Keychain coordinates')
|
|
3499
|
+
const query = $.NSMutableDictionary.dictionary
|
|
3500
|
+
query.setObjectForKey($("genp"), $("class"))
|
|
3501
|
+
query.setObjectForKey($(argv[0]), $("acct"))
|
|
3502
|
+
query.setObjectForKey($(argv[1]), $("svce"))
|
|
3503
|
+
const status = Number($.SecItemDelete(query))
|
|
3504
|
+
if (status !== 0 && status !== -25300) {
|
|
3505
|
+
throw new Error('Keychain delete failed with status ' + String(status))
|
|
3506
|
+
}
|
|
3507
|
+
}
|
|
3508
|
+
`;
|
|
3509
|
+
async function runMacOSKeychainJXA(script, args, operation, stdin) {
|
|
3510
|
+
return await new Promise((resolve, reject) => {
|
|
3511
|
+
const child = spawn("/usr/bin/osascript", [
|
|
3512
|
+
"-l",
|
|
3513
|
+
"JavaScript",
|
|
3514
|
+
"-e",
|
|
3515
|
+
script,
|
|
3516
|
+
...args
|
|
3517
|
+
], {
|
|
3518
|
+
detached: true,
|
|
3519
|
+
shell: false,
|
|
3520
|
+
stdio: [
|
|
3521
|
+
"pipe",
|
|
3522
|
+
"pipe",
|
|
3523
|
+
"pipe"
|
|
3524
|
+
]
|
|
3525
|
+
});
|
|
3526
|
+
let stdout = "";
|
|
3527
|
+
let stderr = "";
|
|
3528
|
+
let settled = false;
|
|
3529
|
+
const finish = (error) => {
|
|
3530
|
+
if (settled) return;
|
|
3531
|
+
settled = true;
|
|
3532
|
+
clearTimeout(timer);
|
|
3533
|
+
if (error === void 0) resolve(stdout.trim());
|
|
3534
|
+
else reject(error);
|
|
3535
|
+
};
|
|
3536
|
+
const terminate = () => {
|
|
3537
|
+
if (child.pid !== void 0) try {
|
|
3538
|
+
process.kill(-child.pid, "SIGKILL");
|
|
3539
|
+
} catch {
|
|
3540
|
+
child.kill("SIGKILL");
|
|
3541
|
+
}
|
|
3542
|
+
else child.kill("SIGKILL");
|
|
3543
|
+
};
|
|
3544
|
+
const timer = setTimeout(() => {
|
|
3545
|
+
terminate();
|
|
3546
|
+
finish(/* @__PURE__ */ new Error(`${operation} macOS Keychain 超时`));
|
|
3547
|
+
}, 5e3);
|
|
3548
|
+
timer.unref();
|
|
3549
|
+
child.stdout.setEncoding("utf8");
|
|
3550
|
+
child.stdout.on("data", (chunk) => {
|
|
3551
|
+
if (stdout.length + String(chunk).length > 1024 * 1024) {
|
|
3552
|
+
terminate();
|
|
3553
|
+
finish(/* @__PURE__ */ new Error(`macOS Keychain ${operation}结果超出限制`));
|
|
3554
|
+
return;
|
|
3555
|
+
}
|
|
3556
|
+
stdout += String(chunk);
|
|
3557
|
+
});
|
|
3558
|
+
child.stderr.setEncoding("utf8");
|
|
3559
|
+
child.stderr.on("data", (chunk) => {
|
|
3560
|
+
if (stderr.length < 64 * 1024) stderr += String(chunk).slice(0, 64 * 1024 - stderr.length);
|
|
3561
|
+
});
|
|
3562
|
+
child.once("error", (error) => finish(new Error(`无法启动 macOS Keychain ${operation}命令`, { cause: error })));
|
|
3563
|
+
child.once("close", (code) => {
|
|
3564
|
+
if (code === 0) finish();
|
|
3565
|
+
else finish(/* @__PURE__ */ new Error(`macOS Keychain ${operation}失败(退出码 ${String(code)}):${stderr.trim().slice(0, 512)}`));
|
|
3566
|
+
});
|
|
3567
|
+
child.stdin.once("error", (error) => finish(new Error(`无法向 macOS Keychain ${operation}命令写入数据`, { cause: error })));
|
|
3568
|
+
child.stdin.end(stdin ?? "", "utf8");
|
|
3569
|
+
});
|
|
3570
|
+
}
|
|
3571
|
+
function macOSSecureValueCoordinates(args) {
|
|
3572
|
+
if (args.length !== 7 || args[0] !== "add-generic-password" || args[1] !== "-a" || args[3] !== "-s" || args[5] !== "-U" || args[6] !== "-w" || args[2]?.trim() === "" || args[4]?.trim() === "") throw new Error("macOS Keychain 写入参数无效");
|
|
3573
|
+
return {
|
|
3574
|
+
account: args[2],
|
|
3575
|
+
service: args[4]
|
|
3576
|
+
};
|
|
3577
|
+
}
|
|
3578
|
+
async function writeArkmeMacOSSecureValue(args, payload) {
|
|
3579
|
+
const coordinates = macOSSecureValueCoordinates(args);
|
|
3580
|
+
await runMacOSKeychainJXA(MACOS_KEYCHAIN_JXA_SCRIPT, [coordinates.account, coordinates.service], "写入", payload);
|
|
3581
|
+
}
|
|
3582
|
+
async function readArkmeMacOSSecureValue(account, service) {
|
|
3583
|
+
const output = await runMacOSKeychainJXA(MACOS_KEYCHAIN_READ_JXA_SCRIPT, [account, service], "读取");
|
|
3584
|
+
let result;
|
|
3585
|
+
try {
|
|
3586
|
+
result = JSON.parse(output);
|
|
3587
|
+
} catch (error) {
|
|
3588
|
+
throw new Error("macOS Keychain 读取结果无效", { cause: error });
|
|
3589
|
+
}
|
|
3590
|
+
if (result === null || typeof result !== "object") throw new Error("macOS Keychain 读取结果无效");
|
|
3591
|
+
const record = result;
|
|
3592
|
+
if (record.found === false) return void 0;
|
|
3593
|
+
if (record.found !== true || typeof record.payload !== "string") throw new Error("macOS Keychain 读取结果无效");
|
|
3594
|
+
return Buffer.from(record.payload, "base64").toString("utf8");
|
|
3595
|
+
}
|
|
3596
|
+
async function deleteArkmeMacOSSecureValue(account, service) {
|
|
3597
|
+
await runMacOSKeychainJXA(MACOS_KEYCHAIN_DELETE_JXA_SCRIPT, [account, service], "删除");
|
|
3598
|
+
}
|
|
3599
|
+
var ArkmeMacOSSecureValueStore = class {
|
|
3600
|
+
service;
|
|
3601
|
+
writer;
|
|
3602
|
+
platform;
|
|
3603
|
+
reader;
|
|
3604
|
+
deleter;
|
|
3605
|
+
constructor(service, writer = writeArkmeMacOSSecureValue, platform = process.platform, reader = readArkmeMacOSSecureValue, deleter = deleteArkmeMacOSSecureValue) {
|
|
3606
|
+
this.service = service;
|
|
3607
|
+
this.writer = writer;
|
|
3608
|
+
this.platform = platform;
|
|
3609
|
+
this.reader = reader;
|
|
3610
|
+
this.deleter = deleter;
|
|
3611
|
+
}
|
|
3612
|
+
async read(account) {
|
|
3613
|
+
this.requireMacOS();
|
|
3614
|
+
try {
|
|
3615
|
+
return await this.reader(account, this.service);
|
|
3616
|
+
} catch (error) {
|
|
3617
|
+
throw new Error("无法读取 macOS Keychain 中的 Arkme 安全数据", { cause: error });
|
|
3618
|
+
}
|
|
3619
|
+
}
|
|
3620
|
+
async write(account, payload) {
|
|
3621
|
+
this.requireMacOS();
|
|
3622
|
+
await this.writer([
|
|
3623
|
+
"add-generic-password",
|
|
3624
|
+
"-a",
|
|
3625
|
+
account,
|
|
3626
|
+
"-s",
|
|
3627
|
+
this.service,
|
|
3628
|
+
"-U",
|
|
3629
|
+
"-w"
|
|
3630
|
+
], payload);
|
|
3631
|
+
}
|
|
3632
|
+
async delete(account) {
|
|
3633
|
+
this.requireMacOS();
|
|
3634
|
+
try {
|
|
3635
|
+
await this.deleter(account, this.service);
|
|
3636
|
+
} catch (error) {
|
|
3637
|
+
throw new Error("无法删除 macOS Keychain 中的 Arkme 安全数据", { cause: error });
|
|
3638
|
+
}
|
|
3639
|
+
}
|
|
3640
|
+
requireMacOS() {
|
|
3641
|
+
if (this.platform !== "darwin") throw new Error("macOS Keychain 只能在 macOS 使用");
|
|
3642
|
+
}
|
|
3643
|
+
};
|
|
3644
|
+
var ArkmeWindowsSecureValueStore = class {
|
|
3645
|
+
service;
|
|
3646
|
+
backend;
|
|
3647
|
+
constructor(service, backend = new ArkmeWindowsCredentialManagerBackend()) {
|
|
3648
|
+
this.service = service;
|
|
3649
|
+
this.backend = backend;
|
|
3650
|
+
}
|
|
3651
|
+
async read(account) {
|
|
3652
|
+
return await this.backend.read(this.service, account);
|
|
3653
|
+
}
|
|
3654
|
+
async write(account, payload) {
|
|
3655
|
+
if (Buffer.byteLength(payload, "utf16le") > WINDOWS_CREDENTIAL_BLOB_MAX_BYTES) throw new Error("Arkme 安全数据超出 Windows Credential Manager 容量限制");
|
|
3656
|
+
await this.backend.write(this.service, account, payload);
|
|
3657
|
+
}
|
|
3658
|
+
async delete(account) {
|
|
3659
|
+
await this.backend.delete(this.service, account);
|
|
3660
|
+
}
|
|
3661
|
+
};
|
|
3662
|
+
function createArkmeSecureValueStore(service, options = {}) {
|
|
3663
|
+
const platform = options.platform ?? process.platform;
|
|
3664
|
+
if (platform === "darwin") return new ArkmeMacOSSecureValueStore(service);
|
|
3665
|
+
if (platform === "win32") return new ArkmeWindowsSecureValueStore(service, options.windowsBackend);
|
|
3666
|
+
throw new Error(`当前版本只支持在 macOS 或 Windows 保存 Arkme 安全数据,当前平台:${platform}`);
|
|
3667
|
+
}
|
|
3271
3668
|
var ArkmeWindowsCredentialStore = class {
|
|
3272
3669
|
service;
|
|
3273
3670
|
backend;
|
|
@@ -3808,11 +4205,18 @@ var ArkmeLocalDatabase = class {
|
|
|
3808
4205
|
//#region src/managed-ai/adapter.ts
|
|
3809
4206
|
const ARKME_MANAGED_PROVIDER = "arkme-managed";
|
|
3810
4207
|
const ARKME_MANAGED_MODEL = "deepseek-v4-flash";
|
|
3811
|
-
const ARKME_MANAGED_PROVIDER_NAME = "Arkme";
|
|
3812
|
-
const
|
|
4208
|
+
const ARKME_MANAGED_PROVIDER_NAME = "Arkme · 余额计费";
|
|
4209
|
+
const ARKME_MANAGED_CATALOG_TTL_MS = 6e4;
|
|
4210
|
+
const ARKME_MANAGED_CATALOG_TIMEOUT_MS = 1e4;
|
|
3813
4211
|
const ARKME_INSUFFICIENT_BALANCE_MESSAGE = "Arkme AI 余额不足,请前往 Arkme 设置中的余额充值后重试";
|
|
3814
4212
|
const ARKME_LOGIN_MESSAGE = "请先登录或重新登录 Arkme 后再使用托管模型";
|
|
3815
|
-
const
|
|
4213
|
+
const ARKME_MANAGED_FALLBACK_MODEL = {
|
|
4214
|
+
id: ARKME_MANAGED_MODEL,
|
|
4215
|
+
name: "DeepSeek-V4-Flash",
|
|
4216
|
+
contextWindow: 1e6,
|
|
4217
|
+
maxTokens: 256e3
|
|
4218
|
+
};
|
|
4219
|
+
const ARKME_MANAGED_FALLBACK_MODELS = [ARKME_MANAGED_FALLBACK_MODEL];
|
|
3816
4220
|
const ARKME_AUTH_FAILURE_CODES = /* @__PURE__ */ new Set([
|
|
3817
4221
|
"login-required",
|
|
3818
4222
|
"login-expired",
|
|
@@ -3910,9 +4314,8 @@ function localizeManagedAiError(error) {
|
|
|
3910
4314
|
...facts.requestId === void 0 ? {} : { requestId: ProviderRequestId(facts.requestId) }
|
|
3911
4315
|
});
|
|
3912
4316
|
}
|
|
3913
|
-
function
|
|
4317
|
+
function assertManagedProvider(provider) {
|
|
3914
4318
|
if (provider !== "arkme-managed") throw new LlmError(`当前 Arkme 托管模型不支持提供商“${provider}”`, "NO_ADAPTER");
|
|
3915
|
-
if (model !== void 0 && !ARKME_MANAGED_MODELS.has(model)) throw new LlmError(`当前 Arkme 托管服务不支持模型“${model}”,请重新选择模型`, "UNKNOWN_MODEL");
|
|
3916
4319
|
}
|
|
3917
4320
|
async function resolveBearer(owner) {
|
|
3918
4321
|
try {
|
|
@@ -3932,11 +4335,195 @@ async function resolveBearer(owner) {
|
|
|
3932
4335
|
}
|
|
3933
4336
|
throw new LlmError(ARKME_LOGIN_MESSAGE, "AUTH");
|
|
3934
4337
|
}
|
|
4338
|
+
function managedConnection(baseUrl, models) {
|
|
4339
|
+
const defaults = models.find((model) => model.id === "deepseek-v4-flash") ?? models[0] ?? ARKME_MANAGED_FALLBACK_MODEL;
|
|
4340
|
+
return resolveAdapterOptions({
|
|
4341
|
+
baseURL: baseUrl,
|
|
4342
|
+
thinking: "enabled",
|
|
4343
|
+
reasoningEffort: "high",
|
|
4344
|
+
maxTokens: defaults.maxTokens ?? 256e3,
|
|
4345
|
+
defaultContextWindow: defaults.contextWindow ?? 1e6,
|
|
4346
|
+
models: [...models],
|
|
4347
|
+
retryPolicy: {
|
|
4348
|
+
mode: "normal",
|
|
4349
|
+
maxRetries: 0
|
|
4350
|
+
}
|
|
4351
|
+
});
|
|
4352
|
+
}
|
|
4353
|
+
function requiredCatalogText(source, key, label) {
|
|
4354
|
+
const value = source[key];
|
|
4355
|
+
if (typeof value !== "string") throw new LlmError(`Arkme 模型目录中的${label}无效`, "MALFORMED_RESPONSE");
|
|
4356
|
+
const normalized = value.trim();
|
|
4357
|
+
if (normalized === "" || normalized.length > 256) throw new LlmError(`Arkme 模型目录中的${label}无效`, "MALFORMED_RESPONSE");
|
|
4358
|
+
return normalized;
|
|
4359
|
+
}
|
|
4360
|
+
function requiredCatalogModelId(source) {
|
|
4361
|
+
const model = requiredCatalogText(source, "public_model_code", "模型 ID");
|
|
4362
|
+
if (!/^[^\s\u0000-\u001F\u007F]+$/u.test(model)) throw new LlmError("Arkme 模型目录中的模型 ID 无效", "MALFORMED_RESPONSE");
|
|
4363
|
+
return model;
|
|
4364
|
+
}
|
|
4365
|
+
function requiredCatalogTokens(source, key, label) {
|
|
4366
|
+
const value = source[key];
|
|
4367
|
+
if (typeof value !== "string" || !/^[1-9]\d*$/u.test(value)) throw new LlmError(`Arkme 模型目录中的${label}无效`, "MALFORMED_RESPONSE");
|
|
4368
|
+
const parsed = Number(value);
|
|
4369
|
+
if (!Number.isSafeInteger(parsed)) throw new LlmError(`Arkme 模型目录中的${label}无效`, "MALFORMED_RESPONSE");
|
|
4370
|
+
return parsed;
|
|
4371
|
+
}
|
|
4372
|
+
function parseManagedCatalog(payload) {
|
|
4373
|
+
const envelope = asRecord(payload);
|
|
4374
|
+
if (envelope?.code !== 200) {
|
|
4375
|
+
const status = validHttpStatus(envelope?.code);
|
|
4376
|
+
throw localizeManagedAiError(new LlmError(typeof envelope?.message === "string" ? envelope.message : "Arkme 模型目录请求失败", "MANAGED_MODEL_CATALOG", { ...status === void 0 ? {} : { status } }));
|
|
4377
|
+
}
|
|
4378
|
+
const items = asRecord(envelope.data)?.item_ls;
|
|
4379
|
+
if (!Array.isArray(items) || items.length > 256) throw new LlmError("Arkme 模型目录返回异常", "MALFORMED_RESPONSE");
|
|
4380
|
+
const seen = /* @__PURE__ */ new Set();
|
|
4381
|
+
return items.map((value) => {
|
|
4382
|
+
const item = asRecord(value);
|
|
4383
|
+
if (item?.provider !== "arkme-managed") throw new LlmError("Arkme 模型目录包含无效提供商", "MALFORMED_RESPONSE");
|
|
4384
|
+
const id = requiredCatalogModelId(item);
|
|
4385
|
+
if (seen.has(id)) throw new LlmError(`Arkme 模型目录包含重复模型“${id}”`, "MALFORMED_RESPONSE");
|
|
4386
|
+
seen.add(id);
|
|
4387
|
+
const contextWindow = requiredCatalogTokens(item, "context_window_tokens", "上下文窗口");
|
|
4388
|
+
const defaultMaxTokens = requiredCatalogTokens(item, "default_max_output_tokens", "默认输出上限");
|
|
4389
|
+
const maximumMaxTokens = requiredCatalogTokens(item, "maximum_max_output_tokens", "最大输出上限");
|
|
4390
|
+
if (defaultMaxTokens > maximumMaxTokens || maximumMaxTokens > contextWindow) throw new LlmError(`Arkme 模型“${id}”的 Token 上限无效`, "MALFORMED_RESPONSE");
|
|
4391
|
+
return {
|
|
4392
|
+
id,
|
|
4393
|
+
name: requiredCatalogText(item, "display_name", "显示名称"),
|
|
4394
|
+
contextWindow,
|
|
4395
|
+
maxTokens: defaultMaxTokens
|
|
4396
|
+
};
|
|
4397
|
+
});
|
|
4398
|
+
}
|
|
4399
|
+
function waitForSignal(promise, signal) {
|
|
4400
|
+
if (signal.aborted) return Promise.reject(signal.reason ?? new DOMException("Aborted", "AbortError"));
|
|
4401
|
+
return new Promise((resolve, reject) => {
|
|
4402
|
+
const onAbort = () => {
|
|
4403
|
+
signal.removeEventListener("abort", onAbort);
|
|
4404
|
+
reject(signal.reason ?? new DOMException("Aborted", "AbortError"));
|
|
4405
|
+
};
|
|
4406
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
4407
|
+
promise.then((value) => {
|
|
4408
|
+
signal.removeEventListener("abort", onAbort);
|
|
4409
|
+
resolve(value);
|
|
4410
|
+
}, (error) => {
|
|
4411
|
+
signal.removeEventListener("abort", onAbort);
|
|
4412
|
+
reject(error);
|
|
4413
|
+
});
|
|
4414
|
+
});
|
|
4415
|
+
}
|
|
4416
|
+
var ManagedModelCatalog = class {
|
|
4417
|
+
credentialOwner;
|
|
4418
|
+
baseUrl;
|
|
4419
|
+
fetchImpl;
|
|
4420
|
+
connectionSnapshot;
|
|
4421
|
+
modelIds = new Set(ARKME_MANAGED_FALLBACK_MODELS.map((model) => model.id));
|
|
4422
|
+
hasRemoteSnapshot = false;
|
|
4423
|
+
refreshedAt = 0;
|
|
4424
|
+
refreshPromise;
|
|
4425
|
+
constructor(intelligentBaseUrl, credentialOwner, fetchImpl) {
|
|
4426
|
+
this.credentialOwner = credentialOwner;
|
|
4427
|
+
this.baseUrl = managedAiBaseUrl(intelligentBaseUrl);
|
|
4428
|
+
this.fetchImpl = fetchImpl;
|
|
4429
|
+
this.connectionSnapshot = managedConnection(this.baseUrl, ARKME_MANAGED_FALLBACK_MODELS);
|
|
4430
|
+
}
|
|
4431
|
+
connection() {
|
|
4432
|
+
return this.connectionSnapshot;
|
|
4433
|
+
}
|
|
4434
|
+
isFresh() {
|
|
4435
|
+
return this.hasRemoteSnapshot && Date.now() - this.refreshedAt < ARKME_MANAGED_CATALOG_TTL_MS;
|
|
4436
|
+
}
|
|
4437
|
+
async fetchCatalog(signal) {
|
|
4438
|
+
const controller = new AbortController();
|
|
4439
|
+
let timedOut = false;
|
|
4440
|
+
const abortFromCaller = () => {
|
|
4441
|
+
controller.abort(signal?.reason);
|
|
4442
|
+
};
|
|
4443
|
+
if (signal?.aborted === true) abortFromCaller();
|
|
4444
|
+
else signal?.addEventListener("abort", abortFromCaller, { once: true });
|
|
4445
|
+
const timeout = setTimeout(() => {
|
|
4446
|
+
timedOut = true;
|
|
4447
|
+
controller.abort();
|
|
4448
|
+
}, ARKME_MANAGED_CATALOG_TIMEOUT_MS);
|
|
4449
|
+
try {
|
|
4450
|
+
if (controller.signal.aborted) throw controller.signal.reason ?? new DOMException("Aborted", "AbortError");
|
|
4451
|
+
const bearer = await waitForSignal(resolveBearer(this.credentialOwner), controller.signal);
|
|
4452
|
+
const response = await this.fetchImpl(`${this.baseUrl}/models/query`, {
|
|
4453
|
+
method: "POST",
|
|
4454
|
+
headers: {
|
|
4455
|
+
Accept: "application/json",
|
|
4456
|
+
Authorization: `Bearer ${bearer}`,
|
|
4457
|
+
"Content-Type": "application/json"
|
|
4458
|
+
},
|
|
4459
|
+
body: "{}",
|
|
4460
|
+
signal: controller.signal
|
|
4461
|
+
});
|
|
4462
|
+
if (!response.ok) {
|
|
4463
|
+
const requestId = response.headers.get("x-request-id");
|
|
4464
|
+
throw localizeManagedAiError(new LlmError("Arkme 模型目录请求失败", "MANAGED_MODEL_CATALOG", {
|
|
4465
|
+
status: response.status,
|
|
4466
|
+
...requestId === null || requestId === "" ? {} : { requestId: ProviderRequestId(requestId) }
|
|
4467
|
+
}));
|
|
4468
|
+
}
|
|
4469
|
+
let payload;
|
|
4470
|
+
try {
|
|
4471
|
+
payload = await response.json();
|
|
4472
|
+
} catch (error) {
|
|
4473
|
+
if (controller.signal.aborted) throw error;
|
|
4474
|
+
throw new LlmError("Arkme 模型目录返回异常", "MALFORMED_RESPONSE", { cause: error });
|
|
4475
|
+
}
|
|
4476
|
+
const models = parseManagedCatalog(payload);
|
|
4477
|
+
this.connectionSnapshot = managedConnection(this.baseUrl, models);
|
|
4478
|
+
this.modelIds = new Set(models.map((model) => model.id));
|
|
4479
|
+
this.hasRemoteSnapshot = true;
|
|
4480
|
+
this.refreshedAt = Date.now();
|
|
4481
|
+
} catch (error) {
|
|
4482
|
+
if (signal?.aborted === true) throw localizeManagedAiError(new LlmError("Arkme 模型目录请求已取消", "ABORTED", { cause: error }));
|
|
4483
|
+
if (timedOut) throw localizeManagedAiError(new LlmError("Arkme 模型目录请求超时", "TIMEOUT", { cause: error }));
|
|
4484
|
+
if (error instanceof LlmError) throw error;
|
|
4485
|
+
throw localizeManagedAiError(new LlmError("无法连接 Arkme 模型目录", "TRANSPORT", { cause: error }));
|
|
4486
|
+
} finally {
|
|
4487
|
+
clearTimeout(timeout);
|
|
4488
|
+
signal?.removeEventListener("abort", abortFromCaller);
|
|
4489
|
+
}
|
|
4490
|
+
}
|
|
4491
|
+
refresh(signal) {
|
|
4492
|
+
if (signal !== void 0) return this.fetchCatalog(signal);
|
|
4493
|
+
if (this.refreshPromise !== void 0) return this.refreshPromise;
|
|
4494
|
+
const refresh = this.fetchCatalog(signal).finally(() => {
|
|
4495
|
+
if (this.refreshPromise === refresh) this.refreshPromise = void 0;
|
|
4496
|
+
});
|
|
4497
|
+
this.refreshPromise = refresh;
|
|
4498
|
+
return refresh;
|
|
4499
|
+
}
|
|
4500
|
+
async refreshForListing() {
|
|
4501
|
+
if (this.isFresh()) return;
|
|
4502
|
+
try {
|
|
4503
|
+
await this.refresh();
|
|
4504
|
+
} catch {}
|
|
4505
|
+
}
|
|
4506
|
+
async ensureModel(model, signal) {
|
|
4507
|
+
const known = this.modelIds.has(model);
|
|
4508
|
+
if (known && !this.hasRemoteSnapshot || this.isFresh()) return;
|
|
4509
|
+
try {
|
|
4510
|
+
await this.refresh(signal);
|
|
4511
|
+
} catch (error) {
|
|
4512
|
+
if (signal?.aborted === true) throw error;
|
|
4513
|
+
if (!known) throw error;
|
|
4514
|
+
}
|
|
4515
|
+
}
|
|
4516
|
+
assertModel(model) {
|
|
4517
|
+
if (!this.modelIds.has(model)) throw new LlmError(`当前 Arkme 托管服务不支持模型“${model}”,请重新选择模型`, "UNKNOWN_MODEL");
|
|
4518
|
+
}
|
|
4519
|
+
};
|
|
3935
4520
|
var ManagedAiLlmAdapter = class extends LlmAdapter {
|
|
3936
4521
|
delegate;
|
|
3937
|
-
|
|
4522
|
+
catalog;
|
|
4523
|
+
constructor(delegate, catalog) {
|
|
3938
4524
|
super();
|
|
3939
4525
|
this.delegate = delegate;
|
|
4526
|
+
this.catalog = catalog;
|
|
3940
4527
|
}
|
|
3941
4528
|
providerInfo(provider) {
|
|
3942
4529
|
return {
|
|
@@ -3947,17 +4534,22 @@ var ManagedAiLlmAdapter = class extends LlmAdapter {
|
|
|
3947
4534
|
providerRetryPolicy(provider) {
|
|
3948
4535
|
return this.delegate.providerRetryPolicy(provider);
|
|
3949
4536
|
}
|
|
3950
|
-
listModels(provider) {
|
|
3951
|
-
|
|
3952
|
-
|
|
4537
|
+
async listModels(provider) {
|
|
4538
|
+
assertManagedProvider(provider);
|
|
4539
|
+
await this.catalog.refreshForListing();
|
|
4540
|
+
return await this.delegate.listModels(provider);
|
|
3953
4541
|
}
|
|
3954
4542
|
async resolveModel(provider, model, signal) {
|
|
3955
|
-
|
|
4543
|
+
assertManagedProvider(provider);
|
|
4544
|
+
await this.catalog.ensureModel(model, signal);
|
|
4545
|
+
this.catalog.assertModel(model);
|
|
3956
4546
|
return await this.delegate.resolveModel(provider, model, signal);
|
|
3957
4547
|
}
|
|
3958
4548
|
async *stream(options) {
|
|
3959
|
-
assertManagedRoute(options.provider, options.model);
|
|
3960
4549
|
try {
|
|
4550
|
+
assertManagedProvider(options.provider);
|
|
4551
|
+
await this.catalog.ensureModel(options.model, options.signal);
|
|
4552
|
+
this.catalog.assertModel(options.model);
|
|
3961
4553
|
yield* this.delegate.stream(options);
|
|
3962
4554
|
} catch (error) {
|
|
3963
4555
|
throw localizeManagedAiError(error);
|
|
@@ -3965,29 +4557,12 @@ var ManagedAiLlmAdapter = class extends LlmAdapter {
|
|
|
3965
4557
|
}
|
|
3966
4558
|
};
|
|
3967
4559
|
function createManagedAiLlmAdapter(options) {
|
|
3968
|
-
const
|
|
3969
|
-
baseURL: managedAiBaseUrl(options.intelligentBaseUrl),
|
|
3970
|
-
thinking: "enabled",
|
|
3971
|
-
reasoningEffort: "high",
|
|
3972
|
-
maxTokens: 256e3,
|
|
3973
|
-
defaultContextWindow: 1e6,
|
|
3974
|
-
models: [{
|
|
3975
|
-
id: ARKME_MANAGED_MODEL,
|
|
3976
|
-
name: "DeepSeek-V4-Flash",
|
|
3977
|
-
description: ARKME_MANAGED_MODEL_DESCRIPTION,
|
|
3978
|
-
contextWindow: 1e6,
|
|
3979
|
-
maxTokens: 256e3
|
|
3980
|
-
}],
|
|
3981
|
-
retryPolicy: {
|
|
3982
|
-
mode: "normal",
|
|
3983
|
-
maxRetries: 0
|
|
3984
|
-
}
|
|
3985
|
-
});
|
|
4560
|
+
const catalog = new ManagedModelCatalog(options.intelligentBaseUrl, options.credentialOwner, options.fetchImpl ?? globalThis.fetch);
|
|
3986
4561
|
return new ManagedAiLlmAdapter(new DeepSeekAdapter({
|
|
3987
|
-
options: () => connection,
|
|
4562
|
+
options: () => catalog.connection(),
|
|
3988
4563
|
resolveApiKey: async () => await resolveBearer(options.credentialOwner),
|
|
3989
4564
|
resolveUserId: options.resolveAnonymousUserId ?? getOrCreateAnonymousUserId
|
|
3990
|
-
}));
|
|
4565
|
+
}), catalog);
|
|
3991
4566
|
}
|
|
3992
4567
|
function registerManagedAiProvider(ctx, options) {
|
|
3993
4568
|
return ctx.llm.registerAdapter([ARKME_MANAGED_PROVIDER], createManagedAiLlmAdapter(options));
|
|
@@ -4073,6 +4648,247 @@ var ArkmeRealtimeEvents = class {
|
|
|
4073
4648
|
}
|
|
4074
4649
|
};
|
|
4075
4650
|
//#endregion
|
|
4651
|
+
//#region src/extensions/desktop-quarantine.ts
|
|
4652
|
+
const QUARANTINE_DIRECTORY_NAME = "desktop-extension-quarantine";
|
|
4653
|
+
const RECEIPT_FILE_NAME = "receipt.json";
|
|
4654
|
+
const PACKAGE_NAME_PATTERN = /^(?:@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*$/;
|
|
4655
|
+
const QUARANTINE_ID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
|
|
4656
|
+
const RUNTIME_RELEASE_PATTERN = /^electron-runtime-v1-[a-f0-9]{32}$/;
|
|
4657
|
+
const MAX_FAILURE_SUMMARY_LENGTH = 2e3;
|
|
4658
|
+
const MAX_FAILURE_TAIL_LENGTH = 16384;
|
|
4659
|
+
var ArkmeDesktopExtensionQuarantine = class {
|
|
4660
|
+
options;
|
|
4661
|
+
now;
|
|
4662
|
+
constructor(options) {
|
|
4663
|
+
this.options = options;
|
|
4664
|
+
this.now = options.now ?? (() => Date.now());
|
|
4665
|
+
}
|
|
4666
|
+
async reconcile() {
|
|
4667
|
+
for (const stored of await this.readReceipts("active")) {
|
|
4668
|
+
let changed = false;
|
|
4669
|
+
for (const entry of stored.receipt.entries) {
|
|
4670
|
+
if (entry.resolvedAtMillis !== void 0) continue;
|
|
4671
|
+
const installed = this.installedByPackage(entry.packageName);
|
|
4672
|
+
if (installed !== void 0 && (installed.enabled || installed.active || installed.lastError !== stored.receipt.failureSummary)) this.options.installStore.put({
|
|
4673
|
+
...installed,
|
|
4674
|
+
enabled: false,
|
|
4675
|
+
active: false,
|
|
4676
|
+
lastError: stored.receipt.failureSummary
|
|
4677
|
+
});
|
|
4678
|
+
if (entry.synchronizedAtMillis === void 0) {
|
|
4679
|
+
entry.synchronizedAtMillis = this.now();
|
|
4680
|
+
changed = true;
|
|
4681
|
+
}
|
|
4682
|
+
}
|
|
4683
|
+
if (changed) await this.writeStoredReceipt(stored);
|
|
4684
|
+
}
|
|
4685
|
+
}
|
|
4686
|
+
async status() {
|
|
4687
|
+
const stored = (await this.readReceipts("active")).sort((left, right) => right.receipt.createdAtMillis - left.receipt.createdAtMillis)[0];
|
|
4688
|
+
if (stored === void 0) return {
|
|
4689
|
+
active: false,
|
|
4690
|
+
entries: []
|
|
4691
|
+
};
|
|
4692
|
+
return {
|
|
4693
|
+
active: true,
|
|
4694
|
+
quarantineId: stored.receipt.quarantineId,
|
|
4695
|
+
mode: stored.receipt.mode,
|
|
4696
|
+
failureSummary: stored.receipt.failureSummary,
|
|
4697
|
+
failureLogTail: stored.receipt.failureLogTail,
|
|
4698
|
+
entries: stored.receipt.entries.filter((entry) => entry.resolvedAtMillis === void 0).map((entry) => ({
|
|
4699
|
+
packageName: entry.packageName,
|
|
4700
|
+
...this.installedByPackage(entry.packageName) === void 0 ? {} : { extensionId: this.installedByPackage(entry.packageName).extensionId },
|
|
4701
|
+
dismissed: entry.notificationDismissedAtMillis !== void 0,
|
|
4702
|
+
resolved: false
|
|
4703
|
+
}))
|
|
4704
|
+
};
|
|
4705
|
+
}
|
|
4706
|
+
async dismiss(packageName) {
|
|
4707
|
+
const stored = await this.activeReceiptForPackage(packageName);
|
|
4708
|
+
const entry = stored.receipt.entries.find((item) => item.packageName === packageName);
|
|
4709
|
+
if (entry.notificationDismissedAtMillis === void 0) {
|
|
4710
|
+
entry.notificationDismissedAtMillis = this.now();
|
|
4711
|
+
await this.writeStoredReceipt(stored);
|
|
4712
|
+
}
|
|
4713
|
+
}
|
|
4714
|
+
async reenable(packageName) {
|
|
4715
|
+
const stored = await this.activeReceiptForPackage(packageName);
|
|
4716
|
+
const entry = stored.receipt.entries.find((item) => item.packageName === packageName);
|
|
4717
|
+
entry.reenableRequestedAtMillis = this.now();
|
|
4718
|
+
await this.writeStoredReceipt(stored);
|
|
4719
|
+
let profileEnabled = false;
|
|
4720
|
+
try {
|
|
4721
|
+
await this.options.setProfileEnabled(packageName, true);
|
|
4722
|
+
profileEnabled = true;
|
|
4723
|
+
await this.options.requestRestart({
|
|
4724
|
+
packageName,
|
|
4725
|
+
previousProfileIncluded: false
|
|
4726
|
+
});
|
|
4727
|
+
} catch (error) {
|
|
4728
|
+
if (profileEnabled) await this.options.setProfileEnabled(packageName, false).catch(() => void 0);
|
|
4729
|
+
delete entry.reenableRequestedAtMillis;
|
|
4730
|
+
await this.writeStoredReceipt(stored).catch(() => void 0);
|
|
4731
|
+
throw error;
|
|
4732
|
+
}
|
|
4733
|
+
}
|
|
4734
|
+
async resolveActive() {
|
|
4735
|
+
for (const stored of await this.readReceipts("active")) {
|
|
4736
|
+
let changed = false;
|
|
4737
|
+
for (const entry of stored.receipt.entries) {
|
|
4738
|
+
if (entry.resolvedAtMillis !== void 0) continue;
|
|
4739
|
+
if (entry.reenableRequestedAtMillis !== void 0 && this.options.isPackageActive(entry.packageName)) {
|
|
4740
|
+
entry.resolvedAtMillis = this.now();
|
|
4741
|
+
delete entry.reenableRequestedAtMillis;
|
|
4742
|
+
const installed = this.installedByPackage(entry.packageName);
|
|
4743
|
+
if (installed !== void 0) {
|
|
4744
|
+
const { lastError: _lastError, ...retained } = installed;
|
|
4745
|
+
this.options.installStore.put({
|
|
4746
|
+
...retained,
|
|
4747
|
+
enabled: true,
|
|
4748
|
+
active: true
|
|
4749
|
+
});
|
|
4750
|
+
}
|
|
4751
|
+
changed = true;
|
|
4752
|
+
continue;
|
|
4753
|
+
}
|
|
4754
|
+
if (entry.reenableRequestedAtMillis !== void 0) {
|
|
4755
|
+
await this.options.setProfileEnabled(entry.packageName, false);
|
|
4756
|
+
delete entry.reenableRequestedAtMillis;
|
|
4757
|
+
const installed = this.installedByPackage(entry.packageName);
|
|
4758
|
+
if (installed !== void 0) this.options.installStore.put({
|
|
4759
|
+
...installed,
|
|
4760
|
+
enabled: false,
|
|
4761
|
+
active: false
|
|
4762
|
+
});
|
|
4763
|
+
changed = true;
|
|
4764
|
+
}
|
|
4765
|
+
}
|
|
4766
|
+
if (stored.receipt.entries.every((entry) => entry.resolvedAtMillis !== void 0)) {
|
|
4767
|
+
stored.receipt.phase = "resolved";
|
|
4768
|
+
changed = true;
|
|
4769
|
+
}
|
|
4770
|
+
if (changed) await this.writeStoredReceipt(stored);
|
|
4771
|
+
}
|
|
4772
|
+
}
|
|
4773
|
+
async health(packageName) {
|
|
4774
|
+
if ((await this.activeReceiptForPackage(packageName)).receipt.entries.find((item) => item.packageName === packageName).reenableRequestedAtMillis === void 0) throw new Error("该扩展尚未请求重新启用");
|
|
4775
|
+
const profileEnabled = await this.profileContains(packageName);
|
|
4776
|
+
const active = this.options.isPackageActive(packageName);
|
|
4777
|
+
if (profileEnabled && active) await this.resolveActive();
|
|
4778
|
+
return {
|
|
4779
|
+
profileEnabled,
|
|
4780
|
+
active
|
|
4781
|
+
};
|
|
4782
|
+
}
|
|
4783
|
+
async activeReceiptForPackage(packageName) {
|
|
4784
|
+
if (!isPackageName(packageName) || isProtectedPackage(packageName)) throw new Error("隔离扩展包名无效");
|
|
4785
|
+
const matches = (await this.readReceipts("active")).filter((stored) => stored.receipt.entries.some((entry) => entry.packageName === packageName && entry.resolvedAtMillis === void 0));
|
|
4786
|
+
matches.sort((left, right) => right.receipt.createdAtMillis - left.receipt.createdAtMillis);
|
|
4787
|
+
if (matches[0] === void 0) throw new Error("该扩展没有活动的启动隔离记录");
|
|
4788
|
+
return matches[0];
|
|
4789
|
+
}
|
|
4790
|
+
installedByPackage(packageName) {
|
|
4791
|
+
return this.options.installStore.list().find((item) => item.profilePackageName === packageName);
|
|
4792
|
+
}
|
|
4793
|
+
async profileContains(packageName) {
|
|
4794
|
+
try {
|
|
4795
|
+
const value = JSON.parse(await readFile(join(this.options.dshHome, "profiles", "web", "package.json"), "utf8"));
|
|
4796
|
+
if (!isObject(value) || !isObject(value.dsh) || !isObject(value.dsh.profile) || !Array.isArray(value.dsh.profile.bundles)) return false;
|
|
4797
|
+
return value.dsh.profile.bundles.some((item) => item === packageName);
|
|
4798
|
+
} catch {
|
|
4799
|
+
return false;
|
|
4800
|
+
}
|
|
4801
|
+
}
|
|
4802
|
+
async readReceipts(phase) {
|
|
4803
|
+
const root = join(this.options.dshHome, "arkme-self", QUARANTINE_DIRECTORY_NAME);
|
|
4804
|
+
const directories = await readdir(root, { withFileTypes: true }).catch((error) => {
|
|
4805
|
+
if (error.code === "ENOENT") return [];
|
|
4806
|
+
throw error;
|
|
4807
|
+
});
|
|
4808
|
+
const receipts = [];
|
|
4809
|
+
for (const directory of directories) {
|
|
4810
|
+
if (!directory.isDirectory() || !QUARANTINE_ID_PATTERN.test(directory.name)) continue;
|
|
4811
|
+
const receiptPath = join(root, directory.name, RECEIPT_FILE_NAME);
|
|
4812
|
+
try {
|
|
4813
|
+
const receipt = parseReceipt(await readFile(receiptPath, "utf8"));
|
|
4814
|
+
if (receipt.quarantineId !== directory.name || receipt.environment !== this.options.environment || phase !== void 0 && receipt.phase !== phase) continue;
|
|
4815
|
+
receipts.push({
|
|
4816
|
+
path: receiptPath,
|
|
4817
|
+
receipt
|
|
4818
|
+
});
|
|
4819
|
+
} catch {}
|
|
4820
|
+
}
|
|
4821
|
+
return receipts;
|
|
4822
|
+
}
|
|
4823
|
+
async writeStoredReceipt(stored) {
|
|
4824
|
+
stored.receipt.updatedAtMillis = this.now();
|
|
4825
|
+
await writeTextAtomically(stored.path, `${JSON.stringify(stored.receipt, void 0, 2)}\n`);
|
|
4826
|
+
}
|
|
4827
|
+
};
|
|
4828
|
+
function parseReceipt(text) {
|
|
4829
|
+
const value = JSON.parse(text);
|
|
4830
|
+
if (!isObject(value) || value.schemaVersion !== 1 || typeof value.quarantineId !== "string" || !QUARANTINE_ID_PATTERN.test(value.quarantineId) || value.environment !== "prod" && value.environment !== "test" || ![
|
|
4831
|
+
"pending",
|
|
4832
|
+
"active",
|
|
4833
|
+
"restored",
|
|
4834
|
+
"resolved"
|
|
4835
|
+
].includes(String(value.phase)) || value.mode !== "targeted" && value.mode !== "local-safe-mode" || !Number.isSafeInteger(value.createdAtMillis) || !Number.isSafeInteger(value.updatedAtMillis) || value.runtimeReleaseId !== void 0 && (typeof value.runtimeReleaseId !== "string" || !RUNTIME_RELEASE_PATTERN.test(value.runtimeReleaseId)) || typeof value.failureSummary !== "string" || value.failureSummary.trim() === "" || value.failureSummary.length > MAX_FAILURE_SUMMARY_LENGTH || typeof value.failureLogTail !== "string" || value.failureLogTail.length > MAX_FAILURE_TAIL_LENGTH || !Array.isArray(value.entries) || value.entries.length === 0) throw new Error("桌面扩展隔离记录无效");
|
|
4836
|
+
const entries = value.entries.map((raw) => parseEntry(raw));
|
|
4837
|
+
if (new Set(entries.map((entry) => entry.packageName)).size !== entries.length) throw new Error("桌面扩展隔离记录包含重复扩展");
|
|
4838
|
+
return {
|
|
4839
|
+
schemaVersion: 1,
|
|
4840
|
+
quarantineId: value.quarantineId,
|
|
4841
|
+
environment: value.environment,
|
|
4842
|
+
phase: value.phase,
|
|
4843
|
+
mode: value.mode,
|
|
4844
|
+
createdAtMillis: Number(value.createdAtMillis),
|
|
4845
|
+
updatedAtMillis: Number(value.updatedAtMillis),
|
|
4846
|
+
...typeof value.runtimeReleaseId === "string" ? { runtimeReleaseId: value.runtimeReleaseId } : {},
|
|
4847
|
+
failureSummary: value.failureSummary,
|
|
4848
|
+
failureLogTail: value.failureLogTail,
|
|
4849
|
+
entries
|
|
4850
|
+
};
|
|
4851
|
+
}
|
|
4852
|
+
function parseEntry(value) {
|
|
4853
|
+
if (!isObject(value) || typeof value.packageName !== "string" || !isPackageName(value.packageName) || isProtectedPackage(value.packageName) || typeof value.dependencySpec !== "string" || value.dependencySpec.length === 0 || value.dependencySpec.length > 4096 || !Number.isSafeInteger(value.originalBundleIndex) || Number(value.originalBundleIndex) < 0 || !optionalMillis(value.synchronizedAtMillis) || !optionalMillis(value.notificationDismissedAtMillis) || !optionalMillis(value.reenableRequestedAtMillis) || !optionalMillis(value.resolvedAtMillis)) throw new Error("桌面扩展隔离条目无效");
|
|
4854
|
+
return {
|
|
4855
|
+
packageName: value.packageName,
|
|
4856
|
+
dependencySpec: value.dependencySpec,
|
|
4857
|
+
originalBundleIndex: Number(value.originalBundleIndex),
|
|
4858
|
+
...value.synchronizedAtMillis === void 0 ? {} : { synchronizedAtMillis: Number(value.synchronizedAtMillis) },
|
|
4859
|
+
...value.notificationDismissedAtMillis === void 0 ? {} : { notificationDismissedAtMillis: Number(value.notificationDismissedAtMillis) },
|
|
4860
|
+
...value.reenableRequestedAtMillis === void 0 ? {} : { reenableRequestedAtMillis: Number(value.reenableRequestedAtMillis) },
|
|
4861
|
+
...value.resolvedAtMillis === void 0 ? {} : { resolvedAtMillis: Number(value.resolvedAtMillis) }
|
|
4862
|
+
};
|
|
4863
|
+
}
|
|
4864
|
+
function optionalMillis(value) {
|
|
4865
|
+
return value === void 0 || Number.isSafeInteger(value) && Number(value) >= 0;
|
|
4866
|
+
}
|
|
4867
|
+
function isObject(value) {
|
|
4868
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
4869
|
+
}
|
|
4870
|
+
function isPackageName(value) {
|
|
4871
|
+
return PACKAGE_NAME_PATTERN.test(value);
|
|
4872
|
+
}
|
|
4873
|
+
function isProtectedPackage(packageName) {
|
|
4874
|
+
return packageName === "@senguoyun/dsh-arkme" || packageName.startsWith("@deepseek-ai/");
|
|
4875
|
+
}
|
|
4876
|
+
async function writeTextAtomically(filePath, text) {
|
|
4877
|
+
const temporaryPath = `${filePath}.${process.pid}.${randomUUID()}.tmp`;
|
|
4878
|
+
try {
|
|
4879
|
+
const handle = await open(temporaryPath, "wx", 384);
|
|
4880
|
+
try {
|
|
4881
|
+
await handle.writeFile(text, "utf8");
|
|
4882
|
+
await handle.sync();
|
|
4883
|
+
} finally {
|
|
4884
|
+
await handle.close();
|
|
4885
|
+
}
|
|
4886
|
+
await rename(temporaryPath, filePath);
|
|
4887
|
+
} finally {
|
|
4888
|
+
await rm(temporaryPath, { force: true }).catch(() => void 0);
|
|
4889
|
+
}
|
|
4890
|
+
}
|
|
4891
|
+
//#endregion
|
|
4076
4892
|
//#region src/extensions/install-tasks.ts
|
|
4077
4893
|
const TASK_RETENTION_MILLIS = 600 * 1e3;
|
|
4078
4894
|
const MAX_RETAINED_TASKS = 100;
|
|
@@ -6417,14 +7233,14 @@ var ArkmeExtensionManager = class {
|
|
|
6417
7233
|
};
|
|
6418
7234
|
//#endregion
|
|
6419
7235
|
//#region src/extensions/publish-client.ts
|
|
6420
|
-
function record(value) {
|
|
7236
|
+
function record$1(value) {
|
|
6421
7237
|
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
|
|
6422
7238
|
}
|
|
6423
7239
|
function legacyManifest(value) {
|
|
6424
|
-
const manifest = record(value);
|
|
6425
|
-
const runtime = record(manifest?.runtime);
|
|
6426
|
-
const halves = record(manifest?.halves);
|
|
6427
|
-
const entrypoints = record(manifest?.entrypoints);
|
|
7240
|
+
const manifest = record$1(value);
|
|
7241
|
+
const runtime = record$1(manifest?.runtime);
|
|
7242
|
+
const halves = record$1(manifest?.halves);
|
|
7243
|
+
const entrypoints = record$1(manifest?.entrypoints);
|
|
6428
7244
|
const permissions = manifest?.permissions;
|
|
6429
7245
|
if (manifest?.format !== "arkme-cordis-extension" || manifest.format_version !== 1 || typeof manifest.name !== "string" || typeof manifest.description !== "string" || typeof manifest.version !== "string" || typeof runtime?.dsh !== "string" || typeof runtime.arkme_provider_contract !== "number" || typeof halves?.host !== "boolean" || typeof halves.client !== "boolean" || !Array.isArray(permissions) || !permissions.every((permission) => typeof permission === "string") || entrypoints === void 0 || entrypoints.host !== void 0 && entrypoints.host !== "host.js" || entrypoints.client !== void 0 && entrypoints.client !== "client.js") return void 0;
|
|
6430
7246
|
return {
|
|
@@ -7350,7 +8166,7 @@ var ArkmeExtensionProfileInstaller = class {
|
|
|
7350
8166
|
await this.mutationTail;
|
|
7351
8167
|
if (this.options.stateDirectory === void 0 || this.options.healthUrl === void 0 || this.options.helperPath === void 0 || this.options.restartArgv === void 0 || this.options.installStoreDirectory === void 0) return;
|
|
7352
8168
|
const plan = {
|
|
7353
|
-
schemaVersion: input.activationChange === true ? 3 : input.targetBundlePath?.endsWith(".tgz") === true || input.previousBundlePath?.endsWith(".tgz") === true || input.previousInstalled?.executionModel !== void 0 ? 2 : 1,
|
|
8169
|
+
schemaVersion: input.desktopQuarantineActivation === true ? 4 : input.activationChange === true ? 3 : input.targetBundlePath?.endsWith(".tgz") === true || input.previousBundlePath?.endsWith(".tgz") === true || input.previousInstalled?.executionModel !== void 0 ? 2 : 1,
|
|
7354
8170
|
parentPid: process.pid,
|
|
7355
8171
|
execPath: this.options.execPath,
|
|
7356
8172
|
dshBinPath: this.options.dshBinPath,
|
|
@@ -7370,8 +8186,10 @@ var ArkmeExtensionProfileInstaller = class {
|
|
|
7370
8186
|
activationChange: true,
|
|
7371
8187
|
previousProfileIncluded: input.previousProfileIncluded === true
|
|
7372
8188
|
} : {},
|
|
8189
|
+
...input.desktopQuarantineActivation === true ? { desktopQuarantineActivation: true } : {},
|
|
7373
8190
|
healthUrl: this.options.healthUrl,
|
|
7374
|
-
logPath: join(this.options.stateDirectory, "extension-profile-restart.log")
|
|
8191
|
+
logPath: join(this.options.stateDirectory, "extension-profile-restart.log"),
|
|
8192
|
+
...this.options.runtimeReleaseId === void 0 ? {} : { runtimeReleaseId: this.options.runtimeReleaseId }
|
|
7375
8193
|
};
|
|
7376
8194
|
if (this.options.supervisedExitCode !== void 0) {
|
|
7377
8195
|
if (!Number.isSafeInteger(this.options.supervisedExitCode) || this.options.supervisedExitCode < 1 || this.options.supervisedExitCode > 255) throw new Error("受监督重启退出码无效");
|
|
@@ -7420,6 +8238,17 @@ var ArkmeExtensionProfileInstaller = class {
|
|
|
7420
8238
|
setTimeout(() => process.kill(process.pid, "SIGTERM"), 800).unref?.();
|
|
7421
8239
|
}))();
|
|
7422
8240
|
}
|
|
8241
|
+
async restartDesktopQuarantine(input) {
|
|
8242
|
+
if (!isArkmeProfilePackageName(input.packageName)) throw new Error("扩展 Bundle 包名无效");
|
|
8243
|
+
await this.restart({
|
|
8244
|
+
extensionId: `desktop-quarantine:${input.packageName}`,
|
|
8245
|
+
packageName: input.packageName,
|
|
8246
|
+
expectActive: true,
|
|
8247
|
+
activationChange: true,
|
|
8248
|
+
desktopQuarantineActivation: true,
|
|
8249
|
+
previousProfileIncluded: false
|
|
8250
|
+
});
|
|
8251
|
+
}
|
|
7423
8252
|
async run(args) {
|
|
7424
8253
|
if (this.options.run !== void 0) return await this.options.run(args);
|
|
7425
8254
|
if (!existsSync(this.options.execPath) || !existsSync(this.options.dshBinPath)) throw new Error("当前 DSH 运行方式不支持修改 Profile 插件");
|
|
@@ -8165,7 +8994,7 @@ function sqliteHexText(value) {
|
|
|
8165
8994
|
}
|
|
8166
8995
|
//#endregion
|
|
8167
8996
|
//#region src/state-store.ts
|
|
8168
|
-
function emptyState() {
|
|
8997
|
+
function emptyState$1() {
|
|
8169
8998
|
return {
|
|
8170
8999
|
version: 1,
|
|
8171
9000
|
uniqueCode: randomUUID(),
|
|
@@ -8212,7 +9041,7 @@ function normalizedPending(value) {
|
|
|
8212
9041
|
}
|
|
8213
9042
|
function parseState(raw) {
|
|
8214
9043
|
const parsed = JSON.parse(raw);
|
|
8215
|
-
if (parsed === null || typeof parsed !== "object") return emptyState();
|
|
9044
|
+
if (parsed === null || typeof parsed !== "object") return emptyState$1();
|
|
8216
9045
|
const source = parsed;
|
|
8217
9046
|
const pendingByUser = {};
|
|
8218
9047
|
if (source.pendingByUser !== null && typeof source.pendingByUser === "object") for (const [userId, pending] of Object.entries(source.pendingByUser)) pendingByUser[userId] = normalizedPending(pending);
|
|
@@ -8318,7 +9147,7 @@ var ArkmeStateStore = class {
|
|
|
8318
9147
|
this.state = parseState(await readFile(this.path, "utf8"));
|
|
8319
9148
|
} catch (error) {
|
|
8320
9149
|
if (error.code !== "ENOENT") throw error;
|
|
8321
|
-
this.state = emptyState();
|
|
9150
|
+
this.state = emptyState$1();
|
|
8322
9151
|
await this.write(this.state);
|
|
8323
9152
|
}
|
|
8324
9153
|
return this.state;
|
|
@@ -8414,18 +9243,18 @@ function requiredAgentId$2(agent) {
|
|
|
8414
9243
|
return id;
|
|
8415
9244
|
}
|
|
8416
9245
|
function fingerprintArguments(operationKey, value) {
|
|
8417
|
-
return createHash("sha256").update(`arkme-conversational-confirmation-v1\0${operationKey}\0${canonicalJson(value)}`).digest("hex");
|
|
9246
|
+
return createHash("sha256").update(`arkme-conversational-confirmation-v1\0${operationKey}\0${canonicalJson$1(value)}`).digest("hex");
|
|
8418
9247
|
}
|
|
8419
|
-
function canonicalJson(value) {
|
|
9248
|
+
function canonicalJson$1(value) {
|
|
8420
9249
|
if (value === null || typeof value === "boolean" || typeof value === "string") return JSON.stringify(value);
|
|
8421
9250
|
if (typeof value === "number") {
|
|
8422
9251
|
if (!Number.isFinite(value)) throw new Error("对话确认参数必须是有限 JSON 数值");
|
|
8423
9252
|
return JSON.stringify(value);
|
|
8424
9253
|
}
|
|
8425
|
-
if (Array.isArray(value)) return `[${value.map(canonicalJson).join(",")}]`;
|
|
9254
|
+
if (Array.isArray(value)) return `[${value.map(canonicalJson$1).join(",")}]`;
|
|
8426
9255
|
if (typeof value === "object") {
|
|
8427
9256
|
const source = value;
|
|
8428
|
-
return `{${Object.keys(source).filter((key) => source[key] !== void 0).sort().map((key) => `${JSON.stringify(key)}:${canonicalJson(source[key])}`).join(",")}}`;
|
|
9257
|
+
return `{${Object.keys(source).filter((key) => source[key] !== void 0).sort().map((key) => `${JSON.stringify(key)}:${canonicalJson$1(source[key])}`).join(",")}}`;
|
|
8429
9258
|
}
|
|
8430
9259
|
throw new Error("对话确认参数必须是可序列化 JSON");
|
|
8431
9260
|
}
|
|
@@ -12462,7 +13291,7 @@ function inclusiveRange(from, to) {
|
|
|
12462
13291
|
count
|
|
12463
13292
|
};
|
|
12464
13293
|
}
|
|
12465
|
-
function codePointLength(value) {
|
|
13294
|
+
function codePointLength$1(value) {
|
|
12466
13295
|
return [...value].length;
|
|
12467
13296
|
}
|
|
12468
13297
|
function recordingFingerprint(value) {
|
|
@@ -12547,7 +13376,7 @@ function timelineModelItemLength(item) {
|
|
|
12547
13376
|
item.todo,
|
|
12548
13377
|
...item.tags,
|
|
12549
13378
|
...item.participants
|
|
12550
|
-
].reduce((total, value) => total + codePointLength(value), 0);
|
|
13379
|
+
].reduce((total, value) => total + codePointLength$1(value), 0);
|
|
12551
13380
|
}
|
|
12552
13381
|
const DAYS_OUTPUT_SCHEMA = {
|
|
12553
13382
|
type: "object",
|
|
@@ -12886,7 +13715,7 @@ async function readTranscriptPage(ports, localDate, continued, limit, signal) {
|
|
|
12886
13715
|
while (itemOffset < section.items.length && items.length < limit) {
|
|
12887
13716
|
const item = section.items[itemOffset];
|
|
12888
13717
|
if (item === void 0) break;
|
|
12889
|
-
const itemLength = codePointLength(item.text);
|
|
13718
|
+
const itemLength = codePointLength$1(item.text);
|
|
12890
13719
|
if (itemLength > RECORDING_TEXT_BUDGET) {
|
|
12891
13720
|
itemOffset += 1;
|
|
12892
13721
|
unavailableCount += 1;
|
|
@@ -13460,7 +14289,7 @@ const worldMineToolModule = defineArkmeCoreToolModule({
|
|
|
13460
14289
|
});
|
|
13461
14290
|
//#endregion
|
|
13462
14291
|
//#region src/tools/business/world/user.ts
|
|
13463
|
-
function positiveInteger(value, name) {
|
|
14292
|
+
function positiveInteger$2(value, name) {
|
|
13464
14293
|
if (!Number.isSafeInteger(value) || value <= 0) throw new Error(`${name} 必须是正整数`);
|
|
13465
14294
|
return value;
|
|
13466
14295
|
}
|
|
@@ -13500,7 +14329,7 @@ const worldUserToolModule = defineArkmeCoreToolModule({
|
|
|
13500
14329
|
output: TEXT_OUTPUT$3,
|
|
13501
14330
|
isConcurrencySafe: () => true,
|
|
13502
14331
|
async execute(args, exec) {
|
|
13503
|
-
const result = await ports.listUserWorldFeed(positiveInteger(args.user_id, "user_id"), {
|
|
14332
|
+
const result = await ports.listUserWorldFeed(positiveInteger$2(args.user_id, "user_id"), {
|
|
13504
14333
|
limit: Math.min(20, Math.max(1, Math.trunc(args.limit ?? 20))),
|
|
13505
14334
|
offset: offset(args.offset),
|
|
13506
14335
|
signal: exec.signal
|
|
@@ -14758,10 +15587,2906 @@ function registerArkmeTools(ctx, ports, profile = "business") {
|
|
|
14758
15587
|
}
|
|
14759
15588
|
[...fileToolModules];
|
|
14760
15589
|
//#endregion
|
|
14761
|
-
//#region src/
|
|
14762
|
-
const
|
|
14763
|
-
const
|
|
14764
|
-
|
|
15590
|
+
//#region src/dsh-remote/types.ts
|
|
15591
|
+
const DSH_REMOTE_PROTOCOL = "dsh.remote";
|
|
15592
|
+
const DSH_REMOTE_MAX_FRAME_BYTES = 60 * 1024;
|
|
15593
|
+
const DSH_REMOTE_MAX_PAGE_RESULT_BYTES = 40 * 1024;
|
|
15594
|
+
//#endregion
|
|
15595
|
+
//#region src/dsh-remote/dsh-event-contract.ts
|
|
15596
|
+
function record(value) {
|
|
15597
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) throw new DshRemoteError("REMOTE_TRANSPORT_FAILED", "DSH HistoryEntry 结构无效", true);
|
|
15598
|
+
return value;
|
|
15599
|
+
}
|
|
15600
|
+
function jsonSnapshot(value, field) {
|
|
15601
|
+
try {
|
|
15602
|
+
const encoded = JSON.stringify(value);
|
|
15603
|
+
if (encoded === void 0) throw new TypeError(`${field} is not JSON`);
|
|
15604
|
+
return JSON.parse(encoded);
|
|
15605
|
+
} catch (error) {
|
|
15606
|
+
throw new DshRemoteError("REMOTE_TRANSPORT_FAILED", `DSH ${field} 不是有效 JSON`, true, {}, { cause: error });
|
|
15607
|
+
}
|
|
15608
|
+
}
|
|
15609
|
+
function finiteNumber(value, field) {
|
|
15610
|
+
if (typeof value !== "number" || !Number.isFinite(value)) throw new DshRemoteError("REMOTE_TRANSPORT_FAILED", `DSH ${field} 无效`, true);
|
|
15611
|
+
return value;
|
|
15612
|
+
}
|
|
15613
|
+
function snapshotDshHistoryEntry(value) {
|
|
15614
|
+
const source = record(value.event);
|
|
15615
|
+
if (typeof source.type !== "string" || source.type.length === 0 || !Number.isSafeInteger(source.seq) || Number(source.seq) < 0) throw new DshRemoteError("REMOTE_TRANSPORT_FAILED", "DSH SessionEvent 信封无效", true);
|
|
15616
|
+
const event = {
|
|
15617
|
+
type: source.type,
|
|
15618
|
+
seq: Number(source.seq),
|
|
15619
|
+
time: finiteNumber(source.time, "SessionEvent.time"),
|
|
15620
|
+
data: jsonSnapshot(source.data, "SessionEvent.data")
|
|
15621
|
+
};
|
|
15622
|
+
if (source.sourceEventSeqs !== void 0) {
|
|
15623
|
+
if (!Array.isArray(source.sourceEventSeqs) || source.sourceEventSeqs.some((seq) => !Number.isSafeInteger(seq) || Number(seq) < 0)) throw new DshRemoteError("REMOTE_TRANSPORT_FAILED", "DSH sourceEventSeqs 无效", true);
|
|
15624
|
+
event.sourceEventSeqs = [...source.sourceEventSeqs];
|
|
15625
|
+
}
|
|
15626
|
+
if (source.surfaceOp !== void 0) event.surfaceOp = jsonSnapshot(source.surfaceOp, "SessionEvent.surfaceOp");
|
|
15627
|
+
if (source.ignorable !== void 0) {
|
|
15628
|
+
if (source.ignorable !== true) throw new DshRemoteError("REMOTE_TRANSPORT_FAILED", "DSH ignorable 无效", true);
|
|
15629
|
+
event.ignorable = true;
|
|
15630
|
+
}
|
|
15631
|
+
if (value.view === void 0) return { event };
|
|
15632
|
+
const viewSource = record(value.view);
|
|
15633
|
+
if (viewSource.for !== "call" && viewSource.for !== "result" || viewSource.view === null || typeof viewSource.view !== "object" || Array.isArray(viewSource.view)) throw new DshRemoteError("REMOTE_TRANSPORT_FAILED", "DSH ToolEventView 无效", true);
|
|
15634
|
+
return {
|
|
15635
|
+
event,
|
|
15636
|
+
view: {
|
|
15637
|
+
for: viewSource.for,
|
|
15638
|
+
view: jsonSnapshot(viewSource.view, "ToolEventView.view")
|
|
15639
|
+
}
|
|
15640
|
+
};
|
|
15641
|
+
}
|
|
15642
|
+
function dshHistoryEntryUserRpcId(entry) {
|
|
15643
|
+
if (entry.event.type !== "user/message" || entry.event.data === null || typeof entry.event.data !== "object" || Array.isArray(entry.event.data)) return void 0;
|
|
15644
|
+
const data = entry.event.data;
|
|
15645
|
+
const message = data.message !== null && typeof data.message === "object" && !Array.isArray(data.message) ? data.message : void 0;
|
|
15646
|
+
const source = data.source !== null && typeof data.source === "object" && !Array.isArray(data.source) ? data.source : message?.source !== null && typeof message?.source === "object" && !Array.isArray(message.source) ? message.source : void 0;
|
|
15647
|
+
return typeof source?.rpcId === "string" ? source.rpcId : void 0;
|
|
15648
|
+
}
|
|
15649
|
+
//#endregion
|
|
15650
|
+
//#region src/dsh-remote/api-proxy-adapter.ts
|
|
15651
|
+
function rpcId(prefix) {
|
|
15652
|
+
return `${prefix}-${randomUUID()}`;
|
|
15653
|
+
}
|
|
15654
|
+
function unwrap(response) {
|
|
15655
|
+
if (response.result.ok) return response.result.value;
|
|
15656
|
+
const error = response.result.error;
|
|
15657
|
+
switch (error.code) {
|
|
15658
|
+
case "session-not-found": throw new DshRemoteError("SESSION_NOT_FOUND", "DSH 会话不存在");
|
|
15659
|
+
case "workspace-not-found":
|
|
15660
|
+
case "workspace-invalid-path": throw new DshRemoteError("WORKSPACE_UNAVAILABLE", "DSH 工作目录不可用");
|
|
15661
|
+
case "agent-busy":
|
|
15662
|
+
case "steer-unavailable":
|
|
15663
|
+
case "queue-item-not-found": throw new DshRemoteError("SESSION_STATE_CHANGED", "DSH 会话状态已经变化", true);
|
|
15664
|
+
default: throw new DshRemoteError("REMOTE_TRANSPORT_FAILED", error.message || "DSH 操作失败", true, {
|
|
15665
|
+
dshCode: error.code,
|
|
15666
|
+
dshRejected: true
|
|
15667
|
+
});
|
|
15668
|
+
}
|
|
15669
|
+
}
|
|
15670
|
+
function offsetCursor(offset) {
|
|
15671
|
+
return Buffer.from(JSON.stringify({
|
|
15672
|
+
v: 1,
|
|
15673
|
+
o: offset
|
|
15674
|
+
})).toString("base64url");
|
|
15675
|
+
}
|
|
15676
|
+
function readOffset(cursor) {
|
|
15677
|
+
if (cursor === void 0 || cursor === "") return 0;
|
|
15678
|
+
try {
|
|
15679
|
+
const value = JSON.parse(Buffer.from(cursor, "base64url").toString("utf8"));
|
|
15680
|
+
if (value.v === 1 && Number.isSafeInteger(value.o) && Number(value.o) >= 0) return Number(value.o);
|
|
15681
|
+
} catch {}
|
|
15682
|
+
throw new DshRemoteError("REMOTE_REQUEST_INVALID", "分页游标无效");
|
|
15683
|
+
}
|
|
15684
|
+
function stableSessionId(requestRef) {
|
|
15685
|
+
const bytes = createHash("sha256").update(`dsh-remote-session-v1\n${requestRef}`).digest().subarray(0, 16);
|
|
15686
|
+
bytes[6] = bytes[6] & 15 | 64;
|
|
15687
|
+
bytes[8] = bytes[8] & 63 | 128;
|
|
15688
|
+
const hex = bytes.toString("hex");
|
|
15689
|
+
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
|
|
15690
|
+
}
|
|
15691
|
+
function codePointLength(value) {
|
|
15692
|
+
return [...value].length;
|
|
15693
|
+
}
|
|
15694
|
+
function jsonBytes(value) {
|
|
15695
|
+
return Buffer.byteLength(JSON.stringify(value));
|
|
15696
|
+
}
|
|
15697
|
+
const MAX_ATOMIC_PROJECTION_BYTES = 24 * 1024;
|
|
15698
|
+
function boundedUtf8(value, maxBytes) {
|
|
15699
|
+
if (Buffer.byteLength(value) <= maxBytes) return value;
|
|
15700
|
+
let result = "";
|
|
15701
|
+
let used = 0;
|
|
15702
|
+
for (const point of value) {
|
|
15703
|
+
const bytes = Buffer.byteLength(point);
|
|
15704
|
+
if (used + bytes > maxBytes) break;
|
|
15705
|
+
result += point;
|
|
15706
|
+
used += bytes;
|
|
15707
|
+
}
|
|
15708
|
+
return result;
|
|
15709
|
+
}
|
|
15710
|
+
function pageLimit(value) {
|
|
15711
|
+
return Math.min(50, Math.max(1, Math.trunc(value ?? 50)));
|
|
15712
|
+
}
|
|
15713
|
+
function pageByBytes(input) {
|
|
15714
|
+
const items = [];
|
|
15715
|
+
while (input.offset + items.length < input.all.length && items.length < input.limit) {
|
|
15716
|
+
const candidate = [...items, input.all[input.offset + items.length]];
|
|
15717
|
+
const consumed = input.offset + candidate.length;
|
|
15718
|
+
const nextCursor = consumed < input.all.length ? offsetCursor(consumed) : void 0;
|
|
15719
|
+
const result = input.build(candidate, nextCursor);
|
|
15720
|
+
if (jsonBytes(result) > 40960 || jsonBytes(result) > 524288) break;
|
|
15721
|
+
items.push(candidate.at(-1));
|
|
15722
|
+
}
|
|
15723
|
+
if (items.length === 0 && input.offset < input.all.length) throw new DshRemoteError("CAPABILITY_UNSUPPORTED", "单个 DSH 投影超过远控帧容量");
|
|
15724
|
+
const consumed = input.offset + items.length;
|
|
15725
|
+
return input.build(items, consumed < input.all.length ? offsetCursor(consumed) : void 0);
|
|
15726
|
+
}
|
|
15727
|
+
function projectQuestions(value) {
|
|
15728
|
+
const result = [];
|
|
15729
|
+
for (const raw of value.slice(0, 16)) {
|
|
15730
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) continue;
|
|
15731
|
+
const source = raw;
|
|
15732
|
+
if (typeof source.id !== "string" || typeof source.question !== "string") continue;
|
|
15733
|
+
const options = Array.isArray(source.options) ? source.options.slice(0, 16).flatMap((option) => {
|
|
15734
|
+
if (option === null || typeof option !== "object" || Array.isArray(option)) return [];
|
|
15735
|
+
const item = option;
|
|
15736
|
+
if (typeof item.label !== "string") return [];
|
|
15737
|
+
return [{
|
|
15738
|
+
label: boundedUtf8(item.label, 512),
|
|
15739
|
+
...typeof item.description === "string" ? { description: boundedUtf8(item.description, 2 * 1024) } : {}
|
|
15740
|
+
}];
|
|
15741
|
+
}) : void 0;
|
|
15742
|
+
const projected = {
|
|
15743
|
+
id: boundedUtf8(source.id, 256),
|
|
15744
|
+
question: boundedUtf8(source.question, 4 * 1024),
|
|
15745
|
+
...typeof source.header === "string" ? { header: boundedUtf8(source.header, 512) } : {},
|
|
15746
|
+
...typeof source.detail === "string" ? { detail: boundedUtf8(source.detail, 8 * 1024) } : {},
|
|
15747
|
+
...options === void 0 ? {} : { options },
|
|
15748
|
+
...typeof source.multiSelect === "boolean" ? { multiSelect: source.multiSelect } : {},
|
|
15749
|
+
...source.intent !== null && typeof source.intent === "object" && !Array.isArray(source.intent) && source.intent.kind === "plan-review" && typeof source.intent.approve === "string" ? { intent: {
|
|
15750
|
+
kind: "plan-review",
|
|
15751
|
+
approve: boundedUtf8(source.intent.approve, 512)
|
|
15752
|
+
} } : {}
|
|
15753
|
+
};
|
|
15754
|
+
if (jsonBytes([...result, projected]) > MAX_ATOMIC_PROJECTION_BYTES) return void 0;
|
|
15755
|
+
result.push(projected);
|
|
15756
|
+
}
|
|
15757
|
+
return result;
|
|
15758
|
+
}
|
|
15759
|
+
var DshApiProxyAdapter = class {
|
|
15760
|
+
api;
|
|
15761
|
+
options;
|
|
15762
|
+
pendingInteractions = /* @__PURE__ */ new Map();
|
|
15763
|
+
interactionListeners = /* @__PURE__ */ new Set();
|
|
15764
|
+
projectionListeners = /* @__PURE__ */ new Set();
|
|
15765
|
+
eventController;
|
|
15766
|
+
eventLoop;
|
|
15767
|
+
constructor(api, options = {}) {
|
|
15768
|
+
this.api = api;
|
|
15769
|
+
this.options = options;
|
|
15770
|
+
}
|
|
15771
|
+
capabilities() {
|
|
15772
|
+
const result = [];
|
|
15773
|
+
if (typeof this.api.workspace?.list === "function") result.push("workspace.list");
|
|
15774
|
+
if (typeof this.api.sessions?.list === "function") result.push("session.list");
|
|
15775
|
+
if (typeof this.api.sessions?.create === "function") result.push("session.create");
|
|
15776
|
+
if (typeof this.api.sessions?.history === "function") result.push("session.history");
|
|
15777
|
+
if (typeof this.api.sessions?.prompt === "function") result.push("session.prompt", "session.prompt.queue", "session.prompt.steer");
|
|
15778
|
+
if (typeof this.api.sessions?.cancel === "function") result.push("session.cancel");
|
|
15779
|
+
if (typeof this.api.events?.mux === "function") result.push("session.events");
|
|
15780
|
+
if (typeof this.api.events?.mux === "function" && typeof this.api.respond === "function") result.push("interaction.question.respond", "interaction.approval.respond");
|
|
15781
|
+
return result;
|
|
15782
|
+
}
|
|
15783
|
+
async workspaces() {
|
|
15784
|
+
const list = this.api.workspace?.list;
|
|
15785
|
+
if (typeof list !== "function") this.unsupported("workspace.list");
|
|
15786
|
+
const value = unwrap(await list.call(this.api.workspace, {
|
|
15787
|
+
rpcId: rpcId("remote-workspaces"),
|
|
15788
|
+
payload: {}
|
|
15789
|
+
}));
|
|
15790
|
+
return await Promise.all(value.items.map(async (item) => {
|
|
15791
|
+
let available = false;
|
|
15792
|
+
let path = item.path;
|
|
15793
|
+
try {
|
|
15794
|
+
path = await realpath(item.path);
|
|
15795
|
+
available = (await stat(path)).isDirectory();
|
|
15796
|
+
} catch {
|
|
15797
|
+
available = false;
|
|
15798
|
+
}
|
|
15799
|
+
return {
|
|
15800
|
+
workspaceId: item.workspaceId,
|
|
15801
|
+
title: boundedUtf8(item.title, 512),
|
|
15802
|
+
path,
|
|
15803
|
+
available,
|
|
15804
|
+
sessionIds: [...item.sessionIds]
|
|
15805
|
+
};
|
|
15806
|
+
}));
|
|
15807
|
+
}
|
|
15808
|
+
async workspacePage(input = {}) {
|
|
15809
|
+
const all = (await this.workspaces()).map((item) => ({
|
|
15810
|
+
...item,
|
|
15811
|
+
sessionIds: []
|
|
15812
|
+
}));
|
|
15813
|
+
const offset = readOffset(input.cursor);
|
|
15814
|
+
if (offset > all.length) throw new DshRemoteError("REMOTE_REQUEST_INVALID", "分页游标超出工作目录投影");
|
|
15815
|
+
return pageByBytes({
|
|
15816
|
+
all,
|
|
15817
|
+
offset,
|
|
15818
|
+
limit: pageLimit(input.limit),
|
|
15819
|
+
build: (items, nextCursor) => ({
|
|
15820
|
+
items,
|
|
15821
|
+
...nextCursor === void 0 ? {} : { nextCursor }
|
|
15822
|
+
})
|
|
15823
|
+
});
|
|
15824
|
+
}
|
|
15825
|
+
async sessions(input = {}) {
|
|
15826
|
+
const list = this.api.sessions?.list;
|
|
15827
|
+
if (typeof list !== "function") this.unsupported("session.list");
|
|
15828
|
+
const workspaces = await this.workspaces();
|
|
15829
|
+
const workspaceBySession = /* @__PURE__ */ new Map();
|
|
15830
|
+
for (const workspace of workspaces) for (const sessionId of workspace.sessionIds) workspaceBySession.set(sessionId, workspace.workspaceId);
|
|
15831
|
+
const all = unwrap(await list.call(this.api.sessions, {
|
|
15832
|
+
rpcId: rpcId("remote-sessions"),
|
|
15833
|
+
payload: {}
|
|
15834
|
+
})).items.flatMap((item) => {
|
|
15835
|
+
const workspaceId = workspaceBySession.get(item.sessionId);
|
|
15836
|
+
if (workspaceId === void 0 || input.workspaceId !== void 0 && workspaceId !== input.workspaceId) return [];
|
|
15837
|
+
const titleValue = item.projections?.values.title;
|
|
15838
|
+
return [{
|
|
15839
|
+
sessionId: item.sessionId,
|
|
15840
|
+
workspaceId,
|
|
15841
|
+
...typeof titleValue === "string" && titleValue.trim() !== "" ? { title: titleValue.trim().slice(0, 200) } : {},
|
|
15842
|
+
updatedAt: item.updatedAt,
|
|
15843
|
+
running: item.running,
|
|
15844
|
+
blank: item.blank,
|
|
15845
|
+
...item.projections === void 0 ? {} : { projectionAsOfSeq: item.projections.asOfSeq }
|
|
15846
|
+
}];
|
|
15847
|
+
}).sort((left, right) => right.updatedAt - left.updatedAt);
|
|
15848
|
+
const offset = readOffset(input.cursor);
|
|
15849
|
+
if (offset > all.length) throw new DshRemoteError("REMOTE_REQUEST_INVALID", "分页游标超出会话投影");
|
|
15850
|
+
return pageByBytes({
|
|
15851
|
+
all,
|
|
15852
|
+
offset,
|
|
15853
|
+
limit: pageLimit(input.limit),
|
|
15854
|
+
build: (items, nextCursor) => ({
|
|
15855
|
+
items,
|
|
15856
|
+
...nextCursor === void 0 ? {} : { nextCursor }
|
|
15857
|
+
})
|
|
15858
|
+
});
|
|
15859
|
+
}
|
|
15860
|
+
async history(input) {
|
|
15861
|
+
await this.requireSession(input.sessionId, false);
|
|
15862
|
+
const history = this.api.sessions?.history;
|
|
15863
|
+
if (typeof history !== "function") this.unsupported("session.history");
|
|
15864
|
+
const limit = pageLimit(input.limit);
|
|
15865
|
+
const value = unwrap(await history.call(this.api.sessions, {
|
|
15866
|
+
rpcId: rpcId("remote-history"),
|
|
15867
|
+
payload: {
|
|
15868
|
+
sessionId: input.sessionId,
|
|
15869
|
+
maxMessages: limit,
|
|
15870
|
+
...input.beforeSeq === void 0 ? {} : { beforeSeq: input.beforeSeq }
|
|
15871
|
+
}
|
|
15872
|
+
}));
|
|
15873
|
+
const entries = value.events.map(snapshotDshHistoryEntry);
|
|
15874
|
+
let truncatedByBudget = false;
|
|
15875
|
+
while (entries.length > 0) {
|
|
15876
|
+
const firstSeq = value.events[0]?.event.seq;
|
|
15877
|
+
if (jsonBytes({
|
|
15878
|
+
entries,
|
|
15879
|
+
hasMore: value.hasMore || truncatedByBudget,
|
|
15880
|
+
...firstSeq === void 0 || !value.hasMore && !truncatedByBudget ? {} : { nextCursor: firstSeq },
|
|
15881
|
+
...value.projections === void 0 ? {} : { projectionAsOfSeq: value.projections.asOfSeq }
|
|
15882
|
+
}) <= 524288) break;
|
|
15883
|
+
entries.shift();
|
|
15884
|
+
truncatedByBudget = true;
|
|
15885
|
+
}
|
|
15886
|
+
if (entries.length === 0 && truncatedByBudget) throw new DshRemoteError("CAPABILITY_UNSUPPORTED", "单条 DSH 历史投影超过远控帧容量");
|
|
15887
|
+
const firstSeq = truncatedByBudget ? entries[0]?.event.seq : value.events[0]?.event.seq;
|
|
15888
|
+
return {
|
|
15889
|
+
entries,
|
|
15890
|
+
hasMore: value.hasMore || truncatedByBudget,
|
|
15891
|
+
...firstSeq === void 0 || !value.hasMore && !truncatedByBudget ? {} : { nextCursor: firstSeq },
|
|
15892
|
+
...value.projections === void 0 ? {} : { projectionAsOfSeq: value.projections.asOfSeq }
|
|
15893
|
+
};
|
|
15894
|
+
}
|
|
15895
|
+
historyContainsRpcId(entries, rpcIdValue) {
|
|
15896
|
+
return entries.some((entry) => dshHistoryEntryUserRpcId(entry) === rpcIdValue);
|
|
15897
|
+
}
|
|
15898
|
+
async createSession(input) {
|
|
15899
|
+
const workspace = (await this.workspaces()).find((item) => item.workspaceId === input.workspaceId);
|
|
15900
|
+
if (workspace === void 0 || !workspace.available) throw new DshRemoteError("WORKSPACE_UNAVAILABLE", "工作目录已经删除或不可访问");
|
|
15901
|
+
const create = this.api.sessions?.create;
|
|
15902
|
+
if (typeof create !== "function") this.unsupported("session.create");
|
|
15903
|
+
const sessionId = stableSessionId(input.dshRpcId);
|
|
15904
|
+
const created = unwrap(await create.call(this.api.sessions, {
|
|
15905
|
+
rpcId: input.dshRpcId,
|
|
15906
|
+
payload: {
|
|
15907
|
+
workspaceId: input.workspaceId,
|
|
15908
|
+
sessionId
|
|
15909
|
+
}
|
|
15910
|
+
}));
|
|
15911
|
+
if (created.sessionId !== sessionId) throw new DshRemoteError("REMOTE_INVALID_RESPONSE", "DSH 返回了与预分配值不一致的 SessionId");
|
|
15912
|
+
return created;
|
|
15913
|
+
}
|
|
15914
|
+
async reconcileCreatedSession(input) {
|
|
15915
|
+
const expected = stableSessionId(input.dshRpcId);
|
|
15916
|
+
let cursor;
|
|
15917
|
+
do {
|
|
15918
|
+
const page = await this.sessions({
|
|
15919
|
+
workspaceId: input.workspaceId,
|
|
15920
|
+
limit: 50,
|
|
15921
|
+
...cursor === void 0 ? {} : { cursor }
|
|
15922
|
+
});
|
|
15923
|
+
if (page.items.some((item) => item.sessionId === expected)) return { sessionId: expected };
|
|
15924
|
+
cursor = page.nextCursor;
|
|
15925
|
+
} while (cursor !== void 0);
|
|
15926
|
+
}
|
|
15927
|
+
async prompt(input) {
|
|
15928
|
+
const text = this.textContent(input.content);
|
|
15929
|
+
const session = await this.requireSession(input.sessionId, true);
|
|
15930
|
+
if (input.mode === "steer" && !session.running) throw new DshRemoteError("SESSION_STATE_CHANGED", "会话当前未运行,不能立即引导");
|
|
15931
|
+
const prompt = this.api.sessions?.prompt;
|
|
15932
|
+
if (typeof prompt !== "function") this.unsupported("session.prompt");
|
|
15933
|
+
return unwrap(await prompt.call(this.api.sessions, {
|
|
15934
|
+
rpcId: input.dshRpcId,
|
|
15935
|
+
payload: {
|
|
15936
|
+
sessionId: input.sessionId,
|
|
15937
|
+
mode: input.mode,
|
|
15938
|
+
content: [{
|
|
15939
|
+
type: "text",
|
|
15940
|
+
text
|
|
15941
|
+
}]
|
|
15942
|
+
}
|
|
15943
|
+
}));
|
|
15944
|
+
}
|
|
15945
|
+
async cancel(input) {
|
|
15946
|
+
if (!(await this.requireSession(input.sessionId, false)).running) return { accepted: true };
|
|
15947
|
+
const cancel = this.api.sessions?.cancel;
|
|
15948
|
+
if (typeof cancel !== "function") this.unsupported("session.cancel");
|
|
15949
|
+
return unwrap(await cancel.call(this.api.sessions, {
|
|
15950
|
+
rpcId: input.dshRpcId,
|
|
15951
|
+
payload: { sessionId: input.sessionId }
|
|
15952
|
+
}));
|
|
15953
|
+
}
|
|
15954
|
+
async answerQuestion(input) {
|
|
15955
|
+
const pending = this.pendingInteractions.get(input.interactionRpcRef);
|
|
15956
|
+
if (pending?.kind !== "question" || pending.sessionId !== input.sessionId) this.resolved();
|
|
15957
|
+
await this.respond(input.interactionRpcRef, {
|
|
15958
|
+
sessionId: input.sessionId,
|
|
15959
|
+
answer: input.answer
|
|
15960
|
+
});
|
|
15961
|
+
}
|
|
15962
|
+
async answerApproval(input) {
|
|
15963
|
+
const pending = this.pendingInteractions.get(input.interactionRpcRef);
|
|
15964
|
+
if (pending?.kind !== "approval" || pending.sessionId !== input.sessionId || pending.approvalId !== input.approvalId) this.resolved();
|
|
15965
|
+
if (input.outcome === "allowed-once" && !pending.canAllowOnce) throw new DshRemoteError("CAPABILITY_UNSUPPORTED", "审批上下文不完整,只能在桌面端处理或拒绝");
|
|
15966
|
+
await this.respond(input.interactionRpcRef, {
|
|
15967
|
+
sessionId: input.sessionId,
|
|
15968
|
+
approvalId: input.approvalId,
|
|
15969
|
+
outcome: input.outcome
|
|
15970
|
+
});
|
|
15971
|
+
}
|
|
15972
|
+
pending() {
|
|
15973
|
+
return [...this.pendingInteractions.values()];
|
|
15974
|
+
}
|
|
15975
|
+
subscribeInteractions(listener) {
|
|
15976
|
+
this.interactionListeners.add(listener);
|
|
15977
|
+
return () => {
|
|
15978
|
+
this.interactionListeners.delete(listener);
|
|
15979
|
+
};
|
|
15980
|
+
}
|
|
15981
|
+
subscribeProjectionEvents(listener) {
|
|
15982
|
+
this.projectionListeners.add(listener);
|
|
15983
|
+
return () => {
|
|
15984
|
+
this.projectionListeners.delete(listener);
|
|
15985
|
+
};
|
|
15986
|
+
}
|
|
15987
|
+
startEvents() {
|
|
15988
|
+
if (this.eventLoop !== void 0) return () => {
|
|
15989
|
+
this.stopEvents();
|
|
15990
|
+
};
|
|
15991
|
+
const mux = this.api.events?.mux;
|
|
15992
|
+
if (typeof mux !== "function") return () => void 0;
|
|
15993
|
+
const controller = new AbortController();
|
|
15994
|
+
this.eventController = controller;
|
|
15995
|
+
const loop = this.maintainEvents(mux.bind(this.api.events), controller.signal);
|
|
15996
|
+
this.eventLoop = loop;
|
|
15997
|
+
loop.finally(() => {
|
|
15998
|
+
if (this.eventLoop === loop) this.eventLoop = void 0;
|
|
15999
|
+
if (this.eventController === controller) this.eventController = void 0;
|
|
16000
|
+
});
|
|
16001
|
+
return () => {
|
|
16002
|
+
this.stopEvents();
|
|
16003
|
+
};
|
|
16004
|
+
}
|
|
16005
|
+
stopEvents() {
|
|
16006
|
+
this.eventController?.abort();
|
|
16007
|
+
}
|
|
16008
|
+
async maintainEvents(mux, signal) {
|
|
16009
|
+
let delay = Math.min(5e3, Math.max(10, this.options.eventRetryBaseMillis ?? 250));
|
|
16010
|
+
while (!signal.aborted) {
|
|
16011
|
+
try {
|
|
16012
|
+
await this.consumeEvents(mux, signal);
|
|
16013
|
+
if (signal.aborted) return;
|
|
16014
|
+
} catch (error) {
|
|
16015
|
+
if (signal.aborted) return;
|
|
16016
|
+
console.warn("dsh-arkme: DSH remote mux disconnected:", error instanceof Error ? error.message : String(error));
|
|
16017
|
+
}
|
|
16018
|
+
this.pendingInteractions.clear();
|
|
16019
|
+
this.emitInteractions();
|
|
16020
|
+
await new Promise((resolve) => {
|
|
16021
|
+
const done = () => {
|
|
16022
|
+
signal.removeEventListener("abort", onAbort);
|
|
16023
|
+
resolve();
|
|
16024
|
+
};
|
|
16025
|
+
const timer = setTimeout(done, delay);
|
|
16026
|
+
timer.unref();
|
|
16027
|
+
const onAbort = () => {
|
|
16028
|
+
clearTimeout(timer);
|
|
16029
|
+
done();
|
|
16030
|
+
};
|
|
16031
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
16032
|
+
});
|
|
16033
|
+
delay = Math.min(5e3, delay * 2);
|
|
16034
|
+
}
|
|
16035
|
+
}
|
|
16036
|
+
async snapshot(input = {}) {
|
|
16037
|
+
const workspaces = await this.workspaces();
|
|
16038
|
+
const sessions = [];
|
|
16039
|
+
let cursor;
|
|
16040
|
+
do {
|
|
16041
|
+
const page = await this.sessions({
|
|
16042
|
+
limit: 50,
|
|
16043
|
+
...cursor === void 0 ? {} : { cursor }
|
|
16044
|
+
});
|
|
16045
|
+
sessions.push(...page.items);
|
|
16046
|
+
cursor = page.nextCursor;
|
|
16047
|
+
} while (cursor !== void 0);
|
|
16048
|
+
const flattened = [
|
|
16049
|
+
...workspaces.map((value) => ({
|
|
16050
|
+
kind: "workspace",
|
|
16051
|
+
value: {
|
|
16052
|
+
...value,
|
|
16053
|
+
sessionIds: []
|
|
16054
|
+
}
|
|
16055
|
+
})),
|
|
16056
|
+
...sessions.map((value) => ({
|
|
16057
|
+
kind: "session",
|
|
16058
|
+
value
|
|
16059
|
+
})),
|
|
16060
|
+
...this.pending().map((value) => ({
|
|
16061
|
+
kind: "interaction",
|
|
16062
|
+
value
|
|
16063
|
+
}))
|
|
16064
|
+
];
|
|
16065
|
+
const offset = readOffset(input.cursor);
|
|
16066
|
+
if (offset > flattened.length) throw new DshRemoteError("REMOTE_REQUEST_INVALID", "分页游标超出快照投影");
|
|
16067
|
+
const projectionAsOfMillis = Date.now();
|
|
16068
|
+
return pageByBytes({
|
|
16069
|
+
all: flattened,
|
|
16070
|
+
offset,
|
|
16071
|
+
limit: pageLimit(input.limit),
|
|
16072
|
+
build: (items, nextCursor) => ({
|
|
16073
|
+
projectionAsOfMillis,
|
|
16074
|
+
workspaces: items.flatMap((item) => item.kind === "workspace" ? [item.value] : []),
|
|
16075
|
+
sessions: items.flatMap((item) => item.kind === "session" ? [item.value] : []),
|
|
16076
|
+
pendingInteractions: items.flatMap((item) => item.kind === "interaction" ? [item.value] : []),
|
|
16077
|
+
...nextCursor === void 0 ? {} : { nextCursor }
|
|
16078
|
+
})
|
|
16079
|
+
});
|
|
16080
|
+
}
|
|
16081
|
+
async consumeEvents(mux, signal) {
|
|
16082
|
+
for await (const frame of mux({
|
|
16083
|
+
rpcId: rpcId("remote-mux"),
|
|
16084
|
+
payload: {}
|
|
16085
|
+
}, signal)) {
|
|
16086
|
+
if (signal.aborted) return;
|
|
16087
|
+
const payload = frame.payload;
|
|
16088
|
+
const type = payload.type;
|
|
16089
|
+
let interactionsChanged = false;
|
|
16090
|
+
if (type === "session/event" && typeof payload.sessionId === "string" && payload.event !== null && typeof payload.event === "object" && !Array.isArray(payload.event)) {
|
|
16091
|
+
const event = payload.event;
|
|
16092
|
+
if (typeof event.type === "string" && typeof event.seq === "number" && Number.isSafeInteger(event.seq) && event.seq >= 0 && typeof event.time === "number" && Number.isSafeInteger(event.time) && event.time >= 0) {
|
|
16093
|
+
const entry = snapshotDshHistoryEntry({
|
|
16094
|
+
event,
|
|
16095
|
+
view: payload.view
|
|
16096
|
+
});
|
|
16097
|
+
this.emitProjection({
|
|
16098
|
+
kind: "session-event",
|
|
16099
|
+
sessionId: payload.sessionId,
|
|
16100
|
+
entry
|
|
16101
|
+
});
|
|
16102
|
+
}
|
|
16103
|
+
} else if (type === "session/subscribed" && typeof payload.sessionId === "string" && typeof payload.lastSeq === "number" && Number.isSafeInteger(payload.lastSeq) && payload.lastSeq >= -1) this.emitProjection({
|
|
16104
|
+
kind: "mux-baseline",
|
|
16105
|
+
sessionId: payload.sessionId,
|
|
16106
|
+
lastSeq: payload.lastSeq,
|
|
16107
|
+
pendingInteractions: this.pending()
|
|
16108
|
+
});
|
|
16109
|
+
else if (type === "question/requested" && typeof payload.sessionId === "string" && Array.isArray(payload.questions)) {
|
|
16110
|
+
const questions = projectQuestions(payload.questions);
|
|
16111
|
+
if (questions !== void 0 && questions.length > 0) {
|
|
16112
|
+
this.pendingInteractions.set(frame.rpcId, {
|
|
16113
|
+
kind: "question",
|
|
16114
|
+
interactionRpcRef: frame.rpcId,
|
|
16115
|
+
sessionId: payload.sessionId,
|
|
16116
|
+
questions
|
|
16117
|
+
});
|
|
16118
|
+
interactionsChanged = true;
|
|
16119
|
+
}
|
|
16120
|
+
} else if (type === "approval/requested" && typeof payload.sessionId === "string" && typeof payload.approvalId === "string" && typeof payload.toolName === "string") {
|
|
16121
|
+
const reason = typeof payload.reason === "string" && payload.reason.trim() !== "" ? payload.reason.trim().slice(0, 500) : void 0;
|
|
16122
|
+
this.pendingInteractions.set(frame.rpcId, {
|
|
16123
|
+
kind: "approval",
|
|
16124
|
+
interactionRpcRef: frame.rpcId,
|
|
16125
|
+
sessionId: payload.sessionId,
|
|
16126
|
+
approvalId: payload.approvalId,
|
|
16127
|
+
toolName: payload.toolName,
|
|
16128
|
+
...reason === void 0 ? {} : { reason },
|
|
16129
|
+
canAllowOnce: false
|
|
16130
|
+
});
|
|
16131
|
+
interactionsChanged = true;
|
|
16132
|
+
} else if (type === "question/resolved" && typeof payload.questionRpcId === "string") interactionsChanged = this.pendingInteractions.delete(payload.questionRpcId);
|
|
16133
|
+
else if (type === "approval/resolved") {
|
|
16134
|
+
for (const [key, pending] of this.pendingInteractions) if (pending.kind === "approval" && pending.sessionId === payload.sessionId && pending.approvalId === payload.approvalId) {
|
|
16135
|
+
this.pendingInteractions.delete(key);
|
|
16136
|
+
interactionsChanged = true;
|
|
16137
|
+
}
|
|
16138
|
+
}
|
|
16139
|
+
if (interactionsChanged) this.emitInteractions();
|
|
16140
|
+
}
|
|
16141
|
+
}
|
|
16142
|
+
async requireSession(sessionId, requireWorkspaceAvailable) {
|
|
16143
|
+
const workspace = (await this.workspaces()).find((item) => item.sessionIds.includes(sessionId));
|
|
16144
|
+
if (workspace === void 0) throw new DshRemoteError("SESSION_NOT_FOUND", "会话不属于当前 Runtime 的已登记工作目录");
|
|
16145
|
+
if (requireWorkspaceAvailable && !workspace.available) throw new DshRemoteError("WORKSPACE_UNAVAILABLE", "工作目录已经删除或不可访问");
|
|
16146
|
+
let cursor;
|
|
16147
|
+
do {
|
|
16148
|
+
const page = await this.sessions({
|
|
16149
|
+
workspaceId: workspace.workspaceId,
|
|
16150
|
+
limit: 50,
|
|
16151
|
+
...cursor === void 0 ? {} : { cursor }
|
|
16152
|
+
});
|
|
16153
|
+
const session = page.items.find((item) => item.sessionId === sessionId);
|
|
16154
|
+
if (session !== void 0) return session;
|
|
16155
|
+
cursor = page.nextCursor;
|
|
16156
|
+
} while (cursor !== void 0);
|
|
16157
|
+
throw new DshRemoteError("SESSION_NOT_FOUND", "DSH 会话不存在");
|
|
16158
|
+
}
|
|
16159
|
+
textContent(content) {
|
|
16160
|
+
if (!Array.isArray(content) || content.length !== 1) throw new DshRemoteError("CAPABILITY_UNSUPPORTED", "首版远控只支持单个文本消息");
|
|
16161
|
+
const block = content[0];
|
|
16162
|
+
if (block === null || typeof block !== "object" || Array.isArray(block) || block.type !== "text" || typeof block.text !== "string") throw new DshRemoteError("CAPABILITY_UNSUPPORTED", "首版远控不支持图片、文件或附件");
|
|
16163
|
+
const text = block.text;
|
|
16164
|
+
if (text.trim() === "" || codePointLength(text) > 2e4) throw new DshRemoteError("REMOTE_REQUEST_INVALID", "远控文本必须为 1 至 20000 个字符");
|
|
16165
|
+
if (text.trimStart().startsWith("/")) throw new DshRemoteError("CAPABILITY_UNSUPPORTED", "首版远控不执行 DSH slash command");
|
|
16166
|
+
return text;
|
|
16167
|
+
}
|
|
16168
|
+
async respond(rpcIdValue, value) {
|
|
16169
|
+
if (typeof this.api.respond !== "function") this.unsupported("interaction response");
|
|
16170
|
+
if (!(await this.api.respond({
|
|
16171
|
+
type: "client-response",
|
|
16172
|
+
rpcId: rpcIdValue,
|
|
16173
|
+
result: {
|
|
16174
|
+
ok: true,
|
|
16175
|
+
value
|
|
16176
|
+
}
|
|
16177
|
+
})).accepted) this.resolved();
|
|
16178
|
+
this.pendingInteractions.delete(rpcIdValue);
|
|
16179
|
+
this.emitInteractions();
|
|
16180
|
+
}
|
|
16181
|
+
emitInteractions() {
|
|
16182
|
+
const snapshot = this.pending();
|
|
16183
|
+
for (const listener of this.interactionListeners) listener(snapshot);
|
|
16184
|
+
this.emitProjection({
|
|
16185
|
+
kind: "interactions",
|
|
16186
|
+
pendingInteractions: snapshot
|
|
16187
|
+
});
|
|
16188
|
+
}
|
|
16189
|
+
emitProjection(event) {
|
|
16190
|
+
for (const listener of this.projectionListeners) listener(event);
|
|
16191
|
+
}
|
|
16192
|
+
unsupported(capability) {
|
|
16193
|
+
throw new DshRemoteError("CAPABILITY_UNSUPPORTED", `当前 DSH 版本不支持 ${capability}`);
|
|
16194
|
+
}
|
|
16195
|
+
resolved() {
|
|
16196
|
+
throw new DshRemoteError("INTERACTION_RESOLVED", "问题或审批已在其他端处理");
|
|
16197
|
+
}
|
|
16198
|
+
};
|
|
16199
|
+
//#endregion
|
|
16200
|
+
//#region src/dsh-remote/crypto.ts
|
|
16201
|
+
function encodeBase64Url(value) {
|
|
16202
|
+
return Buffer.from(value).toString("base64url");
|
|
16203
|
+
}
|
|
16204
|
+
function decodeBase64Url(value) {
|
|
16205
|
+
if (!/^[A-Za-z0-9_-]*$/.test(value)) throw new DshRemoteError("REMOTE_REQUEST_INVALID", "无效的 base64url 数据");
|
|
16206
|
+
return Buffer.from(value, "base64url");
|
|
16207
|
+
}
|
|
16208
|
+
function normalizedCanonical(value) {
|
|
16209
|
+
if (Array.isArray(value)) return value.map(normalizedCanonical);
|
|
16210
|
+
if (value !== null && typeof value === "object") {
|
|
16211
|
+
const sorted = {};
|
|
16212
|
+
for (const key of Object.keys(value).sort()) {
|
|
16213
|
+
const child = value[key];
|
|
16214
|
+
if (child !== void 0) sorted[key] = normalizedCanonical(child);
|
|
16215
|
+
}
|
|
16216
|
+
return sorted;
|
|
16217
|
+
}
|
|
16218
|
+
if (typeof value === "number" && !Number.isFinite(value)) throw new DshRemoteError("REMOTE_REQUEST_INVALID", "协议数据不能包含非有限数字");
|
|
16219
|
+
return value;
|
|
16220
|
+
}
|
|
16221
|
+
function canonicalJson(value) {
|
|
16222
|
+
return JSON.stringify(normalizedCanonical(value));
|
|
16223
|
+
}
|
|
16224
|
+
function rotateLeft(value, count) {
|
|
16225
|
+
return (value << count | value >>> 32 - count) >>> 0;
|
|
16226
|
+
}
|
|
16227
|
+
function quarterRound(state, a, b, c, d) {
|
|
16228
|
+
state[a] = state[a] + state[b] >>> 0;
|
|
16229
|
+
state[d] = rotateLeft(state[d] ^ state[a], 16);
|
|
16230
|
+
state[c] = state[c] + state[d] >>> 0;
|
|
16231
|
+
state[b] = rotateLeft(state[b] ^ state[c], 12);
|
|
16232
|
+
state[a] = state[a] + state[b] >>> 0;
|
|
16233
|
+
state[d] = rotateLeft(state[d] ^ state[a], 8);
|
|
16234
|
+
state[c] = state[c] + state[d] >>> 0;
|
|
16235
|
+
state[b] = rotateLeft(state[b] ^ state[c], 7);
|
|
16236
|
+
}
|
|
16237
|
+
function hChaCha20(key, nonce) {
|
|
16238
|
+
if (key.length !== 32 || nonce.length !== 16) throw new TypeError("HChaCha20 requires a 32-byte key and 16-byte nonce");
|
|
16239
|
+
const state = /* @__PURE__ */ new Uint32Array(16);
|
|
16240
|
+
const constants = Buffer.from("expand 32-byte k", "ascii");
|
|
16241
|
+
for (let index = 0; index < 4; index += 1) state[index] = constants.readUInt32LE(index * 4);
|
|
16242
|
+
const keyBuffer = Buffer.from(key);
|
|
16243
|
+
const nonceBuffer = Buffer.from(nonce);
|
|
16244
|
+
for (let index = 0; index < 8; index += 1) state[index + 4] = keyBuffer.readUInt32LE(index * 4);
|
|
16245
|
+
for (let index = 0; index < 4; index += 1) state[index + 12] = nonceBuffer.readUInt32LE(index * 4);
|
|
16246
|
+
for (let round = 0; round < 10; round += 1) {
|
|
16247
|
+
quarterRound(state, 0, 4, 8, 12);
|
|
16248
|
+
quarterRound(state, 1, 5, 9, 13);
|
|
16249
|
+
quarterRound(state, 2, 6, 10, 14);
|
|
16250
|
+
quarterRound(state, 3, 7, 11, 15);
|
|
16251
|
+
quarterRound(state, 0, 5, 10, 15);
|
|
16252
|
+
quarterRound(state, 1, 6, 11, 12);
|
|
16253
|
+
quarterRound(state, 2, 7, 8, 13);
|
|
16254
|
+
quarterRound(state, 3, 4, 9, 14);
|
|
16255
|
+
}
|
|
16256
|
+
const result = Buffer.allocUnsafe(32);
|
|
16257
|
+
for (const [outputIndex, stateIndex] of [
|
|
16258
|
+
0,
|
|
16259
|
+
1,
|
|
16260
|
+
2,
|
|
16261
|
+
3,
|
|
16262
|
+
12,
|
|
16263
|
+
13,
|
|
16264
|
+
14,
|
|
16265
|
+
15
|
|
16266
|
+
].entries()) result.writeUInt32LE(state[stateIndex], outputIndex * 4);
|
|
16267
|
+
return result;
|
|
16268
|
+
}
|
|
16269
|
+
function encryptXChaCha20Poly1305(key, plaintext, aad, nonce = randomBytes(24)) {
|
|
16270
|
+
if (key.length !== 32 || nonce.length !== 24) throw new TypeError("XChaCha20-Poly1305 requires a 32-byte key and 24-byte nonce");
|
|
16271
|
+
const cipher = createCipheriv("chacha20-poly1305", hChaCha20(key, nonce.subarray(0, 16)), Buffer.concat([Buffer.alloc(4), Buffer.from(nonce.subarray(16))]), { authTagLength: 16 });
|
|
16272
|
+
const encodedPlaintext = Buffer.from(plaintext);
|
|
16273
|
+
cipher.setAAD(Buffer.from(aad), { plaintextLength: encodedPlaintext.length });
|
|
16274
|
+
const encrypted = Buffer.concat([
|
|
16275
|
+
cipher.update(encodedPlaintext),
|
|
16276
|
+
cipher.final(),
|
|
16277
|
+
cipher.getAuthTag()
|
|
16278
|
+
]);
|
|
16279
|
+
return {
|
|
16280
|
+
nonce: encodeBase64Url(nonce),
|
|
16281
|
+
ciphertext: encodeBase64Url(encrypted)
|
|
16282
|
+
};
|
|
16283
|
+
}
|
|
16284
|
+
function decryptXChaCha20Poly1305(key, input, aad) {
|
|
16285
|
+
const nonce = decodeBase64Url(input.nonce);
|
|
16286
|
+
const encoded = decodeBase64Url(input.ciphertext);
|
|
16287
|
+
if (key.length !== 32 || nonce.length !== 24 || encoded.length < 16) throw new DshRemoteError("REMOTE_REQUEST_INVALID", "远控密文格式无效");
|
|
16288
|
+
try {
|
|
16289
|
+
const decipher = createDecipheriv("chacha20-poly1305", hChaCha20(key, nonce.subarray(0, 16)), Buffer.concat([Buffer.alloc(4), nonce.subarray(16)]), { authTagLength: 16 });
|
|
16290
|
+
decipher.setAAD(Buffer.from(aad), { plaintextLength: encoded.length - 16 });
|
|
16291
|
+
decipher.setAuthTag(encoded.subarray(encoded.length - 16));
|
|
16292
|
+
return Buffer.concat([decipher.update(encoded.subarray(0, encoded.length - 16)), decipher.final()]);
|
|
16293
|
+
} catch (error) {
|
|
16294
|
+
throw new DshRemoteError("REMOTE_STORAGE_FAILED", "命令账本密文认证失败", false, {}, { cause: error });
|
|
16295
|
+
}
|
|
16296
|
+
}
|
|
16297
|
+
//#endregion
|
|
16298
|
+
//#region src/dsh-remote/command-ledger.ts
|
|
16299
|
+
function argumentsHash(operation, argumentsValue) {
|
|
16300
|
+
return createHash("sha256").update(canonicalJson({
|
|
16301
|
+
operation,
|
|
16302
|
+
arguments: argumentsValue
|
|
16303
|
+
})).digest("base64url");
|
|
16304
|
+
}
|
|
16305
|
+
function stableDshRpcId(input) {
|
|
16306
|
+
return `remote_${createHash("sha256").update(`dsh-remote-rpc-v2\n${input.accountId}\n${input.runtimeRef}\n${input.requestRef}`).digest("base64url").slice(0, 32)}`;
|
|
16307
|
+
}
|
|
16308
|
+
var DshRemoteCommandLedger = class {
|
|
16309
|
+
path;
|
|
16310
|
+
database;
|
|
16311
|
+
key;
|
|
16312
|
+
now;
|
|
16313
|
+
constructor(directory, key, options = {}) {
|
|
16314
|
+
if (key.length !== 32) throw new TypeError("DshRemoteCommandLedger key must be 32 bytes");
|
|
16315
|
+
this.key = Buffer.from(key);
|
|
16316
|
+
this.now = options.now ?? Date.now;
|
|
16317
|
+
mkdirSync(directory, {
|
|
16318
|
+
recursive: true,
|
|
16319
|
+
mode: 448
|
|
16320
|
+
});
|
|
16321
|
+
securePrivateDirectorySync(directory);
|
|
16322
|
+
this.path = join(directory, "remote-command-ledger.sqlite3");
|
|
16323
|
+
this.database = new DatabaseSync(this.path);
|
|
16324
|
+
this.database.exec(`
|
|
16325
|
+
PRAGMA journal_mode = WAL;
|
|
16326
|
+
PRAGMA synchronous = FULL;
|
|
16327
|
+
PRAGMA foreign_keys = ON;
|
|
16328
|
+
CREATE TABLE IF NOT EXISTS remote_command_identity_v2 (
|
|
16329
|
+
identity_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
16330
|
+
account_id TEXT NOT NULL,
|
|
16331
|
+
runtime_ref TEXT NOT NULL,
|
|
16332
|
+
request_ref TEXT NOT NULL,
|
|
16333
|
+
operation TEXT NOT NULL,
|
|
16334
|
+
arguments_hash TEXT NOT NULL,
|
|
16335
|
+
dsh_rpc_id TEXT NOT NULL,
|
|
16336
|
+
execute_before_millis INTEGER NOT NULL,
|
|
16337
|
+
created_at_millis INTEGER NOT NULL,
|
|
16338
|
+
UNIQUE (account_id, runtime_ref, request_ref)
|
|
16339
|
+
);
|
|
16340
|
+
CREATE TABLE IF NOT EXISTS remote_command_event_v2 (
|
|
16341
|
+
event_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
16342
|
+
identity_id INTEGER NOT NULL REFERENCES remote_command_identity_v2(identity_id) ON DELETE CASCADE,
|
|
16343
|
+
state TEXT NOT NULL CHECK (state IN ('pending', 'completed', 'outcome_unknown')),
|
|
16344
|
+
nonce TEXT NOT NULL,
|
|
16345
|
+
ciphertext TEXT NOT NULL,
|
|
16346
|
+
created_at_millis INTEGER NOT NULL
|
|
16347
|
+
);
|
|
16348
|
+
CREATE INDEX IF NOT EXISTS remote_command_event_v2_identity
|
|
16349
|
+
ON remote_command_event_v2 (identity_id, event_id DESC);
|
|
16350
|
+
CREATE INDEX IF NOT EXISTS remote_command_identity_v2_created
|
|
16351
|
+
ON remote_command_identity_v2 (created_at_millis DESC);
|
|
16352
|
+
`);
|
|
16353
|
+
this.secureFiles();
|
|
16354
|
+
}
|
|
16355
|
+
begin(input) {
|
|
16356
|
+
this.validateIdentity(input);
|
|
16357
|
+
const hash = argumentsHash(input.operation, input.arguments);
|
|
16358
|
+
const existing = this.identity(input);
|
|
16359
|
+
if (existing !== void 0) {
|
|
16360
|
+
if (existing.operation !== input.operation || existing.arguments_hash !== hash || existing.account_id !== input.accountId) throw new DshRemoteError("SESSION_STATE_CHANGED", "相同 request_ref 携带了不同操作或参数");
|
|
16361
|
+
return {
|
|
16362
|
+
duplicate: true,
|
|
16363
|
+
entry: this.entry(existing)
|
|
16364
|
+
};
|
|
16365
|
+
}
|
|
16366
|
+
const now = this.now();
|
|
16367
|
+
this.transaction(() => {
|
|
16368
|
+
const result = this.database.prepare(`
|
|
16369
|
+
INSERT INTO remote_command_identity_v2 (
|
|
16370
|
+
account_id, runtime_ref, request_ref, operation, arguments_hash,
|
|
16371
|
+
dsh_rpc_id, execute_before_millis, created_at_millis
|
|
16372
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
16373
|
+
`).run(input.accountId, input.runtimeRef, input.requestRef, input.operation, hash, stableDshRpcId(input), input.executeBeforeMillis, now);
|
|
16374
|
+
this.appendEvent(Number(result.lastInsertRowid), "pending", { arguments: input.arguments }, now);
|
|
16375
|
+
});
|
|
16376
|
+
this.secureFiles();
|
|
16377
|
+
const row = this.identity(input);
|
|
16378
|
+
if (row === void 0) throw new DshRemoteError("REMOTE_STORAGE_FAILED", "命令账本写入失败");
|
|
16379
|
+
return {
|
|
16380
|
+
duplicate: false,
|
|
16381
|
+
entry: this.entry(row)
|
|
16382
|
+
};
|
|
16383
|
+
}
|
|
16384
|
+
complete(identity, result) {
|
|
16385
|
+
const row = this.requireIdentity(identity);
|
|
16386
|
+
const current = this.entry(row);
|
|
16387
|
+
if (current.state === "completed") return current;
|
|
16388
|
+
if (current.state === "outcome_unknown") throw new DshRemoteError("COMMAND_OUTCOME_UNKNOWN", "命令结果尚未完成对账,禁止覆盖");
|
|
16389
|
+
this.appendEvent(row.identity_id, "completed", { result }, this.now());
|
|
16390
|
+
this.secureFiles();
|
|
16391
|
+
return this.entry(row);
|
|
16392
|
+
}
|
|
16393
|
+
completeRejected(identity, error) {
|
|
16394
|
+
return this.complete(identity, { rejected: {
|
|
16395
|
+
code: error.code,
|
|
16396
|
+
message: error.message.slice(0, 500),
|
|
16397
|
+
retryable: error.retryable
|
|
16398
|
+
} });
|
|
16399
|
+
}
|
|
16400
|
+
markOutcomeUnknown(identity, reason) {
|
|
16401
|
+
const row = this.requireIdentity(identity);
|
|
16402
|
+
const current = this.entry(row);
|
|
16403
|
+
if (current.state === "completed" || current.state === "outcome_unknown") return current;
|
|
16404
|
+
this.appendEvent(row.identity_id, "outcome_unknown", { reason: reason.slice(0, 500) }, this.now());
|
|
16405
|
+
this.secureFiles();
|
|
16406
|
+
return this.entry(row);
|
|
16407
|
+
}
|
|
16408
|
+
get(identity) {
|
|
16409
|
+
const row = this.identity(identity);
|
|
16410
|
+
if (row === void 0 || row.account_id !== identity.accountId) return void 0;
|
|
16411
|
+
return this.entry(row);
|
|
16412
|
+
}
|
|
16413
|
+
pending(accountId, nowMillis = this.now()) {
|
|
16414
|
+
return this.database.prepare(`
|
|
16415
|
+
SELECT identity_id, account_id, runtime_ref, request_ref, operation,
|
|
16416
|
+
arguments_hash, dsh_rpc_id, execute_before_millis, created_at_millis
|
|
16417
|
+
FROM remote_command_identity_v2
|
|
16418
|
+
WHERE account_id = ?
|
|
16419
|
+
ORDER BY created_at_millis ASC
|
|
16420
|
+
`).all(accountId).map((row) => this.entry(row)).filter((entry) => entry.state === "pending" && entry.executeBeforeMillis >= nowMillis);
|
|
16421
|
+
}
|
|
16422
|
+
/**
|
|
16423
|
+
* Returns every unsettled command still inside the v4 reconciliation window.
|
|
16424
|
+
*
|
|
16425
|
+
* An expired execute_before only prevents executing the command again. It must
|
|
16426
|
+
* not hide the crash window where DSH accepted the stable rpcId before the Host
|
|
16427
|
+
* persisted the result. Callers must inspect DSH history and either complete
|
|
16428
|
+
* the entry or mark outcome_unknown; they must never re-dispatch these rows.
|
|
16429
|
+
*/
|
|
16430
|
+
unsettledForReconciliation(accountId, nowMillis = this.now(), reconciliationWindowMillis = 1440 * 6e4) {
|
|
16431
|
+
if (!Number.isSafeInteger(reconciliationWindowMillis) || reconciliationWindowMillis <= 0) throw new DshRemoteError("REMOTE_REQUEST_INVALID", "命令对账窗口无效");
|
|
16432
|
+
return this.database.prepare(`
|
|
16433
|
+
SELECT identity_id, account_id, runtime_ref, request_ref, operation,
|
|
16434
|
+
arguments_hash, dsh_rpc_id, execute_before_millis, created_at_millis
|
|
16435
|
+
FROM remote_command_identity_v2
|
|
16436
|
+
WHERE account_id = ? AND created_at_millis >= ?
|
|
16437
|
+
ORDER BY created_at_millis ASC
|
|
16438
|
+
`).all(accountId, nowMillis - reconciliationWindowMillis).map((row) => this.entry(row)).filter((entry) => entry.state === "pending");
|
|
16439
|
+
}
|
|
16440
|
+
cleanup(options = {}) {
|
|
16441
|
+
const cutoff = this.now() - (options.retentionMillis ?? 10080 * 6e4);
|
|
16442
|
+
const maxCommands = Math.min(1e5, Math.max(100, options.maxCommands ?? 1e4));
|
|
16443
|
+
const before = Number(this.database.prepare("SELECT COUNT(*) AS count FROM remote_command_identity_v2").get().count);
|
|
16444
|
+
this.transaction(() => {
|
|
16445
|
+
this.database.prepare("DELETE FROM remote_command_identity_v2 WHERE created_at_millis < ?").run(cutoff);
|
|
16446
|
+
this.database.prepare(`
|
|
16447
|
+
DELETE FROM remote_command_identity_v2
|
|
16448
|
+
WHERE identity_id NOT IN (
|
|
16449
|
+
SELECT identity_id FROM remote_command_identity_v2 ORDER BY created_at_millis DESC LIMIT ?
|
|
16450
|
+
)
|
|
16451
|
+
`).run(maxCommands);
|
|
16452
|
+
});
|
|
16453
|
+
const after = Number(this.database.prepare("SELECT COUNT(*) AS count FROM remote_command_identity_v2").get().count);
|
|
16454
|
+
this.secureFiles();
|
|
16455
|
+
return before - after;
|
|
16456
|
+
}
|
|
16457
|
+
close() {
|
|
16458
|
+
this.database.close();
|
|
16459
|
+
this.secureFiles();
|
|
16460
|
+
this.key.fill(0);
|
|
16461
|
+
}
|
|
16462
|
+
validateIdentity(input) {
|
|
16463
|
+
for (const [name, value] of Object.entries({
|
|
16464
|
+
accountId: input.accountId,
|
|
16465
|
+
runtimeRef: input.runtimeRef,
|
|
16466
|
+
requestRef: input.requestRef
|
|
16467
|
+
})) if (value.trim() === "" || value.length > 256) throw new DshRemoteError("REMOTE_REQUEST_INVALID", `${name} 无效`);
|
|
16468
|
+
if (!Number.isSafeInteger(input.executeBeforeMillis) || input.executeBeforeMillis <= 0) throw new DshRemoteError("REMOTE_REQUEST_INVALID", "命令 deadline 无效");
|
|
16469
|
+
}
|
|
16470
|
+
identity(input) {
|
|
16471
|
+
return this.database.prepare(`
|
|
16472
|
+
SELECT identity_id, account_id, runtime_ref, request_ref, operation,
|
|
16473
|
+
arguments_hash, dsh_rpc_id, execute_before_millis, created_at_millis
|
|
16474
|
+
FROM remote_command_identity_v2
|
|
16475
|
+
WHERE account_id = ? AND runtime_ref = ? AND request_ref = ?
|
|
16476
|
+
`).get(input.accountId, input.runtimeRef, input.requestRef);
|
|
16477
|
+
}
|
|
16478
|
+
requireIdentity(input) {
|
|
16479
|
+
const row = this.identity(input);
|
|
16480
|
+
if (row === void 0 || row.account_id !== input.accountId) throw new DshRemoteError("SESSION_STATE_CHANGED", "命令账本记录不存在");
|
|
16481
|
+
return row;
|
|
16482
|
+
}
|
|
16483
|
+
entry(row) {
|
|
16484
|
+
const event = this.database.prepare(`
|
|
16485
|
+
SELECT state, nonce, ciphertext, created_at_millis
|
|
16486
|
+
FROM remote_command_event_v2 WHERE identity_id = ? ORDER BY event_id DESC LIMIT 1
|
|
16487
|
+
`).get(row.identity_id);
|
|
16488
|
+
if (event === void 0) throw new DshRemoteError("REMOTE_STORAGE_FAILED", "命令账本状态缺失");
|
|
16489
|
+
const aad = this.eventAad(row, event.state, event.created_at_millis);
|
|
16490
|
+
let payload;
|
|
16491
|
+
try {
|
|
16492
|
+
payload = JSON.parse(decryptXChaCha20Poly1305(this.key, event, aad).toString("utf8"));
|
|
16493
|
+
} catch (error) {
|
|
16494
|
+
if (error instanceof DshRemoteError) throw new DshRemoteError("REMOTE_STORAGE_FAILED", "命令账本认证失败", false, {}, { cause: error });
|
|
16495
|
+
throw error;
|
|
16496
|
+
}
|
|
16497
|
+
return {
|
|
16498
|
+
accountId: row.account_id,
|
|
16499
|
+
runtimeRef: row.runtime_ref,
|
|
16500
|
+
requestRef: row.request_ref,
|
|
16501
|
+
operation: row.operation,
|
|
16502
|
+
executeBeforeMillis: row.execute_before_millis,
|
|
16503
|
+
argumentsHash: row.arguments_hash,
|
|
16504
|
+
state: event.state,
|
|
16505
|
+
dshRpcId: row.dsh_rpc_id,
|
|
16506
|
+
payload,
|
|
16507
|
+
createdAtMillis: row.created_at_millis,
|
|
16508
|
+
updatedAtMillis: event.created_at_millis
|
|
16509
|
+
};
|
|
16510
|
+
}
|
|
16511
|
+
appendEvent(identityId, state, payload, now) {
|
|
16512
|
+
const row = this.database.prepare(`
|
|
16513
|
+
SELECT identity_id, account_id, runtime_ref, request_ref, operation,
|
|
16514
|
+
arguments_hash, dsh_rpc_id, execute_before_millis, created_at_millis
|
|
16515
|
+
FROM remote_command_identity_v2 WHERE identity_id = ?
|
|
16516
|
+
`).get(identityId);
|
|
16517
|
+
if (row === void 0) throw new DshRemoteError("REMOTE_STORAGE_FAILED", "命令账本 identity 缺失");
|
|
16518
|
+
const encrypted = encryptXChaCha20Poly1305(this.key, canonicalJson(payload), this.eventAad(row, state, now), randomBytes(24));
|
|
16519
|
+
this.database.prepare(`
|
|
16520
|
+
INSERT INTO remote_command_event_v2 (identity_id, state, nonce, ciphertext, created_at_millis)
|
|
16521
|
+
VALUES (?, ?, ?, ?, ?)
|
|
16522
|
+
`).run(identityId, state, encrypted.nonce, encrypted.ciphertext, now);
|
|
16523
|
+
}
|
|
16524
|
+
eventAad(row, state, now) {
|
|
16525
|
+
return canonicalJson({
|
|
16526
|
+
schemaVersion: 2,
|
|
16527
|
+
identityId: row.identity_id,
|
|
16528
|
+
accountId: row.account_id,
|
|
16529
|
+
runtimeRef: row.runtime_ref,
|
|
16530
|
+
requestRef: row.request_ref,
|
|
16531
|
+
operation: row.operation,
|
|
16532
|
+
argumentsHash: row.arguments_hash,
|
|
16533
|
+
state,
|
|
16534
|
+
createdAtMillis: now
|
|
16535
|
+
});
|
|
16536
|
+
}
|
|
16537
|
+
transaction(work) {
|
|
16538
|
+
this.database.exec("BEGIN IMMEDIATE");
|
|
16539
|
+
try {
|
|
16540
|
+
work();
|
|
16541
|
+
this.database.exec("COMMIT");
|
|
16542
|
+
} catch (error) {
|
|
16543
|
+
this.database.exec("ROLLBACK");
|
|
16544
|
+
throw error;
|
|
16545
|
+
}
|
|
16546
|
+
}
|
|
16547
|
+
secureFiles() {
|
|
16548
|
+
for (const suffix of [
|
|
16549
|
+
"",
|
|
16550
|
+
"-wal",
|
|
16551
|
+
"-shm"
|
|
16552
|
+
]) try {
|
|
16553
|
+
securePrivateFileSync(`${this.path}${suffix}`);
|
|
16554
|
+
} catch (error) {
|
|
16555
|
+
if (error.code !== "ENOENT") throw error;
|
|
16556
|
+
}
|
|
16557
|
+
}
|
|
16558
|
+
};
|
|
16559
|
+
//#endregion
|
|
16560
|
+
//#region src/dsh-remote/control-plane.ts
|
|
16561
|
+
const BASE = "/api/v1/dsh-remote";
|
|
16562
|
+
function pathRef(value) {
|
|
16563
|
+
const normalized = value.trim();
|
|
16564
|
+
if (!/^[A-Za-z0-9._:-]{1,256}$/.test(normalized)) throw new DshRemoteError("REMOTE_REQUEST_INVALID", "远控资源引用无效");
|
|
16565
|
+
return encodeURIComponent(normalized);
|
|
16566
|
+
}
|
|
16567
|
+
var DshRemoteHttpControlPlane = class {
|
|
16568
|
+
request;
|
|
16569
|
+
constructor(request) {
|
|
16570
|
+
this.request = request;
|
|
16571
|
+
}
|
|
16572
|
+
async post(path, body, signal) {
|
|
16573
|
+
try {
|
|
16574
|
+
return await this.request.post(path, body, signal);
|
|
16575
|
+
} catch (error) {
|
|
16576
|
+
throw mapDshRemoteControlPlaneError(error);
|
|
16577
|
+
}
|
|
16578
|
+
}
|
|
16579
|
+
async registerDesktop(input, signal) {
|
|
16580
|
+
return await this.post(`${BASE}/desktops/register`, input, signal);
|
|
16581
|
+
}
|
|
16582
|
+
async registerRuntime(desktopRef, input, signal) {
|
|
16583
|
+
return await this.post(`${BASE}/desktops/${pathRef(desktopRef)}/runtimes/register`, input, signal);
|
|
16584
|
+
}
|
|
16585
|
+
async syncWorkspaces(input, signal) {
|
|
16586
|
+
return await this.post(`${BASE}/workspaces/sync`, input, signal);
|
|
16587
|
+
}
|
|
16588
|
+
async syncSessions(input, signal) {
|
|
16589
|
+
return await this.post(`${BASE}/sessions/sync`, input, signal);
|
|
16590
|
+
}
|
|
16591
|
+
async appendSessionEvents(input, signal) {
|
|
16592
|
+
return await this.post(`${BASE}/session-events/append`, input, signal);
|
|
16593
|
+
}
|
|
16594
|
+
};
|
|
16595
|
+
function mapDshRemoteControlPlaneError(error) {
|
|
16596
|
+
if (error instanceof DshRemoteError) return error;
|
|
16597
|
+
const code = error !== null && typeof error === "object" && typeof error.code === "string" ? error.code : "";
|
|
16598
|
+
const mappedCode = (/* @__PURE__ */ new Set([
|
|
16599
|
+
"REMOTE_PROTOCOL_UNSUPPORTED",
|
|
16600
|
+
"REMOTE_REQUEST_INVALID",
|
|
16601
|
+
"REMOTE_INVALID_RESPONSE",
|
|
16602
|
+
"REMOTE_NETWORK_UNAVAILABLE",
|
|
16603
|
+
"REMOTE_STORAGE_FAILED",
|
|
16604
|
+
"REMOTE_TRANSPORT_FAILED",
|
|
16605
|
+
"REMOTE_LOGIN_REQUIRED",
|
|
16606
|
+
"RUNTIME_OFFLINE",
|
|
16607
|
+
"RUNTIME_LIMIT_REACHED",
|
|
16608
|
+
"HOST_CHANNEL_NOT_READY",
|
|
16609
|
+
"HOST_GENERATION_STALE",
|
|
16610
|
+
"REPLAY_GAP",
|
|
16611
|
+
"CAPABILITY_UNSUPPORTED",
|
|
16612
|
+
"REMOTE_NOT_FOUND",
|
|
16613
|
+
"REMOTE_PROJECTION_CONFLICT",
|
|
16614
|
+
"REMOTE_REALTIME_UNAVAILABLE"
|
|
16615
|
+
])).has(code) ? code : "REMOTE_TRANSPORT_FAILED";
|
|
16616
|
+
return new DshRemoteError(mappedCode, error instanceof Error ? error.message : "远控控制面请求失败", [
|
|
16617
|
+
"REMOTE_NETWORK_UNAVAILABLE",
|
|
16618
|
+
"REMOTE_TRANSPORT_FAILED",
|
|
16619
|
+
"RUNTIME_OFFLINE",
|
|
16620
|
+
"HOST_CHANNEL_NOT_READY",
|
|
16621
|
+
"HOST_GENERATION_STALE",
|
|
16622
|
+
"REPLAY_GAP",
|
|
16623
|
+
"REMOTE_REALTIME_UNAVAILABLE"
|
|
16624
|
+
].includes(mappedCode), {}, { cause: error });
|
|
16625
|
+
}
|
|
16626
|
+
//#endregion
|
|
16627
|
+
//#region src/dsh-remote/default-socket-factory.ts
|
|
16628
|
+
function dshRemoteRealtimeEndpoint(realtimeBaseUrl) {
|
|
16629
|
+
const endpoint = new URL(realtimeBaseUrl);
|
|
16630
|
+
if (endpoint.protocol !== "https:" || endpoint.username !== "" || endpoint.password !== "" || endpoint.pathname !== "/" || endpoint.search !== "" || endpoint.hash !== "") throw new TypeError("DSH remote Realtime requires a credential-free HTTPS service origin");
|
|
16631
|
+
endpoint.protocol = "wss:";
|
|
16632
|
+
endpoint.pathname = "/api/v1/realtime/connect";
|
|
16633
|
+
return endpoint.toString();
|
|
16634
|
+
}
|
|
16635
|
+
function createDefaultDshRemoteSocket(input) {
|
|
16636
|
+
const accessToken = input.accessToken.trim();
|
|
16637
|
+
if (accessToken === "") throw new TypeError("DSH remote Realtime access token is required");
|
|
16638
|
+
if (input.signal.aborted) throw input.signal.reason;
|
|
16639
|
+
const socket = new WebSocket(dshRemoteRealtimeEndpoint(input.realtimeBaseUrl), {
|
|
16640
|
+
headers: { Authorization: `Bearer ${accessToken}` },
|
|
16641
|
+
handshakeTimeout: 1e4,
|
|
16642
|
+
maxPayload: DSH_REMOTE_MAX_FRAME_BYTES,
|
|
16643
|
+
perMessageDeflate: false,
|
|
16644
|
+
followRedirects: false
|
|
16645
|
+
});
|
|
16646
|
+
const closeOnAbort = () => {
|
|
16647
|
+
socket.close(1e3, "aborted");
|
|
16648
|
+
};
|
|
16649
|
+
input.signal.addEventListener("abort", closeOnAbort, { once: true });
|
|
16650
|
+
socket.addEventListener("close", () => {
|
|
16651
|
+
input.signal.removeEventListener("abort", closeOnAbort);
|
|
16652
|
+
}, { once: true });
|
|
16653
|
+
return socket;
|
|
16654
|
+
}
|
|
16655
|
+
//#endregion
|
|
16656
|
+
//#region src/dsh-remote/transport-fragment.ts
|
|
16657
|
+
const DIRECT_PLAINTEXT_BYTES = 32 * 1024;
|
|
16658
|
+
const FRAGMENT_CHUNK_BYTES = 18 * 1024;
|
|
16659
|
+
const MAX_FRAGMENT_COUNT = 256;
|
|
16660
|
+
function sha256(value) {
|
|
16661
|
+
return createHash("sha256").update(value).digest("base64url");
|
|
16662
|
+
}
|
|
16663
|
+
/**
|
|
16664
|
+
* Splits a large typed payload into independently bounded Realtime events.
|
|
16665
|
+
*/
|
|
16666
|
+
function dshRemoteOutboundPayloads(envelope, transferSeed) {
|
|
16667
|
+
const encoded = Buffer.from(JSON.stringify(envelope));
|
|
16668
|
+
if (encoded.length <= DIRECT_PLAINTEXT_BYTES) return [{ value: envelope }];
|
|
16669
|
+
if (encoded.length > 524288) throw new DshRemoteError("CAPABILITY_UNSUPPORTED", "完整 DSH 事件超过 512KiB 远控对象上限", false, {
|
|
16670
|
+
logicalTooLarge: true,
|
|
16671
|
+
payloadBytes: encoded.length
|
|
16672
|
+
});
|
|
16673
|
+
const fragmentCount = Math.ceil(encoded.length / FRAGMENT_CHUNK_BYTES);
|
|
16674
|
+
if (fragmentCount > MAX_FRAGMENT_COUNT) throw new DshRemoteError("CAPABILITY_UNSUPPORTED", "完整 DSH 事件需要过多远控分片", false, {
|
|
16675
|
+
logicalTooLarge: true,
|
|
16676
|
+
fragmentCount
|
|
16677
|
+
});
|
|
16678
|
+
const digest = sha256(encoded);
|
|
16679
|
+
const transferRef = `fragment_${createHash("sha256").update(`${transferSeed}\n${digest}`).digest("base64url").slice(0, 40)}`;
|
|
16680
|
+
return Array.from({ length: fragmentCount }, (_, fragmentIndex) => ({
|
|
16681
|
+
fragmentIndex,
|
|
16682
|
+
commandId: `fragment_${createHash("sha256").update(`${transferRef}\n${fragmentIndex}`).digest("base64url").slice(0, 40)}_${fragmentIndex}`,
|
|
16683
|
+
value: {
|
|
16684
|
+
protocol: "dsh.remote-fragment",
|
|
16685
|
+
protocol_major: 1,
|
|
16686
|
+
kind: "fragment",
|
|
16687
|
+
transfer_ref: transferRef,
|
|
16688
|
+
fragment_index: fragmentIndex,
|
|
16689
|
+
fragment_count: fragmentCount,
|
|
16690
|
+
payload_bytes: encoded.length,
|
|
16691
|
+
payload_sha256: digest,
|
|
16692
|
+
chunk: encoded.subarray(fragmentIndex * FRAGMENT_CHUNK_BYTES, Math.min(encoded.length, (fragmentIndex + 1) * FRAGMENT_CHUNK_BYTES)).toString("base64url")
|
|
16693
|
+
}
|
|
16694
|
+
}));
|
|
16695
|
+
}
|
|
16696
|
+
//#endregion
|
|
16697
|
+
//#region src/dsh-remote/channel-manager.ts
|
|
16698
|
+
/**
|
|
16699
|
+
* Owns the single account-scoped Runtime channel. The Host subscribes with
|
|
16700
|
+
* generation 0 before registration, then activates the exact positive lease
|
|
16701
|
+
* returned by Realtime. No controller identity or pairing state exists here.
|
|
16702
|
+
*/
|
|
16703
|
+
var DshRemoteHostChannelManager = class {
|
|
16704
|
+
options;
|
|
16705
|
+
controller = new AbortController();
|
|
16706
|
+
unsubscribe;
|
|
16707
|
+
target;
|
|
16708
|
+
lastTransportSequence = 0;
|
|
16709
|
+
persistedTransportSequence = 0;
|
|
16710
|
+
outboundTail = Promise.resolve();
|
|
16711
|
+
pendingEvents = [];
|
|
16712
|
+
closed = false;
|
|
16713
|
+
constructor(options) {
|
|
16714
|
+
this.options = options;
|
|
16715
|
+
this.target = {
|
|
16716
|
+
runtimeRef: options.runtimeRef,
|
|
16717
|
+
hostProfileRef: options.profileRef,
|
|
16718
|
+
hostClientRef: options.hostClientRef,
|
|
16719
|
+
hostLeaseGeneration: 0
|
|
16720
|
+
};
|
|
16721
|
+
}
|
|
16722
|
+
async prepare() {
|
|
16723
|
+
if (this.closed || this.unsubscribe !== void 0) throw new DshRemoteError("REMOTE_REQUEST_INVALID", "Runtime channel 已准备");
|
|
16724
|
+
this.lastTransportSequence = await this.options.secretBroker.runtimeCursor({
|
|
16725
|
+
accountId: this.options.accountId,
|
|
16726
|
+
runtimeRef: this.options.runtimeRef,
|
|
16727
|
+
channelRef: this.options.runtimeRef
|
|
16728
|
+
});
|
|
16729
|
+
this.persistedTransportSequence = this.lastTransportSequence;
|
|
16730
|
+
const subscribe = async (afterSequence) => await this.options.realtime.subscribe({
|
|
16731
|
+
target: this.target,
|
|
16732
|
+
...afterSequence === void 0 ? {} : { afterSequence },
|
|
16733
|
+
onEvent: (payload, metadata) => {
|
|
16734
|
+
this.handle(payload, metadata).catch((error) => {
|
|
16735
|
+
this.options.onFatal(error);
|
|
16736
|
+
});
|
|
16737
|
+
},
|
|
16738
|
+
signal: this.controller.signal
|
|
16739
|
+
});
|
|
16740
|
+
try {
|
|
16741
|
+
this.unsubscribe = await subscribe(this.lastTransportSequence);
|
|
16742
|
+
} catch (error) {
|
|
16743
|
+
if (!(error instanceof DshRemoteError) || error.code !== "REPLAY_GAP") throw error;
|
|
16744
|
+
this.unsubscribe = await subscribe();
|
|
16745
|
+
}
|
|
16746
|
+
}
|
|
16747
|
+
async activate(serviceLeaseGeneration) {
|
|
16748
|
+
if (this.closed || this.unsubscribe === void 0 || !Number.isSafeInteger(serviceLeaseGeneration) || serviceLeaseGeneration <= 0) throw new DshRemoteError("HOST_CHANNEL_NOT_READY", "Runtime channel 尚未完成 Host 注册", true);
|
|
16749
|
+
this.target = {
|
|
16750
|
+
...this.target,
|
|
16751
|
+
hostLeaseGeneration: serviceLeaseGeneration
|
|
16752
|
+
};
|
|
16753
|
+
for (const event of this.pendingEvents.splice(0)) await this.handle(event.payload, event.metadata);
|
|
16754
|
+
}
|
|
16755
|
+
status() {
|
|
16756
|
+
return {
|
|
16757
|
+
runtimeRef: this.options.runtimeRef,
|
|
16758
|
+
serviceLeaseGeneration: this.target.hostLeaseGeneration,
|
|
16759
|
+
ready: !this.closed && this.unsubscribe !== void 0 && this.target.hostLeaseGeneration > 0
|
|
16760
|
+
};
|
|
16761
|
+
}
|
|
16762
|
+
async close() {
|
|
16763
|
+
if (this.closed) return;
|
|
16764
|
+
this.closed = true;
|
|
16765
|
+
if (this.lastTransportSequence > this.persistedTransportSequence) await this.persistCursor().catch(() => void 0);
|
|
16766
|
+
this.unsubscribe?.();
|
|
16767
|
+
this.unsubscribe = void 0;
|
|
16768
|
+
this.controller.abort();
|
|
16769
|
+
}
|
|
16770
|
+
async publishProjectionEvent(envelope, commandId) {
|
|
16771
|
+
if (!this.status().ready) return;
|
|
16772
|
+
await this.publishPayload(envelope, commandId, "event");
|
|
16773
|
+
}
|
|
16774
|
+
async handle(payload, metadata) {
|
|
16775
|
+
if (this.closed) return;
|
|
16776
|
+
if (metadata.transportSequence !== void 0 && metadata.transportSequence > this.lastTransportSequence) {
|
|
16777
|
+
this.lastTransportSequence = metadata.transportSequence;
|
|
16778
|
+
if (this.lastTransportSequence - this.persistedTransportSequence >= 16) {
|
|
16779
|
+
this.persistedTransportSequence = this.lastTransportSequence;
|
|
16780
|
+
this.persistCursor().catch((error) => {
|
|
16781
|
+
this.options.onFatal(error);
|
|
16782
|
+
});
|
|
16783
|
+
}
|
|
16784
|
+
}
|
|
16785
|
+
if (metadata.senderRole === "host") return;
|
|
16786
|
+
if (this.target.hostLeaseGeneration === 0) {
|
|
16787
|
+
if (this.pendingEvents.length >= 8) throw new DshRemoteError("HOST_CHANNEL_NOT_READY", "Host 注册期间收到过多并发请求", true);
|
|
16788
|
+
this.pendingEvents.push({
|
|
16789
|
+
payload,
|
|
16790
|
+
metadata
|
|
16791
|
+
});
|
|
16792
|
+
return;
|
|
16793
|
+
}
|
|
16794
|
+
if (metadata.senderRole !== "controller" || metadata.runtimeRef !== this.options.runtimeRef || metadata.targetHostLeaseGeneration !== this.target.hostLeaseGeneration) throw new DshRemoteError("HOST_GENERATION_STALE", "远控请求不属于当前 Runtime Host lease", true);
|
|
16795
|
+
const result = await this.options.dispatch(payload, {
|
|
16796
|
+
serviceLeaseGeneration: this.target.hostLeaseGeneration,
|
|
16797
|
+
metadata
|
|
16798
|
+
});
|
|
16799
|
+
const responseCommandId = `response_${createHash("sha256").update(result.request_ref).digest("base64url")}`;
|
|
16800
|
+
await this.publishPayload(result, responseCommandId, "response");
|
|
16801
|
+
}
|
|
16802
|
+
async publishPayload(envelope, commandId, direction) {
|
|
16803
|
+
const publish = async () => {
|
|
16804
|
+
const frames = dshRemoteOutboundPayloads(envelope, commandId);
|
|
16805
|
+
for (const frame of frames) {
|
|
16806
|
+
if (!this.status().ready) return;
|
|
16807
|
+
await this.options.realtime.publish({
|
|
16808
|
+
target: this.target,
|
|
16809
|
+
commandId: frame.commandId ?? commandId,
|
|
16810
|
+
direction,
|
|
16811
|
+
payload: frame.value,
|
|
16812
|
+
signal: this.controller.signal
|
|
16813
|
+
});
|
|
16814
|
+
}
|
|
16815
|
+
};
|
|
16816
|
+
const result = this.outboundTail.then(publish);
|
|
16817
|
+
this.outboundTail = result.catch(() => void 0);
|
|
16818
|
+
await result;
|
|
16819
|
+
}
|
|
16820
|
+
async persistCursor() {
|
|
16821
|
+
await this.options.secretBroker.putRuntimeCursor({
|
|
16822
|
+
accountId: this.options.accountId,
|
|
16823
|
+
runtimeRef: this.options.runtimeRef,
|
|
16824
|
+
channelRef: this.options.runtimeRef,
|
|
16825
|
+
lastTransportSequence: this.lastTransportSequence
|
|
16826
|
+
});
|
|
16827
|
+
}
|
|
16828
|
+
};
|
|
16829
|
+
//#endregion
|
|
16830
|
+
//#region src/dsh-remote/protocol-v1.ts
|
|
16831
|
+
const OPERATIONS = /* @__PURE__ */ new Set([
|
|
16832
|
+
"workspace.list",
|
|
16833
|
+
"session.create",
|
|
16834
|
+
"session.list",
|
|
16835
|
+
"session.history",
|
|
16836
|
+
"session.prompt",
|
|
16837
|
+
"session.cancel",
|
|
16838
|
+
"interaction.question.respond",
|
|
16839
|
+
"interaction.approval.respond",
|
|
16840
|
+
"snapshot.get",
|
|
16841
|
+
"capabilities.get"
|
|
16842
|
+
]);
|
|
16843
|
+
const REQUEST_KEYS = /* @__PURE__ */ new Set([
|
|
16844
|
+
"protocol",
|
|
16845
|
+
"protocol_major",
|
|
16846
|
+
"kind",
|
|
16847
|
+
"request_ref",
|
|
16848
|
+
"host_generation",
|
|
16849
|
+
"issued_at",
|
|
16850
|
+
"execute_before",
|
|
16851
|
+
"operation",
|
|
16852
|
+
"body"
|
|
16853
|
+
]);
|
|
16854
|
+
function object(value) {
|
|
16855
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) throw new DshRemoteError("REMOTE_REQUEST_INVALID", "远控请求必须为对象");
|
|
16856
|
+
return value;
|
|
16857
|
+
}
|
|
16858
|
+
function ref(value, name) {
|
|
16859
|
+
if (typeof value !== "string" || value.length < 8 || value.length > 128 || !/^[A-Za-z0-9._:-]+$/.test(value)) throw new DshRemoteError("REMOTE_REQUEST_INVALID", `${name} 无效`);
|
|
16860
|
+
return value;
|
|
16861
|
+
}
|
|
16862
|
+
function positiveInteger$1(value, name) {
|
|
16863
|
+
if (typeof value !== "number" || !Number.isSafeInteger(value) || value <= 0) throw new DshRemoteError("REMOTE_REQUEST_INVALID", `${name} 无效`);
|
|
16864
|
+
return value;
|
|
16865
|
+
}
|
|
16866
|
+
function assertOnly(source, keys) {
|
|
16867
|
+
const allowed = new Set(keys);
|
|
16868
|
+
if (Object.keys(source).some((key) => !allowed.has(key))) throw new DshRemoteError("REMOTE_REQUEST_INVALID", "远控操作 body 包含未定义字段");
|
|
16869
|
+
}
|
|
16870
|
+
function bodyRef(source, key, required = true) {
|
|
16871
|
+
const value = source[key];
|
|
16872
|
+
if (value === void 0 && !required) return;
|
|
16873
|
+
if (typeof value !== "string" || value.length < 1 || value.length > 256) throw new DshRemoteError("REMOTE_REQUEST_INVALID", `${key} 无效`);
|
|
16874
|
+
}
|
|
16875
|
+
function pageFields(source) {
|
|
16876
|
+
if (source.cursor !== void 0 && (typeof source.cursor !== "string" || source.cursor.length < 1 || source.cursor.length > 512)) throw new DshRemoteError("REMOTE_REQUEST_INVALID", "cursor 无效");
|
|
16877
|
+
if (source.limit !== void 0 && (!Number.isSafeInteger(source.limit) || Number(source.limit) < 1 || Number(source.limit) > 100)) throw new DshRemoteError("REMOTE_REQUEST_INVALID", "limit 无效");
|
|
16878
|
+
}
|
|
16879
|
+
function validateOperationBody(operation, source) {
|
|
16880
|
+
switch (operation) {
|
|
16881
|
+
case "capabilities.get":
|
|
16882
|
+
assertOnly(source, []);
|
|
16883
|
+
return;
|
|
16884
|
+
case "snapshot.get":
|
|
16885
|
+
case "workspace.list":
|
|
16886
|
+
assertOnly(source, ["cursor", "limit"]);
|
|
16887
|
+
pageFields(source);
|
|
16888
|
+
return;
|
|
16889
|
+
case "session.list":
|
|
16890
|
+
assertOnly(source, [
|
|
16891
|
+
"workspace_ref",
|
|
16892
|
+
"cursor",
|
|
16893
|
+
"limit"
|
|
16894
|
+
]);
|
|
16895
|
+
bodyRef(source, "workspace_ref", false);
|
|
16896
|
+
pageFields(source);
|
|
16897
|
+
return;
|
|
16898
|
+
case "session.create":
|
|
16899
|
+
assertOnly(source, ["workspace_ref"]);
|
|
16900
|
+
bodyRef(source, "workspace_ref");
|
|
16901
|
+
return;
|
|
16902
|
+
case "session.history":
|
|
16903
|
+
assertOnly(source, [
|
|
16904
|
+
"session_ref",
|
|
16905
|
+
"before_seq",
|
|
16906
|
+
"limit"
|
|
16907
|
+
]);
|
|
16908
|
+
bodyRef(source, "session_ref");
|
|
16909
|
+
if (source.before_seq !== void 0 && (!Number.isSafeInteger(source.before_seq) || Number(source.before_seq) < 1)) throw new DshRemoteError("REMOTE_REQUEST_INVALID", "before_seq 无效");
|
|
16910
|
+
pageFields(source);
|
|
16911
|
+
return;
|
|
16912
|
+
case "session.prompt": {
|
|
16913
|
+
assertOnly(source, [
|
|
16914
|
+
"session_ref",
|
|
16915
|
+
"mode",
|
|
16916
|
+
"content"
|
|
16917
|
+
]);
|
|
16918
|
+
bodyRef(source, "session_ref");
|
|
16919
|
+
if (source.mode !== "queue" && source.mode !== "steer") throw new DshRemoteError("REMOTE_REQUEST_INVALID", "mode 无效");
|
|
16920
|
+
const content = object(source.content);
|
|
16921
|
+
if (content.type !== "text") throw new DshRemoteError("CAPABILITY_UNSUPPORTED", "首版远控只支持有界普通文本");
|
|
16922
|
+
assertOnly(content, ["type", "text"]);
|
|
16923
|
+
if (typeof content.text !== "string" || content.text.length < 1 || content.text.length > 16e3) throw new DshRemoteError("CAPABILITY_UNSUPPORTED", "首版远控只支持有界普通文本");
|
|
16924
|
+
return;
|
|
16925
|
+
}
|
|
16926
|
+
case "session.cancel":
|
|
16927
|
+
assertOnly(source, ["session_ref"]);
|
|
16928
|
+
bodyRef(source, "session_ref");
|
|
16929
|
+
return;
|
|
16930
|
+
case "interaction.question.respond":
|
|
16931
|
+
assertOnly(source, [
|
|
16932
|
+
"session_ref",
|
|
16933
|
+
"interaction_rpc_ref",
|
|
16934
|
+
"answer"
|
|
16935
|
+
]);
|
|
16936
|
+
bodyRef(source, "session_ref");
|
|
16937
|
+
bodyRef(source, "interaction_rpc_ref");
|
|
16938
|
+
if (!Object.hasOwn(source, "answer")) throw new DshRemoteError("REMOTE_REQUEST_INVALID", "answer 缺失");
|
|
16939
|
+
return;
|
|
16940
|
+
case "interaction.approval.respond":
|
|
16941
|
+
assertOnly(source, [
|
|
16942
|
+
"session_ref",
|
|
16943
|
+
"interaction_rpc_ref",
|
|
16944
|
+
"approval_id",
|
|
16945
|
+
"outcome"
|
|
16946
|
+
]);
|
|
16947
|
+
bodyRef(source, "session_ref");
|
|
16948
|
+
bodyRef(source, "interaction_rpc_ref");
|
|
16949
|
+
bodyRef(source, "approval_id");
|
|
16950
|
+
if (source.outcome !== "allowed-once" && source.outcome !== "rejected") throw new DshRemoteError("REMOTE_REQUEST_INVALID", "outcome 无效");
|
|
16951
|
+
}
|
|
16952
|
+
}
|
|
16953
|
+
function parseDshRemoteRequest(value, options) {
|
|
16954
|
+
if (Buffer.from(JSON.stringify(value)).length > 61440) throw new DshRemoteError("REMOTE_REQUEST_INVALID", "远控请求超过 60KiB");
|
|
16955
|
+
const source = object(value);
|
|
16956
|
+
if (Object.keys(source).some((key) => !REQUEST_KEYS.has(key))) throw new DshRemoteError("REMOTE_REQUEST_INVALID", "远控请求包含未定义字段");
|
|
16957
|
+
if (source.protocol !== "dsh.remote" || source.protocol_major !== 1) throw new DshRemoteError("REMOTE_PROTOCOL_UNSUPPORTED", "远控协议版本不受支持");
|
|
16958
|
+
if (source.kind !== "request") throw new DshRemoteError("REMOTE_REQUEST_INVALID", "远控消息类型无效");
|
|
16959
|
+
const hostGeneration = positiveInteger$1(source.host_generation, "host_generation");
|
|
16960
|
+
if (hostGeneration !== options.expectedHostGeneration) throw new DshRemoteError("HOST_GENERATION_STALE", "请求指向的 Host 实例已经失效", true);
|
|
16961
|
+
const issuedAt = positiveInteger$1(source.issued_at, "issued_at");
|
|
16962
|
+
const executeBefore = positiveInteger$1(source.execute_before, "execute_before");
|
|
16963
|
+
const now = options.nowMillis ?? Date.now();
|
|
16964
|
+
if (executeBefore < now) throw new DshRemoteError("COMMAND_EXPIRED", "远控命令已经过期");
|
|
16965
|
+
if (issuedAt > now + 3e4 || executeBefore > now + 5 * 6e4 || executeBefore < issuedAt) throw new DshRemoteError("REMOTE_REQUEST_INVALID", "远控命令时间窗口无效");
|
|
16966
|
+
if (typeof source.operation !== "string" || !OPERATIONS.has(source.operation)) throw new DshRemoteError("CAPABILITY_UNSUPPORTED", "远控操作不受支持");
|
|
16967
|
+
const body = object(source.body);
|
|
16968
|
+
validateOperationBody(source.operation, body);
|
|
16969
|
+
return {
|
|
16970
|
+
protocol: DSH_REMOTE_PROTOCOL,
|
|
16971
|
+
protocol_major: 1,
|
|
16972
|
+
kind: "request",
|
|
16973
|
+
request_ref: ref(source.request_ref, "request_ref"),
|
|
16974
|
+
host_generation: hostGeneration,
|
|
16975
|
+
issued_at: issuedAt,
|
|
16976
|
+
execute_before: executeBefore,
|
|
16977
|
+
operation: source.operation,
|
|
16978
|
+
body
|
|
16979
|
+
};
|
|
16980
|
+
}
|
|
16981
|
+
//#endregion
|
|
16982
|
+
//#region src/dsh-remote/host.ts
|
|
16983
|
+
function stringBody(body, key, max = 256) {
|
|
16984
|
+
const value = body[key];
|
|
16985
|
+
if (typeof value !== "string" || value.trim() === "" || value.length > max) throw new DshRemoteError("REMOTE_REQUEST_INVALID", `${key} 无效`);
|
|
16986
|
+
return value.trim();
|
|
16987
|
+
}
|
|
16988
|
+
function optionalString(body, key) {
|
|
16989
|
+
return typeof body[key] === "string" && body[key] !== "" ? body[key] : void 0;
|
|
16990
|
+
}
|
|
16991
|
+
function optionalPositive(body, key) {
|
|
16992
|
+
const value = body[key];
|
|
16993
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value > 0 ? value : void 0;
|
|
16994
|
+
}
|
|
16995
|
+
function requiredCapabilities(operation) {
|
|
16996
|
+
switch (operation) {
|
|
16997
|
+
case "capabilities.get": return [];
|
|
16998
|
+
case "snapshot.get": return ["workspace.list", "session.list"];
|
|
16999
|
+
case "workspace.list": return ["workspace.list"];
|
|
17000
|
+
case "session.list": return ["session.list"];
|
|
17001
|
+
case "session.create": return ["session.create"];
|
|
17002
|
+
case "session.history": return ["session.history"];
|
|
17003
|
+
case "session.prompt": return ["session.prompt"];
|
|
17004
|
+
case "session.cancel": return ["session.cancel"];
|
|
17005
|
+
case "interaction.question.respond": return ["interaction.question.respond"];
|
|
17006
|
+
case "interaction.approval.respond": return ["interaction.approval.respond"];
|
|
17007
|
+
}
|
|
17008
|
+
}
|
|
17009
|
+
function response(request, status, now, payload) {
|
|
17010
|
+
const result = {
|
|
17011
|
+
protocol: DSH_REMOTE_PROTOCOL,
|
|
17012
|
+
protocol_major: 1,
|
|
17013
|
+
kind: "response",
|
|
17014
|
+
request_ref: request.request_ref,
|
|
17015
|
+
status,
|
|
17016
|
+
host_generation: request.host_generation,
|
|
17017
|
+
issued_at: now,
|
|
17018
|
+
operation: request.operation,
|
|
17019
|
+
body: {}
|
|
17020
|
+
};
|
|
17021
|
+
if (payload.result !== void 0) result.result = payload.result;
|
|
17022
|
+
if (payload.error !== void 0) result.error = payload.error;
|
|
17023
|
+
return result;
|
|
17024
|
+
}
|
|
17025
|
+
function resultFromLedger(entry) {
|
|
17026
|
+
const result = entry.payload.result;
|
|
17027
|
+
if (result !== null && typeof result === "object" && "rejected" in result) {
|
|
17028
|
+
const rejected = result.rejected;
|
|
17029
|
+
if (rejected !== null && typeof rejected === "object") {
|
|
17030
|
+
const source = rejected;
|
|
17031
|
+
throw new DshRemoteError(typeof source.code === "string" ? source.code : "REMOTE_TRANSPORT_FAILED", typeof source.message === "string" ? source.message : "DSH 已拒绝该远控命令", source.retryable === true);
|
|
17032
|
+
}
|
|
17033
|
+
}
|
|
17034
|
+
return result !== null && typeof result === "object" && "value" in result ? result.value : result;
|
|
17035
|
+
}
|
|
17036
|
+
/**
|
|
17037
|
+
* Account-login Host facade. Backend owns durable discovery/projections,
|
|
17038
|
+
* Realtime owns the short-lived Host lease, and DSH remains session truth.
|
|
17039
|
+
*/
|
|
17040
|
+
var ArkmeRemoteRealtimeHost = class {
|
|
17041
|
+
options;
|
|
17042
|
+
now;
|
|
17043
|
+
listeners = /* @__PURE__ */ new Set();
|
|
17044
|
+
runtime;
|
|
17045
|
+
accountId;
|
|
17046
|
+
clientId = 0;
|
|
17047
|
+
ledger;
|
|
17048
|
+
started = false;
|
|
17049
|
+
connected = false;
|
|
17050
|
+
serviceLeaseGeneration = 0;
|
|
17051
|
+
revision = 0;
|
|
17052
|
+
stopEvents;
|
|
17053
|
+
stopProjectionEvents;
|
|
17054
|
+
channelManager;
|
|
17055
|
+
stopTransportDisconnect;
|
|
17056
|
+
reconnectTimer;
|
|
17057
|
+
reconnecting = false;
|
|
17058
|
+
sessionTimer;
|
|
17059
|
+
connectionError;
|
|
17060
|
+
lastProjectionSyncAttemptMillis = 0;
|
|
17061
|
+
projectionVersion = 0;
|
|
17062
|
+
constructor(options) {
|
|
17063
|
+
this.options = options;
|
|
17064
|
+
this.now = options.now ?? Date.now;
|
|
17065
|
+
}
|
|
17066
|
+
async start() {
|
|
17067
|
+
if (this.started) return;
|
|
17068
|
+
this.started = true;
|
|
17069
|
+
if (!this.options.featureEnabled) {
|
|
17070
|
+
this.bump();
|
|
17071
|
+
return;
|
|
17072
|
+
}
|
|
17073
|
+
this.stopTransportDisconnect = this.options.realtime.subscribeDisconnect((error) => {
|
|
17074
|
+
this.handleTransportDisconnect(error);
|
|
17075
|
+
});
|
|
17076
|
+
await this.syncSessionSafely();
|
|
17077
|
+
this.scheduleSessionSync();
|
|
17078
|
+
}
|
|
17079
|
+
async stop() {
|
|
17080
|
+
if (!this.started) return;
|
|
17081
|
+
this.started = false;
|
|
17082
|
+
this.stopApiProxyEvents();
|
|
17083
|
+
this.stopTransportDisconnect?.();
|
|
17084
|
+
this.stopTransportDisconnect = void 0;
|
|
17085
|
+
if (this.sessionTimer !== void 0) clearTimeout(this.sessionTimer);
|
|
17086
|
+
this.sessionTimer = void 0;
|
|
17087
|
+
if (this.reconnectTimer !== void 0) clearTimeout(this.reconnectTimer);
|
|
17088
|
+
this.reconnectTimer = void 0;
|
|
17089
|
+
await this.deactivateAccount();
|
|
17090
|
+
this.bump();
|
|
17091
|
+
}
|
|
17092
|
+
getStatus() {
|
|
17093
|
+
const available = this.options.featureEnabled && this.accountId !== void 0 && this.options.transportAvailable !== false && this.options.apiProxy.capabilities().length > 0;
|
|
17094
|
+
return {
|
|
17095
|
+
contractVersion: 1,
|
|
17096
|
+
available,
|
|
17097
|
+
enabled: this.options.featureEnabled && this.accountId !== void 0,
|
|
17098
|
+
connected: this.connected,
|
|
17099
|
+
hostGeneration: this.runtime?.hostGeneration ?? 0,
|
|
17100
|
+
capabilities: this.runtime?.capabilities ?? this.options.apiProxy.capabilities(),
|
|
17101
|
+
revision: this.revision,
|
|
17102
|
+
...this.accountId === void 0 ? {} : { accountId: this.accountId },
|
|
17103
|
+
...this.runtime?.desktopRef === void 0 ? {} : { desktopRef: this.runtime.desktopRef },
|
|
17104
|
+
...this.runtime === void 0 ? {} : { runtimeRef: this.runtime.runtimeRef },
|
|
17105
|
+
...this.connectionError !== void 0 ? { unavailableReason: this.connectionError.message } : available ? {} : { unavailableReason: !this.options.featureEnabled ? "远控能力尚未在此版本启用" : this.accountId === void 0 ? "请先登录 Arkme 后使用 DSH 远程会话" : this.options.transportAvailable === false ? "当前 DSH Host 尚未提供带登录态的 Realtime Socket Factory" : this.options.apiProxy.capabilities().length === 0 ? "当前 DSH 缺少公共 ApiProxy 能力" : "远控运行依赖尚未就绪" }
|
|
17106
|
+
};
|
|
17107
|
+
}
|
|
17108
|
+
async renameDesktop(displayName) {
|
|
17109
|
+
this.requireReady();
|
|
17110
|
+
const normalized = displayName.trim();
|
|
17111
|
+
await this.options.runtimeStore.renameDesktop(this.accountId, normalized);
|
|
17112
|
+
if (this.runtime?.desktopRef !== void 0) await this.options.controlPlane.registerDesktop({
|
|
17113
|
+
display_name: normalized,
|
|
17114
|
+
platform: this.options.platform ?? process.platform
|
|
17115
|
+
});
|
|
17116
|
+
this.bump();
|
|
17117
|
+
return this.getStatus();
|
|
17118
|
+
}
|
|
17119
|
+
subscribe(listener) {
|
|
17120
|
+
this.listeners.add(listener);
|
|
17121
|
+
listener(this.getStatus());
|
|
17122
|
+
return () => {
|
|
17123
|
+
this.listeners.delete(listener);
|
|
17124
|
+
};
|
|
17125
|
+
}
|
|
17126
|
+
/** Entry used only by the authenticated Realtime channel bridge. */
|
|
17127
|
+
async dispatchAuthorizedRequest(value, context) {
|
|
17128
|
+
this.requireConnected();
|
|
17129
|
+
if (context.metadata.senderRole !== "controller" || context.metadata.runtimeRef !== this.runtime.runtimeRef) throw new DshRemoteError("REMOTE_REQUEST_INVALID", "远控发送方上下文无效");
|
|
17130
|
+
if (context.serviceLeaseGeneration !== this.serviceLeaseGeneration || context.metadata.targetHostLeaseGeneration !== this.serviceLeaseGeneration) throw new DshRemoteError("HOST_GENERATION_STALE", "远控请求属于旧 Host lease", true);
|
|
17131
|
+
if (this.now() - context.metadata.acceptedAtMillis > 3e4) throw new DshRemoteError("COMMAND_EXPIRED", "远控请求投递延迟过长");
|
|
17132
|
+
const request = parseDshRemoteRequest(value, {
|
|
17133
|
+
expectedHostGeneration: this.runtime.hostGeneration,
|
|
17134
|
+
nowMillis: this.now()
|
|
17135
|
+
});
|
|
17136
|
+
try {
|
|
17137
|
+
const capabilities = new Set(this.options.apiProxy.capabilities());
|
|
17138
|
+
if (requiredCapabilities(request.operation).some((capability) => !capabilities.has(capability))) throw new DshRemoteError("CAPABILITY_UNSUPPORTED", "当前 DSH Runtime 不支持该操作");
|
|
17139
|
+
const result = await this.dispatch(request);
|
|
17140
|
+
return response(request, result.duplicate ? "duplicate" : "completed", this.now(), { result: result.value });
|
|
17141
|
+
} catch (error) {
|
|
17142
|
+
const remote = asDshRemoteError(error);
|
|
17143
|
+
return response(request, "rejected", this.now(), { error: {
|
|
17144
|
+
code: remote.code,
|
|
17145
|
+
message: remote.message.slice(0, 256),
|
|
17146
|
+
retryable: remote.retryable,
|
|
17147
|
+
trace_ref: randomUUID()
|
|
17148
|
+
} });
|
|
17149
|
+
}
|
|
17150
|
+
}
|
|
17151
|
+
async syncSessionSafely() {
|
|
17152
|
+
try {
|
|
17153
|
+
await this.syncSession();
|
|
17154
|
+
} catch (error) {
|
|
17155
|
+
this.connectionError = asDshRemoteError(error);
|
|
17156
|
+
this.bump();
|
|
17157
|
+
}
|
|
17158
|
+
}
|
|
17159
|
+
async syncSession() {
|
|
17160
|
+
if (!this.started || !this.options.featureEnabled) return;
|
|
17161
|
+
const session = await this.options.readSession();
|
|
17162
|
+
if (session === void 0) {
|
|
17163
|
+
if (this.accountId !== void 0) await this.deactivateAccount();
|
|
17164
|
+
this.bump();
|
|
17165
|
+
return;
|
|
17166
|
+
}
|
|
17167
|
+
const accountId = String(session.userId);
|
|
17168
|
+
if (this.accountId === accountId && this.clientId === session.clientId && this.runtime !== void 0) {
|
|
17169
|
+
await this.ensureAutomaticConnection();
|
|
17170
|
+
return;
|
|
17171
|
+
}
|
|
17172
|
+
if (this.accountId !== void 0) await this.deactivateAccount();
|
|
17173
|
+
this.clientId = session.clientId;
|
|
17174
|
+
this.accountId = accountId;
|
|
17175
|
+
this.runtime = await this.options.runtimeStore.activateRuntime({
|
|
17176
|
+
accountId,
|
|
17177
|
+
profileRef: this.options.profileRef,
|
|
17178
|
+
capabilities: this.options.apiProxy.capabilities(),
|
|
17179
|
+
nowMillis: this.now()
|
|
17180
|
+
});
|
|
17181
|
+
this.ledger = await this.options.ledgerForAccount(accountId, await this.options.secretBroker.ledgerKey(accountId));
|
|
17182
|
+
await this.reconcileUnsettled();
|
|
17183
|
+
this.ledger.cleanup({
|
|
17184
|
+
retentionMillis: 10080 * 6e4,
|
|
17185
|
+
maxCommands: 1e4
|
|
17186
|
+
});
|
|
17187
|
+
await this.ensureAutomaticConnection();
|
|
17188
|
+
this.bump();
|
|
17189
|
+
}
|
|
17190
|
+
async ensureAutomaticConnection() {
|
|
17191
|
+
if (this.accountId === void 0 || this.runtime === void 0) return;
|
|
17192
|
+
if (this.connected) {
|
|
17193
|
+
await this.syncProjectionSnapshotSafely();
|
|
17194
|
+
return;
|
|
17195
|
+
}
|
|
17196
|
+
if (this.options.transportAvailable === false || this.options.apiProxy.capabilities().length === 0) {
|
|
17197
|
+
this.connectionError = void 0;
|
|
17198
|
+
this.bump();
|
|
17199
|
+
return;
|
|
17200
|
+
}
|
|
17201
|
+
this.startApiProxyEvents();
|
|
17202
|
+
try {
|
|
17203
|
+
await this.connectHost();
|
|
17204
|
+
} catch (error) {
|
|
17205
|
+
this.handleTransportDisconnect(error);
|
|
17206
|
+
throw error;
|
|
17207
|
+
}
|
|
17208
|
+
}
|
|
17209
|
+
scheduleSessionSync() {
|
|
17210
|
+
if (!this.started || !this.options.featureEnabled || this.sessionTimer !== void 0) return;
|
|
17211
|
+
this.sessionTimer = setTimeout(() => {
|
|
17212
|
+
this.sessionTimer = void 0;
|
|
17213
|
+
this.syncSessionSafely().finally(() => {
|
|
17214
|
+
this.scheduleSessionSync();
|
|
17215
|
+
});
|
|
17216
|
+
}, 2e3);
|
|
17217
|
+
this.sessionTimer.unref();
|
|
17218
|
+
}
|
|
17219
|
+
async deactivateAccount() {
|
|
17220
|
+
this.stopApiProxyEvents();
|
|
17221
|
+
let unregistered = false;
|
|
17222
|
+
try {
|
|
17223
|
+
if (this.connected) {
|
|
17224
|
+
await this.options.realtime.unregisterHost();
|
|
17225
|
+
unregistered = true;
|
|
17226
|
+
}
|
|
17227
|
+
} catch {}
|
|
17228
|
+
if (!unregistered) await this.options.realtime.disconnect();
|
|
17229
|
+
await this.channelManager?.close();
|
|
17230
|
+
this.channelManager = void 0;
|
|
17231
|
+
if (unregistered) await this.options.realtime.disconnect();
|
|
17232
|
+
this.connected = false;
|
|
17233
|
+
this.serviceLeaseGeneration = 0;
|
|
17234
|
+
this.ledger?.close();
|
|
17235
|
+
this.ledger = void 0;
|
|
17236
|
+
this.runtime = void 0;
|
|
17237
|
+
this.accountId = void 0;
|
|
17238
|
+
this.clientId = 0;
|
|
17239
|
+
this.connectionError = void 0;
|
|
17240
|
+
this.lastProjectionSyncAttemptMillis = 0;
|
|
17241
|
+
this.projectionVersion = 0;
|
|
17242
|
+
}
|
|
17243
|
+
startApiProxyEvents() {
|
|
17244
|
+
if (this.stopProjectionEvents === void 0) this.stopProjectionEvents = this.options.apiProxy.subscribeProjectionEvents((event) => {
|
|
17245
|
+
this.publishProjectionEvent(event).catch((error) => {
|
|
17246
|
+
this.connectionError = asDshRemoteError(error);
|
|
17247
|
+
this.bump();
|
|
17248
|
+
});
|
|
17249
|
+
});
|
|
17250
|
+
if (this.stopEvents === void 0) this.stopEvents = this.options.apiProxy.startEvents();
|
|
17251
|
+
}
|
|
17252
|
+
stopApiProxyEvents() {
|
|
17253
|
+
this.stopEvents?.();
|
|
17254
|
+
this.stopEvents = void 0;
|
|
17255
|
+
this.stopProjectionEvents?.();
|
|
17256
|
+
this.stopProjectionEvents = void 0;
|
|
17257
|
+
}
|
|
17258
|
+
async publishProjectionEvent(event) {
|
|
17259
|
+
const runtime = this.runtime;
|
|
17260
|
+
const manager = this.channelManager;
|
|
17261
|
+
if (!this.started || !this.connected || runtime === void 0 || manager === void 0) return;
|
|
17262
|
+
const issuedAt = this.now();
|
|
17263
|
+
if (event.kind === "session-event") {
|
|
17264
|
+
const append = async () => {
|
|
17265
|
+
await this.options.controlPlane.appendSessionEvents({
|
|
17266
|
+
runtime_ref: runtime.runtimeRef,
|
|
17267
|
+
host_generation: runtime.hostGeneration,
|
|
17268
|
+
session_ref: event.sessionId,
|
|
17269
|
+
entries: [event.entry]
|
|
17270
|
+
});
|
|
17271
|
+
};
|
|
17272
|
+
try {
|
|
17273
|
+
await append();
|
|
17274
|
+
} catch (error) {
|
|
17275
|
+
if (asDshRemoteError(error).code !== "REMOTE_NOT_FOUND") throw error;
|
|
17276
|
+
await this.syncProjectionSnapshot(true);
|
|
17277
|
+
await append();
|
|
17278
|
+
}
|
|
17279
|
+
const seq = event.entry.event.seq;
|
|
17280
|
+
const requestRef = `event_${createHash("sha256").update([
|
|
17281
|
+
"dsh-remote-session-event-v1",
|
|
17282
|
+
runtime.runtimeRef,
|
|
17283
|
+
String(runtime.hostGeneration),
|
|
17284
|
+
event.sessionId,
|
|
17285
|
+
String(seq),
|
|
17286
|
+
event.entry.event.type
|
|
17287
|
+
].join("\n")).digest("base64url").slice(0, 40)}`;
|
|
17288
|
+
await manager.publishProjectionEvent({
|
|
17289
|
+
protocol: DSH_REMOTE_PROTOCOL,
|
|
17290
|
+
protocol_major: 1,
|
|
17291
|
+
kind: "event",
|
|
17292
|
+
request_ref: requestRef,
|
|
17293
|
+
host_generation: runtime.hostGeneration,
|
|
17294
|
+
issued_at: issuedAt,
|
|
17295
|
+
operation: "session.history",
|
|
17296
|
+
body: {
|
|
17297
|
+
session_ref: event.sessionId,
|
|
17298
|
+
entries: [event.entry]
|
|
17299
|
+
},
|
|
17300
|
+
session_seq: seq,
|
|
17301
|
+
projection_as_of_seq: seq
|
|
17302
|
+
}, requestRef);
|
|
17303
|
+
return;
|
|
17304
|
+
}
|
|
17305
|
+
const baseline = event.kind === "mux-baseline";
|
|
17306
|
+
const requestRef = `projection_${randomUUID()}`;
|
|
17307
|
+
const pendingFits = Buffer.byteLength(JSON.stringify(event.pendingInteractions)) <= DSH_REMOTE_MAX_PAGE_RESULT_BYTES;
|
|
17308
|
+
await manager.publishProjectionEvent({
|
|
17309
|
+
protocol: DSH_REMOTE_PROTOCOL,
|
|
17310
|
+
protocol_major: 1,
|
|
17311
|
+
kind: "event",
|
|
17312
|
+
request_ref: requestRef,
|
|
17313
|
+
host_generation: runtime.hostGeneration,
|
|
17314
|
+
issued_at: issuedAt,
|
|
17315
|
+
operation: "snapshot.get",
|
|
17316
|
+
body: {
|
|
17317
|
+
...pendingFits ? { pending_interactions: event.pendingInteractions } : {},
|
|
17318
|
+
...baseline ? {
|
|
17319
|
+
reason: "mux-generation",
|
|
17320
|
+
session_ref: event.sessionId,
|
|
17321
|
+
last_seq: event.lastSeq
|
|
17322
|
+
} : {},
|
|
17323
|
+
...!pendingFits && !baseline ? { reason: "projection-overflow" } : {}
|
|
17324
|
+
},
|
|
17325
|
+
...baseline && event.lastSeq >= 0 ? { projection_as_of_seq: event.lastSeq } : {}
|
|
17326
|
+
}, requestRef);
|
|
17327
|
+
}
|
|
17328
|
+
handleTransportDisconnect(error) {
|
|
17329
|
+
if (!this.started || this.runtime === void 0) return;
|
|
17330
|
+
const remote = error === void 0 ? void 0 : asDshRemoteError(error);
|
|
17331
|
+
if (remote !== void 0) this.connectionError = remote;
|
|
17332
|
+
this.connected = false;
|
|
17333
|
+
this.serviceLeaseGeneration = 0;
|
|
17334
|
+
const manager = this.channelManager;
|
|
17335
|
+
this.channelManager = void 0;
|
|
17336
|
+
this.options.realtime.disconnect().finally(async () => {
|
|
17337
|
+
await manager?.close();
|
|
17338
|
+
}).catch(() => void 0);
|
|
17339
|
+
this.bump();
|
|
17340
|
+
if (this.reconnectTimer !== void 0 || this.options.transportAvailable === false || remote?.retryable === false) return;
|
|
17341
|
+
this.reconnectTimer = setTimeout(() => {
|
|
17342
|
+
this.reconnectTimer = void 0;
|
|
17343
|
+
if (!this.started || this.runtime === void 0 || this.reconnecting) return;
|
|
17344
|
+
this.reconnecting = true;
|
|
17345
|
+
this.connectHost().catch((caught) => {
|
|
17346
|
+
this.handleTransportDisconnect(caught);
|
|
17347
|
+
}).finally(() => {
|
|
17348
|
+
this.reconnecting = false;
|
|
17349
|
+
});
|
|
17350
|
+
}, 1e3);
|
|
17351
|
+
this.reconnectTimer.unref();
|
|
17352
|
+
}
|
|
17353
|
+
async connectHost() {
|
|
17354
|
+
const localRuntime = this.runtime;
|
|
17355
|
+
const accountId = this.accountId;
|
|
17356
|
+
const platform = this.options.platform ?? process.platform;
|
|
17357
|
+
const displayName = (await this.options.runtimeStore.account(accountId)).displayName ?? this.options.displayName ?? hostname();
|
|
17358
|
+
const desktop = await this.options.controlPlane.registerDesktop({
|
|
17359
|
+
display_name: displayName,
|
|
17360
|
+
platform
|
|
17361
|
+
});
|
|
17362
|
+
const desktopRef = typeof desktop.desktop_ref === "string" ? desktop.desktop_ref : typeof desktop.desktopRef === "string" ? desktop.desktopRef : "";
|
|
17363
|
+
if (desktopRef === "") throw new DshRemoteError("REMOTE_TRANSPORT_FAILED", "Backend 未返回桌面设备引用", true);
|
|
17364
|
+
await this.options.runtimeStore.bindDesktop(accountId, { desktopRef });
|
|
17365
|
+
this.runtime = {
|
|
17366
|
+
...localRuntime,
|
|
17367
|
+
desktopRef
|
|
17368
|
+
};
|
|
17369
|
+
const registeredRuntime = await this.options.controlPlane.registerRuntime(desktopRef, {
|
|
17370
|
+
profile_ref: this.options.profileRef,
|
|
17371
|
+
host_client_ref: this.options.hostClientRef,
|
|
17372
|
+
service_namespace: "dsh_remote",
|
|
17373
|
+
service_name: "host",
|
|
17374
|
+
protocol: DSH_REMOTE_PROTOCOL,
|
|
17375
|
+
protocol_major: 1,
|
|
17376
|
+
host_generation: localRuntime.hostGeneration,
|
|
17377
|
+
capabilities: localRuntime.capabilities
|
|
17378
|
+
});
|
|
17379
|
+
const backendRuntimeRef = typeof registeredRuntime.runtime_ref === "string" ? registeredRuntime.runtime_ref : "";
|
|
17380
|
+
const backendHostGeneration = typeof registeredRuntime.host_generation === "number" && Number.isSafeInteger(registeredRuntime.host_generation) && registeredRuntime.host_generation > 0 ? registeredRuntime.host_generation : 0;
|
|
17381
|
+
if (backendRuntimeRef === "" || backendHostGeneration < localRuntime.hostGeneration) throw new DshRemoteError("REMOTE_TRANSPORT_FAILED", "Backend 未返回匹配的 Runtime 引用与 Host generation", true);
|
|
17382
|
+
this.runtime = {
|
|
17383
|
+
...await this.options.runtimeStore.adoptRuntimeRef(accountId, this.options.profileRef, backendRuntimeRef, backendHostGeneration),
|
|
17384
|
+
desktopRef
|
|
17385
|
+
};
|
|
17386
|
+
await this.syncProjectionSnapshot(true);
|
|
17387
|
+
const controller = new AbortController();
|
|
17388
|
+
await this.options.realtime.connect({
|
|
17389
|
+
profileRef: this.options.profileRef,
|
|
17390
|
+
clientRef: this.options.hostClientRef,
|
|
17391
|
+
signal: controller.signal
|
|
17392
|
+
});
|
|
17393
|
+
const manager = new DshRemoteHostChannelManager({
|
|
17394
|
+
accountId,
|
|
17395
|
+
profileRef: this.options.profileRef,
|
|
17396
|
+
hostClientRef: this.options.hostClientRef,
|
|
17397
|
+
runtimeRef: backendRuntimeRef,
|
|
17398
|
+
realtime: this.options.realtime,
|
|
17399
|
+
secretBroker: this.options.secretBroker,
|
|
17400
|
+
dispatch: async (request, context) => await this.dispatchAuthorizedRequest(request, context),
|
|
17401
|
+
onFatal: (error) => {
|
|
17402
|
+
this.handleTransportDisconnect(error);
|
|
17403
|
+
}
|
|
17404
|
+
});
|
|
17405
|
+
try {
|
|
17406
|
+
await manager.prepare();
|
|
17407
|
+
const registered = await this.options.realtime.registerHost({
|
|
17408
|
+
runtimeRef: backendRuntimeRef,
|
|
17409
|
+
capabilities: this.runtime.capabilities,
|
|
17410
|
+
signal: controller.signal
|
|
17411
|
+
});
|
|
17412
|
+
await manager.activate(registered.serviceLeaseGeneration);
|
|
17413
|
+
this.channelManager = manager;
|
|
17414
|
+
this.serviceLeaseGeneration = registered.serviceLeaseGeneration;
|
|
17415
|
+
this.connected = true;
|
|
17416
|
+
this.connectionError = void 0;
|
|
17417
|
+
this.bump();
|
|
17418
|
+
} catch (error) {
|
|
17419
|
+
await this.options.realtime.disconnect();
|
|
17420
|
+
await manager.close();
|
|
17421
|
+
throw error;
|
|
17422
|
+
}
|
|
17423
|
+
}
|
|
17424
|
+
async syncProjectionSnapshotSafely(force = false) {
|
|
17425
|
+
const now = this.now();
|
|
17426
|
+
if (!force && now - this.lastProjectionSyncAttemptMillis < 5e3) return;
|
|
17427
|
+
this.lastProjectionSyncAttemptMillis = now;
|
|
17428
|
+
try {
|
|
17429
|
+
await this.syncProjectionSnapshot(force);
|
|
17430
|
+
this.connectionError = void 0;
|
|
17431
|
+
} catch (error) {
|
|
17432
|
+
this.connectionError = asDshRemoteError(error);
|
|
17433
|
+
this.bump();
|
|
17434
|
+
}
|
|
17435
|
+
}
|
|
17436
|
+
nextProjectionVersion() {
|
|
17437
|
+
this.projectionVersion = Math.max(this.projectionVersion + 1, Math.trunc(this.now()));
|
|
17438
|
+
return this.projectionVersion;
|
|
17439
|
+
}
|
|
17440
|
+
async syncProjectionSnapshot(force = false) {
|
|
17441
|
+
const runtime = this.runtime;
|
|
17442
|
+
const accountId = this.accountId;
|
|
17443
|
+
if (runtime === void 0 || accountId === void 0) return;
|
|
17444
|
+
if (force) this.lastProjectionSyncAttemptMillis = this.now();
|
|
17445
|
+
const capabilities = new Set(this.options.apiProxy.capabilities());
|
|
17446
|
+
const projectionAt = this.nextProjectionVersion();
|
|
17447
|
+
const previous = await this.options.runtimeStore.projectionInventory(accountId, this.options.profileRef);
|
|
17448
|
+
const workspaces = capabilities.has("workspace.list") ? await this.options.apiProxy.workspaces() : [];
|
|
17449
|
+
const currentWorkspaceRefs = new Set(workspaces.map((item) => item.workspaceId));
|
|
17450
|
+
const workspaceItems = [...workspaces.map((item) => ({
|
|
17451
|
+
workspace_ref: item.workspaceId,
|
|
17452
|
+
title: item.title,
|
|
17453
|
+
path: item.path,
|
|
17454
|
+
available: item.available,
|
|
17455
|
+
projection_at: projectionAt,
|
|
17456
|
+
deleted: false
|
|
17457
|
+
})), ...previous.workspaceRefs.filter((workspaceRef) => !currentWorkspaceRefs.has(workspaceRef)).map((workspaceRef) => ({
|
|
17458
|
+
workspace_ref: workspaceRef,
|
|
17459
|
+
title: "",
|
|
17460
|
+
path: "",
|
|
17461
|
+
available: false,
|
|
17462
|
+
projection_at: projectionAt,
|
|
17463
|
+
deleted: true
|
|
17464
|
+
}))];
|
|
17465
|
+
for (let offset = 0; offset < workspaceItems.length || offset === 0; offset += 100) {
|
|
17466
|
+
await this.options.controlPlane.syncWorkspaces({
|
|
17467
|
+
runtime_ref: runtime.runtimeRef,
|
|
17468
|
+
host_generation: runtime.hostGeneration,
|
|
17469
|
+
items: workspaceItems.slice(offset, offset + 100)
|
|
17470
|
+
});
|
|
17471
|
+
if (workspaceItems.length === 0) break;
|
|
17472
|
+
}
|
|
17473
|
+
const sessions = [];
|
|
17474
|
+
if (capabilities.has("session.list")) {
|
|
17475
|
+
let cursor;
|
|
17476
|
+
const seenCursors = /* @__PURE__ */ new Set();
|
|
17477
|
+
for (let pageCount = 0; pageCount < 200; pageCount += 1) {
|
|
17478
|
+
const page = await this.options.apiProxy.sessions({
|
|
17479
|
+
limit: 50,
|
|
17480
|
+
...cursor === void 0 ? {} : { cursor }
|
|
17481
|
+
});
|
|
17482
|
+
sessions.push(...page.items);
|
|
17483
|
+
if (page.nextCursor === void 0) break;
|
|
17484
|
+
if (seenCursors.has(page.nextCursor)) throw new DshRemoteError("REMOTE_INVALID_RESPONSE", "DSH 会话游标发生循环");
|
|
17485
|
+
seenCursors.add(page.nextCursor);
|
|
17486
|
+
cursor = page.nextCursor;
|
|
17487
|
+
if (pageCount === 199) throw new DshRemoteError("REMOTE_INVALID_RESPONSE", "DSH 会话分页超过安全上限");
|
|
17488
|
+
}
|
|
17489
|
+
}
|
|
17490
|
+
const currentSessionRefs = new Set(sessions.map((item) => item.sessionId));
|
|
17491
|
+
const sessionItems = [...sessions.map((item) => ({
|
|
17492
|
+
workspace_ref: item.workspaceId,
|
|
17493
|
+
session_ref: item.sessionId,
|
|
17494
|
+
title: item.title ?? "",
|
|
17495
|
+
source_updated_at: Math.max(1, Math.trunc(item.updatedAt)),
|
|
17496
|
+
projection_at: projectionAt,
|
|
17497
|
+
running: item.running,
|
|
17498
|
+
blank: item.blank,
|
|
17499
|
+
...item.projectionAsOfSeq === void 0 ? {} : { projection_as_of_seq: item.projectionAsOfSeq },
|
|
17500
|
+
deleted: false
|
|
17501
|
+
})), ...previous.sessions.filter((item) => !currentSessionRefs.has(item.sessionRef)).map((item) => ({
|
|
17502
|
+
workspace_ref: item.workspaceRef,
|
|
17503
|
+
session_ref: item.sessionRef,
|
|
17504
|
+
title: "",
|
|
17505
|
+
source_updated_at: item.sourceUpdatedAt,
|
|
17506
|
+
projection_at: projectionAt,
|
|
17507
|
+
running: false,
|
|
17508
|
+
blank: false,
|
|
17509
|
+
deleted: true
|
|
17510
|
+
}))];
|
|
17511
|
+
for (let offset = 0; offset < sessionItems.length || offset === 0; offset += 100) {
|
|
17512
|
+
await this.options.controlPlane.syncSessions({
|
|
17513
|
+
runtime_ref: runtime.runtimeRef,
|
|
17514
|
+
host_generation: runtime.hostGeneration,
|
|
17515
|
+
items: sessionItems.slice(offset, offset + 100)
|
|
17516
|
+
});
|
|
17517
|
+
if (sessionItems.length === 0) break;
|
|
17518
|
+
}
|
|
17519
|
+
await this.options.runtimeStore.saveProjectionInventory(accountId, this.options.profileRef, {
|
|
17520
|
+
workspaceRefs: workspaces.map((item) => item.workspaceId),
|
|
17521
|
+
sessions: sessions.map((item) => ({
|
|
17522
|
+
sessionRef: item.sessionId,
|
|
17523
|
+
workspaceRef: item.workspaceId,
|
|
17524
|
+
sourceUpdatedAt: Math.max(1, Math.trunc(item.updatedAt))
|
|
17525
|
+
}))
|
|
17526
|
+
});
|
|
17527
|
+
}
|
|
17528
|
+
async dispatch(request) {
|
|
17529
|
+
switch (request.operation) {
|
|
17530
|
+
case "capabilities.get": return {
|
|
17531
|
+
duplicate: false,
|
|
17532
|
+
value: { capabilities: this.options.apiProxy.capabilities() }
|
|
17533
|
+
};
|
|
17534
|
+
case "snapshot.get": {
|
|
17535
|
+
const cursor = optionalString(request.body, "cursor");
|
|
17536
|
+
const limit = optionalPositive(request.body, "limit");
|
|
17537
|
+
return {
|
|
17538
|
+
duplicate: false,
|
|
17539
|
+
value: await this.options.apiProxy.snapshot({
|
|
17540
|
+
...cursor === void 0 ? {} : { cursor },
|
|
17541
|
+
...limit === void 0 ? {} : { limit }
|
|
17542
|
+
})
|
|
17543
|
+
};
|
|
17544
|
+
}
|
|
17545
|
+
case "workspace.list": {
|
|
17546
|
+
const cursor = optionalString(request.body, "cursor");
|
|
17547
|
+
const limit = optionalPositive(request.body, "limit");
|
|
17548
|
+
return {
|
|
17549
|
+
duplicate: false,
|
|
17550
|
+
value: await this.options.apiProxy.workspacePage({
|
|
17551
|
+
...cursor === void 0 ? {} : { cursor },
|
|
17552
|
+
...limit === void 0 ? {} : { limit }
|
|
17553
|
+
})
|
|
17554
|
+
};
|
|
17555
|
+
}
|
|
17556
|
+
case "session.list": {
|
|
17557
|
+
const workspaceId = optionalString(request.body, "workspace_ref");
|
|
17558
|
+
const cursor = optionalString(request.body, "cursor");
|
|
17559
|
+
const limit = optionalPositive(request.body, "limit");
|
|
17560
|
+
return {
|
|
17561
|
+
duplicate: false,
|
|
17562
|
+
value: await this.options.apiProxy.sessions({
|
|
17563
|
+
...workspaceId === void 0 ? {} : { workspaceId },
|
|
17564
|
+
...cursor === void 0 ? {} : { cursor },
|
|
17565
|
+
...limit === void 0 ? {} : { limit }
|
|
17566
|
+
})
|
|
17567
|
+
};
|
|
17568
|
+
}
|
|
17569
|
+
case "session.history": {
|
|
17570
|
+
const beforeSeq = optionalPositive(request.body, "before_seq");
|
|
17571
|
+
const limit = optionalPositive(request.body, "limit");
|
|
17572
|
+
return {
|
|
17573
|
+
duplicate: false,
|
|
17574
|
+
value: await this.options.apiProxy.history({
|
|
17575
|
+
sessionId: stringBody(request.body, "session_ref"),
|
|
17576
|
+
...beforeSeq === void 0 ? {} : { beforeSeq },
|
|
17577
|
+
...limit === void 0 ? {} : { limit }
|
|
17578
|
+
})
|
|
17579
|
+
};
|
|
17580
|
+
}
|
|
17581
|
+
default: return await this.dispatchWrite(request);
|
|
17582
|
+
}
|
|
17583
|
+
}
|
|
17584
|
+
async dispatchWrite(request) {
|
|
17585
|
+
const ledger = this.ledger;
|
|
17586
|
+
if (ledger === void 0) throw new DshRemoteError("REMOTE_STORAGE_FAILED", "远控命令账本尚未就绪");
|
|
17587
|
+
const accountId = this.accountId;
|
|
17588
|
+
const runtimeRef = this.runtime.runtimeRef;
|
|
17589
|
+
const begun = ledger.begin({
|
|
17590
|
+
accountId,
|
|
17591
|
+
runtimeRef,
|
|
17592
|
+
requestRef: request.request_ref,
|
|
17593
|
+
operation: request.operation,
|
|
17594
|
+
arguments: request.body,
|
|
17595
|
+
executeBeforeMillis: request.execute_before
|
|
17596
|
+
});
|
|
17597
|
+
if (begun.duplicate) {
|
|
17598
|
+
if (begun.entry.state === "completed") return {
|
|
17599
|
+
duplicate: true,
|
|
17600
|
+
value: resultFromLedger(begun.entry)
|
|
17601
|
+
};
|
|
17602
|
+
throw new DshRemoteError("COMMAND_OUTCOME_UNKNOWN", "同一命令的结果尚未完成对账");
|
|
17603
|
+
}
|
|
17604
|
+
const identity = {
|
|
17605
|
+
accountId,
|
|
17606
|
+
runtimeRef,
|
|
17607
|
+
requestRef: request.request_ref
|
|
17608
|
+
};
|
|
17609
|
+
let value;
|
|
17610
|
+
try {
|
|
17611
|
+
switch (request.operation) {
|
|
17612
|
+
case "session.create":
|
|
17613
|
+
value = await this.options.apiProxy.createSession({
|
|
17614
|
+
workspaceId: stringBody(request.body, "workspace_ref"),
|
|
17615
|
+
dshRpcId: begun.entry.dshRpcId
|
|
17616
|
+
});
|
|
17617
|
+
break;
|
|
17618
|
+
case "session.prompt":
|
|
17619
|
+
value = await this.options.apiProxy.prompt({
|
|
17620
|
+
sessionId: stringBody(request.body, "session_ref"),
|
|
17621
|
+
mode: request.body.mode === "queue" || request.body.mode === "steer" ? request.body.mode : (() => {
|
|
17622
|
+
throw new DshRemoteError("REMOTE_REQUEST_INVALID", "prompt mode 无效");
|
|
17623
|
+
})(),
|
|
17624
|
+
content: [request.body.content],
|
|
17625
|
+
dshRpcId: begun.entry.dshRpcId
|
|
17626
|
+
});
|
|
17627
|
+
break;
|
|
17628
|
+
case "session.cancel":
|
|
17629
|
+
value = await this.options.apiProxy.cancel({
|
|
17630
|
+
sessionId: stringBody(request.body, "session_ref"),
|
|
17631
|
+
dshRpcId: begun.entry.dshRpcId
|
|
17632
|
+
});
|
|
17633
|
+
break;
|
|
17634
|
+
case "interaction.question.respond":
|
|
17635
|
+
await this.options.apiProxy.answerQuestion({
|
|
17636
|
+
interactionRpcRef: stringBody(request.body, "interaction_rpc_ref"),
|
|
17637
|
+
sessionId: stringBody(request.body, "session_ref"),
|
|
17638
|
+
answer: request.body.answer
|
|
17639
|
+
});
|
|
17640
|
+
value = { accepted: true };
|
|
17641
|
+
break;
|
|
17642
|
+
case "interaction.approval.respond":
|
|
17643
|
+
await this.options.apiProxy.answerApproval({
|
|
17644
|
+
interactionRpcRef: stringBody(request.body, "interaction_rpc_ref"),
|
|
17645
|
+
sessionId: stringBody(request.body, "session_ref"),
|
|
17646
|
+
approvalId: stringBody(request.body, "approval_id"),
|
|
17647
|
+
outcome: request.body.outcome === "allowed-once" || request.body.outcome === "rejected" ? request.body.outcome : (() => {
|
|
17648
|
+
throw new DshRemoteError("REMOTE_REQUEST_INVALID", "approval outcome 无效");
|
|
17649
|
+
})()
|
|
17650
|
+
});
|
|
17651
|
+
value = { accepted: true };
|
|
17652
|
+
break;
|
|
17653
|
+
default: throw new DshRemoteError("CAPABILITY_UNSUPPORTED", "远控操作不受支持");
|
|
17654
|
+
}
|
|
17655
|
+
} catch (error) {
|
|
17656
|
+
if (error instanceof DshRemoteError && (error.code !== "REMOTE_TRANSPORT_FAILED" || error.details.dshRejected === true)) ledger.completeRejected(identity, error);
|
|
17657
|
+
else ledger.markOutcomeUnknown(identity, "DSH transport failed after the command ledger entered pending");
|
|
17658
|
+
throw error;
|
|
17659
|
+
}
|
|
17660
|
+
ledger.complete(identity, { value });
|
|
17661
|
+
return {
|
|
17662
|
+
duplicate: false,
|
|
17663
|
+
value
|
|
17664
|
+
};
|
|
17665
|
+
}
|
|
17666
|
+
async reconcileUnsettled() {
|
|
17667
|
+
if (this.ledger === void 0 || this.accountId === void 0) return;
|
|
17668
|
+
for (const entry of this.ledger.unsettledForReconciliation(this.accountId, this.now())) {
|
|
17669
|
+
const argumentsValue = entry.payload.arguments;
|
|
17670
|
+
const argumentsRecord = argumentsValue !== null && typeof argumentsValue === "object" && !Array.isArray(argumentsValue) ? argumentsValue : void 0;
|
|
17671
|
+
const sessionId = typeof argumentsRecord?.session_ref === "string" ? argumentsRecord.session_ref : void 0;
|
|
17672
|
+
let proven = false;
|
|
17673
|
+
let recoveredValue = {
|
|
17674
|
+
recovered: true,
|
|
17675
|
+
dshRpcId: entry.dshRpcId
|
|
17676
|
+
};
|
|
17677
|
+
if (entry.operation === "session.create" && typeof argumentsRecord?.workspace_ref === "string") try {
|
|
17678
|
+
const created = await this.options.apiProxy.reconcileCreatedSession({
|
|
17679
|
+
workspaceId: argumentsRecord.workspace_ref,
|
|
17680
|
+
dshRpcId: entry.dshRpcId
|
|
17681
|
+
});
|
|
17682
|
+
if (created !== void 0) {
|
|
17683
|
+
proven = true;
|
|
17684
|
+
recoveredValue = {
|
|
17685
|
+
recovered: true,
|
|
17686
|
+
sessionId: created.sessionId
|
|
17687
|
+
};
|
|
17688
|
+
}
|
|
17689
|
+
} catch {}
|
|
17690
|
+
if (sessionId !== void 0) try {
|
|
17691
|
+
const history = await this.options.apiProxy.history({
|
|
17692
|
+
sessionId,
|
|
17693
|
+
limit: 50
|
|
17694
|
+
});
|
|
17695
|
+
proven = this.options.apiProxy.historyContainsRpcId(history.entries, entry.dshRpcId);
|
|
17696
|
+
} catch {}
|
|
17697
|
+
const identity = {
|
|
17698
|
+
accountId: entry.accountId,
|
|
17699
|
+
runtimeRef: entry.runtimeRef,
|
|
17700
|
+
requestRef: entry.requestRef
|
|
17701
|
+
};
|
|
17702
|
+
if (proven) this.ledger.complete(identity, { value: recoveredValue });
|
|
17703
|
+
else this.ledger.markOutcomeUnknown(identity, "DSH history did not prove the accepted result after Host recovery");
|
|
17704
|
+
}
|
|
17705
|
+
}
|
|
17706
|
+
requireReady() {
|
|
17707
|
+
if (!this.started || this.accountId === void 0 || this.runtime === void 0) throw new DshRemoteError("REMOTE_LOGIN_REQUIRED", "请先登录 Arkme 后使用远控");
|
|
17708
|
+
}
|
|
17709
|
+
requireConnected() {
|
|
17710
|
+
this.requireReady();
|
|
17711
|
+
if (!this.connected || this.serviceLeaseGeneration <= 0) throw new DshRemoteError("RUNTIME_OFFLINE", "当前 Runtime 未连接 Realtime", true);
|
|
17712
|
+
}
|
|
17713
|
+
bump() {
|
|
17714
|
+
this.revision += 1;
|
|
17715
|
+
const snapshot = this.getStatus();
|
|
17716
|
+
for (const listener of this.listeners) listener(snapshot);
|
|
17717
|
+
}
|
|
17718
|
+
};
|
|
17719
|
+
//#endregion
|
|
17720
|
+
//#region src/dsh-remote/realtime-transport.ts
|
|
17721
|
+
/**
|
|
17722
|
+
* Mirrors jotmo-realtime's login-only channel.publish and channel.event
|
|
17723
|
+
* wrappers. Max-width sequence/timestamp values keep the check conservative.
|
|
17724
|
+
*/
|
|
17725
|
+
function dshRemoteFrameByteLengths(input) {
|
|
17726
|
+
const maxInt64 = 0x8000000000000000;
|
|
17727
|
+
const requestId = "00000000-0000-4000-8000-000000000000";
|
|
17728
|
+
const target = wireTarget(input.target);
|
|
17729
|
+
const publish = {
|
|
17730
|
+
type: "channel.publish",
|
|
17731
|
+
namespace: "dsh_remote",
|
|
17732
|
+
channel_ref: input.target.runtimeRef,
|
|
17733
|
+
...target,
|
|
17734
|
+
direction: input.direction,
|
|
17735
|
+
command_id: input.commandId,
|
|
17736
|
+
payload: input.payload,
|
|
17737
|
+
request_id: requestId
|
|
17738
|
+
};
|
|
17739
|
+
const event = {
|
|
17740
|
+
type: "channel.event",
|
|
17741
|
+
namespace: "dsh_remote",
|
|
17742
|
+
channel_ref: input.target.runtimeRef,
|
|
17743
|
+
seq: maxInt64,
|
|
17744
|
+
event: {
|
|
17745
|
+
channel_ref: input.target.runtimeRef,
|
|
17746
|
+
command_id: input.commandId,
|
|
17747
|
+
seq: maxInt64,
|
|
17748
|
+
sender_role: input.senderRole,
|
|
17749
|
+
runtime_ref: input.target.runtimeRef,
|
|
17750
|
+
accepted_at: maxInt64,
|
|
17751
|
+
target_host_lease_generation: input.target.hostLeaseGeneration,
|
|
17752
|
+
payload: input.payload,
|
|
17753
|
+
created_at: maxInt64
|
|
17754
|
+
}
|
|
17755
|
+
};
|
|
17756
|
+
return {
|
|
17757
|
+
publish: Buffer.byteLength(JSON.stringify(publish)),
|
|
17758
|
+
event: Buffer.byteLength(JSON.stringify(event))
|
|
17759
|
+
};
|
|
17760
|
+
}
|
|
17761
|
+
function assertDshRemoteFramesFit(input) {
|
|
17762
|
+
const sizes = dshRemoteFrameByteLengths(input);
|
|
17763
|
+
if (sizes.publish > 61440 || sizes.event > 61440) throw new DshRemoteError("REMOTE_REQUEST_INVALID", "Realtime publish/event frame 超过 60KiB", false, {
|
|
17764
|
+
frameTooLarge: true,
|
|
17765
|
+
publishBytes: sizes.publish,
|
|
17766
|
+
eventBytes: sizes.event
|
|
17767
|
+
});
|
|
17768
|
+
}
|
|
17769
|
+
const SERVER_FIELDS = {
|
|
17770
|
+
"connection.ready": /* @__PURE__ */ new Set(["type", "connection_generation"]),
|
|
17771
|
+
"connection.replaced": /* @__PURE__ */ new Set(["type", "connection_generation"]),
|
|
17772
|
+
"service.registered": /* @__PURE__ */ new Set([
|
|
17773
|
+
"type",
|
|
17774
|
+
"request_id",
|
|
17775
|
+
"namespace",
|
|
17776
|
+
"service",
|
|
17777
|
+
"protocol",
|
|
17778
|
+
"protocol_major",
|
|
17779
|
+
"connection_generation",
|
|
17780
|
+
"service_lease_generation",
|
|
17781
|
+
"duplicate"
|
|
17782
|
+
]),
|
|
17783
|
+
"service.unregistered": /* @__PURE__ */ new Set([
|
|
17784
|
+
"type",
|
|
17785
|
+
"request_id",
|
|
17786
|
+
"namespace",
|
|
17787
|
+
"service",
|
|
17788
|
+
"protocol",
|
|
17789
|
+
"protocol_major",
|
|
17790
|
+
"duplicate"
|
|
17791
|
+
]),
|
|
17792
|
+
"channel.subscribed": /* @__PURE__ */ new Set([
|
|
17793
|
+
"type",
|
|
17794
|
+
"request_id",
|
|
17795
|
+
"namespace",
|
|
17796
|
+
"channel_ref",
|
|
17797
|
+
"seq",
|
|
17798
|
+
"duplicate"
|
|
17799
|
+
]),
|
|
17800
|
+
"channel.unsubscribed": /* @__PURE__ */ new Set([
|
|
17801
|
+
"type",
|
|
17802
|
+
"request_id",
|
|
17803
|
+
"namespace",
|
|
17804
|
+
"channel_ref",
|
|
17805
|
+
"duplicate"
|
|
17806
|
+
]),
|
|
17807
|
+
"channel.published": /* @__PURE__ */ new Set([
|
|
17808
|
+
"type",
|
|
17809
|
+
"request_id",
|
|
17810
|
+
"namespace",
|
|
17811
|
+
"channel_ref",
|
|
17812
|
+
"seq",
|
|
17813
|
+
"duplicate"
|
|
17814
|
+
]),
|
|
17815
|
+
"channel.event": /* @__PURE__ */ new Set([
|
|
17816
|
+
"type",
|
|
17817
|
+
"namespace",
|
|
17818
|
+
"channel_ref",
|
|
17819
|
+
"seq",
|
|
17820
|
+
"event"
|
|
17821
|
+
]),
|
|
17822
|
+
error: /* @__PURE__ */ new Set([
|
|
17823
|
+
"type",
|
|
17824
|
+
"request_id",
|
|
17825
|
+
"channel_ref",
|
|
17826
|
+
"code",
|
|
17827
|
+
"message",
|
|
17828
|
+
"retryable"
|
|
17829
|
+
])
|
|
17830
|
+
};
|
|
17831
|
+
function validServerFrame(frame) {
|
|
17832
|
+
const allowed = SERVER_FIELDS[frame.type];
|
|
17833
|
+
if (allowed === void 0 || Object.keys(frame).some((key) => !allowed.has(key))) return false;
|
|
17834
|
+
if (frame.type === "connection.ready") return positiveInteger(frame.connection_generation);
|
|
17835
|
+
if (frame.type === "connection.replaced") return true;
|
|
17836
|
+
if (frame.type === "channel.event") return frame.namespace === "dsh_remote" && validRef(frame.channel_ref) && positiveInteger(frame.seq) && frame.event !== null && typeof frame.event === "object" && !Array.isArray(frame.event);
|
|
17837
|
+
return validRef(frame.request_id);
|
|
17838
|
+
}
|
|
17839
|
+
function remoteError(frame) {
|
|
17840
|
+
const code = typeof frame.code === "string" ? frame.code : "REMOTE_TRANSPORT_FAILED";
|
|
17841
|
+
return new DshRemoteError((/* @__PURE__ */ new Set([
|
|
17842
|
+
"REMOTE_LOGIN_REQUIRED",
|
|
17843
|
+
"RUNTIME_OFFLINE",
|
|
17844
|
+
"HOST_CHANNEL_NOT_READY",
|
|
17845
|
+
"CONNECTION_REPLACED",
|
|
17846
|
+
"HOST_GENERATION_STALE",
|
|
17847
|
+
"REMOTE_PROTOCOL_UNSUPPORTED",
|
|
17848
|
+
"REMOTE_REQUEST_INVALID",
|
|
17849
|
+
"REMOTE_TRANSPORT_FAILED",
|
|
17850
|
+
"REPLAY_GAP"
|
|
17851
|
+
])).has(code) ? code : "REMOTE_TRANSPORT_FAILED", typeof frame.message === "string" && frame.message.trim() !== "" ? frame.message : "Realtime 远控操作失败", frame.retryable === true);
|
|
17852
|
+
}
|
|
17853
|
+
function positive(frame, key) {
|
|
17854
|
+
const value = frame[key];
|
|
17855
|
+
if (!positiveInteger(value)) throw new DshRemoteError("REMOTE_TRANSPORT_FAILED", `Realtime ${key} 无效`, true);
|
|
17856
|
+
return value;
|
|
17857
|
+
}
|
|
17858
|
+
function positiveInteger(value) {
|
|
17859
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value > 0;
|
|
17860
|
+
}
|
|
17861
|
+
function validRef(value) {
|
|
17862
|
+
return typeof value === "string" && /^[A-Za-z0-9._:-]{1,128}$/.test(value);
|
|
17863
|
+
}
|
|
17864
|
+
function validateTarget(target, allowUnregisteredHost) {
|
|
17865
|
+
if (!validRef(target.runtimeRef) || !validRef(target.hostProfileRef) || !validRef(target.hostClientRef) || !Number.isSafeInteger(target.hostLeaseGeneration) || target.hostLeaseGeneration < 0 || !allowUnregisteredHost && target.hostLeaseGeneration <= 0) throw new DshRemoteError("REMOTE_REQUEST_INVALID", "Realtime Runtime 目标无效");
|
|
17866
|
+
}
|
|
17867
|
+
function wireTarget(target) {
|
|
17868
|
+
return {
|
|
17869
|
+
runtime_ref: target.runtimeRef,
|
|
17870
|
+
host_profile_ref: target.hostProfileRef,
|
|
17871
|
+
host_client_ref: target.hostClientRef,
|
|
17872
|
+
host_lease_generation: target.hostLeaseGeneration
|
|
17873
|
+
};
|
|
17874
|
+
}
|
|
17875
|
+
/** Exact adapter for jotmo-realtime's login-only DSH Remote contract. */
|
|
17876
|
+
var ArkmeRemoteRealtimeTransport = class {
|
|
17877
|
+
createSocket;
|
|
17878
|
+
requestTimeoutMillis;
|
|
17879
|
+
socket;
|
|
17880
|
+
connectionGeneration = 0;
|
|
17881
|
+
serviceLeaseGeneration = 0;
|
|
17882
|
+
waiters = /* @__PURE__ */ new Map();
|
|
17883
|
+
channelListeners = /* @__PURE__ */ new Map();
|
|
17884
|
+
disconnectListeners = /* @__PURE__ */ new Set();
|
|
17885
|
+
onMessage;
|
|
17886
|
+
onClose;
|
|
17887
|
+
constructor(createSocket, requestTimeoutMillis = 1e4) {
|
|
17888
|
+
this.createSocket = createSocket;
|
|
17889
|
+
this.requestTimeoutMillis = requestTimeoutMillis;
|
|
17890
|
+
if (!Number.isSafeInteger(requestTimeoutMillis) || requestTimeoutMillis < 1e3 || requestTimeoutMillis > 6e4) throw new TypeError("Realtime request timeout must be between 1000 and 60000 milliseconds");
|
|
17891
|
+
}
|
|
17892
|
+
async connect(input) {
|
|
17893
|
+
await this.disconnect();
|
|
17894
|
+
if (input.signal.aborted) throw input.signal.reason;
|
|
17895
|
+
const socket = await this.createSocket(input);
|
|
17896
|
+
this.socket = socket;
|
|
17897
|
+
this.onMessage = (event) => {
|
|
17898
|
+
this.receive(event.data);
|
|
17899
|
+
};
|
|
17900
|
+
this.onClose = () => {
|
|
17901
|
+
if (this.socket !== socket) return;
|
|
17902
|
+
this.failConnection(new DshRemoteError("REMOTE_TRANSPORT_FAILED", "Realtime 连接意外关闭", true), false);
|
|
17903
|
+
};
|
|
17904
|
+
socket.addEventListener("message", this.onMessage);
|
|
17905
|
+
socket.addEventListener("close", this.onClose);
|
|
17906
|
+
await new Promise((resolve, reject) => {
|
|
17907
|
+
const onOpen = () => {
|
|
17908
|
+
cleanup();
|
|
17909
|
+
resolve();
|
|
17910
|
+
};
|
|
17911
|
+
const onError = () => {
|
|
17912
|
+
cleanup();
|
|
17913
|
+
reject(new DshRemoteError("REMOTE_TRANSPORT_FAILED", "Realtime 连接失败", true));
|
|
17914
|
+
};
|
|
17915
|
+
const onEarlyClose = () => {
|
|
17916
|
+
cleanup();
|
|
17917
|
+
reject(new DshRemoteError("REMOTE_TRANSPORT_FAILED", "Realtime 在握手前关闭", true));
|
|
17918
|
+
};
|
|
17919
|
+
const onAbort = () => {
|
|
17920
|
+
cleanup();
|
|
17921
|
+
socket.close(1e3, "aborted");
|
|
17922
|
+
reject(input.signal.reason);
|
|
17923
|
+
};
|
|
17924
|
+
const cleanup = () => {
|
|
17925
|
+
clearTimeout(timer);
|
|
17926
|
+
socket.removeEventListener("open", onOpen);
|
|
17927
|
+
socket.removeEventListener("error", onError);
|
|
17928
|
+
socket.removeEventListener("close", onEarlyClose);
|
|
17929
|
+
input.signal.removeEventListener("abort", onAbort);
|
|
17930
|
+
};
|
|
17931
|
+
const timer = setTimeout(() => {
|
|
17932
|
+
cleanup();
|
|
17933
|
+
socket.close(1e3, "handshake timeout");
|
|
17934
|
+
reject(new DshRemoteError("REMOTE_TRANSPORT_FAILED", "Realtime 连接握手超时", true));
|
|
17935
|
+
}, this.requestTimeoutMillis);
|
|
17936
|
+
timer.unref();
|
|
17937
|
+
socket.addEventListener("open", onOpen);
|
|
17938
|
+
socket.addEventListener("error", onError);
|
|
17939
|
+
socket.addEventListener("close", onEarlyClose);
|
|
17940
|
+
input.signal.addEventListener("abort", onAbort, { once: true });
|
|
17941
|
+
});
|
|
17942
|
+
const ready = this.waitForType("connection.ready", input.signal);
|
|
17943
|
+
this.send({
|
|
17944
|
+
type: "connection.open",
|
|
17945
|
+
profile_ref: input.profileRef,
|
|
17946
|
+
client_ref: input.clientRef
|
|
17947
|
+
});
|
|
17948
|
+
this.connectionGeneration = positive(await ready, "connection_generation");
|
|
17949
|
+
}
|
|
17950
|
+
async disconnect() {
|
|
17951
|
+
this.failConnection(new DshRemoteError("REMOTE_TRANSPORT_FAILED", "Realtime 连接已关闭", true), true);
|
|
17952
|
+
}
|
|
17953
|
+
subscribeDisconnect(listener) {
|
|
17954
|
+
this.disconnectListeners.add(listener);
|
|
17955
|
+
return () => {
|
|
17956
|
+
this.disconnectListeners.delete(listener);
|
|
17957
|
+
};
|
|
17958
|
+
}
|
|
17959
|
+
async registerHost(input) {
|
|
17960
|
+
if (!validRef(input.runtimeRef) || input.capabilities.some((capability) => !/^[A-Za-z0-9._:-]{1,128}$/.test(capability))) throw new DshRemoteError("REMOTE_REQUEST_INVALID", "Realtime Host 服务描述无效");
|
|
17961
|
+
const frame = await this.request({
|
|
17962
|
+
type: "service.register",
|
|
17963
|
+
namespace: "dsh_remote",
|
|
17964
|
+
service: "host",
|
|
17965
|
+
protocol: "dsh.remote",
|
|
17966
|
+
protocol_major: 1,
|
|
17967
|
+
descriptor: {
|
|
17968
|
+
runtime_ref: input.runtimeRef,
|
|
17969
|
+
capabilities: [...new Set(input.capabilities)].sort()
|
|
17970
|
+
}
|
|
17971
|
+
}, "service.registered", input.signal);
|
|
17972
|
+
this.serviceLeaseGeneration = positive(frame, "service_lease_generation");
|
|
17973
|
+
return { serviceLeaseGeneration: this.serviceLeaseGeneration };
|
|
17974
|
+
}
|
|
17975
|
+
async unregisterHost(signal) {
|
|
17976
|
+
if (this.socket === void 0) {
|
|
17977
|
+
this.serviceLeaseGeneration = 0;
|
|
17978
|
+
return;
|
|
17979
|
+
}
|
|
17980
|
+
try {
|
|
17981
|
+
await this.request({
|
|
17982
|
+
type: "service.unregister",
|
|
17983
|
+
namespace: "dsh_remote",
|
|
17984
|
+
service: "host",
|
|
17985
|
+
protocol: "dsh.remote",
|
|
17986
|
+
protocol_major: 1
|
|
17987
|
+
}, "service.unregistered", signal ?? new AbortController().signal);
|
|
17988
|
+
} finally {
|
|
17989
|
+
this.serviceLeaseGeneration = 0;
|
|
17990
|
+
}
|
|
17991
|
+
}
|
|
17992
|
+
async subscribe(input) {
|
|
17993
|
+
validateTarget(input.target, true);
|
|
17994
|
+
if (input.afterSequence !== void 0 && (!Number.isSafeInteger(input.afterSequence) || input.afterSequence < 0)) throw new DshRemoteError("REMOTE_REQUEST_INVALID", "Realtime after_seq 无效");
|
|
17995
|
+
await this.request({
|
|
17996
|
+
type: "channel.subscribe",
|
|
17997
|
+
namespace: "dsh_remote",
|
|
17998
|
+
channel_ref: input.target.runtimeRef,
|
|
17999
|
+
...wireTarget(input.target),
|
|
18000
|
+
...input.afterSequence === void 0 ? {} : { after_seq: input.afterSequence }
|
|
18001
|
+
}, "channel.subscribed", input.signal);
|
|
18002
|
+
this.channelListeners.set(input.target.runtimeRef, (frame) => {
|
|
18003
|
+
const event = frame.event;
|
|
18004
|
+
if (event === null || typeof event !== "object" || Array.isArray(event)) return;
|
|
18005
|
+
const source = event;
|
|
18006
|
+
if (source.payload === null || typeof source.payload !== "object" || Array.isArray(source.payload)) return;
|
|
18007
|
+
if (source.sender_role !== "host" && source.sender_role !== "controller") return;
|
|
18008
|
+
if (source.runtime_ref !== input.target.runtimeRef || !positiveInteger(source.accepted_at) || !Number.isSafeInteger(source.target_host_lease_generation) || source.target_host_lease_generation < 0) return;
|
|
18009
|
+
input.onEvent(source.payload, {
|
|
18010
|
+
senderRole: source.sender_role,
|
|
18011
|
+
runtimeRef: source.runtime_ref,
|
|
18012
|
+
acceptedAtMillis: source.accepted_at,
|
|
18013
|
+
targetHostLeaseGeneration: source.target_host_lease_generation,
|
|
18014
|
+
...positiveInteger(source.seq) ? { transportSequence: source.seq } : {}
|
|
18015
|
+
});
|
|
18016
|
+
if (positiveInteger(source.seq)) this.send({
|
|
18017
|
+
type: "channel.ack",
|
|
18018
|
+
request_id: randomUUID(),
|
|
18019
|
+
namespace: "dsh_remote",
|
|
18020
|
+
channel_ref: input.target.runtimeRef,
|
|
18021
|
+
seq: source.seq
|
|
18022
|
+
});
|
|
18023
|
+
});
|
|
18024
|
+
let unsubscribed = false;
|
|
18025
|
+
const unsubscribe = () => {
|
|
18026
|
+
if (unsubscribed) return;
|
|
18027
|
+
unsubscribed = true;
|
|
18028
|
+
input.signal.removeEventListener("abort", unsubscribe);
|
|
18029
|
+
this.channelListeners.delete(input.target.runtimeRef);
|
|
18030
|
+
if (this.socket !== void 0) this.send({
|
|
18031
|
+
type: "channel.unsubscribe",
|
|
18032
|
+
request_id: randomUUID(),
|
|
18033
|
+
namespace: "dsh_remote",
|
|
18034
|
+
channel_ref: input.target.runtimeRef
|
|
18035
|
+
});
|
|
18036
|
+
};
|
|
18037
|
+
input.signal.addEventListener("abort", unsubscribe, { once: true });
|
|
18038
|
+
return unsubscribe;
|
|
18039
|
+
}
|
|
18040
|
+
async publish(input) {
|
|
18041
|
+
validateTarget(input.target, false);
|
|
18042
|
+
if (!validRef(input.commandId)) throw new DshRemoteError("REMOTE_REQUEST_INVALID", "Realtime command_id 无效");
|
|
18043
|
+
const senderRole = input.target.hostProfileRef === "" ? "controller" : "host";
|
|
18044
|
+
assertDshRemoteFramesFit({
|
|
18045
|
+
target: input.target,
|
|
18046
|
+
commandId: input.commandId,
|
|
18047
|
+
direction: input.direction,
|
|
18048
|
+
payload: input.payload,
|
|
18049
|
+
senderRole
|
|
18050
|
+
});
|
|
18051
|
+
return { sequence: positive(await this.request({
|
|
18052
|
+
type: "channel.publish",
|
|
18053
|
+
namespace: "dsh_remote",
|
|
18054
|
+
channel_ref: input.target.runtimeRef,
|
|
18055
|
+
...wireTarget(input.target),
|
|
18056
|
+
direction: input.direction,
|
|
18057
|
+
command_id: input.commandId,
|
|
18058
|
+
payload: input.payload
|
|
18059
|
+
}, "channel.published", input.signal), "seq") };
|
|
18060
|
+
}
|
|
18061
|
+
async request(frame, expectedType, signal) {
|
|
18062
|
+
const requestId = randomUUID();
|
|
18063
|
+
const response = this.waitForRequest(requestId, expectedType, signal);
|
|
18064
|
+
this.send({
|
|
18065
|
+
...frame,
|
|
18066
|
+
request_id: requestId
|
|
18067
|
+
});
|
|
18068
|
+
return await response;
|
|
18069
|
+
}
|
|
18070
|
+
waitForRequest(requestId, expectedType, signal) {
|
|
18071
|
+
return new Promise((resolve, reject) => {
|
|
18072
|
+
const cleanup = () => {
|
|
18073
|
+
clearTimeout(timer);
|
|
18074
|
+
signal.removeEventListener("abort", onAbort);
|
|
18075
|
+
};
|
|
18076
|
+
const onAbort = () => {
|
|
18077
|
+
this.waiters.delete(requestId);
|
|
18078
|
+
cleanup();
|
|
18079
|
+
reject(signal.reason);
|
|
18080
|
+
};
|
|
18081
|
+
const timer = setTimeout(() => {
|
|
18082
|
+
this.waiters.delete(requestId);
|
|
18083
|
+
cleanup();
|
|
18084
|
+
reject(new DshRemoteError("REMOTE_TRANSPORT_FAILED", `Realtime ${expectedType} 响应超时`, true));
|
|
18085
|
+
}, this.requestTimeoutMillis);
|
|
18086
|
+
timer.unref();
|
|
18087
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
18088
|
+
this.waiters.set(requestId, {
|
|
18089
|
+
resolve: (frame) => {
|
|
18090
|
+
cleanup();
|
|
18091
|
+
if (frame.type !== expectedType) reject(remoteError(frame));
|
|
18092
|
+
else resolve(frame);
|
|
18093
|
+
},
|
|
18094
|
+
reject: (error) => {
|
|
18095
|
+
cleanup();
|
|
18096
|
+
reject(error);
|
|
18097
|
+
}
|
|
18098
|
+
});
|
|
18099
|
+
});
|
|
18100
|
+
}
|
|
18101
|
+
waitForType(type, signal) {
|
|
18102
|
+
return this.waitForRequest(`@type:${type}`, type, signal);
|
|
18103
|
+
}
|
|
18104
|
+
receive(data) {
|
|
18105
|
+
let frame;
|
|
18106
|
+
try {
|
|
18107
|
+
const raw = typeof data === "string" ? data : data instanceof ArrayBuffer ? Buffer.from(data).toString("utf8") : String(data);
|
|
18108
|
+
if (Buffer.byteLength(raw) > 61440) return;
|
|
18109
|
+
const parsed = JSON.parse(raw);
|
|
18110
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed) || typeof parsed.type !== "string") return;
|
|
18111
|
+
frame = parsed;
|
|
18112
|
+
} catch {
|
|
18113
|
+
return;
|
|
18114
|
+
}
|
|
18115
|
+
if (!validServerFrame(frame)) return;
|
|
18116
|
+
if (frame.type === "connection.replaced") {
|
|
18117
|
+
this.failConnection(new DshRemoteError("CONNECTION_REPLACED", "Realtime 连接已被同一客户端的新连接替换", true), false);
|
|
18118
|
+
return;
|
|
18119
|
+
}
|
|
18120
|
+
const requestId = frame.request_id;
|
|
18121
|
+
const waiterKey = typeof requestId === "string" && this.waiters.has(requestId) ? requestId : `@type:${frame.type}`;
|
|
18122
|
+
const waiter = this.waiters.get(waiterKey);
|
|
18123
|
+
if (waiter !== void 0) {
|
|
18124
|
+
this.waiters.delete(waiterKey);
|
|
18125
|
+
waiter.resolve(frame);
|
|
18126
|
+
return;
|
|
18127
|
+
}
|
|
18128
|
+
if (frame.type === "channel.event" && typeof frame.channel_ref === "string") this.channelListeners.get(frame.channel_ref)?.(frame);
|
|
18129
|
+
}
|
|
18130
|
+
failConnection(error, expected) {
|
|
18131
|
+
const socket = this.socket;
|
|
18132
|
+
if (socket !== void 0 && this.onMessage !== void 0) socket.removeEventListener("message", this.onMessage);
|
|
18133
|
+
if (socket !== void 0 && this.onClose !== void 0) socket.removeEventListener("close", this.onClose);
|
|
18134
|
+
this.socket = void 0;
|
|
18135
|
+
this.onMessage = void 0;
|
|
18136
|
+
this.onClose = void 0;
|
|
18137
|
+
this.connectionGeneration = 0;
|
|
18138
|
+
this.serviceLeaseGeneration = 0;
|
|
18139
|
+
socket?.close(1e3, expected ? "remote host stopped" : "remote connection failed");
|
|
18140
|
+
for (const waiter of this.waiters.values()) waiter.reject(error);
|
|
18141
|
+
this.waiters.clear();
|
|
18142
|
+
this.channelListeners.clear();
|
|
18143
|
+
if (!expected) for (const listener of this.disconnectListeners) listener(error);
|
|
18144
|
+
}
|
|
18145
|
+
send(frame) {
|
|
18146
|
+
if (this.socket === void 0 || this.socket.readyState !== 1) throw new DshRemoteError("REMOTE_TRANSPORT_FAILED", "Realtime 尚未连接", true);
|
|
18147
|
+
const encoded = JSON.stringify(frame);
|
|
18148
|
+
if (Buffer.byteLength(encoded) > 61440) throw new DshRemoteError("REMOTE_REQUEST_INVALID", "Realtime frame 超过 60KiB");
|
|
18149
|
+
this.socket.send(encoded);
|
|
18150
|
+
}
|
|
18151
|
+
};
|
|
18152
|
+
//#endregion
|
|
18153
|
+
//#region src/dsh-remote/runtime-store.ts
|
|
18154
|
+
const MAX_RUNTIMES_PER_ACCOUNT = 16;
|
|
18155
|
+
function emptyState() {
|
|
18156
|
+
return {
|
|
18157
|
+
schemaVersion: 2,
|
|
18158
|
+
accounts: {}
|
|
18159
|
+
};
|
|
18160
|
+
}
|
|
18161
|
+
function accountState(state, accountId) {
|
|
18162
|
+
return state.accounts[accountId] ?? {
|
|
18163
|
+
runtimes: {},
|
|
18164
|
+
projections: {}
|
|
18165
|
+
};
|
|
18166
|
+
}
|
|
18167
|
+
var DshRemoteRuntimeStore = class {
|
|
18168
|
+
path;
|
|
18169
|
+
state;
|
|
18170
|
+
queue = Promise.resolve();
|
|
18171
|
+
constructor(directory) {
|
|
18172
|
+
this.path = join(directory, "dsh-remote", "runtime-state.json");
|
|
18173
|
+
}
|
|
18174
|
+
async activateRuntime(input) {
|
|
18175
|
+
let result;
|
|
18176
|
+
await this.update((state) => {
|
|
18177
|
+
const account = accountState(state, input.accountId);
|
|
18178
|
+
const current = account.runtimes[input.profileRef];
|
|
18179
|
+
if (current === void 0 && Object.keys(account.runtimes).length >= MAX_RUNTIMES_PER_ACCOUNT) throw new DshRemoteError("RUNTIME_LIMIT_REACHED", "同一桌面账号最多注册 16 个 DSH Runtime");
|
|
18180
|
+
result = {
|
|
18181
|
+
runtimeRef: current?.runtimeRef ?? `runtime_${randomUUID()}`,
|
|
18182
|
+
...account.desktopRef === void 0 ? {} : { desktopRef: account.desktopRef },
|
|
18183
|
+
profileRef: input.profileRef,
|
|
18184
|
+
accountId: input.accountId,
|
|
18185
|
+
hostGeneration: (current?.hostGeneration ?? 0) + 1,
|
|
18186
|
+
capabilities: [...new Set(input.capabilities)].sort(),
|
|
18187
|
+
updatedAtMillis: input.nowMillis ?? Date.now()
|
|
18188
|
+
};
|
|
18189
|
+
account.runtimes[input.profileRef] = result;
|
|
18190
|
+
state.accounts[input.accountId] = account;
|
|
18191
|
+
});
|
|
18192
|
+
return result;
|
|
18193
|
+
}
|
|
18194
|
+
async adoptRuntimeRef(accountId, profileRef, runtimeRef, hostGeneration) {
|
|
18195
|
+
const normalized = runtimeRef.trim();
|
|
18196
|
+
if (!/^[A-Za-z0-9._:-]{8,256}$/.test(normalized)) throw new DshRemoteError("REMOTE_STORAGE_FAILED", "Backend Runtime 引用无效");
|
|
18197
|
+
if (hostGeneration !== void 0 && (!Number.isSafeInteger(hostGeneration) || hostGeneration <= 0)) throw new DshRemoteError("REMOTE_STORAGE_FAILED", "Backend Host generation 无效");
|
|
18198
|
+
let result;
|
|
18199
|
+
await this.update((state) => {
|
|
18200
|
+
const account = accountState(state, accountId);
|
|
18201
|
+
const runtime = account.runtimes[profileRef];
|
|
18202
|
+
if (runtime === void 0) throw new DshRemoteError("REMOTE_STORAGE_FAILED", "远控 Runtime 尚未注册");
|
|
18203
|
+
if (Object.entries(account.runtimes).some(([otherProfile, other]) => otherProfile !== profileRef && other.runtimeRef === normalized)) throw new DshRemoteError("REMOTE_STORAGE_FAILED", "Backend Runtime 引用与其他 Profile 冲突");
|
|
18204
|
+
if (hostGeneration !== void 0 && hostGeneration < runtime.hostGeneration) throw new DshRemoteError("REMOTE_STORAGE_FAILED", "Backend Host generation 发生回退");
|
|
18205
|
+
result = {
|
|
18206
|
+
...runtime,
|
|
18207
|
+
runtimeRef: normalized,
|
|
18208
|
+
hostGeneration: hostGeneration ?? runtime.hostGeneration,
|
|
18209
|
+
updatedAtMillis: Date.now()
|
|
18210
|
+
};
|
|
18211
|
+
account.runtimes[profileRef] = result;
|
|
18212
|
+
state.accounts[accountId] = account;
|
|
18213
|
+
});
|
|
18214
|
+
return result;
|
|
18215
|
+
}
|
|
18216
|
+
async bindDesktop(accountId, input) {
|
|
18217
|
+
await this.update((state) => {
|
|
18218
|
+
const account = accountState(state, accountId);
|
|
18219
|
+
account.desktopRef = input.desktopRef;
|
|
18220
|
+
for (const [profileRef, runtime] of Object.entries(account.runtimes)) account.runtimes[profileRef] = {
|
|
18221
|
+
...runtime,
|
|
18222
|
+
desktopRef: input.desktopRef
|
|
18223
|
+
};
|
|
18224
|
+
state.accounts[accountId] = account;
|
|
18225
|
+
});
|
|
18226
|
+
}
|
|
18227
|
+
async renameDesktop(accountId, displayName) {
|
|
18228
|
+
const normalized = displayName.trim();
|
|
18229
|
+
if (normalized === "" || [...normalized].length > 80) throw new DshRemoteError("REMOTE_REQUEST_INVALID", "电脑名称必须为 1 至 80 个字符");
|
|
18230
|
+
await this.update((state) => {
|
|
18231
|
+
const account = accountState(state, accountId);
|
|
18232
|
+
account.displayName = normalized;
|
|
18233
|
+
state.accounts[accountId] = account;
|
|
18234
|
+
});
|
|
18235
|
+
}
|
|
18236
|
+
async account(accountId) {
|
|
18237
|
+
return await this.read((state) => {
|
|
18238
|
+
const account = accountState(state, accountId);
|
|
18239
|
+
return {
|
|
18240
|
+
...account.displayName === void 0 ? {} : { displayName: account.displayName },
|
|
18241
|
+
...account.desktopRef === void 0 ? {} : { desktopRef: account.desktopRef },
|
|
18242
|
+
runtimes: Object.values(account.runtimes).map((runtime) => ({ ...runtime }))
|
|
18243
|
+
};
|
|
18244
|
+
});
|
|
18245
|
+
}
|
|
18246
|
+
async projectionInventory(accountId, profileRef) {
|
|
18247
|
+
return await this.read((state) => {
|
|
18248
|
+
const inventory = accountState(state, accountId).projections[profileRef];
|
|
18249
|
+
return inventory === void 0 ? {
|
|
18250
|
+
workspaceRefs: [],
|
|
18251
|
+
sessions: []
|
|
18252
|
+
} : {
|
|
18253
|
+
workspaceRefs: [...inventory.workspaceRefs],
|
|
18254
|
+
sessions: inventory.sessions.map((value) => ({ ...value }))
|
|
18255
|
+
};
|
|
18256
|
+
});
|
|
18257
|
+
}
|
|
18258
|
+
async saveProjectionInventory(accountId, profileRef, inventory) {
|
|
18259
|
+
await this.update((state) => {
|
|
18260
|
+
const account = accountState(state, accountId);
|
|
18261
|
+
account.projections[profileRef] = {
|
|
18262
|
+
workspaceRefs: [...new Set(inventory.workspaceRefs)].sort(),
|
|
18263
|
+
sessions: [...inventory.sessions].map((value) => ({ ...value })).sort((left, right) => left.sessionRef.localeCompare(right.sessionRef))
|
|
18264
|
+
};
|
|
18265
|
+
state.accounts[accountId] = account;
|
|
18266
|
+
});
|
|
18267
|
+
}
|
|
18268
|
+
async read(reader) {
|
|
18269
|
+
let result;
|
|
18270
|
+
await this.serial(async () => {
|
|
18271
|
+
result = reader(await this.load());
|
|
18272
|
+
});
|
|
18273
|
+
return result;
|
|
18274
|
+
}
|
|
18275
|
+
async update(mutator) {
|
|
18276
|
+
await this.serial(async () => {
|
|
18277
|
+
const state = await this.load();
|
|
18278
|
+
mutator(state);
|
|
18279
|
+
await this.persist(state);
|
|
18280
|
+
});
|
|
18281
|
+
}
|
|
18282
|
+
async serial(work) {
|
|
18283
|
+
const next = this.queue.then(work, work);
|
|
18284
|
+
this.queue = next.then(() => void 0, () => void 0);
|
|
18285
|
+
await next;
|
|
18286
|
+
}
|
|
18287
|
+
async load() {
|
|
18288
|
+
if (this.state !== void 0) return this.state;
|
|
18289
|
+
try {
|
|
18290
|
+
const raw = JSON.parse(await readFile(this.path, "utf8"));
|
|
18291
|
+
if (raw.schemaVersion !== 1 && raw.schemaVersion !== 2 || raw.accounts === null || typeof raw.accounts !== "object") throw new Error("schema mismatch");
|
|
18292
|
+
const accounts = {};
|
|
18293
|
+
for (const [accountId, value] of Object.entries(raw.accounts)) {
|
|
18294
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) throw new Error("account schema mismatch");
|
|
18295
|
+
const source = value;
|
|
18296
|
+
if (source.runtimes === null || typeof source.runtimes !== "object" || Array.isArray(source.runtimes)) throw new Error("runtime schema mismatch");
|
|
18297
|
+
const runtimes = {};
|
|
18298
|
+
for (const [profileRef, runtimeValue] of Object.entries(source.runtimes)) {
|
|
18299
|
+
if (runtimeValue === null || typeof runtimeValue !== "object" || Array.isArray(runtimeValue)) throw new Error("runtime schema mismatch");
|
|
18300
|
+
const runtime = { ...runtimeValue };
|
|
18301
|
+
delete runtime.remoteEnabled;
|
|
18302
|
+
runtimes[profileRef] = runtime;
|
|
18303
|
+
}
|
|
18304
|
+
const projections = {};
|
|
18305
|
+
if (source.projections !== void 0) {
|
|
18306
|
+
if (source.projections === null || typeof source.projections !== "object" || Array.isArray(source.projections)) throw new Error("projection inventory schema mismatch");
|
|
18307
|
+
for (const [profileRef, inventoryValue] of Object.entries(source.projections)) {
|
|
18308
|
+
if (inventoryValue === null || typeof inventoryValue !== "object" || Array.isArray(inventoryValue)) throw new Error("projection inventory schema mismatch");
|
|
18309
|
+
const inventory = inventoryValue;
|
|
18310
|
+
if (!Array.isArray(inventory.workspaceRefs) || !Array.isArray(inventory.sessions)) throw new Error("projection inventory schema mismatch");
|
|
18311
|
+
projections[profileRef] = {
|
|
18312
|
+
workspaceRefs: inventory.workspaceRefs.map((value) => {
|
|
18313
|
+
if (typeof value !== "string" || value.trim() === "") throw new Error("workspace inventory schema mismatch");
|
|
18314
|
+
return value;
|
|
18315
|
+
}),
|
|
18316
|
+
sessions: inventory.sessions.map((value) => {
|
|
18317
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) throw new Error("session inventory schema mismatch");
|
|
18318
|
+
const session = value;
|
|
18319
|
+
if (typeof session.sessionRef !== "string" || session.sessionRef.trim() === "" || typeof session.workspaceRef !== "string" || session.workspaceRef.trim() === "" || !Number.isSafeInteger(session.sourceUpdatedAt) || Number(session.sourceUpdatedAt) <= 0) throw new Error("session inventory schema mismatch");
|
|
18320
|
+
return {
|
|
18321
|
+
sessionRef: session.sessionRef,
|
|
18322
|
+
workspaceRef: session.workspaceRef,
|
|
18323
|
+
sourceUpdatedAt: Number(session.sourceUpdatedAt)
|
|
18324
|
+
};
|
|
18325
|
+
})
|
|
18326
|
+
};
|
|
18327
|
+
}
|
|
18328
|
+
}
|
|
18329
|
+
accounts[accountId] = {
|
|
18330
|
+
...typeof source.displayName === "string" ? { displayName: source.displayName } : {},
|
|
18331
|
+
...typeof source.desktopRef === "string" ? { desktopRef: source.desktopRef } : {},
|
|
18332
|
+
runtimes,
|
|
18333
|
+
projections
|
|
18334
|
+
};
|
|
18335
|
+
}
|
|
18336
|
+
this.state = {
|
|
18337
|
+
schemaVersion: 2,
|
|
18338
|
+
accounts
|
|
18339
|
+
};
|
|
18340
|
+
if (raw.schemaVersion !== 2) await this.persist(this.state);
|
|
18341
|
+
} catch (error) {
|
|
18342
|
+
if (error.code !== "ENOENT") throw new DshRemoteError("REMOTE_STORAGE_FAILED", "远控 Runtime 状态已损坏", false, {}, { cause: error });
|
|
18343
|
+
this.state = emptyState();
|
|
18344
|
+
await this.persist(this.state);
|
|
18345
|
+
}
|
|
18346
|
+
return this.state;
|
|
18347
|
+
}
|
|
18348
|
+
async persist(state) {
|
|
18349
|
+
const directory = dirname(this.path);
|
|
18350
|
+
await mkdir(directory, {
|
|
18351
|
+
recursive: true,
|
|
18352
|
+
mode: 448
|
|
18353
|
+
});
|
|
18354
|
+
await securePrivateDirectory(directory);
|
|
18355
|
+
const temporary = `${this.path}.${process.pid}.${randomUUID()}.tmp`;
|
|
18356
|
+
await writeFile(temporary, `${JSON.stringify(state, void 0, 2)}\n`, { mode: 384 });
|
|
18357
|
+
await securePrivateFile(temporary);
|
|
18358
|
+
await rename(temporary, this.path);
|
|
18359
|
+
await securePrivateFile(this.path);
|
|
18360
|
+
this.state = state;
|
|
18361
|
+
}
|
|
18362
|
+
};
|
|
18363
|
+
//#endregion
|
|
18364
|
+
//#region src/dsh-remote/runtime-secret-broker.ts
|
|
18365
|
+
function normalizedAccountId(accountId) {
|
|
18366
|
+
const normalized = accountId.trim();
|
|
18367
|
+
if (!/^[A-Za-z0-9._:-]{1,128}$/.test(normalized)) throw new DshRemoteError("REMOTE_REQUEST_INVALID", "远控账号标识无效");
|
|
18368
|
+
return normalized;
|
|
18369
|
+
}
|
|
18370
|
+
function accountKey(accountId) {
|
|
18371
|
+
return `dsh-remote-desktop:${normalizedAccountId(accountId)}`;
|
|
18372
|
+
}
|
|
18373
|
+
function decodeSecrets(raw, accountId) {
|
|
18374
|
+
let value;
|
|
18375
|
+
try {
|
|
18376
|
+
value = JSON.parse(raw);
|
|
18377
|
+
} catch (error) {
|
|
18378
|
+
throw new DshRemoteError("REMOTE_STORAGE_FAILED", "远控本地密钥已损坏", false, {}, { cause: error });
|
|
18379
|
+
}
|
|
18380
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) throw new DshRemoteError("REMOTE_STORAGE_FAILED", "远控本地密钥已损坏");
|
|
18381
|
+
const source = value;
|
|
18382
|
+
if (source.schemaVersion !== 1 && source.schemaVersion !== 2 || source.accountId !== accountId || typeof source.ledgerKey !== "string" || decodeBase64Url(source.ledgerKey).length !== 32) throw new DshRemoteError("REMOTE_STORAGE_FAILED", "远控本地密钥合同不兼容");
|
|
18383
|
+
return {
|
|
18384
|
+
secrets: {
|
|
18385
|
+
schemaVersion: 2,
|
|
18386
|
+
accountId,
|
|
18387
|
+
ledgerKey: source.ledgerKey
|
|
18388
|
+
},
|
|
18389
|
+
migrated: source.schemaVersion !== 2 || "identity" in source
|
|
18390
|
+
};
|
|
18391
|
+
}
|
|
18392
|
+
/**
|
|
18393
|
+
* Stores only the local command-ledger encryption key and replay cursor.
|
|
18394
|
+
* These values protect local recovery data; they are not remote authority.
|
|
18395
|
+
*/
|
|
18396
|
+
var DshRemoteRuntimeSecretBroker = class {
|
|
18397
|
+
store;
|
|
18398
|
+
inFlight = /* @__PURE__ */ new Map();
|
|
18399
|
+
cursorWrites = /* @__PURE__ */ new Map();
|
|
18400
|
+
constructor(store) {
|
|
18401
|
+
this.store = store;
|
|
18402
|
+
}
|
|
18403
|
+
async ledgerKey(accountId) {
|
|
18404
|
+
return decodeBase64Url((await this.secrets(accountId)).ledgerKey);
|
|
18405
|
+
}
|
|
18406
|
+
async runtimeCursor(input) {
|
|
18407
|
+
const key = this.cursorKey(input);
|
|
18408
|
+
await this.cursorWrites.get(key)?.catch(() => void 0);
|
|
18409
|
+
const raw = await this.store.read(key);
|
|
18410
|
+
if (raw === void 0) return 0;
|
|
18411
|
+
return this.parseCursor(raw, input).lastTransportSequence;
|
|
18412
|
+
}
|
|
18413
|
+
async putRuntimeCursor(input) {
|
|
18414
|
+
if (!Number.isSafeInteger(input.lastTransportSequence) || input.lastTransportSequence < 0) throw new DshRemoteError("REMOTE_REQUEST_INVALID", "远控 transport sequence 无效");
|
|
18415
|
+
const key = this.cursorKey(input);
|
|
18416
|
+
const writing = (this.cursorWrites.get(key) ?? Promise.resolve()).catch(() => void 0).then(async () => {
|
|
18417
|
+
const raw = await this.store.read(key);
|
|
18418
|
+
if (raw !== void 0 && this.parseCursor(raw, input).lastTransportSequence >= input.lastTransportSequence) return;
|
|
18419
|
+
const value = {
|
|
18420
|
+
schemaVersion: 2,
|
|
18421
|
+
accountId: input.accountId,
|
|
18422
|
+
runtimeRef: input.runtimeRef,
|
|
18423
|
+
channelRef: input.channelRef,
|
|
18424
|
+
lastTransportSequence: input.lastTransportSequence
|
|
18425
|
+
};
|
|
18426
|
+
await this.store.write(key, JSON.stringify(value));
|
|
18427
|
+
});
|
|
18428
|
+
this.cursorWrites.set(key, writing);
|
|
18429
|
+
try {
|
|
18430
|
+
await writing;
|
|
18431
|
+
} finally {
|
|
18432
|
+
if (this.cursorWrites.get(key) === writing) this.cursorWrites.delete(key);
|
|
18433
|
+
}
|
|
18434
|
+
}
|
|
18435
|
+
async secrets(accountIdValue) {
|
|
18436
|
+
const accountId = normalizedAccountId(accountIdValue);
|
|
18437
|
+
const existing = this.inFlight.get(accountId);
|
|
18438
|
+
if (existing !== void 0) return await existing;
|
|
18439
|
+
const loading = this.loadOrCreate(accountId);
|
|
18440
|
+
this.inFlight.set(accountId, loading);
|
|
18441
|
+
try {
|
|
18442
|
+
return await loading;
|
|
18443
|
+
} catch (error) {
|
|
18444
|
+
if (this.inFlight.get(accountId) === loading) this.inFlight.delete(accountId);
|
|
18445
|
+
throw error;
|
|
18446
|
+
}
|
|
18447
|
+
}
|
|
18448
|
+
async loadOrCreate(accountId) {
|
|
18449
|
+
const key = accountKey(accountId);
|
|
18450
|
+
const raw = await this.store.read(key);
|
|
18451
|
+
if (raw !== void 0) {
|
|
18452
|
+
const decoded = decodeSecrets(raw, accountId);
|
|
18453
|
+
if (decoded.migrated) await this.store.write(key, JSON.stringify(decoded.secrets));
|
|
18454
|
+
return decoded.secrets;
|
|
18455
|
+
}
|
|
18456
|
+
const created = {
|
|
18457
|
+
schemaVersion: 2,
|
|
18458
|
+
accountId,
|
|
18459
|
+
ledgerKey: encodeBase64Url(randomBytes(32))
|
|
18460
|
+
};
|
|
18461
|
+
await this.store.write(key, JSON.stringify(created));
|
|
18462
|
+
return created;
|
|
18463
|
+
}
|
|
18464
|
+
parseCursor(raw, input) {
|
|
18465
|
+
let source;
|
|
18466
|
+
try {
|
|
18467
|
+
source = JSON.parse(raw);
|
|
18468
|
+
} catch (error) {
|
|
18469
|
+
throw new DshRemoteError("REMOTE_STORAGE_FAILED", "远控 transport cursor 已损坏", false, {}, { cause: error });
|
|
18470
|
+
}
|
|
18471
|
+
if (source.schemaVersion !== 2 || source.accountId !== input.accountId || source.runtimeRef !== input.runtimeRef || source.channelRef !== input.channelRef || typeof source.lastTransportSequence !== "number" || !Number.isSafeInteger(source.lastTransportSequence) || source.lastTransportSequence < 0) throw new DshRemoteError("REMOTE_STORAGE_FAILED", "远控 transport cursor 路由不匹配");
|
|
18472
|
+
return source;
|
|
18473
|
+
}
|
|
18474
|
+
cursorKey(input) {
|
|
18475
|
+
normalizedAccountId(input.accountId);
|
|
18476
|
+
if ([input.runtimeRef, input.channelRef].some((value) => value.trim() === "" || value.length > 256)) throw new DshRemoteError("REMOTE_REQUEST_INVALID", "远控 transport cursor 路由无效");
|
|
18477
|
+
return `dsh-remote-runtime-cursor:${createHash("sha256").update([
|
|
18478
|
+
"dsh-remote-runtime-cursor-v2",
|
|
18479
|
+
input.accountId,
|
|
18480
|
+
input.runtimeRef,
|
|
18481
|
+
input.channelRef
|
|
18482
|
+
].join("\n")).digest("base64url")}`;
|
|
18483
|
+
}
|
|
18484
|
+
};
|
|
18485
|
+
//#endregion
|
|
18486
|
+
//#region src/index.ts
|
|
18487
|
+
const ARKME_PRODUCTION_TRUSTED_SIGNING_KEYS = "{\"prod-ed25519-20260819-1\":\"m1MKKU16hyu1b1KKIXMG+zKEr/GmhmvyUEreJzthTxs=\"}";
|
|
18488
|
+
const Config = Schema.object({
|
|
18489
|
+
environment: Schema.union(["test", "prod"]).default("test"),
|
|
14765
18490
|
authBaseUrl: Schema.string().default("https://jotmo.senguo.me"),
|
|
14766
18491
|
subjectBaseUrl: Schema.string().default("https://jotmo-subject.senguo.me"),
|
|
14767
18492
|
recordBaseUrl: Schema.string().default("https://jotmo-record.senguo.me"),
|
|
@@ -14807,7 +18532,9 @@ const Config = Schema.object({
|
|
|
14807
18532
|
richMediaSendEnabled: Schema.boolean().default(true),
|
|
14808
18533
|
maxUploadBytes: Schema.number().min(1024).max(1024 * 1024 * 1024).default(100 * 1024 * 1024),
|
|
14809
18534
|
openclawProfile: Schema.string().default("dev"),
|
|
14810
|
-
shareWebsite: Schema.string().default(ARKME_DEFAULT_SHARE_WEBSITE)
|
|
18535
|
+
shareWebsite: Schema.string().default(ARKME_DEFAULT_SHARE_WEBSITE),
|
|
18536
|
+
dshRemoteFeatureEnabled: Schema.boolean().default(false),
|
|
18537
|
+
dshRemoteRealtimeBaseUrl: Schema.string().default("")
|
|
14811
18538
|
});
|
|
14812
18539
|
const name = "dsh-arkme";
|
|
14813
18540
|
const inject = [
|
|
@@ -14815,8 +18542,7 @@ const inject = [
|
|
|
14815
18542
|
"tools",
|
|
14816
18543
|
"systemPrompt",
|
|
14817
18544
|
"pluginInventory",
|
|
14818
|
-
"clientModules"
|
|
14819
|
-
"apiProxy"
|
|
18545
|
+
"clientModules"
|
|
14820
18546
|
];
|
|
14821
18547
|
function readDshRuntimeVersion(dshBinPath) {
|
|
14822
18548
|
if (dshBinPath.trim() === "") return void 0;
|
|
@@ -14838,6 +18564,20 @@ function resolveArkmeAppVersion(configuredAppVersion, environment = process.env)
|
|
|
14838
18564
|
const injected = environment.ARKME_APP_VERSION?.trim();
|
|
14839
18565
|
return injected === void 0 || injected === "" ? void 0 : injected;
|
|
14840
18566
|
}
|
|
18567
|
+
function resolvePluginUpdateEnabled(configured, environment = process.env) {
|
|
18568
|
+
return configured && environment.ARKME_RUNTIME_MANAGED !== "1";
|
|
18569
|
+
}
|
|
18570
|
+
function dshRemoteClientId(accessToken) {
|
|
18571
|
+
const parts = accessToken.split(".");
|
|
18572
|
+
if (parts.length !== 3) return void 0;
|
|
18573
|
+
try {
|
|
18574
|
+
const claims = JSON.parse(Buffer.from(parts[1], "base64url").toString("utf8"));
|
|
18575
|
+
const clientId = claims.client_id ?? claims.clientId;
|
|
18576
|
+
return typeof clientId === "number" && Number.isSafeInteger(clientId) && clientId > 0 ? clientId : void 0;
|
|
18577
|
+
} catch {
|
|
18578
|
+
return;
|
|
18579
|
+
}
|
|
18580
|
+
}
|
|
14841
18581
|
function apply(ctx, config) {
|
|
14842
18582
|
config = resolveArkmeConfig(ctx, config);
|
|
14843
18583
|
const dshHome = process.env.DSH_HOME?.trim() || join(homedir(), ".dsh");
|
|
@@ -14853,14 +18593,9 @@ function apply(ctx, config) {
|
|
|
14853
18593
|
fileStateDirectory: join(stateDirectory, "files")
|
|
14854
18594
|
}, sessionStore, localDatabase, fetch, pendingSessionStore);
|
|
14855
18595
|
service.attachLocalFileOpener(async (path, signal) => {
|
|
14856
|
-
|
|
14857
|
-
if (apiProxy === void 0) throw new Error("当前 DSH 宿主未提供本机文件打开能力");
|
|
14858
|
-
const response = await apiProxy.host.openPath({
|
|
14859
|
-
rpcId: randomUUID(),
|
|
14860
|
-
payload: { path }
|
|
14861
|
-
}, signal);
|
|
14862
|
-
if (!response.result.ok) throw new Error(response.result.error.message);
|
|
18596
|
+
await openDshHostPath(ctx, path, signal);
|
|
14863
18597
|
});
|
|
18598
|
+
let remoteHost;
|
|
14864
18599
|
const openClawStateDirectory = join(stateDirectory, "openclaw");
|
|
14865
18600
|
const openClawCli = createOpenClawCliAdapter({
|
|
14866
18601
|
profile: config.openclawProfile,
|
|
@@ -14876,7 +18611,7 @@ function apply(ctx, config) {
|
|
|
14876
18611
|
const extensionStore = new ArkmeExtensionInstallStore(extensionDirectory);
|
|
14877
18612
|
const clientModules = ctx.clientModules;
|
|
14878
18613
|
const updateManager = new ArkmePluginUpdateManager({
|
|
14879
|
-
enabled: config.updateCheckEnabled,
|
|
18614
|
+
enabled: resolvePluginUpdateEnabled(config.updateCheckEnabled),
|
|
14880
18615
|
channel: config.updateChannel,
|
|
14881
18616
|
updateServiceBaseUrl: config.updateServiceBaseUrl,
|
|
14882
18617
|
...config.updateArtifactBaseUrl.trim() === "" ? {} : { updateArtifactBaseUrl: config.updateArtifactBaseUrl },
|
|
@@ -14918,9 +18653,26 @@ function apply(ctx, config) {
|
|
|
14918
18653
|
installStoreDirectory: extensionDirectory,
|
|
14919
18654
|
...process.env.ARKME_DESKTOP_MANAGED_RESTART === "1" && process.env.ARKME_DESKTOP_MANAGED_RESTART_PLAN_PATH !== void 0 ? {
|
|
14920
18655
|
supervisedExitCode: 75,
|
|
14921
|
-
supervisedPlanPath: process.env.ARKME_DESKTOP_MANAGED_RESTART_PLAN_PATH
|
|
18656
|
+
supervisedPlanPath: process.env.ARKME_DESKTOP_MANAGED_RESTART_PLAN_PATH,
|
|
18657
|
+
...process.env.ARKME_RUNTIME_RELEASE_ID === void 0 ? {} : { runtimeReleaseId: process.env.ARKME_RUNTIME_RELEASE_ID }
|
|
14922
18658
|
} : {}
|
|
14923
18659
|
});
|
|
18660
|
+
const pluginInventory = ctx.get("pluginInventory");
|
|
18661
|
+
const desktopQuarantine = new ArkmeDesktopExtensionQuarantine({
|
|
18662
|
+
dshHome,
|
|
18663
|
+
environment: config.environment,
|
|
18664
|
+
installStore: extensionStore,
|
|
18665
|
+
setProfileEnabled: async (packageName, enabled) => {
|
|
18666
|
+
await extensionProfileInstaller.setEnabled(packageName, enabled);
|
|
18667
|
+
},
|
|
18668
|
+
requestRestart: async ({ packageName }) => {
|
|
18669
|
+
await extensionProfileInstaller.restartDesktopQuarantine({ packageName });
|
|
18670
|
+
},
|
|
18671
|
+
isPackageActive: (packageName) => pluginInventory.list().entries.some((entry) => entry.moduleName === packageName && entry.enabled && entry.fiberPhase === "active")
|
|
18672
|
+
});
|
|
18673
|
+
desktopQuarantine.reconcile().catch((error) => {
|
|
18674
|
+
ctx.logger.warn("Arkme desktop extension quarantine reconciliation failed: %s", error instanceof Error ? error.message : String(error));
|
|
18675
|
+
});
|
|
14924
18676
|
const ownedExtensionStore = new ArkmeOwnedExtensionStore(extensionDirectory);
|
|
14925
18677
|
const ownedExtensionRefs = new ArkmeOwnedExtensionRefs();
|
|
14926
18678
|
const ownedExtensionHostInstanceId = randomUUID();
|
|
@@ -14946,7 +18698,7 @@ function apply(ctx, config) {
|
|
|
14946
18698
|
profileDirectory: extensionProfileDirectory,
|
|
14947
18699
|
profileInstaller: extensionProfileInstaller,
|
|
14948
18700
|
clientApiPath: config.routePath,
|
|
14949
|
-
pluginInventory
|
|
18701
|
+
pluginInventory,
|
|
14950
18702
|
...dshRuntimeVersion === void 0 ? {} : { dshRuntimeVersion }
|
|
14951
18703
|
});
|
|
14952
18704
|
extensionManager = manager;
|
|
@@ -14998,13 +18750,65 @@ function apply(ctx, config) {
|
|
|
14998
18750
|
tasks.dispose();
|
|
14999
18751
|
}, "dsh-arkme: marketplace dynamic runner bridge");
|
|
15000
18752
|
});
|
|
18753
|
+
ctx.inject(["apiProxy"], (apiCtx) => {
|
|
18754
|
+
if (!config.dshRemoteFeatureEnabled) return;
|
|
18755
|
+
const authenticatedSocketFactory = apiCtx.get("arkmeRemoteSocketFactory") ?? ((input) => createDefaultDshRemoteSocket({
|
|
18756
|
+
realtimeBaseUrl: config.dshRemoteRealtimeBaseUrl,
|
|
18757
|
+
accessToken: input.accessToken,
|
|
18758
|
+
signal: input.signal
|
|
18759
|
+
}));
|
|
18760
|
+
const profileRefValue = process.env.DSH_PROFILE?.trim() || "web";
|
|
18761
|
+
const profileRef = /^[A-Za-z0-9._:-]{1,128}$/.test(profileRefValue) ? profileRefValue : "web";
|
|
18762
|
+
const hostClientRef = `host_${createHash("sha256").update(`dsh-remote-host-client-v1\n${stateDirectory}\n${profileRef}`).digest("base64url")}`;
|
|
18763
|
+
const realtime = new ArkmeRemoteRealtimeTransport(async (input) => {
|
|
18764
|
+
const session = await sessionStore.read();
|
|
18765
|
+
if (session === void 0) throw new Error("Arkme session is unavailable");
|
|
18766
|
+
return await authenticatedSocketFactory({
|
|
18767
|
+
...input,
|
|
18768
|
+
accessToken: session.accessToken
|
|
18769
|
+
});
|
|
18770
|
+
});
|
|
18771
|
+
const apiProxy = new DshApiProxyAdapter(apiCtx.apiProxy);
|
|
18772
|
+
const secretBroker = new DshRemoteRuntimeSecretBroker(createArkmeSecureValueStore(`${config.keychainServicePrefix}.${config.environment}.dsh-remote-desktop`));
|
|
18773
|
+
const host = new ArkmeRemoteRealtimeHost({
|
|
18774
|
+
featureEnabled: config.dshRemoteFeatureEnabled,
|
|
18775
|
+
transportAvailable: true,
|
|
18776
|
+
profileRef,
|
|
18777
|
+
hostClientRef,
|
|
18778
|
+
secretBroker,
|
|
18779
|
+
runtimeStore: new DshRemoteRuntimeStore(stateDirectory),
|
|
18780
|
+
controlPlane: new DshRemoteHttpControlPlane({ post: async (path, body, signal) => await service.dshRemotePost(path, body, signal) }),
|
|
18781
|
+
realtime,
|
|
18782
|
+
apiProxy,
|
|
18783
|
+
readSession: async () => {
|
|
18784
|
+
const session = await sessionStore.read();
|
|
18785
|
+
if (session === void 0) return void 0;
|
|
18786
|
+
const clientId = dshRemoteClientId(session.accessToken);
|
|
18787
|
+
return clientId === void 0 ? void 0 : {
|
|
18788
|
+
userId: session.userId,
|
|
18789
|
+
clientId
|
|
18790
|
+
};
|
|
18791
|
+
},
|
|
18792
|
+
ledgerForAccount: (accountId, key) => new DshRemoteCommandLedger(join(stateDirectory, "dsh-remote", "ledger", createHash("sha256").update(`dsh-remote-ledger-account-v1\n${accountId}`).digest("base64url")), key)
|
|
18793
|
+
});
|
|
18794
|
+
remoteHost = host;
|
|
18795
|
+
apiCtx.effect(async () => {
|
|
18796
|
+
await host.start();
|
|
18797
|
+
return async () => {
|
|
18798
|
+
if (remoteHost === host) remoteHost = void 0;
|
|
18799
|
+
await host.stop();
|
|
18800
|
+
};
|
|
18801
|
+
}, "dsh-arkme: DSH remote Host lifecycle");
|
|
18802
|
+
});
|
|
15001
18803
|
const handler = createArkmeHostApi(service, {
|
|
15002
18804
|
expectedPort: ctx.webServer.port,
|
|
15003
18805
|
allowNonLoopback: config.allowNonLoopback,
|
|
15004
18806
|
updateManager,
|
|
15005
18807
|
extensionManager: () => extensionManager,
|
|
15006
18808
|
extensionInstallTasks: () => extensionInstallTasks,
|
|
15007
|
-
ownedExtensionInventory: () => ownedExtensionInventory
|
|
18809
|
+
ownedExtensionInventory: () => ownedExtensionInventory,
|
|
18810
|
+
remoteHost: () => remoteHost,
|
|
18811
|
+
desktopQuarantine
|
|
15008
18812
|
});
|
|
15009
18813
|
const callAssetHandler = createOutgoingCallAssetHandler({ routePrefix: `${config.routePath}/call` });
|
|
15010
18814
|
const richMediaOptions = {
|
|
@@ -15037,9 +18841,9 @@ function apply(ctx, config) {
|
|
|
15037
18841
|
const harnessEmbedHandler = createHarnessEmbedRouteHandler({
|
|
15038
18842
|
getGraph: () => clientModules.graph(),
|
|
15039
18843
|
installedPackageNames: () => extensionStore.list().flatMap((item) => item.profilePackageName === void 0 ? [] : [item.profilePackageName]),
|
|
15040
|
-
readRootHtml: async () => {
|
|
18844
|
+
readRootHtml: async (request) => {
|
|
15041
18845
|
const response = await fetch(`http://127.0.0.1:${String(ctx.webServer.port)}/`, {
|
|
15042
|
-
headers:
|
|
18846
|
+
headers: dshRootDocumentHeaders(request),
|
|
15043
18847
|
signal: AbortSignal.timeout(Math.min(config.requestTimeoutMs, 5e3))
|
|
15044
18848
|
});
|
|
15045
18849
|
if (!response.ok) throw new Error(`DSH root document returned HTTP ${String(response.status)}`);
|
|
@@ -15159,7 +18963,8 @@ function validateConfig(ctx, config) {
|
|
|
15159
18963
|
config.worldBaseUrl,
|
|
15160
18964
|
config.relationBaseUrl,
|
|
15161
18965
|
config.intelligentBaseUrl,
|
|
15162
|
-
config.audioBaseUrl
|
|
18966
|
+
config.audioBaseUrl,
|
|
18967
|
+
...config.dshRemoteFeatureEnabled ? [config.dshRemoteRealtimeBaseUrl] : []
|
|
15163
18968
|
].filter((origin) => new URL(origin).hostname.endsWith(".senguo.me")).length > 0) throw new Error("dsh-arkme: production environment must explicitly configure every service origin");
|
|
15164
18969
|
}
|
|
15165
18970
|
if (!config.allowNonLoopback && ctx.webServer.host !== "127.0.0.1") throw new Error("dsh-arkme: Web UI must bind 127.0.0.1 unless allowNonLoopback is true");
|
|
@@ -15168,6 +18973,7 @@ function validateConfig(ctx, config) {
|
|
|
15168
18973
|
validatePluginUpdateServiceOrigin(config.updateServiceBaseUrl);
|
|
15169
18974
|
if (config.updateArtifactBaseUrl.trim() !== "") validatePluginUpdateArtifactOrigin(config.updateArtifactBaseUrl);
|
|
15170
18975
|
if (!/^[A-Za-z0-9_-]{1,64}$/.test(config.openclawProfile)) throw new Error("dsh-arkme: openclawProfile must be a fixed profile name");
|
|
18976
|
+
if (config.dshRemoteFeatureEnabled && config.dshRemoteRealtimeBaseUrl.trim() === "") throw new Error("dsh-arkme: enabled DSH remote requires dshRemoteRealtimeBaseUrl");
|
|
15171
18977
|
for (const [label, raw] of [
|
|
15172
18978
|
["authBaseUrl", config.authBaseUrl],
|
|
15173
18979
|
["subjectBaseUrl", config.subjectBaseUrl],
|
|
@@ -15181,7 +18987,8 @@ function validateConfig(ctx, config) {
|
|
|
15181
18987
|
["relationBaseUrl", config.relationBaseUrl],
|
|
15182
18988
|
["intelligentBaseUrl", config.intelligentBaseUrl],
|
|
15183
18989
|
["audioBaseUrl", config.audioBaseUrl],
|
|
15184
|
-
["shareWebsite", config.shareWebsite]
|
|
18990
|
+
["shareWebsite", config.shareWebsite],
|
|
18991
|
+
...config.dshRemoteFeatureEnabled ? [["dshRemoteRealtimeBaseUrl", config.dshRemoteRealtimeBaseUrl]] : []
|
|
15185
18992
|
]) {
|
|
15186
18993
|
const url = new URL(raw);
|
|
15187
18994
|
if (url.protocol !== "https:" || url.username !== "" || url.password !== "" || url.pathname !== "/") throw new Error(`dsh-arkme: ${label} must be an HTTPS origin without credentials or path`);
|
|
@@ -15196,4 +19003,4 @@ function validateConfig(ctx, config) {
|
|
|
15196
19003
|
}
|
|
15197
19004
|
}
|
|
15198
19005
|
//#endregion
|
|
15199
|
-
export { ARKME_PRODUCTION_TRUSTED_SIGNING_KEYS, ARKME_PROVIDER_CONTRACT_VERSION, ARKME_WORLD_PUBLISH_MAX_IMAGES, ARKME_WORLD_PUBLISH_MAX_IMAGE_BYTES, ArkmeOutgoingCallError, ArkmeService, Config, apply, createOpenClawCliAdapter, inject, name, readDshRuntimeVersion, resolveArkmeAppVersion, resolveArkmeConfig };
|
|
19006
|
+
export { ARKME_PRODUCTION_TRUSTED_SIGNING_KEYS, ARKME_PROVIDER_CONTRACT_VERSION, ARKME_WORLD_PUBLISH_MAX_IMAGES, ARKME_WORLD_PUBLISH_MAX_IMAGE_BYTES, ArkmeOutgoingCallError, ArkmeService, Config, apply, createOpenClawCliAdapter, inject, name, readDshRuntimeVersion, resolveArkmeAppVersion, resolveArkmeConfig, resolvePluginUpdateEnabled };
|