逐項對照 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:
@@ -35,7 +35,8 @@ const BUBBLE_H = 72; // 氣泡保留高度
|
||||
const PAD_X = 24; // 左右最小透明邊
|
||||
const PAD_BOTTOM = 8;
|
||||
const MIN_WIN_W = 240; // 視窗最小寬度,保留氣泡的可讀寬度(Codex 的氣泡也遠寬於寵物本身)
|
||||
const SCALES = [0.5, 0.75, 1, 1.25, 1.5];
|
||||
// Codex 的 mascot 寬度可調範圍是 80–224 px(ike() 的 clamp);以 126 為 100%,這幾檔都落在範圍內
|
||||
const SCALES = [0.65, 0.8, 1, 1.25, 1.5, 1.75];
|
||||
|
||||
const DEFAULT_CONFIG = {
|
||||
activePetId: "xiao-nian",
|
||||
@@ -43,7 +44,8 @@ const DEFAULT_CONFIG = {
|
||||
scale: 1,
|
||||
position: null,
|
||||
alwaysOnTop: true,
|
||||
followCursor: true,
|
||||
followCursor: false, // Codex 的寵物不看滑鼠(它看的是 quick chat 文字游標/computer-use 游標);預設關
|
||||
greetedPetIds: [], // 已經打過招呼的寵物(Codex 的 first-awake 每隻只做一次)
|
||||
port: 17333,
|
||||
};
|
||||
|
||||
@@ -71,6 +73,7 @@ function loadConfig() {
|
||||
.filter((p) => fs.existsSync(p));
|
||||
if (!SCALES.includes(Number(merged.scale))) merged.scale = 1;
|
||||
merged.scale = Number(merged.scale);
|
||||
if (!Array.isArray(merged.greetedPetIds)) merged.greetedPetIds = [];
|
||||
return merged;
|
||||
}
|
||||
|
||||
@@ -157,6 +160,7 @@ function petPayload(pet) {
|
||||
displayName: pet.displayName,
|
||||
description: pet.description,
|
||||
version: pet.version,
|
||||
greeted: config.greetedPetIds.includes(pet.id),
|
||||
dataUrl: `data:${mime};base64,${data}`,
|
||||
};
|
||||
}
|
||||
@@ -173,8 +177,9 @@ function windowSizeFor(scale) {
|
||||
function defaultPosition(size) {
|
||||
const { workArea } = screen.getPrimaryDisplay();
|
||||
return {
|
||||
// Codex 的 anchor:離工作區右邊與底邊各 24 px(z5 = 24)
|
||||
x: workArea.x + workArea.width - size.width - 24,
|
||||
y: workArea.y + workArea.height - size.height - 4,
|
||||
y: workArea.y + workArea.height - size.height - 24,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -242,16 +247,14 @@ function startDrag(offsetX, offsetY) {
|
||||
if (!win) return;
|
||||
endDrag(false);
|
||||
let last = screen.getCursorScreenPoint();
|
||||
let lastDx = 0;
|
||||
const startedAt = Date.now();
|
||||
dragTimer = setInterval(() => {
|
||||
if (!win || Date.now() - startedAt > 60000) return endDrag(true);
|
||||
const p = screen.getCursorScreenPoint();
|
||||
const dx = p.x - last.x;
|
||||
last = p;
|
||||
if (Math.abs(dx) >= 1) lastDx = dx;
|
||||
win.setPosition(Math.round(p.x - offsetX), Math.round(p.y - offsetY), false);
|
||||
send("pet:drag-move", { dx: lastDx });
|
||||
send("pet:drag-move", { dx });
|
||||
}, 16);
|
||||
}
|
||||
|
||||
@@ -368,8 +371,8 @@ function info(message) {
|
||||
function buildMenu() {
|
||||
const pet = activePet();
|
||||
const TESTS = [
|
||||
["idle", "待機"], ["running", "工作中"], ["waiting", "等待授權"], ["review", "檢視成果"],
|
||||
["failed", "失敗"], ["jumping", "跳躍(完成)"], ["waving", "揮手"],
|
||||
["idle", "待機"], ["running", "工作中"], ["waiting", "等待授權"], ["review", "檢視成果(完成)"],
|
||||
["failed", "失敗"], ["waving", "揮手"], ["jumping", "跳躍"],
|
||||
];
|
||||
return Menu.buildFromTemplate([
|
||||
{ label: pet ? `${pet.displayName}(${pet.id})` : "沒有可用的寵物", enabled: false },
|
||||
@@ -505,6 +508,11 @@ ipcMain.on("pet:state", (_e, state) => {
|
||||
refreshTray();
|
||||
});
|
||||
ipcMain.on("pet:log", (_e, line) => appendLog(String(line)));
|
||||
ipcMain.on("pet:greeted", (_e, id) => {
|
||||
if (typeof id !== "string" || config.greetedPetIds.includes(id)) return;
|
||||
config.greetedPetIds.push(id);
|
||||
saveConfig();
|
||||
});
|
||||
|
||||
// ---------- app ----------
|
||||
|
||||
|
||||
Reference in New Issue
Block a user