desktop-pet-app 0.3.5 → 0.3.24
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -1
- package/out/main/index.js +625 -116
- package/out/mcp-server/index.js +36611 -343
- package/package.json +13 -5
package/out/main/index.js
CHANGED
|
@@ -80,7 +80,17 @@ function writeMainLog(line) {
|
|
|
80
80
|
}
|
|
81
81
|
}
|
|
82
82
|
const logger = new Logger(levelFromEnv(), writeMainLog);
|
|
83
|
-
|
|
83
|
+
function resolvePortFile(env = process.env, homeDirectory = os.homedir()) {
|
|
84
|
+
const explicit = env.DESKTOP_PET_PORT_FILE?.trim();
|
|
85
|
+
if (explicit) return path.resolve(explicit);
|
|
86
|
+
const profile = env.DESKTOP_PET_PROFILE?.trim();
|
|
87
|
+
if (profile) return `${path.resolve(profile)}.port`;
|
|
88
|
+
if (env.NODE_ENV_ELECTRON_VITE === "development") {
|
|
89
|
+
return path.join(homeDirectory, ".desktop-pet-development.port");
|
|
90
|
+
}
|
|
91
|
+
return path.join(homeDirectory, ".desktop-pet-port");
|
|
92
|
+
}
|
|
93
|
+
const PORT_FILE = resolvePortFile();
|
|
84
94
|
const LOCAL_PET_PROTOCOL_VERSION = "1.0";
|
|
85
95
|
const LOCAL_PET_MAX_BODY_BYTES = 64 * 1024;
|
|
86
96
|
const MAX_PETS = 4;
|
|
@@ -345,6 +355,17 @@ function center(bounds) {
|
|
|
345
355
|
return { x: bounds.x + bounds.width / 2, y: bounds.y + bounds.height / 2 };
|
|
346
356
|
}
|
|
347
357
|
const petManager = new PetManager();
|
|
358
|
+
const OFFICIAL_SERVER_ROOT = "https://api.xinbit.top/desktop-pet";
|
|
359
|
+
const OFFICIAL_PORTAL_ORIGIN = "https://deskpalai.com";
|
|
360
|
+
const LEGACY_OFFICIAL_SERVER_HOSTS = "47.94.20.104".split(",").map((host) => host.trim()).filter(Boolean);
|
|
361
|
+
function isLegacyOfficialServerURL(value) {
|
|
362
|
+
try {
|
|
363
|
+
const url = new URL(value);
|
|
364
|
+
return url.protocol === "http:" && LEGACY_OFFICIAL_SERVER_HOSTS.includes(url.hostname);
|
|
365
|
+
} catch {
|
|
366
|
+
return false;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
348
369
|
function credentialPath() {
|
|
349
370
|
return node_path.join(electron.app.getPath("userData"), "device-credentials.enc");
|
|
350
371
|
}
|
|
@@ -361,10 +382,18 @@ function loadDeviceCredentials() {
|
|
|
361
382
|
const path2 = credentialPath();
|
|
362
383
|
if (!node_fs.existsSync(path2)) return void 0;
|
|
363
384
|
if (!electron.safeStorage.isEncryptionAvailable()) return void 0;
|
|
364
|
-
const credentials =
|
|
385
|
+
const credentials = migrateLegacyOfficialServer(
|
|
386
|
+
JSON.parse(electron.safeStorage.decryptString(node_fs.readFileSync(path2)))
|
|
387
|
+
);
|
|
365
388
|
validate(credentials);
|
|
366
389
|
return credentials;
|
|
367
390
|
}
|
|
391
|
+
function migrateLegacyOfficialServer(credentials) {
|
|
392
|
+
if (isLegacyOfficialServerURL(credentials.serverUrl)) {
|
|
393
|
+
return { ...credentials, serverUrl: OFFICIAL_SERVER_ROOT };
|
|
394
|
+
}
|
|
395
|
+
return credentials;
|
|
396
|
+
}
|
|
368
397
|
function clearDeviceCredentials() {
|
|
369
398
|
node_fs.rmSync(credentialPath(), { force: true });
|
|
370
399
|
}
|
|
@@ -377,11 +406,39 @@ function validate(credentials) {
|
|
|
377
406
|
throw new Error("设备配置的 serverUrl 必须使用 http 或 https");
|
|
378
407
|
}
|
|
379
408
|
}
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
409
|
+
function configuredRelayURL(options = {}) {
|
|
410
|
+
if (options.allowRuntimeOverrides !== false) {
|
|
411
|
+
const host = process.env.DESKTOP_PET_HOST?.trim();
|
|
412
|
+
if (host) return relayURLFromHost(host);
|
|
413
|
+
const server = process.env.DESKTOP_PET_SERVER?.trim();
|
|
414
|
+
if (server) return server;
|
|
415
|
+
}
|
|
416
|
+
return relayURLFromHost(OFFICIAL_SERVER_ROOT);
|
|
417
|
+
}
|
|
418
|
+
function hasRuntimeServerOverride(options = {}) {
|
|
419
|
+
return options.allowRuntimeOverrides !== false && Boolean(
|
|
420
|
+
process.env.DESKTOP_PET_HOST?.trim() || process.env.DESKTOP_PET_SERVER?.trim()
|
|
421
|
+
);
|
|
422
|
+
}
|
|
423
|
+
function relayHTTPRootURL(relayURL = configuredRelayURL()) {
|
|
424
|
+
const url = new URL(relayURL);
|
|
425
|
+
if (url.protocol === "wss:") url.protocol = "https:";
|
|
426
|
+
else if (url.protocol === "ws:") url.protocol = "http:";
|
|
427
|
+
else throw new Error("中转地址必须使用 ws:// 或 wss://");
|
|
428
|
+
url.pathname = url.pathname.replace(/\/ws\/?$/, "").replace(/\/+$/, "");
|
|
429
|
+
url.search = "";
|
|
430
|
+
url.hash = "";
|
|
431
|
+
return url.toString().replace(/\/$/, "");
|
|
432
|
+
}
|
|
433
|
+
function canonicalHTTPRootURL(value) {
|
|
434
|
+
const url = new URL(value);
|
|
435
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
|
436
|
+
throw new Error("设备服务器地址必须使用 http:// 或 https://");
|
|
437
|
+
}
|
|
438
|
+
url.pathname = url.pathname.replace(/\/+$/, "");
|
|
439
|
+
url.search = "";
|
|
440
|
+
url.hash = "";
|
|
441
|
+
return url.toString().replace(/\/$/, "");
|
|
385
442
|
}
|
|
386
443
|
function relayURLFromHost(value) {
|
|
387
444
|
const source = value.includes("://") ? value : `${isLocalHost(value) ? "http" : "https"}://${value}`;
|
|
@@ -396,7 +453,7 @@ function relayURLFromHost(value) {
|
|
|
396
453
|
if (url.protocol !== "ws:" && url.protocol !== "wss:") {
|
|
397
454
|
throw new Error("DESKTOP_PET_HOST 必须使用 http:// 或 https://");
|
|
398
455
|
}
|
|
399
|
-
url.pathname = "/ws
|
|
456
|
+
url.pathname = `${url.pathname.replace(/\/+$/, "")}/ws`;
|
|
400
457
|
url.search = "";
|
|
401
458
|
url.hash = "";
|
|
402
459
|
return url.toString();
|
|
@@ -405,9 +462,20 @@ function isLocalHost(value) {
|
|
|
405
462
|
return /^(localhost|127\.0\.0\.1|\[::1\])(?::\d+)?(?:\/|$)/i.test(value.trim());
|
|
406
463
|
}
|
|
407
464
|
const TASK_PROTOCOL_VERSION = "1.0";
|
|
465
|
+
const WORKSPACE_SNAPSHOT_MIME = "application/vnd.desktop-pet.git-workspace-bundle";
|
|
466
|
+
function taskParticipantRole(task, authenticatedUserId) {
|
|
467
|
+
const current = authenticatedUserId.trim().toLowerCase();
|
|
468
|
+
if (!current) return void 0;
|
|
469
|
+
if (task.envelope.from.userId.trim().toLowerCase() === current) return "requester";
|
|
470
|
+
if (task.envelope.to.userId.trim().toLowerCase() === current) return "executor";
|
|
471
|
+
return void 0;
|
|
472
|
+
}
|
|
408
473
|
const sensitivePathPatterns = [
|
|
409
474
|
/(^|\/)\.env(?:\.|$)/i,
|
|
475
|
+
/(^|\/)(?:\.npmrc|\.pypirc|\.netrc)(?:$|\/)/i,
|
|
476
|
+
/(^|\/)(?:\.aws|\.azure|\.kube)(?:$|\/)/i,
|
|
410
477
|
/(^|\/)(?:id_rsa|id_ed25519|known_hosts|credentials)(?:$|\/)/i,
|
|
478
|
+
/(?:^|\/)[^/]+\.(?:pem|key|p12|pfx|keystore)(?:$|\/)/i,
|
|
411
479
|
/(^|\/)\.ssh(?:$|\/)/i,
|
|
412
480
|
/(^|\/)\.gnupg(?:$|\/)/i,
|
|
413
481
|
/(^|\/)node_modules(?:$|\/)/i,
|
|
@@ -459,6 +527,9 @@ function inlineResultArtifact(input) {
|
|
|
459
527
|
inlineText: input.text
|
|
460
528
|
};
|
|
461
529
|
}
|
|
530
|
+
function isWorkspaceSnapshotContextItem(item) {
|
|
531
|
+
return item.kind === "file" && item.mimeType === WORKSPACE_SNAPSHOT_MIME && item.encoding === "binary" && item.transport === "relay-blob" && item.required && Boolean(item.artifactRef);
|
|
532
|
+
}
|
|
462
533
|
function isTerminalTaskStatus(status2) {
|
|
463
534
|
return status2 === "completed" || status2 === "failed" || status2 === "cancelled" || status2 === "expired";
|
|
464
535
|
}
|
|
@@ -478,6 +549,37 @@ function showDesktopNotification(title, body) {
|
|
|
478
549
|
if (!electron.Notification.isSupported()) return;
|
|
479
550
|
new electron.Notification({ title, body }).show();
|
|
480
551
|
}
|
|
552
|
+
const PRODUCT_DATA_DIRECTORY = "Desktop Pet";
|
|
553
|
+
const DEVELOPMENT_DATA_DIRECTORY = "Desktop Pet Development";
|
|
554
|
+
const LEGACY_NPM_DATA_DIRECTORY = "desktop-pet-app";
|
|
555
|
+
function developmentRuntimeOverridesAllowed() {
|
|
556
|
+
return !electron.app?.isPackaged || Boolean(process.env.DESKTOP_PET_PROFILE?.trim());
|
|
557
|
+
}
|
|
558
|
+
function configureRuntimeIdentity() {
|
|
559
|
+
const profileDirectory = process.env.DESKTOP_PET_PROFILE?.trim();
|
|
560
|
+
if (profileDirectory) {
|
|
561
|
+
electron.app.setPath("userData", node_path.resolve(profileDirectory));
|
|
562
|
+
return;
|
|
563
|
+
}
|
|
564
|
+
const appDataDirectory = electron.app.getPath("appData");
|
|
565
|
+
if (electron.app.isPackaged || process.env.DESKTOP_PET_NPM_DISTRIBUTION === "1") {
|
|
566
|
+
electron.app.setPath("userData", node_path.join(appDataDirectory, PRODUCT_DATA_DIRECTORY));
|
|
567
|
+
return;
|
|
568
|
+
}
|
|
569
|
+
const developmentDirectory = node_path.join(appDataDirectory, DEVELOPMENT_DATA_DIRECTORY);
|
|
570
|
+
migrateLegacyData(node_path.join(appDataDirectory, LEGACY_NPM_DATA_DIRECTORY), developmentDirectory);
|
|
571
|
+
electron.app.setPath("userData", developmentDirectory);
|
|
572
|
+
}
|
|
573
|
+
function migrateLegacyData(legacyDirectory, canonicalDirectory) {
|
|
574
|
+
if (!node_fs.existsSync(legacyDirectory) || node_fs.existsSync(canonicalDirectory)) return false;
|
|
575
|
+
try {
|
|
576
|
+
node_fs.renameSync(legacyDirectory, canonicalDirectory);
|
|
577
|
+
return true;
|
|
578
|
+
} catch (error) {
|
|
579
|
+
console.warn(`无法迁移 Desktop Pet 用户数据:${String(error)}`);
|
|
580
|
+
return false;
|
|
581
|
+
}
|
|
582
|
+
}
|
|
481
583
|
function p2pSigningPayload(message, fromDeviceId) {
|
|
482
584
|
const signalJSON = JSON.stringify(message.signal ?? null);
|
|
483
585
|
const signalHash = node_crypto.createHash("sha256").update(signalJSON).digest("hex");
|
|
@@ -504,11 +606,12 @@ function verifyP2PSignal(message) {
|
|
|
504
606
|
return false;
|
|
505
607
|
}
|
|
506
608
|
}
|
|
507
|
-
|
|
508
|
-
let
|
|
609
|
+
const allowDevelopmentOverrides = developmentRuntimeOverridesAllowed();
|
|
610
|
+
let serverURL = configuredRelayURL({ allowRuntimeOverrides: allowDevelopmentOverrides });
|
|
611
|
+
let userID = allowDevelopmentOverrides ? process.env.DESKTOP_PET_USER ?? os.hostname() : os.hostname();
|
|
509
612
|
const SKIN = process.env.DESKTOP_PET_SKIN ?? "";
|
|
510
|
-
let authToken = process.env.DESKTOP_PET_TOKEN?.trim();
|
|
511
|
-
let deviceID = process.env.DESKTOP_PET_DEVICE_ID?.trim() || "";
|
|
613
|
+
let authToken = allowDevelopmentOverrides ? process.env.DESKTOP_PET_TOKEN?.trim() : void 0;
|
|
614
|
+
let deviceID = allowDevelopmentOverrides ? process.env.DESKTOP_PET_DEVICE_ID?.trim() || "" : "";
|
|
512
615
|
let signingPrivateKey = "";
|
|
513
616
|
const pendingTasks = /* @__PURE__ */ new Map();
|
|
514
617
|
const pendingAnswers = /* @__PURE__ */ new Map();
|
|
@@ -523,13 +626,41 @@ let ws = null;
|
|
|
523
626
|
let connectPromise = null;
|
|
524
627
|
let intentionalClose = false;
|
|
525
628
|
let protocolV1Available = false;
|
|
629
|
+
let workspaceSnapshotStagingAvailable = false;
|
|
526
630
|
let mcpRegistration;
|
|
631
|
+
const RETRY_BASE_DELAY_MS = 3e3;
|
|
632
|
+
const RETRY_MAX_DELAY_MS = 5 * 6e4;
|
|
633
|
+
let reconnectTimer = null;
|
|
634
|
+
let retryAttempt = 0;
|
|
635
|
+
function scheduleRelayReconnect() {
|
|
636
|
+
if (reconnectTimer) return;
|
|
637
|
+
const delay = Math.min(RETRY_BASE_DELAY_MS * 2 ** Math.min(retryAttempt, 10), RETRY_MAX_DELAY_MS);
|
|
638
|
+
retryAttempt += 1;
|
|
639
|
+
logger.info("relay reconnect scheduled", { attempt: retryAttempt, delayMs: delay });
|
|
640
|
+
reconnectTimer = setTimeout(() => {
|
|
641
|
+
reconnectTimer = null;
|
|
642
|
+
void ensureConnected().catch(() => {
|
|
643
|
+
});
|
|
644
|
+
}, delay);
|
|
645
|
+
}
|
|
646
|
+
function cancelRelayReconnect() {
|
|
647
|
+
if (reconnectTimer) {
|
|
648
|
+
clearTimeout(reconnectTimer);
|
|
649
|
+
reconnectTimer = null;
|
|
650
|
+
}
|
|
651
|
+
}
|
|
527
652
|
function getAuthenticatedRelayUserId() {
|
|
528
653
|
return authToken ? userID : void 0;
|
|
529
654
|
}
|
|
655
|
+
function getPresentedRelayUserId() {
|
|
656
|
+
return authToken || allowDevelopmentOverrides && process.env.DESKTOP_PET_USER?.trim() ? userID : void 0;
|
|
657
|
+
}
|
|
530
658
|
function isProtocolV1Available() {
|
|
531
659
|
return protocolV1Available;
|
|
532
660
|
}
|
|
661
|
+
function isWorkspaceSnapshotStagingAvailable() {
|
|
662
|
+
return protocolV1Available && workspaceSnapshotStagingAvailable;
|
|
663
|
+
}
|
|
533
664
|
function getRelayHTTPAuth() {
|
|
534
665
|
if (!authToken) throw new Error("relay device access token is unavailable");
|
|
535
666
|
const url = new URL(serverURL);
|
|
@@ -547,6 +678,14 @@ function configureRelayIdentity(config) {
|
|
|
547
678
|
authToken = config.accessToken;
|
|
548
679
|
signingPrivateKey = config.signingPrivateKey ?? "";
|
|
549
680
|
}
|
|
681
|
+
function clearRelayIdentity() {
|
|
682
|
+
if (ws || connectPromise) throw new Error("stop relay before clearing identity");
|
|
683
|
+
serverURL = configuredRelayURL({ allowRuntimeOverrides: allowDevelopmentOverrides });
|
|
684
|
+
userID = allowDevelopmentOverrides ? process.env.DESKTOP_PET_USER ?? os.hostname() : os.hostname();
|
|
685
|
+
deviceID = allowDevelopmentOverrides ? process.env.DESKTOP_PET_DEVICE_ID?.trim() || "" : "";
|
|
686
|
+
authToken = allowDevelopmentOverrides ? process.env.DESKTOP_PET_TOKEN?.trim() : void 0;
|
|
687
|
+
signingPrivateKey = "";
|
|
688
|
+
}
|
|
550
689
|
async function registerMcpClient(agentHost, capabilities = {}) {
|
|
551
690
|
mcpRegistration = { agentHost, capabilities };
|
|
552
691
|
await ensureConnected();
|
|
@@ -561,6 +700,7 @@ async function registerMcpClient(agentHost, capabilities = {}) {
|
|
|
561
700
|
}
|
|
562
701
|
function startRelay() {
|
|
563
702
|
intentionalClose = false;
|
|
703
|
+
retryAttempt = 0;
|
|
564
704
|
void ensureConnected().catch((err) => {
|
|
565
705
|
logger.warn("relay connect failed on startup", { error: String(err) });
|
|
566
706
|
});
|
|
@@ -568,6 +708,9 @@ function startRelay() {
|
|
|
568
708
|
function stopRelay() {
|
|
569
709
|
intentionalClose = true;
|
|
570
710
|
protocolV1Available = false;
|
|
711
|
+
workspaceSnapshotStagingAvailable = false;
|
|
712
|
+
cancelRelayReconnect();
|
|
713
|
+
retryAttempt = 0;
|
|
571
714
|
connectPromise = null;
|
|
572
715
|
if (ws) {
|
|
573
716
|
ws.close();
|
|
@@ -611,6 +754,7 @@ function ensureConnected() {
|
|
|
611
754
|
if (connectPromise) return connectPromise;
|
|
612
755
|
connectPromise = new Promise((resolve, reject) => {
|
|
613
756
|
let settled = false;
|
|
757
|
+
let authRejected = false;
|
|
614
758
|
const sock = new WebSocket(serverURL, {
|
|
615
759
|
...authToken ? { headers: { Authorization: `Bearer ${authToken}` } } : {}
|
|
616
760
|
});
|
|
@@ -634,8 +778,12 @@ function ensureConnected() {
|
|
|
634
778
|
} catch {
|
|
635
779
|
return;
|
|
636
780
|
}
|
|
781
|
+
if (msg.task) msg.task = withParticipantRole(msg.task);
|
|
782
|
+
if (msg.tasks) msg.tasks = msg.tasks.map(withParticipantRole);
|
|
637
783
|
if (msg.type === "registered") {
|
|
784
|
+
retryAttempt = 0;
|
|
638
785
|
protocolV1Available = msg.protocolVersion === TASK_PROTOCOL_VERSION;
|
|
786
|
+
workspaceSnapshotStagingAvailable = msg.capabilities?.workspaceSnapshotStaging === true;
|
|
639
787
|
logger.info("registered to relay", { userId: userID, server: serverURL });
|
|
640
788
|
if (!settled) {
|
|
641
789
|
settled = true;
|
|
@@ -729,6 +877,7 @@ function ensureConnected() {
|
|
|
729
877
|
}
|
|
730
878
|
});
|
|
731
879
|
sock.on("error", (err) => {
|
|
880
|
+
if (/Unexpected server response: 40\d/.test(String(err))) authRejected = true;
|
|
732
881
|
logger.warn("relay socket error", { error: String(err) });
|
|
733
882
|
if (!settled) {
|
|
734
883
|
settled = true;
|
|
@@ -755,10 +904,14 @@ function ensureConnected() {
|
|
|
755
904
|
reject(new Error(`relay connection closed before registration (${code})`));
|
|
756
905
|
}
|
|
757
906
|
if (isCurrentSocket && !intentionalClose) {
|
|
758
|
-
|
|
759
|
-
|
|
907
|
+
if (authRejected) {
|
|
908
|
+
logger.warn("relay rejected by server authentication; auto reconnect paused until identity changes", {
|
|
909
|
+
server: serverURL,
|
|
910
|
+
authenticated: Boolean(authToken)
|
|
760
911
|
});
|
|
761
|
-
}
|
|
912
|
+
} else {
|
|
913
|
+
scheduleRelayReconnect();
|
|
914
|
+
}
|
|
762
915
|
}
|
|
763
916
|
});
|
|
764
917
|
});
|
|
@@ -887,6 +1040,14 @@ async function delegateTask(envelope) {
|
|
|
887
1040
|
}
|
|
888
1041
|
return { task: response.task, warning: response.warning };
|
|
889
1042
|
}
|
|
1043
|
+
async function finalizeRemoteTaskWorkspace(taskId) {
|
|
1044
|
+
const response = await relayRPC(
|
|
1045
|
+
{ type: "task.workspace.ready", protocolVersion: TASK_PROTOCOL_VERSION, taskId },
|
|
1046
|
+
15e3
|
|
1047
|
+
);
|
|
1048
|
+
if (!response.task) throw new Error("relay did not return the finalized task");
|
|
1049
|
+
return { task: response.task, warning: response.warning };
|
|
1050
|
+
}
|
|
890
1051
|
function relayCapabilities() {
|
|
891
1052
|
return {
|
|
892
1053
|
agentHost: mcpRegistration?.agentHost ?? (process.env.DESKTOP_PET_AGENT_HOST || "desktop-pet-mcp"),
|
|
@@ -1095,6 +1256,9 @@ function stopLease(taskId) {
|
|
|
1095
1256
|
if (timer) clearInterval(timer);
|
|
1096
1257
|
leaseTimers.delete(taskId);
|
|
1097
1258
|
}
|
|
1259
|
+
function withParticipantRole(task) {
|
|
1260
|
+
return { ...task, participantRole: taskParticipantRole(task, userID) };
|
|
1261
|
+
}
|
|
1098
1262
|
function onAsk(msg) {
|
|
1099
1263
|
if (!msg.questionId || !msg.from || !msg.text) return;
|
|
1100
1264
|
const task = {
|
|
@@ -1168,11 +1332,21 @@ function getDeviceIceServers() {
|
|
|
1168
1332
|
return deviceIceServers;
|
|
1169
1333
|
}
|
|
1170
1334
|
async function activateStoredDevice() {
|
|
1171
|
-
|
|
1335
|
+
const allowRuntimeOverrides = developmentRuntimeOverridesAllowed();
|
|
1336
|
+
if (allowRuntimeOverrides && process.env.DESKTOP_PET_TOKEN?.trim()) {
|
|
1172
1337
|
startRelay();
|
|
1173
1338
|
return true;
|
|
1174
1339
|
}
|
|
1175
1340
|
const credentials = loadDeviceCredentials();
|
|
1341
|
+
const configuredServer = configuredRelayURL({ allowRuntimeOverrides });
|
|
1342
|
+
if (!allowRuntimeOverrides && credentials && canonicalHTTPRootURL(credentials.serverUrl) !== relayHTTPRootURL(configuredServer)) {
|
|
1343
|
+
logger.warn("stored device credentials belong to a different runtime environment");
|
|
1344
|
+
return false;
|
|
1345
|
+
}
|
|
1346
|
+
if (hasRuntimeServerOverride({ allowRuntimeOverrides }) && (!credentials || canonicalHTTPRootURL(credentials.serverUrl) !== relayHTTPRootURL(configuredServer))) {
|
|
1347
|
+
startRelay();
|
|
1348
|
+
return true;
|
|
1349
|
+
}
|
|
1176
1350
|
if (!credentials) return false;
|
|
1177
1351
|
await activateDevice(credentials, false);
|
|
1178
1352
|
return true;
|
|
@@ -1215,7 +1389,7 @@ async function activateDevice(credentials, restart) {
|
|
|
1215
1389
|
if (restart) stopRelay();
|
|
1216
1390
|
const httpURL = new URL(base);
|
|
1217
1391
|
httpURL.protocol = httpURL.protocol === "https:" ? "wss:" : "ws:";
|
|
1218
|
-
httpURL.pathname = "/ws
|
|
1392
|
+
httpURL.pathname = `${httpURL.pathname.replace(/\/+$/, "")}/ws`;
|
|
1219
1393
|
configureRelayIdentity({
|
|
1220
1394
|
serverUrl: httpURL.toString(),
|
|
1221
1395
|
userId: credentials.userId,
|
|
@@ -1260,14 +1434,20 @@ const deviceAuth = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.definePr
|
|
|
1260
1434
|
function defaultAccountServerURL(relayURL = configuredRelayURL()) {
|
|
1261
1435
|
const url = new URL(relayURL);
|
|
1262
1436
|
url.protocol = url.protocol === "wss:" ? "https:" : "http:";
|
|
1263
|
-
url.pathname = "";
|
|
1437
|
+
url.pathname = url.pathname.replace(/\/ws$/, "");
|
|
1264
1438
|
url.search = "";
|
|
1265
1439
|
url.hash = "";
|
|
1266
1440
|
return url.toString().replace(/\/$/, "");
|
|
1267
1441
|
}
|
|
1268
|
-
function defaultAccountPortalURL(
|
|
1269
|
-
const override = process.env.DESKTOP_PET_PORTAL_URL?.trim();
|
|
1270
|
-
|
|
1442
|
+
function defaultAccountPortalURL(allowRuntimeOverrides = true) {
|
|
1443
|
+
const override = allowRuntimeOverrides ? process.env.DESKTOP_PET_PORTAL_URL?.trim() : void 0;
|
|
1444
|
+
if (override) return normalizeServerURL(override);
|
|
1445
|
+
return hasRuntimeServerOverride({ allowRuntimeOverrides }) ? defaultAccountServerURL(configuredRelayURL({ allowRuntimeOverrides })) : OFFICIAL_PORTAL_ORIGIN;
|
|
1446
|
+
}
|
|
1447
|
+
function preferredAccountServerURL(storedServerURL2, allowRuntimeOverrides = true) {
|
|
1448
|
+
const configured = defaultAccountServerURL(configuredRelayURL({ allowRuntimeOverrides }));
|
|
1449
|
+
if (!allowRuntimeOverrides) return configured;
|
|
1450
|
+
return hasRuntimeServerOverride({ allowRuntimeOverrides }) ? configured : storedServerURL2 ?? configured;
|
|
1271
1451
|
}
|
|
1272
1452
|
async function bindDesktopPetToWebSession(server, accessToken) {
|
|
1273
1453
|
const serverUrl = normalizeServerURL(server);
|
|
@@ -1307,7 +1487,7 @@ function normalizeServerURL(value) {
|
|
|
1307
1487
|
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
|
1308
1488
|
throw new Error("中转服务器地址必须以 http:// 或 https:// 开头");
|
|
1309
1489
|
}
|
|
1310
|
-
url.pathname = "";
|
|
1490
|
+
url.pathname = url.pathname.replace(/\/+$/, "");
|
|
1311
1491
|
url.search = "";
|
|
1312
1492
|
url.hash = "";
|
|
1313
1493
|
return url.toString().replace(/\/$/, "");
|
|
@@ -1408,10 +1588,11 @@ function stopAccountCallbackRouter() {
|
|
|
1408
1588
|
callbackNonce = void 0;
|
|
1409
1589
|
}
|
|
1410
1590
|
function openAccountWindow(mode) {
|
|
1411
|
-
const
|
|
1591
|
+
const allowRuntimeOverrides = developmentRuntimeOverridesAllowed();
|
|
1592
|
+
const serverUrl = preferredAccountServerURL(storedServerURL(), allowRuntimeOverrides);
|
|
1412
1593
|
let portalBase;
|
|
1413
1594
|
try {
|
|
1414
|
-
portalBase = defaultAccountPortalURL(
|
|
1595
|
+
portalBase = defaultAccountPortalURL(allowRuntimeOverrides);
|
|
1415
1596
|
} catch (error) {
|
|
1416
1597
|
electron.dialog.showErrorBox("门户地址无效", error instanceof Error ? error.message : "请检查 DESKTOP_PET_PORTAL_URL 配置");
|
|
1417
1598
|
return;
|
|
@@ -1454,15 +1635,16 @@ function handleAccountCallback(url) {
|
|
|
1454
1635
|
}
|
|
1455
1636
|
function logoutDesktopPet() {
|
|
1456
1637
|
stopRelay();
|
|
1638
|
+
clearRelayIdentity();
|
|
1457
1639
|
clearDeviceCredentials();
|
|
1458
1640
|
petManager.refreshLocalIdentityBadges();
|
|
1459
1641
|
dispatch({ type: "bubble", text: "已退出当前账号" });
|
|
1460
1642
|
}
|
|
1461
1643
|
function currentAccountUsername() {
|
|
1462
1644
|
try {
|
|
1463
|
-
return loadDeviceCredentials()?.userId ?? getAuthenticatedRelayUserId();
|
|
1645
|
+
return getPresentedRelayUserId() ?? loadDeviceCredentials()?.userId ?? getAuthenticatedRelayUserId();
|
|
1464
1646
|
} catch {
|
|
1465
|
-
return getAuthenticatedRelayUserId();
|
|
1647
|
+
return getPresentedRelayUserId() ?? getAuthenticatedRelayUserId();
|
|
1466
1648
|
}
|
|
1467
1649
|
}
|
|
1468
1650
|
function storedServerURL() {
|
|
@@ -1515,22 +1697,27 @@ async function finishWebAuthentication(callbackURL) {
|
|
|
1515
1697
|
}
|
|
1516
1698
|
}
|
|
1517
1699
|
function copyMcpConfig() {
|
|
1518
|
-
const serverEntry =
|
|
1700
|
+
const serverEntry = resolveMcpServerEntry(electron.app.isPackaged, electron.app.getAppPath(), process.resourcesPath);
|
|
1519
1701
|
if (!fs.existsSync(serverEntry)) {
|
|
1520
1702
|
return { ok: false, message: "MCP server 尚未构建,请先在 app 目录执行 npm run build" };
|
|
1521
1703
|
}
|
|
1522
|
-
const
|
|
1523
|
-
|
|
1704
|
+
const config = createMcpConfig(serverEntry, PORT_FILE);
|
|
1705
|
+
electron.clipboard.writeText(JSON.stringify(config, null, 2));
|
|
1706
|
+
return { ok: true, message: "MCP 配置已复制,直接粘贴到 Agent 的 MCP 配置里即可" };
|
|
1707
|
+
}
|
|
1708
|
+
function createMcpConfig(serverEntry, portFile) {
|
|
1709
|
+
return {
|
|
1524
1710
|
mcpServers: {
|
|
1525
1711
|
"desktop-pet": {
|
|
1526
1712
|
command: "node",
|
|
1527
1713
|
args: [serverEntry],
|
|
1528
|
-
|
|
1714
|
+
env: { DESKTOP_PET_PORT_FILE: portFile }
|
|
1529
1715
|
}
|
|
1530
1716
|
}
|
|
1531
1717
|
};
|
|
1532
|
-
|
|
1533
|
-
|
|
1718
|
+
}
|
|
1719
|
+
function resolveMcpServerEntry(packaged, appPath, resourcesPath) {
|
|
1720
|
+
return packaged ? path.join(resourcesPath, "mcp-server", "index.js") : path.join(appPath, "out", "mcp-server", "index.js");
|
|
1534
1721
|
}
|
|
1535
1722
|
const P2P_CHAT_CONNECT_TIMEOUT_MS = 2500;
|
|
1536
1723
|
let transportWindow;
|
|
@@ -1678,17 +1865,15 @@ async function fallbackChat(operationId) {
|
|
|
1678
1865
|
}
|
|
1679
1866
|
const REQUEST_TIMEOUT_MS = 6e3;
|
|
1680
1867
|
const DOWNLOAD_TIMEOUT_MS = 10 * 6e4;
|
|
1681
|
-
const
|
|
1868
|
+
const MAX_ARTIFACT_BYTES = 2 * 1024 * 1024 * 1024;
|
|
1682
1869
|
const MAC_BUNDLE_ID = "com.xinshu.desktoppet";
|
|
1683
1870
|
const MAC_INSTALLED_APP = "/Applications/Desktop Pet.app";
|
|
1684
1871
|
let status = { kind: "idle" };
|
|
1685
1872
|
function updateCheckURL() {
|
|
1686
|
-
const override = process.env.DESKTOP_PET_UPDATE_API?.trim();
|
|
1873
|
+
const override = !electron.app.isPackaged ? process.env.DESKTOP_PET_UPDATE_API?.trim() : void 0;
|
|
1687
1874
|
if (override) return validateUpdateAPIURL(override).toString();
|
|
1688
|
-
const
|
|
1689
|
-
|
|
1690
|
-
url.protocol = url.protocol === "wss:" ? "https:" : "http:";
|
|
1691
|
-
url.pathname = "/api/updates/check";
|
|
1875
|
+
const url = updateServiceRootURL(updateServiceURL());
|
|
1876
|
+
url.pathname = `${url.pathname.replace(/\/+$/, "")}/api/updates/check`;
|
|
1692
1877
|
url.search = "";
|
|
1693
1878
|
url.hash = "";
|
|
1694
1879
|
return validateUpdateAPIURL(url.toString()).toString();
|
|
@@ -1704,8 +1889,10 @@ async function checkForUpdates(currentVersion = electron.app.getVersion()) {
|
|
|
1704
1889
|
endpoint.searchParams.set("platform", process.platform);
|
|
1705
1890
|
endpoint.searchParams.set("arch", process.arch);
|
|
1706
1891
|
endpoint.searchParams.set("currentVersion", currentVersion);
|
|
1892
|
+
endpoint.searchParams.set("channel", updateChannel());
|
|
1893
|
+
endpoint.searchParams.set("installationId", await updateInstallationID());
|
|
1707
1894
|
const response = await electron.net.fetch(endpoint.toString(), {
|
|
1708
|
-
headers:
|
|
1895
|
+
headers: { accept: "application/json" },
|
|
1709
1896
|
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
1710
1897
|
cache: "no-store"
|
|
1711
1898
|
});
|
|
@@ -1714,6 +1901,7 @@ async function checkForUpdates(currentVersion = electron.app.getVersion()) {
|
|
|
1714
1901
|
throw new Error(body.message || "更新服务器返回异常");
|
|
1715
1902
|
}
|
|
1716
1903
|
const update = parseUpdate(body.data.update);
|
|
1904
|
+
if (update) verifyManifestSignature(update);
|
|
1717
1905
|
if (!update || compareVersions(update.version, currentVersion) <= 0) {
|
|
1718
1906
|
status = { kind: "up-to-date", checkedAt: Date.now() };
|
|
1719
1907
|
return status;
|
|
@@ -1734,20 +1922,13 @@ async function checkForUpdates(currentVersion = electron.app.getVersion()) {
|
|
|
1734
1922
|
return status;
|
|
1735
1923
|
}
|
|
1736
1924
|
}
|
|
1737
|
-
function updateCheckHeaders(endpoint) {
|
|
1738
|
-
const headers = { accept: "application/json" };
|
|
1739
|
-
try {
|
|
1740
|
-
const auth = getRelayHTTPAuth();
|
|
1741
|
-
if (new URL(auth.baseURL).origin === endpoint.origin) {
|
|
1742
|
-
headers.authorization = `Bearer ${auth.accessToken}`;
|
|
1743
|
-
}
|
|
1744
|
-
} catch {
|
|
1745
|
-
}
|
|
1746
|
-
return headers;
|
|
1747
|
-
}
|
|
1748
1925
|
async function downloadAvailableUpdate() {
|
|
1749
1926
|
const available = status.kind === "available" ? status.update : void 0;
|
|
1750
1927
|
if (!available) return status;
|
|
1928
|
+
if (available.signature && (!available.expiresAt || Date.parse(available.expiresAt) < Date.now() - 6e4)) {
|
|
1929
|
+
status = { kind: "error", message: "更新信息已过期,请重新检查", checkedAt: Date.now() };
|
|
1930
|
+
return status;
|
|
1931
|
+
}
|
|
1751
1932
|
status = { kind: "downloading", update: available, downloadedBytes: 0 };
|
|
1752
1933
|
const updateDirectory = node_path.join(electron.app.getPath("userData"), "updates");
|
|
1753
1934
|
const artifactPath = updateArtifactPath(available);
|
|
@@ -1755,7 +1936,7 @@ async function downloadAvailableUpdate() {
|
|
|
1755
1936
|
try {
|
|
1756
1937
|
await promises.mkdir(updateDirectory, { recursive: true });
|
|
1757
1938
|
const response = await electron.net.fetch(available.releaseUrl, {
|
|
1758
|
-
headers: { accept: "application/octet-stream, application/wasm, */*" },
|
|
1939
|
+
headers: { accept: "application/octet-stream, application/wasm, */*", "accept-encoding": "identity" },
|
|
1759
1940
|
signal: AbortSignal.timeout(DOWNLOAD_TIMEOUT_MS),
|
|
1760
1941
|
cache: "no-store",
|
|
1761
1942
|
redirect: "follow"
|
|
@@ -1766,13 +1947,17 @@ async function downloadAvailableUpdate() {
|
|
|
1766
1947
|
throw new Error("更新下载地址必须使用 HTTPS");
|
|
1767
1948
|
}
|
|
1768
1949
|
const totalBytes = parseContentLength(response.headers.get("content-length"));
|
|
1950
|
+
if (totalBytes && available.sizeBytes && totalBytes !== available.sizeBytes) {
|
|
1951
|
+
throw new Error("更新包大小与发布清单不一致");
|
|
1952
|
+
}
|
|
1769
1953
|
let downloadedBytes = 0;
|
|
1770
1954
|
status = { kind: "downloading", update: available, downloadedBytes, totalBytes };
|
|
1771
|
-
const
|
|
1955
|
+
const saved = await saveAndHash(response.body, temporaryPath, available.sizeBytes, (chunkBytes) => {
|
|
1772
1956
|
downloadedBytes += chunkBytes;
|
|
1773
1957
|
status = { kind: "downloading", update: available, downloadedBytes, totalBytes };
|
|
1774
1958
|
});
|
|
1775
|
-
if (
|
|
1959
|
+
if (available.sizeBytes && saved.bytes !== available.sizeBytes) throw new Error("更新包大小与发布清单不一致");
|
|
1960
|
+
if (saved.sha256 !== available.sha256) throw new Error("更新包校验失败,文件未安装");
|
|
1776
1961
|
await promises.rm(artifactPath, { force: true });
|
|
1777
1962
|
await moveFile(temporaryPath, artifactPath);
|
|
1778
1963
|
status = { kind: "downloaded", update: available, artifactPath, downloadedAt: Date.now() };
|
|
@@ -1843,10 +2028,16 @@ function sha256File(path2) {
|
|
|
1843
2028
|
input.on("end", () => resolve(hash.digest("hex")));
|
|
1844
2029
|
});
|
|
1845
2030
|
}
|
|
1846
|
-
async function saveAndHash(body, destination, onChunk) {
|
|
2031
|
+
async function saveAndHash(body, destination, expectedBytes, onChunk) {
|
|
1847
2032
|
const hash = node_crypto.createHash("sha256");
|
|
2033
|
+
let bytes = 0;
|
|
1848
2034
|
const meter = new node_stream.Transform({
|
|
1849
2035
|
transform(chunk, _encoding, done) {
|
|
2036
|
+
bytes += chunk.length;
|
|
2037
|
+
if (bytes > (expectedBytes ?? MAX_ARTIFACT_BYTES)) {
|
|
2038
|
+
done(new Error("更新包超过发布清单声明的大小"));
|
|
2039
|
+
return;
|
|
2040
|
+
}
|
|
1850
2041
|
hash.update(chunk);
|
|
1851
2042
|
onChunk(chunk.length);
|
|
1852
2043
|
done(null, chunk);
|
|
@@ -1857,7 +2048,7 @@ async function saveAndHash(body, destination, onChunk) {
|
|
|
1857
2048
|
meter,
|
|
1858
2049
|
node_fs.createWriteStream(destination, { flags: "wx" })
|
|
1859
2050
|
);
|
|
1860
|
-
return hash.digest("hex");
|
|
2051
|
+
return { sha256: hash.digest("hex"), bytes };
|
|
1861
2052
|
}
|
|
1862
2053
|
function parseContentLength(value) {
|
|
1863
2054
|
if (!value) return void 0;
|
|
@@ -1895,12 +2086,50 @@ const { promisify } = require('node:util')
|
|
|
1895
2086
|
const [, , parentPidText, sourcePath, expectedSHA256, platform, targetVersion, fallbackExecutable, fallbackArgsJSON] = process.argv
|
|
1896
2087
|
const parentPid = Number(parentPidText)
|
|
1897
2088
|
const fallbackArgs = JSON.parse(fallbackArgsJSON || '[]')
|
|
1898
|
-
const
|
|
2089
|
+
const tracePath = join(dirname(sourcePath), 'update-helper.log')
|
|
2090
|
+
const execute = promisify(execFile)
|
|
2091
|
+
|
|
2092
|
+
const trace = (event, fields = {}) => fs.appendFile(
|
|
2093
|
+
tracePath,
|
|
2094
|
+
JSON.stringify({ ts: new Date().toISOString(), pid: process.pid, event, ...fields }) + '\n'
|
|
2095
|
+
).catch(() => undefined)
|
|
2096
|
+
|
|
2097
|
+
const outputTail = (value) => String(value || '').slice(-2000)
|
|
2098
|
+
const run = async (name, executable, args) => {
|
|
2099
|
+
await trace('command-start', { name, executable, args })
|
|
2100
|
+
try {
|
|
2101
|
+
const result = await execute(executable, args)
|
|
2102
|
+
await trace('command-success', {
|
|
2103
|
+
name,
|
|
2104
|
+
stdout: outputTail(result.stdout),
|
|
2105
|
+
stderr: outputTail(result.stderr)
|
|
2106
|
+
})
|
|
2107
|
+
return result
|
|
2108
|
+
} catch (error) {
|
|
2109
|
+
await trace('command-failed', {
|
|
2110
|
+
name,
|
|
2111
|
+
error: String(error),
|
|
2112
|
+
stdout: outputTail(error && error.stdout),
|
|
2113
|
+
stderr: outputTail(error && error.stderr)
|
|
2114
|
+
})
|
|
2115
|
+
throw error
|
|
2116
|
+
}
|
|
2117
|
+
}
|
|
1899
2118
|
|
|
1900
2119
|
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
|
|
1901
2120
|
const parentIsRunning = () => {
|
|
1902
2121
|
try { process.kill(parentPid, 0); return true } catch { return false }
|
|
1903
2122
|
}
|
|
2123
|
+
const waitForParentExit = async () => {
|
|
2124
|
+
const startedAt = Date.now()
|
|
2125
|
+
let checks = 0
|
|
2126
|
+
await trace('waiting-for-parent-exit', { parentPid })
|
|
2127
|
+
while (parentIsRunning()) {
|
|
2128
|
+
checks += 1
|
|
2129
|
+
await sleep(200)
|
|
2130
|
+
}
|
|
2131
|
+
await trace('parent-exited', { parentPid, waitMs: Date.now() - startedAt, checks })
|
|
2132
|
+
}
|
|
1904
2133
|
const sha256 = (path) => new Promise((resolve, reject) => {
|
|
1905
2134
|
const hash = createHash('sha256')
|
|
1906
2135
|
const input = createReadStream(path)
|
|
@@ -1909,17 +2138,23 @@ const sha256 = (path) => new Promise((resolve, reject) => {
|
|
|
1909
2138
|
input.on('end', () => resolve(hash.digest('hex')))
|
|
1910
2139
|
})
|
|
1911
2140
|
const restartFallback = () => {
|
|
2141
|
+
void trace('fallback-spawn-requested', { fallbackExecutable, fallbackArgs })
|
|
1912
2142
|
const env = { ...process.env }
|
|
1913
2143
|
delete env.ELECTRON_RUN_AS_NODE
|
|
1914
2144
|
const child = spawn(fallbackExecutable, fallbackArgs, { detached: true, stdio: 'ignore', env })
|
|
1915
|
-
child.on('
|
|
2145
|
+
child.on('spawn', () => { void trace('fallback-spawned', { childPid: child.pid }) })
|
|
2146
|
+
child.on('error', (error) => { void trace('fallback-spawn-failed', { error: String(error) }) })
|
|
1916
2147
|
child.unref()
|
|
1917
2148
|
}
|
|
1918
2149
|
const findInstalledMacApp = async () => {
|
|
1919
2150
|
const plist = join('${MAC_INSTALLED_APP}', 'Contents', 'Info.plist')
|
|
1920
|
-
|
|
1921
|
-
const
|
|
1922
|
-
|
|
2151
|
+
await trace('installed-app-check-start', { plist, expectedBundle: '${MAC_BUNDLE_ID}', expectedVersion: targetVersion })
|
|
2152
|
+
const bundle = await run('read-bundle-id', '/usr/libexec/PlistBuddy', ['-c', 'Print :CFBundleIdentifier', plist])
|
|
2153
|
+
const version = await run('read-bundle-version', '/usr/libexec/PlistBuddy', ['-c', 'Print :CFBundleShortVersionString', plist])
|
|
2154
|
+
const actualBundle = bundle.stdout.trim()
|
|
2155
|
+
const actualVersion = version.stdout.trim()
|
|
2156
|
+
await trace('installed-app-check-result', { actualBundle, actualVersion })
|
|
2157
|
+
if (actualBundle !== '${MAC_BUNDLE_ID}' || actualVersion !== targetVersion) {
|
|
1923
2158
|
throw new Error('installed app identity or version does not match the verified update')
|
|
1924
2159
|
}
|
|
1925
2160
|
return '${MAC_INSTALLED_APP}'
|
|
@@ -1928,7 +2163,18 @@ const restartInstalledApp = async () => {
|
|
|
1928
2163
|
if (platform === 'darwin') {
|
|
1929
2164
|
// PKG 禁止重定位;安装后再次核对固定路径的 bundle ID 与版本再启动。
|
|
1930
2165
|
const installedApp = await findInstalledMacApp()
|
|
1931
|
-
|
|
2166
|
+
// 不要使用 open -n:它会强制创建第二个实例,而主进程的单实例锁
|
|
2167
|
+
// 会让这个新进程立即退出,表现为 Dock 图标闪一下就消失。
|
|
2168
|
+
// 后台更新助手调用 LaunchServices 时可能只返回成功但没有真正拉起应用。
|
|
2169
|
+
// 直接启动已经核对过的 bundle executable,避免依赖 open 的 GUI 会话。
|
|
2170
|
+
const executable = join(installedApp, 'Contents', 'MacOS', 'Desktop Pet')
|
|
2171
|
+
await trace('direct-launch-requested', { installedApp, executable })
|
|
2172
|
+
const env = { ...process.env }
|
|
2173
|
+
delete env.ELECTRON_RUN_AS_NODE
|
|
2174
|
+
const child = spawn(executable, [], { detached: true, stdio: 'ignore', env })
|
|
2175
|
+
child.on('spawn', () => { void trace('direct-launch-spawned', { childPid: child.pid }) })
|
|
2176
|
+
child.on('error', (error) => { void trace('direct-launch-failed', { error: String(error) }) })
|
|
2177
|
+
child.unref()
|
|
1932
2178
|
return
|
|
1933
2179
|
}
|
|
1934
2180
|
restartFallback()
|
|
@@ -1938,32 +2184,50 @@ const shellQuote = (value) => "'" + value.replace(/'/g, "'\\''") + "'"
|
|
|
1938
2184
|
async function install() {
|
|
1939
2185
|
if (platform === 'darwin') {
|
|
1940
2186
|
const command = '/usr/sbin/installer -pkg ' + shellQuote(sourcePath) + ' -target /'
|
|
1941
|
-
await run('/usr/bin/osascript', ['-e', 'do shell script ' + JSON.stringify(command) + ' with administrator privileges'])
|
|
2187
|
+
await run('install-macos-pkg', '/usr/bin/osascript', ['-e', 'do shell script ' + JSON.stringify(command) + ' with administrator privileges'])
|
|
1942
2188
|
return
|
|
1943
2189
|
}
|
|
1944
2190
|
if (platform === 'win32') {
|
|
1945
2191
|
// 发布端须提供支持 /S 静默安装的签名 NSIS 安装器。
|
|
1946
|
-
await run(sourcePath, ['/S'])
|
|
2192
|
+
await run('install-windows-exe', sourcePath, ['/S'])
|
|
1947
2193
|
return
|
|
1948
2194
|
}
|
|
1949
2195
|
throw new Error('unsupported platform')
|
|
1950
2196
|
}
|
|
1951
2197
|
|
|
1952
2198
|
async function main() {
|
|
1953
|
-
|
|
2199
|
+
await trace('helper-start', {
|
|
2200
|
+
parentPid,
|
|
2201
|
+
sourcePath,
|
|
2202
|
+
expectedSHA256,
|
|
2203
|
+
platform,
|
|
2204
|
+
targetVersion,
|
|
2205
|
+
fallbackExecutable,
|
|
2206
|
+
fallbackArgs
|
|
2207
|
+
})
|
|
2208
|
+
await waitForParentExit()
|
|
1954
2209
|
|
|
1955
2210
|
let installed = false
|
|
1956
2211
|
try {
|
|
1957
|
-
|
|
2212
|
+
await trace('hash-check-start', { sourcePath })
|
|
2213
|
+
const actualSHA256 = await sha256(sourcePath)
|
|
2214
|
+
await trace('hash-check-result', { matches: actualSHA256 === expectedSHA256 })
|
|
2215
|
+
if (actualSHA256 !== expectedSHA256) throw new Error('downloaded update hash changed')
|
|
2216
|
+
await trace('install-start', { platform, targetVersion })
|
|
1958
2217
|
await install()
|
|
1959
2218
|
installed = true
|
|
2219
|
+
await trace('install-success', { platform, targetVersion })
|
|
1960
2220
|
} catch (error) {
|
|
2221
|
+
await trace('install-failed', { error: String(error) })
|
|
1961
2222
|
await fs.writeFile(join(dirname(sourcePath), 'last-install-error.log'), String(error)).catch(() => undefined)
|
|
1962
2223
|
} finally {
|
|
1963
2224
|
if (installed) {
|
|
1964
2225
|
try {
|
|
2226
|
+
await trace('restart-start', { platform, targetVersion })
|
|
1965
2227
|
await restartInstalledApp()
|
|
2228
|
+
await trace('restart-command-finished', { platform, targetVersion })
|
|
1966
2229
|
} catch (error) {
|
|
2230
|
+
await trace('restart-failed', { error: String(error) })
|
|
1967
2231
|
await fs.writeFile(join(dirname(sourcePath), 'last-restart-error.log'), String(error)).catch(() => undefined)
|
|
1968
2232
|
restartFallback()
|
|
1969
2233
|
}
|
|
@@ -1973,7 +2237,10 @@ async function main() {
|
|
|
1973
2237
|
}
|
|
1974
2238
|
}
|
|
1975
2239
|
|
|
1976
|
-
main().catch(() =>
|
|
2240
|
+
main().catch(async (error) => {
|
|
2241
|
+
await trace('helper-failed', { error: String(error) })
|
|
2242
|
+
restartFallback()
|
|
2243
|
+
})
|
|
1977
2244
|
`;
|
|
1978
2245
|
function installerExtension(releaseUrl) {
|
|
1979
2246
|
const pathname = new URL(releaseUrl).pathname.toLowerCase();
|
|
@@ -1989,23 +2256,30 @@ function storedRelayURL() {
|
|
|
1989
2256
|
return void 0;
|
|
1990
2257
|
}
|
|
1991
2258
|
}
|
|
1992
|
-
function
|
|
2259
|
+
function updateServiceURL() {
|
|
2260
|
+
if (electron.app.isPackaged) return OFFICIAL_SERVER_ROOT;
|
|
2261
|
+
if (hasRuntimeServerOverride()) return configuredRelayURL();
|
|
1993
2262
|
const stored = storedRelayURL();
|
|
1994
|
-
if (stored && !
|
|
2263
|
+
if (stored && !isLegacyOfficialServerURL(stored)) return stored;
|
|
1995
2264
|
return configuredRelayURL();
|
|
1996
2265
|
}
|
|
1997
|
-
function
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2266
|
+
function updateServiceRootURL(value) {
|
|
2267
|
+
const url = new URL(value);
|
|
2268
|
+
if (url.protocol === "wss:") url.protocol = "https:";
|
|
2269
|
+
else if (url.protocol === "ws:") url.protocol = "http:";
|
|
2270
|
+
else if (url.protocol !== "https:" && url.protocol !== "http:") {
|
|
2271
|
+
throw new Error("更新服务器地址格式无效");
|
|
2003
2272
|
}
|
|
2273
|
+
url.pathname = url.pathname.replace(/\/ws\/?$/, "").replace(/\/+$/, "");
|
|
2274
|
+
url.search = "";
|
|
2275
|
+
url.hash = "";
|
|
2276
|
+
return url;
|
|
2004
2277
|
}
|
|
2005
2278
|
function validateUpdateAPIURL(value) {
|
|
2006
2279
|
const url = new URL(value);
|
|
2007
2280
|
const local = url.hostname === "127.0.0.1" || url.hostname === "localhost" || url.hostname === "::1";
|
|
2008
|
-
|
|
2281
|
+
const officialHTTPTunnel = isLegacyOfficialServerURL(value);
|
|
2282
|
+
if (url.protocol !== "https:" && !(officialHTTPTunnel || url.protocol === "http:" && local && !electron.app.isPackaged)) {
|
|
2009
2283
|
throw new Error("更新服务器必须使用 HTTPS");
|
|
2010
2284
|
}
|
|
2011
2285
|
return url;
|
|
@@ -2021,15 +2295,128 @@ function parseUpdate(value) {
|
|
|
2021
2295
|
}
|
|
2022
2296
|
const url = new URL(releaseUrl);
|
|
2023
2297
|
if (url.protocol !== "https:") throw new Error("更新下载地址必须使用 HTTPS");
|
|
2298
|
+
const sizeBytes = optionalPositiveInteger(value.sizeBytes);
|
|
2299
|
+
if (sizeBytes && sizeBytes > MAX_ARTIFACT_BYTES) throw new Error("更新包超过客户端允许的最大大小");
|
|
2300
|
+
const signature = parseManifestSignature(value.signature);
|
|
2024
2301
|
return {
|
|
2302
|
+
releaseId: optionalString(value.releaseId),
|
|
2303
|
+
channel: optionalString(value.channel),
|
|
2304
|
+
platform: optionalString(value.platform),
|
|
2305
|
+
arch: optionalString(value.arch),
|
|
2025
2306
|
version,
|
|
2026
2307
|
releaseUrl: url.toString(),
|
|
2027
2308
|
sha256: sha256.toLowerCase(),
|
|
2309
|
+
sizeBytes,
|
|
2028
2310
|
notes: optionalString(value.notes),
|
|
2029
2311
|
publishedAt: optionalString(value.publishedAt),
|
|
2030
|
-
|
|
2312
|
+
expiresAt: optionalString(value.expiresAt),
|
|
2313
|
+
mandatory: value.mandatory === true,
|
|
2314
|
+
signature
|
|
2031
2315
|
};
|
|
2032
2316
|
}
|
|
2317
|
+
function optionalPositiveInteger(value) {
|
|
2318
|
+
if (value === void 0 || value === null) return void 0;
|
|
2319
|
+
if (!Number.isSafeInteger(value) || Number(value) <= 0) {
|
|
2320
|
+
throw new Error("更新信息包含无效的安装包大小");
|
|
2321
|
+
}
|
|
2322
|
+
return Number(value);
|
|
2323
|
+
}
|
|
2324
|
+
function parseManifestSignature(value) {
|
|
2325
|
+
if (value === void 0 || value === null) return void 0;
|
|
2326
|
+
if (!isRecord(value)) throw new Error("更新清单签名格式无效");
|
|
2327
|
+
const keyId = asString(value.keyId);
|
|
2328
|
+
const algorithm = asString(value.algorithm);
|
|
2329
|
+
const signature = asString(value.value);
|
|
2330
|
+
if (!keyId || algorithm !== "ed25519" || !/^[A-Za-z0-9+/]{86}==$/.test(signature)) {
|
|
2331
|
+
throw new Error("更新清单签名格式无效");
|
|
2332
|
+
}
|
|
2333
|
+
return { keyId, algorithm, value: signature };
|
|
2334
|
+
}
|
|
2335
|
+
function verifyManifestSignature(update) {
|
|
2336
|
+
const trustedKeys = trustedManifestPublicKeys();
|
|
2337
|
+
if (!update.signature) {
|
|
2338
|
+
if (electron.app.isPackaged || Object.keys(trustedKeys).length > 0) throw new Error("更新清单缺少可信签名");
|
|
2339
|
+
return;
|
|
2340
|
+
}
|
|
2341
|
+
if (!update.releaseId || !update.channel || !update.platform || !update.arch || !update.sizeBytes || !update.publishedAt || !update.expiresAt) {
|
|
2342
|
+
throw new Error("签名更新清单缺少必要字段");
|
|
2343
|
+
}
|
|
2344
|
+
if (update.channel !== updateChannel() || update.platform !== process.platform || update.arch !== process.arch) {
|
|
2345
|
+
throw new Error("更新清单与当前渠道或平台不匹配");
|
|
2346
|
+
}
|
|
2347
|
+
const expiresAt = Date.parse(update.expiresAt);
|
|
2348
|
+
if (!Number.isFinite(expiresAt) || expiresAt < Date.now() - 6e4) {
|
|
2349
|
+
throw new Error("更新清单已过期,请重新检查");
|
|
2350
|
+
}
|
|
2351
|
+
const encodedKey = trustedKeys[update.signature.keyId];
|
|
2352
|
+
if (!encodedKey) throw new Error(`更新清单使用了不受信任的签名密钥:${update.signature.keyId}`);
|
|
2353
|
+
try {
|
|
2354
|
+
const publicKey = node_crypto.createPublicKey({ key: Buffer.from(encodedKey, "base64"), format: "der", type: "spki" });
|
|
2355
|
+
const payload = JSON.stringify([
|
|
2356
|
+
update.releaseId,
|
|
2357
|
+
update.channel,
|
|
2358
|
+
update.platform,
|
|
2359
|
+
update.arch,
|
|
2360
|
+
update.version,
|
|
2361
|
+
update.releaseUrl,
|
|
2362
|
+
update.sha256,
|
|
2363
|
+
update.sizeBytes,
|
|
2364
|
+
update.notes ?? "",
|
|
2365
|
+
update.publishedAt,
|
|
2366
|
+
update.expiresAt,
|
|
2367
|
+
update.mandatory
|
|
2368
|
+
]).replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
|
|
2369
|
+
if (!node_crypto.verify(null, Buffer.from(payload), publicKey, Buffer.from(update.signature.value, "base64"))) {
|
|
2370
|
+
throw new Error("signature mismatch");
|
|
2371
|
+
}
|
|
2372
|
+
} catch (error) {
|
|
2373
|
+
logger.warn("update manifest signature rejected", { error: String(error), keyId: update.signature.keyId });
|
|
2374
|
+
throw new Error("更新清单签名验证失败");
|
|
2375
|
+
}
|
|
2376
|
+
}
|
|
2377
|
+
function trustedManifestPublicKeys() {
|
|
2378
|
+
const embedded = '{"primary-2026":"MCowBQYDK2VwAyEAtXVi15v8FHIVJeGKf4Fjm94PGr1GzRFAStC7N2utTkc="}';
|
|
2379
|
+
const encoded = !electron.app.isPackaged && embedded === "{}" ? process.env.DESKTOP_PET_UPDATE_PUBLIC_KEYS_JSON?.trim() || embedded : embedded;
|
|
2380
|
+
try {
|
|
2381
|
+
const parsed = JSON.parse(encoded);
|
|
2382
|
+
if (!isRecord(parsed)) return {};
|
|
2383
|
+
return Object.fromEntries(Object.entries(parsed).filter((entry) => entry[0].length > 0 && typeof entry[1] === "string" && entry[1].length > 0));
|
|
2384
|
+
} catch {
|
|
2385
|
+
return {};
|
|
2386
|
+
}
|
|
2387
|
+
}
|
|
2388
|
+
function updateChannel() {
|
|
2389
|
+
const channel = process.env.DESKTOP_PET_UPDATE_CHANNEL?.trim() || "stable";
|
|
2390
|
+
return /^[a-z][a-z0-9-]{0,31}$/.test(channel) ? channel : "stable";
|
|
2391
|
+
}
|
|
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
|
+
}
|
|
2033
2420
|
function asString(value) {
|
|
2034
2421
|
return typeof value === "string" ? value.trim() : "";
|
|
2035
2422
|
}
|
|
@@ -2250,7 +2637,9 @@ function createPetWindow(skin, options) {
|
|
|
2250
2637
|
}
|
|
2251
2638
|
function updateMenuLabel() {
|
|
2252
2639
|
const status2 = getUpdateStatus();
|
|
2253
|
-
if (status2.kind === "available")
|
|
2640
|
+
if (status2.kind === "available") {
|
|
2641
|
+
return status2.update.mandatory ? `软件更新(必须升级到 v${status2.update.version})` : `软件更新(v${status2.update.version} 可下载)`;
|
|
2642
|
+
}
|
|
2254
2643
|
if (status2.kind === "downloading") {
|
|
2255
2644
|
return `软件更新(v${status2.update.version} 下载 ${downloadProgressText(status2)})`;
|
|
2256
2645
|
}
|
|
@@ -2266,7 +2655,8 @@ function updateMenuItems(petId) {
|
|
|
2266
2655
|
click: () => {
|
|
2267
2656
|
void checkForUpdates().then((result) => {
|
|
2268
2657
|
if (result.kind === "available") {
|
|
2269
|
-
|
|
2658
|
+
const message = result.update.mandatory ? `v${result.update.version} 是必须安装的重要更新` : `发现 v${result.update.version},可从“软件更新”安装`;
|
|
2659
|
+
dispatch({ type: "bubble", text: message, ttl: result.update.mandatory ? 9e3 : 6e3 }, petId);
|
|
2270
2660
|
} else if (result.kind === "up-to-date") {
|
|
2271
2661
|
dispatch({ type: "bubble", text: "已经是最新版本啦", ttl: 4500 }, petId);
|
|
2272
2662
|
} else if (result.kind === "downloading") {
|
|
@@ -2500,11 +2890,13 @@ function isRelayCommand(raw) {
|
|
|
2500
2890
|
const t = raw.type;
|
|
2501
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";
|
|
2502
2892
|
}
|
|
2893
|
+
const RELAY_BLOB_TRANSFER_TIMEOUT_MS = 10 * 6e4;
|
|
2503
2894
|
async function uploadRelayBlob(input) {
|
|
2504
2895
|
if (input.kind === "patch" && !input.baseCommit) throw new Error("patch artifact requires baseCommit");
|
|
2505
2896
|
const auth = getRelayHTTPAuth();
|
|
2506
2897
|
const peersResponse = await fetch(`${auth.baseURL}/api/tasks/${encodeURIComponent(input.taskId)}/peer-keys`, {
|
|
2507
|
-
headers: { Authorization: `Bearer ${auth.accessToken}` }
|
|
2898
|
+
headers: { Authorization: `Bearer ${auth.accessToken}` },
|
|
2899
|
+
signal: AbortSignal.timeout(15e3)
|
|
2508
2900
|
});
|
|
2509
2901
|
const peersBody = await peersResponse.json();
|
|
2510
2902
|
if (!peersResponse.ok) throw new Error(peersBody.message || "unable to load peer encryption keys");
|
|
@@ -2513,6 +2905,7 @@ async function uploadRelayBlob(input) {
|
|
|
2513
2905
|
const encrypted = encryptForRecipients(input.bytes, peers);
|
|
2514
2906
|
const artifactId = input.artifactId ?? node_crypto.randomUUID();
|
|
2515
2907
|
const sha256 = node_crypto.createHash("sha256").update(input.bytes).digest("hex");
|
|
2908
|
+
const normalizedKind = normalizeResultKind(input.kind);
|
|
2516
2909
|
const response = await fetch(
|
|
2517
2910
|
`${auth.baseURL}/api/tasks/${encodeURIComponent(input.taskId)}/artifacts/${encodeURIComponent(artifactId)}`,
|
|
2518
2911
|
{
|
|
@@ -2523,17 +2916,18 @@ async function uploadRelayBlob(input) {
|
|
|
2523
2916
|
"X-Artifact-Size": String(input.bytes.byteLength),
|
|
2524
2917
|
"X-Artifact-SHA256": sha256,
|
|
2525
2918
|
"X-Artifact-Direction": input.direction,
|
|
2526
|
-
"X-Artifact-Kind":
|
|
2919
|
+
"X-Artifact-Kind": normalizedKind,
|
|
2527
2920
|
"X-Artifact-Mime": input.mimeType,
|
|
2528
2921
|
"X-Artifact-Key-Wraps": Buffer.from(JSON.stringify(encrypted.keyWraps)).toString("base64url")
|
|
2529
2922
|
},
|
|
2530
|
-
body: Buffer.from(encrypted.ciphertext)
|
|
2923
|
+
body: Buffer.from(encrypted.ciphertext),
|
|
2924
|
+
signal: AbortSignal.timeout(RELAY_BLOB_TRANSFER_TIMEOUT_MS)
|
|
2531
2925
|
}
|
|
2532
2926
|
);
|
|
2533
2927
|
if (!response.ok) throw new Error(`relay blob upload failed (${response.status})`);
|
|
2534
2928
|
return {
|
|
2535
2929
|
id: artifactId,
|
|
2536
|
-
kind:
|
|
2930
|
+
kind: normalizedKind,
|
|
2537
2931
|
path: input.path,
|
|
2538
2932
|
mimeType: input.mimeType,
|
|
2539
2933
|
size: input.bytes.byteLength,
|
|
@@ -2550,7 +2944,10 @@ async function downloadRelayBlob(taskId, artifact) {
|
|
|
2550
2944
|
const auth = getRelayHTTPAuth();
|
|
2551
2945
|
const response = await fetch(
|
|
2552
2946
|
`${auth.baseURL}/api/tasks/${encodeURIComponent(taskId)}/artifacts/${encodeURIComponent(artifact.id)}`,
|
|
2553
|
-
{
|
|
2947
|
+
{
|
|
2948
|
+
headers: { Authorization: `Bearer ${auth.accessToken}` },
|
|
2949
|
+
signal: AbortSignal.timeout(RELAY_BLOB_TRANSFER_TIMEOUT_MS)
|
|
2950
|
+
}
|
|
2554
2951
|
);
|
|
2555
2952
|
if (!response.ok) throw new Error(`relay blob download failed (${response.status})`);
|
|
2556
2953
|
const metadataEncoded = response.headers.get("x-artifact-metadata") ?? "";
|
|
@@ -2848,8 +3245,59 @@ async function handleRelay(cmd, res) {
|
|
|
2848
3245
|
return;
|
|
2849
3246
|
}
|
|
2850
3247
|
case "delegate_task": {
|
|
2851
|
-
|
|
2852
|
-
|
|
3248
|
+
if (cmd.workspaceSnapshot && !isWorkspaceSnapshotStagingAvailable()) {
|
|
3249
|
+
throw new Error("当前中转服务器尚未支持安全的工作区快照门控,请先升级服务端");
|
|
3250
|
+
}
|
|
3251
|
+
const created = await delegateTask(cmd.envelope);
|
|
3252
|
+
if (!cmd.workspaceSnapshot) {
|
|
3253
|
+
ok(res, created);
|
|
3254
|
+
return;
|
|
3255
|
+
}
|
|
3256
|
+
if (created.task.taskId !== cmd.envelope.taskId) {
|
|
3257
|
+
if (created.task.status !== "created") {
|
|
3258
|
+
ok(res, created);
|
|
3259
|
+
return;
|
|
3260
|
+
}
|
|
3261
|
+
throw new Error("idempotencyKey 对应的旧任务仍在等待原工作区快照;请使用新的 idempotencyKey 重新派发");
|
|
3262
|
+
}
|
|
3263
|
+
if (created.task.status !== "created") {
|
|
3264
|
+
await cancelRemoteTask(created.task.taskId).catch(() => void 0);
|
|
3265
|
+
throw new Error("当前中转服务器不支持工作区快照上传前门控;已取消任务,请先升级服务端");
|
|
3266
|
+
}
|
|
3267
|
+
const declared = created.task.envelope.context.items.find(isWorkspaceSnapshotContextItem);
|
|
3268
|
+
if (!declared || declared.id !== cmd.workspaceSnapshot.artifactId) {
|
|
3269
|
+
await cancelRemoteTask(created.task.taskId).catch(() => void 0);
|
|
3270
|
+
throw new Error("本地工作区快照与任务声明不一致");
|
|
3271
|
+
}
|
|
3272
|
+
try {
|
|
3273
|
+
const source = resolveWorkspaceFile(
|
|
3274
|
+
cmd.workspaceSnapshot.workspaceRoot,
|
|
3275
|
+
cmd.workspaceSnapshot.path,
|
|
3276
|
+
true
|
|
3277
|
+
);
|
|
3278
|
+
const uploaded = await uploadRelayBlob({
|
|
3279
|
+
taskId: created.task.taskId,
|
|
3280
|
+
artifactId: cmd.workspaceSnapshot.artifactId,
|
|
3281
|
+
bytes: fs.readFileSync(source),
|
|
3282
|
+
direction: "context",
|
|
3283
|
+
kind: "file",
|
|
3284
|
+
mimeType: declared.mimeType,
|
|
3285
|
+
path: declared.path
|
|
3286
|
+
});
|
|
3287
|
+
if (uploaded.id !== declared.id || uploaded.size !== declared.size || uploaded.sha256 !== declared.sha256 || uploaded.artifactRef !== declared.artifactRef) {
|
|
3288
|
+
throw new Error("加密工作区快照上传结果与任务声明不一致");
|
|
3289
|
+
}
|
|
3290
|
+
const finalized = await finalizeRemoteTaskWorkspace(created.task.taskId);
|
|
3291
|
+
ok(res, finalized);
|
|
3292
|
+
} catch (err) {
|
|
3293
|
+
const current = await getRemoteTask(created.task.taskId).catch(() => void 0);
|
|
3294
|
+
if (current && current.status !== "created") {
|
|
3295
|
+
ok(res, { task: current });
|
|
3296
|
+
return;
|
|
3297
|
+
}
|
|
3298
|
+
await cancelRemoteTask(created.task.taskId).catch(() => void 0);
|
|
3299
|
+
throw err;
|
|
3300
|
+
}
|
|
2853
3301
|
return;
|
|
2854
3302
|
}
|
|
2855
3303
|
case "register_mcp_client": {
|
|
@@ -3141,32 +3589,6 @@ async function dutyLoop(adapter) {
|
|
|
3141
3589
|
}
|
|
3142
3590
|
}
|
|
3143
3591
|
}
|
|
3144
|
-
const PRODUCT_DATA_DIRECTORY = "Desktop Pet";
|
|
3145
|
-
const LEGACY_NPM_DATA_DIRECTORY = "desktop-pet-app";
|
|
3146
|
-
function configureRuntimeIdentity() {
|
|
3147
|
-
const profileDirectory = process.env.DESKTOP_PET_PROFILE?.trim();
|
|
3148
|
-
if (profileDirectory) {
|
|
3149
|
-
electron.app.setPath("userData", node_path.resolve(profileDirectory));
|
|
3150
|
-
return;
|
|
3151
|
-
}
|
|
3152
|
-
if (!electron.app.isPackaged && process.env.DESKTOP_PET_NPM_DISTRIBUTION !== "1") return;
|
|
3153
|
-
const appDataDirectory = electron.app.getPath("appData");
|
|
3154
|
-
const canonicalDirectory = node_path.join(appDataDirectory, PRODUCT_DATA_DIRECTORY);
|
|
3155
|
-
if (process.env.DESKTOP_PET_NPM_DISTRIBUTION === "1") {
|
|
3156
|
-
migrateLegacyNpmData(node_path.join(appDataDirectory, LEGACY_NPM_DATA_DIRECTORY), canonicalDirectory);
|
|
3157
|
-
}
|
|
3158
|
-
electron.app.setPath("userData", canonicalDirectory);
|
|
3159
|
-
}
|
|
3160
|
-
function migrateLegacyNpmData(legacyDirectory, canonicalDirectory) {
|
|
3161
|
-
if (!node_fs.existsSync(legacyDirectory) || node_fs.existsSync(canonicalDirectory)) return false;
|
|
3162
|
-
try {
|
|
3163
|
-
node_fs.renameSync(legacyDirectory, canonicalDirectory);
|
|
3164
|
-
return true;
|
|
3165
|
-
} catch (error) {
|
|
3166
|
-
console.warn(`无法迁移 Desktop Pet 用户数据:${String(error)}`);
|
|
3167
|
-
return false;
|
|
3168
|
-
}
|
|
3169
|
-
}
|
|
3170
3592
|
const trayIcon1xPath = path.join(__dirname, "./chunks/tray-iconTemplate-BIK86F-0.png");
|
|
3171
3593
|
const trayIcon2xPath = path.join(__dirname, "./chunks/tray-iconTemplate@2x-B19HpcaE.png");
|
|
3172
3594
|
let tray;
|
|
@@ -3201,20 +3623,46 @@ function createPetTemplateImage() {
|
|
|
3201
3623
|
return image;
|
|
3202
3624
|
}
|
|
3203
3625
|
configureRuntimeIdentity();
|
|
3626
|
+
function errorFields(error) {
|
|
3627
|
+
if (error instanceof Error) return { error: error.toString(), stack: error.stack };
|
|
3628
|
+
return { error: String(error) };
|
|
3629
|
+
}
|
|
3630
|
+
logger.info("app bootstrap", {
|
|
3631
|
+
pid: process.pid,
|
|
3632
|
+
ppid: process.ppid,
|
|
3633
|
+
platform: process.platform,
|
|
3634
|
+
arch: process.arch,
|
|
3635
|
+
isPackaged: electron.app.isPackaged,
|
|
3636
|
+
execPath: process.execPath,
|
|
3637
|
+
resourcesPath: process.resourcesPath,
|
|
3638
|
+
argv: process.argv.slice(1),
|
|
3639
|
+
userData: electron.app.getPath("userData")
|
|
3640
|
+
});
|
|
3641
|
+
process.on("uncaughtException", (error) => {
|
|
3642
|
+
logger.error("uncaught exception", errorFields(error));
|
|
3643
|
+
});
|
|
3644
|
+
process.on("unhandledRejection", (reason) => {
|
|
3645
|
+
logger.error("unhandled rejection", errorFields(reason));
|
|
3646
|
+
});
|
|
3204
3647
|
if (process.platform !== "win32") {
|
|
3205
3648
|
process.once("SIGTERM", () => electron.app.quit());
|
|
3206
3649
|
}
|
|
3207
3650
|
registerAccountProtocol();
|
|
3208
3651
|
const singleInstance = electron.app.requestSingleInstanceLock();
|
|
3652
|
+
let activationRequestedBeforeReady = false;
|
|
3209
3653
|
if (!singleInstance) {
|
|
3210
|
-
logger.warn("another instance is running, quitting"
|
|
3654
|
+
logger.warn("another instance is running, quitting", {
|
|
3655
|
+
pid: process.pid,
|
|
3656
|
+
execPath: process.execPath,
|
|
3657
|
+
argv: process.argv.slice(1)
|
|
3658
|
+
});
|
|
3211
3659
|
electron.app.quit();
|
|
3212
3660
|
} else {
|
|
3213
3661
|
electron.app.on("second-instance", (_event, argv) => {
|
|
3662
|
+
logger.warn("second instance received", { argv });
|
|
3214
3663
|
const callback = argv.find((value) => value.startsWith("desktop-pet://"));
|
|
3215
3664
|
if (callback) handleAccountCallback(callback);
|
|
3216
|
-
|
|
3217
|
-
ensurePrimaryPet();
|
|
3665
|
+
restorePetsOnAppActivation();
|
|
3218
3666
|
});
|
|
3219
3667
|
}
|
|
3220
3668
|
function ensurePrimaryPet() {
|
|
@@ -3223,30 +3671,84 @@ function ensurePrimaryPet() {
|
|
|
3223
3671
|
identity: { kind: "primary" }
|
|
3224
3672
|
});
|
|
3225
3673
|
}
|
|
3674
|
+
function hideMacDockIcon() {
|
|
3675
|
+
if (process.platform !== "darwin") return;
|
|
3676
|
+
if (electron.app.dock.isVisible()) electron.app.dock.hide();
|
|
3677
|
+
}
|
|
3678
|
+
function restorePetsOnAppActivation() {
|
|
3679
|
+
if (!electron.app.isReady()) {
|
|
3680
|
+
activationRequestedBeforeReady = true;
|
|
3681
|
+
logger.info("app activation deferred until ready");
|
|
3682
|
+
return;
|
|
3683
|
+
}
|
|
3684
|
+
const visibleBefore = petManager.arePetsVisible();
|
|
3685
|
+
const petsBefore = petManager.count();
|
|
3686
|
+
const appHiddenBefore = process.platform === "darwin" ? electron.app.isHidden() : void 0;
|
|
3687
|
+
if (process.platform === "darwin") electron.app.show();
|
|
3688
|
+
petManager.setPetsVisible(true);
|
|
3689
|
+
const firstPet = ensurePrimaryPet();
|
|
3690
|
+
hideMacDockIcon();
|
|
3691
|
+
logger.info("pets restored on app activation", {
|
|
3692
|
+
appHiddenBefore,
|
|
3693
|
+
appHiddenAfter: process.platform === "darwin" ? electron.app.isHidden() : void 0,
|
|
3694
|
+
dockVisibleAfter: process.platform === "darwin" ? electron.app.dock.isVisible() : void 0,
|
|
3695
|
+
visibleBefore,
|
|
3696
|
+
visibleAfter: petManager.arePetsVisible(),
|
|
3697
|
+
petsBefore,
|
|
3698
|
+
petsAfter: petManager.count(),
|
|
3699
|
+
firstPet
|
|
3700
|
+
});
|
|
3701
|
+
}
|
|
3702
|
+
if (process.platform === "darwin") {
|
|
3703
|
+
electron.app.on("activate", () => {
|
|
3704
|
+
logger.info("app activated");
|
|
3705
|
+
restorePetsOnAppActivation();
|
|
3706
|
+
});
|
|
3707
|
+
}
|
|
3226
3708
|
electron.app.whenReady().then(async () => {
|
|
3709
|
+
logger.info("app ready");
|
|
3710
|
+
hideMacDockIcon();
|
|
3711
|
+
logger.info("macOS dock icon state", {
|
|
3712
|
+
visible: process.platform === "darwin" ? electron.app.dock.isVisible() : void 0
|
|
3713
|
+
});
|
|
3227
3714
|
createMenuBar();
|
|
3715
|
+
logger.info("menu bar created");
|
|
3228
3716
|
await startAccountCallbackRouter();
|
|
3717
|
+
logger.info("account callback router started");
|
|
3229
3718
|
startEventServer(electron.app.getVersion());
|
|
3719
|
+
logger.info("event server start requested");
|
|
3230
3720
|
const activated = await activateStoredDevice().catch((err) => {
|
|
3231
3721
|
logger.warn("device authentication failed", { error: String(err) });
|
|
3232
3722
|
return false;
|
|
3233
3723
|
});
|
|
3724
|
+
logger.info("device activation finished", { activated });
|
|
3234
3725
|
if (!activated) startRelay();
|
|
3726
|
+
logger.info("relay start requested", { authenticated: activated });
|
|
3235
3727
|
if (process.env.DESKTOP_PET_P2P !== "0") startP2PWindow(getDeviceIceServers());
|
|
3728
|
+
logger.info("p2p start requested", { enabled: process.env.DESKTOP_PET_P2P !== "0" });
|
|
3236
3729
|
void startRunnerManager();
|
|
3237
3730
|
const id = ensurePrimaryPet();
|
|
3731
|
+
logger.info("primary pet initialization finished", { firstPet: id });
|
|
3732
|
+
if (activationRequestedBeforeReady) {
|
|
3733
|
+
activationRequestedBeforeReady = false;
|
|
3734
|
+
restorePetsOnAppActivation();
|
|
3735
|
+
}
|
|
3238
3736
|
setTimeout(() => {
|
|
3239
3737
|
void checkForUpdates().then((result) => {
|
|
3738
|
+
logger.info("update check finished", { kind: result.kind });
|
|
3240
3739
|
if (id && result.kind === "available") {
|
|
3241
3740
|
dispatch({
|
|
3242
3741
|
type: "bubble",
|
|
3243
|
-
text: `发现 v${result.update.version},右键“软件更新”可请求下载`,
|
|
3244
|
-
ttl: 7e3
|
|
3742
|
+
text: result.update.mandatory ? `v${result.update.version} 是必须安装的重要更新,请从右键“软件更新”完成升级` : `发现 v${result.update.version},右键“软件更新”可请求下载`,
|
|
3743
|
+
ttl: result.update.mandatory ? 12e3 : 7e3
|
|
3245
3744
|
}, id);
|
|
3246
3745
|
}
|
|
3247
3746
|
});
|
|
3248
3747
|
}, 1200);
|
|
3249
3748
|
logger.info("app started", { firstPet: id });
|
|
3749
|
+
}).catch((error) => {
|
|
3750
|
+
logger.error("app startup failed", errorFields(error));
|
|
3751
|
+
electron.app.exit(1);
|
|
3250
3752
|
});
|
|
3251
3753
|
electron.app.on("window-all-closed", () => {
|
|
3252
3754
|
if (process.platform !== "darwin") electron.app.quit();
|
|
@@ -3260,3 +3762,10 @@ electron.app.on("quit", () => {
|
|
|
3260
3762
|
void stopEventServer().catch((err) => logger.warn("event server shutdown failed", { error: String(err) }));
|
|
3261
3763
|
logger.info("app quit");
|
|
3262
3764
|
});
|
|
3765
|
+
electron.app.on("render-process-gone", (_event, webContents, details) => {
|
|
3766
|
+
logger.error("render process gone", {
|
|
3767
|
+
reason: details.reason,
|
|
3768
|
+
exitCode: details.exitCode,
|
|
3769
|
+
url: webContents.getURL()
|
|
3770
|
+
});
|
|
3771
|
+
});
|