desktop-pet-app 0.3.27 → 0.4.3
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/out/main/index.js
CHANGED
|
@@ -29,13 +29,13 @@ const os = require("os");
|
|
|
29
29
|
const node_crypto = require("node:crypto");
|
|
30
30
|
const node_http = require("node:http");
|
|
31
31
|
const node_os = require("node:os");
|
|
32
|
+
const promises = require("node:fs/promises");
|
|
32
33
|
const crypto = require("crypto");
|
|
33
34
|
const WebSocket = require("ws");
|
|
35
|
+
const node_child_process = require("node:child_process");
|
|
34
36
|
const fs = require("fs");
|
|
35
|
-
const promises = require("node:fs/promises");
|
|
36
37
|
const node_stream = require("node:stream");
|
|
37
38
|
const promises$1 = require("node:stream/promises");
|
|
38
|
-
const node_child_process = require("node:child_process");
|
|
39
39
|
const http = require("http");
|
|
40
40
|
const ORDER = { debug: 10, info: 20, warn: 30, error: 40 };
|
|
41
41
|
class Logger {
|
|
@@ -183,7 +183,9 @@ const SKIN_IDS = [
|
|
|
183
183
|
"tiger",
|
|
184
184
|
"whale",
|
|
185
185
|
"penguin",
|
|
186
|
-
"robot-blue"
|
|
186
|
+
"robot-blue",
|
|
187
|
+
"yier",
|
|
188
|
+
"bubu"
|
|
187
189
|
];
|
|
188
190
|
const SKIN_LABELS = {
|
|
189
191
|
codex: "Codex 小宠",
|
|
@@ -197,7 +199,9 @@ const SKIN_LABELS = {
|
|
|
197
199
|
tiger: "老虎",
|
|
198
200
|
whale: "蓝鲸",
|
|
199
201
|
penguin: "企鹅",
|
|
200
|
-
"robot-blue": "蓝色协作机器人"
|
|
202
|
+
"robot-blue": "蓝色协作机器人",
|
|
203
|
+
yier: "一二(白色)",
|
|
204
|
+
bubu: "布布(棕色)"
|
|
201
205
|
};
|
|
202
206
|
const PET_EVENT = "pet:event";
|
|
203
207
|
const PET_MOVE = "pet:move";
|
|
@@ -364,6 +368,7 @@ class PetManager {
|
|
|
364
368
|
petsVisible = true;
|
|
365
369
|
localDuty = false;
|
|
366
370
|
remoteDuty = /* @__PURE__ */ new Set();
|
|
371
|
+
remoteDutyKnown = false;
|
|
367
372
|
localAgentConnectionStatus = "disconnected";
|
|
368
373
|
remoteAgentConnectionStatuses = /* @__PURE__ */ new Map();
|
|
369
374
|
observableTasks = /* @__PURE__ */ new Map();
|
|
@@ -410,9 +415,20 @@ class PetManager {
|
|
|
410
415
|
return this.localAgentConnectionStatus;
|
|
411
416
|
}
|
|
412
417
|
setRemoteDuty(usernames) {
|
|
418
|
+
this.remoteDutyKnown = true;
|
|
413
419
|
this.remoteDuty = new Set(usernames.map((username) => username.trim().toLowerCase()));
|
|
414
420
|
this.publishActivities();
|
|
415
421
|
}
|
|
422
|
+
/** undefined 表示尚未收到实时 presence,调用方可回退到后台快照。 */
|
|
423
|
+
getRemoteDuty(username) {
|
|
424
|
+
if (!this.remoteDutyKnown) return void 0;
|
|
425
|
+
return this.remoteDuty.has(username.trim().toLowerCase());
|
|
426
|
+
}
|
|
427
|
+
resetRemoteDuty() {
|
|
428
|
+
this.remoteDutyKnown = false;
|
|
429
|
+
this.remoteDuty.clear();
|
|
430
|
+
this.publishActivities();
|
|
431
|
+
}
|
|
416
432
|
setRemoteAgentConnectionStatuses(statuses) {
|
|
417
433
|
this.remoteAgentConnectionStatuses = new Map(
|
|
418
434
|
Object.entries(statuses).map(([username, status2]) => [username.trim().toLowerCase(), status2])
|
|
@@ -517,6 +533,54 @@ function center(bounds) {
|
|
|
517
533
|
return { x: bounds.x + bounds.width / 2, y: bounds.y + bounds.height / 2 };
|
|
518
534
|
}
|
|
519
535
|
const petManager = new PetManager();
|
|
536
|
+
const MAX_MENU_TASKS = 8;
|
|
537
|
+
const MENU_REFRESH_INTERVAL_MS = 5e3;
|
|
538
|
+
class BackgroundSnapshot {
|
|
539
|
+
constructor(now = Date.now) {
|
|
540
|
+
this.now = now;
|
|
541
|
+
}
|
|
542
|
+
entry;
|
|
543
|
+
pending;
|
|
544
|
+
read(key) {
|
|
545
|
+
return this.entry && Object.is(this.entry.key, key) ? this.entry.value : void 0;
|
|
546
|
+
}
|
|
547
|
+
refresh(key, loader, minIntervalMs = 0) {
|
|
548
|
+
if (this.entry && Object.is(this.entry.key, key) && this.now() - this.entry.refreshedAt < minIntervalMs) {
|
|
549
|
+
return Promise.resolve(this.entry.value);
|
|
550
|
+
}
|
|
551
|
+
if (this.pending && Object.is(this.pending.key, key)) return this.pending.promise;
|
|
552
|
+
const token = Symbol("menu-refresh");
|
|
553
|
+
const promise = Promise.resolve().then(loader).then((value) => {
|
|
554
|
+
if (this.pending?.token === token) {
|
|
555
|
+
this.entry = { key, value, refreshedAt: this.now() };
|
|
556
|
+
}
|
|
557
|
+
return value;
|
|
558
|
+
}).catch(() => this.read(key)).finally(() => {
|
|
559
|
+
if (this.pending?.token === token) this.pending = void 0;
|
|
560
|
+
});
|
|
561
|
+
this.pending = { key, token, promise };
|
|
562
|
+
return promise;
|
|
563
|
+
}
|
|
564
|
+
clear() {
|
|
565
|
+
this.entry = void 0;
|
|
566
|
+
this.pending = void 0;
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
function compactMenuText(value, maxLength) {
|
|
570
|
+
const compact = value.replace(/\s+/g, " ").trim();
|
|
571
|
+
if (compact.length <= maxLength) return compact;
|
|
572
|
+
if (maxLength <= 1) return compact.slice(0, maxLength);
|
|
573
|
+
return compact.slice(0, maxLength - 1).trimEnd() + "…";
|
|
574
|
+
}
|
|
575
|
+
function resolveFriendDuty(snapshotDuty, liveDuty) {
|
|
576
|
+
return liveDuty ?? snapshotDuty ?? false;
|
|
577
|
+
}
|
|
578
|
+
function menuTasks(tasks, limit = MAX_MENU_TASKS) {
|
|
579
|
+
return [...tasks].sort((a, b) => {
|
|
580
|
+
const activeDelta = Number(isActiveTask(b)) - Number(isActiveTask(a));
|
|
581
|
+
return activeDelta || Date.parse(b.updatedAt) - Date.parse(a.updatedAt);
|
|
582
|
+
}).slice(0, limit);
|
|
583
|
+
}
|
|
520
584
|
const OFFICIAL_SERVER_ROOT = "https://api.xinbit.top/desktop-pet";
|
|
521
585
|
const OFFICIAL_PORTAL_ORIGIN = "https://deskpalai.com";
|
|
522
586
|
const LEGACY_OFFICIAL_SERVER_HOSTS = "47.94.20.104".split(",").map((host) => host.trim()).filter(Boolean);
|
|
@@ -623,6 +687,38 @@ function relayURLFromHost(value) {
|
|
|
623
687
|
function isLocalHost(value) {
|
|
624
688
|
return /^(localhost|127\.0\.0\.1|\[::1\])(?::\d+)?(?:\/|$)/i.test(value.trim());
|
|
625
689
|
}
|
|
690
|
+
const INSTALLATION_ID_FILE = "update-installation-id";
|
|
691
|
+
const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
692
|
+
let cachedInstallationID;
|
|
693
|
+
async function installationID() {
|
|
694
|
+
if (cachedInstallationID) return cachedInstallationID;
|
|
695
|
+
const path2 = node_path.join(electron.app.getPath("userData"), INSTALLATION_ID_FILE);
|
|
696
|
+
let replaceInvalidFile = false;
|
|
697
|
+
try {
|
|
698
|
+
const stored = (await promises.readFile(path2, "utf8")).trim();
|
|
699
|
+
if (UUID_PATTERN.test(stored)) {
|
|
700
|
+
cachedInstallationID = stored;
|
|
701
|
+
return stored;
|
|
702
|
+
}
|
|
703
|
+
replaceInvalidFile = true;
|
|
704
|
+
} catch (error) {
|
|
705
|
+
if (error.code !== "ENOENT") {
|
|
706
|
+
logger.warn("could not read installation id", { error: String(error) });
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
const created = node_crypto.randomUUID();
|
|
710
|
+
await promises.mkdir(electron.app.getPath("userData"), { recursive: true });
|
|
711
|
+
try {
|
|
712
|
+
await promises.writeFile(path2, created, { flag: replaceInvalidFile ? "w" : "wx", mode: 384 });
|
|
713
|
+
cachedInstallationID = created;
|
|
714
|
+
} catch (error) {
|
|
715
|
+
if (error.code !== "EEXIST") throw error;
|
|
716
|
+
const stored = (await promises.readFile(path2, "utf8")).trim();
|
|
717
|
+
if (!UUID_PATTERN.test(stored)) throw new Error("设备安装标识无效");
|
|
718
|
+
cachedInstallationID = stored;
|
|
719
|
+
}
|
|
720
|
+
return cachedInstallationID;
|
|
721
|
+
}
|
|
626
722
|
const TASK_PROTOCOL_VERSION = "1.0";
|
|
627
723
|
const WORKSPACE_SNAPSHOT_MIME = "application/vnd.desktop-pet.git-workspace-bundle";
|
|
628
724
|
function taskParticipantRole(task, authenticatedUserId) {
|
|
@@ -726,16 +822,67 @@ const CAPABILITY_ALIASES = {
|
|
|
726
822
|
"web.request": "network",
|
|
727
823
|
"web.browser": "browser",
|
|
728
824
|
"document.write": "document.create",
|
|
729
|
-
"docs.create": "document.create"
|
|
825
|
+
"docs.create": "document.create",
|
|
826
|
+
node: "toolchain.node",
|
|
827
|
+
go: "toolchain.go",
|
|
828
|
+
golang: "toolchain.go",
|
|
829
|
+
git: "toolchain.git",
|
|
830
|
+
python: "toolchain.python",
|
|
831
|
+
python3: "toolchain.python",
|
|
832
|
+
npm: "toolchain.npm",
|
|
833
|
+
pnpm: "toolchain.pnpm",
|
|
834
|
+
yarn: "toolchain.yarn",
|
|
835
|
+
bun: "toolchain.bun",
|
|
836
|
+
rust: "toolchain.rust",
|
|
837
|
+
cargo: "toolchain.cargo",
|
|
838
|
+
java: "toolchain.java",
|
|
839
|
+
maven: "toolchain.maven",
|
|
840
|
+
mvn: "toolchain.maven",
|
|
841
|
+
gradle: "toolchain.gradle"
|
|
730
842
|
};
|
|
731
|
-
|
|
732
|
-
|
|
843
|
+
const TOOLCHAIN_PROBES = [
|
|
844
|
+
["git", "toolchain.git"],
|
|
845
|
+
["go", "toolchain.go"],
|
|
846
|
+
["python3", "toolchain.python"],
|
|
847
|
+
["npm", "toolchain.npm"],
|
|
848
|
+
["pnpm", "toolchain.pnpm"],
|
|
849
|
+
["yarn", "toolchain.yarn"],
|
|
850
|
+
["bun", "toolchain.bun"],
|
|
851
|
+
["rustc", "toolchain.rust"],
|
|
852
|
+
["cargo", "toolchain.cargo"],
|
|
853
|
+
["java", "toolchain.java"],
|
|
854
|
+
["mvn", "toolchain.maven"],
|
|
855
|
+
["gradle", "toolchain.gradle"]
|
|
856
|
+
];
|
|
857
|
+
let cachedDetectedCapabilities;
|
|
858
|
+
function commandAvailable(command) {
|
|
859
|
+
const result = node_child_process.spawnSync(command, ["--version"], {
|
|
860
|
+
stdio: "ignore",
|
|
861
|
+
timeout: 1e3
|
|
862
|
+
});
|
|
863
|
+
return !result.error && result.status === 0;
|
|
864
|
+
}
|
|
865
|
+
function detectEnvironmentCapabilities(probe = commandAvailable) {
|
|
866
|
+
return [
|
|
867
|
+
"toolchain.node",
|
|
868
|
+
...TOOLCHAIN_PROBES.filter(([command]) => probe(command)).map(([, capability]) => capability)
|
|
869
|
+
];
|
|
870
|
+
}
|
|
871
|
+
function detectedEnvironmentCapabilities() {
|
|
872
|
+
if (!cachedDetectedCapabilities) cachedDetectedCapabilities = detectEnvironmentCapabilities();
|
|
873
|
+
return cachedDetectedCapabilities;
|
|
874
|
+
}
|
|
875
|
+
function normalizeCapabilities(values) {
|
|
733
876
|
return [
|
|
734
877
|
...new Set(
|
|
735
|
-
|
|
878
|
+
values.map((value) => value.trim().toLowerCase()).filter(Boolean).map((value) => CAPABILITY_ALIASES[value] ?? value)
|
|
736
879
|
)
|
|
737
880
|
];
|
|
738
881
|
}
|
|
882
|
+
function executionCapabilities(configured = process.env.DESKTOP_PET_EXECUTION_CAPABILITIES, detected = detectedEnvironmentCapabilities(), additional = process.env.DESKTOP_PET_ADDITIONAL_CAPABILITIES) {
|
|
883
|
+
const base = configured?.trim() ? configured.split(",") : [...DEFAULT_EXECUTION_CAPABILITIES, ...detected];
|
|
884
|
+
return normalizeCapabilities([...base, ...additional?.split(",") ?? []]);
|
|
885
|
+
}
|
|
739
886
|
function showDesktopNotification(title, body) {
|
|
740
887
|
if (!electron.Notification.isSupported()) return;
|
|
741
888
|
new electron.Notification({ title, body }).show();
|
|
@@ -821,6 +968,7 @@ let observedMcpBridgeVersion;
|
|
|
821
968
|
let ws = null;
|
|
822
969
|
let connectPromise = null;
|
|
823
970
|
let intentionalClose = false;
|
|
971
|
+
let authenticationBlocked = !allowDevelopmentOverrides && !authToken;
|
|
824
972
|
let protocolV1Available = false;
|
|
825
973
|
let workspaceSnapshotStagingAvailable = false;
|
|
826
974
|
let mcpRegistration;
|
|
@@ -868,6 +1016,7 @@ function configureRelayIdentity(config) {
|
|
|
868
1016
|
deviceID = config.deviceId;
|
|
869
1017
|
authToken = config.accessToken;
|
|
870
1018
|
signingPrivateKey = config.signingPrivateKey ?? "";
|
|
1019
|
+
authenticationBlocked = false;
|
|
871
1020
|
}
|
|
872
1021
|
function clearRelayIdentity() {
|
|
873
1022
|
if (ws || connectPromise) throw new Error("stop relay before clearing identity");
|
|
@@ -876,9 +1025,22 @@ function clearRelayIdentity() {
|
|
|
876
1025
|
deviceID = allowDevelopmentOverrides ? process.env.DESKTOP_PET_DEVICE_ID?.trim() || "" : "";
|
|
877
1026
|
authToken = allowDevelopmentOverrides ? process.env.DESKTOP_PET_TOKEN?.trim() : void 0;
|
|
878
1027
|
signingPrivateKey = "";
|
|
1028
|
+
authenticationBlocked = !allowDevelopmentOverrides && !authToken;
|
|
879
1029
|
}
|
|
880
|
-
|
|
881
|
-
|
|
1030
|
+
function isRelayAuthenticationBlocked() {
|
|
1031
|
+
return authenticationBlocked;
|
|
1032
|
+
}
|
|
1033
|
+
async function registerMcpClient(agentHost, capabilities = {}, hostExecutionCapabilities) {
|
|
1034
|
+
const sanitizedExecutionCapabilities = hostExecutionCapabilities ? [
|
|
1035
|
+
...new Set(
|
|
1036
|
+
hostExecutionCapabilities.map((value) => value.trim().toLowerCase()).filter((value) => /^[a-z0-9][a-z0-9._-]{0,63}$/.test(value))
|
|
1037
|
+
)
|
|
1038
|
+
].slice(0, 100) : void 0;
|
|
1039
|
+
mcpRegistration = {
|
|
1040
|
+
agentHost,
|
|
1041
|
+
capabilities,
|
|
1042
|
+
executionCapabilities: sanitizedExecutionCapabilities
|
|
1043
|
+
};
|
|
882
1044
|
await ensureConnected();
|
|
883
1045
|
ws?.send(
|
|
884
1046
|
JSON.stringify({
|
|
@@ -917,6 +1079,7 @@ function stopRelay() {
|
|
|
917
1079
|
protocolV1Available = false;
|
|
918
1080
|
workspaceSnapshotStagingAvailable = false;
|
|
919
1081
|
dutyPublished = false;
|
|
1082
|
+
petManager.resetRemoteDuty();
|
|
920
1083
|
cancelRelayReconnect();
|
|
921
1084
|
retryAttempt = 0;
|
|
922
1085
|
connectPromise = null;
|
|
@@ -960,6 +1123,7 @@ function stopRelay() {
|
|
|
960
1123
|
function ensureConnected() {
|
|
961
1124
|
if (ws && ws.readyState === WebSocket.OPEN) return Promise.resolve();
|
|
962
1125
|
if (connectPromise) return connectPromise;
|
|
1126
|
+
if (authenticationBlocked) return Promise.reject(relayAuthenticationError());
|
|
963
1127
|
connectPromise = new Promise((resolve, reject) => {
|
|
964
1128
|
let settled = false;
|
|
965
1129
|
let authRejected = false;
|
|
@@ -989,6 +1153,7 @@ function ensureConnected() {
|
|
|
989
1153
|
if (msg.task) msg.task = withParticipantRole(msg.task);
|
|
990
1154
|
if (msg.tasks) msg.tasks = msg.tasks.map(withParticipantRole);
|
|
991
1155
|
if (msg.type === "registered") {
|
|
1156
|
+
authenticationBlocked = false;
|
|
992
1157
|
retryAttempt = 0;
|
|
993
1158
|
protocolV1Available = msg.protocolVersion === TASK_PROTOCOL_VERSION;
|
|
994
1159
|
workspaceSnapshotStagingAvailable = msg.capabilities?.workspaceSnapshotStaging === true;
|
|
@@ -1092,13 +1257,13 @@ function ensureConnected() {
|
|
|
1092
1257
|
}
|
|
1093
1258
|
});
|
|
1094
1259
|
sock.on("error", (err) => {
|
|
1095
|
-
if (/Unexpected server response:
|
|
1260
|
+
if (/Unexpected server response: (?:401|403)/.test(String(err))) {
|
|
1261
|
+
authRejected = true;
|
|
1262
|
+
if (ws === sock) authenticationBlocked = true;
|
|
1263
|
+
}
|
|
1096
1264
|
logger.warn("relay socket error", { error: String(err) });
|
|
1097
1265
|
if (!settled) {
|
|
1098
1266
|
settled = true;
|
|
1099
|
-
if (ws === sock) {
|
|
1100
|
-
connectPromise = null;
|
|
1101
|
-
}
|
|
1102
1267
|
reject(err);
|
|
1103
1268
|
}
|
|
1104
1269
|
});
|
|
@@ -1120,6 +1285,7 @@ function ensureConnected() {
|
|
|
1120
1285
|
}
|
|
1121
1286
|
if (isCurrentSocket && !intentionalClose) {
|
|
1122
1287
|
if (authRejected) {
|
|
1288
|
+
authenticationBlocked = true;
|
|
1123
1289
|
logger.warn("relay rejected by server authentication; auto reconnect paused until identity changes", {
|
|
1124
1290
|
server: serverURL,
|
|
1125
1291
|
authenticated: Boolean(authToken)
|
|
@@ -1132,6 +1298,13 @@ function ensureConnected() {
|
|
|
1132
1298
|
});
|
|
1133
1299
|
return connectPromise;
|
|
1134
1300
|
}
|
|
1301
|
+
function relayAuthenticationError() {
|
|
1302
|
+
const error = new Error(
|
|
1303
|
+
authToken ? "中转服务认证已失效,请重新登录桌宠账号" : "请先登录桌宠账号后再使用协作功能"
|
|
1304
|
+
);
|
|
1305
|
+
error.code = "RELAY_AUTH_REQUIRED";
|
|
1306
|
+
return error;
|
|
1307
|
+
}
|
|
1135
1308
|
async function askFriend(opts) {
|
|
1136
1309
|
await ensureConnected();
|
|
1137
1310
|
const questionId = crypto.randomUUID();
|
|
@@ -1190,6 +1363,9 @@ async function askFriend(opts) {
|
|
|
1190
1363
|
async function getCollaborationState() {
|
|
1191
1364
|
const response = await relayRPC({ type: "help.list" }, 1e4);
|
|
1192
1365
|
const friends = response.friends ?? [];
|
|
1366
|
+
petManager.setRemoteDuty(
|
|
1367
|
+
friends.filter((friend) => friend.duty).map((friend) => friend.username)
|
|
1368
|
+
);
|
|
1193
1369
|
petManager.setRemoteAgentConnectionStatuses(
|
|
1194
1370
|
Object.fromEntries(
|
|
1195
1371
|
friends.flatMap((friend) => friend.agentStatus ? [[friend.username, friend.agentStatus]] : [])
|
|
@@ -1313,10 +1489,13 @@ function relayCapabilities() {
|
|
|
1313
1489
|
connectionStatus: mcpConnectionStatus,
|
|
1314
1490
|
...observedMcpBridgeVersion ? { bridgeProtocolVersion: observedMcpBridgeVersion } : {}
|
|
1315
1491
|
},
|
|
1316
|
-
execution:
|
|
1492
|
+
execution: currentExecutionCapabilities(),
|
|
1317
1493
|
platform: `${process.platform}-${process.arch}`
|
|
1318
1494
|
};
|
|
1319
1495
|
}
|
|
1496
|
+
function currentExecutionCapabilities() {
|
|
1497
|
+
return mcpRegistration?.executionCapabilities ?? executionCapabilities();
|
|
1498
|
+
}
|
|
1320
1499
|
function publishDeviceCapabilities() {
|
|
1321
1500
|
if (!ws || ws.readyState !== WebSocket.OPEN) return;
|
|
1322
1501
|
ws.send(
|
|
@@ -1330,11 +1509,11 @@ function publishDeviceCapabilities() {
|
|
|
1330
1509
|
}
|
|
1331
1510
|
function showCapabilityUnavailable(msg) {
|
|
1332
1511
|
const required = msg.requiredCapabilities ?? [];
|
|
1333
|
-
const available = new Set(
|
|
1512
|
+
const available = new Set(currentExecutionCapabilities());
|
|
1334
1513
|
const missing = required.filter((capability) => !available.has(capability));
|
|
1335
1514
|
const named = (missing.length > 0 ? missing : required).join("、") || "所需工具";
|
|
1336
1515
|
const sender = msg.from ? `@${msg.from} 的` : "这项";
|
|
1337
|
-
const text = `暂时无法匹配:${sender}
|
|
1516
|
+
const text = `暂时无法匹配:${sender}任务强制要求 ${named},但当前设备未通过对应能力声明。可让发起方改为 ignore,或补齐工具链/服务后重连。`;
|
|
1338
1517
|
dispatch({ type: "state", state: "failed", ttl: 12 });
|
|
1339
1518
|
dispatch({ type: "bubble", text, ttl: 18e3 });
|
|
1340
1519
|
showDesktopNotification("桌宠暂时无法接单", text);
|
|
@@ -1665,13 +1844,13 @@ async function activateStoredDevice() {
|
|
|
1665
1844
|
return true;
|
|
1666
1845
|
}
|
|
1667
1846
|
async function configureAndActivateDevice(credentials) {
|
|
1668
|
-
const signedCredentials =
|
|
1847
|
+
const signedCredentials = { ...credentials, ...ensureDeviceKeyMaterial(credentials) };
|
|
1669
1848
|
saveDeviceCredentials(signedCredentials);
|
|
1670
1849
|
await activateDevice(signedCredentials, true);
|
|
1671
1850
|
}
|
|
1672
1851
|
async function activateDevice(credentials, restart) {
|
|
1673
1852
|
if (!credentials.publicKey || !credentials.privateKey || !credentials.encryptionPublicKey || !credentials.encryptionPrivateKey) {
|
|
1674
|
-
credentials =
|
|
1853
|
+
credentials = { ...credentials, ...ensureDeviceKeyMaterial(credentials) };
|
|
1675
1854
|
saveDeviceCredentials(credentials);
|
|
1676
1855
|
}
|
|
1677
1856
|
const base = credentials.serverUrl.replace(/\/$/, "");
|
|
@@ -1681,6 +1860,7 @@ async function activateDevice(credentials, restart) {
|
|
|
1681
1860
|
body: JSON.stringify({
|
|
1682
1861
|
deviceId: credentials.deviceId,
|
|
1683
1862
|
refreshToken: credentials.refreshToken,
|
|
1863
|
+
installationId: await installationID(),
|
|
1684
1864
|
publicKey: credentials.publicKey,
|
|
1685
1865
|
encryptionPublicKey: credentials.encryptionPublicKey
|
|
1686
1866
|
})
|
|
@@ -1719,29 +1899,35 @@ async function activateDevice(credentials, restart) {
|
|
|
1719
1899
|
);
|
|
1720
1900
|
}, refreshAt);
|
|
1721
1901
|
}
|
|
1722
|
-
function
|
|
1723
|
-
let
|
|
1724
|
-
|
|
1725
|
-
|
|
1902
|
+
function ensureDeviceKeyMaterial(credentials = {}) {
|
|
1903
|
+
let publicKey = credentials.publicKey;
|
|
1904
|
+
let privateKey = credentials.privateKey;
|
|
1905
|
+
let encryptionPublicKey = credentials.encryptionPublicKey;
|
|
1906
|
+
let encryptionPrivateKey = credentials.encryptionPrivateKey;
|
|
1907
|
+
if (!publicKey || !privateKey) {
|
|
1908
|
+
const generated = node_crypto.generateKeyPairSync("ed25519", {
|
|
1726
1909
|
publicKeyEncoding: { type: "spki", format: "pem" },
|
|
1727
1910
|
privateKeyEncoding: { type: "pkcs8", format: "pem" }
|
|
1728
1911
|
});
|
|
1729
|
-
|
|
1912
|
+
publicKey = generated.publicKey;
|
|
1913
|
+
privateKey = generated.privateKey;
|
|
1730
1914
|
}
|
|
1731
|
-
if (!
|
|
1732
|
-
const
|
|
1915
|
+
if (!encryptionPublicKey || !encryptionPrivateKey) {
|
|
1916
|
+
const generated = node_crypto.generateKeyPairSync("rsa", {
|
|
1733
1917
|
modulusLength: 2048,
|
|
1734
1918
|
publicKeyEncoding: { type: "spki", format: "pem" },
|
|
1735
1919
|
privateKeyEncoding: { type: "pkcs8", format: "pem" }
|
|
1736
1920
|
});
|
|
1737
|
-
|
|
1921
|
+
encryptionPublicKey = generated.publicKey;
|
|
1922
|
+
encryptionPrivateKey = generated.privateKey;
|
|
1738
1923
|
}
|
|
1739
|
-
return
|
|
1924
|
+
return { publicKey, privateKey, encryptionPublicKey, encryptionPrivateKey };
|
|
1740
1925
|
}
|
|
1741
1926
|
const deviceAuth = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
|
|
1742
1927
|
__proto__: null,
|
|
1743
1928
|
activateStoredDevice,
|
|
1744
1929
|
configureAndActivateDevice,
|
|
1930
|
+
ensureDeviceKeyMaterial,
|
|
1745
1931
|
getDeviceIceServers
|
|
1746
1932
|
}, Symbol.toStringTag, { value: "Module" }));
|
|
1747
1933
|
function defaultAccountServerURL(relayURL = configuredRelayURL()) {
|
|
@@ -1767,11 +1953,16 @@ async function bindDesktopPetToWebSession(server, accessToken) {
|
|
|
1767
1953
|
if (!accessToken) throw new Error("网页没有返回有效的登录会话");
|
|
1768
1954
|
const session = await get(serverUrl, "/api/auth/me", accessToken);
|
|
1769
1955
|
if (!session.user?.username) throw new Error("服务器没有返回有效的账号信息");
|
|
1956
|
+
const previousCredentials = reusableCredentials(serverUrl, session.user.username);
|
|
1957
|
+
const keyMaterial = ensureDeviceKeyMaterial(previousCredentials);
|
|
1770
1958
|
const device = await post(
|
|
1771
1959
|
serverUrl,
|
|
1772
1960
|
"/api/devices",
|
|
1773
1961
|
{
|
|
1774
1962
|
name: `Desktop Pet · ${node_os.hostname()}`,
|
|
1963
|
+
installationId: await installationID(),
|
|
1964
|
+
publicKey: keyMaterial.publicKey,
|
|
1965
|
+
encryptionPublicKey: keyMaterial.encryptionPublicKey,
|
|
1775
1966
|
capabilities: {
|
|
1776
1967
|
agentHost: process.env.DESKTOP_PET_AGENT_HOST || "unknown-mcp-host",
|
|
1777
1968
|
mcp: { tools: true, sampling: false, roots: true, tasks: true },
|
|
@@ -1786,10 +1977,20 @@ async function bindDesktopPetToWebSession(server, accessToken) {
|
|
|
1786
1977
|
serverUrl,
|
|
1787
1978
|
userId: session.user.username,
|
|
1788
1979
|
deviceId: device.device.id,
|
|
1789
|
-
refreshToken: device.refreshToken
|
|
1980
|
+
refreshToken: device.refreshToken,
|
|
1981
|
+
...keyMaterial
|
|
1790
1982
|
});
|
|
1791
1983
|
return { username: session.user.username };
|
|
1792
1984
|
}
|
|
1985
|
+
function reusableCredentials(serverUrl, username) {
|
|
1986
|
+
try {
|
|
1987
|
+
const credentials = loadDeviceCredentials();
|
|
1988
|
+
if (!credentials || credentials.userId.toLowerCase() !== username.toLowerCase()) return void 0;
|
|
1989
|
+
return normalizeServerURL(credentials.serverUrl) === serverUrl ? credentials : void 0;
|
|
1990
|
+
} catch {
|
|
1991
|
+
return void 0;
|
|
1992
|
+
}
|
|
1993
|
+
}
|
|
1793
1994
|
function normalizeServerURL(value) {
|
|
1794
1995
|
let url;
|
|
1795
1996
|
try {
|
|
@@ -2203,7 +2404,7 @@ async function checkForUpdates(currentVersion = electron.app.getVersion()) {
|
|
|
2203
2404
|
endpoint.searchParams.set("arch", process.arch);
|
|
2204
2405
|
endpoint.searchParams.set("currentVersion", currentVersion);
|
|
2205
2406
|
endpoint.searchParams.set("channel", updateChannel());
|
|
2206
|
-
endpoint.searchParams.set("installationId", await
|
|
2407
|
+
endpoint.searchParams.set("installationId", await installationID());
|
|
2207
2408
|
const response = await electron.net.fetch(endpoint.toString(), {
|
|
2208
2409
|
headers: { accept: "application/json" },
|
|
2209
2410
|
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
@@ -2702,34 +2903,6 @@ function updateChannel() {
|
|
|
2702
2903
|
const channel = process.env.DESKTOP_PET_UPDATE_CHANNEL?.trim() || "stable";
|
|
2703
2904
|
return /^[a-z][a-z0-9-]{0,31}$/.test(channel) ? channel : "stable";
|
|
2704
2905
|
}
|
|
2705
|
-
let cachedInstallationID;
|
|
2706
|
-
async function updateInstallationID() {
|
|
2707
|
-
if (cachedInstallationID) return cachedInstallationID;
|
|
2708
|
-
const path2 = node_path.join(electron.app.getPath("userData"), "update-installation-id");
|
|
2709
|
-
let replaceInvalidFile = false;
|
|
2710
|
-
try {
|
|
2711
|
-
const stored = (await promises.readFile(path2, "utf8")).trim();
|
|
2712
|
-
if (/^[0-9a-f-]{36}$/i.test(stored)) {
|
|
2713
|
-
cachedInstallationID = stored;
|
|
2714
|
-
return stored;
|
|
2715
|
-
}
|
|
2716
|
-
replaceInvalidFile = true;
|
|
2717
|
-
} catch (error) {
|
|
2718
|
-
if (error.code !== "ENOENT") {
|
|
2719
|
-
logger.warn("could not read update installation id", { error: String(error) });
|
|
2720
|
-
}
|
|
2721
|
-
}
|
|
2722
|
-
const created = node_crypto.randomUUID();
|
|
2723
|
-
await promises.mkdir(electron.app.getPath("userData"), { recursive: true });
|
|
2724
|
-
try {
|
|
2725
|
-
await promises.writeFile(path2, created, { flag: replaceInvalidFile ? "w" : "wx", mode: 384 });
|
|
2726
|
-
cachedInstallationID = created;
|
|
2727
|
-
} catch (error) {
|
|
2728
|
-
if (error.code !== "EEXIST") throw error;
|
|
2729
|
-
cachedInstallationID = (await promises.readFile(path2, "utf8")).trim();
|
|
2730
|
-
}
|
|
2731
|
-
return cachedInstallationID;
|
|
2732
|
-
}
|
|
2733
2906
|
function asString(value) {
|
|
2734
2907
|
return typeof value === "string" ? value.trim() : "";
|
|
2735
2908
|
}
|
|
@@ -2767,6 +2940,7 @@ function humanizeUpdateError(error) {
|
|
|
2767
2940
|
return "暂时无法检查更新";
|
|
2768
2941
|
}
|
|
2769
2942
|
const SKINS = SKIN_IDS.map((id) => ({ id, label: SKIN_LABELS[id] }));
|
|
2943
|
+
const collaborationSnapshot = new BackgroundSnapshot();
|
|
2770
2944
|
let nextId = 1;
|
|
2771
2945
|
function createPetWindow(skin, options) {
|
|
2772
2946
|
if (petManager.count() >= MAX_PETS) {
|
|
@@ -2780,6 +2954,7 @@ function createPetWindow(skin, options) {
|
|
|
2780
2954
|
const initialSize = PET_WINDOW_BY_SKIN[skin] ?? PET_WINDOW;
|
|
2781
2955
|
let W = initialSize.width;
|
|
2782
2956
|
let H = initialSize.height;
|
|
2957
|
+
let menuOpen = false;
|
|
2783
2958
|
const x = options.near?.x ?? workArea.x + workArea.width - W - 120;
|
|
2784
2959
|
const y = options.near?.y ?? workArea.y + workArea.height - H - 60;
|
|
2785
2960
|
const win = new electron.BrowserWindow({
|
|
@@ -2815,6 +2990,7 @@ function createPetWindow(skin, options) {
|
|
|
2815
2990
|
const presentation = publishIdentity();
|
|
2816
2991
|
dispatch({ type: "bubble", text: presentation.greeting }, id);
|
|
2817
2992
|
petManager.replayActivity(id);
|
|
2993
|
+
refreshCollaborationSnapshot();
|
|
2818
2994
|
});
|
|
2819
2995
|
const publishIdentity = () => {
|
|
2820
2996
|
const presentation = presentPetIdentity(identity, currentAccountUsername());
|
|
@@ -2832,17 +3008,30 @@ function createPetWindow(skin, options) {
|
|
|
2832
3008
|
}
|
|
2833
3009
|
};
|
|
2834
3010
|
const onMenu = (e) => {
|
|
2835
|
-
if (win.isDestroyed() || e.sender !== win.webContents) return;
|
|
2836
|
-
|
|
3011
|
+
if (win.isDestroyed() || e.sender !== win.webContents || menuOpen) return;
|
|
3012
|
+
showPetMenu();
|
|
2837
3013
|
};
|
|
2838
3014
|
const onMousePassthrough = (e, enabled) => {
|
|
2839
3015
|
if (win.isDestroyed() || e.sender !== win.webContents || typeof enabled !== "boolean") return;
|
|
2840
3016
|
win.setIgnoreMouseEvents(enabled, { forward: enabled });
|
|
2841
3017
|
};
|
|
2842
|
-
const
|
|
3018
|
+
const refreshCollaborationSnapshot = () => {
|
|
3019
|
+
const username = currentAccountUsername();
|
|
3020
|
+
if (!username) {
|
|
3021
|
+
collaborationSnapshot.clear();
|
|
3022
|
+
return;
|
|
3023
|
+
}
|
|
3024
|
+
void collaborationSnapshot.refresh(
|
|
3025
|
+
username,
|
|
3026
|
+
getCollaborationState,
|
|
3027
|
+
MENU_REFRESH_INTERVAL_MS
|
|
3028
|
+
);
|
|
3029
|
+
};
|
|
3030
|
+
const showPetMenu = () => {
|
|
2843
3031
|
const accountUsername = currentAccountUsername();
|
|
2844
3032
|
const identityPresentation = publishIdentity();
|
|
2845
|
-
const collaboration = accountUsername ?
|
|
3033
|
+
const collaboration = accountUsername ? collaborationSnapshot.read(accountUsername) : void 0;
|
|
3034
|
+
refreshCollaborationSnapshot();
|
|
2846
3035
|
if (win.isDestroyed()) return;
|
|
2847
3036
|
const incoming = collaboration?.helpRequests.filter(
|
|
2848
3037
|
(request) => request.toUsername === accountUsername && request.status === "pending"
|
|
@@ -2894,7 +3083,7 @@ function createPetWindow(skin, options) {
|
|
|
2894
3083
|
enabled: Boolean(accountUsername),
|
|
2895
3084
|
submenu: collaboration ? collaborationMenuItems(collaboration.tasks) : [
|
|
2896
3085
|
{
|
|
2897
|
-
label: petManager.getLocalAgentConnectionStatus() === "update-required" ? "本机 Agent 需要重连以加载新版 MCP" : petManager.isLocalOnDuty() ? "本机正在驻守 · 服务器状态暂不可用" : "
|
|
3086
|
+
label: petManager.getLocalAgentConnectionStatus() === "update-required" ? "本机 Agent 需要重连以加载新版 MCP" : isRelayAuthenticationBlocked() ? "账号连接已失效,请重新登录" : petManager.isLocalOnDuty() ? "本机正在驻守 · 服务器状态暂不可用" : "正在刷新协作状态,请稍后再打开菜单",
|
|
2898
3087
|
enabled: false
|
|
2899
3088
|
}
|
|
2900
3089
|
]
|
|
@@ -2902,7 +3091,10 @@ function createPetWindow(skin, options) {
|
|
|
2902
3091
|
{
|
|
2903
3092
|
label: `好友状态${collaboration ? `(${collaboration.friends.length})` : ""}`,
|
|
2904
3093
|
enabled: Boolean(accountUsername),
|
|
2905
|
-
submenu: collaboration ? friendItems.length > 0 ? friendItems : [{ label: "还没有好友,请先到个人后台添加", enabled: false }] : [{
|
|
3094
|
+
submenu: collaboration ? friendItems.length > 0 ? friendItems : [{ label: "还没有好友,请先到个人后台添加", enabled: false }] : [{
|
|
3095
|
+
label: accountUsername ? isRelayAuthenticationBlocked() ? "账号认证已失效,请点击上方账号重新登录" : "正在刷新好友状态,请稍后再打开菜单" : "请先登录账号",
|
|
3096
|
+
enabled: false
|
|
3097
|
+
}]
|
|
2906
3098
|
},
|
|
2907
3099
|
{
|
|
2908
3100
|
label: `要我帮忙${incoming.length ? `(${incoming.length})` : ""}`,
|
|
@@ -2946,6 +3138,10 @@ function createPetWindow(skin, options) {
|
|
|
2946
3138
|
...canSendAwayPet(identity) ? [{ label: "送走这只宠物", click: () => win.close() }] : [],
|
|
2947
3139
|
{ label: "退出", click: () => electron.app.quit() }
|
|
2948
3140
|
]);
|
|
3141
|
+
menuOpen = true;
|
|
3142
|
+
menu.once("menu-will-close", () => {
|
|
3143
|
+
menuOpen = false;
|
|
3144
|
+
});
|
|
2949
3145
|
menu.popup({ window: win });
|
|
2950
3146
|
};
|
|
2951
3147
|
electron.ipcMain.on(PET_MOVE, onMove);
|
|
@@ -3031,16 +3227,6 @@ function updateMenuItems(petId) {
|
|
|
3031
3227
|
}
|
|
3032
3228
|
return items;
|
|
3033
3229
|
}
|
|
3034
|
-
async function loadCollaborationState() {
|
|
3035
|
-
try {
|
|
3036
|
-
return await Promise.race([
|
|
3037
|
-
getCollaborationState(),
|
|
3038
|
-
new Promise((resolve) => setTimeout(() => resolve(void 0), 1800))
|
|
3039
|
-
]);
|
|
3040
|
-
} catch {
|
|
3041
|
-
return void 0;
|
|
3042
|
-
}
|
|
3043
|
-
}
|
|
3044
3230
|
function friendMenuItem(friend, state, currentUsername, petId) {
|
|
3045
3231
|
const activeTask = state.tasks.filter((task) => isActiveTask(task) && taskPeer(task).toLowerCase() === friend.username.toLowerCase()).sort((a, b) => Date.parse(b.updatedAt) - Date.parse(a.updatedAt))[0];
|
|
3046
3232
|
const outboundHelp = state.helpRequests.find(
|
|
@@ -3050,13 +3236,14 @@ function friendMenuItem(friend, state, currentUsername, petId) {
|
|
|
3050
3236
|
(request) => request.status === "accepted" && (request.fromUsername === currentUsername && request.toUsername === friend.username || request.fromUsername === friend.username && request.toUsername === currentUsername)
|
|
3051
3237
|
);
|
|
3052
3238
|
const helpLabel = outboundHelp?.status === "accepted" ? " · TA 已同意帮我" : outboundHelp?.status === "pending" ? " · 求助待 TA 接受" : activeHelp ? " · 我正在帮 TA" : "";
|
|
3053
|
-
const
|
|
3239
|
+
const friendOnDuty = resolveFriendDuty(friend.duty, petManager.getRemoteDuty(friend.username));
|
|
3240
|
+
const availability = friend.agentStatus === "update-required" ? " · Agent 需重连" : activeTask ? " · " + taskActivity(activeTask).label.split(" · ").at(-1) : friendOnDuty ? " · 已驻守" : friend.online && friend.agentStatus === "disconnected" ? " · Agent 未连接" : friend.online ? " · 在线,未驻守" : " · 离线";
|
|
3054
3241
|
const chatMessages = ["你好呀!", "现在方便聊聊吗?", "谢谢你的帮助!"];
|
|
3055
3242
|
return {
|
|
3056
3243
|
label: friend.displayName + " (@" + friend.username + ")" + availability + helpLabel,
|
|
3057
3244
|
submenu: [
|
|
3058
3245
|
{
|
|
3059
|
-
label: friend.agentStatus === "update-required" ? "Desktop Pet 已更新,请 TA 重启或重新加载 Agent 的 MCP 连接" : activeTask ? "当前任务:" + taskActivity(activeTask).detail :
|
|
3246
|
+
label: friend.agentStatus === "update-required" ? "Desktop Pet 已更新,请 TA 重启或重新加载 Agent 的 MCP 连接" : activeTask ? "当前任务:" + taskActivity(activeTask).detail : friendOnDuty ? "Agent 正在驻守,等待好友任务" : friend.online && friend.agentStatus === "disconnected" ? "桌宠在线,但 Agent 尚未连接 MCP" : friend.online ? "桌宠在线,但 Agent 尚未驻守" : "桌宠当前离线",
|
|
3060
3247
|
enabled: false
|
|
3061
3248
|
},
|
|
3062
3249
|
{ type: "separator" },
|
|
@@ -3110,17 +3297,17 @@ function collaborationMenuItems(tasks) {
|
|
|
3110
3297
|
return [
|
|
3111
3298
|
duty,
|
|
3112
3299
|
{ type: "separator" },
|
|
3113
|
-
...tasks
|
|
3300
|
+
...menuTasks(tasks).map((task) => {
|
|
3114
3301
|
const view = taskActivity(task);
|
|
3115
3302
|
const measured = view.current != null && view.total != null ? " · " + view.current + "/" + view.total : "";
|
|
3116
|
-
const summary = task.result?.summary
|
|
3303
|
+
const summary = task.result?.summary ? compactMenuText(task.result.summary, 80) : void 0;
|
|
3117
3304
|
return {
|
|
3118
3305
|
label: taskMenuLabel(task),
|
|
3119
3306
|
submenu: [
|
|
3120
3307
|
{ label: "状态:" + view.label + measured, enabled: false },
|
|
3121
|
-
{ label: "当前步骤:" + view.detail, enabled: false },
|
|
3308
|
+
{ label: "当前步骤:" + compactMenuText(view.detail, 120), enabled: false },
|
|
3122
3309
|
{ label: "协作好友:@" + taskPeer(task), enabled: false },
|
|
3123
|
-
{ label: "任务:" + taskTitle(task)
|
|
3310
|
+
{ label: "任务:" + compactMenuText(taskTitle(task), 90), enabled: false },
|
|
3124
3311
|
{ label: "最近更新:" + new Date(task.updatedAt).toLocaleString(), enabled: false },
|
|
3125
3312
|
...summary ? [{ label: "结果:" + summary, enabled: false }] : []
|
|
3126
3313
|
]
|
|
@@ -3540,6 +3727,7 @@ async function handleRelay(cmd, res) {
|
|
|
3540
3727
|
},
|
|
3541
3728
|
executionPolicy: {
|
|
3542
3729
|
capabilities: ["filesystem.read"],
|
|
3730
|
+
capabilityMode: "ignore",
|
|
3543
3731
|
writableScopes: [],
|
|
3544
3732
|
networkPolicy: "ask",
|
|
3545
3733
|
approvalMode: "manual",
|
|
@@ -3702,7 +3890,7 @@ async function handleRelay(cmd, res) {
|
|
|
3702
3890
|
fail(res, 400, "register_mcp_client requires agentHost", ErrorCodes.BAD_EVENT);
|
|
3703
3891
|
return;
|
|
3704
3892
|
}
|
|
3705
|
-
await registerMcpClient(agentHost, cmd.mcpCapabilities ?? {});
|
|
3893
|
+
await registerMcpClient(agentHost, cmd.mcpCapabilities ?? {}, cmd.executionCapabilities);
|
|
3706
3894
|
ok(res);
|
|
3707
3895
|
return;
|
|
3708
3896
|
}
|
|
@@ -4129,8 +4317,8 @@ electron.app.whenReady().then(async () => {
|
|
|
4129
4317
|
return false;
|
|
4130
4318
|
});
|
|
4131
4319
|
logger.info("device activation finished", { activated });
|
|
4132
|
-
if (
|
|
4133
|
-
logger.info("relay
|
|
4320
|
+
if (activated) logger.info("relay start requested", { authenticated: true });
|
|
4321
|
+
else logger.info("relay waiting for account authentication");
|
|
4134
4322
|
if (process.env.DESKTOP_PET_P2P !== "0") startP2PWindow(getDeviceIceServers());
|
|
4135
4323
|
logger.info("p2p start requested", { enabled: process.env.DESKTOP_PET_P2P !== "0" });
|
|
4136
4324
|
void startRunnerManager();
|