逐項對照 Codex 校正所有行為邏輯

摘要:
從 Codex app 的 app.asar 讀出寵物的完整行為邏輯(狀態決策、優先序、到期、hover 版面、
初次問候、拖曳、reduced motion),逐項對照後校正本程式。

根本原因:
先前只對齊了動畫時序,狀態機本身仍是自訂的,與 Codex 有多處不一致:
1. 跨 session 優先序是 waiting>running>review>idle,但 Codex 的 Ai() 是
   waiting>failed>review>running,review 其實高於 running。
2. Stop 用 jumping 一次 + 6 秒 review,Codex 是 review 持續狀態直到該 thread 被讀取。
3. StopFailure 用 failed 播兩次就回 idle,Codex 的 failed 是持續狀態。
4. 工具失敗(PostToolUseFailure)會播 failed,Codex 中單一工具失敗不改變 turn 狀態。
5. 每個 hook 事件都重播動畫,Codex 只在狀態改變時重播(effect 相依只有 state)。
6. hover 是「跳躍」,Codex 實際的 respondToHover 全 app 沒人開啟;真正的 hover 行為是
   proximity 版面:40px 內人物縮 0.9、下移、露出控制鈕,56px 外退出。
7. 追視綁滑鼠且預設開,但 Codex 的追視來源是 quick chat 文字游標/computer-use 游標,
   不是滑鼠。
8. 沒有 first-awake 問候;狀態到期時間、拖曳方向閾值、reduced motion、預設位置也都沒對齊。

影響:
多 session 時顯示的狀態可能不對(running 蓋掉 review);完成與失敗的表現和 Codex 差很多;
待機期間仍可能因連續事件一直重播;hover 行為與 Codex 完全不同。

修法(數值皆取自 codex-pet-assets / avatar-overlay-native-frame / 主程序):
- 優先序改為 waiting(4)>failed(3)>review(2)>running(1)>idle(0)(對應 Ai())。
- 到期改為 failed 1h、waiting 24h、review 7d、running/idle 不到期(對應 Ei()),
  另保留一個 Codex 沒有的安全網:running 30 分鐘無事件視為終端機關閉。
- Stop→review 持續狀態;StopFailure→failed 持續狀態;PostToolUseFailure 只給氣泡不改狀態;
  idle_prompt 不再算 waiting。
- 動畫只在狀態改變時重播(移除 pulse)。
- hover 改為 proximity 版面:enter 40px / exit 56px,人物縮 0.9、下移 14px、
  200ms 過渡,上方露出 24px 選單鈕(對應 hoverControlSize/hoverOffsetY/hoverControlGap)。
- 追視改為選配、預設關;first-awake 問候每隻寵物一次(記在設定檔 greetedPetIds);
  拖曳方向改為單次位移 >= 4px 才決定(對應 Codex 的取樣邏輯);
  reduced motion 只顯示第一格;預設位置改為工作區右下各 24px;尺寸檔位對齊 80–224px 範圍。
- README 新增「與 Codex 的對照」整節,逐項列出相同與刻意不同之處。

驗證:
- baseState 單元測試:優先序(review>running、waiting>failed、failed>review)與各狀態
  到期(failed 1h、waiting 24h、review 7d、running 安全網、idle 清理)全數通過。
- 啟動實機以 HTTP /event 灌事件、/state 讀回:Stop→review、review 蓋過 running、
  授權→waiting、waiting>failed、failed>running、工具失敗不改狀態,皆正確。
- 桌面截圖:寵物正常顯示;滑鼠移到人物 40px 內時人物縮小下移、上方浮出圓形選單鈕,
  與 Codex 的 hover 版面一致。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-21 15:11:56 +08:00
