desktop-pet-app 0.1.1 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/out/main/index.js
CHANGED
|
@@ -443,6 +443,32 @@ function showDesktopNotification(title, body) {
|
|
|
443
443
|
if (!electron.Notification.isSupported()) return;
|
|
444
444
|
new electron.Notification({ title, body }).show();
|
|
445
445
|
}
|
|
446
|
+
function p2pSigningPayload(message, fromDeviceId) {
|
|
447
|
+
const signalJSON = JSON.stringify(message.signal ?? null);
|
|
448
|
+
const signalHash = node_crypto.createHash("sha256").update(signalJSON).digest("hex");
|
|
449
|
+
return [
|
|
450
|
+
message.type,
|
|
451
|
+
message.taskId ?? "",
|
|
452
|
+
message.sessionId ?? "",
|
|
453
|
+
fromDeviceId,
|
|
454
|
+
message.toDeviceId ?? "",
|
|
455
|
+
message.expiresAt ?? "",
|
|
456
|
+
signalHash
|
|
457
|
+
].join("\n");
|
|
458
|
+
}
|
|
459
|
+
function verifyP2PSignal(message) {
|
|
460
|
+
if (!message.signature || !message.publicKey || !message.fromDeviceId || !message.taskId || !message.sessionId || !message.toDeviceId || !message.expiresAt) return false;
|
|
461
|
+
try {
|
|
462
|
+
return node_crypto.verify(
|
|
463
|
+
null,
|
|
464
|
+
Buffer.from(p2pSigningPayload(message, message.fromDeviceId)),
|
|
465
|
+
message.publicKey,
|
|
466
|
+
Buffer.from(message.signature, "base64")
|
|
467
|
+
);
|
|
468
|
+
} catch {
|
|
469
|
+
return false;
|
|
470
|
+
}
|
|
471
|
+
}
|
|
446
472
|
let serverURL = configuredRelayURL();
|
|
447
473
|
let userID = process.env.DESKTOP_PET_USER ?? os.hostname();
|
|
448
474
|
const SKIN = process.env.DESKTOP_PET_SKIN ?? "";
|
|
@@ -623,7 +649,16 @@ function ensureConnected() {
|
|
|
623
649
|
showCapabilityGapResult(msg.task);
|
|
624
650
|
notifyResultWaiters(msg.task);
|
|
625
651
|
} else if (msg.type.startsWith("p2p.") && msg.type !== "p2p.forwarded") {
|
|
626
|
-
|
|
652
|
+
if (!verifyP2PSignal(msg)) {
|
|
653
|
+
logger.warn("received invalid p2p signal", {
|
|
654
|
+
type: msg.type,
|
|
655
|
+
taskId: msg.taskId,
|
|
656
|
+
sessionId: msg.sessionId,
|
|
657
|
+
fromDeviceId: msg.fromDeviceId
|
|
658
|
+
});
|
|
659
|
+
} else {
|
|
660
|
+
for (const listener of p2pSignalListeners) listener(msg);
|
|
661
|
+
}
|
|
627
662
|
} else if (msg.type === "help.request" && msg.from) {
|
|
628
663
|
dispatch({ type: "state", state: "excited", ttl: 10 });
|
|
629
664
|
dispatch({ type: "bubble", text: `@${msg.from} 想请你帮忙,右键我可以同意或婉拒`, ttl: 12e3 });
|
|
@@ -954,14 +989,9 @@ function onP2PSignal(listener) {
|
|
|
954
989
|
async function sendP2PSignal(message) {
|
|
955
990
|
if (!signingPrivateKey) throw new Error("设备未配置 P2P 签名密钥");
|
|
956
991
|
const outgoing = { ...message, protocolVersion: TASK_PROTOCOL_VERSION };
|
|
957
|
-
const signature = crypto.sign(null, Buffer.from(p2pSigningPayload(outgoing)), signingPrivateKey).toString("base64");
|
|
992
|
+
const signature = crypto.sign(null, Buffer.from(p2pSigningPayload(outgoing, deviceID)), signingPrivateKey).toString("base64");
|
|
958
993
|
await relayRPC({ ...outgoing, signature }, 1e4);
|
|
959
994
|
}
|
|
960
|
-
function p2pSigningPayload(message) {
|
|
961
|
-
const signalJSON = JSON.stringify(message.signal ?? null);
|
|
962
|
-
const signalHash = crypto.createHash("sha256").update(signalJSON).digest("hex");
|
|
963
|
-
return [message.type, message.taskId, message.sessionId, deviceID, message.toDeviceId, message.expiresAt, signalHash].join("\n");
|
|
964
|
-
}
|
|
965
995
|
async function requestRemoteTaskContext(taskId, request) {
|
|
966
996
|
const response = await relayRPC(
|
|
967
997
|
{ type: "task.context.request", protocolVersion: TASK_PROTOCOL_VERSION, taskId, payload: request },
|
|
@@ -1550,7 +1580,15 @@ async function sendFriendChatPreferred(input) {
|
|
|
1550
1580
|
}
|
|
1551
1581
|
function onRendererSignal(event, message) {
|
|
1552
1582
|
if (!transportWindow || event.sender !== transportWindow.webContents) return;
|
|
1553
|
-
void sendP2PSignal(message).
|
|
1583
|
+
void sendP2PSignal(message).then(() => {
|
|
1584
|
+
if (message.type !== "p2p.ice-candidate") {
|
|
1585
|
+
logger.info("p2p signal forwarded", {
|
|
1586
|
+
type: message.type,
|
|
1587
|
+
taskId: message.taskId,
|
|
1588
|
+
sessionId: message.sessionId
|
|
1589
|
+
});
|
|
1590
|
+
}
|
|
1591
|
+
}).catch((err) => {
|
|
1554
1592
|
logger.warn("p2p signal failed", { type: message.type, taskId: message.taskId, error: String(err) });
|
|
1555
1593
|
if (message.type === "p2p.offer" && message.taskId.startsWith("chat.")) {
|
|
1556
1594
|
void fallbackChat(message.taskId.slice("chat.".length));
|
|
@@ -1560,6 +1598,18 @@ function onRendererSignal(event, message) {
|
|
|
1560
1598
|
function onChatEvent(event, message) {
|
|
1561
1599
|
if (!transportWindow || event.sender !== transportWindow.webContents || !message || typeof message !== "object") return;
|
|
1562
1600
|
const value = message;
|
|
1601
|
+
if (value.type === "diagnostic") {
|
|
1602
|
+
logger.info("p2p diagnostic", {
|
|
1603
|
+
stage: value.stage,
|
|
1604
|
+
sessionId: value.sessionId,
|
|
1605
|
+
signalType: value.signalType,
|
|
1606
|
+
connectionState: value.connectionState,
|
|
1607
|
+
iceConnectionState: value.iceConnectionState,
|
|
1608
|
+
iceGatheringState: value.iceGatheringState,
|
|
1609
|
+
error: value.error
|
|
1610
|
+
});
|
|
1611
|
+
return;
|
|
1612
|
+
}
|
|
1563
1613
|
if (value.type === "received" && value.text) {
|
|
1564
1614
|
dispatch({ type: "state", state: "excited", ttl: 6 });
|
|
1565
1615
|
dispatch({ type: "bubble", text: `${value.from ?? "好友"}:${value.text}`, ttl: 1e4 });
|
|
@@ -53,11 +53,16 @@ function decodeArtifactChunk(buffer) {
|
|
|
53
53
|
};
|
|
54
54
|
}
|
|
55
55
|
class IceCandidateBuffer {
|
|
56
|
-
constructor(maxPerSession = 64) {
|
|
56
|
+
constructor(maxPerSession = 64, maxSessions = 64) {
|
|
57
57
|
this.maxPerSession = maxPerSession;
|
|
58
|
+
this.maxSessions = maxSessions;
|
|
58
59
|
}
|
|
59
60
|
candidates = /* @__PURE__ */ new Map();
|
|
60
61
|
add(sessionId, candidate) {
|
|
62
|
+
if (!this.candidates.has(sessionId) && this.candidates.size >= this.maxSessions) {
|
|
63
|
+
const oldestSessionId = this.candidates.keys().next().value;
|
|
64
|
+
if (oldestSessionId) this.candidates.delete(oldestSessionId);
|
|
65
|
+
}
|
|
61
66
|
const queued = this.candidates.get(sessionId) ?? [];
|
|
62
67
|
if (queued.length < this.maxPerSession) queued.push(candidate);
|
|
63
68
|
this.candidates.set(sessionId, queued);
|
|
@@ -211,7 +216,13 @@ let iceServers = parseIceServers();
|
|
|
211
216
|
window.p2pApi.onStart((request) => void createOffer(request));
|
|
212
217
|
window.p2pApi.onChatSend((request) => void createOffer(request));
|
|
213
218
|
window.p2pApi.onSignal((message) => {
|
|
214
|
-
void receiveSignal(message).catch(() =>
|
|
219
|
+
void receiveSignal(message).catch((error) => {
|
|
220
|
+
reportDiagnostic("signal-error", message.sessionId, {
|
|
221
|
+
signalType: message.type,
|
|
222
|
+
error: error instanceof Error ? `${error.name}: ${error.message}` : String(error)
|
|
223
|
+
});
|
|
224
|
+
failSession(message.sessionId);
|
|
225
|
+
});
|
|
215
226
|
});
|
|
216
227
|
window.p2pApi.onConfig((config) => {
|
|
217
228
|
if (config.iceServers?.length) iceServers = config.iceServers;
|
|
@@ -228,24 +239,28 @@ async function createOffer(request) {
|
|
|
228
239
|
const session = sessions.get(sessionId);
|
|
229
240
|
session.channel = new ReliableArtifactChannel(channel);
|
|
230
241
|
session.operationId = request.operationId;
|
|
231
|
-
wireChatChannel(channel, request.toUsername, request.operationId, request.text);
|
|
242
|
+
wireChatChannel(channel, sessionId, request.toUsername, request.operationId, request.text);
|
|
232
243
|
const offer = await peer.createOffer();
|
|
233
244
|
await peer.setLocalDescription(offer);
|
|
245
|
+
reportDiagnostic("offer-created", sessionId, peerState(peer));
|
|
234
246
|
send({ type: "p2p.offer", taskId: request.taskId, toDeviceId: request.toDeviceId, sessionId, signal: offer });
|
|
235
247
|
}
|
|
236
248
|
async function receiveSignal(message) {
|
|
237
249
|
if (new Date(message.expiresAt).getTime() <= Date.now()) return;
|
|
238
|
-
if (
|
|
250
|
+
if (message.type !== "p2p.ice-candidate") {
|
|
251
|
+
reportDiagnostic("signal-received", message.sessionId, { signalType: message.type });
|
|
252
|
+
}
|
|
239
253
|
if (message.type === "p2p.offer" && message.fromDeviceId && message.signal) {
|
|
240
254
|
const peer = createPeer(message.taskId, message.sessionId, message.fromDeviceId);
|
|
241
255
|
peer.ondatachannel = (event) => {
|
|
242
256
|
sessions.get(message.sessionId).channel = new ReliableArtifactChannel(event.channel);
|
|
243
|
-
wireChatChannel(event.channel, message.from);
|
|
257
|
+
wireChatChannel(event.channel, message.sessionId, message.from);
|
|
244
258
|
};
|
|
245
259
|
await peer.setRemoteDescription(message.signal);
|
|
246
260
|
await flushPendingIceCandidates(message.sessionId, peer);
|
|
247
261
|
const answer = await peer.createAnswer();
|
|
248
262
|
await peer.setLocalDescription(answer);
|
|
263
|
+
reportDiagnostic("answer-created", message.sessionId, peerState(peer));
|
|
249
264
|
send({
|
|
250
265
|
type: "p2p.answer",
|
|
251
266
|
taskId: message.taskId,
|
|
@@ -270,40 +285,10 @@ async function receiveSignal(message) {
|
|
|
270
285
|
closeSession(message.sessionId);
|
|
271
286
|
}
|
|
272
287
|
}
|
|
273
|
-
async function verifySignal(message) {
|
|
274
|
-
if (!message.signature || !message.publicKey || !message.fromDeviceId) return false;
|
|
275
|
-
try {
|
|
276
|
-
const der = pemToBytes(message.publicKey);
|
|
277
|
-
const key = await crypto.subtle.importKey("spki", der, { name: "Ed25519" }, false, ["verify"]);
|
|
278
|
-
const signalJSON = JSON.stringify(message.signal ?? null);
|
|
279
|
-
const digest2 = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(signalJSON));
|
|
280
|
-
const hash = [...new Uint8Array(digest2)].map((value) => value.toString(16).padStart(2, "0")).join("");
|
|
281
|
-
const payload = [
|
|
282
|
-
message.type,
|
|
283
|
-
message.taskId,
|
|
284
|
-
message.sessionId,
|
|
285
|
-
message.fromDeviceId,
|
|
286
|
-
message.toDeviceId,
|
|
287
|
-
message.expiresAt,
|
|
288
|
-
hash
|
|
289
|
-
].join("\n");
|
|
290
|
-
return crypto.subtle.verify(
|
|
291
|
-
"Ed25519",
|
|
292
|
-
key,
|
|
293
|
-
Uint8Array.from(atob(message.signature), (char) => char.charCodeAt(0)),
|
|
294
|
-
new TextEncoder().encode(payload)
|
|
295
|
-
);
|
|
296
|
-
} catch {
|
|
297
|
-
return false;
|
|
298
|
-
}
|
|
299
|
-
}
|
|
300
|
-
function pemToBytes(value) {
|
|
301
|
-
const base64 = value.replace(/-----[^-]+-----/g, "").replace(/\s/g, "");
|
|
302
|
-
return Uint8Array.from(atob(base64), (char) => char.charCodeAt(0)).buffer;
|
|
303
|
-
}
|
|
304
288
|
function createPeer(taskId, sessionId, toDeviceId, timeoutMs = P2P_CONNECT_TIMEOUT_MS) {
|
|
305
289
|
const peer = new RTCPeerConnection({ iceServers });
|
|
306
290
|
const timer = setTimeout(() => {
|
|
291
|
+
reportDiagnostic("connect-timeout", sessionId, peerState(peer));
|
|
307
292
|
const operationId = sessions.get(sessionId)?.operationId;
|
|
308
293
|
if (operationId) window.p2pApi.emitChatEvent({ type: "failed", operationId });
|
|
309
294
|
send({ type: "p2p.failed", taskId, sessionId, toDeviceId });
|
|
@@ -316,6 +301,7 @@ function createPeer(taskId, sessionId, toDeviceId, timeoutMs = P2P_CONNECT_TIMEO
|
|
|
316
301
|
}
|
|
317
302
|
};
|
|
318
303
|
peer.onconnectionstatechange = () => {
|
|
304
|
+
reportDiagnostic("connection-state", sessionId, peerState(peer));
|
|
319
305
|
if (peer.connectionState === "connected") {
|
|
320
306
|
clearTimeout(timer);
|
|
321
307
|
send({ type: "p2p.ready", taskId, sessionId, toDeviceId });
|
|
@@ -327,9 +313,10 @@ function createPeer(taskId, sessionId, toDeviceId, timeoutMs = P2P_CONNECT_TIMEO
|
|
|
327
313
|
};
|
|
328
314
|
return peer;
|
|
329
315
|
}
|
|
330
|
-
function wireChatChannel(channel, peerUsername, operationId, outgoingText) {
|
|
316
|
+
function wireChatChannel(channel, sessionId, peerUsername, operationId, outgoingText) {
|
|
331
317
|
if (operationId && outgoingText) {
|
|
332
318
|
channel.addEventListener("open", () => {
|
|
319
|
+
reportDiagnostic("channel-open", sessionId);
|
|
333
320
|
channel.send(JSON.stringify({ type: "chat.message", text: outgoingText }));
|
|
334
321
|
window.p2pApi.emitChatEvent({ type: "sent", operationId });
|
|
335
322
|
}, { once: true });
|
|
@@ -366,6 +353,16 @@ async function flushPendingIceCandidates(sessionId, peer) {
|
|
|
366
353
|
function send(message) {
|
|
367
354
|
window.p2pApi.sendSignal({ ...message, expiresAt: new Date(Date.now() + 5 * 6e4).toISOString() });
|
|
368
355
|
}
|
|
356
|
+
function peerState(peer) {
|
|
357
|
+
return {
|
|
358
|
+
connectionState: peer.connectionState,
|
|
359
|
+
iceConnectionState: peer.iceConnectionState,
|
|
360
|
+
iceGatheringState: peer.iceGatheringState
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
function reportDiagnostic(stage, sessionId, fields = {}) {
|
|
364
|
+
window.p2pApi.emitChatEvent({ type: "diagnostic", stage, sessionId, ...fields });
|
|
365
|
+
}
|
|
369
366
|
function parseIceServers() {
|
|
370
367
|
try {
|
|
371
368
|
const env = __vite_import_meta_env__;
|
package/out/renderer/p2p.html
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<!doctype html>
|
|
2
2
|
<html lang="en">
|
|
3
|
-
<head><meta charset="UTF-8" /><title>Desktop Pet P2P</title> <script type="module" crossorigin src="./assets/p2p-
|
|
3
|
+
<head><meta charset="UTF-8" /><title>Desktop Pet P2P</title> <script type="module" crossorigin src="./assets/p2p-LRPQ9Jzg.js"></script>
|
|
4
4
|
</head>
|
|
5
5
|
<body></body>
|
|
6
6
|
</html>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "desktop-pet-app",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "AI desktop pet with MCP support and a self-hosted relay server",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"keywords": [
|
|
@@ -34,7 +34,8 @@
|
|
|
34
34
|
"test": "vitest run",
|
|
35
35
|
"package:mac": "npm run build && electron-builder --mac pkg --publish never",
|
|
36
36
|
"pack:check": "npm pack --dry-run",
|
|
37
|
-
"prepack": "npm run build",
|
|
37
|
+
"prepack": "npm run build && node scripts/prepare-npm-package.mjs",
|
|
38
|
+
"postpack": "node scripts/prepare-npm-package.mjs --restore",
|
|
38
39
|
"prepublishOnly": "npm run typecheck && npm test && npm run pack:check"
|
|
39
40
|
},
|
|
40
41
|
"build": {
|
|
@@ -65,8 +66,8 @@
|
|
|
65
66
|
"vitest": "^3.2.7"
|
|
66
67
|
},
|
|
67
68
|
"dependencies": {
|
|
68
|
-
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
69
69
|
"electron": "^33.2.0",
|
|
70
|
+
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
70
71
|
"ws": "^8.21.3",
|
|
71
72
|
"zod": "^4.5.4"
|
|
72
73
|
}
|