@senguoyun/dsh-arkme 0.1.31 → 0.1.32
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/lib/client.js +1946 -762
- package/lib/client.js.map +1 -1
- package/lib/index.js +3693 -171
- package/lib/persistent-extension.js +1 -1
- package/lib/{persistent-runtime-BJ8konXI.js → persistent-runtime-CFeIg6aw.js} +1520 -681
- 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/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/ArkmePersistentShell.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/ArkmeSidebar.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/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/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/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 +3 -1
- package/lib/types/host-api.d.ts.map +1 -1
- package/lib/types/index.d.ts +14 -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 +23 -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-CFeIg6aw.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
13
|
import { chmod as chmod$1, mkdir, open, readFile, realpath, rename, 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,16 @@ 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
|
+
"remote.renameDesktop"
|
|
1831
1897
|
].includes(request.operation) && origin === void 0) throw new ArkmePluginError("origin-required", "扩展变更必须从当前 DSH 页面发起", false, 403);
|
|
1832
1898
|
writeJson$4(res, 200, {
|
|
1833
1899
|
ok: true,
|
|
1834
|
-
value: await dispatchArkmeHostOperation(service, request.operation, params, options.updateManager, options.extensionManager?.(), options.extensionInstallTasks?.(), options.ownedExtensionInventory?.(), controller.signal)
|
|
1900
|
+
value: await dispatchArkmeHostOperation(service, request.operation, params, options.updateManager, options.extensionManager?.(), options.extensionInstallTasks?.(), options.ownedExtensionInventory?.(), controller.signal, options.remoteHost?.())
|
|
1835
1901
|
});
|
|
1836
1902
|
} catch (error) {
|
|
1837
1903
|
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 });
|
|
1904
|
+
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
1905
|
writeJson$4(res, known.httpStatus, {
|
|
1840
1906
|
ok: false,
|
|
1841
1907
|
error: {
|
|
@@ -1849,12 +1915,17 @@ function createArkmeHostApi(service, options) {
|
|
|
1849
1915
|
}
|
|
1850
1916
|
};
|
|
1851
1917
|
}
|
|
1852
|
-
async function dispatchArkmeHostOperation(service, operation, params, updateManager, extensionManager, extensionInstallTasks, ownedExtensionInventory, requestSignal) {
|
|
1918
|
+
async function dispatchArkmeHostOperation(service, operation, params, updateManager, extensionManager, extensionInstallTasks, ownedExtensionInventory, requestSignal, remoteHost) {
|
|
1853
1919
|
switch (operation) {
|
|
1854
1920
|
case "provider.capabilities": return service.providerCapabilities();
|
|
1855
1921
|
case "provider.instance": return { instanceId: ARKME_RUNTIME_INSTANCE_ID };
|
|
1856
1922
|
case "provider.state": return await service.providerState();
|
|
1857
1923
|
case "chat.realtime.state": return service.chatRealtimeState();
|
|
1924
|
+
case "link.metadata": return await service.resolveLinkMetadata(stringParam(params, "url"), requestSignal === void 0 ? {} : { signal: requestSignal });
|
|
1925
|
+
case "source.link-metadata.resolve": {
|
|
1926
|
+
const url = stringParam(params, "url");
|
|
1927
|
+
return await service.resolveLinkMetadata(url, requestSignal === void 0 ? {} : { signal: requestSignal }) ?? arkmeRequiredLinkMetadataFallback(url);
|
|
1928
|
+
}
|
|
1858
1929
|
case "plugin.update.status": return await requireUpdateManager(updateManager).status();
|
|
1859
1930
|
case "plugin.update.check": return await requireUpdateManager(updateManager).check({ manual: true });
|
|
1860
1931
|
case "plugin.update.install": return await requireUpdateManager(updateManager).install();
|
|
@@ -1875,6 +1946,8 @@ async function dispatchArkmeHostOperation(service, operation, params, updateMana
|
|
|
1875
1946
|
case "auth.phone.send": return await service.sendPhoneCode(stringParam(params, "phone"), captchaParam(params));
|
|
1876
1947
|
case "auth.phone.verify": return await service.verifyPhoneCode(stringParam(params, "phone"), stringParam(params, "code"));
|
|
1877
1948
|
case "auth.logout": return await service.logout();
|
|
1949
|
+
case "remote.getStatus": return requireRemoteHost(remoteHost).getStatus();
|
|
1950
|
+
case "remote.renameDesktop": return await requireRemoteHost(remoteHost).renameDesktop(stringParam(params, "displayName"));
|
|
1878
1951
|
case "billing.quota": return await service.billingQuota();
|
|
1879
1952
|
case "billing.products": return await service.billingProducts();
|
|
1880
1953
|
case "billing.order.create": return await service.createBillingOrder({
|
|
@@ -1921,8 +1994,10 @@ async function dispatchArkmeHostOperation(service, operation, params, updateMana
|
|
|
1921
1994
|
case "directory.group.open-chat": return await service.openDirectoryGroupChat(stringParam(params, "sourceRef").trim(), requestSignal);
|
|
1922
1995
|
case "directory.bot.open-chat": return await service.openBotChat(stringParam(params, "botRef").trim(), requestSignal === void 0 ? {} : { signal: requestSignal });
|
|
1923
1996
|
case "bots.private-chat.open": return await service.openBotPrivateChat(stringParam(params, "botRef").trim(), requestSignal === void 0 ? {} : { signal: requestSignal });
|
|
1997
|
+
case "bots.private-chat.refresh": return await service.refreshBotPrivateChat(stringParam(params, "botRef").trim(), requestSignal === void 0 ? {} : { signal: requestSignal });
|
|
1924
1998
|
case "bots.private-chat.directory": return await service.listBotPrivateChatDirectory(requestSignal === void 0 ? {} : { signal: requestSignal });
|
|
1925
1999
|
case "bots.private-chat.send": return await service.sendBotPrivateChatMessage(stringParam(params, "botRef").trim(), stringParam(params, "content"), requestSignal === void 0 ? {} : { signal: requestSignal });
|
|
2000
|
+
case "bots.private-chat.mark-read": return await service.markBotPrivateChatRead(stringParam(params, "botRef").trim(), numberParam(params, "sequence", 0), requestSignal === void 0 ? {} : { signal: requestSignal });
|
|
1926
2001
|
case "unmarked-speakers.options": return await service.unmarkedSpeakerOptions(stringParam(params, "candidateRef").trim());
|
|
1927
2002
|
case "unmarked-speakers.retry-inference": return await service.retryUnmarkedSpeakerInference(stringParam(params, "candidateRef").trim());
|
|
1928
2003
|
case "unmarked-speakers.segments": {
|
|
@@ -2162,7 +2237,7 @@ async function dispatchArkmeHostOperation(service, operation, params, updateMana
|
|
|
2162
2237
|
case "source.message-copy-link": return await service.copySourceMessageLink(stringParam(params, "sourceRef"), messageActionRefsParam(params), requestSignal === void 0 ? {} : { signal: requestSignal });
|
|
2163
2238
|
case "source.message-copy-link.resolve": return await service.resolveMessageCopyLink(stringParam(params, "sid"), requestSignal === void 0 ? {} : { signal: requestSignal });
|
|
2164
2239
|
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.
|
|
2240
|
+
case "source.shared-recording-detail": return await service.sharedRecordingDetail(stringParam(params, "detailRef"), requestSignal === void 0 ? {} : { signal: requestSignal });
|
|
2166
2241
|
case "source.forward-messages": return await service.forwardSourceMessages(stringParam(params, "sourceRef"), messageActionRefsParam(params), {
|
|
2167
2242
|
...stringParam(params, "targetSourceRef") === "" ? {} : { targetSourceRef: stringParam(params, "targetSourceRef") },
|
|
2168
2243
|
...stringParam(params, "recordUid") === "" ? {} : { recordUid: stringParam(params, "recordUid") },
|
|
@@ -2444,6 +2519,10 @@ function requireUpdateManager(updateManager) {
|
|
|
2444
2519
|
if (updateManager === void 0) throw new ArkmePluginError("plugin-update-unavailable", "插件更新检查暂不可用", true, 503);
|
|
2445
2520
|
return updateManager;
|
|
2446
2521
|
}
|
|
2522
|
+
function requireRemoteHost(host) {
|
|
2523
|
+
if (host === void 0) throw new ArkmePluginError("CAPABILITY_UNSUPPORTED", "当前 DSH 未加载远控 Host", false, 503);
|
|
2524
|
+
return host;
|
|
2525
|
+
}
|
|
2447
2526
|
function requireExtensionInstallTasks(tasks) {
|
|
2448
2527
|
if (tasks === void 0) throw new ArkmePluginError("extension-runtime-unavailable", "当前 DSH 未加载扩展安装运行时", false, 503);
|
|
2449
2528
|
return tasks;
|
|
@@ -2471,19 +2550,71 @@ function extensionEditableVisibilityParam(params) {
|
|
|
2471
2550
|
return value;
|
|
2472
2551
|
}
|
|
2473
2552
|
//#endregion
|
|
2553
|
+
//#region src/dsh-host-capabilities.ts
|
|
2554
|
+
function isRecord(value) {
|
|
2555
|
+
return typeof value === "object" && value !== null;
|
|
2556
|
+
}
|
|
2557
|
+
function sessionController(value) {
|
|
2558
|
+
if (!isRecord(value) || typeof value.openWorkspacePath !== "function") return void 0;
|
|
2559
|
+
return value;
|
|
2560
|
+
}
|
|
2561
|
+
function apiProxy(value) {
|
|
2562
|
+
if (!isRecord(value) || !isRecord(value.host) || typeof value.host.openPath !== "function") return void 0;
|
|
2563
|
+
return value;
|
|
2564
|
+
}
|
|
2565
|
+
/**
|
|
2566
|
+
* Open one Arkme-owned, already-validated local path through the current DSH
|
|
2567
|
+
* Host capability. The service shape is detected at call time so one plugin
|
|
2568
|
+
* package works with both the Typert Remote and legacy ApiProxy generations.
|
|
2569
|
+
*/
|
|
2570
|
+
async function openDshHostPath(ctx, path, signal) {
|
|
2571
|
+
signal.throwIfAborted();
|
|
2572
|
+
const current = sessionController(ctx.get("sessionController"));
|
|
2573
|
+
if (current !== void 0) {
|
|
2574
|
+
const response = await current.openWorkspacePath({ path }, signal);
|
|
2575
|
+
if (!isRecord(response) || response.opened !== true) throw new Error("DSH sessionController.openWorkspacePath 返回了无效响应");
|
|
2576
|
+
return;
|
|
2577
|
+
}
|
|
2578
|
+
const legacy = apiProxy(ctx.get("apiProxy"));
|
|
2579
|
+
if (legacy === void 0) throw new Error("当前 DSH 宿主未提供本机文件打开能力");
|
|
2580
|
+
const response = await legacy.host.openPath({
|
|
2581
|
+
rpcId: randomUUID(),
|
|
2582
|
+
payload: { path }
|
|
2583
|
+
}, signal);
|
|
2584
|
+
if (!isRecord(response) || !isRecord(response.result)) throw new Error("DSH apiProxy.host.openPath 返回了无效响应");
|
|
2585
|
+
if (response.result.ok === false) {
|
|
2586
|
+
const error = response.result.error;
|
|
2587
|
+
throw new Error(isRecord(error) && typeof error.message === "string" ? error.message : "DSH apiProxy.host.openPath 调用失败");
|
|
2588
|
+
}
|
|
2589
|
+
if (response.result.ok !== true || !isRecord(response.result.value) || response.result.value.opened !== true) throw new Error("DSH apiProxy.host.openPath 返回了无效响应");
|
|
2590
|
+
}
|
|
2591
|
+
//#endregion
|
|
2474
2592
|
//#region src/harness-embed-contract.ts
|
|
2475
2593
|
const ARKME_HARNESS_EMBED_PATH = "/arkme-self/harness-frame";
|
|
2476
2594
|
//#endregion
|
|
2477
2595
|
//#region src/harness-embed-route.ts
|
|
2478
2596
|
const ARKME_PLUGIN_PACKAGE_NAME = "@senguoyun/dsh-arkme";
|
|
2479
2597
|
const DSH_CLIENT_HMR_PACKAGE_NAME = "@deepseek-ai/dsh-client-hmr";
|
|
2480
|
-
const
|
|
2598
|
+
const REQUIRED_BOOT_PACKAGE_GROUPS = [{
|
|
2599
|
+
capability: "client module loader",
|
|
2600
|
+
alternatives: ["@deepseek-ai/dsh-client-modules"]
|
|
2601
|
+
}, {
|
|
2602
|
+
capability: "client connection runtime",
|
|
2603
|
+
alternatives: ["@deepseek-ai/dsh-client-runtime", "@deepseek-ai/dsh-client-connection"]
|
|
2604
|
+
}];
|
|
2605
|
+
/** Forward only the same-origin browser session needed by authenticated DSH roots. */
|
|
2606
|
+
function dshRootDocumentHeaders(request) {
|
|
2607
|
+
const headers = { Accept: "text/html" };
|
|
2608
|
+
if (request.headers.cookie !== void 0) headers.Cookie = request.headers.cookie;
|
|
2609
|
+
return headers;
|
|
2610
|
+
}
|
|
2481
2611
|
function shortHash(input) {
|
|
2482
2612
|
return createHash("sha1").update(input).digest("hex").slice(0, 12);
|
|
2483
2613
|
}
|
|
2484
|
-
function
|
|
2485
|
-
return
|
|
2614
|
+
function serializedBootGraph(graph) {
|
|
2615
|
+
return JSON.stringify(graph).replaceAll("<", "\\u003c");
|
|
2486
2616
|
}
|
|
2617
|
+
const BOOT_ASSIGNMENT_RENDERERS = [(graph) => `window.__DSH_BOOT__ = ${serializedBootGraph(graph)}`, (graph) => `globalThis["__DSH_BOOT__"] = ${serializedBootGraph(graph)}`];
|
|
2487
2618
|
function referencesPackage(specifier, packageName) {
|
|
2488
2619
|
return specifier === packageName || specifier.startsWith(`${packageName}/`);
|
|
2489
2620
|
}
|
|
@@ -2493,10 +2624,17 @@ function dependencyRemovedBy(entry, removedPackageNames) {
|
|
|
2493
2624
|
if (removed !== void 0) return removed;
|
|
2494
2625
|
}
|
|
2495
2626
|
}
|
|
2496
|
-
function
|
|
2497
|
-
|
|
2627
|
+
function requiredBootPackages(entries) {
|
|
2628
|
+
const packageNames = new Set(entries.map((entry) => entry.id));
|
|
2629
|
+
const required = /* @__PURE__ */ new Set();
|
|
2630
|
+
for (const group of REQUIRED_BOOT_PACKAGE_GROUPS) {
|
|
2631
|
+
const available = group.alternatives.filter((packageName) => packageNames.has(packageName));
|
|
2632
|
+
if (available.length === 0) throw new Error(`harness boot graph: required ${group.capability} is missing; expected one of ${group.alternatives.join(", ")}`);
|
|
2633
|
+
for (const packageName of available) required.add(packageName);
|
|
2634
|
+
}
|
|
2635
|
+
return required;
|
|
2498
2636
|
}
|
|
2499
|
-
function completeRemovedPackageClosure(entries, removedPackageNames) {
|
|
2637
|
+
function completeRemovedPackageClosure(entries, removedPackageNames, requiredPackageNames) {
|
|
2500
2638
|
let changed = true;
|
|
2501
2639
|
while (changed) {
|
|
2502
2640
|
changed = false;
|
|
@@ -2504,7 +2642,7 @@ function completeRemovedPackageClosure(entries, removedPackageNames) {
|
|
|
2504
2642
|
if (removedPackageNames.has(entry.id)) continue;
|
|
2505
2643
|
const removed = dependencyRemovedBy(entry, removedPackageNames);
|
|
2506
2644
|
if (removed === void 0) continue;
|
|
2507
|
-
if (
|
|
2645
|
+
if (requiredPackageNames.has(entry.id)) throw new Error(`harness boot graph: required package ${entry.id} depends on removed package ${removed}`);
|
|
2508
2646
|
removedPackageNames.add(entry.id);
|
|
2509
2647
|
changed = true;
|
|
2510
2648
|
}
|
|
@@ -2516,32 +2654,67 @@ function assertNoRemovedDependencies(entries, removedPackageNames) {
|
|
|
2516
2654
|
if (removed !== void 0) throw new Error(`harness boot graph: kept package ${entry.id} depends on removed package ${removed}`);
|
|
2517
2655
|
}
|
|
2518
2656
|
}
|
|
2657
|
+
function projectBootBatches(batches, keptPackageNames) {
|
|
2658
|
+
if (batches === void 0) return void 0;
|
|
2659
|
+
const projected = batches.flatMap((batch) => {
|
|
2660
|
+
const entries = batch.entries.filter((packageName) => keptPackageNames.has(packageName));
|
|
2661
|
+
return entries.length === 0 ? [] : [{
|
|
2662
|
+
...batch,
|
|
2663
|
+
entries
|
|
2664
|
+
}];
|
|
2665
|
+
});
|
|
2666
|
+
const covered = /* @__PURE__ */ new Set();
|
|
2667
|
+
for (const batch of projected) for (const packageName of batch.entries) {
|
|
2668
|
+
if (covered.has(packageName)) throw new Error(`harness boot graph: package ${packageName} belongs to more than one projected batch`);
|
|
2669
|
+
covered.add(packageName);
|
|
2670
|
+
}
|
|
2671
|
+
for (const packageName of keptPackageNames) if (!covered.has(packageName)) throw new Error(`harness boot graph: package ${packageName} belongs to no projected batch`);
|
|
2672
|
+
return projected;
|
|
2673
|
+
}
|
|
2519
2674
|
/**
|
|
2520
2675
|
* Build the iframe-only DSH graph without any Arkme-managed extension clients.
|
|
2521
2676
|
* Host halves stay process-owned and untouched; only this document's browser
|
|
2522
2677
|
* boot graph is projected.
|
|
2523
2678
|
*/
|
|
2524
2679
|
function projectHarnessBootGraph(graph, installedPackageNames) {
|
|
2680
|
+
const requiredPackageNames = requiredBootPackages(graph.entries);
|
|
2525
2681
|
const removedPackageNames = /* @__PURE__ */ new Set([
|
|
2526
2682
|
ARKME_PLUGIN_PACKAGE_NAME,
|
|
2527
2683
|
DSH_CLIENT_HMR_PACKAGE_NAME,
|
|
2528
2684
|
...installedPackageNames.map((name) => name.trim()).filter(Boolean)
|
|
2529
2685
|
]);
|
|
2530
|
-
completeRemovedPackageClosure(graph.entries, removedPackageNames);
|
|
2686
|
+
completeRemovedPackageClosure(graph.entries, removedPackageNames, requiredPackageNames);
|
|
2531
2687
|
const entries = graph.entries.filter((entry) => !removedPackageNames.has(entry.id));
|
|
2532
|
-
for (const required of
|
|
2688
|
+
for (const required of requiredPackageNames) if (!entries.some((entry) => entry.id === required)) throw new Error(`harness boot graph: required package ${required} was removed`);
|
|
2533
2689
|
assertNoRemovedDependencies(entries, removedPackageNames);
|
|
2690
|
+
const batches = projectBootBatches(graph.batches, new Set(entries.map((entry) => entry.id)));
|
|
2534
2691
|
return {
|
|
2535
|
-
rev: shortHash(JSON.stringify(entries
|
|
2536
|
-
|
|
2692
|
+
rev: shortHash(JSON.stringify(batches === void 0 ? entries : {
|
|
2693
|
+
entries,
|
|
2694
|
+
batches
|
|
2695
|
+
})),
|
|
2696
|
+
entries,
|
|
2697
|
+
...batches === void 0 ? {} : { batches }
|
|
2537
2698
|
};
|
|
2538
2699
|
}
|
|
2539
2700
|
/** Replace exactly the graph DSH injected into the current root document. */
|
|
2540
2701
|
function replaceHarnessBootGraph(html, fullGraph, projectedGraph) {
|
|
2541
|
-
const
|
|
2542
|
-
const
|
|
2543
|
-
|
|
2544
|
-
|
|
2702
|
+
const matches = [];
|
|
2703
|
+
for (const render of BOOT_ASSIGNMENT_RENDERERS) {
|
|
2704
|
+
const assignment = render(fullGraph);
|
|
2705
|
+
let at = html.indexOf(assignment);
|
|
2706
|
+
while (at !== -1) {
|
|
2707
|
+
matches.push({
|
|
2708
|
+
at,
|
|
2709
|
+
assignment,
|
|
2710
|
+
render
|
|
2711
|
+
});
|
|
2712
|
+
at = html.indexOf(assignment, at + assignment.length);
|
|
2713
|
+
}
|
|
2714
|
+
}
|
|
2715
|
+
if (matches.length !== 1) throw new Error("harness boot graph: expected exactly one current DSH boot assignment");
|
|
2716
|
+
const { at, assignment, render } = matches[0];
|
|
2717
|
+
return `${html.slice(0, at)}${render(projectedGraph)}${html.slice(at + assignment.length)}`;
|
|
2545
2718
|
}
|
|
2546
2719
|
function createHarnessEmbedRouteHandler(options) {
|
|
2547
2720
|
return async (request, response) => {
|
|
@@ -2556,7 +2729,7 @@ function createHarnessEmbedRouteHandler(options) {
|
|
|
2556
2729
|
try {
|
|
2557
2730
|
const fullGraph = options.getGraph();
|
|
2558
2731
|
const projectedGraph = projectHarnessBootGraph(fullGraph, options.installedPackageNames());
|
|
2559
|
-
const html = replaceHarnessBootGraph(await options.readRootHtml(), fullGraph, projectedGraph);
|
|
2732
|
+
const html = replaceHarnessBootGraph(await options.readRootHtml(request), fullGraph, projectedGraph);
|
|
2560
2733
|
const body = Buffer.from(html);
|
|
2561
2734
|
response.writeHead(200, {
|
|
2562
2735
|
"Content-Type": "text/html; charset=utf-8",
|
|
@@ -3268,6 +3441,220 @@ var ArkmeKeychainStore = class {
|
|
|
3268
3441
|
if (process.platform !== "darwin") throw new Error("当前版本只支持使用 macOS Keychain 保存 Arkme 登录凭据");
|
|
3269
3442
|
}
|
|
3270
3443
|
};
|
|
3444
|
+
const MACOS_KEYCHAIN_JXA_SCRIPT = String.raw`
|
|
3445
|
+
ObjC.import('Foundation')
|
|
3446
|
+
ObjC.import('Security')
|
|
3447
|
+
function run(argv) {
|
|
3448
|
+
if (argv.length !== 2) throw new Error('invalid Keychain coordinates')
|
|
3449
|
+
const query = $.NSMutableDictionary.dictionary
|
|
3450
|
+
query.setObjectForKey($("genp"), $("class"))
|
|
3451
|
+
query.setObjectForKey($(argv[0]), $("acct"))
|
|
3452
|
+
query.setObjectForKey($(argv[1]), $("svce"))
|
|
3453
|
+
const payload = $.NSFileHandle.fileHandleWithStandardInput.readDataToEndOfFile
|
|
3454
|
+
const update = $.NSMutableDictionary.dictionary
|
|
3455
|
+
update.setObjectForKey(payload, $("v_Data"))
|
|
3456
|
+
let status = Number($.SecItemUpdate(query, update))
|
|
3457
|
+
if (status === -25300) {
|
|
3458
|
+
query.setObjectForKey(payload, $("v_Data"))
|
|
3459
|
+
status = Number($.SecItemAdd(query, null))
|
|
3460
|
+
}
|
|
3461
|
+
if (status !== 0) throw new Error('Keychain write failed with status ' + String(status))
|
|
3462
|
+
}
|
|
3463
|
+
`;
|
|
3464
|
+
const MACOS_KEYCHAIN_READ_JXA_SCRIPT = String.raw`
|
|
3465
|
+
ObjC.import('Foundation')
|
|
3466
|
+
ObjC.import('Security')
|
|
3467
|
+
function run(argv) {
|
|
3468
|
+
if (argv.length !== 2) throw new Error('invalid Keychain coordinates')
|
|
3469
|
+
const query = $.NSMutableDictionary.dictionary
|
|
3470
|
+
query.setObjectForKey($("genp"), $("class"))
|
|
3471
|
+
query.setObjectForKey($(argv[0]), $("acct"))
|
|
3472
|
+
query.setObjectForKey($(argv[1]), $("svce"))
|
|
3473
|
+
query.setObjectForKey($.NSNumber.numberWithBool(true), $("r_Data"))
|
|
3474
|
+
query.setObjectForKey($("m_LimitOne"), $("m_Limit"))
|
|
3475
|
+
const result = Ref()
|
|
3476
|
+
const status = Number($.SecItemCopyMatching(query, result))
|
|
3477
|
+
if (status === -25300) return JSON.stringify({ found: false })
|
|
3478
|
+
if (status !== 0) throw new Error('Keychain read failed with status ' + String(status))
|
|
3479
|
+
const data = ObjC.castRefToObject(result[0])
|
|
3480
|
+
const encoded = data.base64EncodedStringWithOptions(0)
|
|
3481
|
+
return JSON.stringify({ found: true, payload: ObjC.unwrap(encoded) })
|
|
3482
|
+
}
|
|
3483
|
+
`;
|
|
3484
|
+
const MACOS_KEYCHAIN_DELETE_JXA_SCRIPT = String.raw`
|
|
3485
|
+
ObjC.import('Foundation')
|
|
3486
|
+
ObjC.import('Security')
|
|
3487
|
+
function run(argv) {
|
|
3488
|
+
if (argv.length !== 2) throw new Error('invalid Keychain coordinates')
|
|
3489
|
+
const query = $.NSMutableDictionary.dictionary
|
|
3490
|
+
query.setObjectForKey($("genp"), $("class"))
|
|
3491
|
+
query.setObjectForKey($(argv[0]), $("acct"))
|
|
3492
|
+
query.setObjectForKey($(argv[1]), $("svce"))
|
|
3493
|
+
const status = Number($.SecItemDelete(query))
|
|
3494
|
+
if (status !== 0 && status !== -25300) {
|
|
3495
|
+
throw new Error('Keychain delete failed with status ' + String(status))
|
|
3496
|
+
}
|
|
3497
|
+
}
|
|
3498
|
+
`;
|
|
3499
|
+
async function runMacOSKeychainJXA(script, args, operation, stdin) {
|
|
3500
|
+
return await new Promise((resolve, reject) => {
|
|
3501
|
+
const child = spawn("/usr/bin/osascript", [
|
|
3502
|
+
"-l",
|
|
3503
|
+
"JavaScript",
|
|
3504
|
+
"-e",
|
|
3505
|
+
script,
|
|
3506
|
+
...args
|
|
3507
|
+
], {
|
|
3508
|
+
detached: true,
|
|
3509
|
+
shell: false,
|
|
3510
|
+
stdio: [
|
|
3511
|
+
"pipe",
|
|
3512
|
+
"pipe",
|
|
3513
|
+
"pipe"
|
|
3514
|
+
]
|
|
3515
|
+
});
|
|
3516
|
+
let stdout = "";
|
|
3517
|
+
let stderr = "";
|
|
3518
|
+
let settled = false;
|
|
3519
|
+
const finish = (error) => {
|
|
3520
|
+
if (settled) return;
|
|
3521
|
+
settled = true;
|
|
3522
|
+
clearTimeout(timer);
|
|
3523
|
+
if (error === void 0) resolve(stdout.trim());
|
|
3524
|
+
else reject(error);
|
|
3525
|
+
};
|
|
3526
|
+
const terminate = () => {
|
|
3527
|
+
if (child.pid !== void 0) try {
|
|
3528
|
+
process.kill(-child.pid, "SIGKILL");
|
|
3529
|
+
} catch {
|
|
3530
|
+
child.kill("SIGKILL");
|
|
3531
|
+
}
|
|
3532
|
+
else child.kill("SIGKILL");
|
|
3533
|
+
};
|
|
3534
|
+
const timer = setTimeout(() => {
|
|
3535
|
+
terminate();
|
|
3536
|
+
finish(/* @__PURE__ */ new Error(`${operation} macOS Keychain 超时`));
|
|
3537
|
+
}, 5e3);
|
|
3538
|
+
timer.unref();
|
|
3539
|
+
child.stdout.setEncoding("utf8");
|
|
3540
|
+
child.stdout.on("data", (chunk) => {
|
|
3541
|
+
if (stdout.length + String(chunk).length > 1024 * 1024) {
|
|
3542
|
+
terminate();
|
|
3543
|
+
finish(/* @__PURE__ */ new Error(`macOS Keychain ${operation}结果超出限制`));
|
|
3544
|
+
return;
|
|
3545
|
+
}
|
|
3546
|
+
stdout += String(chunk);
|
|
3547
|
+
});
|
|
3548
|
+
child.stderr.setEncoding("utf8");
|
|
3549
|
+
child.stderr.on("data", (chunk) => {
|
|
3550
|
+
if (stderr.length < 64 * 1024) stderr += String(chunk).slice(0, 64 * 1024 - stderr.length);
|
|
3551
|
+
});
|
|
3552
|
+
child.once("error", (error) => finish(new Error(`无法启动 macOS Keychain ${operation}命令`, { cause: error })));
|
|
3553
|
+
child.once("close", (code) => {
|
|
3554
|
+
if (code === 0) finish();
|
|
3555
|
+
else finish(/* @__PURE__ */ new Error(`macOS Keychain ${operation}失败(退出码 ${String(code)}):${stderr.trim().slice(0, 512)}`));
|
|
3556
|
+
});
|
|
3557
|
+
child.stdin.once("error", (error) => finish(new Error(`无法向 macOS Keychain ${operation}命令写入数据`, { cause: error })));
|
|
3558
|
+
child.stdin.end(stdin ?? "", "utf8");
|
|
3559
|
+
});
|
|
3560
|
+
}
|
|
3561
|
+
function macOSSecureValueCoordinates(args) {
|
|
3562
|
+
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 写入参数无效");
|
|
3563
|
+
return {
|
|
3564
|
+
account: args[2],
|
|
3565
|
+
service: args[4]
|
|
3566
|
+
};
|
|
3567
|
+
}
|
|
3568
|
+
async function writeArkmeMacOSSecureValue(args, payload) {
|
|
3569
|
+
const coordinates = macOSSecureValueCoordinates(args);
|
|
3570
|
+
await runMacOSKeychainJXA(MACOS_KEYCHAIN_JXA_SCRIPT, [coordinates.account, coordinates.service], "写入", payload);
|
|
3571
|
+
}
|
|
3572
|
+
async function readArkmeMacOSSecureValue(account, service) {
|
|
3573
|
+
const output = await runMacOSKeychainJXA(MACOS_KEYCHAIN_READ_JXA_SCRIPT, [account, service], "读取");
|
|
3574
|
+
let result;
|
|
3575
|
+
try {
|
|
3576
|
+
result = JSON.parse(output);
|
|
3577
|
+
} catch (error) {
|
|
3578
|
+
throw new Error("macOS Keychain 读取结果无效", { cause: error });
|
|
3579
|
+
}
|
|
3580
|
+
if (result === null || typeof result !== "object") throw new Error("macOS Keychain 读取结果无效");
|
|
3581
|
+
const record = result;
|
|
3582
|
+
if (record.found === false) return void 0;
|
|
3583
|
+
if (record.found !== true || typeof record.payload !== "string") throw new Error("macOS Keychain 读取结果无效");
|
|
3584
|
+
return Buffer.from(record.payload, "base64").toString("utf8");
|
|
3585
|
+
}
|
|
3586
|
+
async function deleteArkmeMacOSSecureValue(account, service) {
|
|
3587
|
+
await runMacOSKeychainJXA(MACOS_KEYCHAIN_DELETE_JXA_SCRIPT, [account, service], "删除");
|
|
3588
|
+
}
|
|
3589
|
+
var ArkmeMacOSSecureValueStore = class {
|
|
3590
|
+
service;
|
|
3591
|
+
writer;
|
|
3592
|
+
platform;
|
|
3593
|
+
reader;
|
|
3594
|
+
deleter;
|
|
3595
|
+
constructor(service, writer = writeArkmeMacOSSecureValue, platform = process.platform, reader = readArkmeMacOSSecureValue, deleter = deleteArkmeMacOSSecureValue) {
|
|
3596
|
+
this.service = service;
|
|
3597
|
+
this.writer = writer;
|
|
3598
|
+
this.platform = platform;
|
|
3599
|
+
this.reader = reader;
|
|
3600
|
+
this.deleter = deleter;
|
|
3601
|
+
}
|
|
3602
|
+
async read(account) {
|
|
3603
|
+
this.requireMacOS();
|
|
3604
|
+
try {
|
|
3605
|
+
return await this.reader(account, this.service);
|
|
3606
|
+
} catch (error) {
|
|
3607
|
+
throw new Error("无法读取 macOS Keychain 中的 Arkme 安全数据", { cause: error });
|
|
3608
|
+
}
|
|
3609
|
+
}
|
|
3610
|
+
async write(account, payload) {
|
|
3611
|
+
this.requireMacOS();
|
|
3612
|
+
await this.writer([
|
|
3613
|
+
"add-generic-password",
|
|
3614
|
+
"-a",
|
|
3615
|
+
account,
|
|
3616
|
+
"-s",
|
|
3617
|
+
this.service,
|
|
3618
|
+
"-U",
|
|
3619
|
+
"-w"
|
|
3620
|
+
], payload);
|
|
3621
|
+
}
|
|
3622
|
+
async delete(account) {
|
|
3623
|
+
this.requireMacOS();
|
|
3624
|
+
try {
|
|
3625
|
+
await this.deleter(account, this.service);
|
|
3626
|
+
} catch (error) {
|
|
3627
|
+
throw new Error("无法删除 macOS Keychain 中的 Arkme 安全数据", { cause: error });
|
|
3628
|
+
}
|
|
3629
|
+
}
|
|
3630
|
+
requireMacOS() {
|
|
3631
|
+
if (this.platform !== "darwin") throw new Error("macOS Keychain 只能在 macOS 使用");
|
|
3632
|
+
}
|
|
3633
|
+
};
|
|
3634
|
+
var ArkmeWindowsSecureValueStore = class {
|
|
3635
|
+
service;
|
|
3636
|
+
backend;
|
|
3637
|
+
constructor(service, backend = new ArkmeWindowsCredentialManagerBackend()) {
|
|
3638
|
+
this.service = service;
|
|
3639
|
+
this.backend = backend;
|
|
3640
|
+
}
|
|
3641
|
+
async read(account) {
|
|
3642
|
+
return await this.backend.read(this.service, account);
|
|
3643
|
+
}
|
|
3644
|
+
async write(account, payload) {
|
|
3645
|
+
if (Buffer.byteLength(payload, "utf16le") > WINDOWS_CREDENTIAL_BLOB_MAX_BYTES) throw new Error("Arkme 安全数据超出 Windows Credential Manager 容量限制");
|
|
3646
|
+
await this.backend.write(this.service, account, payload);
|
|
3647
|
+
}
|
|
3648
|
+
async delete(account) {
|
|
3649
|
+
await this.backend.delete(this.service, account);
|
|
3650
|
+
}
|
|
3651
|
+
};
|
|
3652
|
+
function createArkmeSecureValueStore(service, options = {}) {
|
|
3653
|
+
const platform = options.platform ?? process.platform;
|
|
3654
|
+
if (platform === "darwin") return new ArkmeMacOSSecureValueStore(service);
|
|
3655
|
+
if (platform === "win32") return new ArkmeWindowsSecureValueStore(service, options.windowsBackend);
|
|
3656
|
+
throw new Error(`当前版本只支持在 macOS 或 Windows 保存 Arkme 安全数据,当前平台:${platform}`);
|
|
3657
|
+
}
|
|
3271
3658
|
var ArkmeWindowsCredentialStore = class {
|
|
3272
3659
|
service;
|
|
3273
3660
|
backend;
|
|
@@ -3808,11 +4195,18 @@ var ArkmeLocalDatabase = class {
|
|
|
3808
4195
|
//#region src/managed-ai/adapter.ts
|
|
3809
4196
|
const ARKME_MANAGED_PROVIDER = "arkme-managed";
|
|
3810
4197
|
const ARKME_MANAGED_MODEL = "deepseek-v4-flash";
|
|
3811
|
-
const ARKME_MANAGED_PROVIDER_NAME = "Arkme";
|
|
3812
|
-
const
|
|
4198
|
+
const ARKME_MANAGED_PROVIDER_NAME = "Arkme · 余额计费";
|
|
4199
|
+
const ARKME_MANAGED_CATALOG_TTL_MS = 6e4;
|
|
4200
|
+
const ARKME_MANAGED_CATALOG_TIMEOUT_MS = 1e4;
|
|
3813
4201
|
const ARKME_INSUFFICIENT_BALANCE_MESSAGE = "Arkme AI 余额不足,请前往 Arkme 设置中的余额充值后重试";
|
|
3814
4202
|
const ARKME_LOGIN_MESSAGE = "请先登录或重新登录 Arkme 后再使用托管模型";
|
|
3815
|
-
const
|
|
4203
|
+
const ARKME_MANAGED_FALLBACK_MODEL = {
|
|
4204
|
+
id: ARKME_MANAGED_MODEL,
|
|
4205
|
+
name: "DeepSeek-V4-Flash",
|
|
4206
|
+
contextWindow: 1e6,
|
|
4207
|
+
maxTokens: 256e3
|
|
4208
|
+
};
|
|
4209
|
+
const ARKME_MANAGED_FALLBACK_MODELS = [ARKME_MANAGED_FALLBACK_MODEL];
|
|
3816
4210
|
const ARKME_AUTH_FAILURE_CODES = /* @__PURE__ */ new Set([
|
|
3817
4211
|
"login-required",
|
|
3818
4212
|
"login-expired",
|
|
@@ -3910,9 +4304,8 @@ function localizeManagedAiError(error) {
|
|
|
3910
4304
|
...facts.requestId === void 0 ? {} : { requestId: ProviderRequestId(facts.requestId) }
|
|
3911
4305
|
});
|
|
3912
4306
|
}
|
|
3913
|
-
function
|
|
4307
|
+
function assertManagedProvider(provider) {
|
|
3914
4308
|
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
4309
|
}
|
|
3917
4310
|
async function resolveBearer(owner) {
|
|
3918
4311
|
try {
|
|
@@ -3932,11 +4325,195 @@ async function resolveBearer(owner) {
|
|
|
3932
4325
|
}
|
|
3933
4326
|
throw new LlmError(ARKME_LOGIN_MESSAGE, "AUTH");
|
|
3934
4327
|
}
|
|
4328
|
+
function managedConnection(baseUrl, models) {
|
|
4329
|
+
const defaults = models.find((model) => model.id === "deepseek-v4-flash") ?? models[0] ?? ARKME_MANAGED_FALLBACK_MODEL;
|
|
4330
|
+
return resolveAdapterOptions({
|
|
4331
|
+
baseURL: baseUrl,
|
|
4332
|
+
thinking: "enabled",
|
|
4333
|
+
reasoningEffort: "high",
|
|
4334
|
+
maxTokens: defaults.maxTokens ?? 256e3,
|
|
4335
|
+
defaultContextWindow: defaults.contextWindow ?? 1e6,
|
|
4336
|
+
models: [...models],
|
|
4337
|
+
retryPolicy: {
|
|
4338
|
+
mode: "normal",
|
|
4339
|
+
maxRetries: 0
|
|
4340
|
+
}
|
|
4341
|
+
});
|
|
4342
|
+
}
|
|
4343
|
+
function requiredCatalogText(source, key, label) {
|
|
4344
|
+
const value = source[key];
|
|
4345
|
+
if (typeof value !== "string") throw new LlmError(`Arkme 模型目录中的${label}无效`, "MALFORMED_RESPONSE");
|
|
4346
|
+
const normalized = value.trim();
|
|
4347
|
+
if (normalized === "" || normalized.length > 256) throw new LlmError(`Arkme 模型目录中的${label}无效`, "MALFORMED_RESPONSE");
|
|
4348
|
+
return normalized;
|
|
4349
|
+
}
|
|
4350
|
+
function requiredCatalogModelId(source) {
|
|
4351
|
+
const model = requiredCatalogText(source, "public_model_code", "模型 ID");
|
|
4352
|
+
if (!/^[^\s\u0000-\u001F\u007F]+$/u.test(model)) throw new LlmError("Arkme 模型目录中的模型 ID 无效", "MALFORMED_RESPONSE");
|
|
4353
|
+
return model;
|
|
4354
|
+
}
|
|
4355
|
+
function requiredCatalogTokens(source, key, label) {
|
|
4356
|
+
const value = source[key];
|
|
4357
|
+
if (typeof value !== "string" || !/^[1-9]\d*$/u.test(value)) throw new LlmError(`Arkme 模型目录中的${label}无效`, "MALFORMED_RESPONSE");
|
|
4358
|
+
const parsed = Number(value);
|
|
4359
|
+
if (!Number.isSafeInteger(parsed)) throw new LlmError(`Arkme 模型目录中的${label}无效`, "MALFORMED_RESPONSE");
|
|
4360
|
+
return parsed;
|
|
4361
|
+
}
|
|
4362
|
+
function parseManagedCatalog(payload) {
|
|
4363
|
+
const envelope = asRecord(payload);
|
|
4364
|
+
if (envelope?.code !== 200) {
|
|
4365
|
+
const status = validHttpStatus(envelope?.code);
|
|
4366
|
+
throw localizeManagedAiError(new LlmError(typeof envelope?.message === "string" ? envelope.message : "Arkme 模型目录请求失败", "MANAGED_MODEL_CATALOG", { ...status === void 0 ? {} : { status } }));
|
|
4367
|
+
}
|
|
4368
|
+
const items = asRecord(envelope.data)?.item_ls;
|
|
4369
|
+
if (!Array.isArray(items) || items.length > 256) throw new LlmError("Arkme 模型目录返回异常", "MALFORMED_RESPONSE");
|
|
4370
|
+
const seen = /* @__PURE__ */ new Set();
|
|
4371
|
+
return items.map((value) => {
|
|
4372
|
+
const item = asRecord(value);
|
|
4373
|
+
if (item?.provider !== "arkme-managed") throw new LlmError("Arkme 模型目录包含无效提供商", "MALFORMED_RESPONSE");
|
|
4374
|
+
const id = requiredCatalogModelId(item);
|
|
4375
|
+
if (seen.has(id)) throw new LlmError(`Arkme 模型目录包含重复模型“${id}”`, "MALFORMED_RESPONSE");
|
|
4376
|
+
seen.add(id);
|
|
4377
|
+
const contextWindow = requiredCatalogTokens(item, "context_window_tokens", "上下文窗口");
|
|
4378
|
+
const defaultMaxTokens = requiredCatalogTokens(item, "default_max_output_tokens", "默认输出上限");
|
|
4379
|
+
const maximumMaxTokens = requiredCatalogTokens(item, "maximum_max_output_tokens", "最大输出上限");
|
|
4380
|
+
if (defaultMaxTokens > maximumMaxTokens || maximumMaxTokens > contextWindow) throw new LlmError(`Arkme 模型“${id}”的 Token 上限无效`, "MALFORMED_RESPONSE");
|
|
4381
|
+
return {
|
|
4382
|
+
id,
|
|
4383
|
+
name: requiredCatalogText(item, "display_name", "显示名称"),
|
|
4384
|
+
contextWindow,
|
|
4385
|
+
maxTokens: defaultMaxTokens
|
|
4386
|
+
};
|
|
4387
|
+
});
|
|
4388
|
+
}
|
|
4389
|
+
function waitForSignal(promise, signal) {
|
|
4390
|
+
if (signal.aborted) return Promise.reject(signal.reason ?? new DOMException("Aborted", "AbortError"));
|
|
4391
|
+
return new Promise((resolve, reject) => {
|
|
4392
|
+
const onAbort = () => {
|
|
4393
|
+
signal.removeEventListener("abort", onAbort);
|
|
4394
|
+
reject(signal.reason ?? new DOMException("Aborted", "AbortError"));
|
|
4395
|
+
};
|
|
4396
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
4397
|
+
promise.then((value) => {
|
|
4398
|
+
signal.removeEventListener("abort", onAbort);
|
|
4399
|
+
resolve(value);
|
|
4400
|
+
}, (error) => {
|
|
4401
|
+
signal.removeEventListener("abort", onAbort);
|
|
4402
|
+
reject(error);
|
|
4403
|
+
});
|
|
4404
|
+
});
|
|
4405
|
+
}
|
|
4406
|
+
var ManagedModelCatalog = class {
|
|
4407
|
+
credentialOwner;
|
|
4408
|
+
baseUrl;
|
|
4409
|
+
fetchImpl;
|
|
4410
|
+
connectionSnapshot;
|
|
4411
|
+
modelIds = new Set(ARKME_MANAGED_FALLBACK_MODELS.map((model) => model.id));
|
|
4412
|
+
hasRemoteSnapshot = false;
|
|
4413
|
+
refreshedAt = 0;
|
|
4414
|
+
refreshPromise;
|
|
4415
|
+
constructor(intelligentBaseUrl, credentialOwner, fetchImpl) {
|
|
4416
|
+
this.credentialOwner = credentialOwner;
|
|
4417
|
+
this.baseUrl = managedAiBaseUrl(intelligentBaseUrl);
|
|
4418
|
+
this.fetchImpl = fetchImpl;
|
|
4419
|
+
this.connectionSnapshot = managedConnection(this.baseUrl, ARKME_MANAGED_FALLBACK_MODELS);
|
|
4420
|
+
}
|
|
4421
|
+
connection() {
|
|
4422
|
+
return this.connectionSnapshot;
|
|
4423
|
+
}
|
|
4424
|
+
isFresh() {
|
|
4425
|
+
return this.hasRemoteSnapshot && Date.now() - this.refreshedAt < ARKME_MANAGED_CATALOG_TTL_MS;
|
|
4426
|
+
}
|
|
4427
|
+
async fetchCatalog(signal) {
|
|
4428
|
+
const controller = new AbortController();
|
|
4429
|
+
let timedOut = false;
|
|
4430
|
+
const abortFromCaller = () => {
|
|
4431
|
+
controller.abort(signal?.reason);
|
|
4432
|
+
};
|
|
4433
|
+
if (signal?.aborted === true) abortFromCaller();
|
|
4434
|
+
else signal?.addEventListener("abort", abortFromCaller, { once: true });
|
|
4435
|
+
const timeout = setTimeout(() => {
|
|
4436
|
+
timedOut = true;
|
|
4437
|
+
controller.abort();
|
|
4438
|
+
}, ARKME_MANAGED_CATALOG_TIMEOUT_MS);
|
|
4439
|
+
try {
|
|
4440
|
+
if (controller.signal.aborted) throw controller.signal.reason ?? new DOMException("Aborted", "AbortError");
|
|
4441
|
+
const bearer = await waitForSignal(resolveBearer(this.credentialOwner), controller.signal);
|
|
4442
|
+
const response = await this.fetchImpl(`${this.baseUrl}/models/query`, {
|
|
4443
|
+
method: "POST",
|
|
4444
|
+
headers: {
|
|
4445
|
+
Accept: "application/json",
|
|
4446
|
+
Authorization: `Bearer ${bearer}`,
|
|
4447
|
+
"Content-Type": "application/json"
|
|
4448
|
+
},
|
|
4449
|
+
body: "{}",
|
|
4450
|
+
signal: controller.signal
|
|
4451
|
+
});
|
|
4452
|
+
if (!response.ok) {
|
|
4453
|
+
const requestId = response.headers.get("x-request-id");
|
|
4454
|
+
throw localizeManagedAiError(new LlmError("Arkme 模型目录请求失败", "MANAGED_MODEL_CATALOG", {
|
|
4455
|
+
status: response.status,
|
|
4456
|
+
...requestId === null || requestId === "" ? {} : { requestId: ProviderRequestId(requestId) }
|
|
4457
|
+
}));
|
|
4458
|
+
}
|
|
4459
|
+
let payload;
|
|
4460
|
+
try {
|
|
4461
|
+
payload = await response.json();
|
|
4462
|
+
} catch (error) {
|
|
4463
|
+
if (controller.signal.aborted) throw error;
|
|
4464
|
+
throw new LlmError("Arkme 模型目录返回异常", "MALFORMED_RESPONSE", { cause: error });
|
|
4465
|
+
}
|
|
4466
|
+
const models = parseManagedCatalog(payload);
|
|
4467
|
+
this.connectionSnapshot = managedConnection(this.baseUrl, models);
|
|
4468
|
+
this.modelIds = new Set(models.map((model) => model.id));
|
|
4469
|
+
this.hasRemoteSnapshot = true;
|
|
4470
|
+
this.refreshedAt = Date.now();
|
|
4471
|
+
} catch (error) {
|
|
4472
|
+
if (signal?.aborted === true) throw localizeManagedAiError(new LlmError("Arkme 模型目录请求已取消", "ABORTED", { cause: error }));
|
|
4473
|
+
if (timedOut) throw localizeManagedAiError(new LlmError("Arkme 模型目录请求超时", "TIMEOUT", { cause: error }));
|
|
4474
|
+
if (error instanceof LlmError) throw error;
|
|
4475
|
+
throw localizeManagedAiError(new LlmError("无法连接 Arkme 模型目录", "TRANSPORT", { cause: error }));
|
|
4476
|
+
} finally {
|
|
4477
|
+
clearTimeout(timeout);
|
|
4478
|
+
signal?.removeEventListener("abort", abortFromCaller);
|
|
4479
|
+
}
|
|
4480
|
+
}
|
|
4481
|
+
refresh(signal) {
|
|
4482
|
+
if (signal !== void 0) return this.fetchCatalog(signal);
|
|
4483
|
+
if (this.refreshPromise !== void 0) return this.refreshPromise;
|
|
4484
|
+
const refresh = this.fetchCatalog(signal).finally(() => {
|
|
4485
|
+
if (this.refreshPromise === refresh) this.refreshPromise = void 0;
|
|
4486
|
+
});
|
|
4487
|
+
this.refreshPromise = refresh;
|
|
4488
|
+
return refresh;
|
|
4489
|
+
}
|
|
4490
|
+
async refreshForListing() {
|
|
4491
|
+
if (this.isFresh()) return;
|
|
4492
|
+
try {
|
|
4493
|
+
await this.refresh();
|
|
4494
|
+
} catch {}
|
|
4495
|
+
}
|
|
4496
|
+
async ensureModel(model, signal) {
|
|
4497
|
+
const known = this.modelIds.has(model);
|
|
4498
|
+
if (known && !this.hasRemoteSnapshot || this.isFresh()) return;
|
|
4499
|
+
try {
|
|
4500
|
+
await this.refresh(signal);
|
|
4501
|
+
} catch (error) {
|
|
4502
|
+
if (signal?.aborted === true) throw error;
|
|
4503
|
+
if (!known) throw error;
|
|
4504
|
+
}
|
|
4505
|
+
}
|
|
4506
|
+
assertModel(model) {
|
|
4507
|
+
if (!this.modelIds.has(model)) throw new LlmError(`当前 Arkme 托管服务不支持模型“${model}”,请重新选择模型`, "UNKNOWN_MODEL");
|
|
4508
|
+
}
|
|
4509
|
+
};
|
|
3935
4510
|
var ManagedAiLlmAdapter = class extends LlmAdapter {
|
|
3936
4511
|
delegate;
|
|
3937
|
-
|
|
4512
|
+
catalog;
|
|
4513
|
+
constructor(delegate, catalog) {
|
|
3938
4514
|
super();
|
|
3939
4515
|
this.delegate = delegate;
|
|
4516
|
+
this.catalog = catalog;
|
|
3940
4517
|
}
|
|
3941
4518
|
providerInfo(provider) {
|
|
3942
4519
|
return {
|
|
@@ -3947,17 +4524,22 @@ var ManagedAiLlmAdapter = class extends LlmAdapter {
|
|
|
3947
4524
|
providerRetryPolicy(provider) {
|
|
3948
4525
|
return this.delegate.providerRetryPolicy(provider);
|
|
3949
4526
|
}
|
|
3950
|
-
listModels(provider) {
|
|
3951
|
-
|
|
3952
|
-
|
|
4527
|
+
async listModels(provider) {
|
|
4528
|
+
assertManagedProvider(provider);
|
|
4529
|
+
await this.catalog.refreshForListing();
|
|
4530
|
+
return await this.delegate.listModels(provider);
|
|
3953
4531
|
}
|
|
3954
4532
|
async resolveModel(provider, model, signal) {
|
|
3955
|
-
|
|
4533
|
+
assertManagedProvider(provider);
|
|
4534
|
+
await this.catalog.ensureModel(model, signal);
|
|
4535
|
+
this.catalog.assertModel(model);
|
|
3956
4536
|
return await this.delegate.resolveModel(provider, model, signal);
|
|
3957
4537
|
}
|
|
3958
4538
|
async *stream(options) {
|
|
3959
|
-
assertManagedRoute(options.provider, options.model);
|
|
3960
4539
|
try {
|
|
4540
|
+
assertManagedProvider(options.provider);
|
|
4541
|
+
await this.catalog.ensureModel(options.model, options.signal);
|
|
4542
|
+
this.catalog.assertModel(options.model);
|
|
3961
4543
|
yield* this.delegate.stream(options);
|
|
3962
4544
|
} catch (error) {
|
|
3963
4545
|
throw localizeManagedAiError(error);
|
|
@@ -3965,29 +4547,12 @@ var ManagedAiLlmAdapter = class extends LlmAdapter {
|
|
|
3965
4547
|
}
|
|
3966
4548
|
};
|
|
3967
4549
|
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
|
-
});
|
|
4550
|
+
const catalog = new ManagedModelCatalog(options.intelligentBaseUrl, options.credentialOwner, options.fetchImpl ?? globalThis.fetch);
|
|
3986
4551
|
return new ManagedAiLlmAdapter(new DeepSeekAdapter({
|
|
3987
|
-
options: () => connection,
|
|
4552
|
+
options: () => catalog.connection(),
|
|
3988
4553
|
resolveApiKey: async () => await resolveBearer(options.credentialOwner),
|
|
3989
4554
|
resolveUserId: options.resolveAnonymousUserId ?? getOrCreateAnonymousUserId
|
|
3990
|
-
}));
|
|
4555
|
+
}), catalog);
|
|
3991
4556
|
}
|
|
3992
4557
|
function registerManagedAiProvider(ctx, options) {
|
|
3993
4558
|
return ctx.llm.registerAdapter([ARKME_MANAGED_PROVIDER], createManagedAiLlmAdapter(options));
|
|
@@ -6417,14 +6982,14 @@ var ArkmeExtensionManager = class {
|
|
|
6417
6982
|
};
|
|
6418
6983
|
//#endregion
|
|
6419
6984
|
//#region src/extensions/publish-client.ts
|
|
6420
|
-
function record(value) {
|
|
6985
|
+
function record$1(value) {
|
|
6421
6986
|
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
|
|
6422
6987
|
}
|
|
6423
6988
|
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);
|
|
6989
|
+
const manifest = record$1(value);
|
|
6990
|
+
const runtime = record$1(manifest?.runtime);
|
|
6991
|
+
const halves = record$1(manifest?.halves);
|
|
6992
|
+
const entrypoints = record$1(manifest?.entrypoints);
|
|
6428
6993
|
const permissions = manifest?.permissions;
|
|
6429
6994
|
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
6995
|
return {
|
|
@@ -8165,7 +8730,7 @@ function sqliteHexText(value) {
|
|
|
8165
8730
|
}
|
|
8166
8731
|
//#endregion
|
|
8167
8732
|
//#region src/state-store.ts
|
|
8168
|
-
function emptyState() {
|
|
8733
|
+
function emptyState$1() {
|
|
8169
8734
|
return {
|
|
8170
8735
|
version: 1,
|
|
8171
8736
|
uniqueCode: randomUUID(),
|
|
@@ -8212,7 +8777,7 @@ function normalizedPending(value) {
|
|
|
8212
8777
|
}
|
|
8213
8778
|
function parseState(raw) {
|
|
8214
8779
|
const parsed = JSON.parse(raw);
|
|
8215
|
-
if (parsed === null || typeof parsed !== "object") return emptyState();
|
|
8780
|
+
if (parsed === null || typeof parsed !== "object") return emptyState$1();
|
|
8216
8781
|
const source = parsed;
|
|
8217
8782
|
const pendingByUser = {};
|
|
8218
8783
|
if (source.pendingByUser !== null && typeof source.pendingByUser === "object") for (const [userId, pending] of Object.entries(source.pendingByUser)) pendingByUser[userId] = normalizedPending(pending);
|
|
@@ -8318,7 +8883,7 @@ var ArkmeStateStore = class {
|
|
|
8318
8883
|
this.state = parseState(await readFile(this.path, "utf8"));
|
|
8319
8884
|
} catch (error) {
|
|
8320
8885
|
if (error.code !== "ENOENT") throw error;
|
|
8321
|
-
this.state = emptyState();
|
|
8886
|
+
this.state = emptyState$1();
|
|
8322
8887
|
await this.write(this.state);
|
|
8323
8888
|
}
|
|
8324
8889
|
return this.state;
|
|
@@ -8414,18 +8979,18 @@ function requiredAgentId$2(agent) {
|
|
|
8414
8979
|
return id;
|
|
8415
8980
|
}
|
|
8416
8981
|
function fingerprintArguments(operationKey, value) {
|
|
8417
|
-
return createHash("sha256").update(`arkme-conversational-confirmation-v1\0${operationKey}\0${canonicalJson(value)}`).digest("hex");
|
|
8982
|
+
return createHash("sha256").update(`arkme-conversational-confirmation-v1\0${operationKey}\0${canonicalJson$1(value)}`).digest("hex");
|
|
8418
8983
|
}
|
|
8419
|
-
function canonicalJson(value) {
|
|
8984
|
+
function canonicalJson$1(value) {
|
|
8420
8985
|
if (value === null || typeof value === "boolean" || typeof value === "string") return JSON.stringify(value);
|
|
8421
8986
|
if (typeof value === "number") {
|
|
8422
8987
|
if (!Number.isFinite(value)) throw new Error("对话确认参数必须是有限 JSON 数值");
|
|
8423
8988
|
return JSON.stringify(value);
|
|
8424
8989
|
}
|
|
8425
|
-
if (Array.isArray(value)) return `[${value.map(canonicalJson).join(",")}]`;
|
|
8990
|
+
if (Array.isArray(value)) return `[${value.map(canonicalJson$1).join(",")}]`;
|
|
8426
8991
|
if (typeof value === "object") {
|
|
8427
8992
|
const source = value;
|
|
8428
|
-
return `{${Object.keys(source).filter((key) => source[key] !== void 0).sort().map((key) => `${JSON.stringify(key)}:${canonicalJson(source[key])}`).join(",")}}`;
|
|
8993
|
+
return `{${Object.keys(source).filter((key) => source[key] !== void 0).sort().map((key) => `${JSON.stringify(key)}:${canonicalJson$1(source[key])}`).join(",")}}`;
|
|
8429
8994
|
}
|
|
8430
8995
|
throw new Error("对话确认参数必须是可序列化 JSON");
|
|
8431
8996
|
}
|
|
@@ -12462,7 +13027,7 @@ function inclusiveRange(from, to) {
|
|
|
12462
13027
|
count
|
|
12463
13028
|
};
|
|
12464
13029
|
}
|
|
12465
|
-
function codePointLength(value) {
|
|
13030
|
+
function codePointLength$1(value) {
|
|
12466
13031
|
return [...value].length;
|
|
12467
13032
|
}
|
|
12468
13033
|
function recordingFingerprint(value) {
|
|
@@ -12547,7 +13112,7 @@ function timelineModelItemLength(item) {
|
|
|
12547
13112
|
item.todo,
|
|
12548
13113
|
...item.tags,
|
|
12549
13114
|
...item.participants
|
|
12550
|
-
].reduce((total, value) => total + codePointLength(value), 0);
|
|
13115
|
+
].reduce((total, value) => total + codePointLength$1(value), 0);
|
|
12551
13116
|
}
|
|
12552
13117
|
const DAYS_OUTPUT_SCHEMA = {
|
|
12553
13118
|
type: "object",
|
|
@@ -12886,7 +13451,7 @@ async function readTranscriptPage(ports, localDate, continued, limit, signal) {
|
|
|
12886
13451
|
while (itemOffset < section.items.length && items.length < limit) {
|
|
12887
13452
|
const item = section.items[itemOffset];
|
|
12888
13453
|
if (item === void 0) break;
|
|
12889
|
-
const itemLength = codePointLength(item.text);
|
|
13454
|
+
const itemLength = codePointLength$1(item.text);
|
|
12890
13455
|
if (itemLength > RECORDING_TEXT_BUDGET) {
|
|
12891
13456
|
itemOffset += 1;
|
|
12892
13457
|
unavailableCount += 1;
|
|
@@ -13460,7 +14025,7 @@ const worldMineToolModule = defineArkmeCoreToolModule({
|
|
|
13460
14025
|
});
|
|
13461
14026
|
//#endregion
|
|
13462
14027
|
//#region src/tools/business/world/user.ts
|
|
13463
|
-
function positiveInteger(value, name) {
|
|
14028
|
+
function positiveInteger$2(value, name) {
|
|
13464
14029
|
if (!Number.isSafeInteger(value) || value <= 0) throw new Error(`${name} 必须是正整数`);
|
|
13465
14030
|
return value;
|
|
13466
14031
|
}
|
|
@@ -13500,7 +14065,7 @@ const worldUserToolModule = defineArkmeCoreToolModule({
|
|
|
13500
14065
|
output: TEXT_OUTPUT$3,
|
|
13501
14066
|
isConcurrencySafe: () => true,
|
|
13502
14067
|
async execute(args, exec) {
|
|
13503
|
-
const result = await ports.listUserWorldFeed(positiveInteger(args.user_id, "user_id"), {
|
|
14068
|
+
const result = await ports.listUserWorldFeed(positiveInteger$2(args.user_id, "user_id"), {
|
|
13504
14069
|
limit: Math.min(20, Math.max(1, Math.trunc(args.limit ?? 20))),
|
|
13505
14070
|
offset: offset(args.offset),
|
|
13506
14071
|
signal: exec.signal
|
|
@@ -14758,91 +15323,2999 @@ function registerArkmeTools(ctx, ports, profile = "business") {
|
|
|
14758
15323
|
}
|
|
14759
15324
|
[...fileToolModules];
|
|
14760
15325
|
//#endregion
|
|
14761
|
-
//#region src/
|
|
14762
|
-
const
|
|
14763
|
-
const
|
|
14764
|
-
|
|
14765
|
-
|
|
14766
|
-
|
|
14767
|
-
|
|
14768
|
-
|
|
14769
|
-
|
|
14770
|
-
|
|
14771
|
-
|
|
14772
|
-
webrtcBaseUrl: Schema.string().default("https://jotmo-webrtc.senguo.me"),
|
|
14773
|
-
worldBaseUrl: Schema.string().default("https://jotmo-world.senguo.me"),
|
|
14774
|
-
relationBaseUrl: Schema.string().default("https://jotmo-relation.senguo.me"),
|
|
14775
|
-
intelligentBaseUrl: Schema.string().default("https://jotmo-intelligent.senguo.me"),
|
|
14776
|
-
audioBaseUrl: Schema.string().default("https://jotmo-audio.senguo.me"),
|
|
14777
|
-
extensionPublishBaseUrl: Schema.string().default(""),
|
|
14778
|
-
extensionArtifactDirectory: Schema.string().default(""),
|
|
14779
|
-
extensionTrustedSigningKeys: Schema.string().default(ARKME_PRODUCTION_TRUSTED_SIGNING_KEYS),
|
|
14780
|
-
extensionShareDiscoveryEnabled: Schema.boolean().default(true),
|
|
14781
|
-
extensionShareDiscoveryPort: Schema.number().min(1).max(65535).default(DEFAULT_EXTENSION_SHARE_DISCOVERY_PORT),
|
|
14782
|
-
routePath: Schema.string().default("/arkme-self/api"),
|
|
14783
|
-
requestTimeoutMs: Schema.number().min(1e3).max(12e4).default(3e4),
|
|
14784
|
-
maxTextLength: Schema.number().min(1).max(1e5).default(2e4),
|
|
14785
|
-
toolProfile: Schema.union([
|
|
14786
|
-
"business",
|
|
14787
|
-
"atomic",
|
|
14788
|
-
"hybrid",
|
|
14789
|
-
"disabled"
|
|
14790
|
-
]).default("business"),
|
|
14791
|
-
relatedRecordingsEnabled: Schema.boolean().default(true),
|
|
14792
|
-
geetestCaptchaId: Schema.string().default("ec81315ab8b0f18a7bfa13602d01e307"),
|
|
14793
|
-
interwovenMomentsEnabled: Schema.boolean().default(true),
|
|
14794
|
-
chatMemberJoinEventsEnabled: Schema.boolean().default(true),
|
|
14795
|
-
stateDirectory: Schema.string().default(""),
|
|
14796
|
-
keychainServicePrefix: Schema.string().default("com.senqisi.dsh-arkme"),
|
|
14797
|
-
allowNonLoopback: Schema.boolean().default(false),
|
|
14798
|
-
allowProduction: Schema.boolean().default(false),
|
|
14799
|
-
updateCheckEnabled: Schema.boolean().default(true),
|
|
14800
|
-
updateChannel: Schema.union(["stable", "next"]).default("stable"),
|
|
14801
|
-
updateServiceBaseUrl: Schema.string().default("https://api.jotmo.cc"),
|
|
14802
|
-
updateArtifactBaseUrl: Schema.string().default(""),
|
|
14803
|
-
appVersion: Schema.string().default(""),
|
|
14804
|
-
updateCheckIntervalHours: Schema.number().min(1).max(168).default(12),
|
|
14805
|
-
updateAllowLocalInstall: Schema.boolean().default(true),
|
|
14806
|
-
richMediaRenderEnabled: Schema.boolean().default(true),
|
|
14807
|
-
richMediaSendEnabled: Schema.boolean().default(true),
|
|
14808
|
-
maxUploadBytes: Schema.number().min(1024).max(1024 * 1024 * 1024).default(100 * 1024 * 1024),
|
|
14809
|
-
openclawProfile: Schema.string().default("dev"),
|
|
14810
|
-
shareWebsite: Schema.string().default(ARKME_DEFAULT_SHARE_WEBSITE)
|
|
14811
|
-
});
|
|
14812
|
-
const name = "dsh-arkme";
|
|
14813
|
-
const inject = [
|
|
14814
|
-
"webServer",
|
|
14815
|
-
"tools",
|
|
14816
|
-
"systemPrompt",
|
|
14817
|
-
"pluginInventory",
|
|
14818
|
-
"clientModules",
|
|
14819
|
-
"apiProxy"
|
|
14820
|
-
];
|
|
14821
|
-
function readDshRuntimeVersion(dshBinPath) {
|
|
14822
|
-
if (dshBinPath.trim() === "") return void 0;
|
|
14823
|
-
if (dshBinPath.endsWith("/src/bin.ts") || dshBinPath.endsWith("\\src\\bin.ts")) return void 0;
|
|
15326
|
+
//#region src/dsh-remote/types.ts
|
|
15327
|
+
const DSH_REMOTE_PROTOCOL = "dsh.remote";
|
|
15328
|
+
const DSH_REMOTE_MAX_FRAME_BYTES = 60 * 1024;
|
|
15329
|
+
const DSH_REMOTE_MAX_PAGE_RESULT_BYTES = 40 * 1024;
|
|
15330
|
+
//#endregion
|
|
15331
|
+
//#region src/dsh-remote/dsh-event-contract.ts
|
|
15332
|
+
function record(value) {
|
|
15333
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) throw new DshRemoteError("REMOTE_TRANSPORT_FAILED", "DSH HistoryEntry 结构无效", true);
|
|
15334
|
+
return value;
|
|
15335
|
+
}
|
|
15336
|
+
function jsonSnapshot(value, field) {
|
|
14824
15337
|
try {
|
|
14825
|
-
|
|
14826
|
-
|
|
14827
|
-
|
|
14828
|
-
|
|
14829
|
-
|
|
14830
|
-
return typeof manifest.version === "string" && /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/.test(manifest.version) ? manifest.version : void 0;
|
|
14831
|
-
} catch {
|
|
14832
|
-
return;
|
|
15338
|
+
const encoded = JSON.stringify(value);
|
|
15339
|
+
if (encoded === void 0) throw new TypeError(`${field} is not JSON`);
|
|
15340
|
+
return JSON.parse(encoded);
|
|
15341
|
+
} catch (error) {
|
|
15342
|
+
throw new DshRemoteError("REMOTE_TRANSPORT_FAILED", `DSH ${field} 不是有效 JSON`, true, {}, { cause: error });
|
|
14833
15343
|
}
|
|
14834
15344
|
}
|
|
14835
|
-
function
|
|
14836
|
-
|
|
14837
|
-
|
|
14838
|
-
const injected = environment.ARKME_APP_VERSION?.trim();
|
|
14839
|
-
return injected === void 0 || injected === "" ? void 0 : injected;
|
|
15345
|
+
function finiteNumber(value, field) {
|
|
15346
|
+
if (typeof value !== "number" || !Number.isFinite(value)) throw new DshRemoteError("REMOTE_TRANSPORT_FAILED", `DSH ${field} 无效`, true);
|
|
15347
|
+
return value;
|
|
14840
15348
|
}
|
|
14841
|
-
function
|
|
14842
|
-
|
|
14843
|
-
|
|
14844
|
-
const
|
|
14845
|
-
|
|
15349
|
+
function snapshotDshHistoryEntry(value) {
|
|
15350
|
+
const source = record(value.event);
|
|
15351
|
+
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);
|
|
15352
|
+
const event = {
|
|
15353
|
+
type: source.type,
|
|
15354
|
+
seq: Number(source.seq),
|
|
15355
|
+
time: finiteNumber(source.time, "SessionEvent.time"),
|
|
15356
|
+
data: jsonSnapshot(source.data, "SessionEvent.data")
|
|
15357
|
+
};
|
|
15358
|
+
if (source.sourceEventSeqs !== void 0) {
|
|
15359
|
+
if (!Array.isArray(source.sourceEventSeqs) || source.sourceEventSeqs.some((seq) => !Number.isSafeInteger(seq) || Number(seq) < 0)) throw new DshRemoteError("REMOTE_TRANSPORT_FAILED", "DSH sourceEventSeqs 无效", true);
|
|
15360
|
+
event.sourceEventSeqs = [...source.sourceEventSeqs];
|
|
15361
|
+
}
|
|
15362
|
+
if (source.surfaceOp !== void 0) event.surfaceOp = jsonSnapshot(source.surfaceOp, "SessionEvent.surfaceOp");
|
|
15363
|
+
if (source.ignorable !== void 0) {
|
|
15364
|
+
if (source.ignorable !== true) throw new DshRemoteError("REMOTE_TRANSPORT_FAILED", "DSH ignorable 无效", true);
|
|
15365
|
+
event.ignorable = true;
|
|
15366
|
+
}
|
|
15367
|
+
if (value.view === void 0) return { event };
|
|
15368
|
+
const viewSource = record(value.view);
|
|
15369
|
+
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);
|
|
15370
|
+
return {
|
|
15371
|
+
event,
|
|
15372
|
+
view: {
|
|
15373
|
+
for: viewSource.for,
|
|
15374
|
+
view: jsonSnapshot(viewSource.view, "ToolEventView.view")
|
|
15375
|
+
}
|
|
15376
|
+
};
|
|
15377
|
+
}
|
|
15378
|
+
function dshHistoryEntryUserRpcId(entry) {
|
|
15379
|
+
if (entry.event.type !== "user/message" || entry.event.data === null || typeof entry.event.data !== "object" || Array.isArray(entry.event.data)) return void 0;
|
|
15380
|
+
const data = entry.event.data;
|
|
15381
|
+
const message = data.message !== null && typeof data.message === "object" && !Array.isArray(data.message) ? data.message : void 0;
|
|
15382
|
+
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;
|
|
15383
|
+
return typeof source?.rpcId === "string" ? source.rpcId : void 0;
|
|
15384
|
+
}
|
|
15385
|
+
//#endregion
|
|
15386
|
+
//#region src/dsh-remote/api-proxy-adapter.ts
|
|
15387
|
+
function rpcId(prefix) {
|
|
15388
|
+
return `${prefix}-${randomUUID()}`;
|
|
15389
|
+
}
|
|
15390
|
+
function unwrap(response) {
|
|
15391
|
+
if (response.result.ok) return response.result.value;
|
|
15392
|
+
const error = response.result.error;
|
|
15393
|
+
switch (error.code) {
|
|
15394
|
+
case "session-not-found": throw new DshRemoteError("SESSION_NOT_FOUND", "DSH 会话不存在");
|
|
15395
|
+
case "workspace-not-found":
|
|
15396
|
+
case "workspace-invalid-path": throw new DshRemoteError("WORKSPACE_UNAVAILABLE", "DSH 工作目录不可用");
|
|
15397
|
+
case "agent-busy":
|
|
15398
|
+
case "steer-unavailable":
|
|
15399
|
+
case "queue-item-not-found": throw new DshRemoteError("SESSION_STATE_CHANGED", "DSH 会话状态已经变化", true);
|
|
15400
|
+
default: throw new DshRemoteError("REMOTE_TRANSPORT_FAILED", error.message || "DSH 操作失败", true, {
|
|
15401
|
+
dshCode: error.code,
|
|
15402
|
+
dshRejected: true
|
|
15403
|
+
});
|
|
15404
|
+
}
|
|
15405
|
+
}
|
|
15406
|
+
function offsetCursor(offset) {
|
|
15407
|
+
return Buffer.from(JSON.stringify({
|
|
15408
|
+
v: 1,
|
|
15409
|
+
o: offset
|
|
15410
|
+
})).toString("base64url");
|
|
15411
|
+
}
|
|
15412
|
+
function readOffset(cursor) {
|
|
15413
|
+
if (cursor === void 0 || cursor === "") return 0;
|
|
15414
|
+
try {
|
|
15415
|
+
const value = JSON.parse(Buffer.from(cursor, "base64url").toString("utf8"));
|
|
15416
|
+
if (value.v === 1 && Number.isSafeInteger(value.o) && Number(value.o) >= 0) return Number(value.o);
|
|
15417
|
+
} catch {}
|
|
15418
|
+
throw new DshRemoteError("REMOTE_REQUEST_INVALID", "分页游标无效");
|
|
15419
|
+
}
|
|
15420
|
+
function stableSessionId(requestRef) {
|
|
15421
|
+
const bytes = createHash("sha256").update(`dsh-remote-session-v1\n${requestRef}`).digest().subarray(0, 16);
|
|
15422
|
+
bytes[6] = bytes[6] & 15 | 64;
|
|
15423
|
+
bytes[8] = bytes[8] & 63 | 128;
|
|
15424
|
+
const hex = bytes.toString("hex");
|
|
15425
|
+
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
|
|
15426
|
+
}
|
|
15427
|
+
function codePointLength(value) {
|
|
15428
|
+
return [...value].length;
|
|
15429
|
+
}
|
|
15430
|
+
function jsonBytes(value) {
|
|
15431
|
+
return Buffer.byteLength(JSON.stringify(value));
|
|
15432
|
+
}
|
|
15433
|
+
const MAX_ATOMIC_PROJECTION_BYTES = 24 * 1024;
|
|
15434
|
+
function boundedUtf8(value, maxBytes) {
|
|
15435
|
+
if (Buffer.byteLength(value) <= maxBytes) return value;
|
|
15436
|
+
let result = "";
|
|
15437
|
+
let used = 0;
|
|
15438
|
+
for (const point of value) {
|
|
15439
|
+
const bytes = Buffer.byteLength(point);
|
|
15440
|
+
if (used + bytes > maxBytes) break;
|
|
15441
|
+
result += point;
|
|
15442
|
+
used += bytes;
|
|
15443
|
+
}
|
|
15444
|
+
return result;
|
|
15445
|
+
}
|
|
15446
|
+
function pageLimit(value) {
|
|
15447
|
+
return Math.min(50, Math.max(1, Math.trunc(value ?? 50)));
|
|
15448
|
+
}
|
|
15449
|
+
function pageByBytes(input) {
|
|
15450
|
+
const items = [];
|
|
15451
|
+
while (input.offset + items.length < input.all.length && items.length < input.limit) {
|
|
15452
|
+
const candidate = [...items, input.all[input.offset + items.length]];
|
|
15453
|
+
const consumed = input.offset + candidate.length;
|
|
15454
|
+
const nextCursor = consumed < input.all.length ? offsetCursor(consumed) : void 0;
|
|
15455
|
+
const result = input.build(candidate, nextCursor);
|
|
15456
|
+
if (jsonBytes(result) > 40960 || jsonBytes(result) > 524288) break;
|
|
15457
|
+
items.push(candidate.at(-1));
|
|
15458
|
+
}
|
|
15459
|
+
if (items.length === 0 && input.offset < input.all.length) throw new DshRemoteError("CAPABILITY_UNSUPPORTED", "单个 DSH 投影超过远控帧容量");
|
|
15460
|
+
const consumed = input.offset + items.length;
|
|
15461
|
+
return input.build(items, consumed < input.all.length ? offsetCursor(consumed) : void 0);
|
|
15462
|
+
}
|
|
15463
|
+
function projectQuestions(value) {
|
|
15464
|
+
const result = [];
|
|
15465
|
+
for (const raw of value.slice(0, 16)) {
|
|
15466
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) continue;
|
|
15467
|
+
const source = raw;
|
|
15468
|
+
if (typeof source.id !== "string" || typeof source.question !== "string") continue;
|
|
15469
|
+
const options = Array.isArray(source.options) ? source.options.slice(0, 16).flatMap((option) => {
|
|
15470
|
+
if (option === null || typeof option !== "object" || Array.isArray(option)) return [];
|
|
15471
|
+
const item = option;
|
|
15472
|
+
if (typeof item.label !== "string") return [];
|
|
15473
|
+
return [{
|
|
15474
|
+
label: boundedUtf8(item.label, 512),
|
|
15475
|
+
...typeof item.description === "string" ? { description: boundedUtf8(item.description, 2 * 1024) } : {}
|
|
15476
|
+
}];
|
|
15477
|
+
}) : void 0;
|
|
15478
|
+
const projected = {
|
|
15479
|
+
id: boundedUtf8(source.id, 256),
|
|
15480
|
+
question: boundedUtf8(source.question, 4 * 1024),
|
|
15481
|
+
...typeof source.header === "string" ? { header: boundedUtf8(source.header, 512) } : {},
|
|
15482
|
+
...typeof source.detail === "string" ? { detail: boundedUtf8(source.detail, 8 * 1024) } : {},
|
|
15483
|
+
...options === void 0 ? {} : { options },
|
|
15484
|
+
...typeof source.multiSelect === "boolean" ? { multiSelect: source.multiSelect } : {},
|
|
15485
|
+
...source.intent !== null && typeof source.intent === "object" && !Array.isArray(source.intent) && source.intent.kind === "plan-review" && typeof source.intent.approve === "string" ? { intent: {
|
|
15486
|
+
kind: "plan-review",
|
|
15487
|
+
approve: boundedUtf8(source.intent.approve, 512)
|
|
15488
|
+
} } : {}
|
|
15489
|
+
};
|
|
15490
|
+
if (jsonBytes([...result, projected]) > MAX_ATOMIC_PROJECTION_BYTES) return void 0;
|
|
15491
|
+
result.push(projected);
|
|
15492
|
+
}
|
|
15493
|
+
return result;
|
|
15494
|
+
}
|
|
15495
|
+
var DshApiProxyAdapter = class {
|
|
15496
|
+
api;
|
|
15497
|
+
options;
|
|
15498
|
+
pendingInteractions = /* @__PURE__ */ new Map();
|
|
15499
|
+
interactionListeners = /* @__PURE__ */ new Set();
|
|
15500
|
+
projectionListeners = /* @__PURE__ */ new Set();
|
|
15501
|
+
eventController;
|
|
15502
|
+
eventLoop;
|
|
15503
|
+
constructor(api, options = {}) {
|
|
15504
|
+
this.api = api;
|
|
15505
|
+
this.options = options;
|
|
15506
|
+
}
|
|
15507
|
+
capabilities() {
|
|
15508
|
+
const result = [];
|
|
15509
|
+
if (typeof this.api.workspace?.list === "function") result.push("workspace.list");
|
|
15510
|
+
if (typeof this.api.sessions?.list === "function") result.push("session.list");
|
|
15511
|
+
if (typeof this.api.sessions?.create === "function") result.push("session.create");
|
|
15512
|
+
if (typeof this.api.sessions?.history === "function") result.push("session.history");
|
|
15513
|
+
if (typeof this.api.sessions?.prompt === "function") result.push("session.prompt", "session.prompt.queue", "session.prompt.steer");
|
|
15514
|
+
if (typeof this.api.sessions?.cancel === "function") result.push("session.cancel");
|
|
15515
|
+
if (typeof this.api.events?.mux === "function") result.push("session.events");
|
|
15516
|
+
if (typeof this.api.events?.mux === "function" && typeof this.api.respond === "function") result.push("interaction.question.respond", "interaction.approval.respond");
|
|
15517
|
+
return result;
|
|
15518
|
+
}
|
|
15519
|
+
async workspaces() {
|
|
15520
|
+
const list = this.api.workspace?.list;
|
|
15521
|
+
if (typeof list !== "function") this.unsupported("workspace.list");
|
|
15522
|
+
const value = unwrap(await list.call(this.api.workspace, {
|
|
15523
|
+
rpcId: rpcId("remote-workspaces"),
|
|
15524
|
+
payload: {}
|
|
15525
|
+
}));
|
|
15526
|
+
return await Promise.all(value.items.map(async (item) => {
|
|
15527
|
+
let available = false;
|
|
15528
|
+
let path = item.path;
|
|
15529
|
+
try {
|
|
15530
|
+
path = await realpath(item.path);
|
|
15531
|
+
available = (await stat(path)).isDirectory();
|
|
15532
|
+
} catch {
|
|
15533
|
+
available = false;
|
|
15534
|
+
}
|
|
15535
|
+
return {
|
|
15536
|
+
workspaceId: item.workspaceId,
|
|
15537
|
+
title: boundedUtf8(item.title, 512),
|
|
15538
|
+
path,
|
|
15539
|
+
available,
|
|
15540
|
+
sessionIds: [...item.sessionIds]
|
|
15541
|
+
};
|
|
15542
|
+
}));
|
|
15543
|
+
}
|
|
15544
|
+
async workspacePage(input = {}) {
|
|
15545
|
+
const all = (await this.workspaces()).map((item) => ({
|
|
15546
|
+
...item,
|
|
15547
|
+
sessionIds: []
|
|
15548
|
+
}));
|
|
15549
|
+
const offset = readOffset(input.cursor);
|
|
15550
|
+
if (offset > all.length) throw new DshRemoteError("REMOTE_REQUEST_INVALID", "分页游标超出工作目录投影");
|
|
15551
|
+
return pageByBytes({
|
|
15552
|
+
all,
|
|
15553
|
+
offset,
|
|
15554
|
+
limit: pageLimit(input.limit),
|
|
15555
|
+
build: (items, nextCursor) => ({
|
|
15556
|
+
items,
|
|
15557
|
+
...nextCursor === void 0 ? {} : { nextCursor }
|
|
15558
|
+
})
|
|
15559
|
+
});
|
|
15560
|
+
}
|
|
15561
|
+
async sessions(input = {}) {
|
|
15562
|
+
const list = this.api.sessions?.list;
|
|
15563
|
+
if (typeof list !== "function") this.unsupported("session.list");
|
|
15564
|
+
const workspaces = await this.workspaces();
|
|
15565
|
+
const workspaceBySession = /* @__PURE__ */ new Map();
|
|
15566
|
+
for (const workspace of workspaces) for (const sessionId of workspace.sessionIds) workspaceBySession.set(sessionId, workspace.workspaceId);
|
|
15567
|
+
const all = unwrap(await list.call(this.api.sessions, {
|
|
15568
|
+
rpcId: rpcId("remote-sessions"),
|
|
15569
|
+
payload: {}
|
|
15570
|
+
})).items.flatMap((item) => {
|
|
15571
|
+
const workspaceId = workspaceBySession.get(item.sessionId);
|
|
15572
|
+
if (workspaceId === void 0 || input.workspaceId !== void 0 && workspaceId !== input.workspaceId) return [];
|
|
15573
|
+
const titleValue = item.projections?.values.title;
|
|
15574
|
+
return [{
|
|
15575
|
+
sessionId: item.sessionId,
|
|
15576
|
+
workspaceId,
|
|
15577
|
+
...typeof titleValue === "string" && titleValue.trim() !== "" ? { title: titleValue.trim().slice(0, 200) } : {},
|
|
15578
|
+
updatedAt: item.updatedAt,
|
|
15579
|
+
running: item.running,
|
|
15580
|
+
blank: item.blank,
|
|
15581
|
+
...item.projections === void 0 ? {} : { projectionAsOfSeq: item.projections.asOfSeq }
|
|
15582
|
+
}];
|
|
15583
|
+
}).sort((left, right) => right.updatedAt - left.updatedAt);
|
|
15584
|
+
const offset = readOffset(input.cursor);
|
|
15585
|
+
if (offset > all.length) throw new DshRemoteError("REMOTE_REQUEST_INVALID", "分页游标超出会话投影");
|
|
15586
|
+
return pageByBytes({
|
|
15587
|
+
all,
|
|
15588
|
+
offset,
|
|
15589
|
+
limit: pageLimit(input.limit),
|
|
15590
|
+
build: (items, nextCursor) => ({
|
|
15591
|
+
items,
|
|
15592
|
+
...nextCursor === void 0 ? {} : { nextCursor }
|
|
15593
|
+
})
|
|
15594
|
+
});
|
|
15595
|
+
}
|
|
15596
|
+
async history(input) {
|
|
15597
|
+
await this.requireSession(input.sessionId, false);
|
|
15598
|
+
const history = this.api.sessions?.history;
|
|
15599
|
+
if (typeof history !== "function") this.unsupported("session.history");
|
|
15600
|
+
const limit = pageLimit(input.limit);
|
|
15601
|
+
const value = unwrap(await history.call(this.api.sessions, {
|
|
15602
|
+
rpcId: rpcId("remote-history"),
|
|
15603
|
+
payload: {
|
|
15604
|
+
sessionId: input.sessionId,
|
|
15605
|
+
maxMessages: limit,
|
|
15606
|
+
...input.beforeSeq === void 0 ? {} : { beforeSeq: input.beforeSeq }
|
|
15607
|
+
}
|
|
15608
|
+
}));
|
|
15609
|
+
const entries = value.events.map(snapshotDshHistoryEntry);
|
|
15610
|
+
let truncatedByBudget = false;
|
|
15611
|
+
while (entries.length > 0) {
|
|
15612
|
+
const firstSeq = value.events[0]?.event.seq;
|
|
15613
|
+
if (jsonBytes({
|
|
15614
|
+
entries,
|
|
15615
|
+
hasMore: value.hasMore || truncatedByBudget,
|
|
15616
|
+
...firstSeq === void 0 || !value.hasMore && !truncatedByBudget ? {} : { nextCursor: firstSeq },
|
|
15617
|
+
...value.projections === void 0 ? {} : { projectionAsOfSeq: value.projections.asOfSeq }
|
|
15618
|
+
}) <= 524288) break;
|
|
15619
|
+
entries.shift();
|
|
15620
|
+
truncatedByBudget = true;
|
|
15621
|
+
}
|
|
15622
|
+
if (entries.length === 0 && truncatedByBudget) throw new DshRemoteError("CAPABILITY_UNSUPPORTED", "单条 DSH 历史投影超过远控帧容量");
|
|
15623
|
+
const firstSeq = truncatedByBudget ? entries[0]?.event.seq : value.events[0]?.event.seq;
|
|
15624
|
+
return {
|
|
15625
|
+
entries,
|
|
15626
|
+
hasMore: value.hasMore || truncatedByBudget,
|
|
15627
|
+
...firstSeq === void 0 || !value.hasMore && !truncatedByBudget ? {} : { nextCursor: firstSeq },
|
|
15628
|
+
...value.projections === void 0 ? {} : { projectionAsOfSeq: value.projections.asOfSeq }
|
|
15629
|
+
};
|
|
15630
|
+
}
|
|
15631
|
+
historyContainsRpcId(entries, rpcIdValue) {
|
|
15632
|
+
return entries.some((entry) => dshHistoryEntryUserRpcId(entry) === rpcIdValue);
|
|
15633
|
+
}
|
|
15634
|
+
async createSession(input) {
|
|
15635
|
+
const workspace = (await this.workspaces()).find((item) => item.workspaceId === input.workspaceId);
|
|
15636
|
+
if (workspace === void 0 || !workspace.available) throw new DshRemoteError("WORKSPACE_UNAVAILABLE", "工作目录已经删除或不可访问");
|
|
15637
|
+
const create = this.api.sessions?.create;
|
|
15638
|
+
if (typeof create !== "function") this.unsupported("session.create");
|
|
15639
|
+
const sessionId = stableSessionId(input.dshRpcId);
|
|
15640
|
+
const created = unwrap(await create.call(this.api.sessions, {
|
|
15641
|
+
rpcId: input.dshRpcId,
|
|
15642
|
+
payload: {
|
|
15643
|
+
workspaceId: input.workspaceId,
|
|
15644
|
+
sessionId
|
|
15645
|
+
}
|
|
15646
|
+
}));
|
|
15647
|
+
if (created.sessionId !== sessionId) throw new DshRemoteError("REMOTE_INVALID_RESPONSE", "DSH 返回了与预分配值不一致的 SessionId");
|
|
15648
|
+
return created;
|
|
15649
|
+
}
|
|
15650
|
+
async reconcileCreatedSession(input) {
|
|
15651
|
+
const expected = stableSessionId(input.dshRpcId);
|
|
15652
|
+
let cursor;
|
|
15653
|
+
do {
|
|
15654
|
+
const page = await this.sessions({
|
|
15655
|
+
workspaceId: input.workspaceId,
|
|
15656
|
+
limit: 50,
|
|
15657
|
+
...cursor === void 0 ? {} : { cursor }
|
|
15658
|
+
});
|
|
15659
|
+
if (page.items.some((item) => item.sessionId === expected)) return { sessionId: expected };
|
|
15660
|
+
cursor = page.nextCursor;
|
|
15661
|
+
} while (cursor !== void 0);
|
|
15662
|
+
}
|
|
15663
|
+
async prompt(input) {
|
|
15664
|
+
const text = this.textContent(input.content);
|
|
15665
|
+
const session = await this.requireSession(input.sessionId, true);
|
|
15666
|
+
if (input.mode === "steer" && !session.running) throw new DshRemoteError("SESSION_STATE_CHANGED", "会话当前未运行,不能立即引导");
|
|
15667
|
+
const prompt = this.api.sessions?.prompt;
|
|
15668
|
+
if (typeof prompt !== "function") this.unsupported("session.prompt");
|
|
15669
|
+
return unwrap(await prompt.call(this.api.sessions, {
|
|
15670
|
+
rpcId: input.dshRpcId,
|
|
15671
|
+
payload: {
|
|
15672
|
+
sessionId: input.sessionId,
|
|
15673
|
+
mode: input.mode,
|
|
15674
|
+
content: [{
|
|
15675
|
+
type: "text",
|
|
15676
|
+
text
|
|
15677
|
+
}]
|
|
15678
|
+
}
|
|
15679
|
+
}));
|
|
15680
|
+
}
|
|
15681
|
+
async cancel(input) {
|
|
15682
|
+
if (!(await this.requireSession(input.sessionId, false)).running) return { accepted: true };
|
|
15683
|
+
const cancel = this.api.sessions?.cancel;
|
|
15684
|
+
if (typeof cancel !== "function") this.unsupported("session.cancel");
|
|
15685
|
+
return unwrap(await cancel.call(this.api.sessions, {
|
|
15686
|
+
rpcId: input.dshRpcId,
|
|
15687
|
+
payload: { sessionId: input.sessionId }
|
|
15688
|
+
}));
|
|
15689
|
+
}
|
|
15690
|
+
async answerQuestion(input) {
|
|
15691
|
+
const pending = this.pendingInteractions.get(input.interactionRpcRef);
|
|
15692
|
+
if (pending?.kind !== "question" || pending.sessionId !== input.sessionId) this.resolved();
|
|
15693
|
+
await this.respond(input.interactionRpcRef, {
|
|
15694
|
+
sessionId: input.sessionId,
|
|
15695
|
+
answer: input.answer
|
|
15696
|
+
});
|
|
15697
|
+
}
|
|
15698
|
+
async answerApproval(input) {
|
|
15699
|
+
const pending = this.pendingInteractions.get(input.interactionRpcRef);
|
|
15700
|
+
if (pending?.kind !== "approval" || pending.sessionId !== input.sessionId || pending.approvalId !== input.approvalId) this.resolved();
|
|
15701
|
+
if (input.outcome === "allowed-once" && !pending.canAllowOnce) throw new DshRemoteError("CAPABILITY_UNSUPPORTED", "审批上下文不完整,只能在桌面端处理或拒绝");
|
|
15702
|
+
await this.respond(input.interactionRpcRef, {
|
|
15703
|
+
sessionId: input.sessionId,
|
|
15704
|
+
approvalId: input.approvalId,
|
|
15705
|
+
outcome: input.outcome
|
|
15706
|
+
});
|
|
15707
|
+
}
|
|
15708
|
+
pending() {
|
|
15709
|
+
return [...this.pendingInteractions.values()];
|
|
15710
|
+
}
|
|
15711
|
+
subscribeInteractions(listener) {
|
|
15712
|
+
this.interactionListeners.add(listener);
|
|
15713
|
+
return () => {
|
|
15714
|
+
this.interactionListeners.delete(listener);
|
|
15715
|
+
};
|
|
15716
|
+
}
|
|
15717
|
+
subscribeProjectionEvents(listener) {
|
|
15718
|
+
this.projectionListeners.add(listener);
|
|
15719
|
+
return () => {
|
|
15720
|
+
this.projectionListeners.delete(listener);
|
|
15721
|
+
};
|
|
15722
|
+
}
|
|
15723
|
+
startEvents() {
|
|
15724
|
+
if (this.eventLoop !== void 0) return () => {
|
|
15725
|
+
this.stopEvents();
|
|
15726
|
+
};
|
|
15727
|
+
const mux = this.api.events?.mux;
|
|
15728
|
+
if (typeof mux !== "function") return () => void 0;
|
|
15729
|
+
const controller = new AbortController();
|
|
15730
|
+
this.eventController = controller;
|
|
15731
|
+
const loop = this.maintainEvents(mux.bind(this.api.events), controller.signal);
|
|
15732
|
+
this.eventLoop = loop;
|
|
15733
|
+
loop.finally(() => {
|
|
15734
|
+
if (this.eventLoop === loop) this.eventLoop = void 0;
|
|
15735
|
+
if (this.eventController === controller) this.eventController = void 0;
|
|
15736
|
+
});
|
|
15737
|
+
return () => {
|
|
15738
|
+
this.stopEvents();
|
|
15739
|
+
};
|
|
15740
|
+
}
|
|
15741
|
+
stopEvents() {
|
|
15742
|
+
this.eventController?.abort();
|
|
15743
|
+
}
|
|
15744
|
+
async maintainEvents(mux, signal) {
|
|
15745
|
+
let delay = Math.min(5e3, Math.max(10, this.options.eventRetryBaseMillis ?? 250));
|
|
15746
|
+
while (!signal.aborted) {
|
|
15747
|
+
try {
|
|
15748
|
+
await this.consumeEvents(mux, signal);
|
|
15749
|
+
if (signal.aborted) return;
|
|
15750
|
+
} catch (error) {
|
|
15751
|
+
if (signal.aborted) return;
|
|
15752
|
+
console.warn("dsh-arkme: DSH remote mux disconnected:", error instanceof Error ? error.message : String(error));
|
|
15753
|
+
}
|
|
15754
|
+
this.pendingInteractions.clear();
|
|
15755
|
+
this.emitInteractions();
|
|
15756
|
+
await new Promise((resolve) => {
|
|
15757
|
+
const done = () => {
|
|
15758
|
+
signal.removeEventListener("abort", onAbort);
|
|
15759
|
+
resolve();
|
|
15760
|
+
};
|
|
15761
|
+
const timer = setTimeout(done, delay);
|
|
15762
|
+
timer.unref();
|
|
15763
|
+
const onAbort = () => {
|
|
15764
|
+
clearTimeout(timer);
|
|
15765
|
+
done();
|
|
15766
|
+
};
|
|
15767
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
15768
|
+
});
|
|
15769
|
+
delay = Math.min(5e3, delay * 2);
|
|
15770
|
+
}
|
|
15771
|
+
}
|
|
15772
|
+
async snapshot(input = {}) {
|
|
15773
|
+
const workspaces = await this.workspaces();
|
|
15774
|
+
const sessions = [];
|
|
15775
|
+
let cursor;
|
|
15776
|
+
do {
|
|
15777
|
+
const page = await this.sessions({
|
|
15778
|
+
limit: 50,
|
|
15779
|
+
...cursor === void 0 ? {} : { cursor }
|
|
15780
|
+
});
|
|
15781
|
+
sessions.push(...page.items);
|
|
15782
|
+
cursor = page.nextCursor;
|
|
15783
|
+
} while (cursor !== void 0);
|
|
15784
|
+
const flattened = [
|
|
15785
|
+
...workspaces.map((value) => ({
|
|
15786
|
+
kind: "workspace",
|
|
15787
|
+
value: {
|
|
15788
|
+
...value,
|
|
15789
|
+
sessionIds: []
|
|
15790
|
+
}
|
|
15791
|
+
})),
|
|
15792
|
+
...sessions.map((value) => ({
|
|
15793
|
+
kind: "session",
|
|
15794
|
+
value
|
|
15795
|
+
})),
|
|
15796
|
+
...this.pending().map((value) => ({
|
|
15797
|
+
kind: "interaction",
|
|
15798
|
+
value
|
|
15799
|
+
}))
|
|
15800
|
+
];
|
|
15801
|
+
const offset = readOffset(input.cursor);
|
|
15802
|
+
if (offset > flattened.length) throw new DshRemoteError("REMOTE_REQUEST_INVALID", "分页游标超出快照投影");
|
|
15803
|
+
const projectionAsOfMillis = Date.now();
|
|
15804
|
+
return pageByBytes({
|
|
15805
|
+
all: flattened,
|
|
15806
|
+
offset,
|
|
15807
|
+
limit: pageLimit(input.limit),
|
|
15808
|
+
build: (items, nextCursor) => ({
|
|
15809
|
+
projectionAsOfMillis,
|
|
15810
|
+
workspaces: items.flatMap((item) => item.kind === "workspace" ? [item.value] : []),
|
|
15811
|
+
sessions: items.flatMap((item) => item.kind === "session" ? [item.value] : []),
|
|
15812
|
+
pendingInteractions: items.flatMap((item) => item.kind === "interaction" ? [item.value] : []),
|
|
15813
|
+
...nextCursor === void 0 ? {} : { nextCursor }
|
|
15814
|
+
})
|
|
15815
|
+
});
|
|
15816
|
+
}
|
|
15817
|
+
async consumeEvents(mux, signal) {
|
|
15818
|
+
for await (const frame of mux({
|
|
15819
|
+
rpcId: rpcId("remote-mux"),
|
|
15820
|
+
payload: {}
|
|
15821
|
+
}, signal)) {
|
|
15822
|
+
if (signal.aborted) return;
|
|
15823
|
+
const payload = frame.payload;
|
|
15824
|
+
const type = payload.type;
|
|
15825
|
+
let interactionsChanged = false;
|
|
15826
|
+
if (type === "session/event" && typeof payload.sessionId === "string" && payload.event !== null && typeof payload.event === "object" && !Array.isArray(payload.event)) {
|
|
15827
|
+
const event = payload.event;
|
|
15828
|
+
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) {
|
|
15829
|
+
const entry = snapshotDshHistoryEntry({
|
|
15830
|
+
event,
|
|
15831
|
+
view: payload.view
|
|
15832
|
+
});
|
|
15833
|
+
this.emitProjection({
|
|
15834
|
+
kind: "session-event",
|
|
15835
|
+
sessionId: payload.sessionId,
|
|
15836
|
+
entry
|
|
15837
|
+
});
|
|
15838
|
+
}
|
|
15839
|
+
} else if (type === "session/subscribed" && typeof payload.sessionId === "string" && typeof payload.lastSeq === "number" && Number.isSafeInteger(payload.lastSeq) && payload.lastSeq >= -1) this.emitProjection({
|
|
15840
|
+
kind: "mux-baseline",
|
|
15841
|
+
sessionId: payload.sessionId,
|
|
15842
|
+
lastSeq: payload.lastSeq,
|
|
15843
|
+
pendingInteractions: this.pending()
|
|
15844
|
+
});
|
|
15845
|
+
else if (type === "question/requested" && typeof payload.sessionId === "string" && Array.isArray(payload.questions)) {
|
|
15846
|
+
const questions = projectQuestions(payload.questions);
|
|
15847
|
+
if (questions !== void 0 && questions.length > 0) {
|
|
15848
|
+
this.pendingInteractions.set(frame.rpcId, {
|
|
15849
|
+
kind: "question",
|
|
15850
|
+
interactionRpcRef: frame.rpcId,
|
|
15851
|
+
sessionId: payload.sessionId,
|
|
15852
|
+
questions
|
|
15853
|
+
});
|
|
15854
|
+
interactionsChanged = true;
|
|
15855
|
+
}
|
|
15856
|
+
} else if (type === "approval/requested" && typeof payload.sessionId === "string" && typeof payload.approvalId === "string" && typeof payload.toolName === "string") {
|
|
15857
|
+
const reason = typeof payload.reason === "string" && payload.reason.trim() !== "" ? payload.reason.trim().slice(0, 500) : void 0;
|
|
15858
|
+
this.pendingInteractions.set(frame.rpcId, {
|
|
15859
|
+
kind: "approval",
|
|
15860
|
+
interactionRpcRef: frame.rpcId,
|
|
15861
|
+
sessionId: payload.sessionId,
|
|
15862
|
+
approvalId: payload.approvalId,
|
|
15863
|
+
toolName: payload.toolName,
|
|
15864
|
+
...reason === void 0 ? {} : { reason },
|
|
15865
|
+
canAllowOnce: false
|
|
15866
|
+
});
|
|
15867
|
+
interactionsChanged = true;
|
|
15868
|
+
} else if (type === "question/resolved" && typeof payload.questionRpcId === "string") interactionsChanged = this.pendingInteractions.delete(payload.questionRpcId);
|
|
15869
|
+
else if (type === "approval/resolved") {
|
|
15870
|
+
for (const [key, pending] of this.pendingInteractions) if (pending.kind === "approval" && pending.sessionId === payload.sessionId && pending.approvalId === payload.approvalId) {
|
|
15871
|
+
this.pendingInteractions.delete(key);
|
|
15872
|
+
interactionsChanged = true;
|
|
15873
|
+
}
|
|
15874
|
+
}
|
|
15875
|
+
if (interactionsChanged) this.emitInteractions();
|
|
15876
|
+
}
|
|
15877
|
+
}
|
|
15878
|
+
async requireSession(sessionId, requireWorkspaceAvailable) {
|
|
15879
|
+
const workspace = (await this.workspaces()).find((item) => item.sessionIds.includes(sessionId));
|
|
15880
|
+
if (workspace === void 0) throw new DshRemoteError("SESSION_NOT_FOUND", "会话不属于当前 Runtime 的已登记工作目录");
|
|
15881
|
+
if (requireWorkspaceAvailable && !workspace.available) throw new DshRemoteError("WORKSPACE_UNAVAILABLE", "工作目录已经删除或不可访问");
|
|
15882
|
+
let cursor;
|
|
15883
|
+
do {
|
|
15884
|
+
const page = await this.sessions({
|
|
15885
|
+
workspaceId: workspace.workspaceId,
|
|
15886
|
+
limit: 50,
|
|
15887
|
+
...cursor === void 0 ? {} : { cursor }
|
|
15888
|
+
});
|
|
15889
|
+
const session = page.items.find((item) => item.sessionId === sessionId);
|
|
15890
|
+
if (session !== void 0) return session;
|
|
15891
|
+
cursor = page.nextCursor;
|
|
15892
|
+
} while (cursor !== void 0);
|
|
15893
|
+
throw new DshRemoteError("SESSION_NOT_FOUND", "DSH 会话不存在");
|
|
15894
|
+
}
|
|
15895
|
+
textContent(content) {
|
|
15896
|
+
if (!Array.isArray(content) || content.length !== 1) throw new DshRemoteError("CAPABILITY_UNSUPPORTED", "首版远控只支持单个文本消息");
|
|
15897
|
+
const block = content[0];
|
|
15898
|
+
if (block === null || typeof block !== "object" || Array.isArray(block) || block.type !== "text" || typeof block.text !== "string") throw new DshRemoteError("CAPABILITY_UNSUPPORTED", "首版远控不支持图片、文件或附件");
|
|
15899
|
+
const text = block.text;
|
|
15900
|
+
if (text.trim() === "" || codePointLength(text) > 2e4) throw new DshRemoteError("REMOTE_REQUEST_INVALID", "远控文本必须为 1 至 20000 个字符");
|
|
15901
|
+
if (text.trimStart().startsWith("/")) throw new DshRemoteError("CAPABILITY_UNSUPPORTED", "首版远控不执行 DSH slash command");
|
|
15902
|
+
return text;
|
|
15903
|
+
}
|
|
15904
|
+
async respond(rpcIdValue, value) {
|
|
15905
|
+
if (typeof this.api.respond !== "function") this.unsupported("interaction response");
|
|
15906
|
+
if (!(await this.api.respond({
|
|
15907
|
+
type: "client-response",
|
|
15908
|
+
rpcId: rpcIdValue,
|
|
15909
|
+
result: {
|
|
15910
|
+
ok: true,
|
|
15911
|
+
value
|
|
15912
|
+
}
|
|
15913
|
+
})).accepted) this.resolved();
|
|
15914
|
+
this.pendingInteractions.delete(rpcIdValue);
|
|
15915
|
+
this.emitInteractions();
|
|
15916
|
+
}
|
|
15917
|
+
emitInteractions() {
|
|
15918
|
+
const snapshot = this.pending();
|
|
15919
|
+
for (const listener of this.interactionListeners) listener(snapshot);
|
|
15920
|
+
this.emitProjection({
|
|
15921
|
+
kind: "interactions",
|
|
15922
|
+
pendingInteractions: snapshot
|
|
15923
|
+
});
|
|
15924
|
+
}
|
|
15925
|
+
emitProjection(event) {
|
|
15926
|
+
for (const listener of this.projectionListeners) listener(event);
|
|
15927
|
+
}
|
|
15928
|
+
unsupported(capability) {
|
|
15929
|
+
throw new DshRemoteError("CAPABILITY_UNSUPPORTED", `当前 DSH 版本不支持 ${capability}`);
|
|
15930
|
+
}
|
|
15931
|
+
resolved() {
|
|
15932
|
+
throw new DshRemoteError("INTERACTION_RESOLVED", "问题或审批已在其他端处理");
|
|
15933
|
+
}
|
|
15934
|
+
};
|
|
15935
|
+
//#endregion
|
|
15936
|
+
//#region src/dsh-remote/crypto.ts
|
|
15937
|
+
function encodeBase64Url(value) {
|
|
15938
|
+
return Buffer.from(value).toString("base64url");
|
|
15939
|
+
}
|
|
15940
|
+
function decodeBase64Url(value) {
|
|
15941
|
+
if (!/^[A-Za-z0-9_-]*$/.test(value)) throw new DshRemoteError("REMOTE_REQUEST_INVALID", "无效的 base64url 数据");
|
|
15942
|
+
return Buffer.from(value, "base64url");
|
|
15943
|
+
}
|
|
15944
|
+
function normalizedCanonical(value) {
|
|
15945
|
+
if (Array.isArray(value)) return value.map(normalizedCanonical);
|
|
15946
|
+
if (value !== null && typeof value === "object") {
|
|
15947
|
+
const sorted = {};
|
|
15948
|
+
for (const key of Object.keys(value).sort()) {
|
|
15949
|
+
const child = value[key];
|
|
15950
|
+
if (child !== void 0) sorted[key] = normalizedCanonical(child);
|
|
15951
|
+
}
|
|
15952
|
+
return sorted;
|
|
15953
|
+
}
|
|
15954
|
+
if (typeof value === "number" && !Number.isFinite(value)) throw new DshRemoteError("REMOTE_REQUEST_INVALID", "协议数据不能包含非有限数字");
|
|
15955
|
+
return value;
|
|
15956
|
+
}
|
|
15957
|
+
function canonicalJson(value) {
|
|
15958
|
+
return JSON.stringify(normalizedCanonical(value));
|
|
15959
|
+
}
|
|
15960
|
+
function rotateLeft(value, count) {
|
|
15961
|
+
return (value << count | value >>> 32 - count) >>> 0;
|
|
15962
|
+
}
|
|
15963
|
+
function quarterRound(state, a, b, c, d) {
|
|
15964
|
+
state[a] = state[a] + state[b] >>> 0;
|
|
15965
|
+
state[d] = rotateLeft(state[d] ^ state[a], 16);
|
|
15966
|
+
state[c] = state[c] + state[d] >>> 0;
|
|
15967
|
+
state[b] = rotateLeft(state[b] ^ state[c], 12);
|
|
15968
|
+
state[a] = state[a] + state[b] >>> 0;
|
|
15969
|
+
state[d] = rotateLeft(state[d] ^ state[a], 8);
|
|
15970
|
+
state[c] = state[c] + state[d] >>> 0;
|
|
15971
|
+
state[b] = rotateLeft(state[b] ^ state[c], 7);
|
|
15972
|
+
}
|
|
15973
|
+
function hChaCha20(key, nonce) {
|
|
15974
|
+
if (key.length !== 32 || nonce.length !== 16) throw new TypeError("HChaCha20 requires a 32-byte key and 16-byte nonce");
|
|
15975
|
+
const state = /* @__PURE__ */ new Uint32Array(16);
|
|
15976
|
+
const constants = Buffer.from("expand 32-byte k", "ascii");
|
|
15977
|
+
for (let index = 0; index < 4; index += 1) state[index] = constants.readUInt32LE(index * 4);
|
|
15978
|
+
const keyBuffer = Buffer.from(key);
|
|
15979
|
+
const nonceBuffer = Buffer.from(nonce);
|
|
15980
|
+
for (let index = 0; index < 8; index += 1) state[index + 4] = keyBuffer.readUInt32LE(index * 4);
|
|
15981
|
+
for (let index = 0; index < 4; index += 1) state[index + 12] = nonceBuffer.readUInt32LE(index * 4);
|
|
15982
|
+
for (let round = 0; round < 10; round += 1) {
|
|
15983
|
+
quarterRound(state, 0, 4, 8, 12);
|
|
15984
|
+
quarterRound(state, 1, 5, 9, 13);
|
|
15985
|
+
quarterRound(state, 2, 6, 10, 14);
|
|
15986
|
+
quarterRound(state, 3, 7, 11, 15);
|
|
15987
|
+
quarterRound(state, 0, 5, 10, 15);
|
|
15988
|
+
quarterRound(state, 1, 6, 11, 12);
|
|
15989
|
+
quarterRound(state, 2, 7, 8, 13);
|
|
15990
|
+
quarterRound(state, 3, 4, 9, 14);
|
|
15991
|
+
}
|
|
15992
|
+
const result = Buffer.allocUnsafe(32);
|
|
15993
|
+
for (const [outputIndex, stateIndex] of [
|
|
15994
|
+
0,
|
|
15995
|
+
1,
|
|
15996
|
+
2,
|
|
15997
|
+
3,
|
|
15998
|
+
12,
|
|
15999
|
+
13,
|
|
16000
|
+
14,
|
|
16001
|
+
15
|
|
16002
|
+
].entries()) result.writeUInt32LE(state[stateIndex], outputIndex * 4);
|
|
16003
|
+
return result;
|
|
16004
|
+
}
|
|
16005
|
+
function encryptXChaCha20Poly1305(key, plaintext, aad, nonce = randomBytes(24)) {
|
|
16006
|
+
if (key.length !== 32 || nonce.length !== 24) throw new TypeError("XChaCha20-Poly1305 requires a 32-byte key and 24-byte nonce");
|
|
16007
|
+
const cipher = createCipheriv("chacha20-poly1305", hChaCha20(key, nonce.subarray(0, 16)), Buffer.concat([Buffer.alloc(4), Buffer.from(nonce.subarray(16))]), { authTagLength: 16 });
|
|
16008
|
+
const encodedPlaintext = Buffer.from(plaintext);
|
|
16009
|
+
cipher.setAAD(Buffer.from(aad), { plaintextLength: encodedPlaintext.length });
|
|
16010
|
+
const encrypted = Buffer.concat([
|
|
16011
|
+
cipher.update(encodedPlaintext),
|
|
16012
|
+
cipher.final(),
|
|
16013
|
+
cipher.getAuthTag()
|
|
16014
|
+
]);
|
|
16015
|
+
return {
|
|
16016
|
+
nonce: encodeBase64Url(nonce),
|
|
16017
|
+
ciphertext: encodeBase64Url(encrypted)
|
|
16018
|
+
};
|
|
16019
|
+
}
|
|
16020
|
+
function decryptXChaCha20Poly1305(key, input, aad) {
|
|
16021
|
+
const nonce = decodeBase64Url(input.nonce);
|
|
16022
|
+
const encoded = decodeBase64Url(input.ciphertext);
|
|
16023
|
+
if (key.length !== 32 || nonce.length !== 24 || encoded.length < 16) throw new DshRemoteError("REMOTE_REQUEST_INVALID", "远控密文格式无效");
|
|
16024
|
+
try {
|
|
16025
|
+
const decipher = createDecipheriv("chacha20-poly1305", hChaCha20(key, nonce.subarray(0, 16)), Buffer.concat([Buffer.alloc(4), nonce.subarray(16)]), { authTagLength: 16 });
|
|
16026
|
+
decipher.setAAD(Buffer.from(aad), { plaintextLength: encoded.length - 16 });
|
|
16027
|
+
decipher.setAuthTag(encoded.subarray(encoded.length - 16));
|
|
16028
|
+
return Buffer.concat([decipher.update(encoded.subarray(0, encoded.length - 16)), decipher.final()]);
|
|
16029
|
+
} catch (error) {
|
|
16030
|
+
throw new DshRemoteError("REMOTE_STORAGE_FAILED", "命令账本密文认证失败", false, {}, { cause: error });
|
|
16031
|
+
}
|
|
16032
|
+
}
|
|
16033
|
+
//#endregion
|
|
16034
|
+
//#region src/dsh-remote/command-ledger.ts
|
|
16035
|
+
function argumentsHash(operation, argumentsValue) {
|
|
16036
|
+
return createHash("sha256").update(canonicalJson({
|
|
16037
|
+
operation,
|
|
16038
|
+
arguments: argumentsValue
|
|
16039
|
+
})).digest("base64url");
|
|
16040
|
+
}
|
|
16041
|
+
function stableDshRpcId(input) {
|
|
16042
|
+
return `remote_${createHash("sha256").update(`dsh-remote-rpc-v2\n${input.accountId}\n${input.runtimeRef}\n${input.requestRef}`).digest("base64url").slice(0, 32)}`;
|
|
16043
|
+
}
|
|
16044
|
+
var DshRemoteCommandLedger = class {
|
|
16045
|
+
path;
|
|
16046
|
+
database;
|
|
16047
|
+
key;
|
|
16048
|
+
now;
|
|
16049
|
+
constructor(directory, key, options = {}) {
|
|
16050
|
+
if (key.length !== 32) throw new TypeError("DshRemoteCommandLedger key must be 32 bytes");
|
|
16051
|
+
this.key = Buffer.from(key);
|
|
16052
|
+
this.now = options.now ?? Date.now;
|
|
16053
|
+
mkdirSync(directory, {
|
|
16054
|
+
recursive: true,
|
|
16055
|
+
mode: 448
|
|
16056
|
+
});
|
|
16057
|
+
securePrivateDirectorySync(directory);
|
|
16058
|
+
this.path = join(directory, "remote-command-ledger.sqlite3");
|
|
16059
|
+
this.database = new DatabaseSync(this.path);
|
|
16060
|
+
this.database.exec(`
|
|
16061
|
+
PRAGMA journal_mode = WAL;
|
|
16062
|
+
PRAGMA synchronous = FULL;
|
|
16063
|
+
PRAGMA foreign_keys = ON;
|
|
16064
|
+
CREATE TABLE IF NOT EXISTS remote_command_identity_v2 (
|
|
16065
|
+
identity_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
16066
|
+
account_id TEXT NOT NULL,
|
|
16067
|
+
runtime_ref TEXT NOT NULL,
|
|
16068
|
+
request_ref TEXT NOT NULL,
|
|
16069
|
+
operation TEXT NOT NULL,
|
|
16070
|
+
arguments_hash TEXT NOT NULL,
|
|
16071
|
+
dsh_rpc_id TEXT NOT NULL,
|
|
16072
|
+
execute_before_millis INTEGER NOT NULL,
|
|
16073
|
+
created_at_millis INTEGER NOT NULL,
|
|
16074
|
+
UNIQUE (account_id, runtime_ref, request_ref)
|
|
16075
|
+
);
|
|
16076
|
+
CREATE TABLE IF NOT EXISTS remote_command_event_v2 (
|
|
16077
|
+
event_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
16078
|
+
identity_id INTEGER NOT NULL REFERENCES remote_command_identity_v2(identity_id) ON DELETE CASCADE,
|
|
16079
|
+
state TEXT NOT NULL CHECK (state IN ('pending', 'completed', 'outcome_unknown')),
|
|
16080
|
+
nonce TEXT NOT NULL,
|
|
16081
|
+
ciphertext TEXT NOT NULL,
|
|
16082
|
+
created_at_millis INTEGER NOT NULL
|
|
16083
|
+
);
|
|
16084
|
+
CREATE INDEX IF NOT EXISTS remote_command_event_v2_identity
|
|
16085
|
+
ON remote_command_event_v2 (identity_id, event_id DESC);
|
|
16086
|
+
CREATE INDEX IF NOT EXISTS remote_command_identity_v2_created
|
|
16087
|
+
ON remote_command_identity_v2 (created_at_millis DESC);
|
|
16088
|
+
`);
|
|
16089
|
+
this.secureFiles();
|
|
16090
|
+
}
|
|
16091
|
+
begin(input) {
|
|
16092
|
+
this.validateIdentity(input);
|
|
16093
|
+
const hash = argumentsHash(input.operation, input.arguments);
|
|
16094
|
+
const existing = this.identity(input);
|
|
16095
|
+
if (existing !== void 0) {
|
|
16096
|
+
if (existing.operation !== input.operation || existing.arguments_hash !== hash || existing.account_id !== input.accountId) throw new DshRemoteError("SESSION_STATE_CHANGED", "相同 request_ref 携带了不同操作或参数");
|
|
16097
|
+
return {
|
|
16098
|
+
duplicate: true,
|
|
16099
|
+
entry: this.entry(existing)
|
|
16100
|
+
};
|
|
16101
|
+
}
|
|
16102
|
+
const now = this.now();
|
|
16103
|
+
this.transaction(() => {
|
|
16104
|
+
const result = this.database.prepare(`
|
|
16105
|
+
INSERT INTO remote_command_identity_v2 (
|
|
16106
|
+
account_id, runtime_ref, request_ref, operation, arguments_hash,
|
|
16107
|
+
dsh_rpc_id, execute_before_millis, created_at_millis
|
|
16108
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
16109
|
+
`).run(input.accountId, input.runtimeRef, input.requestRef, input.operation, hash, stableDshRpcId(input), input.executeBeforeMillis, now);
|
|
16110
|
+
this.appendEvent(Number(result.lastInsertRowid), "pending", { arguments: input.arguments }, now);
|
|
16111
|
+
});
|
|
16112
|
+
this.secureFiles();
|
|
16113
|
+
const row = this.identity(input);
|
|
16114
|
+
if (row === void 0) throw new DshRemoteError("REMOTE_STORAGE_FAILED", "命令账本写入失败");
|
|
16115
|
+
return {
|
|
16116
|
+
duplicate: false,
|
|
16117
|
+
entry: this.entry(row)
|
|
16118
|
+
};
|
|
16119
|
+
}
|
|
16120
|
+
complete(identity, result) {
|
|
16121
|
+
const row = this.requireIdentity(identity);
|
|
16122
|
+
const current = this.entry(row);
|
|
16123
|
+
if (current.state === "completed") return current;
|
|
16124
|
+
if (current.state === "outcome_unknown") throw new DshRemoteError("COMMAND_OUTCOME_UNKNOWN", "命令结果尚未完成对账,禁止覆盖");
|
|
16125
|
+
this.appendEvent(row.identity_id, "completed", { result }, this.now());
|
|
16126
|
+
this.secureFiles();
|
|
16127
|
+
return this.entry(row);
|
|
16128
|
+
}
|
|
16129
|
+
completeRejected(identity, error) {
|
|
16130
|
+
return this.complete(identity, { rejected: {
|
|
16131
|
+
code: error.code,
|
|
16132
|
+
message: error.message.slice(0, 500),
|
|
16133
|
+
retryable: error.retryable
|
|
16134
|
+
} });
|
|
16135
|
+
}
|
|
16136
|
+
markOutcomeUnknown(identity, reason) {
|
|
16137
|
+
const row = this.requireIdentity(identity);
|
|
16138
|
+
const current = this.entry(row);
|
|
16139
|
+
if (current.state === "completed" || current.state === "outcome_unknown") return current;
|
|
16140
|
+
this.appendEvent(row.identity_id, "outcome_unknown", { reason: reason.slice(0, 500) }, this.now());
|
|
16141
|
+
this.secureFiles();
|
|
16142
|
+
return this.entry(row);
|
|
16143
|
+
}
|
|
16144
|
+
get(identity) {
|
|
16145
|
+
const row = this.identity(identity);
|
|
16146
|
+
if (row === void 0 || row.account_id !== identity.accountId) return void 0;
|
|
16147
|
+
return this.entry(row);
|
|
16148
|
+
}
|
|
16149
|
+
pending(accountId, nowMillis = this.now()) {
|
|
16150
|
+
return this.database.prepare(`
|
|
16151
|
+
SELECT identity_id, account_id, runtime_ref, request_ref, operation,
|
|
16152
|
+
arguments_hash, dsh_rpc_id, execute_before_millis, created_at_millis
|
|
16153
|
+
FROM remote_command_identity_v2
|
|
16154
|
+
WHERE account_id = ?
|
|
16155
|
+
ORDER BY created_at_millis ASC
|
|
16156
|
+
`).all(accountId).map((row) => this.entry(row)).filter((entry) => entry.state === "pending" && entry.executeBeforeMillis >= nowMillis);
|
|
16157
|
+
}
|
|
16158
|
+
/**
|
|
16159
|
+
* Returns every unsettled command still inside the v4 reconciliation window.
|
|
16160
|
+
*
|
|
16161
|
+
* An expired execute_before only prevents executing the command again. It must
|
|
16162
|
+
* not hide the crash window where DSH accepted the stable rpcId before the Host
|
|
16163
|
+
* persisted the result. Callers must inspect DSH history and either complete
|
|
16164
|
+
* the entry or mark outcome_unknown; they must never re-dispatch these rows.
|
|
16165
|
+
*/
|
|
16166
|
+
unsettledForReconciliation(accountId, nowMillis = this.now(), reconciliationWindowMillis = 1440 * 6e4) {
|
|
16167
|
+
if (!Number.isSafeInteger(reconciliationWindowMillis) || reconciliationWindowMillis <= 0) throw new DshRemoteError("REMOTE_REQUEST_INVALID", "命令对账窗口无效");
|
|
16168
|
+
return this.database.prepare(`
|
|
16169
|
+
SELECT identity_id, account_id, runtime_ref, request_ref, operation,
|
|
16170
|
+
arguments_hash, dsh_rpc_id, execute_before_millis, created_at_millis
|
|
16171
|
+
FROM remote_command_identity_v2
|
|
16172
|
+
WHERE account_id = ? AND created_at_millis >= ?
|
|
16173
|
+
ORDER BY created_at_millis ASC
|
|
16174
|
+
`).all(accountId, nowMillis - reconciliationWindowMillis).map((row) => this.entry(row)).filter((entry) => entry.state === "pending");
|
|
16175
|
+
}
|
|
16176
|
+
cleanup(options = {}) {
|
|
16177
|
+
const cutoff = this.now() - (options.retentionMillis ?? 10080 * 6e4);
|
|
16178
|
+
const maxCommands = Math.min(1e5, Math.max(100, options.maxCommands ?? 1e4));
|
|
16179
|
+
const before = Number(this.database.prepare("SELECT COUNT(*) AS count FROM remote_command_identity_v2").get().count);
|
|
16180
|
+
this.transaction(() => {
|
|
16181
|
+
this.database.prepare("DELETE FROM remote_command_identity_v2 WHERE created_at_millis < ?").run(cutoff);
|
|
16182
|
+
this.database.prepare(`
|
|
16183
|
+
DELETE FROM remote_command_identity_v2
|
|
16184
|
+
WHERE identity_id NOT IN (
|
|
16185
|
+
SELECT identity_id FROM remote_command_identity_v2 ORDER BY created_at_millis DESC LIMIT ?
|
|
16186
|
+
)
|
|
16187
|
+
`).run(maxCommands);
|
|
16188
|
+
});
|
|
16189
|
+
const after = Number(this.database.prepare("SELECT COUNT(*) AS count FROM remote_command_identity_v2").get().count);
|
|
16190
|
+
this.secureFiles();
|
|
16191
|
+
return before - after;
|
|
16192
|
+
}
|
|
16193
|
+
close() {
|
|
16194
|
+
this.database.close();
|
|
16195
|
+
this.secureFiles();
|
|
16196
|
+
this.key.fill(0);
|
|
16197
|
+
}
|
|
16198
|
+
validateIdentity(input) {
|
|
16199
|
+
for (const [name, value] of Object.entries({
|
|
16200
|
+
accountId: input.accountId,
|
|
16201
|
+
runtimeRef: input.runtimeRef,
|
|
16202
|
+
requestRef: input.requestRef
|
|
16203
|
+
})) if (value.trim() === "" || value.length > 256) throw new DshRemoteError("REMOTE_REQUEST_INVALID", `${name} 无效`);
|
|
16204
|
+
if (!Number.isSafeInteger(input.executeBeforeMillis) || input.executeBeforeMillis <= 0) throw new DshRemoteError("REMOTE_REQUEST_INVALID", "命令 deadline 无效");
|
|
16205
|
+
}
|
|
16206
|
+
identity(input) {
|
|
16207
|
+
return this.database.prepare(`
|
|
16208
|
+
SELECT identity_id, account_id, runtime_ref, request_ref, operation,
|
|
16209
|
+
arguments_hash, dsh_rpc_id, execute_before_millis, created_at_millis
|
|
16210
|
+
FROM remote_command_identity_v2
|
|
16211
|
+
WHERE account_id = ? AND runtime_ref = ? AND request_ref = ?
|
|
16212
|
+
`).get(input.accountId, input.runtimeRef, input.requestRef);
|
|
16213
|
+
}
|
|
16214
|
+
requireIdentity(input) {
|
|
16215
|
+
const row = this.identity(input);
|
|
16216
|
+
if (row === void 0 || row.account_id !== input.accountId) throw new DshRemoteError("SESSION_STATE_CHANGED", "命令账本记录不存在");
|
|
16217
|
+
return row;
|
|
16218
|
+
}
|
|
16219
|
+
entry(row) {
|
|
16220
|
+
const event = this.database.prepare(`
|
|
16221
|
+
SELECT state, nonce, ciphertext, created_at_millis
|
|
16222
|
+
FROM remote_command_event_v2 WHERE identity_id = ? ORDER BY event_id DESC LIMIT 1
|
|
16223
|
+
`).get(row.identity_id);
|
|
16224
|
+
if (event === void 0) throw new DshRemoteError("REMOTE_STORAGE_FAILED", "命令账本状态缺失");
|
|
16225
|
+
const aad = this.eventAad(row, event.state, event.created_at_millis);
|
|
16226
|
+
let payload;
|
|
16227
|
+
try {
|
|
16228
|
+
payload = JSON.parse(decryptXChaCha20Poly1305(this.key, event, aad).toString("utf8"));
|
|
16229
|
+
} catch (error) {
|
|
16230
|
+
if (error instanceof DshRemoteError) throw new DshRemoteError("REMOTE_STORAGE_FAILED", "命令账本认证失败", false, {}, { cause: error });
|
|
16231
|
+
throw error;
|
|
16232
|
+
}
|
|
16233
|
+
return {
|
|
16234
|
+
accountId: row.account_id,
|
|
16235
|
+
runtimeRef: row.runtime_ref,
|
|
16236
|
+
requestRef: row.request_ref,
|
|
16237
|
+
operation: row.operation,
|
|
16238
|
+
executeBeforeMillis: row.execute_before_millis,
|
|
16239
|
+
argumentsHash: row.arguments_hash,
|
|
16240
|
+
state: event.state,
|
|
16241
|
+
dshRpcId: row.dsh_rpc_id,
|
|
16242
|
+
payload,
|
|
16243
|
+
createdAtMillis: row.created_at_millis,
|
|
16244
|
+
updatedAtMillis: event.created_at_millis
|
|
16245
|
+
};
|
|
16246
|
+
}
|
|
16247
|
+
appendEvent(identityId, state, payload, now) {
|
|
16248
|
+
const row = this.database.prepare(`
|
|
16249
|
+
SELECT identity_id, account_id, runtime_ref, request_ref, operation,
|
|
16250
|
+
arguments_hash, dsh_rpc_id, execute_before_millis, created_at_millis
|
|
16251
|
+
FROM remote_command_identity_v2 WHERE identity_id = ?
|
|
16252
|
+
`).get(identityId);
|
|
16253
|
+
if (row === void 0) throw new DshRemoteError("REMOTE_STORAGE_FAILED", "命令账本 identity 缺失");
|
|
16254
|
+
const encrypted = encryptXChaCha20Poly1305(this.key, canonicalJson(payload), this.eventAad(row, state, now), randomBytes(24));
|
|
16255
|
+
this.database.prepare(`
|
|
16256
|
+
INSERT INTO remote_command_event_v2 (identity_id, state, nonce, ciphertext, created_at_millis)
|
|
16257
|
+
VALUES (?, ?, ?, ?, ?)
|
|
16258
|
+
`).run(identityId, state, encrypted.nonce, encrypted.ciphertext, now);
|
|
16259
|
+
}
|
|
16260
|
+
eventAad(row, state, now) {
|
|
16261
|
+
return canonicalJson({
|
|
16262
|
+
schemaVersion: 2,
|
|
16263
|
+
identityId: row.identity_id,
|
|
16264
|
+
accountId: row.account_id,
|
|
16265
|
+
runtimeRef: row.runtime_ref,
|
|
16266
|
+
requestRef: row.request_ref,
|
|
16267
|
+
operation: row.operation,
|
|
16268
|
+
argumentsHash: row.arguments_hash,
|
|
16269
|
+
state,
|
|
16270
|
+
createdAtMillis: now
|
|
16271
|
+
});
|
|
16272
|
+
}
|
|
16273
|
+
transaction(work) {
|
|
16274
|
+
this.database.exec("BEGIN IMMEDIATE");
|
|
16275
|
+
try {
|
|
16276
|
+
work();
|
|
16277
|
+
this.database.exec("COMMIT");
|
|
16278
|
+
} catch (error) {
|
|
16279
|
+
this.database.exec("ROLLBACK");
|
|
16280
|
+
throw error;
|
|
16281
|
+
}
|
|
16282
|
+
}
|
|
16283
|
+
secureFiles() {
|
|
16284
|
+
for (const suffix of [
|
|
16285
|
+
"",
|
|
16286
|
+
"-wal",
|
|
16287
|
+
"-shm"
|
|
16288
|
+
]) try {
|
|
16289
|
+
securePrivateFileSync(`${this.path}${suffix}`);
|
|
16290
|
+
} catch (error) {
|
|
16291
|
+
if (error.code !== "ENOENT") throw error;
|
|
16292
|
+
}
|
|
16293
|
+
}
|
|
16294
|
+
};
|
|
16295
|
+
//#endregion
|
|
16296
|
+
//#region src/dsh-remote/control-plane.ts
|
|
16297
|
+
const BASE = "/api/v1/dsh-remote";
|
|
16298
|
+
function pathRef(value) {
|
|
16299
|
+
const normalized = value.trim();
|
|
16300
|
+
if (!/^[A-Za-z0-9._:-]{1,256}$/.test(normalized)) throw new DshRemoteError("REMOTE_REQUEST_INVALID", "远控资源引用无效");
|
|
16301
|
+
return encodeURIComponent(normalized);
|
|
16302
|
+
}
|
|
16303
|
+
var DshRemoteHttpControlPlane = class {
|
|
16304
|
+
request;
|
|
16305
|
+
constructor(request) {
|
|
16306
|
+
this.request = request;
|
|
16307
|
+
}
|
|
16308
|
+
async post(path, body, signal) {
|
|
16309
|
+
try {
|
|
16310
|
+
return await this.request.post(path, body, signal);
|
|
16311
|
+
} catch (error) {
|
|
16312
|
+
throw mapDshRemoteControlPlaneError(error);
|
|
16313
|
+
}
|
|
16314
|
+
}
|
|
16315
|
+
async registerDesktop(input, signal) {
|
|
16316
|
+
return await this.post(`${BASE}/desktops/register`, input, signal);
|
|
16317
|
+
}
|
|
16318
|
+
async registerRuntime(desktopRef, input, signal) {
|
|
16319
|
+
return await this.post(`${BASE}/desktops/${pathRef(desktopRef)}/runtimes/register`, input, signal);
|
|
16320
|
+
}
|
|
16321
|
+
async syncWorkspaces(input, signal) {
|
|
16322
|
+
return await this.post(`${BASE}/workspaces/sync`, input, signal);
|
|
16323
|
+
}
|
|
16324
|
+
async syncSessions(input, signal) {
|
|
16325
|
+
return await this.post(`${BASE}/sessions/sync`, input, signal);
|
|
16326
|
+
}
|
|
16327
|
+
async appendSessionEvents(input, signal) {
|
|
16328
|
+
return await this.post(`${BASE}/session-events/append`, input, signal);
|
|
16329
|
+
}
|
|
16330
|
+
};
|
|
16331
|
+
function mapDshRemoteControlPlaneError(error) {
|
|
16332
|
+
if (error instanceof DshRemoteError) return error;
|
|
16333
|
+
const code = error !== null && typeof error === "object" && typeof error.code === "string" ? error.code : "";
|
|
16334
|
+
const mappedCode = (/* @__PURE__ */ new Set([
|
|
16335
|
+
"REMOTE_PROTOCOL_UNSUPPORTED",
|
|
16336
|
+
"REMOTE_REQUEST_INVALID",
|
|
16337
|
+
"REMOTE_INVALID_RESPONSE",
|
|
16338
|
+
"REMOTE_NETWORK_UNAVAILABLE",
|
|
16339
|
+
"REMOTE_STORAGE_FAILED",
|
|
16340
|
+
"REMOTE_TRANSPORT_FAILED",
|
|
16341
|
+
"REMOTE_LOGIN_REQUIRED",
|
|
16342
|
+
"RUNTIME_OFFLINE",
|
|
16343
|
+
"RUNTIME_LIMIT_REACHED",
|
|
16344
|
+
"HOST_CHANNEL_NOT_READY",
|
|
16345
|
+
"HOST_GENERATION_STALE",
|
|
16346
|
+
"REPLAY_GAP",
|
|
16347
|
+
"CAPABILITY_UNSUPPORTED",
|
|
16348
|
+
"REMOTE_NOT_FOUND",
|
|
16349
|
+
"REMOTE_PROJECTION_CONFLICT",
|
|
16350
|
+
"REMOTE_REALTIME_UNAVAILABLE"
|
|
16351
|
+
])).has(code) ? code : "REMOTE_TRANSPORT_FAILED";
|
|
16352
|
+
return new DshRemoteError(mappedCode, error instanceof Error ? error.message : "远控控制面请求失败", [
|
|
16353
|
+
"REMOTE_NETWORK_UNAVAILABLE",
|
|
16354
|
+
"REMOTE_TRANSPORT_FAILED",
|
|
16355
|
+
"RUNTIME_OFFLINE",
|
|
16356
|
+
"HOST_CHANNEL_NOT_READY",
|
|
16357
|
+
"HOST_GENERATION_STALE",
|
|
16358
|
+
"REPLAY_GAP",
|
|
16359
|
+
"REMOTE_REALTIME_UNAVAILABLE"
|
|
16360
|
+
].includes(mappedCode), {}, { cause: error });
|
|
16361
|
+
}
|
|
16362
|
+
//#endregion
|
|
16363
|
+
//#region src/dsh-remote/default-socket-factory.ts
|
|
16364
|
+
function dshRemoteRealtimeEndpoint(realtimeBaseUrl) {
|
|
16365
|
+
const endpoint = new URL(realtimeBaseUrl);
|
|
16366
|
+
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");
|
|
16367
|
+
endpoint.protocol = "wss:";
|
|
16368
|
+
endpoint.pathname = "/api/v1/realtime/connect";
|
|
16369
|
+
return endpoint.toString();
|
|
16370
|
+
}
|
|
16371
|
+
function createDefaultDshRemoteSocket(input) {
|
|
16372
|
+
const accessToken = input.accessToken.trim();
|
|
16373
|
+
if (accessToken === "") throw new TypeError("DSH remote Realtime access token is required");
|
|
16374
|
+
if (input.signal.aborted) throw input.signal.reason;
|
|
16375
|
+
const socket = new WebSocket(dshRemoteRealtimeEndpoint(input.realtimeBaseUrl), {
|
|
16376
|
+
headers: { Authorization: `Bearer ${accessToken}` },
|
|
16377
|
+
handshakeTimeout: 1e4,
|
|
16378
|
+
maxPayload: DSH_REMOTE_MAX_FRAME_BYTES,
|
|
16379
|
+
perMessageDeflate: false,
|
|
16380
|
+
followRedirects: false
|
|
16381
|
+
});
|
|
16382
|
+
const closeOnAbort = () => {
|
|
16383
|
+
socket.close(1e3, "aborted");
|
|
16384
|
+
};
|
|
16385
|
+
input.signal.addEventListener("abort", closeOnAbort, { once: true });
|
|
16386
|
+
socket.addEventListener("close", () => {
|
|
16387
|
+
input.signal.removeEventListener("abort", closeOnAbort);
|
|
16388
|
+
}, { once: true });
|
|
16389
|
+
return socket;
|
|
16390
|
+
}
|
|
16391
|
+
//#endregion
|
|
16392
|
+
//#region src/dsh-remote/transport-fragment.ts
|
|
16393
|
+
const DIRECT_PLAINTEXT_BYTES = 32 * 1024;
|
|
16394
|
+
const FRAGMENT_CHUNK_BYTES = 18 * 1024;
|
|
16395
|
+
const MAX_FRAGMENT_COUNT = 256;
|
|
16396
|
+
function sha256(value) {
|
|
16397
|
+
return createHash("sha256").update(value).digest("base64url");
|
|
16398
|
+
}
|
|
16399
|
+
/**
|
|
16400
|
+
* Splits a large typed payload into independently bounded Realtime events.
|
|
16401
|
+
*/
|
|
16402
|
+
function dshRemoteOutboundPayloads(envelope, transferSeed) {
|
|
16403
|
+
const encoded = Buffer.from(JSON.stringify(envelope));
|
|
16404
|
+
if (encoded.length <= DIRECT_PLAINTEXT_BYTES) return [{ value: envelope }];
|
|
16405
|
+
if (encoded.length > 524288) throw new DshRemoteError("CAPABILITY_UNSUPPORTED", "完整 DSH 事件超过 512KiB 远控对象上限", false, {
|
|
16406
|
+
logicalTooLarge: true,
|
|
16407
|
+
payloadBytes: encoded.length
|
|
16408
|
+
});
|
|
16409
|
+
const fragmentCount = Math.ceil(encoded.length / FRAGMENT_CHUNK_BYTES);
|
|
16410
|
+
if (fragmentCount > MAX_FRAGMENT_COUNT) throw new DshRemoteError("CAPABILITY_UNSUPPORTED", "完整 DSH 事件需要过多远控分片", false, {
|
|
16411
|
+
logicalTooLarge: true,
|
|
16412
|
+
fragmentCount
|
|
16413
|
+
});
|
|
16414
|
+
const digest = sha256(encoded);
|
|
16415
|
+
const transferRef = `fragment_${createHash("sha256").update(`${transferSeed}\n${digest}`).digest("base64url").slice(0, 40)}`;
|
|
16416
|
+
return Array.from({ length: fragmentCount }, (_, fragmentIndex) => ({
|
|
16417
|
+
fragmentIndex,
|
|
16418
|
+
commandId: `fragment_${createHash("sha256").update(`${transferRef}\n${fragmentIndex}`).digest("base64url").slice(0, 40)}_${fragmentIndex}`,
|
|
16419
|
+
value: {
|
|
16420
|
+
protocol: "dsh.remote-fragment",
|
|
16421
|
+
protocol_major: 1,
|
|
16422
|
+
kind: "fragment",
|
|
16423
|
+
transfer_ref: transferRef,
|
|
16424
|
+
fragment_index: fragmentIndex,
|
|
16425
|
+
fragment_count: fragmentCount,
|
|
16426
|
+
payload_bytes: encoded.length,
|
|
16427
|
+
payload_sha256: digest,
|
|
16428
|
+
chunk: encoded.subarray(fragmentIndex * FRAGMENT_CHUNK_BYTES, Math.min(encoded.length, (fragmentIndex + 1) * FRAGMENT_CHUNK_BYTES)).toString("base64url")
|
|
16429
|
+
}
|
|
16430
|
+
}));
|
|
16431
|
+
}
|
|
16432
|
+
//#endregion
|
|
16433
|
+
//#region src/dsh-remote/channel-manager.ts
|
|
16434
|
+
/**
|
|
16435
|
+
* Owns the single account-scoped Runtime channel. The Host subscribes with
|
|
16436
|
+
* generation 0 before registration, then activates the exact positive lease
|
|
16437
|
+
* returned by Realtime. No controller identity or pairing state exists here.
|
|
16438
|
+
*/
|
|
16439
|
+
var DshRemoteHostChannelManager = class {
|
|
16440
|
+
options;
|
|
16441
|
+
controller = new AbortController();
|
|
16442
|
+
unsubscribe;
|
|
16443
|
+
target;
|
|
16444
|
+
lastTransportSequence = 0;
|
|
16445
|
+
persistedTransportSequence = 0;
|
|
16446
|
+
outboundTail = Promise.resolve();
|
|
16447
|
+
pendingEvents = [];
|
|
16448
|
+
closed = false;
|
|
16449
|
+
constructor(options) {
|
|
16450
|
+
this.options = options;
|
|
16451
|
+
this.target = {
|
|
16452
|
+
runtimeRef: options.runtimeRef,
|
|
16453
|
+
hostProfileRef: options.profileRef,
|
|
16454
|
+
hostClientRef: options.hostClientRef,
|
|
16455
|
+
hostLeaseGeneration: 0
|
|
16456
|
+
};
|
|
16457
|
+
}
|
|
16458
|
+
async prepare() {
|
|
16459
|
+
if (this.closed || this.unsubscribe !== void 0) throw new DshRemoteError("REMOTE_REQUEST_INVALID", "Runtime channel 已准备");
|
|
16460
|
+
this.lastTransportSequence = await this.options.secretBroker.runtimeCursor({
|
|
16461
|
+
accountId: this.options.accountId,
|
|
16462
|
+
runtimeRef: this.options.runtimeRef,
|
|
16463
|
+
channelRef: this.options.runtimeRef
|
|
16464
|
+
});
|
|
16465
|
+
this.persistedTransportSequence = this.lastTransportSequence;
|
|
16466
|
+
const subscribe = async (afterSequence) => await this.options.realtime.subscribe({
|
|
16467
|
+
target: this.target,
|
|
16468
|
+
...afterSequence === void 0 ? {} : { afterSequence },
|
|
16469
|
+
onEvent: (payload, metadata) => {
|
|
16470
|
+
this.handle(payload, metadata).catch((error) => {
|
|
16471
|
+
this.options.onFatal(error);
|
|
16472
|
+
});
|
|
16473
|
+
},
|
|
16474
|
+
signal: this.controller.signal
|
|
16475
|
+
});
|
|
16476
|
+
try {
|
|
16477
|
+
this.unsubscribe = await subscribe(this.lastTransportSequence);
|
|
16478
|
+
} catch (error) {
|
|
16479
|
+
if (!(error instanceof DshRemoteError) || error.code !== "REPLAY_GAP") throw error;
|
|
16480
|
+
this.unsubscribe = await subscribe();
|
|
16481
|
+
}
|
|
16482
|
+
}
|
|
16483
|
+
async activate(serviceLeaseGeneration) {
|
|
16484
|
+
if (this.closed || this.unsubscribe === void 0 || !Number.isSafeInteger(serviceLeaseGeneration) || serviceLeaseGeneration <= 0) throw new DshRemoteError("HOST_CHANNEL_NOT_READY", "Runtime channel 尚未完成 Host 注册", true);
|
|
16485
|
+
this.target = {
|
|
16486
|
+
...this.target,
|
|
16487
|
+
hostLeaseGeneration: serviceLeaseGeneration
|
|
16488
|
+
};
|
|
16489
|
+
for (const event of this.pendingEvents.splice(0)) await this.handle(event.payload, event.metadata);
|
|
16490
|
+
}
|
|
16491
|
+
status() {
|
|
16492
|
+
return {
|
|
16493
|
+
runtimeRef: this.options.runtimeRef,
|
|
16494
|
+
serviceLeaseGeneration: this.target.hostLeaseGeneration,
|
|
16495
|
+
ready: !this.closed && this.unsubscribe !== void 0 && this.target.hostLeaseGeneration > 0
|
|
16496
|
+
};
|
|
16497
|
+
}
|
|
16498
|
+
async close() {
|
|
16499
|
+
if (this.closed) return;
|
|
16500
|
+
this.closed = true;
|
|
16501
|
+
if (this.lastTransportSequence > this.persistedTransportSequence) await this.persistCursor().catch(() => void 0);
|
|
16502
|
+
this.unsubscribe?.();
|
|
16503
|
+
this.unsubscribe = void 0;
|
|
16504
|
+
this.controller.abort();
|
|
16505
|
+
}
|
|
16506
|
+
async publishProjectionEvent(envelope, commandId) {
|
|
16507
|
+
if (!this.status().ready) return;
|
|
16508
|
+
await this.publishPayload(envelope, commandId, "event");
|
|
16509
|
+
}
|
|
16510
|
+
async handle(payload, metadata) {
|
|
16511
|
+
if (this.closed) return;
|
|
16512
|
+
if (metadata.transportSequence !== void 0 && metadata.transportSequence > this.lastTransportSequence) {
|
|
16513
|
+
this.lastTransportSequence = metadata.transportSequence;
|
|
16514
|
+
if (this.lastTransportSequence - this.persistedTransportSequence >= 16) {
|
|
16515
|
+
this.persistedTransportSequence = this.lastTransportSequence;
|
|
16516
|
+
this.persistCursor().catch((error) => {
|
|
16517
|
+
this.options.onFatal(error);
|
|
16518
|
+
});
|
|
16519
|
+
}
|
|
16520
|
+
}
|
|
16521
|
+
if (metadata.senderRole === "host") return;
|
|
16522
|
+
if (this.target.hostLeaseGeneration === 0) {
|
|
16523
|
+
if (this.pendingEvents.length >= 8) throw new DshRemoteError("HOST_CHANNEL_NOT_READY", "Host 注册期间收到过多并发请求", true);
|
|
16524
|
+
this.pendingEvents.push({
|
|
16525
|
+
payload,
|
|
16526
|
+
metadata
|
|
16527
|
+
});
|
|
16528
|
+
return;
|
|
16529
|
+
}
|
|
16530
|
+
if (metadata.senderRole !== "controller" || metadata.runtimeRef !== this.options.runtimeRef || metadata.targetHostLeaseGeneration !== this.target.hostLeaseGeneration) throw new DshRemoteError("HOST_GENERATION_STALE", "远控请求不属于当前 Runtime Host lease", true);
|
|
16531
|
+
const result = await this.options.dispatch(payload, {
|
|
16532
|
+
serviceLeaseGeneration: this.target.hostLeaseGeneration,
|
|
16533
|
+
metadata
|
|
16534
|
+
});
|
|
16535
|
+
const responseCommandId = `response_${createHash("sha256").update(result.request_ref).digest("base64url")}`;
|
|
16536
|
+
await this.publishPayload(result, responseCommandId, "response");
|
|
16537
|
+
}
|
|
16538
|
+
async publishPayload(envelope, commandId, direction) {
|
|
16539
|
+
const publish = async () => {
|
|
16540
|
+
const frames = dshRemoteOutboundPayloads(envelope, commandId);
|
|
16541
|
+
for (const frame of frames) {
|
|
16542
|
+
if (!this.status().ready) return;
|
|
16543
|
+
await this.options.realtime.publish({
|
|
16544
|
+
target: this.target,
|
|
16545
|
+
commandId: frame.commandId ?? commandId,
|
|
16546
|
+
direction,
|
|
16547
|
+
payload: frame.value,
|
|
16548
|
+
signal: this.controller.signal
|
|
16549
|
+
});
|
|
16550
|
+
}
|
|
16551
|
+
};
|
|
16552
|
+
const result = this.outboundTail.then(publish);
|
|
16553
|
+
this.outboundTail = result.catch(() => void 0);
|
|
16554
|
+
await result;
|
|
16555
|
+
}
|
|
16556
|
+
async persistCursor() {
|
|
16557
|
+
await this.options.secretBroker.putRuntimeCursor({
|
|
16558
|
+
accountId: this.options.accountId,
|
|
16559
|
+
runtimeRef: this.options.runtimeRef,
|
|
16560
|
+
channelRef: this.options.runtimeRef,
|
|
16561
|
+
lastTransportSequence: this.lastTransportSequence
|
|
16562
|
+
});
|
|
16563
|
+
}
|
|
16564
|
+
};
|
|
16565
|
+
//#endregion
|
|
16566
|
+
//#region src/dsh-remote/protocol-v1.ts
|
|
16567
|
+
const OPERATIONS = /* @__PURE__ */ new Set([
|
|
16568
|
+
"workspace.list",
|
|
16569
|
+
"session.create",
|
|
16570
|
+
"session.list",
|
|
16571
|
+
"session.history",
|
|
16572
|
+
"session.prompt",
|
|
16573
|
+
"session.cancel",
|
|
16574
|
+
"interaction.question.respond",
|
|
16575
|
+
"interaction.approval.respond",
|
|
16576
|
+
"snapshot.get",
|
|
16577
|
+
"capabilities.get"
|
|
16578
|
+
]);
|
|
16579
|
+
const REQUEST_KEYS = /* @__PURE__ */ new Set([
|
|
16580
|
+
"protocol",
|
|
16581
|
+
"protocol_major",
|
|
16582
|
+
"kind",
|
|
16583
|
+
"request_ref",
|
|
16584
|
+
"host_generation",
|
|
16585
|
+
"issued_at",
|
|
16586
|
+
"execute_before",
|
|
16587
|
+
"operation",
|
|
16588
|
+
"body"
|
|
16589
|
+
]);
|
|
16590
|
+
function object(value) {
|
|
16591
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) throw new DshRemoteError("REMOTE_REQUEST_INVALID", "远控请求必须为对象");
|
|
16592
|
+
return value;
|
|
16593
|
+
}
|
|
16594
|
+
function ref(value, name) {
|
|
16595
|
+
if (typeof value !== "string" || value.length < 8 || value.length > 128 || !/^[A-Za-z0-9._:-]+$/.test(value)) throw new DshRemoteError("REMOTE_REQUEST_INVALID", `${name} 无效`);
|
|
16596
|
+
return value;
|
|
16597
|
+
}
|
|
16598
|
+
function positiveInteger$1(value, name) {
|
|
16599
|
+
if (typeof value !== "number" || !Number.isSafeInteger(value) || value <= 0) throw new DshRemoteError("REMOTE_REQUEST_INVALID", `${name} 无效`);
|
|
16600
|
+
return value;
|
|
16601
|
+
}
|
|
16602
|
+
function assertOnly(source, keys) {
|
|
16603
|
+
const allowed = new Set(keys);
|
|
16604
|
+
if (Object.keys(source).some((key) => !allowed.has(key))) throw new DshRemoteError("REMOTE_REQUEST_INVALID", "远控操作 body 包含未定义字段");
|
|
16605
|
+
}
|
|
16606
|
+
function bodyRef(source, key, required = true) {
|
|
16607
|
+
const value = source[key];
|
|
16608
|
+
if (value === void 0 && !required) return;
|
|
16609
|
+
if (typeof value !== "string" || value.length < 1 || value.length > 256) throw new DshRemoteError("REMOTE_REQUEST_INVALID", `${key} 无效`);
|
|
16610
|
+
}
|
|
16611
|
+
function pageFields(source) {
|
|
16612
|
+
if (source.cursor !== void 0 && (typeof source.cursor !== "string" || source.cursor.length < 1 || source.cursor.length > 512)) throw new DshRemoteError("REMOTE_REQUEST_INVALID", "cursor 无效");
|
|
16613
|
+
if (source.limit !== void 0 && (!Number.isSafeInteger(source.limit) || Number(source.limit) < 1 || Number(source.limit) > 100)) throw new DshRemoteError("REMOTE_REQUEST_INVALID", "limit 无效");
|
|
16614
|
+
}
|
|
16615
|
+
function validateOperationBody(operation, source) {
|
|
16616
|
+
switch (operation) {
|
|
16617
|
+
case "capabilities.get":
|
|
16618
|
+
assertOnly(source, []);
|
|
16619
|
+
return;
|
|
16620
|
+
case "snapshot.get":
|
|
16621
|
+
case "workspace.list":
|
|
16622
|
+
assertOnly(source, ["cursor", "limit"]);
|
|
16623
|
+
pageFields(source);
|
|
16624
|
+
return;
|
|
16625
|
+
case "session.list":
|
|
16626
|
+
assertOnly(source, [
|
|
16627
|
+
"workspace_ref",
|
|
16628
|
+
"cursor",
|
|
16629
|
+
"limit"
|
|
16630
|
+
]);
|
|
16631
|
+
bodyRef(source, "workspace_ref", false);
|
|
16632
|
+
pageFields(source);
|
|
16633
|
+
return;
|
|
16634
|
+
case "session.create":
|
|
16635
|
+
assertOnly(source, ["workspace_ref"]);
|
|
16636
|
+
bodyRef(source, "workspace_ref");
|
|
16637
|
+
return;
|
|
16638
|
+
case "session.history":
|
|
16639
|
+
assertOnly(source, [
|
|
16640
|
+
"session_ref",
|
|
16641
|
+
"before_seq",
|
|
16642
|
+
"limit"
|
|
16643
|
+
]);
|
|
16644
|
+
bodyRef(source, "session_ref");
|
|
16645
|
+
if (source.before_seq !== void 0 && (!Number.isSafeInteger(source.before_seq) || Number(source.before_seq) < 1)) throw new DshRemoteError("REMOTE_REQUEST_INVALID", "before_seq 无效");
|
|
16646
|
+
pageFields(source);
|
|
16647
|
+
return;
|
|
16648
|
+
case "session.prompt": {
|
|
16649
|
+
assertOnly(source, [
|
|
16650
|
+
"session_ref",
|
|
16651
|
+
"mode",
|
|
16652
|
+
"content"
|
|
16653
|
+
]);
|
|
16654
|
+
bodyRef(source, "session_ref");
|
|
16655
|
+
if (source.mode !== "queue" && source.mode !== "steer") throw new DshRemoteError("REMOTE_REQUEST_INVALID", "mode 无效");
|
|
16656
|
+
const content = object(source.content);
|
|
16657
|
+
if (content.type !== "text") throw new DshRemoteError("CAPABILITY_UNSUPPORTED", "首版远控只支持有界普通文本");
|
|
16658
|
+
assertOnly(content, ["type", "text"]);
|
|
16659
|
+
if (typeof content.text !== "string" || content.text.length < 1 || content.text.length > 16e3) throw new DshRemoteError("CAPABILITY_UNSUPPORTED", "首版远控只支持有界普通文本");
|
|
16660
|
+
return;
|
|
16661
|
+
}
|
|
16662
|
+
case "session.cancel":
|
|
16663
|
+
assertOnly(source, ["session_ref"]);
|
|
16664
|
+
bodyRef(source, "session_ref");
|
|
16665
|
+
return;
|
|
16666
|
+
case "interaction.question.respond":
|
|
16667
|
+
assertOnly(source, [
|
|
16668
|
+
"session_ref",
|
|
16669
|
+
"interaction_rpc_ref",
|
|
16670
|
+
"answer"
|
|
16671
|
+
]);
|
|
16672
|
+
bodyRef(source, "session_ref");
|
|
16673
|
+
bodyRef(source, "interaction_rpc_ref");
|
|
16674
|
+
if (!Object.hasOwn(source, "answer")) throw new DshRemoteError("REMOTE_REQUEST_INVALID", "answer 缺失");
|
|
16675
|
+
return;
|
|
16676
|
+
case "interaction.approval.respond":
|
|
16677
|
+
assertOnly(source, [
|
|
16678
|
+
"session_ref",
|
|
16679
|
+
"interaction_rpc_ref",
|
|
16680
|
+
"approval_id",
|
|
16681
|
+
"outcome"
|
|
16682
|
+
]);
|
|
16683
|
+
bodyRef(source, "session_ref");
|
|
16684
|
+
bodyRef(source, "interaction_rpc_ref");
|
|
16685
|
+
bodyRef(source, "approval_id");
|
|
16686
|
+
if (source.outcome !== "allowed-once" && source.outcome !== "rejected") throw new DshRemoteError("REMOTE_REQUEST_INVALID", "outcome 无效");
|
|
16687
|
+
}
|
|
16688
|
+
}
|
|
16689
|
+
function parseDshRemoteRequest(value, options) {
|
|
16690
|
+
if (Buffer.from(JSON.stringify(value)).length > 61440) throw new DshRemoteError("REMOTE_REQUEST_INVALID", "远控请求超过 60KiB");
|
|
16691
|
+
const source = object(value);
|
|
16692
|
+
if (Object.keys(source).some((key) => !REQUEST_KEYS.has(key))) throw new DshRemoteError("REMOTE_REQUEST_INVALID", "远控请求包含未定义字段");
|
|
16693
|
+
if (source.protocol !== "dsh.remote" || source.protocol_major !== 1) throw new DshRemoteError("REMOTE_PROTOCOL_UNSUPPORTED", "远控协议版本不受支持");
|
|
16694
|
+
if (source.kind !== "request") throw new DshRemoteError("REMOTE_REQUEST_INVALID", "远控消息类型无效");
|
|
16695
|
+
const hostGeneration = positiveInteger$1(source.host_generation, "host_generation");
|
|
16696
|
+
if (hostGeneration !== options.expectedHostGeneration) throw new DshRemoteError("HOST_GENERATION_STALE", "请求指向的 Host 实例已经失效", true);
|
|
16697
|
+
const issuedAt = positiveInteger$1(source.issued_at, "issued_at");
|
|
16698
|
+
const executeBefore = positiveInteger$1(source.execute_before, "execute_before");
|
|
16699
|
+
const now = options.nowMillis ?? Date.now();
|
|
16700
|
+
if (executeBefore < now) throw new DshRemoteError("COMMAND_EXPIRED", "远控命令已经过期");
|
|
16701
|
+
if (issuedAt > now + 3e4 || executeBefore > now + 5 * 6e4 || executeBefore < issuedAt) throw new DshRemoteError("REMOTE_REQUEST_INVALID", "远控命令时间窗口无效");
|
|
16702
|
+
if (typeof source.operation !== "string" || !OPERATIONS.has(source.operation)) throw new DshRemoteError("CAPABILITY_UNSUPPORTED", "远控操作不受支持");
|
|
16703
|
+
const body = object(source.body);
|
|
16704
|
+
validateOperationBody(source.operation, body);
|
|
16705
|
+
return {
|
|
16706
|
+
protocol: DSH_REMOTE_PROTOCOL,
|
|
16707
|
+
protocol_major: 1,
|
|
16708
|
+
kind: "request",
|
|
16709
|
+
request_ref: ref(source.request_ref, "request_ref"),
|
|
16710
|
+
host_generation: hostGeneration,
|
|
16711
|
+
issued_at: issuedAt,
|
|
16712
|
+
execute_before: executeBefore,
|
|
16713
|
+
operation: source.operation,
|
|
16714
|
+
body
|
|
16715
|
+
};
|
|
16716
|
+
}
|
|
16717
|
+
//#endregion
|
|
16718
|
+
//#region src/dsh-remote/host.ts
|
|
16719
|
+
function stringBody(body, key, max = 256) {
|
|
16720
|
+
const value = body[key];
|
|
16721
|
+
if (typeof value !== "string" || value.trim() === "" || value.length > max) throw new DshRemoteError("REMOTE_REQUEST_INVALID", `${key} 无效`);
|
|
16722
|
+
return value.trim();
|
|
16723
|
+
}
|
|
16724
|
+
function optionalString(body, key) {
|
|
16725
|
+
return typeof body[key] === "string" && body[key] !== "" ? body[key] : void 0;
|
|
16726
|
+
}
|
|
16727
|
+
function optionalPositive(body, key) {
|
|
16728
|
+
const value = body[key];
|
|
16729
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value > 0 ? value : void 0;
|
|
16730
|
+
}
|
|
16731
|
+
function requiredCapabilities(operation) {
|
|
16732
|
+
switch (operation) {
|
|
16733
|
+
case "capabilities.get": return [];
|
|
16734
|
+
case "snapshot.get": return ["workspace.list", "session.list"];
|
|
16735
|
+
case "workspace.list": return ["workspace.list"];
|
|
16736
|
+
case "session.list": return ["session.list"];
|
|
16737
|
+
case "session.create": return ["session.create"];
|
|
16738
|
+
case "session.history": return ["session.history"];
|
|
16739
|
+
case "session.prompt": return ["session.prompt"];
|
|
16740
|
+
case "session.cancel": return ["session.cancel"];
|
|
16741
|
+
case "interaction.question.respond": return ["interaction.question.respond"];
|
|
16742
|
+
case "interaction.approval.respond": return ["interaction.approval.respond"];
|
|
16743
|
+
}
|
|
16744
|
+
}
|
|
16745
|
+
function response(request, status, now, payload) {
|
|
16746
|
+
const result = {
|
|
16747
|
+
protocol: DSH_REMOTE_PROTOCOL,
|
|
16748
|
+
protocol_major: 1,
|
|
16749
|
+
kind: "response",
|
|
16750
|
+
request_ref: request.request_ref,
|
|
16751
|
+
status,
|
|
16752
|
+
host_generation: request.host_generation,
|
|
16753
|
+
issued_at: now,
|
|
16754
|
+
operation: request.operation,
|
|
16755
|
+
body: {}
|
|
16756
|
+
};
|
|
16757
|
+
if (payload.result !== void 0) result.result = payload.result;
|
|
16758
|
+
if (payload.error !== void 0) result.error = payload.error;
|
|
16759
|
+
return result;
|
|
16760
|
+
}
|
|
16761
|
+
function resultFromLedger(entry) {
|
|
16762
|
+
const result = entry.payload.result;
|
|
16763
|
+
if (result !== null && typeof result === "object" && "rejected" in result) {
|
|
16764
|
+
const rejected = result.rejected;
|
|
16765
|
+
if (rejected !== null && typeof rejected === "object") {
|
|
16766
|
+
const source = rejected;
|
|
16767
|
+
throw new DshRemoteError(typeof source.code === "string" ? source.code : "REMOTE_TRANSPORT_FAILED", typeof source.message === "string" ? source.message : "DSH 已拒绝该远控命令", source.retryable === true);
|
|
16768
|
+
}
|
|
16769
|
+
}
|
|
16770
|
+
return result !== null && typeof result === "object" && "value" in result ? result.value : result;
|
|
16771
|
+
}
|
|
16772
|
+
/**
|
|
16773
|
+
* Account-login Host facade. Backend owns durable discovery/projections,
|
|
16774
|
+
* Realtime owns the short-lived Host lease, and DSH remains session truth.
|
|
16775
|
+
*/
|
|
16776
|
+
var ArkmeRemoteRealtimeHost = class {
|
|
16777
|
+
options;
|
|
16778
|
+
now;
|
|
16779
|
+
listeners = /* @__PURE__ */ new Set();
|
|
16780
|
+
runtime;
|
|
16781
|
+
accountId;
|
|
16782
|
+
clientId = 0;
|
|
16783
|
+
ledger;
|
|
16784
|
+
started = false;
|
|
16785
|
+
connected = false;
|
|
16786
|
+
serviceLeaseGeneration = 0;
|
|
16787
|
+
revision = 0;
|
|
16788
|
+
stopEvents;
|
|
16789
|
+
stopProjectionEvents;
|
|
16790
|
+
channelManager;
|
|
16791
|
+
stopTransportDisconnect;
|
|
16792
|
+
reconnectTimer;
|
|
16793
|
+
reconnecting = false;
|
|
16794
|
+
sessionTimer;
|
|
16795
|
+
connectionError;
|
|
16796
|
+
lastProjectionSyncAttemptMillis = 0;
|
|
16797
|
+
projectionVersion = 0;
|
|
16798
|
+
constructor(options) {
|
|
16799
|
+
this.options = options;
|
|
16800
|
+
this.now = options.now ?? Date.now;
|
|
16801
|
+
}
|
|
16802
|
+
async start() {
|
|
16803
|
+
if (this.started) return;
|
|
16804
|
+
this.started = true;
|
|
16805
|
+
if (!this.options.featureEnabled) {
|
|
16806
|
+
this.bump();
|
|
16807
|
+
return;
|
|
16808
|
+
}
|
|
16809
|
+
this.stopTransportDisconnect = this.options.realtime.subscribeDisconnect((error) => {
|
|
16810
|
+
this.handleTransportDisconnect(error);
|
|
16811
|
+
});
|
|
16812
|
+
await this.syncSessionSafely();
|
|
16813
|
+
this.scheduleSessionSync();
|
|
16814
|
+
}
|
|
16815
|
+
async stop() {
|
|
16816
|
+
if (!this.started) return;
|
|
16817
|
+
this.started = false;
|
|
16818
|
+
this.stopApiProxyEvents();
|
|
16819
|
+
this.stopTransportDisconnect?.();
|
|
16820
|
+
this.stopTransportDisconnect = void 0;
|
|
16821
|
+
if (this.sessionTimer !== void 0) clearTimeout(this.sessionTimer);
|
|
16822
|
+
this.sessionTimer = void 0;
|
|
16823
|
+
if (this.reconnectTimer !== void 0) clearTimeout(this.reconnectTimer);
|
|
16824
|
+
this.reconnectTimer = void 0;
|
|
16825
|
+
await this.deactivateAccount();
|
|
16826
|
+
this.bump();
|
|
16827
|
+
}
|
|
16828
|
+
getStatus() {
|
|
16829
|
+
const available = this.options.featureEnabled && this.accountId !== void 0 && this.options.transportAvailable !== false && this.options.apiProxy.capabilities().length > 0;
|
|
16830
|
+
return {
|
|
16831
|
+
contractVersion: 1,
|
|
16832
|
+
available,
|
|
16833
|
+
enabled: this.options.featureEnabled && this.accountId !== void 0,
|
|
16834
|
+
connected: this.connected,
|
|
16835
|
+
hostGeneration: this.runtime?.hostGeneration ?? 0,
|
|
16836
|
+
capabilities: this.runtime?.capabilities ?? this.options.apiProxy.capabilities(),
|
|
16837
|
+
revision: this.revision,
|
|
16838
|
+
...this.accountId === void 0 ? {} : { accountId: this.accountId },
|
|
16839
|
+
...this.runtime?.desktopRef === void 0 ? {} : { desktopRef: this.runtime.desktopRef },
|
|
16840
|
+
...this.runtime === void 0 ? {} : { runtimeRef: this.runtime.runtimeRef },
|
|
16841
|
+
...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 能力" : "远控运行依赖尚未就绪" }
|
|
16842
|
+
};
|
|
16843
|
+
}
|
|
16844
|
+
async renameDesktop(displayName) {
|
|
16845
|
+
this.requireReady();
|
|
16846
|
+
const normalized = displayName.trim();
|
|
16847
|
+
await this.options.runtimeStore.renameDesktop(this.accountId, normalized);
|
|
16848
|
+
if (this.runtime?.desktopRef !== void 0) await this.options.controlPlane.registerDesktop({
|
|
16849
|
+
display_name: normalized,
|
|
16850
|
+
platform: this.options.platform ?? process.platform
|
|
16851
|
+
});
|
|
16852
|
+
this.bump();
|
|
16853
|
+
return this.getStatus();
|
|
16854
|
+
}
|
|
16855
|
+
subscribe(listener) {
|
|
16856
|
+
this.listeners.add(listener);
|
|
16857
|
+
listener(this.getStatus());
|
|
16858
|
+
return () => {
|
|
16859
|
+
this.listeners.delete(listener);
|
|
16860
|
+
};
|
|
16861
|
+
}
|
|
16862
|
+
/** Entry used only by the authenticated Realtime channel bridge. */
|
|
16863
|
+
async dispatchAuthorizedRequest(value, context) {
|
|
16864
|
+
this.requireConnected();
|
|
16865
|
+
if (context.metadata.senderRole !== "controller" || context.metadata.runtimeRef !== this.runtime.runtimeRef) throw new DshRemoteError("REMOTE_REQUEST_INVALID", "远控发送方上下文无效");
|
|
16866
|
+
if (context.serviceLeaseGeneration !== this.serviceLeaseGeneration || context.metadata.targetHostLeaseGeneration !== this.serviceLeaseGeneration) throw new DshRemoteError("HOST_GENERATION_STALE", "远控请求属于旧 Host lease", true);
|
|
16867
|
+
if (this.now() - context.metadata.acceptedAtMillis > 3e4) throw new DshRemoteError("COMMAND_EXPIRED", "远控请求投递延迟过长");
|
|
16868
|
+
const request = parseDshRemoteRequest(value, {
|
|
16869
|
+
expectedHostGeneration: this.runtime.hostGeneration,
|
|
16870
|
+
nowMillis: this.now()
|
|
16871
|
+
});
|
|
16872
|
+
try {
|
|
16873
|
+
const capabilities = new Set(this.options.apiProxy.capabilities());
|
|
16874
|
+
if (requiredCapabilities(request.operation).some((capability) => !capabilities.has(capability))) throw new DshRemoteError("CAPABILITY_UNSUPPORTED", "当前 DSH Runtime 不支持该操作");
|
|
16875
|
+
const result = await this.dispatch(request);
|
|
16876
|
+
return response(request, result.duplicate ? "duplicate" : "completed", this.now(), { result: result.value });
|
|
16877
|
+
} catch (error) {
|
|
16878
|
+
const remote = asDshRemoteError(error);
|
|
16879
|
+
return response(request, "rejected", this.now(), { error: {
|
|
16880
|
+
code: remote.code,
|
|
16881
|
+
message: remote.message.slice(0, 256),
|
|
16882
|
+
retryable: remote.retryable,
|
|
16883
|
+
trace_ref: randomUUID()
|
|
16884
|
+
} });
|
|
16885
|
+
}
|
|
16886
|
+
}
|
|
16887
|
+
async syncSessionSafely() {
|
|
16888
|
+
try {
|
|
16889
|
+
await this.syncSession();
|
|
16890
|
+
} catch (error) {
|
|
16891
|
+
this.connectionError = asDshRemoteError(error);
|
|
16892
|
+
this.bump();
|
|
16893
|
+
}
|
|
16894
|
+
}
|
|
16895
|
+
async syncSession() {
|
|
16896
|
+
if (!this.started || !this.options.featureEnabled) return;
|
|
16897
|
+
const session = await this.options.readSession();
|
|
16898
|
+
if (session === void 0) {
|
|
16899
|
+
if (this.accountId !== void 0) await this.deactivateAccount();
|
|
16900
|
+
this.bump();
|
|
16901
|
+
return;
|
|
16902
|
+
}
|
|
16903
|
+
const accountId = String(session.userId);
|
|
16904
|
+
if (this.accountId === accountId && this.clientId === session.clientId && this.runtime !== void 0) {
|
|
16905
|
+
await this.ensureAutomaticConnection();
|
|
16906
|
+
return;
|
|
16907
|
+
}
|
|
16908
|
+
if (this.accountId !== void 0) await this.deactivateAccount();
|
|
16909
|
+
this.clientId = session.clientId;
|
|
16910
|
+
this.accountId = accountId;
|
|
16911
|
+
this.runtime = await this.options.runtimeStore.activateRuntime({
|
|
16912
|
+
accountId,
|
|
16913
|
+
profileRef: this.options.profileRef,
|
|
16914
|
+
capabilities: this.options.apiProxy.capabilities(),
|
|
16915
|
+
nowMillis: this.now()
|
|
16916
|
+
});
|
|
16917
|
+
this.ledger = await this.options.ledgerForAccount(accountId, await this.options.secretBroker.ledgerKey(accountId));
|
|
16918
|
+
await this.reconcileUnsettled();
|
|
16919
|
+
this.ledger.cleanup({
|
|
16920
|
+
retentionMillis: 10080 * 6e4,
|
|
16921
|
+
maxCommands: 1e4
|
|
16922
|
+
});
|
|
16923
|
+
await this.ensureAutomaticConnection();
|
|
16924
|
+
this.bump();
|
|
16925
|
+
}
|
|
16926
|
+
async ensureAutomaticConnection() {
|
|
16927
|
+
if (this.accountId === void 0 || this.runtime === void 0) return;
|
|
16928
|
+
if (this.connected) {
|
|
16929
|
+
await this.syncProjectionSnapshotSafely();
|
|
16930
|
+
return;
|
|
16931
|
+
}
|
|
16932
|
+
if (this.options.transportAvailable === false || this.options.apiProxy.capabilities().length === 0) {
|
|
16933
|
+
this.connectionError = void 0;
|
|
16934
|
+
this.bump();
|
|
16935
|
+
return;
|
|
16936
|
+
}
|
|
16937
|
+
this.startApiProxyEvents();
|
|
16938
|
+
try {
|
|
16939
|
+
await this.connectHost();
|
|
16940
|
+
} catch (error) {
|
|
16941
|
+
this.handleTransportDisconnect(error);
|
|
16942
|
+
throw error;
|
|
16943
|
+
}
|
|
16944
|
+
}
|
|
16945
|
+
scheduleSessionSync() {
|
|
16946
|
+
if (!this.started || !this.options.featureEnabled || this.sessionTimer !== void 0) return;
|
|
16947
|
+
this.sessionTimer = setTimeout(() => {
|
|
16948
|
+
this.sessionTimer = void 0;
|
|
16949
|
+
this.syncSessionSafely().finally(() => {
|
|
16950
|
+
this.scheduleSessionSync();
|
|
16951
|
+
});
|
|
16952
|
+
}, 2e3);
|
|
16953
|
+
this.sessionTimer.unref();
|
|
16954
|
+
}
|
|
16955
|
+
async deactivateAccount() {
|
|
16956
|
+
this.stopApiProxyEvents();
|
|
16957
|
+
let unregistered = false;
|
|
16958
|
+
try {
|
|
16959
|
+
if (this.connected) {
|
|
16960
|
+
await this.options.realtime.unregisterHost();
|
|
16961
|
+
unregistered = true;
|
|
16962
|
+
}
|
|
16963
|
+
} catch {}
|
|
16964
|
+
if (!unregistered) await this.options.realtime.disconnect();
|
|
16965
|
+
await this.channelManager?.close();
|
|
16966
|
+
this.channelManager = void 0;
|
|
16967
|
+
if (unregistered) await this.options.realtime.disconnect();
|
|
16968
|
+
this.connected = false;
|
|
16969
|
+
this.serviceLeaseGeneration = 0;
|
|
16970
|
+
this.ledger?.close();
|
|
16971
|
+
this.ledger = void 0;
|
|
16972
|
+
this.runtime = void 0;
|
|
16973
|
+
this.accountId = void 0;
|
|
16974
|
+
this.clientId = 0;
|
|
16975
|
+
this.connectionError = void 0;
|
|
16976
|
+
this.lastProjectionSyncAttemptMillis = 0;
|
|
16977
|
+
this.projectionVersion = 0;
|
|
16978
|
+
}
|
|
16979
|
+
startApiProxyEvents() {
|
|
16980
|
+
if (this.stopProjectionEvents === void 0) this.stopProjectionEvents = this.options.apiProxy.subscribeProjectionEvents((event) => {
|
|
16981
|
+
this.publishProjectionEvent(event).catch((error) => {
|
|
16982
|
+
this.connectionError = asDshRemoteError(error);
|
|
16983
|
+
this.bump();
|
|
16984
|
+
});
|
|
16985
|
+
});
|
|
16986
|
+
if (this.stopEvents === void 0) this.stopEvents = this.options.apiProxy.startEvents();
|
|
16987
|
+
}
|
|
16988
|
+
stopApiProxyEvents() {
|
|
16989
|
+
this.stopEvents?.();
|
|
16990
|
+
this.stopEvents = void 0;
|
|
16991
|
+
this.stopProjectionEvents?.();
|
|
16992
|
+
this.stopProjectionEvents = void 0;
|
|
16993
|
+
}
|
|
16994
|
+
async publishProjectionEvent(event) {
|
|
16995
|
+
const runtime = this.runtime;
|
|
16996
|
+
const manager = this.channelManager;
|
|
16997
|
+
if (!this.started || !this.connected || runtime === void 0 || manager === void 0) return;
|
|
16998
|
+
const issuedAt = this.now();
|
|
16999
|
+
if (event.kind === "session-event") {
|
|
17000
|
+
const append = async () => {
|
|
17001
|
+
await this.options.controlPlane.appendSessionEvents({
|
|
17002
|
+
runtime_ref: runtime.runtimeRef,
|
|
17003
|
+
host_generation: runtime.hostGeneration,
|
|
17004
|
+
session_ref: event.sessionId,
|
|
17005
|
+
entries: [event.entry]
|
|
17006
|
+
});
|
|
17007
|
+
};
|
|
17008
|
+
try {
|
|
17009
|
+
await append();
|
|
17010
|
+
} catch (error) {
|
|
17011
|
+
if (asDshRemoteError(error).code !== "REMOTE_NOT_FOUND") throw error;
|
|
17012
|
+
await this.syncProjectionSnapshot(true);
|
|
17013
|
+
await append();
|
|
17014
|
+
}
|
|
17015
|
+
const seq = event.entry.event.seq;
|
|
17016
|
+
const requestRef = `event_${createHash("sha256").update([
|
|
17017
|
+
"dsh-remote-session-event-v1",
|
|
17018
|
+
runtime.runtimeRef,
|
|
17019
|
+
String(runtime.hostGeneration),
|
|
17020
|
+
event.sessionId,
|
|
17021
|
+
String(seq),
|
|
17022
|
+
event.entry.event.type
|
|
17023
|
+
].join("\n")).digest("base64url").slice(0, 40)}`;
|
|
17024
|
+
await manager.publishProjectionEvent({
|
|
17025
|
+
protocol: DSH_REMOTE_PROTOCOL,
|
|
17026
|
+
protocol_major: 1,
|
|
17027
|
+
kind: "event",
|
|
17028
|
+
request_ref: requestRef,
|
|
17029
|
+
host_generation: runtime.hostGeneration,
|
|
17030
|
+
issued_at: issuedAt,
|
|
17031
|
+
operation: "session.history",
|
|
17032
|
+
body: {
|
|
17033
|
+
session_ref: event.sessionId,
|
|
17034
|
+
entries: [event.entry]
|
|
17035
|
+
},
|
|
17036
|
+
session_seq: seq,
|
|
17037
|
+
projection_as_of_seq: seq
|
|
17038
|
+
}, requestRef);
|
|
17039
|
+
return;
|
|
17040
|
+
}
|
|
17041
|
+
const baseline = event.kind === "mux-baseline";
|
|
17042
|
+
const requestRef = `projection_${randomUUID()}`;
|
|
17043
|
+
const pendingFits = Buffer.byteLength(JSON.stringify(event.pendingInteractions)) <= DSH_REMOTE_MAX_PAGE_RESULT_BYTES;
|
|
17044
|
+
await manager.publishProjectionEvent({
|
|
17045
|
+
protocol: DSH_REMOTE_PROTOCOL,
|
|
17046
|
+
protocol_major: 1,
|
|
17047
|
+
kind: "event",
|
|
17048
|
+
request_ref: requestRef,
|
|
17049
|
+
host_generation: runtime.hostGeneration,
|
|
17050
|
+
issued_at: issuedAt,
|
|
17051
|
+
operation: "snapshot.get",
|
|
17052
|
+
body: {
|
|
17053
|
+
...pendingFits ? { pending_interactions: event.pendingInteractions } : {},
|
|
17054
|
+
...baseline ? {
|
|
17055
|
+
reason: "mux-generation",
|
|
17056
|
+
session_ref: event.sessionId,
|
|
17057
|
+
last_seq: event.lastSeq
|
|
17058
|
+
} : {},
|
|
17059
|
+
...!pendingFits && !baseline ? { reason: "projection-overflow" } : {}
|
|
17060
|
+
},
|
|
17061
|
+
...baseline && event.lastSeq >= 0 ? { projection_as_of_seq: event.lastSeq } : {}
|
|
17062
|
+
}, requestRef);
|
|
17063
|
+
}
|
|
17064
|
+
handleTransportDisconnect(error) {
|
|
17065
|
+
if (!this.started || this.runtime === void 0) return;
|
|
17066
|
+
const remote = error === void 0 ? void 0 : asDshRemoteError(error);
|
|
17067
|
+
if (remote !== void 0) this.connectionError = remote;
|
|
17068
|
+
this.connected = false;
|
|
17069
|
+
this.serviceLeaseGeneration = 0;
|
|
17070
|
+
const manager = this.channelManager;
|
|
17071
|
+
this.channelManager = void 0;
|
|
17072
|
+
this.options.realtime.disconnect().finally(async () => {
|
|
17073
|
+
await manager?.close();
|
|
17074
|
+
}).catch(() => void 0);
|
|
17075
|
+
this.bump();
|
|
17076
|
+
if (this.reconnectTimer !== void 0 || this.options.transportAvailable === false || remote?.retryable === false) return;
|
|
17077
|
+
this.reconnectTimer = setTimeout(() => {
|
|
17078
|
+
this.reconnectTimer = void 0;
|
|
17079
|
+
if (!this.started || this.runtime === void 0 || this.reconnecting) return;
|
|
17080
|
+
this.reconnecting = true;
|
|
17081
|
+
this.connectHost().catch((caught) => {
|
|
17082
|
+
this.handleTransportDisconnect(caught);
|
|
17083
|
+
}).finally(() => {
|
|
17084
|
+
this.reconnecting = false;
|
|
17085
|
+
});
|
|
17086
|
+
}, 1e3);
|
|
17087
|
+
this.reconnectTimer.unref();
|
|
17088
|
+
}
|
|
17089
|
+
async connectHost() {
|
|
17090
|
+
const localRuntime = this.runtime;
|
|
17091
|
+
const accountId = this.accountId;
|
|
17092
|
+
const platform = this.options.platform ?? process.platform;
|
|
17093
|
+
const displayName = (await this.options.runtimeStore.account(accountId)).displayName ?? this.options.displayName ?? hostname();
|
|
17094
|
+
const desktop = await this.options.controlPlane.registerDesktop({
|
|
17095
|
+
display_name: displayName,
|
|
17096
|
+
platform
|
|
17097
|
+
});
|
|
17098
|
+
const desktopRef = typeof desktop.desktop_ref === "string" ? desktop.desktop_ref : typeof desktop.desktopRef === "string" ? desktop.desktopRef : "";
|
|
17099
|
+
if (desktopRef === "") throw new DshRemoteError("REMOTE_TRANSPORT_FAILED", "Backend 未返回桌面设备引用", true);
|
|
17100
|
+
await this.options.runtimeStore.bindDesktop(accountId, { desktopRef });
|
|
17101
|
+
this.runtime = {
|
|
17102
|
+
...localRuntime,
|
|
17103
|
+
desktopRef
|
|
17104
|
+
};
|
|
17105
|
+
const registeredRuntime = await this.options.controlPlane.registerRuntime(desktopRef, {
|
|
17106
|
+
profile_ref: this.options.profileRef,
|
|
17107
|
+
host_client_ref: this.options.hostClientRef,
|
|
17108
|
+
service_namespace: "dsh_remote",
|
|
17109
|
+
service_name: "host",
|
|
17110
|
+
protocol: DSH_REMOTE_PROTOCOL,
|
|
17111
|
+
protocol_major: 1,
|
|
17112
|
+
host_generation: localRuntime.hostGeneration,
|
|
17113
|
+
capabilities: localRuntime.capabilities
|
|
17114
|
+
});
|
|
17115
|
+
const backendRuntimeRef = typeof registeredRuntime.runtime_ref === "string" ? registeredRuntime.runtime_ref : "";
|
|
17116
|
+
const backendHostGeneration = typeof registeredRuntime.host_generation === "number" && Number.isSafeInteger(registeredRuntime.host_generation) && registeredRuntime.host_generation > 0 ? registeredRuntime.host_generation : 0;
|
|
17117
|
+
if (backendRuntimeRef === "" || backendHostGeneration < localRuntime.hostGeneration) throw new DshRemoteError("REMOTE_TRANSPORT_FAILED", "Backend 未返回匹配的 Runtime 引用与 Host generation", true);
|
|
17118
|
+
this.runtime = {
|
|
17119
|
+
...await this.options.runtimeStore.adoptRuntimeRef(accountId, this.options.profileRef, backendRuntimeRef, backendHostGeneration),
|
|
17120
|
+
desktopRef
|
|
17121
|
+
};
|
|
17122
|
+
await this.syncProjectionSnapshot(true);
|
|
17123
|
+
const controller = new AbortController();
|
|
17124
|
+
await this.options.realtime.connect({
|
|
17125
|
+
profileRef: this.options.profileRef,
|
|
17126
|
+
clientRef: this.options.hostClientRef,
|
|
17127
|
+
signal: controller.signal
|
|
17128
|
+
});
|
|
17129
|
+
const manager = new DshRemoteHostChannelManager({
|
|
17130
|
+
accountId,
|
|
17131
|
+
profileRef: this.options.profileRef,
|
|
17132
|
+
hostClientRef: this.options.hostClientRef,
|
|
17133
|
+
runtimeRef: backendRuntimeRef,
|
|
17134
|
+
realtime: this.options.realtime,
|
|
17135
|
+
secretBroker: this.options.secretBroker,
|
|
17136
|
+
dispatch: async (request, context) => await this.dispatchAuthorizedRequest(request, context),
|
|
17137
|
+
onFatal: (error) => {
|
|
17138
|
+
this.handleTransportDisconnect(error);
|
|
17139
|
+
}
|
|
17140
|
+
});
|
|
17141
|
+
try {
|
|
17142
|
+
await manager.prepare();
|
|
17143
|
+
const registered = await this.options.realtime.registerHost({
|
|
17144
|
+
runtimeRef: backendRuntimeRef,
|
|
17145
|
+
capabilities: this.runtime.capabilities,
|
|
17146
|
+
signal: controller.signal
|
|
17147
|
+
});
|
|
17148
|
+
await manager.activate(registered.serviceLeaseGeneration);
|
|
17149
|
+
this.channelManager = manager;
|
|
17150
|
+
this.serviceLeaseGeneration = registered.serviceLeaseGeneration;
|
|
17151
|
+
this.connected = true;
|
|
17152
|
+
this.connectionError = void 0;
|
|
17153
|
+
this.bump();
|
|
17154
|
+
} catch (error) {
|
|
17155
|
+
await this.options.realtime.disconnect();
|
|
17156
|
+
await manager.close();
|
|
17157
|
+
throw error;
|
|
17158
|
+
}
|
|
17159
|
+
}
|
|
17160
|
+
async syncProjectionSnapshotSafely(force = false) {
|
|
17161
|
+
const now = this.now();
|
|
17162
|
+
if (!force && now - this.lastProjectionSyncAttemptMillis < 5e3) return;
|
|
17163
|
+
this.lastProjectionSyncAttemptMillis = now;
|
|
17164
|
+
try {
|
|
17165
|
+
await this.syncProjectionSnapshot(force);
|
|
17166
|
+
this.connectionError = void 0;
|
|
17167
|
+
} catch (error) {
|
|
17168
|
+
this.connectionError = asDshRemoteError(error);
|
|
17169
|
+
this.bump();
|
|
17170
|
+
}
|
|
17171
|
+
}
|
|
17172
|
+
nextProjectionVersion() {
|
|
17173
|
+
this.projectionVersion = Math.max(this.projectionVersion + 1, Math.trunc(this.now()));
|
|
17174
|
+
return this.projectionVersion;
|
|
17175
|
+
}
|
|
17176
|
+
async syncProjectionSnapshot(force = false) {
|
|
17177
|
+
const runtime = this.runtime;
|
|
17178
|
+
const accountId = this.accountId;
|
|
17179
|
+
if (runtime === void 0 || accountId === void 0) return;
|
|
17180
|
+
if (force) this.lastProjectionSyncAttemptMillis = this.now();
|
|
17181
|
+
const capabilities = new Set(this.options.apiProxy.capabilities());
|
|
17182
|
+
const projectionAt = this.nextProjectionVersion();
|
|
17183
|
+
const previous = await this.options.runtimeStore.projectionInventory(accountId, this.options.profileRef);
|
|
17184
|
+
const workspaces = capabilities.has("workspace.list") ? await this.options.apiProxy.workspaces() : [];
|
|
17185
|
+
const currentWorkspaceRefs = new Set(workspaces.map((item) => item.workspaceId));
|
|
17186
|
+
const workspaceItems = [...workspaces.map((item) => ({
|
|
17187
|
+
workspace_ref: item.workspaceId,
|
|
17188
|
+
title: item.title,
|
|
17189
|
+
path: item.path,
|
|
17190
|
+
available: item.available,
|
|
17191
|
+
projection_at: projectionAt,
|
|
17192
|
+
deleted: false
|
|
17193
|
+
})), ...previous.workspaceRefs.filter((workspaceRef) => !currentWorkspaceRefs.has(workspaceRef)).map((workspaceRef) => ({
|
|
17194
|
+
workspace_ref: workspaceRef,
|
|
17195
|
+
title: "",
|
|
17196
|
+
path: "",
|
|
17197
|
+
available: false,
|
|
17198
|
+
projection_at: projectionAt,
|
|
17199
|
+
deleted: true
|
|
17200
|
+
}))];
|
|
17201
|
+
for (let offset = 0; offset < workspaceItems.length || offset === 0; offset += 100) {
|
|
17202
|
+
await this.options.controlPlane.syncWorkspaces({
|
|
17203
|
+
runtime_ref: runtime.runtimeRef,
|
|
17204
|
+
host_generation: runtime.hostGeneration,
|
|
17205
|
+
items: workspaceItems.slice(offset, offset + 100)
|
|
17206
|
+
});
|
|
17207
|
+
if (workspaceItems.length === 0) break;
|
|
17208
|
+
}
|
|
17209
|
+
const sessions = [];
|
|
17210
|
+
if (capabilities.has("session.list")) {
|
|
17211
|
+
let cursor;
|
|
17212
|
+
const seenCursors = /* @__PURE__ */ new Set();
|
|
17213
|
+
for (let pageCount = 0; pageCount < 200; pageCount += 1) {
|
|
17214
|
+
const page = await this.options.apiProxy.sessions({
|
|
17215
|
+
limit: 50,
|
|
17216
|
+
...cursor === void 0 ? {} : { cursor }
|
|
17217
|
+
});
|
|
17218
|
+
sessions.push(...page.items);
|
|
17219
|
+
if (page.nextCursor === void 0) break;
|
|
17220
|
+
if (seenCursors.has(page.nextCursor)) throw new DshRemoteError("REMOTE_INVALID_RESPONSE", "DSH 会话游标发生循环");
|
|
17221
|
+
seenCursors.add(page.nextCursor);
|
|
17222
|
+
cursor = page.nextCursor;
|
|
17223
|
+
if (pageCount === 199) throw new DshRemoteError("REMOTE_INVALID_RESPONSE", "DSH 会话分页超过安全上限");
|
|
17224
|
+
}
|
|
17225
|
+
}
|
|
17226
|
+
const currentSessionRefs = new Set(sessions.map((item) => item.sessionId));
|
|
17227
|
+
const sessionItems = [...sessions.map((item) => ({
|
|
17228
|
+
workspace_ref: item.workspaceId,
|
|
17229
|
+
session_ref: item.sessionId,
|
|
17230
|
+
title: item.title ?? "",
|
|
17231
|
+
source_updated_at: Math.max(1, Math.trunc(item.updatedAt)),
|
|
17232
|
+
projection_at: projectionAt,
|
|
17233
|
+
running: item.running,
|
|
17234
|
+
blank: item.blank,
|
|
17235
|
+
...item.projectionAsOfSeq === void 0 ? {} : { projection_as_of_seq: item.projectionAsOfSeq },
|
|
17236
|
+
deleted: false
|
|
17237
|
+
})), ...previous.sessions.filter((item) => !currentSessionRefs.has(item.sessionRef)).map((item) => ({
|
|
17238
|
+
workspace_ref: item.workspaceRef,
|
|
17239
|
+
session_ref: item.sessionRef,
|
|
17240
|
+
title: "",
|
|
17241
|
+
source_updated_at: item.sourceUpdatedAt,
|
|
17242
|
+
projection_at: projectionAt,
|
|
17243
|
+
running: false,
|
|
17244
|
+
blank: false,
|
|
17245
|
+
deleted: true
|
|
17246
|
+
}))];
|
|
17247
|
+
for (let offset = 0; offset < sessionItems.length || offset === 0; offset += 100) {
|
|
17248
|
+
await this.options.controlPlane.syncSessions({
|
|
17249
|
+
runtime_ref: runtime.runtimeRef,
|
|
17250
|
+
host_generation: runtime.hostGeneration,
|
|
17251
|
+
items: sessionItems.slice(offset, offset + 100)
|
|
17252
|
+
});
|
|
17253
|
+
if (sessionItems.length === 0) break;
|
|
17254
|
+
}
|
|
17255
|
+
await this.options.runtimeStore.saveProjectionInventory(accountId, this.options.profileRef, {
|
|
17256
|
+
workspaceRefs: workspaces.map((item) => item.workspaceId),
|
|
17257
|
+
sessions: sessions.map((item) => ({
|
|
17258
|
+
sessionRef: item.sessionId,
|
|
17259
|
+
workspaceRef: item.workspaceId,
|
|
17260
|
+
sourceUpdatedAt: Math.max(1, Math.trunc(item.updatedAt))
|
|
17261
|
+
}))
|
|
17262
|
+
});
|
|
17263
|
+
}
|
|
17264
|
+
async dispatch(request) {
|
|
17265
|
+
switch (request.operation) {
|
|
17266
|
+
case "capabilities.get": return {
|
|
17267
|
+
duplicate: false,
|
|
17268
|
+
value: { capabilities: this.options.apiProxy.capabilities() }
|
|
17269
|
+
};
|
|
17270
|
+
case "snapshot.get": {
|
|
17271
|
+
const cursor = optionalString(request.body, "cursor");
|
|
17272
|
+
const limit = optionalPositive(request.body, "limit");
|
|
17273
|
+
return {
|
|
17274
|
+
duplicate: false,
|
|
17275
|
+
value: await this.options.apiProxy.snapshot({
|
|
17276
|
+
...cursor === void 0 ? {} : { cursor },
|
|
17277
|
+
...limit === void 0 ? {} : { limit }
|
|
17278
|
+
})
|
|
17279
|
+
};
|
|
17280
|
+
}
|
|
17281
|
+
case "workspace.list": {
|
|
17282
|
+
const cursor = optionalString(request.body, "cursor");
|
|
17283
|
+
const limit = optionalPositive(request.body, "limit");
|
|
17284
|
+
return {
|
|
17285
|
+
duplicate: false,
|
|
17286
|
+
value: await this.options.apiProxy.workspacePage({
|
|
17287
|
+
...cursor === void 0 ? {} : { cursor },
|
|
17288
|
+
...limit === void 0 ? {} : { limit }
|
|
17289
|
+
})
|
|
17290
|
+
};
|
|
17291
|
+
}
|
|
17292
|
+
case "session.list": {
|
|
17293
|
+
const workspaceId = optionalString(request.body, "workspace_ref");
|
|
17294
|
+
const cursor = optionalString(request.body, "cursor");
|
|
17295
|
+
const limit = optionalPositive(request.body, "limit");
|
|
17296
|
+
return {
|
|
17297
|
+
duplicate: false,
|
|
17298
|
+
value: await this.options.apiProxy.sessions({
|
|
17299
|
+
...workspaceId === void 0 ? {} : { workspaceId },
|
|
17300
|
+
...cursor === void 0 ? {} : { cursor },
|
|
17301
|
+
...limit === void 0 ? {} : { limit }
|
|
17302
|
+
})
|
|
17303
|
+
};
|
|
17304
|
+
}
|
|
17305
|
+
case "session.history": {
|
|
17306
|
+
const beforeSeq = optionalPositive(request.body, "before_seq");
|
|
17307
|
+
const limit = optionalPositive(request.body, "limit");
|
|
17308
|
+
return {
|
|
17309
|
+
duplicate: false,
|
|
17310
|
+
value: await this.options.apiProxy.history({
|
|
17311
|
+
sessionId: stringBody(request.body, "session_ref"),
|
|
17312
|
+
...beforeSeq === void 0 ? {} : { beforeSeq },
|
|
17313
|
+
...limit === void 0 ? {} : { limit }
|
|
17314
|
+
})
|
|
17315
|
+
};
|
|
17316
|
+
}
|
|
17317
|
+
default: return await this.dispatchWrite(request);
|
|
17318
|
+
}
|
|
17319
|
+
}
|
|
17320
|
+
async dispatchWrite(request) {
|
|
17321
|
+
const ledger = this.ledger;
|
|
17322
|
+
if (ledger === void 0) throw new DshRemoteError("REMOTE_STORAGE_FAILED", "远控命令账本尚未就绪");
|
|
17323
|
+
const accountId = this.accountId;
|
|
17324
|
+
const runtimeRef = this.runtime.runtimeRef;
|
|
17325
|
+
const begun = ledger.begin({
|
|
17326
|
+
accountId,
|
|
17327
|
+
runtimeRef,
|
|
17328
|
+
requestRef: request.request_ref,
|
|
17329
|
+
operation: request.operation,
|
|
17330
|
+
arguments: request.body,
|
|
17331
|
+
executeBeforeMillis: request.execute_before
|
|
17332
|
+
});
|
|
17333
|
+
if (begun.duplicate) {
|
|
17334
|
+
if (begun.entry.state === "completed") return {
|
|
17335
|
+
duplicate: true,
|
|
17336
|
+
value: resultFromLedger(begun.entry)
|
|
17337
|
+
};
|
|
17338
|
+
throw new DshRemoteError("COMMAND_OUTCOME_UNKNOWN", "同一命令的结果尚未完成对账");
|
|
17339
|
+
}
|
|
17340
|
+
const identity = {
|
|
17341
|
+
accountId,
|
|
17342
|
+
runtimeRef,
|
|
17343
|
+
requestRef: request.request_ref
|
|
17344
|
+
};
|
|
17345
|
+
let value;
|
|
17346
|
+
try {
|
|
17347
|
+
switch (request.operation) {
|
|
17348
|
+
case "session.create":
|
|
17349
|
+
value = await this.options.apiProxy.createSession({
|
|
17350
|
+
workspaceId: stringBody(request.body, "workspace_ref"),
|
|
17351
|
+
dshRpcId: begun.entry.dshRpcId
|
|
17352
|
+
});
|
|
17353
|
+
break;
|
|
17354
|
+
case "session.prompt":
|
|
17355
|
+
value = await this.options.apiProxy.prompt({
|
|
17356
|
+
sessionId: stringBody(request.body, "session_ref"),
|
|
17357
|
+
mode: request.body.mode === "queue" || request.body.mode === "steer" ? request.body.mode : (() => {
|
|
17358
|
+
throw new DshRemoteError("REMOTE_REQUEST_INVALID", "prompt mode 无效");
|
|
17359
|
+
})(),
|
|
17360
|
+
content: [request.body.content],
|
|
17361
|
+
dshRpcId: begun.entry.dshRpcId
|
|
17362
|
+
});
|
|
17363
|
+
break;
|
|
17364
|
+
case "session.cancel":
|
|
17365
|
+
value = await this.options.apiProxy.cancel({
|
|
17366
|
+
sessionId: stringBody(request.body, "session_ref"),
|
|
17367
|
+
dshRpcId: begun.entry.dshRpcId
|
|
17368
|
+
});
|
|
17369
|
+
break;
|
|
17370
|
+
case "interaction.question.respond":
|
|
17371
|
+
await this.options.apiProxy.answerQuestion({
|
|
17372
|
+
interactionRpcRef: stringBody(request.body, "interaction_rpc_ref"),
|
|
17373
|
+
sessionId: stringBody(request.body, "session_ref"),
|
|
17374
|
+
answer: request.body.answer
|
|
17375
|
+
});
|
|
17376
|
+
value = { accepted: true };
|
|
17377
|
+
break;
|
|
17378
|
+
case "interaction.approval.respond":
|
|
17379
|
+
await this.options.apiProxy.answerApproval({
|
|
17380
|
+
interactionRpcRef: stringBody(request.body, "interaction_rpc_ref"),
|
|
17381
|
+
sessionId: stringBody(request.body, "session_ref"),
|
|
17382
|
+
approvalId: stringBody(request.body, "approval_id"),
|
|
17383
|
+
outcome: request.body.outcome === "allowed-once" || request.body.outcome === "rejected" ? request.body.outcome : (() => {
|
|
17384
|
+
throw new DshRemoteError("REMOTE_REQUEST_INVALID", "approval outcome 无效");
|
|
17385
|
+
})()
|
|
17386
|
+
});
|
|
17387
|
+
value = { accepted: true };
|
|
17388
|
+
break;
|
|
17389
|
+
default: throw new DshRemoteError("CAPABILITY_UNSUPPORTED", "远控操作不受支持");
|
|
17390
|
+
}
|
|
17391
|
+
} catch (error) {
|
|
17392
|
+
if (error instanceof DshRemoteError && (error.code !== "REMOTE_TRANSPORT_FAILED" || error.details.dshRejected === true)) ledger.completeRejected(identity, error);
|
|
17393
|
+
else ledger.markOutcomeUnknown(identity, "DSH transport failed after the command ledger entered pending");
|
|
17394
|
+
throw error;
|
|
17395
|
+
}
|
|
17396
|
+
ledger.complete(identity, { value });
|
|
17397
|
+
return {
|
|
17398
|
+
duplicate: false,
|
|
17399
|
+
value
|
|
17400
|
+
};
|
|
17401
|
+
}
|
|
17402
|
+
async reconcileUnsettled() {
|
|
17403
|
+
if (this.ledger === void 0 || this.accountId === void 0) return;
|
|
17404
|
+
for (const entry of this.ledger.unsettledForReconciliation(this.accountId, this.now())) {
|
|
17405
|
+
const argumentsValue = entry.payload.arguments;
|
|
17406
|
+
const argumentsRecord = argumentsValue !== null && typeof argumentsValue === "object" && !Array.isArray(argumentsValue) ? argumentsValue : void 0;
|
|
17407
|
+
const sessionId = typeof argumentsRecord?.session_ref === "string" ? argumentsRecord.session_ref : void 0;
|
|
17408
|
+
let proven = false;
|
|
17409
|
+
let recoveredValue = {
|
|
17410
|
+
recovered: true,
|
|
17411
|
+
dshRpcId: entry.dshRpcId
|
|
17412
|
+
};
|
|
17413
|
+
if (entry.operation === "session.create" && typeof argumentsRecord?.workspace_ref === "string") try {
|
|
17414
|
+
const created = await this.options.apiProxy.reconcileCreatedSession({
|
|
17415
|
+
workspaceId: argumentsRecord.workspace_ref,
|
|
17416
|
+
dshRpcId: entry.dshRpcId
|
|
17417
|
+
});
|
|
17418
|
+
if (created !== void 0) {
|
|
17419
|
+
proven = true;
|
|
17420
|
+
recoveredValue = {
|
|
17421
|
+
recovered: true,
|
|
17422
|
+
sessionId: created.sessionId
|
|
17423
|
+
};
|
|
17424
|
+
}
|
|
17425
|
+
} catch {}
|
|
17426
|
+
if (sessionId !== void 0) try {
|
|
17427
|
+
const history = await this.options.apiProxy.history({
|
|
17428
|
+
sessionId,
|
|
17429
|
+
limit: 50
|
|
17430
|
+
});
|
|
17431
|
+
proven = this.options.apiProxy.historyContainsRpcId(history.entries, entry.dshRpcId);
|
|
17432
|
+
} catch {}
|
|
17433
|
+
const identity = {
|
|
17434
|
+
accountId: entry.accountId,
|
|
17435
|
+
runtimeRef: entry.runtimeRef,
|
|
17436
|
+
requestRef: entry.requestRef
|
|
17437
|
+
};
|
|
17438
|
+
if (proven) this.ledger.complete(identity, { value: recoveredValue });
|
|
17439
|
+
else this.ledger.markOutcomeUnknown(identity, "DSH history did not prove the accepted result after Host recovery");
|
|
17440
|
+
}
|
|
17441
|
+
}
|
|
17442
|
+
requireReady() {
|
|
17443
|
+
if (!this.started || this.accountId === void 0 || this.runtime === void 0) throw new DshRemoteError("REMOTE_LOGIN_REQUIRED", "请先登录 Arkme 后使用远控");
|
|
17444
|
+
}
|
|
17445
|
+
requireConnected() {
|
|
17446
|
+
this.requireReady();
|
|
17447
|
+
if (!this.connected || this.serviceLeaseGeneration <= 0) throw new DshRemoteError("RUNTIME_OFFLINE", "当前 Runtime 未连接 Realtime", true);
|
|
17448
|
+
}
|
|
17449
|
+
bump() {
|
|
17450
|
+
this.revision += 1;
|
|
17451
|
+
const snapshot = this.getStatus();
|
|
17452
|
+
for (const listener of this.listeners) listener(snapshot);
|
|
17453
|
+
}
|
|
17454
|
+
};
|
|
17455
|
+
//#endregion
|
|
17456
|
+
//#region src/dsh-remote/realtime-transport.ts
|
|
17457
|
+
/**
|
|
17458
|
+
* Mirrors jotmo-realtime's login-only channel.publish and channel.event
|
|
17459
|
+
* wrappers. Max-width sequence/timestamp values keep the check conservative.
|
|
17460
|
+
*/
|
|
17461
|
+
function dshRemoteFrameByteLengths(input) {
|
|
17462
|
+
const maxInt64 = 0x8000000000000000;
|
|
17463
|
+
const requestId = "00000000-0000-4000-8000-000000000000";
|
|
17464
|
+
const target = wireTarget(input.target);
|
|
17465
|
+
const publish = {
|
|
17466
|
+
type: "channel.publish",
|
|
17467
|
+
namespace: "dsh_remote",
|
|
17468
|
+
channel_ref: input.target.runtimeRef,
|
|
17469
|
+
...target,
|
|
17470
|
+
direction: input.direction,
|
|
17471
|
+
command_id: input.commandId,
|
|
17472
|
+
payload: input.payload,
|
|
17473
|
+
request_id: requestId
|
|
17474
|
+
};
|
|
17475
|
+
const event = {
|
|
17476
|
+
type: "channel.event",
|
|
17477
|
+
namespace: "dsh_remote",
|
|
17478
|
+
channel_ref: input.target.runtimeRef,
|
|
17479
|
+
seq: maxInt64,
|
|
17480
|
+
event: {
|
|
17481
|
+
channel_ref: input.target.runtimeRef,
|
|
17482
|
+
command_id: input.commandId,
|
|
17483
|
+
seq: maxInt64,
|
|
17484
|
+
sender_role: input.senderRole,
|
|
17485
|
+
runtime_ref: input.target.runtimeRef,
|
|
17486
|
+
accepted_at: maxInt64,
|
|
17487
|
+
target_host_lease_generation: input.target.hostLeaseGeneration,
|
|
17488
|
+
payload: input.payload,
|
|
17489
|
+
created_at: maxInt64
|
|
17490
|
+
}
|
|
17491
|
+
};
|
|
17492
|
+
return {
|
|
17493
|
+
publish: Buffer.byteLength(JSON.stringify(publish)),
|
|
17494
|
+
event: Buffer.byteLength(JSON.stringify(event))
|
|
17495
|
+
};
|
|
17496
|
+
}
|
|
17497
|
+
function assertDshRemoteFramesFit(input) {
|
|
17498
|
+
const sizes = dshRemoteFrameByteLengths(input);
|
|
17499
|
+
if (sizes.publish > 61440 || sizes.event > 61440) throw new DshRemoteError("REMOTE_REQUEST_INVALID", "Realtime publish/event frame 超过 60KiB", false, {
|
|
17500
|
+
frameTooLarge: true,
|
|
17501
|
+
publishBytes: sizes.publish,
|
|
17502
|
+
eventBytes: sizes.event
|
|
17503
|
+
});
|
|
17504
|
+
}
|
|
17505
|
+
const SERVER_FIELDS = {
|
|
17506
|
+
"connection.ready": /* @__PURE__ */ new Set(["type", "connection_generation"]),
|
|
17507
|
+
"connection.replaced": /* @__PURE__ */ new Set(["type", "connection_generation"]),
|
|
17508
|
+
"service.registered": /* @__PURE__ */ new Set([
|
|
17509
|
+
"type",
|
|
17510
|
+
"request_id",
|
|
17511
|
+
"namespace",
|
|
17512
|
+
"service",
|
|
17513
|
+
"protocol",
|
|
17514
|
+
"protocol_major",
|
|
17515
|
+
"connection_generation",
|
|
17516
|
+
"service_lease_generation",
|
|
17517
|
+
"duplicate"
|
|
17518
|
+
]),
|
|
17519
|
+
"service.unregistered": /* @__PURE__ */ new Set([
|
|
17520
|
+
"type",
|
|
17521
|
+
"request_id",
|
|
17522
|
+
"namespace",
|
|
17523
|
+
"service",
|
|
17524
|
+
"protocol",
|
|
17525
|
+
"protocol_major",
|
|
17526
|
+
"duplicate"
|
|
17527
|
+
]),
|
|
17528
|
+
"channel.subscribed": /* @__PURE__ */ new Set([
|
|
17529
|
+
"type",
|
|
17530
|
+
"request_id",
|
|
17531
|
+
"namespace",
|
|
17532
|
+
"channel_ref",
|
|
17533
|
+
"seq",
|
|
17534
|
+
"duplicate"
|
|
17535
|
+
]),
|
|
17536
|
+
"channel.unsubscribed": /* @__PURE__ */ new Set([
|
|
17537
|
+
"type",
|
|
17538
|
+
"request_id",
|
|
17539
|
+
"namespace",
|
|
17540
|
+
"channel_ref",
|
|
17541
|
+
"duplicate"
|
|
17542
|
+
]),
|
|
17543
|
+
"channel.published": /* @__PURE__ */ new Set([
|
|
17544
|
+
"type",
|
|
17545
|
+
"request_id",
|
|
17546
|
+
"namespace",
|
|
17547
|
+
"channel_ref",
|
|
17548
|
+
"seq",
|
|
17549
|
+
"duplicate"
|
|
17550
|
+
]),
|
|
17551
|
+
"channel.event": /* @__PURE__ */ new Set([
|
|
17552
|
+
"type",
|
|
17553
|
+
"namespace",
|
|
17554
|
+
"channel_ref",
|
|
17555
|
+
"seq",
|
|
17556
|
+
"event"
|
|
17557
|
+
]),
|
|
17558
|
+
error: /* @__PURE__ */ new Set([
|
|
17559
|
+
"type",
|
|
17560
|
+
"request_id",
|
|
17561
|
+
"channel_ref",
|
|
17562
|
+
"code",
|
|
17563
|
+
"message",
|
|
17564
|
+
"retryable"
|
|
17565
|
+
])
|
|
17566
|
+
};
|
|
17567
|
+
function validServerFrame(frame) {
|
|
17568
|
+
const allowed = SERVER_FIELDS[frame.type];
|
|
17569
|
+
if (allowed === void 0 || Object.keys(frame).some((key) => !allowed.has(key))) return false;
|
|
17570
|
+
if (frame.type === "connection.ready") return positiveInteger(frame.connection_generation);
|
|
17571
|
+
if (frame.type === "connection.replaced") return true;
|
|
17572
|
+
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);
|
|
17573
|
+
return validRef(frame.request_id);
|
|
17574
|
+
}
|
|
17575
|
+
function remoteError(frame) {
|
|
17576
|
+
const code = typeof frame.code === "string" ? frame.code : "REMOTE_TRANSPORT_FAILED";
|
|
17577
|
+
return new DshRemoteError((/* @__PURE__ */ new Set([
|
|
17578
|
+
"REMOTE_LOGIN_REQUIRED",
|
|
17579
|
+
"RUNTIME_OFFLINE",
|
|
17580
|
+
"HOST_CHANNEL_NOT_READY",
|
|
17581
|
+
"CONNECTION_REPLACED",
|
|
17582
|
+
"HOST_GENERATION_STALE",
|
|
17583
|
+
"REMOTE_PROTOCOL_UNSUPPORTED",
|
|
17584
|
+
"REMOTE_REQUEST_INVALID",
|
|
17585
|
+
"REMOTE_TRANSPORT_FAILED",
|
|
17586
|
+
"REPLAY_GAP"
|
|
17587
|
+
])).has(code) ? code : "REMOTE_TRANSPORT_FAILED", typeof frame.message === "string" && frame.message.trim() !== "" ? frame.message : "Realtime 远控操作失败", frame.retryable === true);
|
|
17588
|
+
}
|
|
17589
|
+
function positive(frame, key) {
|
|
17590
|
+
const value = frame[key];
|
|
17591
|
+
if (!positiveInteger(value)) throw new DshRemoteError("REMOTE_TRANSPORT_FAILED", `Realtime ${key} 无效`, true);
|
|
17592
|
+
return value;
|
|
17593
|
+
}
|
|
17594
|
+
function positiveInteger(value) {
|
|
17595
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value > 0;
|
|
17596
|
+
}
|
|
17597
|
+
function validRef(value) {
|
|
17598
|
+
return typeof value === "string" && /^[A-Za-z0-9._:-]{1,128}$/.test(value);
|
|
17599
|
+
}
|
|
17600
|
+
function validateTarget(target, allowUnregisteredHost) {
|
|
17601
|
+
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 目标无效");
|
|
17602
|
+
}
|
|
17603
|
+
function wireTarget(target) {
|
|
17604
|
+
return {
|
|
17605
|
+
runtime_ref: target.runtimeRef,
|
|
17606
|
+
host_profile_ref: target.hostProfileRef,
|
|
17607
|
+
host_client_ref: target.hostClientRef,
|
|
17608
|
+
host_lease_generation: target.hostLeaseGeneration
|
|
17609
|
+
};
|
|
17610
|
+
}
|
|
17611
|
+
/** Exact adapter for jotmo-realtime's login-only DSH Remote contract. */
|
|
17612
|
+
var ArkmeRemoteRealtimeTransport = class {
|
|
17613
|
+
createSocket;
|
|
17614
|
+
requestTimeoutMillis;
|
|
17615
|
+
socket;
|
|
17616
|
+
connectionGeneration = 0;
|
|
17617
|
+
serviceLeaseGeneration = 0;
|
|
17618
|
+
waiters = /* @__PURE__ */ new Map();
|
|
17619
|
+
channelListeners = /* @__PURE__ */ new Map();
|
|
17620
|
+
disconnectListeners = /* @__PURE__ */ new Set();
|
|
17621
|
+
onMessage;
|
|
17622
|
+
onClose;
|
|
17623
|
+
constructor(createSocket, requestTimeoutMillis = 1e4) {
|
|
17624
|
+
this.createSocket = createSocket;
|
|
17625
|
+
this.requestTimeoutMillis = requestTimeoutMillis;
|
|
17626
|
+
if (!Number.isSafeInteger(requestTimeoutMillis) || requestTimeoutMillis < 1e3 || requestTimeoutMillis > 6e4) throw new TypeError("Realtime request timeout must be between 1000 and 60000 milliseconds");
|
|
17627
|
+
}
|
|
17628
|
+
async connect(input) {
|
|
17629
|
+
await this.disconnect();
|
|
17630
|
+
if (input.signal.aborted) throw input.signal.reason;
|
|
17631
|
+
const socket = await this.createSocket(input);
|
|
17632
|
+
this.socket = socket;
|
|
17633
|
+
this.onMessage = (event) => {
|
|
17634
|
+
this.receive(event.data);
|
|
17635
|
+
};
|
|
17636
|
+
this.onClose = () => {
|
|
17637
|
+
if (this.socket !== socket) return;
|
|
17638
|
+
this.failConnection(new DshRemoteError("REMOTE_TRANSPORT_FAILED", "Realtime 连接意外关闭", true), false);
|
|
17639
|
+
};
|
|
17640
|
+
socket.addEventListener("message", this.onMessage);
|
|
17641
|
+
socket.addEventListener("close", this.onClose);
|
|
17642
|
+
await new Promise((resolve, reject) => {
|
|
17643
|
+
const onOpen = () => {
|
|
17644
|
+
cleanup();
|
|
17645
|
+
resolve();
|
|
17646
|
+
};
|
|
17647
|
+
const onError = () => {
|
|
17648
|
+
cleanup();
|
|
17649
|
+
reject(new DshRemoteError("REMOTE_TRANSPORT_FAILED", "Realtime 连接失败", true));
|
|
17650
|
+
};
|
|
17651
|
+
const onEarlyClose = () => {
|
|
17652
|
+
cleanup();
|
|
17653
|
+
reject(new DshRemoteError("REMOTE_TRANSPORT_FAILED", "Realtime 在握手前关闭", true));
|
|
17654
|
+
};
|
|
17655
|
+
const onAbort = () => {
|
|
17656
|
+
cleanup();
|
|
17657
|
+
socket.close(1e3, "aborted");
|
|
17658
|
+
reject(input.signal.reason);
|
|
17659
|
+
};
|
|
17660
|
+
const cleanup = () => {
|
|
17661
|
+
clearTimeout(timer);
|
|
17662
|
+
socket.removeEventListener("open", onOpen);
|
|
17663
|
+
socket.removeEventListener("error", onError);
|
|
17664
|
+
socket.removeEventListener("close", onEarlyClose);
|
|
17665
|
+
input.signal.removeEventListener("abort", onAbort);
|
|
17666
|
+
};
|
|
17667
|
+
const timer = setTimeout(() => {
|
|
17668
|
+
cleanup();
|
|
17669
|
+
socket.close(1e3, "handshake timeout");
|
|
17670
|
+
reject(new DshRemoteError("REMOTE_TRANSPORT_FAILED", "Realtime 连接握手超时", true));
|
|
17671
|
+
}, this.requestTimeoutMillis);
|
|
17672
|
+
timer.unref();
|
|
17673
|
+
socket.addEventListener("open", onOpen);
|
|
17674
|
+
socket.addEventListener("error", onError);
|
|
17675
|
+
socket.addEventListener("close", onEarlyClose);
|
|
17676
|
+
input.signal.addEventListener("abort", onAbort, { once: true });
|
|
17677
|
+
});
|
|
17678
|
+
const ready = this.waitForType("connection.ready", input.signal);
|
|
17679
|
+
this.send({
|
|
17680
|
+
type: "connection.open",
|
|
17681
|
+
profile_ref: input.profileRef,
|
|
17682
|
+
client_ref: input.clientRef
|
|
17683
|
+
});
|
|
17684
|
+
this.connectionGeneration = positive(await ready, "connection_generation");
|
|
17685
|
+
}
|
|
17686
|
+
async disconnect() {
|
|
17687
|
+
this.failConnection(new DshRemoteError("REMOTE_TRANSPORT_FAILED", "Realtime 连接已关闭", true), true);
|
|
17688
|
+
}
|
|
17689
|
+
subscribeDisconnect(listener) {
|
|
17690
|
+
this.disconnectListeners.add(listener);
|
|
17691
|
+
return () => {
|
|
17692
|
+
this.disconnectListeners.delete(listener);
|
|
17693
|
+
};
|
|
17694
|
+
}
|
|
17695
|
+
async registerHost(input) {
|
|
17696
|
+
if (!validRef(input.runtimeRef) || input.capabilities.some((capability) => !/^[A-Za-z0-9._:-]{1,128}$/.test(capability))) throw new DshRemoteError("REMOTE_REQUEST_INVALID", "Realtime Host 服务描述无效");
|
|
17697
|
+
const frame = await this.request({
|
|
17698
|
+
type: "service.register",
|
|
17699
|
+
namespace: "dsh_remote",
|
|
17700
|
+
service: "host",
|
|
17701
|
+
protocol: "dsh.remote",
|
|
17702
|
+
protocol_major: 1,
|
|
17703
|
+
descriptor: {
|
|
17704
|
+
runtime_ref: input.runtimeRef,
|
|
17705
|
+
capabilities: [...new Set(input.capabilities)].sort()
|
|
17706
|
+
}
|
|
17707
|
+
}, "service.registered", input.signal);
|
|
17708
|
+
this.serviceLeaseGeneration = positive(frame, "service_lease_generation");
|
|
17709
|
+
return { serviceLeaseGeneration: this.serviceLeaseGeneration };
|
|
17710
|
+
}
|
|
17711
|
+
async unregisterHost(signal) {
|
|
17712
|
+
if (this.socket === void 0) {
|
|
17713
|
+
this.serviceLeaseGeneration = 0;
|
|
17714
|
+
return;
|
|
17715
|
+
}
|
|
17716
|
+
try {
|
|
17717
|
+
await this.request({
|
|
17718
|
+
type: "service.unregister",
|
|
17719
|
+
namespace: "dsh_remote",
|
|
17720
|
+
service: "host",
|
|
17721
|
+
protocol: "dsh.remote",
|
|
17722
|
+
protocol_major: 1
|
|
17723
|
+
}, "service.unregistered", signal ?? new AbortController().signal);
|
|
17724
|
+
} finally {
|
|
17725
|
+
this.serviceLeaseGeneration = 0;
|
|
17726
|
+
}
|
|
17727
|
+
}
|
|
17728
|
+
async subscribe(input) {
|
|
17729
|
+
validateTarget(input.target, true);
|
|
17730
|
+
if (input.afterSequence !== void 0 && (!Number.isSafeInteger(input.afterSequence) || input.afterSequence < 0)) throw new DshRemoteError("REMOTE_REQUEST_INVALID", "Realtime after_seq 无效");
|
|
17731
|
+
await this.request({
|
|
17732
|
+
type: "channel.subscribe",
|
|
17733
|
+
namespace: "dsh_remote",
|
|
17734
|
+
channel_ref: input.target.runtimeRef,
|
|
17735
|
+
...wireTarget(input.target),
|
|
17736
|
+
...input.afterSequence === void 0 ? {} : { after_seq: input.afterSequence }
|
|
17737
|
+
}, "channel.subscribed", input.signal);
|
|
17738
|
+
this.channelListeners.set(input.target.runtimeRef, (frame) => {
|
|
17739
|
+
const event = frame.event;
|
|
17740
|
+
if (event === null || typeof event !== "object" || Array.isArray(event)) return;
|
|
17741
|
+
const source = event;
|
|
17742
|
+
if (source.payload === null || typeof source.payload !== "object" || Array.isArray(source.payload)) return;
|
|
17743
|
+
if (source.sender_role !== "host" && source.sender_role !== "controller") return;
|
|
17744
|
+
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;
|
|
17745
|
+
input.onEvent(source.payload, {
|
|
17746
|
+
senderRole: source.sender_role,
|
|
17747
|
+
runtimeRef: source.runtime_ref,
|
|
17748
|
+
acceptedAtMillis: source.accepted_at,
|
|
17749
|
+
targetHostLeaseGeneration: source.target_host_lease_generation,
|
|
17750
|
+
...positiveInteger(source.seq) ? { transportSequence: source.seq } : {}
|
|
17751
|
+
});
|
|
17752
|
+
if (positiveInteger(source.seq)) this.send({
|
|
17753
|
+
type: "channel.ack",
|
|
17754
|
+
request_id: randomUUID(),
|
|
17755
|
+
namespace: "dsh_remote",
|
|
17756
|
+
channel_ref: input.target.runtimeRef,
|
|
17757
|
+
seq: source.seq
|
|
17758
|
+
});
|
|
17759
|
+
});
|
|
17760
|
+
let unsubscribed = false;
|
|
17761
|
+
const unsubscribe = () => {
|
|
17762
|
+
if (unsubscribed) return;
|
|
17763
|
+
unsubscribed = true;
|
|
17764
|
+
input.signal.removeEventListener("abort", unsubscribe);
|
|
17765
|
+
this.channelListeners.delete(input.target.runtimeRef);
|
|
17766
|
+
if (this.socket !== void 0) this.send({
|
|
17767
|
+
type: "channel.unsubscribe",
|
|
17768
|
+
request_id: randomUUID(),
|
|
17769
|
+
namespace: "dsh_remote",
|
|
17770
|
+
channel_ref: input.target.runtimeRef
|
|
17771
|
+
});
|
|
17772
|
+
};
|
|
17773
|
+
input.signal.addEventListener("abort", unsubscribe, { once: true });
|
|
17774
|
+
return unsubscribe;
|
|
17775
|
+
}
|
|
17776
|
+
async publish(input) {
|
|
17777
|
+
validateTarget(input.target, false);
|
|
17778
|
+
if (!validRef(input.commandId)) throw new DshRemoteError("REMOTE_REQUEST_INVALID", "Realtime command_id 无效");
|
|
17779
|
+
const senderRole = input.target.hostProfileRef === "" ? "controller" : "host";
|
|
17780
|
+
assertDshRemoteFramesFit({
|
|
17781
|
+
target: input.target,
|
|
17782
|
+
commandId: input.commandId,
|
|
17783
|
+
direction: input.direction,
|
|
17784
|
+
payload: input.payload,
|
|
17785
|
+
senderRole
|
|
17786
|
+
});
|
|
17787
|
+
return { sequence: positive(await this.request({
|
|
17788
|
+
type: "channel.publish",
|
|
17789
|
+
namespace: "dsh_remote",
|
|
17790
|
+
channel_ref: input.target.runtimeRef,
|
|
17791
|
+
...wireTarget(input.target),
|
|
17792
|
+
direction: input.direction,
|
|
17793
|
+
command_id: input.commandId,
|
|
17794
|
+
payload: input.payload
|
|
17795
|
+
}, "channel.published", input.signal), "seq") };
|
|
17796
|
+
}
|
|
17797
|
+
async request(frame, expectedType, signal) {
|
|
17798
|
+
const requestId = randomUUID();
|
|
17799
|
+
const response = this.waitForRequest(requestId, expectedType, signal);
|
|
17800
|
+
this.send({
|
|
17801
|
+
...frame,
|
|
17802
|
+
request_id: requestId
|
|
17803
|
+
});
|
|
17804
|
+
return await response;
|
|
17805
|
+
}
|
|
17806
|
+
waitForRequest(requestId, expectedType, signal) {
|
|
17807
|
+
return new Promise((resolve, reject) => {
|
|
17808
|
+
const cleanup = () => {
|
|
17809
|
+
clearTimeout(timer);
|
|
17810
|
+
signal.removeEventListener("abort", onAbort);
|
|
17811
|
+
};
|
|
17812
|
+
const onAbort = () => {
|
|
17813
|
+
this.waiters.delete(requestId);
|
|
17814
|
+
cleanup();
|
|
17815
|
+
reject(signal.reason);
|
|
17816
|
+
};
|
|
17817
|
+
const timer = setTimeout(() => {
|
|
17818
|
+
this.waiters.delete(requestId);
|
|
17819
|
+
cleanup();
|
|
17820
|
+
reject(new DshRemoteError("REMOTE_TRANSPORT_FAILED", `Realtime ${expectedType} 响应超时`, true));
|
|
17821
|
+
}, this.requestTimeoutMillis);
|
|
17822
|
+
timer.unref();
|
|
17823
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
17824
|
+
this.waiters.set(requestId, {
|
|
17825
|
+
resolve: (frame) => {
|
|
17826
|
+
cleanup();
|
|
17827
|
+
if (frame.type !== expectedType) reject(remoteError(frame));
|
|
17828
|
+
else resolve(frame);
|
|
17829
|
+
},
|
|
17830
|
+
reject: (error) => {
|
|
17831
|
+
cleanup();
|
|
17832
|
+
reject(error);
|
|
17833
|
+
}
|
|
17834
|
+
});
|
|
17835
|
+
});
|
|
17836
|
+
}
|
|
17837
|
+
waitForType(type, signal) {
|
|
17838
|
+
return this.waitForRequest(`@type:${type}`, type, signal);
|
|
17839
|
+
}
|
|
17840
|
+
receive(data) {
|
|
17841
|
+
let frame;
|
|
17842
|
+
try {
|
|
17843
|
+
const raw = typeof data === "string" ? data : data instanceof ArrayBuffer ? Buffer.from(data).toString("utf8") : String(data);
|
|
17844
|
+
if (Buffer.byteLength(raw) > 61440) return;
|
|
17845
|
+
const parsed = JSON.parse(raw);
|
|
17846
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed) || typeof parsed.type !== "string") return;
|
|
17847
|
+
frame = parsed;
|
|
17848
|
+
} catch {
|
|
17849
|
+
return;
|
|
17850
|
+
}
|
|
17851
|
+
if (!validServerFrame(frame)) return;
|
|
17852
|
+
if (frame.type === "connection.replaced") {
|
|
17853
|
+
this.failConnection(new DshRemoteError("CONNECTION_REPLACED", "Realtime 连接已被同一客户端的新连接替换", true), false);
|
|
17854
|
+
return;
|
|
17855
|
+
}
|
|
17856
|
+
const requestId = frame.request_id;
|
|
17857
|
+
const waiterKey = typeof requestId === "string" && this.waiters.has(requestId) ? requestId : `@type:${frame.type}`;
|
|
17858
|
+
const waiter = this.waiters.get(waiterKey);
|
|
17859
|
+
if (waiter !== void 0) {
|
|
17860
|
+
this.waiters.delete(waiterKey);
|
|
17861
|
+
waiter.resolve(frame);
|
|
17862
|
+
return;
|
|
17863
|
+
}
|
|
17864
|
+
if (frame.type === "channel.event" && typeof frame.channel_ref === "string") this.channelListeners.get(frame.channel_ref)?.(frame);
|
|
17865
|
+
}
|
|
17866
|
+
failConnection(error, expected) {
|
|
17867
|
+
const socket = this.socket;
|
|
17868
|
+
if (socket !== void 0 && this.onMessage !== void 0) socket.removeEventListener("message", this.onMessage);
|
|
17869
|
+
if (socket !== void 0 && this.onClose !== void 0) socket.removeEventListener("close", this.onClose);
|
|
17870
|
+
this.socket = void 0;
|
|
17871
|
+
this.onMessage = void 0;
|
|
17872
|
+
this.onClose = void 0;
|
|
17873
|
+
this.connectionGeneration = 0;
|
|
17874
|
+
this.serviceLeaseGeneration = 0;
|
|
17875
|
+
socket?.close(1e3, expected ? "remote host stopped" : "remote connection failed");
|
|
17876
|
+
for (const waiter of this.waiters.values()) waiter.reject(error);
|
|
17877
|
+
this.waiters.clear();
|
|
17878
|
+
this.channelListeners.clear();
|
|
17879
|
+
if (!expected) for (const listener of this.disconnectListeners) listener(error);
|
|
17880
|
+
}
|
|
17881
|
+
send(frame) {
|
|
17882
|
+
if (this.socket === void 0 || this.socket.readyState !== 1) throw new DshRemoteError("REMOTE_TRANSPORT_FAILED", "Realtime 尚未连接", true);
|
|
17883
|
+
const encoded = JSON.stringify(frame);
|
|
17884
|
+
if (Buffer.byteLength(encoded) > 61440) throw new DshRemoteError("REMOTE_REQUEST_INVALID", "Realtime frame 超过 60KiB");
|
|
17885
|
+
this.socket.send(encoded);
|
|
17886
|
+
}
|
|
17887
|
+
};
|
|
17888
|
+
//#endregion
|
|
17889
|
+
//#region src/dsh-remote/runtime-store.ts
|
|
17890
|
+
const MAX_RUNTIMES_PER_ACCOUNT = 16;
|
|
17891
|
+
function emptyState() {
|
|
17892
|
+
return {
|
|
17893
|
+
schemaVersion: 2,
|
|
17894
|
+
accounts: {}
|
|
17895
|
+
};
|
|
17896
|
+
}
|
|
17897
|
+
function accountState(state, accountId) {
|
|
17898
|
+
return state.accounts[accountId] ?? {
|
|
17899
|
+
runtimes: {},
|
|
17900
|
+
projections: {}
|
|
17901
|
+
};
|
|
17902
|
+
}
|
|
17903
|
+
var DshRemoteRuntimeStore = class {
|
|
17904
|
+
path;
|
|
17905
|
+
state;
|
|
17906
|
+
queue = Promise.resolve();
|
|
17907
|
+
constructor(directory) {
|
|
17908
|
+
this.path = join(directory, "dsh-remote", "runtime-state.json");
|
|
17909
|
+
}
|
|
17910
|
+
async activateRuntime(input) {
|
|
17911
|
+
let result;
|
|
17912
|
+
await this.update((state) => {
|
|
17913
|
+
const account = accountState(state, input.accountId);
|
|
17914
|
+
const current = account.runtimes[input.profileRef];
|
|
17915
|
+
if (current === void 0 && Object.keys(account.runtimes).length >= MAX_RUNTIMES_PER_ACCOUNT) throw new DshRemoteError("RUNTIME_LIMIT_REACHED", "同一桌面账号最多注册 16 个 DSH Runtime");
|
|
17916
|
+
result = {
|
|
17917
|
+
runtimeRef: current?.runtimeRef ?? `runtime_${randomUUID()}`,
|
|
17918
|
+
...account.desktopRef === void 0 ? {} : { desktopRef: account.desktopRef },
|
|
17919
|
+
profileRef: input.profileRef,
|
|
17920
|
+
accountId: input.accountId,
|
|
17921
|
+
hostGeneration: (current?.hostGeneration ?? 0) + 1,
|
|
17922
|
+
capabilities: [...new Set(input.capabilities)].sort(),
|
|
17923
|
+
updatedAtMillis: input.nowMillis ?? Date.now()
|
|
17924
|
+
};
|
|
17925
|
+
account.runtimes[input.profileRef] = result;
|
|
17926
|
+
state.accounts[input.accountId] = account;
|
|
17927
|
+
});
|
|
17928
|
+
return result;
|
|
17929
|
+
}
|
|
17930
|
+
async adoptRuntimeRef(accountId, profileRef, runtimeRef, hostGeneration) {
|
|
17931
|
+
const normalized = runtimeRef.trim();
|
|
17932
|
+
if (!/^[A-Za-z0-9._:-]{8,256}$/.test(normalized)) throw new DshRemoteError("REMOTE_STORAGE_FAILED", "Backend Runtime 引用无效");
|
|
17933
|
+
if (hostGeneration !== void 0 && (!Number.isSafeInteger(hostGeneration) || hostGeneration <= 0)) throw new DshRemoteError("REMOTE_STORAGE_FAILED", "Backend Host generation 无效");
|
|
17934
|
+
let result;
|
|
17935
|
+
await this.update((state) => {
|
|
17936
|
+
const account = accountState(state, accountId);
|
|
17937
|
+
const runtime = account.runtimes[profileRef];
|
|
17938
|
+
if (runtime === void 0) throw new DshRemoteError("REMOTE_STORAGE_FAILED", "远控 Runtime 尚未注册");
|
|
17939
|
+
if (Object.entries(account.runtimes).some(([otherProfile, other]) => otherProfile !== profileRef && other.runtimeRef === normalized)) throw new DshRemoteError("REMOTE_STORAGE_FAILED", "Backend Runtime 引用与其他 Profile 冲突");
|
|
17940
|
+
if (hostGeneration !== void 0 && hostGeneration < runtime.hostGeneration) throw new DshRemoteError("REMOTE_STORAGE_FAILED", "Backend Host generation 发生回退");
|
|
17941
|
+
result = {
|
|
17942
|
+
...runtime,
|
|
17943
|
+
runtimeRef: normalized,
|
|
17944
|
+
hostGeneration: hostGeneration ?? runtime.hostGeneration,
|
|
17945
|
+
updatedAtMillis: Date.now()
|
|
17946
|
+
};
|
|
17947
|
+
account.runtimes[profileRef] = result;
|
|
17948
|
+
state.accounts[accountId] = account;
|
|
17949
|
+
});
|
|
17950
|
+
return result;
|
|
17951
|
+
}
|
|
17952
|
+
async bindDesktop(accountId, input) {
|
|
17953
|
+
await this.update((state) => {
|
|
17954
|
+
const account = accountState(state, accountId);
|
|
17955
|
+
account.desktopRef = input.desktopRef;
|
|
17956
|
+
for (const [profileRef, runtime] of Object.entries(account.runtimes)) account.runtimes[profileRef] = {
|
|
17957
|
+
...runtime,
|
|
17958
|
+
desktopRef: input.desktopRef
|
|
17959
|
+
};
|
|
17960
|
+
state.accounts[accountId] = account;
|
|
17961
|
+
});
|
|
17962
|
+
}
|
|
17963
|
+
async renameDesktop(accountId, displayName) {
|
|
17964
|
+
const normalized = displayName.trim();
|
|
17965
|
+
if (normalized === "" || [...normalized].length > 80) throw new DshRemoteError("REMOTE_REQUEST_INVALID", "电脑名称必须为 1 至 80 个字符");
|
|
17966
|
+
await this.update((state) => {
|
|
17967
|
+
const account = accountState(state, accountId);
|
|
17968
|
+
account.displayName = normalized;
|
|
17969
|
+
state.accounts[accountId] = account;
|
|
17970
|
+
});
|
|
17971
|
+
}
|
|
17972
|
+
async account(accountId) {
|
|
17973
|
+
return await this.read((state) => {
|
|
17974
|
+
const account = accountState(state, accountId);
|
|
17975
|
+
return {
|
|
17976
|
+
...account.displayName === void 0 ? {} : { displayName: account.displayName },
|
|
17977
|
+
...account.desktopRef === void 0 ? {} : { desktopRef: account.desktopRef },
|
|
17978
|
+
runtimes: Object.values(account.runtimes).map((runtime) => ({ ...runtime }))
|
|
17979
|
+
};
|
|
17980
|
+
});
|
|
17981
|
+
}
|
|
17982
|
+
async projectionInventory(accountId, profileRef) {
|
|
17983
|
+
return await this.read((state) => {
|
|
17984
|
+
const inventory = accountState(state, accountId).projections[profileRef];
|
|
17985
|
+
return inventory === void 0 ? {
|
|
17986
|
+
workspaceRefs: [],
|
|
17987
|
+
sessions: []
|
|
17988
|
+
} : {
|
|
17989
|
+
workspaceRefs: [...inventory.workspaceRefs],
|
|
17990
|
+
sessions: inventory.sessions.map((value) => ({ ...value }))
|
|
17991
|
+
};
|
|
17992
|
+
});
|
|
17993
|
+
}
|
|
17994
|
+
async saveProjectionInventory(accountId, profileRef, inventory) {
|
|
17995
|
+
await this.update((state) => {
|
|
17996
|
+
const account = accountState(state, accountId);
|
|
17997
|
+
account.projections[profileRef] = {
|
|
17998
|
+
workspaceRefs: [...new Set(inventory.workspaceRefs)].sort(),
|
|
17999
|
+
sessions: [...inventory.sessions].map((value) => ({ ...value })).sort((left, right) => left.sessionRef.localeCompare(right.sessionRef))
|
|
18000
|
+
};
|
|
18001
|
+
state.accounts[accountId] = account;
|
|
18002
|
+
});
|
|
18003
|
+
}
|
|
18004
|
+
async read(reader) {
|
|
18005
|
+
let result;
|
|
18006
|
+
await this.serial(async () => {
|
|
18007
|
+
result = reader(await this.load());
|
|
18008
|
+
});
|
|
18009
|
+
return result;
|
|
18010
|
+
}
|
|
18011
|
+
async update(mutator) {
|
|
18012
|
+
await this.serial(async () => {
|
|
18013
|
+
const state = await this.load();
|
|
18014
|
+
mutator(state);
|
|
18015
|
+
await this.persist(state);
|
|
18016
|
+
});
|
|
18017
|
+
}
|
|
18018
|
+
async serial(work) {
|
|
18019
|
+
const next = this.queue.then(work, work);
|
|
18020
|
+
this.queue = next.then(() => void 0, () => void 0);
|
|
18021
|
+
await next;
|
|
18022
|
+
}
|
|
18023
|
+
async load() {
|
|
18024
|
+
if (this.state !== void 0) return this.state;
|
|
18025
|
+
try {
|
|
18026
|
+
const raw = JSON.parse(await readFile(this.path, "utf8"));
|
|
18027
|
+
if (raw.schemaVersion !== 1 && raw.schemaVersion !== 2 || raw.accounts === null || typeof raw.accounts !== "object") throw new Error("schema mismatch");
|
|
18028
|
+
const accounts = {};
|
|
18029
|
+
for (const [accountId, value] of Object.entries(raw.accounts)) {
|
|
18030
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) throw new Error("account schema mismatch");
|
|
18031
|
+
const source = value;
|
|
18032
|
+
if (source.runtimes === null || typeof source.runtimes !== "object" || Array.isArray(source.runtimes)) throw new Error("runtime schema mismatch");
|
|
18033
|
+
const runtimes = {};
|
|
18034
|
+
for (const [profileRef, runtimeValue] of Object.entries(source.runtimes)) {
|
|
18035
|
+
if (runtimeValue === null || typeof runtimeValue !== "object" || Array.isArray(runtimeValue)) throw new Error("runtime schema mismatch");
|
|
18036
|
+
const runtime = { ...runtimeValue };
|
|
18037
|
+
delete runtime.remoteEnabled;
|
|
18038
|
+
runtimes[profileRef] = runtime;
|
|
18039
|
+
}
|
|
18040
|
+
const projections = {};
|
|
18041
|
+
if (source.projections !== void 0) {
|
|
18042
|
+
if (source.projections === null || typeof source.projections !== "object" || Array.isArray(source.projections)) throw new Error("projection inventory schema mismatch");
|
|
18043
|
+
for (const [profileRef, inventoryValue] of Object.entries(source.projections)) {
|
|
18044
|
+
if (inventoryValue === null || typeof inventoryValue !== "object" || Array.isArray(inventoryValue)) throw new Error("projection inventory schema mismatch");
|
|
18045
|
+
const inventory = inventoryValue;
|
|
18046
|
+
if (!Array.isArray(inventory.workspaceRefs) || !Array.isArray(inventory.sessions)) throw new Error("projection inventory schema mismatch");
|
|
18047
|
+
projections[profileRef] = {
|
|
18048
|
+
workspaceRefs: inventory.workspaceRefs.map((value) => {
|
|
18049
|
+
if (typeof value !== "string" || value.trim() === "") throw new Error("workspace inventory schema mismatch");
|
|
18050
|
+
return value;
|
|
18051
|
+
}),
|
|
18052
|
+
sessions: inventory.sessions.map((value) => {
|
|
18053
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) throw new Error("session inventory schema mismatch");
|
|
18054
|
+
const session = value;
|
|
18055
|
+
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");
|
|
18056
|
+
return {
|
|
18057
|
+
sessionRef: session.sessionRef,
|
|
18058
|
+
workspaceRef: session.workspaceRef,
|
|
18059
|
+
sourceUpdatedAt: Number(session.sourceUpdatedAt)
|
|
18060
|
+
};
|
|
18061
|
+
})
|
|
18062
|
+
};
|
|
18063
|
+
}
|
|
18064
|
+
}
|
|
18065
|
+
accounts[accountId] = {
|
|
18066
|
+
...typeof source.displayName === "string" ? { displayName: source.displayName } : {},
|
|
18067
|
+
...typeof source.desktopRef === "string" ? { desktopRef: source.desktopRef } : {},
|
|
18068
|
+
runtimes,
|
|
18069
|
+
projections
|
|
18070
|
+
};
|
|
18071
|
+
}
|
|
18072
|
+
this.state = {
|
|
18073
|
+
schemaVersion: 2,
|
|
18074
|
+
accounts
|
|
18075
|
+
};
|
|
18076
|
+
if (raw.schemaVersion !== 2) await this.persist(this.state);
|
|
18077
|
+
} catch (error) {
|
|
18078
|
+
if (error.code !== "ENOENT") throw new DshRemoteError("REMOTE_STORAGE_FAILED", "远控 Runtime 状态已损坏", false, {}, { cause: error });
|
|
18079
|
+
this.state = emptyState();
|
|
18080
|
+
await this.persist(this.state);
|
|
18081
|
+
}
|
|
18082
|
+
return this.state;
|
|
18083
|
+
}
|
|
18084
|
+
async persist(state) {
|
|
18085
|
+
const directory = dirname(this.path);
|
|
18086
|
+
await mkdir(directory, {
|
|
18087
|
+
recursive: true,
|
|
18088
|
+
mode: 448
|
|
18089
|
+
});
|
|
18090
|
+
await securePrivateDirectory(directory);
|
|
18091
|
+
const temporary = `${this.path}.${process.pid}.${randomUUID()}.tmp`;
|
|
18092
|
+
await writeFile(temporary, `${JSON.stringify(state, void 0, 2)}\n`, { mode: 384 });
|
|
18093
|
+
await securePrivateFile(temporary);
|
|
18094
|
+
await rename(temporary, this.path);
|
|
18095
|
+
await securePrivateFile(this.path);
|
|
18096
|
+
this.state = state;
|
|
18097
|
+
}
|
|
18098
|
+
};
|
|
18099
|
+
//#endregion
|
|
18100
|
+
//#region src/dsh-remote/runtime-secret-broker.ts
|
|
18101
|
+
function normalizedAccountId(accountId) {
|
|
18102
|
+
const normalized = accountId.trim();
|
|
18103
|
+
if (!/^[A-Za-z0-9._:-]{1,128}$/.test(normalized)) throw new DshRemoteError("REMOTE_REQUEST_INVALID", "远控账号标识无效");
|
|
18104
|
+
return normalized;
|
|
18105
|
+
}
|
|
18106
|
+
function accountKey(accountId) {
|
|
18107
|
+
return `dsh-remote-desktop:${normalizedAccountId(accountId)}`;
|
|
18108
|
+
}
|
|
18109
|
+
function decodeSecrets(raw, accountId) {
|
|
18110
|
+
let value;
|
|
18111
|
+
try {
|
|
18112
|
+
value = JSON.parse(raw);
|
|
18113
|
+
} catch (error) {
|
|
18114
|
+
throw new DshRemoteError("REMOTE_STORAGE_FAILED", "远控本地密钥已损坏", false, {}, { cause: error });
|
|
18115
|
+
}
|
|
18116
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) throw new DshRemoteError("REMOTE_STORAGE_FAILED", "远控本地密钥已损坏");
|
|
18117
|
+
const source = value;
|
|
18118
|
+
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", "远控本地密钥合同不兼容");
|
|
18119
|
+
return {
|
|
18120
|
+
secrets: {
|
|
18121
|
+
schemaVersion: 2,
|
|
18122
|
+
accountId,
|
|
18123
|
+
ledgerKey: source.ledgerKey
|
|
18124
|
+
},
|
|
18125
|
+
migrated: source.schemaVersion !== 2 || "identity" in source
|
|
18126
|
+
};
|
|
18127
|
+
}
|
|
18128
|
+
/**
|
|
18129
|
+
* Stores only the local command-ledger encryption key and replay cursor.
|
|
18130
|
+
* These values protect local recovery data; they are not remote authority.
|
|
18131
|
+
*/
|
|
18132
|
+
var DshRemoteRuntimeSecretBroker = class {
|
|
18133
|
+
store;
|
|
18134
|
+
inFlight = /* @__PURE__ */ new Map();
|
|
18135
|
+
cursorWrites = /* @__PURE__ */ new Map();
|
|
18136
|
+
constructor(store) {
|
|
18137
|
+
this.store = store;
|
|
18138
|
+
}
|
|
18139
|
+
async ledgerKey(accountId) {
|
|
18140
|
+
return decodeBase64Url((await this.secrets(accountId)).ledgerKey);
|
|
18141
|
+
}
|
|
18142
|
+
async runtimeCursor(input) {
|
|
18143
|
+
const key = this.cursorKey(input);
|
|
18144
|
+
await this.cursorWrites.get(key)?.catch(() => void 0);
|
|
18145
|
+
const raw = await this.store.read(key);
|
|
18146
|
+
if (raw === void 0) return 0;
|
|
18147
|
+
return this.parseCursor(raw, input).lastTransportSequence;
|
|
18148
|
+
}
|
|
18149
|
+
async putRuntimeCursor(input) {
|
|
18150
|
+
if (!Number.isSafeInteger(input.lastTransportSequence) || input.lastTransportSequence < 0) throw new DshRemoteError("REMOTE_REQUEST_INVALID", "远控 transport sequence 无效");
|
|
18151
|
+
const key = this.cursorKey(input);
|
|
18152
|
+
const writing = (this.cursorWrites.get(key) ?? Promise.resolve()).catch(() => void 0).then(async () => {
|
|
18153
|
+
const raw = await this.store.read(key);
|
|
18154
|
+
if (raw !== void 0 && this.parseCursor(raw, input).lastTransportSequence >= input.lastTransportSequence) return;
|
|
18155
|
+
const value = {
|
|
18156
|
+
schemaVersion: 2,
|
|
18157
|
+
accountId: input.accountId,
|
|
18158
|
+
runtimeRef: input.runtimeRef,
|
|
18159
|
+
channelRef: input.channelRef,
|
|
18160
|
+
lastTransportSequence: input.lastTransportSequence
|
|
18161
|
+
};
|
|
18162
|
+
await this.store.write(key, JSON.stringify(value));
|
|
18163
|
+
});
|
|
18164
|
+
this.cursorWrites.set(key, writing);
|
|
18165
|
+
try {
|
|
18166
|
+
await writing;
|
|
18167
|
+
} finally {
|
|
18168
|
+
if (this.cursorWrites.get(key) === writing) this.cursorWrites.delete(key);
|
|
18169
|
+
}
|
|
18170
|
+
}
|
|
18171
|
+
async secrets(accountIdValue) {
|
|
18172
|
+
const accountId = normalizedAccountId(accountIdValue);
|
|
18173
|
+
const existing = this.inFlight.get(accountId);
|
|
18174
|
+
if (existing !== void 0) return await existing;
|
|
18175
|
+
const loading = this.loadOrCreate(accountId);
|
|
18176
|
+
this.inFlight.set(accountId, loading);
|
|
18177
|
+
try {
|
|
18178
|
+
return await loading;
|
|
18179
|
+
} catch (error) {
|
|
18180
|
+
if (this.inFlight.get(accountId) === loading) this.inFlight.delete(accountId);
|
|
18181
|
+
throw error;
|
|
18182
|
+
}
|
|
18183
|
+
}
|
|
18184
|
+
async loadOrCreate(accountId) {
|
|
18185
|
+
const key = accountKey(accountId);
|
|
18186
|
+
const raw = await this.store.read(key);
|
|
18187
|
+
if (raw !== void 0) {
|
|
18188
|
+
const decoded = decodeSecrets(raw, accountId);
|
|
18189
|
+
if (decoded.migrated) await this.store.write(key, JSON.stringify(decoded.secrets));
|
|
18190
|
+
return decoded.secrets;
|
|
18191
|
+
}
|
|
18192
|
+
const created = {
|
|
18193
|
+
schemaVersion: 2,
|
|
18194
|
+
accountId,
|
|
18195
|
+
ledgerKey: encodeBase64Url(randomBytes(32))
|
|
18196
|
+
};
|
|
18197
|
+
await this.store.write(key, JSON.stringify(created));
|
|
18198
|
+
return created;
|
|
18199
|
+
}
|
|
18200
|
+
parseCursor(raw, input) {
|
|
18201
|
+
let source;
|
|
18202
|
+
try {
|
|
18203
|
+
source = JSON.parse(raw);
|
|
18204
|
+
} catch (error) {
|
|
18205
|
+
throw new DshRemoteError("REMOTE_STORAGE_FAILED", "远控 transport cursor 已损坏", false, {}, { cause: error });
|
|
18206
|
+
}
|
|
18207
|
+
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 路由不匹配");
|
|
18208
|
+
return source;
|
|
18209
|
+
}
|
|
18210
|
+
cursorKey(input) {
|
|
18211
|
+
normalizedAccountId(input.accountId);
|
|
18212
|
+
if ([input.runtimeRef, input.channelRef].some((value) => value.trim() === "" || value.length > 256)) throw new DshRemoteError("REMOTE_REQUEST_INVALID", "远控 transport cursor 路由无效");
|
|
18213
|
+
return `dsh-remote-runtime-cursor:${createHash("sha256").update([
|
|
18214
|
+
"dsh-remote-runtime-cursor-v2",
|
|
18215
|
+
input.accountId,
|
|
18216
|
+
input.runtimeRef,
|
|
18217
|
+
input.channelRef
|
|
18218
|
+
].join("\n")).digest("base64url")}`;
|
|
18219
|
+
}
|
|
18220
|
+
};
|
|
18221
|
+
//#endregion
|
|
18222
|
+
//#region src/index.ts
|
|
18223
|
+
const ARKME_PRODUCTION_TRUSTED_SIGNING_KEYS = "{\"prod-ed25519-20260819-1\":\"m1MKKU16hyu1b1KKIXMG+zKEr/GmhmvyUEreJzthTxs=\"}";
|
|
18224
|
+
const Config = Schema.object({
|
|
18225
|
+
environment: Schema.union(["test", "prod"]).default("test"),
|
|
18226
|
+
authBaseUrl: Schema.string().default("https://jotmo.senguo.me"),
|
|
18227
|
+
subjectBaseUrl: Schema.string().default("https://jotmo-subject.senguo.me"),
|
|
18228
|
+
recordBaseUrl: Schema.string().default("https://jotmo-record.senguo.me"),
|
|
18229
|
+
dataBaseUrl: Schema.string().default(""),
|
|
18230
|
+
chatBaseUrl: Schema.string().default("https://jotmo-chat.senguo.me"),
|
|
18231
|
+
botBaseUrl: Schema.string().default("https://jotmo-bot.senguo.me"),
|
|
18232
|
+
imBaseUrl: Schema.string().default("https://jotmo-im.senguo.me"),
|
|
18233
|
+
webrtcBaseUrl: Schema.string().default("https://jotmo-webrtc.senguo.me"),
|
|
18234
|
+
worldBaseUrl: Schema.string().default("https://jotmo-world.senguo.me"),
|
|
18235
|
+
relationBaseUrl: Schema.string().default("https://jotmo-relation.senguo.me"),
|
|
18236
|
+
intelligentBaseUrl: Schema.string().default("https://jotmo-intelligent.senguo.me"),
|
|
18237
|
+
audioBaseUrl: Schema.string().default("https://jotmo-audio.senguo.me"),
|
|
18238
|
+
extensionPublishBaseUrl: Schema.string().default(""),
|
|
18239
|
+
extensionArtifactDirectory: Schema.string().default(""),
|
|
18240
|
+
extensionTrustedSigningKeys: Schema.string().default(ARKME_PRODUCTION_TRUSTED_SIGNING_KEYS),
|
|
18241
|
+
extensionShareDiscoveryEnabled: Schema.boolean().default(true),
|
|
18242
|
+
extensionShareDiscoveryPort: Schema.number().min(1).max(65535).default(DEFAULT_EXTENSION_SHARE_DISCOVERY_PORT),
|
|
18243
|
+
routePath: Schema.string().default("/arkme-self/api"),
|
|
18244
|
+
requestTimeoutMs: Schema.number().min(1e3).max(12e4).default(3e4),
|
|
18245
|
+
maxTextLength: Schema.number().min(1).max(1e5).default(2e4),
|
|
18246
|
+
toolProfile: Schema.union([
|
|
18247
|
+
"business",
|
|
18248
|
+
"atomic",
|
|
18249
|
+
"hybrid",
|
|
18250
|
+
"disabled"
|
|
18251
|
+
]).default("business"),
|
|
18252
|
+
relatedRecordingsEnabled: Schema.boolean().default(true),
|
|
18253
|
+
geetestCaptchaId: Schema.string().default("ec81315ab8b0f18a7bfa13602d01e307"),
|
|
18254
|
+
interwovenMomentsEnabled: Schema.boolean().default(true),
|
|
18255
|
+
chatMemberJoinEventsEnabled: Schema.boolean().default(true),
|
|
18256
|
+
stateDirectory: Schema.string().default(""),
|
|
18257
|
+
keychainServicePrefix: Schema.string().default("com.senqisi.dsh-arkme"),
|
|
18258
|
+
allowNonLoopback: Schema.boolean().default(false),
|
|
18259
|
+
allowProduction: Schema.boolean().default(false),
|
|
18260
|
+
updateCheckEnabled: Schema.boolean().default(true),
|
|
18261
|
+
updateChannel: Schema.union(["stable", "next"]).default("stable"),
|
|
18262
|
+
updateServiceBaseUrl: Schema.string().default("https://api.jotmo.cc"),
|
|
18263
|
+
updateArtifactBaseUrl: Schema.string().default(""),
|
|
18264
|
+
appVersion: Schema.string().default(""),
|
|
18265
|
+
updateCheckIntervalHours: Schema.number().min(1).max(168).default(12),
|
|
18266
|
+
updateAllowLocalInstall: Schema.boolean().default(true),
|
|
18267
|
+
richMediaRenderEnabled: Schema.boolean().default(true),
|
|
18268
|
+
richMediaSendEnabled: Schema.boolean().default(true),
|
|
18269
|
+
maxUploadBytes: Schema.number().min(1024).max(1024 * 1024 * 1024).default(100 * 1024 * 1024),
|
|
18270
|
+
openclawProfile: Schema.string().default("dev"),
|
|
18271
|
+
shareWebsite: Schema.string().default(ARKME_DEFAULT_SHARE_WEBSITE),
|
|
18272
|
+
dshRemoteFeatureEnabled: Schema.boolean().default(false),
|
|
18273
|
+
dshRemoteRealtimeBaseUrl: Schema.string().default("")
|
|
18274
|
+
});
|
|
18275
|
+
const name = "dsh-arkme";
|
|
18276
|
+
const inject = [
|
|
18277
|
+
"webServer",
|
|
18278
|
+
"tools",
|
|
18279
|
+
"systemPrompt",
|
|
18280
|
+
"pluginInventory",
|
|
18281
|
+
"clientModules"
|
|
18282
|
+
];
|
|
18283
|
+
function readDshRuntimeVersion(dshBinPath) {
|
|
18284
|
+
if (dshBinPath.trim() === "") return void 0;
|
|
18285
|
+
if (dshBinPath.endsWith("/src/bin.ts") || dshBinPath.endsWith("\\src\\bin.ts")) return void 0;
|
|
18286
|
+
try {
|
|
18287
|
+
let resolvedBinPath = dshBinPath;
|
|
18288
|
+
try {
|
|
18289
|
+
resolvedBinPath = realpathSync(dshBinPath);
|
|
18290
|
+
} catch {}
|
|
18291
|
+
const manifest = JSON.parse(readFileSync(join(dirname(resolvedBinPath), "..", "package.json"), "utf8"));
|
|
18292
|
+
return typeof manifest.version === "string" && /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/.test(manifest.version) ? manifest.version : void 0;
|
|
18293
|
+
} catch {
|
|
18294
|
+
return;
|
|
18295
|
+
}
|
|
18296
|
+
}
|
|
18297
|
+
function resolveArkmeAppVersion(configuredAppVersion, environment = process.env) {
|
|
18298
|
+
const configured = configuredAppVersion.trim();
|
|
18299
|
+
if (configured !== "") return configured;
|
|
18300
|
+
const injected = environment.ARKME_APP_VERSION?.trim();
|
|
18301
|
+
return injected === void 0 || injected === "" ? void 0 : injected;
|
|
18302
|
+
}
|
|
18303
|
+
function dshRemoteClientId(accessToken) {
|
|
18304
|
+
const parts = accessToken.split(".");
|
|
18305
|
+
if (parts.length !== 3) return void 0;
|
|
18306
|
+
try {
|
|
18307
|
+
const claims = JSON.parse(Buffer.from(parts[1], "base64url").toString("utf8"));
|
|
18308
|
+
const clientId = claims.client_id ?? claims.clientId;
|
|
18309
|
+
return typeof clientId === "number" && Number.isSafeInteger(clientId) && clientId > 0 ? clientId : void 0;
|
|
18310
|
+
} catch {
|
|
18311
|
+
return;
|
|
18312
|
+
}
|
|
18313
|
+
}
|
|
18314
|
+
function apply(ctx, config) {
|
|
18315
|
+
config = resolveArkmeConfig(ctx, config);
|
|
18316
|
+
const dshHome = process.env.DSH_HOME?.trim() || join(homedir(), ".dsh");
|
|
18317
|
+
const dshBinPath = process.argv[1] ?? "";
|
|
18318
|
+
const dshRuntimeVersion = readDshRuntimeVersion(dshBinPath);
|
|
14846
18319
|
const appVersion = resolveArkmeAppVersion(config.appVersion);
|
|
14847
18320
|
const stateDirectory = config.stateDirectory.trim() || join(dshHome, "arkme-self", config.environment);
|
|
14848
18321
|
const localDatabase = new ArkmeLocalDatabase(stateDirectory, new ArkmeStateStore(stateDirectory));
|
|
@@ -14853,14 +18326,9 @@ function apply(ctx, config) {
|
|
|
14853
18326
|
fileStateDirectory: join(stateDirectory, "files")
|
|
14854
18327
|
}, sessionStore, localDatabase, fetch, pendingSessionStore);
|
|
14855
18328
|
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);
|
|
18329
|
+
await openDshHostPath(ctx, path, signal);
|
|
14863
18330
|
});
|
|
18331
|
+
let remoteHost;
|
|
14864
18332
|
const openClawStateDirectory = join(stateDirectory, "openclaw");
|
|
14865
18333
|
const openClawCli = createOpenClawCliAdapter({
|
|
14866
18334
|
profile: config.openclawProfile,
|
|
@@ -14998,13 +18466,64 @@ function apply(ctx, config) {
|
|
|
14998
18466
|
tasks.dispose();
|
|
14999
18467
|
}, "dsh-arkme: marketplace dynamic runner bridge");
|
|
15000
18468
|
});
|
|
18469
|
+
ctx.inject(["apiProxy"], (apiCtx) => {
|
|
18470
|
+
if (!config.dshRemoteFeatureEnabled) return;
|
|
18471
|
+
const authenticatedSocketFactory = apiCtx.get("arkmeRemoteSocketFactory") ?? ((input) => createDefaultDshRemoteSocket({
|
|
18472
|
+
realtimeBaseUrl: config.dshRemoteRealtimeBaseUrl,
|
|
18473
|
+
accessToken: input.accessToken,
|
|
18474
|
+
signal: input.signal
|
|
18475
|
+
}));
|
|
18476
|
+
const profileRefValue = process.env.DSH_PROFILE?.trim() || "web";
|
|
18477
|
+
const profileRef = /^[A-Za-z0-9._:-]{1,128}$/.test(profileRefValue) ? profileRefValue : "web";
|
|
18478
|
+
const hostClientRef = `host_${createHash("sha256").update(`dsh-remote-host-client-v1\n${stateDirectory}\n${profileRef}`).digest("base64url")}`;
|
|
18479
|
+
const realtime = new ArkmeRemoteRealtimeTransport(async (input) => {
|
|
18480
|
+
const session = await sessionStore.read();
|
|
18481
|
+
if (session === void 0) throw new Error("Arkme session is unavailable");
|
|
18482
|
+
return await authenticatedSocketFactory({
|
|
18483
|
+
...input,
|
|
18484
|
+
accessToken: session.accessToken
|
|
18485
|
+
});
|
|
18486
|
+
});
|
|
18487
|
+
const apiProxy = new DshApiProxyAdapter(apiCtx.apiProxy);
|
|
18488
|
+
const secretBroker = new DshRemoteRuntimeSecretBroker(createArkmeSecureValueStore(`${config.keychainServicePrefix}.${config.environment}.dsh-remote-desktop`));
|
|
18489
|
+
const host = new ArkmeRemoteRealtimeHost({
|
|
18490
|
+
featureEnabled: config.dshRemoteFeatureEnabled,
|
|
18491
|
+
transportAvailable: true,
|
|
18492
|
+
profileRef,
|
|
18493
|
+
hostClientRef,
|
|
18494
|
+
secretBroker,
|
|
18495
|
+
runtimeStore: new DshRemoteRuntimeStore(stateDirectory),
|
|
18496
|
+
controlPlane: new DshRemoteHttpControlPlane({ post: async (path, body, signal) => await service.dshRemotePost(path, body, signal) }),
|
|
18497
|
+
realtime,
|
|
18498
|
+
apiProxy,
|
|
18499
|
+
readSession: async () => {
|
|
18500
|
+
const session = await sessionStore.read();
|
|
18501
|
+
if (session === void 0) return void 0;
|
|
18502
|
+
const clientId = dshRemoteClientId(session.accessToken);
|
|
18503
|
+
return clientId === void 0 ? void 0 : {
|
|
18504
|
+
userId: session.userId,
|
|
18505
|
+
clientId
|
|
18506
|
+
};
|
|
18507
|
+
},
|
|
18508
|
+
ledgerForAccount: (accountId, key) => new DshRemoteCommandLedger(join(stateDirectory, "dsh-remote", "ledger", createHash("sha256").update(`dsh-remote-ledger-account-v1\n${accountId}`).digest("base64url")), key)
|
|
18509
|
+
});
|
|
18510
|
+
remoteHost = host;
|
|
18511
|
+
apiCtx.effect(async () => {
|
|
18512
|
+
await host.start();
|
|
18513
|
+
return async () => {
|
|
18514
|
+
if (remoteHost === host) remoteHost = void 0;
|
|
18515
|
+
await host.stop();
|
|
18516
|
+
};
|
|
18517
|
+
}, "dsh-arkme: DSH remote Host lifecycle");
|
|
18518
|
+
});
|
|
15001
18519
|
const handler = createArkmeHostApi(service, {
|
|
15002
18520
|
expectedPort: ctx.webServer.port,
|
|
15003
18521
|
allowNonLoopback: config.allowNonLoopback,
|
|
15004
18522
|
updateManager,
|
|
15005
18523
|
extensionManager: () => extensionManager,
|
|
15006
18524
|
extensionInstallTasks: () => extensionInstallTasks,
|
|
15007
|
-
ownedExtensionInventory: () => ownedExtensionInventory
|
|
18525
|
+
ownedExtensionInventory: () => ownedExtensionInventory,
|
|
18526
|
+
remoteHost: () => remoteHost
|
|
15008
18527
|
});
|
|
15009
18528
|
const callAssetHandler = createOutgoingCallAssetHandler({ routePrefix: `${config.routePath}/call` });
|
|
15010
18529
|
const richMediaOptions = {
|
|
@@ -15037,9 +18556,9 @@ function apply(ctx, config) {
|
|
|
15037
18556
|
const harnessEmbedHandler = createHarnessEmbedRouteHandler({
|
|
15038
18557
|
getGraph: () => clientModules.graph(),
|
|
15039
18558
|
installedPackageNames: () => extensionStore.list().flatMap((item) => item.profilePackageName === void 0 ? [] : [item.profilePackageName]),
|
|
15040
|
-
readRootHtml: async () => {
|
|
18559
|
+
readRootHtml: async (request) => {
|
|
15041
18560
|
const response = await fetch(`http://127.0.0.1:${String(ctx.webServer.port)}/`, {
|
|
15042
|
-
headers:
|
|
18561
|
+
headers: dshRootDocumentHeaders(request),
|
|
15043
18562
|
signal: AbortSignal.timeout(Math.min(config.requestTimeoutMs, 5e3))
|
|
15044
18563
|
});
|
|
15045
18564
|
if (!response.ok) throw new Error(`DSH root document returned HTTP ${String(response.status)}`);
|
|
@@ -15159,7 +18678,8 @@ function validateConfig(ctx, config) {
|
|
|
15159
18678
|
config.worldBaseUrl,
|
|
15160
18679
|
config.relationBaseUrl,
|
|
15161
18680
|
config.intelligentBaseUrl,
|
|
15162
|
-
config.audioBaseUrl
|
|
18681
|
+
config.audioBaseUrl,
|
|
18682
|
+
...config.dshRemoteFeatureEnabled ? [config.dshRemoteRealtimeBaseUrl] : []
|
|
15163
18683
|
].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
18684
|
}
|
|
15165
18685
|
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 +18688,7 @@ function validateConfig(ctx, config) {
|
|
|
15168
18688
|
validatePluginUpdateServiceOrigin(config.updateServiceBaseUrl);
|
|
15169
18689
|
if (config.updateArtifactBaseUrl.trim() !== "") validatePluginUpdateArtifactOrigin(config.updateArtifactBaseUrl);
|
|
15170
18690
|
if (!/^[A-Za-z0-9_-]{1,64}$/.test(config.openclawProfile)) throw new Error("dsh-arkme: openclawProfile must be a fixed profile name");
|
|
18691
|
+
if (config.dshRemoteFeatureEnabled && config.dshRemoteRealtimeBaseUrl.trim() === "") throw new Error("dsh-arkme: enabled DSH remote requires dshRemoteRealtimeBaseUrl");
|
|
15171
18692
|
for (const [label, raw] of [
|
|
15172
18693
|
["authBaseUrl", config.authBaseUrl],
|
|
15173
18694
|
["subjectBaseUrl", config.subjectBaseUrl],
|
|
@@ -15181,7 +18702,8 @@ function validateConfig(ctx, config) {
|
|
|
15181
18702
|
["relationBaseUrl", config.relationBaseUrl],
|
|
15182
18703
|
["intelligentBaseUrl", config.intelligentBaseUrl],
|
|
15183
18704
|
["audioBaseUrl", config.audioBaseUrl],
|
|
15184
|
-
["shareWebsite", config.shareWebsite]
|
|
18705
|
+
["shareWebsite", config.shareWebsite],
|
|
18706
|
+
...config.dshRemoteFeatureEnabled ? [["dshRemoteRealtimeBaseUrl", config.dshRemoteRealtimeBaseUrl]] : []
|
|
15185
18707
|
]) {
|
|
15186
18708
|
const url = new URL(raw);
|
|
15187
18709
|
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`);
|