From 30d3e481e464923bc219b6f40609241222a33f86 Mon Sep 17 00:00:00 2001 From: JianMiau Date: Tue, 25 Aug 2026 14:56:59 +0800 Subject: [PATCH] =?UTF-8?q?=E5=8A=A0=E5=85=A5=E6=BB=91=E9=BC=A0=E4=BA=8B?= =?UTF-8?q?=E4=BB=B6=E7=B4=80=E9=8C=84=E8=88=87=E9=8E=96=E5=AE=9A=EF=BC=8F?= =?UTF-8?q?=E8=A7=A3=E9=8E=96=E8=99=95=E7=90=86=EF=BC=8C=E8=BF=BD=E6=9F=A5?= =?UTF-8?q?=E8=A7=A3=E9=8E=96=E5=BE=8C=E9=BB=9E=E4=B8=8D=E5=88=B0=E5=AF=B5?= =?UTF-8?q?=E7=89=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 根本原因: 使用者回報登出/鎖定螢幕再回來後就不能拖曳、不能右鍵。現場量測顯示 OS 這一層正常(點穿位元會切、WindowFromPoint 打到寵物視窗),但事件紀錄裡 完全沒有拖曳或選單的痕跡,代表問題在 renderer 有沒有收到滑鼠事件這一層, 而這一層目前沒有任何可觀測資料,無法判斷。 影響: 解鎖後偶發點不到寵物,且發生時查不出卡在哪裡。 修法: 1. renderer 把 mousedown/mouseup(含按鍵與座標、當時的 pressed/drag)與 hover 進出寫進事件紀錄;pet:state 帶 rendererMouse(pressed/drag/hovered /overlay),/state 可直接查。 2. 主程序監聽 powerMonitor 的 lock-screen/unlock-screen/suspend/resume: 記錄到事件紀錄與 /state.mouse.sessionEvents,解鎖或喚醒後重新套用目前的 點穿狀態、showInactive 並重宣告置頂。 版號 2.0.12。 Co-Authored-By: Claude Fable 5 --- main.js | 23 ++++++++++++++++++++++- package-lock.json | 4 ++-- package.json | 2 +- renderer/renderer.js | 13 ++++++++++++- 4 files changed, 37 insertions(+), 5 deletions(-) diff --git a/main.js b/main.js index f9bdbba..f851284 100644 --- a/main.js +++ b/main.js @@ -1,5 +1,5 @@ "use strict"; -const { app, BrowserWindow, Tray, Menu, screen, ipcMain, nativeImage, shell, dialog } = require("electron"); +const { app, BrowserWindow, Tray, Menu, screen, ipcMain, nativeImage, shell, dialog, powerMonitor } = require("electron"); const http = require("node:http"); const fs = require("node:fs"); const path = require("node:path"); @@ -69,6 +69,7 @@ const mouseDiag = { toggles: 0, lastToggleAt: null, polls: 0, lastPollAt: null, rel: null, inBox: false, inBubble: false, rendererErrors: 0, lastRendererError: null, dragEnds: [], + sessionEvents: [], // 鎖定/解鎖/睡眠/喚醒 }; let currentState = { anim: "idle", base: "idle", sessions: 0 }; @@ -374,6 +375,25 @@ function reassertTopmost() { } catch { /* 視窗正在關閉就算了 */ } } +// 鎖定/解鎖與睡眠喚醒:session 切換後 Windows 可能把視窗的輸入或 z-order 狀態弄掉, +// 記下來並把目前的點穿狀態與置頂重新套用一次 +function watchSessionChanges() { + for (const ev of ["lock-screen", "unlock-screen", "suspend", "resume"]) { + powerMonitor.on(ev, () => { + appendLog(`power: ${ev}`); + mouseDiag.sessionEvents.push({ at: Date.now(), ev }); + if (mouseDiag.sessionEvents.length > 10) mouseDiag.sessionEvents.shift(); + if (ev !== "unlock-screen" && ev !== "resume") return; + if (!win || win.isDestroyed()) return; + try { + win.setIgnoreMouseEvents(ignoring, { forward: true }); + win.showInactive(); + } catch { /* 視窗正在關閉 */ } + reassertTopmost(); + }); + } +} + function startTopmostGuard() { topmostTimer = setInterval(reassertTopmost, TOPMOST_REASSERT_MS); // 螢幕配置變動最容易觸發降級,變動後立刻補一次 @@ -735,6 +755,7 @@ if (!app.requestSingleInstanceLock()) { startServer(); startCursorPolling(); startTopmostGuard(); + watchSessionChanges(); appendLog(`started pet=${activePet()?.id || "none"} scale=${config.scale} port=${config.port}`); }); diff --git a/package-lock.json b/package-lock.json index 645446d..08ed285 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "claude-pet", - "version": "2.0.11", + "version": "2.0.12", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "claude-pet", - "version": "2.0.11", + "version": "2.0.12", "devDependencies": { "electron": "^43.4.1", "electron-builder": "^26.0.12" diff --git a/package.json b/package.json index f162348..e3bca0e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "claude-pet", - "version": "2.0.11", + "version": "2.0.12", "description": "Codex Pets 相容的 Claude Code 桌面寵物(spriteVersionNumber 2:9 個動作列 + 16 方向追視)", "main": "main.js", "private": true, diff --git a/renderer/renderer.js b/renderer/renderer.js index b069a39..e613d4e 100644 --- a/renderer/renderer.js +++ b/renderer/renderer.js @@ -518,7 +518,11 @@ function handleTest(p) { function report() { const base = baseState(); - const snapshot = JSON.stringify({ anim: player.anim, base, sessions: sessions.size, pet: petInfo?.id || null }); + const snapshot = JSON.stringify({ + anim: player.anim, base, sessions: sessions.size, pet: petInfo?.id || null, + // renderer 這邊的滑鼠狀態也露到 /state,查「點不到寵物」時才看得出是不是卡在這裡 + rendererMouse: { pressed: !!pressed, drag: !!drag, hovered, overlay: overlay?.anim || null }, + }); if (snapshot !== lastReported) { lastReported = snapshot; window.pet.send("pet:state", JSON.parse(snapshot)); @@ -650,6 +654,8 @@ function overPetArea(x, y) { function setHovered(on) { if (on === hovered) return; hovered = on; + log(`hover ${on ? "enter" : "leave"} pressed=${!!pressed} drag=${!!drag} overlay=${overlay?.anim || "-"}`); + report(); if (on) { // 只在「進入」時觸發一次;一次性動作或拖曳進行中不打斷 if (!overlay && !drag) { @@ -689,15 +695,19 @@ document.addEventListener("mouseout", (e) => { setHovered(false); }); +// 滑鼠按下/放開都寫進事件紀錄:這是「有沒有收到滑鼠事件」唯一的直接證據 window.addEventListener("mousedown", (e) => { + log(`mousedown b=${e.button} at ${e.clientX},${e.clientY} pressed=${!!pressed} drag=${!!drag}`); if (e.button === 0) { pressed = { x: e.clientX, y: e.clientY, sx: e.screenX, sy: e.screenY, moved: false }; } else if (e.button === 2) { window.pet.send("pet:context-menu"); } + report(); }); window.addEventListener("mouseup", (e) => { + log(`mouseup b=${e.button} at ${e.clientX},${e.clientY} pressed=${!!pressed} moved=${!!pressed?.moved}`); if (e.button !== 0 || !pressed) return; const wasDrag = pressed.moved; pressed = null; @@ -728,6 +738,7 @@ window.pet.on("pet:drag-ended", () => { pressed = null; drag = null; canvas.classList.remove("grabbing"); + report(); }); // 主程序切了點穿狀態:只負責換游標樣式