加上免安裝版,寵物來源改成執行目錄的 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:
@@ -51,9 +51,17 @@
|
||||
|
||||
## 快速開始
|
||||
|
||||
### 用安裝檔(一般使用)
|
||||
### 直接用打包好的(一般使用)
|
||||
|
||||
執行 `dist\Claude Pet-<版本>-setup.exe`(見[打包成 exe](#打包成-exe)),裝在 `%LOCALAPPDATA%\Programs\Claude Pet`,不需要系統管理員權限。裝完從開始選單開啟,再用系統匣選單的 **Claude Code hooks → 安裝** 接上 Claude Code。
|
||||
三種都不需要系統管理員權限,選一種即可(見[打包成 exe](#打包成-exe)):
|
||||
|
||||
| 方式 | 檔案 | 適合 |
|
||||
|---|---|---|
|
||||
| **免安裝資料夾**(推薦) | `Claude Pet-<版本>-win.zip` | 解壓到任何地方,點兩下裡面的 `Claude Pet.exe` 就跑。啟動快、寵物放旁邊的 `pets\`、適合開機自動啟動 |
|
||||
| 安裝檔 | `Claude Pet-<版本>-setup.exe` | 裝到 `%LOCALAPPDATA%\Programs\Claude Pet`,有開始選單與桌面捷徑 |
|
||||
| 免安裝單檔 | `Claude Pet-<版本>-portable.exe` | 只想帶一個檔案走。代價是**每次啟動都要重新解壓約 200 MB**,開得比較慢 |
|
||||
|
||||
開起來之後用系統匣選單的 **Claude Code hooks → 安裝** 接上 Claude Code,然後**重開一個新的 Claude Code session**。
|
||||
|
||||
### 從原始碼跑(開發)
|
||||
|
||||
@@ -166,8 +174,10 @@ Spritesheet:**1536 × 2288**,8 欄 × 11 列,每格 **192 × 208**,透
|
||||
|
||||
## 新增 / 切換寵物
|
||||
|
||||
1. **用 Codex hatch** — 在 Codex 裡 `/hatch`,完成後寵物會在 `%USERPROFILE%\.codex\pets\<id>\`,本程式預設掃描該資料夾,選單「切換寵物 → 重新掃描」就會出現。
|
||||
2. **手動放入** — 把寵物資料夾複製到 `pets\<id>\`(選單「開啟寵物資料夾」可直接打開)。
|
||||
掃描順序:**執行目錄的 `pets\`**(portable 版是那顆 exe 旁邊的,其次才是內附的)→ `~/.codex/pets` → 設定檔 `petSources` 裡的額外路徑。同一個 id 以先掃到的為準。
|
||||
|
||||
1. **手動放入**(最直接)— 把寵物資料夾丟到 exe 旁邊的 `pets\<id>\`,選單「切換寵物 → 重新掃描」就會出現。選單的「開啟寵物資料夾」會直接開這一個。
|
||||
2. **用 Codex hatch** — 在 Codex 裡 `/hatch`,完成後寵物在 `%USERPROFILE%\.codex\pets\<id>\`,本程式一律會掃這個資料夾。
|
||||
3. **其他位置** — 在設定檔 `petSources` 加路徑。
|
||||
|
||||
切換後立即生效,不用重啟。
|
||||
@@ -179,7 +189,7 @@ Spritesheet:**1536 × 2288**,8 欄 × 11 列,每格 **192 × 208**,透
|
||||
| 欄位 | 預設 | 說明 |
|
||||
|---|---|---|
|
||||
| `activePetId` | `"xiao-nian"` | 目前使用的寵物 id |
|
||||
| `petSources` | `[<app>/pets, ~/.codex/pets]` | 掃描寵物的資料夾清單;程式自己的 `pets/` 永遠會被加在最前面 |
|
||||
| `petSources` | `[]` | **額外**的寵物資料夾。兩個隱含來源永遠會被掃到且不寫進這裡:**執行目錄的 `pets/`**(exe 旁邊,開發時是專案資料夾,排最前面)與 `~/.codex/pets`。不存在的路徑會在載入時自動剔除 |
|
||||
| `scale` | `1` | 縮放:0.5 / 0.75 / 1 / 1.25 / 1.5。`1` 表示 126 × 137,與 Codex 桌面寵物同大小 |
|
||||
| `position` | `null` | 視窗左上角座標;`null` 為主螢幕右下角。若座標落在所有螢幕之外會自動拉回 |
|
||||
| `alwaysOnTop` | `true` | 永遠置頂 |
|
||||
@@ -208,7 +218,7 @@ Claude Code ──hook(stdin JSON)──▶ hook/claude-pet-hook.js ──PO
|
||||
| `hook/claude-pet-hook.js` | Claude Code hook 端:讀 stdin → 擷取欄位 → POST;800 ms 逾時、永遠 exit 0 |
|
||||
| `lib/hooks-installer.js` | 安全地合併 / 移除 `~/.claude/settings.json` 的 hooks(保留其他設定與其他 hooks,先備份) |
|
||||
| `lib/autostart.js` | `HKCU\Software\Microsoft\Windows\CurrentVersion\Run\ClaudePet` 登錄值;開發時指向 `electron.exe + 專案路徑`,打包後直接指向那顆 exe |
|
||||
| `lib/paths.js` | 開發/打包兩種情境的路徑解析(`app.asar` → `app.asar.unpacked`、是否為打包版) |
|
||||
| `lib/paths.js` | 開發/安裝版/portable 三種情境的路徑解析(`app.asar` → `app.asar.unpacked`、是否打包、portable 的真實 exe 位置) |
|
||||
| `scripts/*.js` | 上述兩者的 CLI 包裝 |
|
||||
| `pets/` | 內附寵物 |
|
||||
| `docs/` | README 用圖 |
|
||||
@@ -269,24 +279,47 @@ curl http://127.0.0.1:17333/state
|
||||
## 打包成 exe
|
||||
|
||||
```powershell
|
||||
npm run dist # 安裝檔 + 免安裝版
|
||||
npm run pack # 只產免安裝版,快很多,開發時驗證用
|
||||
npm run dist # 三種都做
|
||||
npm run dist:portable # 只做免安裝單檔
|
||||
npm run pack # 只產 win-unpacked 資料夾,最快,改程式時驗證用
|
||||
```
|
||||
|
||||
產物在 `dist\`:
|
||||
|
||||
| 檔案 | 說明 |
|
||||
|---|---|
|
||||
| `Claude Pet-<版本>-setup.exe` | NSIS 安裝檔(約 100 MB)。per-user 安裝、不需要系統管理員權限,預設裝到 `%LOCALAPPDATA%\Programs\Claude Pet`,可自選路徑,會建立桌面與開始選單捷徑 |
|
||||
| `win-unpacked\Claude Pet.exe` | 免安裝版,整個資料夾複製走就能直接執行 |
|
||||
| 檔案 | 大小 | 說明 |
|
||||
|---|---|---|
|
||||
| `Claude Pet-<版本>-win.zip` | ~140 MB | **免安裝資料夾版**。解壓後點兩下 `Claude Pet.exe`。啟動最快,寵物就放旁邊的 `pets\` |
|
||||
| `Claude Pet-<版本>-setup.exe` | ~100 MB | NSIS 安裝檔。per-user、可改安裝路徑、建立桌面與開始選單捷徑 |
|
||||
| `Claude Pet-<版本>-portable.exe` | ~100 MB | **免安裝單檔**,點兩下直接跑 |
|
||||
| `win-unpacked\` | ~200 MB | 上面 zip 的未壓縮版,整個資料夾複製走也能用 |
|
||||
|
||||
第一次建置時 electron-builder 會從 GitHub 抓 NSIS 與相關工具到 `%LOCALAPPDATA%\electron-builder\Cache`,需要網路,之後就會走快取。
|
||||
第一次建置時 electron-builder 會從 GitHub 抓 NSIS 與相關工具到 `%LOCALAPPDATA%\electron-builder\Cache`,需要網路,之後走快取。
|
||||
|
||||
**打包後和原始碼跑有三個地方不一樣**(都已經處理好,改程式時要留意):
|
||||
### portable 單檔版的代價
|
||||
|
||||
- **`hook/` 和 `pets/` 不放進 asar**(`build.asarUnpack`)。前者是因為 Claude Code 用 `node <路徑>` 執行 hook,而 node 讀不到 asar 裡的檔案;後者是因為使用者要能自己丟寵物進去、選單「開啟寵物資料夾」也要打得開。程式裡由 `lib/paths.js` 的 `unpacked()` 把 `app.asar` 換成 `app.asar.unpacked`。
|
||||
- **開機自動啟動**寫入的指令是那顆 exe 本身(`process.execPath`),不再是 `electron.exe + 專案路徑`。判斷方式是 `lib/paths.js` 的 `isPackaged()`。
|
||||
- **hook 指令仍然需要 `node` 在 PATH 裡**。安裝路徑含空白(`Claude Pet`)時會自動加引號。
|
||||
NSIS 的 portable 外殼**每次啟動**都會把整包解壓到 `%TEMP%\ClaudePet`,**程式結束後又整個刪掉**(見 `app-builder-lib/templates/nsis/portable.nsi` 的 `RMDir /r $INSTDIR`)。所以:
|
||||
|
||||
- 每次開都要重新解壓約 200 MB,比資料夾版慢好幾秒。**要開機自動啟動的話建議用資料夾版**。
|
||||
- 程式本體的位置是暫時的,因此 hook 腳本會另外複製一份到 `%USERPROFILE%\.claude-pet\hook\`,用那個穩定路徑去註冊。否則寵物沒開的時候 Claude Code 每次觸發 hook 都會找不到檔案而報錯。
|
||||
- 使用者自己的寵物要放在**那顆 exe 旁邊**的 `pets\`(程式第一次跑會自己建),放進解壓目錄的會跟著被刪掉。同 id 時以 exe 旁邊那份為準。
|
||||
- 程式被強制結束(工作管理員)時外殼來不及清理,`%TEMP%\ClaudePet` 會留下約 300 MB,可以手動刪。
|
||||
|
||||
### 打包後和原始碼跑不一樣的地方
|
||||
|
||||
都已經處理好,改程式時要留意:
|
||||
|
||||
| 項目 | 開發 | 安裝/資料夾版 | portable 單檔 |
|
||||
|---|---|---|---|
|
||||
| 內附寵物 | `<專案>\pets` | `<exe 旁>\pets` | `%TEMP%\ClaudePet\pets` |
|
||||
| 使用者寵物 | 同上 | 同上 | `<exe 旁>\pets` |
|
||||
| hook 腳本 | `<專案>\hook\` | `resources\app.asar.unpacked\hook\` | `%USERPROFILE%\.claude-pet\hook\` |
|
||||
| 開機自動啟動 | `electron.exe` + 專案路徑 | 那顆 exe | `PORTABLE_EXECUTABLE_FILE` |
|
||||
|
||||
判斷邏輯都在 `lib/paths.js`:`isPackaged()` 用 `process.defaultApp`、`isPortable()` 用 `PORTABLE_EXECUTABLE_FILE`、`unpacked()` 把 `app.asar` 換成 `app.asar.unpacked`。
|
||||
|
||||
- **`pets/` 用 `extraFiles` 放到 exe 旁邊**(不是 `extraResources`,那會進 `resources\`),使用者直接把寵物資料夾丟進去就會出現,不用碰 asar。
|
||||
- **`hook/` 用 `asarUnpack`**,因為 Claude Code 是用 `node <路徑>` 執行它,而 node 讀不到 asar 裡的檔案。
|
||||
- **hook 指令仍然需要 `node` 在 PATH 裡**。路徑含空白時會自動加引號。
|
||||
|
||||
換圖示就換掉 `build/icon.ico`(要含 256×256)。目前這顆是從 `pets/xiao-nian/spritesheet.webp` 第 0 列第 6 欄(中立正面)取頭肩方形裁切產生的。
|
||||
|
||||
|
||||
+4
-3
@@ -3,7 +3,7 @@
|
||||
const path = require("node:path");
|
||||
const fs = require("node:fs");
|
||||
const { spawnSync } = require("node:child_process");
|
||||
const { isPackaged } = require("./paths");
|
||||
const { isPackaged, portableExe } = require("./paths");
|
||||
|
||||
const APP_DIR = path.resolve(__dirname, "..");
|
||||
const RUN_KEY = "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Run";
|
||||
@@ -14,8 +14,9 @@ function electronExe() {
|
||||
}
|
||||
|
||||
function command() {
|
||||
// 打包後就是那顆 exe 本身;開發時要用 node_modules 裡的 electron 帶上專案路徑
|
||||
if (isPackaged()) return `"${process.execPath}"`;
|
||||
// 打包後就是那顆 exe 本身。portable 版的 process.execPath 會指到 %TEMP% 的解壓副本,
|
||||
// 要用 PORTABLE_EXECUTABLE_FILE 才是使用者手上那顆 exe。
|
||||
if (isPackaged()) return `"${portableExe() || process.execPath}"`;
|
||||
return `"${electronExe()}" "${APP_DIR}"`;
|
||||
}
|
||||
|
||||
|
||||
+20
-4
@@ -3,14 +3,29 @@
|
||||
const fs = require("node:fs");
|
||||
const os = require("node:os");
|
||||
const path = require("node:path");
|
||||
const { unpacked } = require("./paths");
|
||||
const { unpacked, isPortable } = require("./paths");
|
||||
|
||||
const APP_DIR = path.resolve(__dirname, "..");
|
||||
const CLAUDE_DIR = path.join(os.homedir(), ".claude");
|
||||
const SETTINGS_PATH = path.join(CLAUDE_DIR, "settings.json");
|
||||
const BACKUP_DIR = path.join(CLAUDE_DIR, "backups");
|
||||
// node 執行不了 asar 內的檔案,打包後要指到解開的那份
|
||||
const HOOK_SCRIPT = unpacked(path.join(APP_DIR, "hook", "claude-pet-hook.js"));
|
||||
const BUNDLED_HOOK_SCRIPT = unpacked(path.join(APP_DIR, "hook", "claude-pet-hook.js"));
|
||||
// portable 版整包解在 %TEMP%、關掉程式就被刪,hook 路徑不能指在那裡(寵物沒開時
|
||||
// Claude Code 會找不到檔案而每次噴錯),所以複製一份到不會被清掉的地方。
|
||||
const STABLE_HOOK_SCRIPT = path.join(os.homedir(), ".claude-pet", "hook", "claude-pet-hook.js");
|
||||
|
||||
function hookScript() {
|
||||
return isPortable() ? STABLE_HOOK_SCRIPT : BUNDLED_HOOK_SCRIPT;
|
||||
}
|
||||
|
||||
// 只有 portable 需要;每次安裝 hook 都重新複製,版本才不會走鐘
|
||||
function syncHookScript() {
|
||||
if (!isPortable()) return;
|
||||
fs.mkdirSync(path.dirname(STABLE_HOOK_SCRIPT), { recursive: true });
|
||||
fs.copyFileSync(BUNDLED_HOOK_SCRIPT, STABLE_HOOK_SCRIPT);
|
||||
}
|
||||
|
||||
const MARKER = "claude-pet-hook";
|
||||
|
||||
// Claude Code 2.1.x 支援的 hook 事件(對應到寵物狀態)
|
||||
@@ -31,7 +46,7 @@ const EVENTS = [
|
||||
];
|
||||
|
||||
function hookCommand() {
|
||||
const p = HOOK_SCRIPT.replace(/\\/g, "/");
|
||||
const p = hookScript().replace(/\\/g, "/");
|
||||
return /\s/.test(p) ? `node "${p}"` : `node ${p}`;
|
||||
}
|
||||
|
||||
@@ -72,6 +87,7 @@ function normalize(list) {
|
||||
}
|
||||
|
||||
function install() {
|
||||
syncHookScript();
|
||||
const settings = readSettings();
|
||||
const backupPath = backup();
|
||||
settings.hooks = settings.hooks && typeof settings.hooks === "object" ? settings.hooks : {};
|
||||
@@ -110,4 +126,4 @@ function isInstalled() {
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { install, uninstall, isInstalled, hookCommand, EVENTS, SETTINGS_PATH };
|
||||
module.exports = { install, uninstall, isInstalled, hookCommand, hookScript, EVENTS, SETTINGS_PATH };
|
||||
|
||||
+15
-1
@@ -18,4 +18,18 @@ function isPackaged() {
|
||||
return Boolean(process.versions.electron) && !process.defaultApp;
|
||||
}
|
||||
|
||||
module.exports = { unpacked, isPackaged };
|
||||
// portable 單檔版:NSIS 外殼每次啟動都把整包解到 %TEMP%,程式結束後又整個刪掉,
|
||||
// 所以「程式本身的位置」是暫時的,只有這兩個環境變數指得到那顆 exe 真正待的地方。
|
||||
function portableExe() {
|
||||
return process.env.PORTABLE_EXECUTABLE_FILE || null;
|
||||
}
|
||||
|
||||
function portableDir() {
|
||||
return process.env.PORTABLE_EXECUTABLE_DIR || null;
|
||||
}
|
||||
|
||||
function isPortable() {
|
||||
return Boolean(portableExe());
|
||||
}
|
||||
|
||||
module.exports = { unpacked, isPackaged, portableExe, portableDir, isPortable };
|
||||
|
||||
@@ -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();
|
||||
|
||||
+30
-8
@@ -12,7 +12,8 @@
|
||||
"autostart:uninstall": "node scripts/autostart.js uninstall",
|
||||
"autostart:status": "node scripts/autostart.js status",
|
||||
"dist": "electron-builder --win",
|
||||
"pack": "electron-builder --win --dir"
|
||||
"pack": "electron-builder --win --dir",
|
||||
"dist:portable": "electron-builder --win portable"
|
||||
},
|
||||
"devDependencies": {
|
||||
"electron": "^43.4.1",
|
||||
@@ -33,12 +34,10 @@
|
||||
"renderer/**/*",
|
||||
"lib/**/*",
|
||||
"hook/**/*",
|
||||
"pets/**/*",
|
||||
"package.json"
|
||||
],
|
||||
"asarUnpack": [
|
||||
"hook/**/*",
|
||||
"pets/**/*"
|
||||
"hook/**/*"
|
||||
],
|
||||
"win": {
|
||||
"target": [
|
||||
@@ -47,10 +46,21 @@
|
||||
"arch": [
|
||||
"x64"
|
||||
]
|
||||
},
|
||||
{
|
||||
"target": "portable",
|
||||
"arch": [
|
||||
"x64"
|
||||
]
|
||||
},
|
||||
{
|
||||
"target": "zip",
|
||||
"arch": [
|
||||
"x64"
|
||||
]
|
||||
}
|
||||
],
|
||||
"icon": "build/icon.ico",
|
||||
"artifactName": "${productName}-${version}-setup.${ext}"
|
||||
"icon": "build/icon.ico"
|
||||
},
|
||||
"nsis": {
|
||||
"oneClick": false,
|
||||
@@ -59,7 +69,19 @@
|
||||
"createDesktopShortcut": true,
|
||||
"createStartMenuShortcut": true,
|
||||
"shortcutName": "Claude Pet",
|
||||
"deleteAppDataOnUninstall": false
|
||||
}
|
||||
"deleteAppDataOnUninstall": false,
|
||||
"artifactName": "${productName}-${version}-setup.${ext}"
|
||||
},
|
||||
"portable": {
|
||||
"artifactName": "${productName}-${version}-portable.${ext}",
|
||||
"unpackDirName": "ClaudePet",
|
||||
"requestExecutionLevel": "user"
|
||||
},
|
||||
"extraFiles": [
|
||||
{
|
||||
"from": "pets",
|
||||
"to": "pets"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"id": "xiao-nian-realistic",
|
||||
"displayName": "小念・擬真",
|
||||
"description": "一個阿呆",
|
||||
"spriteVersionNumber": 2,
|
||||
"spritesheetPath": "spritesheet.webp"
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 1.1 MiB |
Reference in New Issue
Block a user