desktop-pet-app 0.3.24 → 0.4.1

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 {
@@ -92,6 +92,7 @@ function resolvePortFile(env = process.env, homeDirectory = os.homedir()) {
92
92
  }
93
93
  const PORT_FILE = resolvePortFile();
94
94
  const LOCAL_PET_PROTOCOL_VERSION = "1.0";
95
+ const MCP_BRIDGE_PROTOCOL_VERSION = "1.1";
95
96
  const LOCAL_PET_MAX_BODY_BYTES = 64 * 1024;
96
97
  const MAX_PETS = 4;
97
98
  const PET_WINDOW = { width: 200, height: 190 };
@@ -101,6 +102,75 @@ const PET_WINDOW_BY_SKIN = {
101
102
  const GREET_DISTANCE = 260;
102
103
  const GREET_COOLDOWN_MS = 15e3;
103
104
  const PROXIMITY_SCAN_MS = 500;
105
+ const STATUS_LABELS = {
106
+ created: "正在安全打包工作区",
107
+ queued: "等待好友接单",
108
+ delivered: "已送达好友桌宠",
109
+ accepted: "好友已领取",
110
+ running: "好友正在执行",
111
+ needs_context: "等待补充资料",
112
+ cancelling: "正在取消",
113
+ completed: "任务已完成",
114
+ failed: "任务执行失败",
115
+ cancelled: "任务已取消",
116
+ expired: "任务已过期"
117
+ };
118
+ const PHASE_LABELS = {
119
+ receiving: "读取任务",
120
+ preparing: "准备工作区",
121
+ analyzing: "分析任务",
122
+ working: "实施处理",
123
+ testing: "运行验证",
124
+ packaging: "整理结果",
125
+ submitting: "回传结果"
126
+ };
127
+ const TERMINAL = /* @__PURE__ */ new Set(["completed", "failed", "cancelled", "expired"]);
128
+ function taskPeer(task) {
129
+ return task.participantRole === "executor" ? task.envelope.from.userId : task.envelope.to.userId;
130
+ }
131
+ function taskTitle(task) {
132
+ return (task.envelope.title || task.envelope.objective).trim();
133
+ }
134
+ function isActiveTask(task) {
135
+ return !TERMINAL.has(task.status);
136
+ }
137
+ function taskActivity(task) {
138
+ const peer = taskPeer(task);
139
+ const rolePrefix = task.participantRole === "executor" ? `来自 @${peer}` : `交给 @${peer}`;
140
+ const progress = task.progress;
141
+ if (task.status === "completed") {
142
+ return { label: `${rolePrefix} · 已完成`, detail: task.result?.summary || taskTitle(task), tone: "success" };
143
+ }
144
+ if (task.status === "failed" || task.status === "cancelled" || task.status === "expired") {
145
+ return {
146
+ label: `${rolePrefix} · ${STATUS_LABELS[task.status]}`,
147
+ detail: task.result?.summary || taskTitle(task),
148
+ tone: "failed"
149
+ };
150
+ }
151
+ if (task.status === "needs_context" || task.status === "cancelling") {
152
+ return { label: `${rolePrefix} · ${STATUS_LABELS[task.status]}`, detail: taskTitle(task), tone: "blocked" };
153
+ }
154
+ if (progress && (task.status === "accepted" || task.status === "running")) {
155
+ return {
156
+ label: `${rolePrefix} · ${PHASE_LABELS[progress.phase]}`,
157
+ detail: progress.message,
158
+ tone: "working",
159
+ current: progress.current,
160
+ total: progress.total
161
+ };
162
+ }
163
+ return {
164
+ label: `${rolePrefix} · ${STATUS_LABELS[task.status]}`,
165
+ detail: taskTitle(task),
166
+ tone: task.status === "running" || task.status === "accepted" ? "working" : "waiting"
167
+ };
168
+ }
169
+ function taskMenuLabel(task) {
170
+ const view = taskActivity(task);
171
+ const compactTitle = taskTitle(task).replace(/\s+/g, " ").slice(0, 28);
172
+ return `${view.label}|${compactTitle}${taskTitle(task).length > 28 ? "…" : ""}`;
173
+ }
104
174
  const SKIN_IDS = [
105
175
  "codex",
106
176
  "claude",
@@ -292,6 +362,12 @@ class PetManager {
292
362
  lastGreetAt = /* @__PURE__ */ new Map();
293
363
  timer;
294
364
  petsVisible = true;
365
+ localDuty = false;
366
+ remoteDuty = /* @__PURE__ */ new Set();
367
+ remoteDutyKnown = false;
368
+ localAgentConnectionStatus = "disconnected";
369
+ remoteAgentConnectionStatuses = /* @__PURE__ */ new Map();
370
+ observableTasks = /* @__PURE__ */ new Map();
295
371
  register(id, win, skin, identity) {
296
372
  this.pets.set(id, { id, win, skin, identity });
297
373
  win.on("closed", () => this.pets.delete(id));
@@ -318,6 +394,58 @@ class PetManager {
318
394
  else pet.win.hide();
319
395
  }
320
396
  }
397
+ setLocalDuty(active) {
398
+ if (this.localDuty === active) return;
399
+ this.localDuty = active;
400
+ this.publishActivities();
401
+ }
402
+ isLocalOnDuty() {
403
+ return this.localDuty;
404
+ }
405
+ setLocalAgentConnectionStatus(status2) {
406
+ if (this.localAgentConnectionStatus === status2) return;
407
+ this.localAgentConnectionStatus = status2;
408
+ this.publishActivities();
409
+ }
410
+ getLocalAgentConnectionStatus() {
411
+ return this.localAgentConnectionStatus;
412
+ }
413
+ setRemoteDuty(usernames) {
414
+ this.remoteDutyKnown = true;
415
+ this.remoteDuty = new Set(usernames.map((username) => username.trim().toLowerCase()));
416
+ this.publishActivities();
417
+ }
418
+ /** undefined 表示尚未收到实时 presence,调用方可回退到后台快照。 */
419
+ getRemoteDuty(username) {
420
+ if (!this.remoteDutyKnown) return void 0;
421
+ return this.remoteDuty.has(username.trim().toLowerCase());
422
+ }
423
+ resetRemoteDuty() {
424
+ this.remoteDutyKnown = false;
425
+ this.remoteDuty.clear();
426
+ this.publishActivities();
427
+ }
428
+ setRemoteAgentConnectionStatuses(statuses) {
429
+ this.remoteAgentConnectionStatuses = new Map(
430
+ Object.entries(statuses).map(([username, status2]) => [username.trim().toLowerCase(), status2])
431
+ );
432
+ this.publishActivities();
433
+ }
434
+ syncObservableTasks(tasks) {
435
+ this.observableTasks = new Map(tasks.map((task) => [task.taskId, task]));
436
+ this.publishActivities();
437
+ }
438
+ updateObservableTask(task) {
439
+ this.observableTasks.set(task.taskId, task);
440
+ this.publishActivities();
441
+ }
442
+ observableTaskCount() {
443
+ return [...this.observableTasks.values()].filter(isActiveTask).length;
444
+ }
445
+ replayActivity(petId) {
446
+ const pet = this.pets.get(petId);
447
+ if (pet && !pet.win.isDestroyed()) this.publishPetActivity(pet);
448
+ }
321
449
  findRemoteFriend(username) {
322
450
  const normalized = username.trim().toLowerCase();
323
451
  return [...this.pets.values()].find(
@@ -331,6 +459,52 @@ class PetManager {
331
459
  dispatch({ type: "identity", label: presentation.badge, kind: pet.identity.kind }, pet.id);
332
460
  }
333
461
  }
462
+ publishActivities() {
463
+ for (const pet of this.pets.values()) {
464
+ if (!pet.win.isDestroyed()) this.publishPetActivity(pet);
465
+ }
466
+ }
467
+ publishPetActivity(pet) {
468
+ const agentStatus = pet.identity.kind === "primary" ? this.localAgentConnectionStatus : this.remoteAgentConnectionStatuses.get(pet.identity.username.toLowerCase());
469
+ if (agentStatus === "update-required") {
470
+ dispatch({
471
+ type: "activity",
472
+ label: pet.identity.kind === "primary" ? "需要重连 Agent" : `@${pet.identity.username} 需要重连 Agent`,
473
+ detail: "Desktop Pet 已更新,请重启或重新加载 MCP 连接",
474
+ tone: "blocked"
475
+ }, pet.id);
476
+ return;
477
+ }
478
+ const now = Date.now();
479
+ const tasks = [...this.observableTasks.values()].filter((task2) => pet.identity.kind === "primary" || taskPeer(task2).toLowerCase() === pet.identity.username.toLowerCase()).filter((task2) => isActiveTask(task2) || now - Date.parse(task2.updatedAt) < 5 * 6e4).sort((a, b) => {
480
+ const activeDelta = Number(isActiveTask(b)) - Number(isActiveTask(a));
481
+ return activeDelta || Date.parse(b.updatedAt) - Date.parse(a.updatedAt);
482
+ });
483
+ const task = tasks[0];
484
+ if (task) {
485
+ dispatch({ type: "activity", ...taskActivity(task) }, pet.id);
486
+ return;
487
+ }
488
+ if (pet.identity.kind === "primary" && this.localDuty) {
489
+ dispatch({
490
+ type: "activity",
491
+ label: "驻守中 · 等待好友任务",
492
+ detail: "Agent 已连接,收到任务会立即领取",
493
+ tone: "waiting"
494
+ }, pet.id);
495
+ return;
496
+ }
497
+ if (pet.identity.kind === "remote-friend" && this.remoteDuty.has(pet.identity.username.toLowerCase())) {
498
+ dispatch({
499
+ type: "activity",
500
+ label: `@${pet.identity.username} 正在驻守`,
501
+ detail: "可以向 TA 派发任务",
502
+ tone: "waiting"
503
+ }, pet.id);
504
+ return;
505
+ }
506
+ dispatch({ type: "activity" }, pet.id);
507
+ }
334
508
  checkProximity() {
335
509
  const arr = [...this.pets.values()].filter((p) => !p.win.isDestroyed());
336
510
  for (let i = 0; i < arr.length; i++) {
@@ -355,6 +529,54 @@ function center(bounds) {
355
529
  return { x: bounds.x + bounds.width / 2, y: bounds.y + bounds.height / 2 };
356
530
  }
357
531
  const petManager = new PetManager();
532
+ const MAX_MENU_TASKS = 8;
533
+ const MENU_REFRESH_INTERVAL_MS = 5e3;
534
+ class BackgroundSnapshot {
535
+ constructor(now = Date.now) {
536
+ this.now = now;
537
+ }
538
+ entry;
539
+ pending;
540
+ read(key) {
541
+ return this.entry && Object.is(this.entry.key, key) ? this.entry.value : void 0;
542
+ }
543
+ refresh(key, loader, minIntervalMs = 0) {
544
+ if (this.entry && Object.is(this.entry.key, key) && this.now() - this.entry.refreshedAt < minIntervalMs) {
545
+ return Promise.resolve(this.entry.value);
546
+ }
547
+ if (this.pending && Object.is(this.pending.key, key)) return this.pending.promise;
548
+ const token = Symbol("menu-refresh");
549
+ const promise = Promise.resolve().then(loader).then((value) => {
550
+ if (this.pending?.token === token) {
551
+ this.entry = { key, value, refreshedAt: this.now() };
552
+ }
553
+ return value;
554
+ }).catch(() => this.read(key)).finally(() => {
555
+ if (this.pending?.token === token) this.pending = void 0;
556
+ });
557
+ this.pending = { key, token, promise };
558
+ return promise;
559
+ }
560
+ clear() {
561
+ this.entry = void 0;
562
+ this.pending = void 0;
563
+ }
564
+ }
565
+ function compactMenuText(value, maxLength) {
566
+ const compact = value.replace(/\s+/g, " ").trim();
567
+ if (compact.length <= maxLength) return compact;
568
+ if (maxLength <= 1) return compact.slice(0, maxLength);
569
+ return compact.slice(0, maxLength - 1).trimEnd() + "…";
570
+ }
571
+ function resolveFriendDuty(snapshotDuty, liveDuty) {
572
+ return liveDuty ?? snapshotDuty ?? false;
573
+ }
574
+ function menuTasks(tasks, limit = MAX_MENU_TASKS) {
575
+ return [...tasks].sort((a, b) => {
576
+ const activeDelta = Number(isActiveTask(b)) - Number(isActiveTask(a));
577
+ return activeDelta || Date.parse(b.updatedAt) - Date.parse(a.updatedAt);
578
+ }).slice(0, limit);
579
+ }
358
580
  const OFFICIAL_SERVER_ROOT = "https://api.xinbit.top/desktop-pet";
359
581
  const OFFICIAL_PORTAL_ORIGIN = "https://deskpalai.com";
360
582
  const LEGACY_OFFICIAL_SERVER_HOSTS = "47.94.20.104".split(",").map((host) => host.trim()).filter(Boolean);
@@ -461,6 +683,38 @@ function relayURLFromHost(value) {
461
683
  function isLocalHost(value) {
462
684
  return /^(localhost|127\.0\.0\.1|\[::1\])(?::\d+)?(?:\/|$)/i.test(value.trim());
463
685
  }
686
+ const INSTALLATION_ID_FILE = "update-installation-id";
687
+ 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;
688
+ let cachedInstallationID;
689
+ async function installationID() {
690
+ if (cachedInstallationID) return cachedInstallationID;
691
+ const path2 = node_path.join(electron.app.getPath("userData"), INSTALLATION_ID_FILE);
692
+ let replaceInvalidFile = false;
693
+ try {
694
+ const stored = (await promises.readFile(path2, "utf8")).trim();
695
+ if (UUID_PATTERN.test(stored)) {
696
+ cachedInstallationID = stored;
697
+ return stored;
698
+ }
699
+ replaceInvalidFile = true;
700
+ } catch (error) {
701
+ if (error.code !== "ENOENT") {
702
+ logger.warn("could not read installation id", { error: String(error) });
703
+ }
704
+ }
705
+ const created = node_crypto.randomUUID();
706
+ await promises.mkdir(electron.app.getPath("userData"), { recursive: true });
707
+ try {
708
+ await promises.writeFile(path2, created, { flag: replaceInvalidFile ? "w" : "wx", mode: 384 });
709
+ cachedInstallationID = created;
710
+ } catch (error) {
711
+ if (error.code !== "EEXIST") throw error;
712
+ const stored = (await promises.readFile(path2, "utf8")).trim();
713
+ if (!UUID_PATTERN.test(stored)) throw new Error("设备安装标识无效");
714
+ cachedInstallationID = stored;
715
+ }
716
+ return cachedInstallationID;
717
+ }
464
718
  const TASK_PROTOCOL_VERSION = "1.0";
465
719
  const WORKSPACE_SNAPSHOT_MIME = "application/vnd.desktop-pet.git-workspace-bundle";
466
720
  function taskParticipantRole(task, authenticatedUserId) {
@@ -541,9 +795,89 @@ const DEFAULT_EXECUTION_CAPABILITIES = [
541
795
  "browser",
542
796
  "document.create"
543
797
  ];
544
- function executionCapabilities(configured = process.env.DESKTOP_PET_EXECUTION_CAPABILITIES) {
545
- const source = configured?.trim() || DEFAULT_EXECUTION_CAPABILITIES.join(",");
546
- return [...new Set(source.split(",").map((value) => value.trim()).filter(Boolean))];
798
+ const CAPABILITY_ALIASES = {
799
+ "fs.read": "filesystem.read",
800
+ "file.read": "filesystem.read",
801
+ "files.read": "filesystem.read",
802
+ read_file: "filesystem.read",
803
+ "workspace.read": "filesystem.read",
804
+ "fs.write": "filesystem.write",
805
+ "file.write": "filesystem.write",
806
+ "files.write": "filesystem.write",
807
+ write_file: "filesystem.write",
808
+ edit_file: "filesystem.write",
809
+ apply_patch: "filesystem.write",
810
+ "workspace.write": "filesystem.write",
811
+ "workspace.modify": "filesystem.write",
812
+ terminal: "shell",
813
+ exec: "shell",
814
+ "shell.exec": "shell",
815
+ "command.execute": "shell",
816
+ fetch: "network",
817
+ http: "network",
818
+ "web.request": "network",
819
+ "web.browser": "browser",
820
+ "document.write": "document.create",
821
+ "docs.create": "document.create",
822
+ node: "toolchain.node",
823
+ go: "toolchain.go",
824
+ golang: "toolchain.go",
825
+ git: "toolchain.git",
826
+ python: "toolchain.python",
827
+ python3: "toolchain.python",
828
+ npm: "toolchain.npm",
829
+ pnpm: "toolchain.pnpm",
830
+ yarn: "toolchain.yarn",
831
+ bun: "toolchain.bun",
832
+ rust: "toolchain.rust",
833
+ cargo: "toolchain.cargo",
834
+ java: "toolchain.java",
835
+ maven: "toolchain.maven",
836
+ mvn: "toolchain.maven",
837
+ gradle: "toolchain.gradle"
838
+ };
839
+ const TOOLCHAIN_PROBES = [
840
+ ["git", "toolchain.git"],
841
+ ["go", "toolchain.go"],
842
+ ["python3", "toolchain.python"],
843
+ ["npm", "toolchain.npm"],
844
+ ["pnpm", "toolchain.pnpm"],
845
+ ["yarn", "toolchain.yarn"],
846
+ ["bun", "toolchain.bun"],
847
+ ["rustc", "toolchain.rust"],
848
+ ["cargo", "toolchain.cargo"],
849
+ ["java", "toolchain.java"],
850
+ ["mvn", "toolchain.maven"],
851
+ ["gradle", "toolchain.gradle"]
852
+ ];
853
+ let cachedDetectedCapabilities;
854
+ function commandAvailable(command) {
855
+ const result = node_child_process.spawnSync(command, ["--version"], {
856
+ stdio: "ignore",
857
+ timeout: 1e3
858
+ });
859
+ return !result.error && result.status === 0;
860
+ }
861
+ function detectEnvironmentCapabilities(probe = commandAvailable) {
862
+ return [
863
+ "toolchain.node",
864
+ ...TOOLCHAIN_PROBES.filter(([command]) => probe(command)).map(([, capability]) => capability)
865
+ ];
866
+ }
867
+ function detectedEnvironmentCapabilities() {
868
+ if (!cachedDetectedCapabilities) cachedDetectedCapabilities = detectEnvironmentCapabilities();
869
+ return cachedDetectedCapabilities;
870
+ }
871
+ function normalizeCapabilities(values) {
872
+ return [
873
+ ...new Set(
874
+ values.map((value) => value.trim().toLowerCase()).filter(Boolean).map((value) => CAPABILITY_ALIASES[value] ?? value)
875
+ )
876
+ ];
877
+ }
878
+ function executionCapabilities(configured = process.env.DESKTOP_PET_EXECUTION_CAPABILITIES, detected = detectedEnvironmentCapabilities(), additional = process.env.DESKTOP_PET_ADDITIONAL_CAPABILITIES) {
879
+ const base = configured?.trim() ? configured.split(",") : [...DEFAULT_EXECUTION_CAPABILITIES, ...detected];
880
+ return normalizeCapabilities([...base, ...additional?.split(",") ?? []]);
547
881
  }
548
882
  function showDesktopNotification(title, body) {
549
883
  if (!electron.Notification.isSupported()) return;
@@ -622,9 +956,15 @@ const resultWaiters = /* @__PURE__ */ new Map();
622
956
  const p2pSignalListeners = /* @__PURE__ */ new Set();
623
957
  const leaseTimers = /* @__PURE__ */ new Map();
624
958
  const taskSequences = /* @__PURE__ */ new Map();
959
+ const dutySessions = /* @__PURE__ */ new Set();
960
+ const pendingDutyStops = /* @__PURE__ */ new Map();
961
+ let dutyPublished = false;
962
+ let mcpConnectionStatus = "disconnected";
963
+ let observedMcpBridgeVersion;
625
964
  let ws = null;
626
965
  let connectPromise = null;
627
966
  let intentionalClose = false;
967
+ let authenticationBlocked = !allowDevelopmentOverrides && !authToken;
628
968
  let protocolV1Available = false;
629
969
  let workspaceSnapshotStagingAvailable = false;
630
970
  let mcpRegistration;
@@ -663,12 +1003,7 @@ function isWorkspaceSnapshotStagingAvailable() {
663
1003
  }
664
1004
  function getRelayHTTPAuth() {
665
1005
  if (!authToken) throw new Error("relay device access token is unavailable");
666
- const url = new URL(serverURL);
667
- url.protocol = url.protocol === "wss:" ? "https:" : "http:";
668
- url.pathname = "";
669
- url.search = "";
670
- url.hash = "";
671
- return { baseURL: url.toString().replace(/\/$/, ""), accessToken: authToken, deviceId: deviceID };
1006
+ return { baseURL: relayHTTPRootURL(serverURL), accessToken: authToken, deviceId: deviceID };
672
1007
  }
673
1008
  function configureRelayIdentity(config) {
674
1009
  if (ws || connectPromise) throw new Error("stop relay before changing identity");
@@ -677,6 +1012,7 @@ function configureRelayIdentity(config) {
677
1012
  deviceID = config.deviceId;
678
1013
  authToken = config.accessToken;
679
1014
  signingPrivateKey = config.signingPrivateKey ?? "";
1015
+ authenticationBlocked = false;
680
1016
  }
681
1017
  function clearRelayIdentity() {
682
1018
  if (ws || connectPromise) throw new Error("stop relay before clearing identity");
@@ -685,9 +1021,22 @@ function clearRelayIdentity() {
685
1021
  deviceID = allowDevelopmentOverrides ? process.env.DESKTOP_PET_DEVICE_ID?.trim() || "" : "";
686
1022
  authToken = allowDevelopmentOverrides ? process.env.DESKTOP_PET_TOKEN?.trim() : void 0;
687
1023
  signingPrivateKey = "";
688
- }
689
- async function registerMcpClient(agentHost, capabilities = {}) {
690
- mcpRegistration = { agentHost, capabilities };
1024
+ authenticationBlocked = !allowDevelopmentOverrides && !authToken;
1025
+ }
1026
+ function isRelayAuthenticationBlocked() {
1027
+ return authenticationBlocked;
1028
+ }
1029
+ async function registerMcpClient(agentHost, capabilities = {}, hostExecutionCapabilities) {
1030
+ const sanitizedExecutionCapabilities = hostExecutionCapabilities ? [
1031
+ ...new Set(
1032
+ hostExecutionCapabilities.map((value) => value.trim().toLowerCase()).filter((value) => /^[a-z0-9][a-z0-9._-]{0,63}$/.test(value))
1033
+ )
1034
+ ].slice(0, 100) : void 0;
1035
+ mcpRegistration = {
1036
+ agentHost,
1037
+ capabilities,
1038
+ executionCapabilities: sanitizedExecutionCapabilities
1039
+ };
691
1040
  await ensureConnected();
692
1041
  ws?.send(
693
1042
  JSON.stringify({
@@ -698,6 +1047,22 @@ async function registerMcpClient(agentHost, capabilities = {}) {
698
1047
  })
699
1048
  );
700
1049
  }
1050
+ function recordMcpBridgeActivity(version) {
1051
+ const nextStatus = version === MCP_BRIDGE_PROTOCOL_VERSION ? "ready" : "update-required";
1052
+ const changed = mcpConnectionStatus !== nextStatus || observedMcpBridgeVersion !== version;
1053
+ mcpConnectionStatus = nextStatus;
1054
+ observedMcpBridgeVersion = version;
1055
+ petManager.setLocalAgentConnectionStatus(nextStatus);
1056
+ if (changed) {
1057
+ logger.info("MCP bridge compatibility changed", {
1058
+ status: nextStatus,
1059
+ observedVersion: version ?? "missing",
1060
+ requiredVersion: MCP_BRIDGE_PROTOCOL_VERSION
1061
+ });
1062
+ publishDeviceCapabilities();
1063
+ }
1064
+ return nextStatus === "ready";
1065
+ }
701
1066
  function startRelay() {
702
1067
  intentionalClose = false;
703
1068
  retryAttempt = 0;
@@ -709,6 +1074,8 @@ function stopRelay() {
709
1074
  intentionalClose = true;
710
1075
  protocolV1Available = false;
711
1076
  workspaceSnapshotStagingAvailable = false;
1077
+ dutyPublished = false;
1078
+ petManager.resetRemoteDuty();
712
1079
  cancelRelayReconnect();
713
1080
  retryAttempt = 0;
714
1081
  connectPromise = null;
@@ -752,6 +1119,7 @@ function stopRelay() {
752
1119
  function ensureConnected() {
753
1120
  if (ws && ws.readyState === WebSocket.OPEN) return Promise.resolve();
754
1121
  if (connectPromise) return connectPromise;
1122
+ if (authenticationBlocked) return Promise.reject(relayAuthenticationError());
755
1123
  connectPromise = new Promise((resolve, reject) => {
756
1124
  let settled = false;
757
1125
  let authRejected = false;
@@ -781,6 +1149,7 @@ function ensureConnected() {
781
1149
  if (msg.task) msg.task = withParticipantRole(msg.task);
782
1150
  if (msg.tasks) msg.tasks = msg.tasks.map(withParticipantRole);
783
1151
  if (msg.type === "registered") {
1152
+ authenticationBlocked = false;
784
1153
  retryAttempt = 0;
785
1154
  protocolV1Available = msg.protocolVersion === TASK_PROTOCOL_VERSION;
786
1155
  workspaceSnapshotStagingAvailable = msg.capabilities?.workspaceSnapshotStaging === true;
@@ -789,6 +1158,11 @@ function ensureConnected() {
789
1158
  settled = true;
790
1159
  resolve();
791
1160
  }
1161
+ if (dutySessions.size > 0) void publishDutyState(true);
1162
+ void refreshObservableTasks();
1163
+ } else if (msg.type === "presence") {
1164
+ petManager.setRemoteDuty(msg.duty ?? []);
1165
+ petManager.setRemoteAgentConnectionStatuses(msg.agentStatuses ?? {});
792
1166
  } else if (msg.type === "error" && msg.messageId) {
793
1167
  const pending = pendingRPCs.get(msg.messageId);
794
1168
  if (pending) {
@@ -804,6 +1178,7 @@ function ensureConnected() {
804
1178
  clearTimeout(pending.timer);
805
1179
  pending.resolve(msg);
806
1180
  } else if (msg.type === "task.available") {
1181
+ if (msg.task) petManager.updateObservableTask(msg.task);
807
1182
  const sender = msg.from ?? msg.task?.envelope.from.userId;
808
1183
  if (sender) showRemoteFriendVisitor(sender, msg.skin, "有新的远端任务");
809
1184
  if (msg.taskId && sock.readyState === WebSocket.OPEN) {
@@ -827,9 +1202,10 @@ function ensureConnected() {
827
1202
  } else if (msg.type === "task.capability.unavailable") {
828
1203
  showCapabilityUnavailable(msg);
829
1204
  } else if (msg.type === "task.updated" && msg.task) {
1205
+ petManager.updateObservableTask(msg.task);
830
1206
  if (isTerminalTaskStatus(msg.task.status) || msg.task.status === "cancelling") stopLease(msg.task.taskId);
831
1207
  if (msg.task.status === "running" && msg.task.assignedDeviceId === deviceID) startLease(msg.task.taskId);
832
- showCapabilityGapResult(msg.task);
1208
+ showExecutionBlockerResult(msg.task);
833
1209
  notifyResultWaiters(msg.task);
834
1210
  } else if (msg.type.startsWith("p2p.") && msg.type !== "p2p.forwarded") {
835
1211
  if (!verifyP2PSignal(msg)) {
@@ -877,13 +1253,13 @@ function ensureConnected() {
877
1253
  }
878
1254
  });
879
1255
  sock.on("error", (err) => {
880
- if (/Unexpected server response: 40\d/.test(String(err))) authRejected = true;
1256
+ if (/Unexpected server response: (?:401|403)/.test(String(err))) {
1257
+ authRejected = true;
1258
+ if (ws === sock) authenticationBlocked = true;
1259
+ }
881
1260
  logger.warn("relay socket error", { error: String(err) });
882
1261
  if (!settled) {
883
1262
  settled = true;
884
- if (ws === sock) {
885
- connectPromise = null;
886
- }
887
1263
  reject(err);
888
1264
  }
889
1265
  });
@@ -905,6 +1281,7 @@ function ensureConnected() {
905
1281
  }
906
1282
  if (isCurrentSocket && !intentionalClose) {
907
1283
  if (authRejected) {
1284
+ authenticationBlocked = true;
908
1285
  logger.warn("relay rejected by server authentication; auto reconnect paused until identity changes", {
909
1286
  server: serverURL,
910
1287
  authenticated: Boolean(authToken)
@@ -917,6 +1294,13 @@ function ensureConnected() {
917
1294
  });
918
1295
  return connectPromise;
919
1296
  }
1297
+ function relayAuthenticationError() {
1298
+ const error = new Error(
1299
+ authToken ? "中转服务认证已失效,请重新登录桌宠账号" : "请先登录桌宠账号后再使用协作功能"
1300
+ );
1301
+ error.code = "RELAY_AUTH_REQUIRED";
1302
+ return error;
1303
+ }
920
1304
  async function askFriend(opts) {
921
1305
  await ensureConnected();
922
1306
  const questionId = crypto.randomUUID();
@@ -974,7 +1358,46 @@ async function askFriend(opts) {
974
1358
  }
975
1359
  async function getCollaborationState() {
976
1360
  const response = await relayRPC({ type: "help.list" }, 1e4);
977
- return { friends: response.friends ?? [], helpRequests: response.helpRequests ?? [] };
1361
+ const friends = response.friends ?? [];
1362
+ petManager.setRemoteDuty(
1363
+ friends.filter((friend) => friend.duty).map((friend) => friend.username)
1364
+ );
1365
+ petManager.setRemoteAgentConnectionStatuses(
1366
+ Object.fromEntries(
1367
+ friends.flatMap((friend) => friend.agentStatus ? [[friend.username, friend.agentStatus]] : [])
1368
+ )
1369
+ );
1370
+ const tasks = protocolV1Available ? await listRemoteTasks({ limit: 20 }).catch(() => []) : [];
1371
+ return { friends, helpRequests: response.helpRequests ?? [], tasks };
1372
+ }
1373
+ async function updateDutySession(sessionId, active) {
1374
+ const pendingStop = pendingDutyStops.get(sessionId);
1375
+ if (pendingStop) {
1376
+ clearTimeout(pendingStop);
1377
+ pendingDutyStops.delete(sessionId);
1378
+ }
1379
+ if (!active) {
1380
+ pendingDutyStops.set(sessionId, setTimeout(() => {
1381
+ pendingDutyStops.delete(sessionId);
1382
+ void updateDutySessionNow(sessionId, false);
1383
+ }, 2500));
1384
+ return;
1385
+ }
1386
+ await updateDutySessionNow(sessionId, true);
1387
+ }
1388
+ async function updateDutySessionNow(sessionId, active) {
1389
+ const before = dutySessions.size > 0;
1390
+ if (active) dutySessions.add(sessionId);
1391
+ else dutySessions.delete(sessionId);
1392
+ const after = dutySessions.size > 0;
1393
+ petManager.setLocalDuty(after);
1394
+ if (before !== after || dutyPublished !== after) {
1395
+ try {
1396
+ await publishDutyState(after);
1397
+ } catch (error) {
1398
+ logger.warn("relay does not accept duty presence yet", { error: String(error) });
1399
+ }
1400
+ }
978
1401
  }
979
1402
  async function requestFriendHelp(to) {
980
1403
  const response = await relayRPC({ type: "help.request", to }, 1e4);
@@ -1032,6 +1455,7 @@ async function delegateTask(envelope) {
1032
1455
  15e3
1033
1456
  );
1034
1457
  if (!response.task) throw new Error("relay did not return the created task");
1458
+ petManager.updateObservableTask(response.task);
1035
1459
  if (response.warning) {
1036
1460
  logger.warn("task created with delivery warning", { warning: response.warning });
1037
1461
  dispatch({ type: "state", state: "failed", ttl: 10 });
@@ -1046,6 +1470,7 @@ async function finalizeRemoteTaskWorkspace(taskId) {
1046
1470
  15e3
1047
1471
  );
1048
1472
  if (!response.task) throw new Error("relay did not return the finalized task");
1473
+ petManager.updateObservableTask(response.task);
1049
1474
  return { task: response.task, warning: response.warning };
1050
1475
  }
1051
1476
  function relayCapabilities() {
@@ -1056,19 +1481,35 @@ function relayCapabilities() {
1056
1481
  sampling: false,
1057
1482
  roots: true,
1058
1483
  tasks: true,
1059
- ...mcpRegistration?.capabilities ?? {}
1484
+ ...mcpRegistration?.capabilities ?? {},
1485
+ connectionStatus: mcpConnectionStatus,
1486
+ ...observedMcpBridgeVersion ? { bridgeProtocolVersion: observedMcpBridgeVersion } : {}
1060
1487
  },
1061
- execution: executionCapabilities(),
1488
+ execution: currentExecutionCapabilities(),
1062
1489
  platform: `${process.platform}-${process.arch}`
1063
1490
  };
1064
1491
  }
1492
+ function currentExecutionCapabilities() {
1493
+ return mcpRegistration?.executionCapabilities ?? executionCapabilities();
1494
+ }
1495
+ function publishDeviceCapabilities() {
1496
+ if (!ws || ws.readyState !== WebSocket.OPEN) return;
1497
+ ws.send(
1498
+ JSON.stringify({
1499
+ type: "device.capabilities",
1500
+ protocolVersion: TASK_PROTOCOL_VERSION,
1501
+ messageId: crypto.randomUUID(),
1502
+ capabilities: relayCapabilities()
1503
+ })
1504
+ );
1505
+ }
1065
1506
  function showCapabilityUnavailable(msg) {
1066
1507
  const required = msg.requiredCapabilities ?? [];
1067
- const available = new Set(executionCapabilities());
1508
+ const available = new Set(currentExecutionCapabilities());
1068
1509
  const missing = required.filter((capability) => !available.has(capability));
1069
1510
  const named = (missing.length > 0 ? missing : required).join("、") || "所需工具";
1070
1511
  const sender = msg.from ? `@${msg.from} 的` : "这项";
1071
- const text = `暂时无法帮忙:${sender}任务需要 ${named}。请在当前 Agent 中安装或启用对应工具后重新连接桌宠。`;
1512
+ const text = `暂时无法匹配:${sender}任务强制要求 ${named},但当前设备未通过对应能力声明。可让发起方改为 ignore,或补齐工具链/服务后重连。`;
1072
1513
  dispatch({ type: "state", state: "failed", ttl: 12 });
1073
1514
  dispatch({ type: "bubble", text, ttl: 18e3 });
1074
1515
  showDesktopNotification("桌宠暂时无法接单", text);
@@ -1079,6 +1520,7 @@ async function getRemoteTask(taskId) {
1079
1520
  1e4
1080
1521
  );
1081
1522
  if (!response.task) throw new Error("relay did not return task details");
1523
+ petManager.updateObservableTask(response.task);
1082
1524
  return response.task;
1083
1525
  }
1084
1526
  async function listRemoteTasks(options) {
@@ -1092,12 +1534,21 @@ async function listRemoteTasks(options) {
1092
1534
  },
1093
1535
  1e4
1094
1536
  );
1095
- return response.tasks ?? [];
1537
+ const tasks = response.tasks ?? [];
1538
+ if (!options.direction && !options.status) petManager.syncObservableTasks(tasks);
1539
+ return tasks;
1096
1540
  }
1097
1541
  async function claimRemoteTask(timeoutMs = 45e3) {
1098
1542
  const immediate = await claimOnce();
1099
1543
  if (immediate) {
1544
+ petManager.updateObservableTask(immediate);
1100
1545
  startLease(immediate.taskId);
1546
+ void reportRemoteTaskProgress(immediate.taskId, {
1547
+ phase: "receiving",
1548
+ message: "任务已领取,正在读取目标和交付约束",
1549
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
1550
+ }).catch(() => {
1551
+ });
1101
1552
  dispatch({ type: "state", state: "working", ttl: 30 });
1102
1553
  return immediate;
1103
1554
  }
@@ -1122,7 +1573,14 @@ async function claimRemoteTask(timeoutMs = 45e3) {
1122
1573
  if (!available) return null;
1123
1574
  const claimed = await claimOnce();
1124
1575
  if (claimed) {
1576
+ petManager.updateObservableTask(claimed);
1125
1577
  startLease(claimed.taskId);
1578
+ void reportRemoteTaskProgress(claimed.taskId, {
1579
+ phase: "receiving",
1580
+ message: "任务已领取,正在读取目标和交付约束",
1581
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
1582
+ }).catch(() => {
1583
+ });
1126
1584
  dispatch({ type: "state", state: "working", ttl: 30 });
1127
1585
  }
1128
1586
  return claimed;
@@ -1156,26 +1614,42 @@ async function submitRemoteTaskResult(result) {
1156
1614
  15e3
1157
1615
  );
1158
1616
  stopLease(result.taskId);
1617
+ void getRemoteTask(result.taskId).then((task) => petManager.updateObservableTask(task)).catch(() => {
1618
+ });
1159
1619
  dispatch({ type: "state", state: result.status === "completed" ? "success" : "failed", ttl: 12 });
1160
- const gap = result.diagnostics.find((diagnostic) => diagnostic.code === "CAPABILITY_UNAVAILABLE");
1161
- if (gap) {
1162
- dispatch({ type: "bubble", text: `暂时无法帮忙:${gap.message}`, ttl: 18e3 });
1163
- showDesktopNotification("桌宠暂时无法接单", gap.message);
1620
+ const blocker = result.diagnostics.find(
1621
+ (diagnostic) => diagnostic.code === "CAPABILITY_UNAVAILABLE" || diagnostic.code === "EXECUTION_BLOCKED"
1622
+ );
1623
+ if (blocker) {
1624
+ dispatch({ type: "bubble", text: `任务执行受阻:${blocker.message}`, ttl: 18e3 });
1625
+ showDesktopNotification("桌宠任务执行受阻", blocker.message);
1164
1626
  }
1165
1627
  }
1166
- function showCapabilityGapResult(task) {
1167
- const gap = task.result?.diagnostics.find((diagnostic) => diagnostic.code === "CAPABILITY_UNAVAILABLE");
1168
- if (!gap) return;
1169
- const text = `@${task.envelope.to.userId} 暂时无法帮助:${gap.message}`;
1628
+ async function reportRemoteTaskProgress(taskId, progress) {
1629
+ const response = await relayRPC(
1630
+ { type: "task.progress", protocolVersion: TASK_PROTOCOL_VERSION, taskId, payload: progress },
1631
+ 1e4
1632
+ );
1633
+ if (!response.task) throw new Error("relay did not return updated task progress");
1634
+ petManager.updateObservableTask(response.task);
1635
+ return response.task;
1636
+ }
1637
+ function showExecutionBlockerResult(task) {
1638
+ const blocker = task.result?.diagnostics.find(
1639
+ (diagnostic) => diagnostic.code === "CAPABILITY_UNAVAILABLE" || diagnostic.code === "EXECUTION_BLOCKED"
1640
+ );
1641
+ if (!blocker) return;
1642
+ const text = `@${task.envelope.to.userId} 执行受阻:${blocker.message}`;
1170
1643
  dispatch({ type: "state", state: "failed", ttl: 12 });
1171
1644
  dispatch({ type: "bubble", text, ttl: 18e3 });
1172
- showDesktopNotification("桌宠任务无法完成", text);
1645
+ showDesktopNotification("桌宠任务执行受阻", text);
1173
1646
  }
1174
1647
  async function cancelRemoteTask(taskId) {
1175
1648
  const response = await relayRPC(
1176
1649
  { type: "task.cancel", protocolVersion: TASK_PROTOCOL_VERSION, taskId },
1177
1650
  1e4
1178
1651
  );
1652
+ if (response.task) petManager.updateObservableTask(response.task);
1179
1653
  return response.task;
1180
1654
  }
1181
1655
  function onP2PSignal(listener) {
@@ -1193,6 +1667,7 @@ async function requestRemoteTaskContext(taskId, request) {
1193
1667
  { type: "task.context.request", protocolVersion: TASK_PROTOCOL_VERSION, taskId, payload: request },
1194
1668
  1e4
1195
1669
  );
1670
+ if (response.task) petManager.updateObservableTask(response.task);
1196
1671
  return response.task;
1197
1672
  }
1198
1673
  async function supplyRemoteTaskContext(taskId, items) {
@@ -1200,6 +1675,7 @@ async function supplyRemoteTaskContext(taskId, items) {
1200
1675
  { type: "task.context.supply", protocolVersion: TASK_PROTOCOL_VERSION, taskId, payload: { items } },
1201
1676
  15e3
1202
1677
  );
1678
+ if (response.task) petManager.updateObservableTask(response.task);
1203
1679
  return response.task;
1204
1680
  }
1205
1681
  async function claimOnce() {
@@ -1244,6 +1720,8 @@ function notifyResultWaiters(task) {
1244
1720
  }
1245
1721
  function startLease(taskId) {
1246
1722
  stopLease(taskId);
1723
+ void relayRPC({ type: "task.lease", protocolVersion: TASK_PROTOCOL_VERSION, taskId }, 1e4).catch(() => {
1724
+ });
1247
1725
  const timer = setInterval(() => {
1248
1726
  void relayRPC({ type: "task.lease", protocolVersion: TASK_PROTOCOL_VERSION, taskId }, 1e4).catch(() => {
1249
1727
  stopLease(taskId);
@@ -1251,6 +1729,16 @@ function startLease(taskId) {
1251
1729
  }, 3e4);
1252
1730
  leaseTimers.set(taskId, timer);
1253
1731
  }
1732
+ async function publishDutyState(active) {
1733
+ await relayRPC({ type: "duty.update", status: active ? "active" : "inactive" }, 1e4);
1734
+ dutyPublished = active;
1735
+ }
1736
+ async function refreshObservableTasks() {
1737
+ try {
1738
+ await listRemoteTasks({ limit: 20 });
1739
+ } catch {
1740
+ }
1741
+ }
1254
1742
  function stopLease(taskId) {
1255
1743
  const timer = leaseTimers.get(taskId);
1256
1744
  if (timer) clearInterval(timer);
@@ -1352,13 +1840,13 @@ async function activateStoredDevice() {
1352
1840
  return true;
1353
1841
  }
1354
1842
  async function configureAndActivateDevice(credentials) {
1355
- const signedCredentials = withSigningKeys(credentials);
1843
+ const signedCredentials = { ...credentials, ...ensureDeviceKeyMaterial(credentials) };
1356
1844
  saveDeviceCredentials(signedCredentials);
1357
1845
  await activateDevice(signedCredentials, true);
1358
1846
  }
1359
1847
  async function activateDevice(credentials, restart) {
1360
1848
  if (!credentials.publicKey || !credentials.privateKey || !credentials.encryptionPublicKey || !credentials.encryptionPrivateKey) {
1361
- credentials = withSigningKeys(credentials);
1849
+ credentials = { ...credentials, ...ensureDeviceKeyMaterial(credentials) };
1362
1850
  saveDeviceCredentials(credentials);
1363
1851
  }
1364
1852
  const base = credentials.serverUrl.replace(/\/$/, "");
@@ -1368,6 +1856,7 @@ async function activateDevice(credentials, restart) {
1368
1856
  body: JSON.stringify({
1369
1857
  deviceId: credentials.deviceId,
1370
1858
  refreshToken: credentials.refreshToken,
1859
+ installationId: await installationID(),
1371
1860
  publicKey: credentials.publicKey,
1372
1861
  encryptionPublicKey: credentials.encryptionPublicKey
1373
1862
  })
@@ -1406,29 +1895,35 @@ async function activateDevice(credentials, restart) {
1406
1895
  );
1407
1896
  }, refreshAt);
1408
1897
  }
1409
- function withSigningKeys(credentials) {
1410
- let next = credentials;
1411
- if (!next.publicKey || !next.privateKey) {
1412
- const { publicKey, privateKey } = node_crypto.generateKeyPairSync("ed25519", {
1898
+ function ensureDeviceKeyMaterial(credentials = {}) {
1899
+ let publicKey = credentials.publicKey;
1900
+ let privateKey = credentials.privateKey;
1901
+ let encryptionPublicKey = credentials.encryptionPublicKey;
1902
+ let encryptionPrivateKey = credentials.encryptionPrivateKey;
1903
+ if (!publicKey || !privateKey) {
1904
+ const generated = node_crypto.generateKeyPairSync("ed25519", {
1413
1905
  publicKeyEncoding: { type: "spki", format: "pem" },
1414
1906
  privateKeyEncoding: { type: "pkcs8", format: "pem" }
1415
1907
  });
1416
- next = { ...next, publicKey, privateKey };
1908
+ publicKey = generated.publicKey;
1909
+ privateKey = generated.privateKey;
1417
1910
  }
1418
- if (!next.encryptionPublicKey || !next.encryptionPrivateKey) {
1419
- const { publicKey, privateKey } = node_crypto.generateKeyPairSync("rsa", {
1911
+ if (!encryptionPublicKey || !encryptionPrivateKey) {
1912
+ const generated = node_crypto.generateKeyPairSync("rsa", {
1420
1913
  modulusLength: 2048,
1421
1914
  publicKeyEncoding: { type: "spki", format: "pem" },
1422
1915
  privateKeyEncoding: { type: "pkcs8", format: "pem" }
1423
1916
  });
1424
- next = { ...next, encryptionPublicKey: publicKey, encryptionPrivateKey: privateKey };
1917
+ encryptionPublicKey = generated.publicKey;
1918
+ encryptionPrivateKey = generated.privateKey;
1425
1919
  }
1426
- return next;
1920
+ return { publicKey, privateKey, encryptionPublicKey, encryptionPrivateKey };
1427
1921
  }
1428
1922
  const deviceAuth = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
1429
1923
  __proto__: null,
1430
1924
  activateStoredDevice,
1431
1925
  configureAndActivateDevice,
1926
+ ensureDeviceKeyMaterial,
1432
1927
  getDeviceIceServers
1433
1928
  }, Symbol.toStringTag, { value: "Module" }));
1434
1929
  function defaultAccountServerURL(relayURL = configuredRelayURL()) {
@@ -1454,11 +1949,16 @@ async function bindDesktopPetToWebSession(server, accessToken) {
1454
1949
  if (!accessToken) throw new Error("网页没有返回有效的登录会话");
1455
1950
  const session = await get(serverUrl, "/api/auth/me", accessToken);
1456
1951
  if (!session.user?.username) throw new Error("服务器没有返回有效的账号信息");
1952
+ const previousCredentials = reusableCredentials(serverUrl, session.user.username);
1953
+ const keyMaterial = ensureDeviceKeyMaterial(previousCredentials);
1457
1954
  const device = await post(
1458
1955
  serverUrl,
1459
1956
  "/api/devices",
1460
1957
  {
1461
1958
  name: `Desktop Pet · ${node_os.hostname()}`,
1959
+ installationId: await installationID(),
1960
+ publicKey: keyMaterial.publicKey,
1961
+ encryptionPublicKey: keyMaterial.encryptionPublicKey,
1462
1962
  capabilities: {
1463
1963
  agentHost: process.env.DESKTOP_PET_AGENT_HOST || "unknown-mcp-host",
1464
1964
  mcp: { tools: true, sampling: false, roots: true, tasks: true },
@@ -1473,10 +1973,20 @@ async function bindDesktopPetToWebSession(server, accessToken) {
1473
1973
  serverUrl,
1474
1974
  userId: session.user.username,
1475
1975
  deviceId: device.device.id,
1476
- refreshToken: device.refreshToken
1976
+ refreshToken: device.refreshToken,
1977
+ ...keyMaterial
1477
1978
  });
1478
1979
  return { username: session.user.username };
1479
1980
  }
1981
+ function reusableCredentials(serverUrl, username) {
1982
+ try {
1983
+ const credentials = loadDeviceCredentials();
1984
+ if (!credentials || credentials.userId.toLowerCase() !== username.toLowerCase()) return void 0;
1985
+ return normalizeServerURL(credentials.serverUrl) === serverUrl ? credentials : void 0;
1986
+ } catch {
1987
+ return void 0;
1988
+ }
1989
+ }
1480
1990
  function normalizeServerURL(value) {
1481
1991
  let url;
1482
1992
  try {
@@ -1890,7 +2400,7 @@ async function checkForUpdates(currentVersion = electron.app.getVersion()) {
1890
2400
  endpoint.searchParams.set("arch", process.arch);
1891
2401
  endpoint.searchParams.set("currentVersion", currentVersion);
1892
2402
  endpoint.searchParams.set("channel", updateChannel());
1893
- endpoint.searchParams.set("installationId", await updateInstallationID());
2403
+ endpoint.searchParams.set("installationId", await installationID());
1894
2404
  const response = await electron.net.fetch(endpoint.toString(), {
1895
2405
  headers: { accept: "application/json" },
1896
2406
  signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
@@ -2389,34 +2899,6 @@ function updateChannel() {
2389
2899
  const channel = process.env.DESKTOP_PET_UPDATE_CHANNEL?.trim() || "stable";
2390
2900
  return /^[a-z][a-z0-9-]{0,31}$/.test(channel) ? channel : "stable";
2391
2901
  }
2392
- let cachedInstallationID;
2393
- async function updateInstallationID() {
2394
- if (cachedInstallationID) return cachedInstallationID;
2395
- const path2 = node_path.join(electron.app.getPath("userData"), "update-installation-id");
2396
- let replaceInvalidFile = false;
2397
- try {
2398
- const stored = (await promises.readFile(path2, "utf8")).trim();
2399
- if (/^[0-9a-f-]{36}$/i.test(stored)) {
2400
- cachedInstallationID = stored;
2401
- return stored;
2402
- }
2403
- replaceInvalidFile = true;
2404
- } catch (error) {
2405
- if (error.code !== "ENOENT") {
2406
- logger.warn("could not read update installation id", { error: String(error) });
2407
- }
2408
- }
2409
- const created = node_crypto.randomUUID();
2410
- await promises.mkdir(electron.app.getPath("userData"), { recursive: true });
2411
- try {
2412
- await promises.writeFile(path2, created, { flag: replaceInvalidFile ? "w" : "wx", mode: 384 });
2413
- cachedInstallationID = created;
2414
- } catch (error) {
2415
- if (error.code !== "EEXIST") throw error;
2416
- cachedInstallationID = (await promises.readFile(path2, "utf8")).trim();
2417
- }
2418
- return cachedInstallationID;
2419
- }
2420
2902
  function asString(value) {
2421
2903
  return typeof value === "string" ? value.trim() : "";
2422
2904
  }
@@ -2454,6 +2936,7 @@ function humanizeUpdateError(error) {
2454
2936
  return "暂时无法检查更新";
2455
2937
  }
2456
2938
  const SKINS = SKIN_IDS.map((id) => ({ id, label: SKIN_LABELS[id] }));
2939
+ const collaborationSnapshot = new BackgroundSnapshot();
2457
2940
  let nextId = 1;
2458
2941
  function createPetWindow(skin, options) {
2459
2942
  if (petManager.count() >= MAX_PETS) {
@@ -2467,6 +2950,7 @@ function createPetWindow(skin, options) {
2467
2950
  const initialSize = PET_WINDOW_BY_SKIN[skin] ?? PET_WINDOW;
2468
2951
  let W = initialSize.width;
2469
2952
  let H = initialSize.height;
2953
+ let menuOpen = false;
2470
2954
  const x = options.near?.x ?? workArea.x + workArea.width - W - 120;
2471
2955
  const y = options.near?.y ?? workArea.y + workArea.height - H - 60;
2472
2956
  const win = new electron.BrowserWindow({
@@ -2501,6 +2985,8 @@ function createPetWindow(skin, options) {
2501
2985
  dispatch({ type: "skin", skin: currentSkin }, id);
2502
2986
  const presentation = publishIdentity();
2503
2987
  dispatch({ type: "bubble", text: presentation.greeting }, id);
2988
+ petManager.replayActivity(id);
2989
+ refreshCollaborationSnapshot();
2504
2990
  });
2505
2991
  const publishIdentity = () => {
2506
2992
  const presentation = presentPetIdentity(identity, currentAccountUsername());
@@ -2518,17 +3004,30 @@ function createPetWindow(skin, options) {
2518
3004
  }
2519
3005
  };
2520
3006
  const onMenu = (e) => {
2521
- if (win.isDestroyed() || e.sender !== win.webContents) return;
2522
- void showPetMenu();
3007
+ if (win.isDestroyed() || e.sender !== win.webContents || menuOpen) return;
3008
+ showPetMenu();
2523
3009
  };
2524
3010
  const onMousePassthrough = (e, enabled) => {
2525
3011
  if (win.isDestroyed() || e.sender !== win.webContents || typeof enabled !== "boolean") return;
2526
3012
  win.setIgnoreMouseEvents(enabled, { forward: enabled });
2527
3013
  };
2528
- const showPetMenu = async () => {
3014
+ const refreshCollaborationSnapshot = () => {
3015
+ const username = currentAccountUsername();
3016
+ if (!username) {
3017
+ collaborationSnapshot.clear();
3018
+ return;
3019
+ }
3020
+ void collaborationSnapshot.refresh(
3021
+ username,
3022
+ getCollaborationState,
3023
+ MENU_REFRESH_INTERVAL_MS
3024
+ );
3025
+ };
3026
+ const showPetMenu = () => {
2529
3027
  const accountUsername = currentAccountUsername();
2530
3028
  const identityPresentation = publishIdentity();
2531
- const collaboration = accountUsername ? await loadCollaborationState() : void 0;
3029
+ const collaboration = accountUsername ? collaborationSnapshot.read(accountUsername) : void 0;
3030
+ refreshCollaborationSnapshot();
2532
3031
  if (win.isDestroyed()) return;
2533
3032
  const incoming = collaboration?.helpRequests.filter(
2534
3033
  (request) => request.toUsername === accountUsername && request.status === "pending"
@@ -2536,6 +3035,7 @@ function createPetWindow(skin, options) {
2536
3035
  const friendItems = collaboration?.friends.map(
2537
3036
  (friend) => friendMenuItem(friend, collaboration, accountUsername ?? "", id)
2538
3037
  ) ?? [];
3038
+ const activeTasks = collaboration?.tasks.filter(isActiveTask) ?? [];
2539
3039
  const menu = electron.Menu.buildFromTemplate([
2540
3040
  {
2541
3041
  label: identityPresentation.menuHeader,
@@ -2574,10 +3074,23 @@ function createPetWindow(skin, options) {
2574
3074
  }))
2575
3075
  },
2576
3076
  { type: "separator" },
3077
+ {
3078
+ label: "协作动态(" + activeTasks.length + " 项进行中)",
3079
+ enabled: Boolean(accountUsername),
3080
+ submenu: collaboration ? collaborationMenuItems(collaboration.tasks) : [
3081
+ {
3082
+ label: petManager.getLocalAgentConnectionStatus() === "update-required" ? "本机 Agent 需要重连以加载新版 MCP" : isRelayAuthenticationBlocked() ? "账号连接已失效,请重新登录" : petManager.isLocalOnDuty() ? "本机正在驻守 · 服务器状态暂不可用" : "正在刷新协作状态,请稍后再打开菜单",
3083
+ enabled: false
3084
+ }
3085
+ ]
3086
+ },
2577
3087
  {
2578
3088
  label: `好友状态${collaboration ? `(${collaboration.friends.length})` : ""}`,
2579
3089
  enabled: Boolean(accountUsername),
2580
- submenu: collaboration ? friendItems.length > 0 ? friendItems : [{ label: "还没有好友,请先到个人后台添加", enabled: false }] : [{ label: accountUsername ? "暂时无法连接服务器" : "请先登录账号", enabled: false }]
3090
+ submenu: collaboration ? friendItems.length > 0 ? friendItems : [{ label: "还没有好友,请先到个人后台添加", enabled: false }] : [{
3091
+ label: accountUsername ? isRelayAuthenticationBlocked() ? "账号认证已失效,请点击上方账号重新登录" : "正在刷新好友状态,请稍后再打开菜单" : "请先登录账号",
3092
+ enabled: false
3093
+ }]
2581
3094
  },
2582
3095
  {
2583
3096
  label: `要我帮忙${incoming.length ? `(${incoming.length})` : ""}`,
@@ -2621,6 +3134,10 @@ function createPetWindow(skin, options) {
2621
3134
  ...canSendAwayPet(identity) ? [{ label: "送走这只宠物", click: () => win.close() }] : [],
2622
3135
  { label: "退出", click: () => electron.app.quit() }
2623
3136
  ]);
3137
+ menuOpen = true;
3138
+ menu.once("menu-will-close", () => {
3139
+ menuOpen = false;
3140
+ });
2624
3141
  menu.popup({ window: win });
2625
3142
  };
2626
3143
  electron.ipcMain.on(PET_MOVE, onMove);
@@ -2706,17 +3223,8 @@ function updateMenuItems(petId) {
2706
3223
  }
2707
3224
  return items;
2708
3225
  }
2709
- async function loadCollaborationState() {
2710
- try {
2711
- return await Promise.race([
2712
- getCollaborationState(),
2713
- new Promise((resolve) => setTimeout(() => resolve(void 0), 1800))
2714
- ]);
2715
- } catch {
2716
- return void 0;
2717
- }
2718
- }
2719
3226
  function friendMenuItem(friend, state, currentUsername, petId) {
3227
+ 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];
2720
3228
  const outboundHelp = state.helpRequests.find(
2721
3229
  (request) => request.fromUsername === currentUsername && request.toUsername === friend.username
2722
3230
  );
@@ -2724,10 +3232,17 @@ function friendMenuItem(friend, state, currentUsername, petId) {
2724
3232
  (request) => request.status === "accepted" && (request.fromUsername === currentUsername && request.toUsername === friend.username || request.fromUsername === friend.username && request.toUsername === currentUsername)
2725
3233
  );
2726
3234
  const helpLabel = outboundHelp?.status === "accepted" ? " · TA 已同意帮我" : outboundHelp?.status === "pending" ? " · 求助待 TA 接受" : activeHelp ? " · 我正在帮 TA" : "";
3235
+ const friendOnDuty = resolveFriendDuty(friend.duty, petManager.getRemoteDuty(friend.username));
3236
+ const availability = friend.agentStatus === "update-required" ? " · Agent 需重连" : activeTask ? " · " + taskActivity(activeTask).label.split(" · ").at(-1) : friendOnDuty ? " · 已驻守" : friend.online && friend.agentStatus === "disconnected" ? " · Agent 未连接" : friend.online ? " · 在线,未驻守" : " · 离线";
2727
3237
  const chatMessages = ["你好呀!", "现在方便聊聊吗?", "谢谢你的帮助!"];
2728
3238
  return {
2729
- label: `${friend.online ? "●" : "○"} ${friend.displayName} (@${friend.username})${helpLabel}`,
3239
+ label: friend.displayName + " (@" + friend.username + ")" + availability + helpLabel,
2730
3240
  submenu: [
3241
+ {
3242
+ 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 尚未驻守" : "桌宠当前离线",
3243
+ enabled: false
3244
+ },
3245
+ { type: "separator" },
2731
3246
  {
2732
3247
  label: outboundHelp?.status === "accepted" ? "TA 已同意帮我(可让 Agent 派任务)" : outboundHelp?.status === "pending" ? "求助已发出,等 TA 接受" : "请 TA 帮我",
2733
3248
  enabled: outboundHelp?.status !== "accepted" && outboundHelp?.status !== "pending",
@@ -2767,6 +3282,35 @@ function friendMenuItem(friend, state, currentUsername, petId) {
2767
3282
  ]
2768
3283
  };
2769
3284
  }
3285
+ function collaborationMenuItems(tasks) {
3286
+ const duty = {
3287
+ label: petManager.getLocalAgentConnectionStatus() === "update-required" ? "本机 Agent:需要重连以加载新版 MCP" : petManager.isLocalOnDuty() ? "本机 Agent:驻守中,正在等待任务" : petManager.getLocalAgentConnectionStatus() === "disconnected" ? "本机 Agent:尚未连接 MCP" : "本机 Agent:未驻守",
3288
+ enabled: false
3289
+ };
3290
+ if (tasks.length === 0) {
3291
+ return [duty, { type: "separator" }, { label: "还没有远程任务记录", enabled: false }];
3292
+ }
3293
+ return [
3294
+ duty,
3295
+ { type: "separator" },
3296
+ ...menuTasks(tasks).map((task) => {
3297
+ const view = taskActivity(task);
3298
+ const measured = view.current != null && view.total != null ? " · " + view.current + "/" + view.total : "";
3299
+ const summary = task.result?.summary ? compactMenuText(task.result.summary, 80) : void 0;
3300
+ return {
3301
+ label: taskMenuLabel(task),
3302
+ submenu: [
3303
+ { label: "状态:" + view.label + measured, enabled: false },
3304
+ { label: "当前步骤:" + compactMenuText(view.detail, 120), enabled: false },
3305
+ { label: "协作好友:@" + taskPeer(task), enabled: false },
3306
+ { label: "任务:" + compactMenuText(taskTitle(task), 90), enabled: false },
3307
+ { label: "最近更新:" + new Date(task.updatedAt).toLocaleString(), enabled: false },
3308
+ ...summary ? [{ label: "结果:" + summary, enabled: false }] : []
3309
+ ]
3310
+ };
3311
+ })
3312
+ ];
3313
+ }
2770
3314
  function runMenuAction(action, successText, petId) {
2771
3315
  void action.then(() => {
2772
3316
  dispatch({ type: "state", state: "excited", ttl: 6 }, petId);
@@ -2806,6 +3350,7 @@ const ErrorCodes = {
2806
3350
  UNKNOWN_TYPE: "UNKNOWN_TYPE",
2807
3351
  INTERNAL: "INTERNAL",
2808
3352
  UNSUPPORTED_PROTOCOL: "UNSUPPORTED_PROTOCOL",
3353
+ MCP_UPDATE_REQUIRED: "MCP_UPDATE_REQUIRED",
2809
3354
  TASK_NOT_FOUND: "TASK_NOT_FOUND"
2810
3355
  };
2811
3356
  const ERROR_CODE_VALUES = new Set(Object.values(ErrorCodes));
@@ -2888,7 +3433,7 @@ function optString(v, max) {
2888
3433
  function isRelayCommand(raw) {
2889
3434
  if (!raw || typeof raw !== "object") return false;
2890
3435
  const t = raw.type;
2891
- return t === "ask" || t === "list_tasks" || t === "wait_task" || t === "answer" || t === "delegate_task" || t === "get_task" || t === "wait_task_result" || t === "claim_task" || t === "submit_task_result" || t === "cancel_task" || t === "list_remote_tasks" || t === "request_task_context" || t === "supply_task_context" || t === "configure_device" || t === "register_mcp_client" || t === "upload_relay_artifact" || t === "download_relay_artifact";
3436
+ return t === "ask" || t === "list_tasks" || t === "wait_task" || t === "answer" || t === "delegate_task" || t === "get_task" || t === "wait_task_result" || t === "claim_task" || t === "submit_task_result" || t === "cancel_task" || t === "list_remote_tasks" || t === "request_task_context" || t === "supply_task_context" || t === "report_task_progress" || t === "configure_device" || t === "register_mcp_client" || t === "upload_relay_artifact" || t === "download_relay_artifact";
2892
3437
  }
2893
3438
  const RELAY_BLOB_TRANSFER_TIMEOUT_MS = 10 * 6e4;
2894
3439
  async function uploadRelayBlob(input) {
@@ -2924,7 +3469,7 @@ async function uploadRelayBlob(input) {
2924
3469
  signal: AbortSignal.timeout(RELAY_BLOB_TRANSFER_TIMEOUT_MS)
2925
3470
  }
2926
3471
  );
2927
- if (!response.ok) throw new Error(`relay blob upload failed (${response.status})`);
3472
+ if (!response.ok) throw await relayBlobHTTPError("upload", response);
2928
3473
  return {
2929
3474
  id: artifactId,
2930
3475
  kind: normalizedKind,
@@ -2949,7 +3494,7 @@ async function downloadRelayBlob(taskId, artifact) {
2949
3494
  signal: AbortSignal.timeout(RELAY_BLOB_TRANSFER_TIMEOUT_MS)
2950
3495
  }
2951
3496
  );
2952
- if (!response.ok) throw new Error(`relay blob download failed (${response.status})`);
3497
+ if (!response.ok) throw await relayBlobHTTPError("download", response);
2953
3498
  const metadataEncoded = response.headers.get("x-artifact-metadata") ?? "";
2954
3499
  const metadata = JSON.parse(Buffer.from(metadataEncoded, "base64url").toString("utf8"));
2955
3500
  const bytes = decryptForDevice(new Uint8Array(await response.arrayBuffer()), metadata.keyWraps, auth.deviceId, credentials.encryptionPrivateKey);
@@ -2989,6 +3534,28 @@ function decryptForDevice(ciphertext, wraps, deviceId, privateKey) {
2989
3534
  function normalizeResultKind(kind) {
2990
3535
  return kind === "text" || kind === "markdown" || kind === "patch" ? kind : "file";
2991
3536
  }
3537
+ async function relayBlobHTTPError(operation, response) {
3538
+ let detail = "";
3539
+ try {
3540
+ const body = await response.text();
3541
+ if (body.trim()) {
3542
+ try {
3543
+ const decoded = JSON.parse(body);
3544
+ detail = typeof decoded.message === "string" ? decoded.message : body;
3545
+ } catch {
3546
+ detail = body.replace(/<[^>]*>/g, " ");
3547
+ }
3548
+ detail = detail.replace(/\s+/g, " ").trim().slice(0, 300);
3549
+ }
3550
+ } catch {
3551
+ }
3552
+ const server = response.headers.get("server")?.trim();
3553
+ const suffix = [server ? `server=${server}` : "", detail].filter(Boolean).join("; ");
3554
+ return new AppError(
3555
+ ErrorCodes.RELAY_UNREACHABLE,
3556
+ `relay blob ${operation} failed (${response.status})${suffix ? `: ${suffix}` : ""}`
3557
+ );
3558
+ }
2992
3559
  function writeJSON(res, status2, body) {
2993
3560
  res.writeHead(status2, { "content-type": "application/json" });
2994
3561
  res.end(JSON.stringify(body));
@@ -3005,6 +3572,7 @@ function fail(res, status2, message, errorCode) {
3005
3572
  }
3006
3573
  class BodyTooLargeError extends Error {
3007
3574
  }
3575
+ const MCP_UPDATE_REQUIRED_MESSAGE = `Desktop Pet 已更新,当前 Agent 的 MCP 连接版本过旧(需要 ${MCP_BRIDGE_PROTOCOL_VERSION})。请重启或重新加载 Agent 的 MCP 连接,然后再次开始驻守。`;
3008
3576
  function hasJSONContentType(req) {
3009
3577
  const value = req.headers["content-type"];
3010
3578
  return typeof value === "string" && value.toLowerCase().startsWith("application/json");
@@ -3066,6 +3634,11 @@ function startEventServer(appVersion = "0.0.0") {
3066
3634
  try {
3067
3635
  const raw = JSON.parse(await readBody(req, LOCAL_PET_MAX_BODY_BYTES));
3068
3636
  if (isRelayCommand(raw)) {
3637
+ const compatible = recordMcpBridgeActivity(raw.mcpBridgeProtocolVersion);
3638
+ if (!compatible && (raw.type === "wait_task" || raw.type === "register_mcp_client")) {
3639
+ fail(res, 409, MCP_UPDATE_REQUIRED_MESSAGE, ErrorCodes.MCP_UPDATE_REQUIRED);
3640
+ return;
3641
+ }
3069
3642
  await handleRelay(raw, res);
3070
3643
  return;
3071
3644
  }
@@ -3117,7 +3690,7 @@ function startEventServer(appVersion = "0.0.0") {
3117
3690
  }
3118
3691
  function relayErrorHTTPStatus(code) {
3119
3692
  if (code === ErrorCodes.TASK_NOT_FOUND || code === ErrorCodes.UNKNOWN_QUESTION) return 404;
3120
- if (code === ErrorCodes.HELP_NOT_ACCEPTED || code === ErrorCodes.NOT_FRIENDS || code === ErrorCodes.USER_NOT_ONLINE || code === ErrorCodes.QUESTION_NOT_ASSIGNED) {
3693
+ if (code === ErrorCodes.HELP_NOT_ACCEPTED || code === ErrorCodes.NOT_FRIENDS || code === ErrorCodes.USER_NOT_ONLINE || code === ErrorCodes.QUESTION_NOT_ASSIGNED || code === ErrorCodes.MCP_UPDATE_REQUIRED) {
3121
3694
  return 409;
3122
3695
  }
3123
3696
  return 400;
@@ -3150,6 +3723,7 @@ async function handleRelay(cmd, res) {
3150
3723
  },
3151
3724
  executionPolicy: {
3152
3725
  capabilities: ["filesystem.read"],
3726
+ capabilityMode: "ignore",
3153
3727
  writableScopes: [],
3154
3728
  networkPolicy: "ask",
3155
3729
  approvalMode: "manual",
@@ -3196,14 +3770,20 @@ async function handleRelay(cmd, res) {
3196
3770
  return;
3197
3771
  }
3198
3772
  case "wait_task": {
3199
- if (isProtocolV1Available()) {
3200
- const taskV1 = await claimRemoteTask(cmd.timeoutMs);
3201
- ok(res, { taskV1 });
3773
+ if (cmd.dutySessionId) await updateDutySession(cmd.dutySessionId, true);
3774
+ try {
3775
+ if (isProtocolV1Available()) {
3776
+ const taskV1 = await claimRemoteTask(cmd.timeoutMs);
3777
+ ok(res, { taskV1 });
3778
+ return;
3779
+ }
3780
+ const task = await waitForPendingTask(cmd.timeoutMs);
3781
+ ok(res, { task });
3202
3782
  return;
3783
+ } finally {
3784
+ if (cmd.dutySessionId) await updateDutySession(cmd.dutySessionId, false).catch(() => {
3785
+ });
3203
3786
  }
3204
- const task = await waitForPendingTask(cmd.timeoutMs);
3205
- ok(res, { task });
3206
- return;
3207
3787
  }
3208
3788
  case "answer": {
3209
3789
  if (!cmd.questionId?.trim() || !cmd.text?.trim()) {
@@ -3306,7 +3886,7 @@ async function handleRelay(cmd, res) {
3306
3886
  fail(res, 400, "register_mcp_client requires agentHost", ErrorCodes.BAD_EVENT);
3307
3887
  return;
3308
3888
  }
3309
- await registerMcpClient(agentHost, cmd.mcpCapabilities ?? {});
3889
+ await registerMcpClient(agentHost, cmd.mcpCapabilities ?? {}, cmd.executionCapabilities);
3310
3890
  ok(res);
3311
3891
  return;
3312
3892
  }
@@ -3357,6 +3937,11 @@ async function handleRelay(cmd, res) {
3357
3937
  ok(res, { task });
3358
3938
  return;
3359
3939
  }
3940
+ case "report_task_progress": {
3941
+ const task = await reportRemoteTaskProgress(cmd.taskId, cmd.progress);
3942
+ ok(res, { task });
3943
+ return;
3944
+ }
3360
3945
  case "configure_device": {
3361
3946
  const { configureAndActivateDevice: configureAndActivateDevice2 } = await Promise.resolve().then(() => deviceAuth);
3362
3947
  await configureAndActivateDevice2({
@@ -3607,6 +4192,12 @@ function showMenu() {
3607
4192
  tray?.popUpContextMenu(electron.Menu.buildFromTemplate([
3608
4193
  { label: `Desktop Pet v${electron.app.getVersion()}`, enabled: false },
3609
4194
  { type: "separator" },
4195
+ {
4196
+ label: petManager.getLocalAgentConnectionStatus() === "update-required" ? "协作状态:需要重连 Agent" : petManager.observableTaskCount() > 0 ? `协作状态:${petManager.observableTaskCount()} 项任务进行中` : petManager.isLocalOnDuty() ? "协作状态:驻守中,等待好友任务" : "协作状态:空闲",
4197
+ enabled: false
4198
+ },
4199
+ { label: "任务明细:右键桌宠查看", enabled: false },
4200
+ { type: "separator" },
3610
4201
  {
3611
4202
  label: petManager.arePetsVisible() ? "隐藏所有桌面宠物" : "显示所有桌面宠物",
3612
4203
  click: () => petManager.setPetsVisible(!petManager.arePetsVisible())
@@ -3722,8 +4313,8 @@ electron.app.whenReady().then(async () => {
3722
4313
  return false;
3723
4314
  });
3724
4315
  logger.info("device activation finished", { activated });
3725
- if (!activated) startRelay();
3726
- logger.info("relay start requested", { authenticated: activated });
4316
+ if (activated) logger.info("relay start requested", { authenticated: true });
4317
+ else logger.info("relay waiting for account authentication");
3727
4318
  if (process.env.DESKTOP_PET_P2P !== "0") startP2PWindow(getDeviceIceServers());
3728
4319
  logger.info("p2p start requested", { enabled: process.env.DESKTOP_PET_P2P !== "0" });
3729
4320
  void startRunnerManager();