# M1-10 Review — 改寫 app.go + Python 雙策略空殼 + 生命週期邏輯 **審查日期**:2026-04-10 **審查對象**:`/Users/jimchen/visionA/local-tool/visiona-local/`(`app.go` 重寫、`main.go`、`platform_*.go`、`embed.go` 已刪) **任務描述**:把原 installer 的肥胖 app.go 整份砍掉重寫,留下啟動殼層:single-instance、舊路徑遷移、port picking、Python 雙策略空殼、spawn server 子行程、graceful shutdown、前端 binding ## 結論:✅ 通過 全部檢查點過關。舊 installer(Relay / Gitea / Tray / Cluster / Firmware / auto-update / libusb / ffmpeg setup / installer wizard)完全清光;`go build ./...` 與 `go vet ./...` 都乾淨通過;三平台資料路徑與 tray-and-lifecycle.md §6 完全一致;Python 雙策略介面符合 dependency-bundling.md §1.5;生命週期邏輯(lock、migrate、port、start/stop、health)都照 TDD 落地。Backend 回報的五個 TODO 均屬 M1+/M2 範圍,不阻斷 M1-10 驗收。**可以進入 M1-12(wails build + dmg)**。 --- ## 檢查清單 ### 1. 平台資料路徑正確 | 平台 | 期望 | 實際(`platform_*.go` + `appName`)| 狀態 | |------|------|----------------------------------|------| | macOS | `~/Library/Application Support/visiona-local` | `filepath.Join(home, "Library", "Application Support", appName)`(appName=`visiona-local`)| ✅ | | Linux | `$XDG_DATA_HOME/visiona-local` → fallback `~/.local/share/visiona-local` | XDG 檢查齊全,fallback 正確 | ✅ | | Windows | `%APPDATA%\visiona-local` | `os.Getenv("APPDATA")` + home fallback | ✅ | **殘留舊名檢查**:只在 `oldDataDirCandidates()` 中出現 `.edge-ai-platform` / `visionA-local`(駝峰)/ `EdgeAIPlatform`,全部為 migration source path,屬正確用法。active 路徑無殘留。✅ ### 2. 被砍功能確實不在 `grep` 掃過整個 `visiona-local/*.go`: | 被砍項目 | 出現位置 | 狀態 | |---------|---------|------| | Relay | — | ✅ 無 | | Gitea | — | ✅ 無 | | Tray | 只出現在檔頭註解提到「tray 已被整份刪除」 | ✅ 無實作 | | Cluster | — | ✅ 無 | | Firmware | — | ✅ 無 | | auto-update | 只出現在檔頭「已刪除」註解 | ✅ 無實作 | | libusb | — | ✅ 無 | | ffmpeg setup | — | ✅ 無 | | installer wizard | 只出現在檔頭「已刪除」註解 | ✅ 無實作 | | `embed.go` | 檔案已刪 | ✅ | | `Installer` 型別 | — | ✅ 無 | `main.go` 的 `Bind: []interface{}{app}` 只綁 `App`(`NewApp()`),不是舊的 `Installer`。✅ ### 3. Python 雙策略介面 對照 `dependency-bundling.md §1.5` 決策樹: | 檢查項 | 實作 | 狀態 | |-------|------|------| | `PythonMode` const | `auto` / `bundled` / `system` 三值齊全 | ✅ | | `ensurePythonRuntime(auto)` → 先 system fallback bundled | `case PythonModeAuto` 先 `findSystemPython()` 再 `ensureBundledPython()` | ✅(符合 R4 決策:M1 先 system)| | `findSystemPython()` 檢查 ≥ 3.10 | `isPython310OrNewer()` 解析 `Python 3.X.Y` 並比對 | ✅ | | 候選名單 | `python3.12` / `3.11` / `3.10` / `python3` / `python` | ✅ | | 避開 Windows Store stub | `strings.Contains(strings.ToLower(p), "windowsapps")` → skip | ✅ | | `ensureBundledPython()` 為 placeholder | 直接回 `"bundled python runtime not yet implemented (M2 feature)"` 並附 `TODO(M2)` 註解指向 §1.3 | ✅ | **小建議(不阻擋)**:`findSystemPython()` 沒有把 venv 建立 / wheel 安裝包進去,這是刻意的——M1 範圍只要「找到 interpreter 並把路徑傳給 server」,venv/wheels 留到 M2 由 bundled flow 一起處理。符合 M1 縮限範圍。 ### 4. 生命週期邏輯 對照 `tray-and-lifecycle.md`: | 檢查項 | § | 實作 | 狀態 | |-------|-----|------|------| | `acquireSingleInstance` lock + PID check | §2.2 | `O_CREATE|O_EXCL` + read PID + `processAlive` + stale lock 清理 | ✅ | | `processAlive` 跨平台 | §2.4 | Unix: `proc.Signal(0)`;Windows: `tasklist /FI "PID eq ..."` | ✅ | | 喚起既有 instance | §2.3 | `tryRaiseExistingInstance` 讀 `.ipc-port` 並打 `/api/system/health`(M1+ 才改成 `/ipc/raise`,已有 TODO 註解)| ✅(M1 可接受)| | `migrateOldDataDirs` 在 lock 之前 | §4.5 | `startup()` 順序:`MkdirAll` → `migrate` → `acquireSingleInstance` | ✅ | | 遷移失敗不擋啟動 | §4.5 | `continue` + stderr 警告 | ✅ | | 新路徑已存在不覆蓋 | §4.5 | 檢查空目錄才 `os.Remove` + `Rename` | ✅(比 TDD 稍寬鬆但更實用:允許剛 MkdirAll 的空資料夾被替換)| | `.migrated-from` breadcrumb | §4.5 | 寫入 `old\n + RFC3339 time` | ✅ | | `pickPort(3721)` | §3.2 | 從 `defaultPreferredPort=3721` 起跳,掃 20 個 | ✅ | | `isOurStaleServer` 偵測 | §3.2 | 尚未實作,已註 `TODO(M1+)` | ⚠️ 非阻斷(Backend 回報 known TODO)| | `startServer` 流程 | §4.1 | ensurePython → pickPort → locateBinary → spawn → writeIPCPort → waitHealthy | ✅ | | `stopServer` / `ServerProcess.stop()` SIGTERM → grace → SIGKILL | §4.1 | `shutdownGracePeriod=5s`,超時後 `Kill` + `<-done`;Windows 分支直接 `Kill` | ✅(TDD 寫 3s,實作 5s 更寬鬆,可接受)| | `waitHealthy(port, timeout)` | §4.1 | `/api/system/health` 輪詢,`healthCheckTimeout=15s`,300ms 間隔 | ✅(TDD 寫 10s,實作 15s 更寬鬆)| | `watchServer` 每 10 秒輪詢 | §4.2 | 尚未實作,Backend 回報 M1+ | ⚠️ 非阻斷 | | `writeIPCPort` | §2.3 | 寫到 `dataDir/visiona-local.ipc-port` | ✅ | **timeout 差異說明**:TDD 寫 3s grace / 10s health,實作用 5s / 15s。實作放寬不是縮緊,對正確性無影響;若 reviewer 嚴格要對齊 TDD 可留待 M1+ 統一調,不阻斷 M1-10。 ### 5. `go build ./...` 重現 ``` $ cd /Users/jimchen/visionA/local-tool/visiona-local && go build ./... (無輸出) $ go vet ./... (無輸出) ``` ✅ 乾淨通過,無 warning、無 unused import。 ### 6. Wails binding 清單 `main.go`: ```go Bind: []interface{}{app} ``` `app` 為 `*App`(`NewApp()` 回傳)。可被前端呼叫的 exported methods: - `GetServerStatus() ServerStatus` - `GetServerURL() string` - `OpenBrowser(url string) error` 未綁舊 `Installer`、未綁任何 installer wizard method。✅ `main.go` Title:`"visionA Local"`(不是 `Edge AI Platform Installer`)。✅ ### 7. 已知 TODO 清單(Backend 回報) | TODO | 位置 | M 版本 | 阻斷 M1-10?| |------|------|-------|------------| | `ensureBundledPython` | `app.go:417` | M2 | ❌ 不阻斷(M1 先 system)| | `/ipc/raise` endpoint | `app.go:550` TODO 註解 | M1+ | ❌ 不阻斷 | | `watchServer` healthcheck 迴圈 | — | M1+ | ❌ 不阻斷 | | `isOurStaleServer` + 自動 kill | `app.go:452` TODO 註解 | M1+ | ❌ 不阻斷 | | Fatal 原生對話框(目前只寫 stderr + event emit)| `reportFatal` | M1+ | ❌ 不阻斷 | 五個 TODO 全部都有明確註解或計畫位置,符合「M1 為最小可跑殼層」的定位。 --- ## 發現的次要問題(不阻擋 M1-10) 1. **`startServer` 的 error 分支對 `mockMode` 判斷位置怪異**(`app.go:205-208`): ```go pyBin, pyMode, err := a.ensurePythonRuntime(a.pythonMode) if err != nil && !a.mockMode { return fmt.Errorf(...) } ``` 當 `mockMode=true` 且 python 失敗時,`pyBin` 會是空字串、`pyMode` 可能是 `PythonModeAuto`,後續 `a.pythonBin = pyBin` 會存空字串。邏輯上能跑(因為 mock 不需要 python),但會讓 `GetServerStatus` 回報空的 `pythonBin`,前端顯示可能怪。建議 M1+ 在 mock 分支明確記一個 sentinel value(如 `""`)。 2. **`log` 檔開啟錯誤被吞**(`app.go:243-246`):`OpenFile` 回傳 error 被丟棄,只有 nil check。如果 `logsDir` 磁碟滿或權限異常,會靜默退到 `io.Discard`。建議 M1+ 把 error 寫進 `lastError`。 3. **`locateServerBinary` 的 candidates 順序**在開發模式下,`cwd/dist/` 在「與 exe 同目錄」之後。實務上 `wails dev` 會把 exe 放在 `build/bin/`,與 server binary 的實際位置(`dist/`)不同目錄,所以 candidate 3-4 會被用到。順序合理,但建議在 M1-12 build packaging 時驗證打包後 candidate 1 能命中。 4. **`migrateOldDataDirs` 的 stderr 警告**在 GUI app 情境下使用者看不到。搭配 #Fatal 原生對話框(M1+ TODO)一起改。 以上全部都是建議,**不影響 M1-10 驗收**。 --- ## 結論與下一步 **M1-10 通過 ✅,可以進入 M1-12(wails build + dmg packaging)。** 下一階段的重點: - M1-12 用 `wails build` 產生 macOS `.app` - 驗證 `locateServerBinary` candidate 1(與 Wails exe 同目錄)能正確命中打包後的 `visiona-local-server` - 產生 `.dmg` 並確認資料路徑在實機能正確建出 Backend 列出的 M1+ TODO(`/ipc/raise`、`watchServer`、`isOurStaleServer`、Fatal dialog)建議收在一個 M1-13 追蹤 issue,跟 M1-12 平行進行或接在其後。