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
- for (const listener of p2pSignalListeners) listener(msg);
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).catch((err) => {
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(() => failSession(message.sessionId));
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 (!await verifySignal(message)) return;
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__;
@@ -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-Ch5g1MsK.js"></script>
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.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
  }