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>
8.8 KiB
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:
Bind: []interface{}{app}
app 為 *App(NewApp() 回傳)。可被前端呼叫的 exported methods:
GetServerStatus() ServerStatusGetServerURL() stringOpenBrowser(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)
-
startServer的 error 分支對mockMode判斷位置怪異(app.go:205-208):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>")。 -
log檔開啟錯誤被吞(app.go:243-246):OpenFile回傳 error 被丟棄,只有 nil check。如果logsDir磁碟滿或權限異常,會靜默退到io.Discard。建議 M1+ 把 error 寫進lastError。 -
locateServerBinary的 candidates 順序在開發模式下,cwd/dist/在「與 exe 同目錄」之後。實務上wails dev會把 exe 放在build/bin/,與 server binary 的實際位置(dist/)不同目錄,所以 candidate 3-4 會被用到。順序合理,但建議在 M1-12 build packaging 時驗證打包後 candidate 1 能命中。 -
migrateOldDataDirs的 stderr 警告在 GUI app 情境下使用者看不到。搭配 #Fatal 原生對話框(M1+ TODO)一起改。
以上全部都是建議,不影響 M1-10 驗收。
結論與下一步
M1-10 通過 ✅,可以進入 M1-12(wails build + dmg packaging)。
下一階段的重點:
- M1-12 用
wails build產生 macOS.app - 驗證
locateServerBinarycandidate 1(與 Wails exe 同目錄)能正確命中打包後的visiona-local-server - 產生
.dmg並確認資料路徑在實機能正確建出
Backend 列出的 M1+ TODO(/ipc/raise、watchServer、isOurStaleServer、Fatal dialog)建議收在一個 M1-13 追蹤 issue,跟 M1-12 平行進行或接在其後。