加上免安裝版,寵物來源改成執行目錄的 pets
摘要: 新增 portable 單檔與 zip 資料夾兩種免安裝產物;寵物一律從執行目錄的 pets/ 掃,設定檔不再累積程式自己算出來的路徑。 根本原因: 1. 只有 NSIS 安裝檔,沒有點兩下就能跑的版本。 2. 內附寵物被 asarUnpack 埋在 resources/app.asar.unpacked/pets,使用者要新增寵物得先鑽進 resources 資料夾。 3. loadConfig 會把算出來的來源路徑寫回 config.json,程式一搬家或在安裝版與 portable 之間切換,petSources 就越積越多失效的絕對路徑(實際已累積出 dist/win-unpacked/resources/app.asar.unpacked/pets 這種殘留)。 4. CODEX_PETS_DIR 用到 HOME,但 const HOME 宣告在它下面,會在啟動時就 TDZ ReferenceError。 影響: 沒有免安裝選項;新增寵物的路徑不直覺;設定檔會長出一堆死路徑;且上一版打包後的程式其實一啟動就會崩潰。 修法: - win target 加上 portable 與 zip;portable 設 unpackDirName=ClaudePet、requestExecutionLevel=user。 - pets/ 改用 extraFiles 放到 exe 旁邊(不再進 asar),APP_ROOT 打包後取 process.execPath 的目錄。 - portable 的 NSIS 外殼每次啟動都重解壓、結束就刪整個目錄,因此:hook 腳本另外複製到 ~/.claude-pet/hook/ 並註冊該穩定路徑(否則寵物沒開時 Claude Code 每次觸發 hook 都找不到檔案);使用者寵物放那顆 exe 旁邊;開機自動啟動改用 PORTABLE_EXECUTABLE_FILE。 - petSources 語意改為「只存使用者自己加的額外路徑」;執行目錄的 pets 與 ~/.codex/pets 為隱含來源,由 petSearchDirs() 組出實際掃描清單,載入時剔除隱含項與不存在的路徑。同 id 以使用者自己放的優先。 - 把 HOME/CONFIG_DIR 移到常數區最前面,修掉 TDZ。 - 內附第二隻寵物 xiao-nian-realistic。 驗證: 以 ELECTRON_RUN_AS_NODE 執行打包後的 exe(不開視窗): - 一般模式與模擬 portable 模式下的 pets 路徑、hook 路徑、autostart 指令皆正確。 - 用假的 USERPROFILE 實跑 install/uninstall:hook 腳本確實複製、指令含空白路徑會加引號、其他人的 Stop/PreToolUse hook 與 model 設定在安裝與移除後都完整保留、備份有產生。 - 帶殘留路徑的設定檔會被正確清理,掃描順序為「使用者 pets → 內附 pets → ~/.codex/pets → 額外路徑」。 - 從 app.asar 讀回 main.js 確認出貨的程式碼順序正確。 - 以純 node 載入 main.js,確認常數區與 loadConfig 不再有 TDZ。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -6,16 +6,26 @@ const path = require("node:path");
|
||||
const os = require("node:os");
|
||||
const autostart = require("./lib/autostart");
|
||||
const hooksInstaller = require("./lib/hooks-installer");
|
||||
const { unpacked } = require("./lib/paths");
|
||||
const { unpacked, portableDir, isPackaged } = require("./lib/paths");
|
||||
|
||||
const APP_DIR = __dirname;
|
||||
// 打包後 pets 會被 asarUnpack 解到 app.asar.unpacked,才能讓使用者自己丟寵物進去、選單也開得起來
|
||||
const PETS_DIR = unpacked(path.join(APP_DIR, "pets"));
|
||||
const HOME = os.homedir();
|
||||
const CONFIG_DIR = path.join(HOME, ".claude-pet");
|
||||
const CONFIG_PATH = path.join(CONFIG_DIR, "config.json");
|
||||
const LOG_PATH = path.join(CONFIG_DIR, "events.log");
|
||||
|
||||
// 執行檔所在的資料夾:開發時是專案資料夾,打包後是 exe 旁邊
|
||||
// (portable 版這裡會是 %TEMP% 的解壓目錄,使用者那顆 exe 的位置要看 portableDir())
|
||||
const APP_ROOT = isPackaged() ? path.dirname(process.execPath) : APP_DIR;
|
||||
// 寵物一律放執行目錄的 pets/,不埋在 asar 裡,使用者直接丟資料夾進去就會出現
|
||||
const BUNDLED_PETS_DIR = path.join(APP_ROOT, "pets");
|
||||
// portable 版整包解在 %TEMP%、關掉就被刪,使用者自己的寵物要放在那顆 exe 旁邊才留得住
|
||||
const USER_PETS_DIR = portableDir() ? path.join(portableDir(), "pets") : BUNDLED_PETS_DIR;
|
||||
const CODEX_PETS_DIR = path.join(HOME, ".codex", "pets");
|
||||
// 一律會被掃到、且不寫進設定檔的來源。使用者自己放的排最前面,同 id 才不會被內附的蓋掉
|
||||
// (非 portable 時 USER 就等於 BUNDLED,去重後只剩一個)
|
||||
const IMPLICIT_PET_DIRS = [...new Set([USER_PETS_DIR, BUNDLED_PETS_DIR, CODEX_PETS_DIR])];
|
||||
|
||||
const CELL_W = 192; // spritesheet 單格來源尺寸
|
||||
const CELL_H = 208;
|
||||
// 100% 時的顯示尺寸,對齊 Codex 桌面寵物:寬度為基準,高度用 Codex 的 ake() 公式 ceil(w * 208/192)
|
||||
@@ -29,7 +39,7 @@ const SCALES = [0.5, 0.75, 1, 1.25, 1.5];
|
||||
|
||||
const DEFAULT_CONFIG = {
|
||||
activePetId: "xiao-nian",
|
||||
petSources: [PETS_DIR, path.join(HOME, ".codex", "pets")],
|
||||
petSources: [], // 只放使用者自己加的額外路徑;執行目錄的 pets 與 ~/.codex/pets 一律隱含
|
||||
scale: 1,
|
||||
position: null,
|
||||
alwaysOnTop: true,
|
||||
@@ -52,11 +62,13 @@ function loadConfig() {
|
||||
let saved = {};
|
||||
try { saved = JSON.parse(fs.readFileSync(CONFIG_PATH, "utf8")); } catch { /* first run */ }
|
||||
const merged = { ...DEFAULT_CONFIG, ...saved };
|
||||
if (!Array.isArray(merged.petSources)) merged.petSources = [];
|
||||
// 永遠包含 app 自己的 pets 資料夾(就算專案搬家了也找得到)
|
||||
const ownPets = PETS_DIR;
|
||||
merged.petSources = [ownPets, ...merged.petSources.filter((p) => typeof p === "string" && path.resolve(p) !== ownPets && fs.existsSync(p))];
|
||||
if (!merged.petSources.includes(DEFAULT_CONFIG.petSources[1]) && fs.existsSync(DEFAULT_CONFIG.petSources[1])) merged.petSources.push(DEFAULT_CONFIG.petSources[1]);
|
||||
// petSources 只存「使用者自己加的」。隱含來源(執行目錄的 pets、~/.codex/pets)不寫進設定檔,
|
||||
// 否則程式一搬家、或在安裝版與 portable 版之間換來換去,設定檔就會一直累積失效的絕對路徑。
|
||||
const implicit = new Set(IMPLICIT_PET_DIRS.map(dirKey));
|
||||
merged.petSources = (Array.isArray(merged.petSources) ? merged.petSources : [])
|
||||
.filter((p) => typeof p === "string" && p.trim())
|
||||
.filter((p) => !implicit.has(dirKey(p)))
|
||||
.filter((p) => fs.existsSync(p));
|
||||
if (!SCALES.includes(Number(merged.scale))) merged.scale = 1;
|
||||
merged.scale = Number(merged.scale);
|
||||
return merged;
|
||||
@@ -81,10 +93,31 @@ function appendLog(line) {
|
||||
|
||||
// ---------- pets ----------
|
||||
|
||||
function dirKey(p) {
|
||||
return path.resolve(p).toLowerCase();
|
||||
}
|
||||
|
||||
// 實際要掃的資料夾:執行目錄的 pets 永遠排最前面,其次是 Codex 的,最後才是使用者自己加的
|
||||
function petSearchDirs() {
|
||||
const seen = new Set();
|
||||
return [...IMPLICIT_PET_DIRS, ...config.petSources].filter((p) => {
|
||||
const key = dirKey(p);
|
||||
if (seen.has(key)) return false;
|
||||
seen.add(key);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
// portable 版第一次跑時,在 exe 旁邊開一個 pets 資料夾給使用者放自己的寵物
|
||||
function ensureUserPetsDir() {
|
||||
if (USER_PETS_DIR === BUNDLED_PETS_DIR) return;
|
||||
try { fs.mkdirSync(USER_PETS_DIR, { recursive: true }); } catch { /* 唯讀路徑就算了 */ }
|
||||
}
|
||||
|
||||
function discoverPets() {
|
||||
const found = [];
|
||||
const seen = new Set();
|
||||
for (const src of config.petSources) {
|
||||
for (const src of petSearchDirs()) {
|
||||
let entries = [];
|
||||
try { entries = fs.readdirSync(src, { withFileTypes: true }); } catch { continue; }
|
||||
for (const ent of entries) {
|
||||
@@ -352,7 +385,7 @@ function buildMenu() {
|
||||
})),
|
||||
{ type: "separator" },
|
||||
{ label: "重新掃描", click: () => { pets = discoverPets(); if (!activePet()) return; refreshTray(); } },
|
||||
{ label: "開啟寵物資料夾", click: () => shell.openPath(PETS_DIR) },
|
||||
{ label: "開啟寵物資料夾", click: () => { ensureUserPetsDir(); shell.openPath(USER_PETS_DIR); } },
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -485,9 +518,10 @@ if (!app.requestSingleInstanceLock()) {
|
||||
});
|
||||
|
||||
app.whenReady().then(() => {
|
||||
ensureUserPetsDir();
|
||||
pets = discoverPets();
|
||||
if (pets.length === 0) {
|
||||
dialog.showErrorBox("Claude Pet", `找不到任何寵物。\n請把 Codex 寵物包(pet.json + spritesheet.webp)放到:\n${config.petSources.join("\n")}`);
|
||||
dialog.showErrorBox("Claude Pet", `找不到任何寵物。\n請把 Codex 寵物包(pet.json + spritesheet.webp)放到:\n${petSearchDirs().join("\n")}`);
|
||||
}
|
||||
createWindow();
|
||||
createTray();
|
||||
|
||||
Reference in New Issue
Block a user