local-tool/: visionA-local desktop app
- M1: Wails shell + Go server + Next.js UI + Mock mode (macOS dmg ready)
- M2: i18n (zh-TW/en) + Settings 4-tab refactor
- M3: Embedded Python 3.12 runtime (python-build-standalone) + KneronPLUS wheels
- M4: Windows Inno Setup script (build on Windows runner)
- M5: Linux AppImage script + udev rule (build on Linux runner)
- M6: ffmpeg (GPL, pending legal review) + yt-dlp bundled
- Lifecycle: watchServer health check, fatal native dialog,
Wails IPC raise endpoint, stale process cleanup
.autoflow/: full PRD / Design Spec / Architecture / Testing docs
(4 rounds tri-party discussion + cross review)
.github/workflows/: macOS / Windows / Linux build CI
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
154 lines
8.8 KiB
Markdown
154 lines
8.8 KiB
Markdown
# 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(如 `"<mock>"`)。
|
||
|
||
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 平行進行或接在其後。
|