jim800121chen c54f16fca0 Initial commit: visionA monorepo with local-tool subproject
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>
2026-04-11 22:10:38 +08:00

154 lines
8.8 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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
## 結論:✅ 通過
全部檢查點過關。舊 installerRelay / 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-12wails 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-12wails 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 平行進行或接在其後。