co-authored by Claude Fable 5
parent 7d4c5d849c
commit f886df554e
6 changed files with 216 additions and 77 deletions
+49 -17
View File
@@ -20,6 +20,7 @@
- [新增 / 切換寵物](#新增--切換寵物)
- [設定檔](#設定檔)
- [架構與檔案](#架構與檔案)
- [與 Codex 的對照](#與-codex-的對照)
- [HTTP API](#http-api)
- [打包成 exe](#打包成-exe)
- [疑難排解](#疑難排解)
@@ -30,7 +31,8 @@
## 特色
- **完整實作 Codex V2 pet contract** — 8 欄 × 11 列、192×208 cell 的 spritesheet9 個標準動作列 + 16 個看向方向,**每一幀的時長都照 Codex 規格**,動起來跟在 Codex app 裡一模一樣。
- **16 方向追視** — 游標在哪,她就看哪(000 = 正上方、順時針 22.5° 一格)。追視時是靜態擺姿、完全不動,與 Codex 相同;游標靜止 6 秒才改播慢速 idle
- **行為邏輯逐項對照 Codex** — 狀態優先序、各狀態到期時間、動畫時序、hover 版面、初次問候、拖曳方向判定……都是從 Codex app 的 `app.asar` 讀出來的數值(見[與 Codex 的對照](#與-codex-的對照)
- **16 方向追視(選配,預設關)** — 開啟後游標在哪她就看哪(000 = 正上方、順時針 22.5° 一格),追視時是靜態擺姿。Codex 的寵物**不看滑鼠**——它看的是 quick chat 的文字游標與 computer-use 的虛擬游標,所以預設關閉以求一致;喜歡的話在選單打開。
- **自動修正圖檔缺陷** — Codex 的 hatch 產生器有時會把整列 look 格子畫得比 idle 小(內附的小念第 10 列就小了 22%,游標一往下移人物就縮水)。以整列中位數偵測、用腳底當支點放大回正確身高;單格的姿勢差異不動。Codex 本身沒有做這件事。
- **與 Codex 同尺寸** — 100% 就是 Codex 桌面寵物的大小(126 × 137),高度用 Codex 的 `ceil(寬 × 208/192)` 算,兩邊並排看起來一樣大。
- **真正的桌面寵物** — 透明、無邊框、永遠置頂;透明區域**點穿**,只有壓在人物像素上滑鼠才會被吃掉,不擋你操作底下的視窗。
@@ -85,8 +87,8 @@ npm run autostart:install # (可選)開機自動啟動
| 左鍵點一下 | 揮手(若有非固定的氣泡,先關閉氣泡) |
| 左鍵拖曳 | 搬家;往左播 running-left、往右播 running-right;放開後位置寫入設定檔 |
| 右鍵 / 系統匣左鍵 | 開啟選單 |
| 滑鼠移到她身上 | 跳躍 3 次Codex 的 hover 行為),離開後回到原本狀態 |
| 滑鼠靠近 | 盯著游標看(可在選單關閉 |
| 滑鼠靠近(40 px 內) | 她縮到 0.9、往下讓出位置,上方出現選單鈕;離開 56 px 外才復原Codex 的 hover 版面) |
| 滑鼠靠近 | 盯著游標看(選單開啟「跟著游標看」後才有 |
**選單功能**
@@ -103,34 +105,36 @@ npm run autostart:install # (可選)開機自動啟動
| Claude Code hook | 條件 | 動作 | 氣泡 |
|---|---|---|---|
| `SessionStart` | `source = startup` | `waving` 一次 → `idle` | 👋 嗨!(專案 |
| (程式啟動) | 該寵物第一次出現 | `waving` × 3 → 慢速 `idle` | 👋 嗨,我是〈寵物名〉(8 秒 |
| `SessionStart` | `source = startup` | `idle` | 👋 嗨!(專案) |
| `UserPromptSubmit` | | `running` | (關閉舊氣泡) |
| `PreToolUse` | 一般工具 | `running` | |
| `PreToolUse` | `AskUserQuestion` | `waiting` | ❓ 有問題想問你 |
| `PreToolUse` | `ExitPlanMode` | `waiting` | 📋 計畫等你確認 |
| `PostToolUse` | | `running` | |
| `PostToolUseFailure` | | `failed` 一次 → `running` | 😵 〈工具〉失敗了 |
| `PostToolUseFailure` | | `running`(不變) | 😵 〈工具〉失敗了 |
| `PermissionRequest` | | `waiting` | 🔐 需要你授權:〈工具〉 |
| `Notification` | `permission_prompt` | `waiting` | 🔐 需要你授權 |
| `Notification` | `idle_prompt` | `waiting` | 💤 在等你回覆 |
| `Notification` | `idle_prompt` | (不變) | 💤 在等你回覆 |
| `Notification` | `elicitation_dialog` | `waiting` | ❓ 有問題想問你 |
| `Notification` | 其他 | (不變) | 💬 訊息內容 |
| `Stop` | `stop_hook_active = false` | `jumping` 一次 → `review` 6 秒 → `idle` | ✅ 完成!(專案) |
| `StopFailure` | | `failed` 兩次 → `idle` | ❌ 錯誤訊息 |
| `Stop` | `stop_hook_active = false` | `review`,直到該 session 下一個動作/結束/7 天 | ✅ 完成!(專案) |
| `StopFailure` | | `failed`,直到該 session 下一個動作/結束/1 小時 | ❌ 錯誤訊息 |
| `SubagentStart` / `SubagentStop` | | `running` | |
| `PreCompact` | | `running` | 🗜️ 整理記憶中… |
| `SessionEnd` | | 移除該 session | 最後一個結束時 👋 掰掰 |
| (無事件) | 游標移動 | 16 方向 `look` | |
| (無事件) | 拖曳 | `running-left` / `running-right` | |
| (無事件) | 滑鼠移到人物像素上 | `jumping` × 3 | |
| (無事件) | 游標移動(追視開啟時) | 16 方向 `look`,覆蓋所有狀態 | |
| (無事件) | 拖曳 | 單次位移 ≥ 4 px 才決定 `running-left` / `running-right` | |
| (無事件) | 游標靠近 40 px 內 | 動畫不變;人物縮 0.9、顯示選單鈕 | |
規則:
- **待機時幾乎不動** — idle 每幀時長是合約值的 **6 倍**(一輪 6.6 秒,不是 1.1 秒),與 Codex 的 `codex-pet-assets` 一致。
- **非 idle 狀態播 3 次就沉澱** 成慢速 idle,也和 Codex 相同;但每收到一個 hook 事件就重播一次,所以工作中她會隨著每次工具呼叫動一下,安靜下來才靜下來
- **一次性動作**waving / jumping / failed)播完自動回到該 session 的基礎狀態。
- **基礎狀態**依 session 彙整:`waiting(3) > running(2) > review(1) > idle(0)`,取最高者顯示。
- 30 分鐘沒有任何事件的 session 會被視為結束而忽略(避免沒送 `SessionEnd` 的終端機卡住狀態)
- **非 idle 狀態播 3 次就沉澱** 成慢速 idle。動畫只在**狀態改變**時重新開始(Codex 的 effect 相依只有 state),工作中連續的工具呼叫不會讓她一直動
- **一次性動作**點擊揮手、初次問候)播完自動回到基礎狀態。
- **基礎狀態**依 session 彙整:`waiting(4) > failed(3) > review(2) > running(1) > idle(0)`,取最高者顯示——與 Codex 通知排序 `Ai()` 相同,注意 **review 高於 running**
- **到期**與 Codex 的 `Ei()` 相同:`failed` 1 小時、`waiting` 24 小時、`review` 7 天後退回 idle`running``idle` 不到期。另有一個 Codex 沒有的安全網:`running` 30 分鐘沒任何事件視為終端機已被關掉
- **系統「減少動態效果」開啟時**只顯示每個狀態的第一格,與 Codex 相同。
- `waiting` 的氣泡是固定的,狀態離開 `waiting` 時自動消失。
## 寵物包格式(Codex V2
@@ -198,7 +202,8 @@ Spritesheet**1536 × 2288**8 欄 × 11 列,每格 **192 × 208**,透
| `scale` | `1` | 縮放:0.5 / 0.75 / 1 / 1.25 / 1.5。`1` 表示 126 × 137,與 Codex 桌面寵物同大小 |
| `position` | `null` | 視窗左上角座標;`null` 為主螢幕右下角。若座標落在所有螢幕之外會自動拉回 |
| `alwaysOnTop` | `true` | 永遠置頂 |
| `followCursor` | `true` | 追視游標 |
| `followCursor` | `false` | 追視游標Codex 的寵物不看滑鼠,預設關) |
| `greetedPetIds` | `[]` | 已打過招呼的寵物 id;刪掉可讓該寵物下次啟動再揮手一次 |
| `port` | `17333` | 接收 hook 事件的本機埠(改了之後 hook 端要設環境變數 `CLAUDE_PET_PORT` |
事件紀錄:`%USERPROFILE%\.claude-pet\events.log`(超過 2 MB 自動清空)。
@@ -233,11 +238,38 @@ Claude Code ──hookstdin JSON)──▶ hook/claude-pet-hook.js ──PO
- **點穿**:視窗預設 `setIgnoreMouseEvents(true, { forward: true })`renderer 在 mousemove 時讀 canvas 該點 alpha(或是否在氣泡上)來即時切換,所以透明處永遠點得到底下的視窗。
- **拖曳**:由主程序用 `screen.getCursorScreenPoint()` 每 16 ms 更新視窗位置,游標離開視窗也不會掉;方向由水平位移決定。
- **動畫時序完全照 Codex**`IDLE_SLOWDOWN = 6``STATE_REPEATS = 3`、追視死區 1 px、追視參考點為人物方框中心——這幾個值都是從 Codex app 的 `resources/app.asar` 裡的 `codex-pet-assets` 讀出來的(`_ = 6``[...n,...n,...n]``N = 1`)。唯一刻意不同的是:Codex 的追視會覆蓋所有狀態,本程式只在 idle 時追視,否則工作中的動畫會被蓋掉、看不出 Claude Code 在忙
- **行為邏輯以 Codex 為準**,細節見[與 Codex 的對照](#與-codex-的對照)
- **顯示尺寸與 sprite 來源尺寸分開**:`CELL_W/CELL_H`192 × 208)只用來從 spritesheet 取格子,畫到畫面上的是 `PET_W/PET_H`126 × 137Codex 的 mascot 尺寸)。視窗寬度另有 `MIN_WIN_W = 240` 的下限,免得寵物變小後氣泡被截掉。
- **Spritesheet 以 data URL 傳給 renderer**,避免 `file://` 跨來源污染 canvas 導致 `getImageData` 失敗。
- **hook 合併規則**:以指令中是否含 `claude-pet-hook` 辨識自己的項目,重裝只替換自己的、不動別人的。
## 與 Codex 的對照
所有數值都是從 Codex app`WindowsApps\OpenAI.Codex_*\app\resources\app.asar`)的 `codex-pet-assets``avatar-overlay-native-frame` 與主程序讀出來的。
| 項目 | Codex | 本程式 |
|---|---|---|
| idle 速度 | 合約幀長 × 6(`_ = 6` | 相同 |
| 非 idle 狀態 | 播 3 次 → 慢速 idle(`[...n,...n,...n]` | 相同 |
| 動畫何時重播 | 只在狀態改變時(effect 相依 `[state, lookFrame, reducedMotion]` | 相同 |
| 跨 thread 優先序 | `waiting 0 > failed 1 > review 2 > running 3 > idle 4``Ai()` | 相同 |
| 狀態到期 | failed 1 h、waiting 24 h、review 7 d、running/idle 不到期(`Ei()` | 相同,另加 running 30 分鐘無事件的安全網 |
| `failed` | 持續狀態(turn 失敗/取消) | 相同(`StopFailure`);工具失敗不算 |
| `waiting` | 授權、`requestUserInput`、計畫待確認 | 相同;`idle_prompt` 不算 |
| `review` | turn 完成且未讀,直到使用者回到該 thread | 直到該 session 下一個動作 |
| `waving` | 只在 first-awake(每隻寵物一次,8 秒) | 相同(記在設定檔 `greetedPetIds` |
| `jumping` | 只在 `respondToHover`,而整個 app 沒人開啟它 | 只在選單「動作測試」 |
| hover | 40 px 內進入、56 px 外退出;人物縮 0.9、下移 14 px、上方 24 px 控制鈕、200 ms | 相同;控制鈕是選單 |
| 追視 | 看 quick chat 文字游標/computer-use 游標,死區 1 px,覆蓋所有狀態 | 沒有對應來源;改為**選配**看滑鼠,預設關 |
| 拖曳方向 | 單次取樣水平位移 ≥ 4 px | 相同 |
| 拖曳放開 | 彈簧回彈(spring 220 / damping 26 | 無(直接停在放開處) |
| 點擊 | 開 quick chat | 揮手(沒有對應功能) |
| reduced motion | 只顯示第一格 | 相同 |
| 預設位置 | 工作區右下各留 24 px | 相同 |
| 尺寸 | 寬 80224 px 可調 | 65%175%82221 px |
刻意不同的地方都是因為 Claude Code 沒有對應的功能(quick chat、computer-use 游標)或對應的訊號(thread 是否已讀)。
## HTTP API
只監聽 `127.0.0.1`