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 +695 -104
- package/out/mcp-server/index.js +323 -73
- package/out/renderer/assets/{index-CHc4qvDr.js → index-BWDd9Gk_.js} +29 -0
- package/out/renderer/index.html +101 -3
- package/package.json +3 -1
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
|
-
|
|
545
|
-
|
|
546
|
-
|
|
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
|
-
|
|
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
|
-
|
|
690
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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:
|
|
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(
|
|
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 =
|
|
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
|
-
|
|
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
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
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
|
|
1167
|
-
const
|
|
1168
|
-
|
|
1169
|
-
|
|
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("
|
|
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 =
|
|
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 =
|
|
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
|
|
1410
|
-
let
|
|
1411
|
-
|
|
1412
|
-
|
|
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
|
-
|
|
1908
|
+
publicKey = generated.publicKey;
|
|
1909
|
+
privateKey = generated.privateKey;
|
|
1417
1910
|
}
|
|
1418
|
-
if (!
|
|
1419
|
-
const
|
|
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
|
-
|
|
1917
|
+
encryptionPublicKey = generated.publicKey;
|
|
1918
|
+
encryptionPrivateKey = generated.privateKey;
|
|
1425
1919
|
}
|
|
1426
|
-
return
|
|
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
|
|
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
|
-
|
|
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
|
|
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 ?
|
|
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 }] : [{
|
|
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:
|
|
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
|
|
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
|
|
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 (
|
|
3200
|
-
|
|
3201
|
-
|
|
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 (
|
|
3726
|
-
logger.info("relay
|
|
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();
|