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>
231 lines
12 KiB
Markdown
231 lines
12 KiB
Markdown
# M1 End-to-End Verification Report
|
||
|
||
## 測試環境
|
||
- macOS: 14.7.6 (BuildVersion 23H626)
|
||
- 架構: x86_64
|
||
- 檔案系統: APFS(case-insensitive,預設)
|
||
- 日期: 2026-04-11
|
||
- 測試方式: 複製 `.app` 到 `/tmp/` 模擬全新機器,清空 `~/Library/Application Support/visiona-local`
|
||
|
||
## 結論(TL;DR)
|
||
|
||
**M1 未通過。** `.dmg` 本身可正常 mount、`.app` bundle 結構正確、codesign 驗證通過,但**應用程式一啟動就失敗**。發現 **2 個 P0 阻斷 bug**:
|
||
|
||
1. **資料目錄遷移邏輯在 case-insensitive APFS 上壞掉** — 預設 Mac 根本啟動不了。
|
||
2. **Wails 端呼叫 server binary 時用了不存在的 flag `--python`** — 就算繞過 bug 1,server 子行程也會立刻 crash。
|
||
|
||
Mock 模式無法在全新 Mac 上運作,**瀏覽器無法看到主 UI**,M1 的核心承諾全部跳票。
|
||
|
||
---
|
||
|
||
## 前置清理
|
||
|
||
啟動前發現系統上 legacy `edge-ai-server` 持續被 launchd 重啟(`~/Library/LaunchAgents/com.innovedus.edge-ai-server.plist`),已執行:
|
||
```
|
||
launchctl unload ~/Library/LaunchAgents/com.innovedus.edge-ai-server.plist
|
||
pkill -9 -f edge-ai-server
|
||
```
|
||
之後 port 3721 淨空。**注意:這是測試環境前置清理,不是 M1 驗收的一部分。但這提醒:使用者第一次安裝若曾裝過 edge-ai-platform,升級 visiona-local 可能 port 衝突。**
|
||
|
||
---
|
||
|
||
## 驗證步驟結果
|
||
|
||
### Step 1: .dmg 檔案 ✅
|
||
- 路徑:`/Users/jimchen/visionA/local-tool/dist/visiona-local.dmg`
|
||
- 大小:70 MB
|
||
- 檔案類型:`zlib compressed data`
|
||
|
||
### Step 2: Mount ✅
|
||
`hdiutil attach` 成功,檢查碼全部通過:
|
||
- 掛載點:`/Volumes/visionA-local`
|
||
- 可見 `visiona-local.app`
|
||
|
||
### Step 3: .app 結構 ✅
|
||
- `Contents/Info.plist` ✓
|
||
- `Contents/MacOS/visiona-local`(9.8 MB,Mach-O 64-bit x86_64)✓
|
||
- `Contents/Resources/bin/visiona-local-server`(30 MB)✓
|
||
- `Contents/Resources/data/models.json`、`data/nef/` ✓
|
||
- `Contents/Resources/iconfile.icns`、`scripts/` ✓
|
||
- `codesign --verify --verbose`:**valid on disk, satisfies Designated Requirement** ✓
|
||
|
||
### Step 4: 全新機器模擬 ✅
|
||
`.app` 複製到 `/tmp/visiona-local.app`,DMG 已 detach。
|
||
|
||
### Step 5: 資料目錄清理 ✅
|
||
確認 `~/Library/Application Support/` 下無任何 `vision*` 殘留。
|
||
|
||
### Step 6 + 7: 啟動 → Server 起來 ❌❌
|
||
|
||
#### 第一次啟動(走正常流程)
|
||
```
|
||
$ /tmp/visiona-local.app/Contents/MacOS/visiona-local
|
||
[visiona-local] 遷移 /Users/jimchen/Library/Application Support/visionA-local
|
||
→ /Users/jimchen/Library/Application Support/visiona-local 失敗:
|
||
rename ...: no such file or directory
|
||
visiona-local already running
|
||
```
|
||
**Wails app 直接 exit(0)。** 沒有 server、沒有 UI、什麼都沒有。
|
||
|
||
**根因分析(P0 Bug #1):** `visiona-local/app.go:572 migrateOldDataDirs` 的邏輯在 case-insensitive APFS 上會自我毀滅。
|
||
|
||
實際流程:
|
||
1. `startup()` 先 `MkdirAll(newDir)` 建立 `visiona-local/`。
|
||
2. `migrateOldDataDirs` 迭代舊候選路徑,其中包含 `visionA-local`(大寫 A)。
|
||
3. 在 case-insensitive APFS 上,`os.Stat(visionA-local)` **會 hit 剛建立的 `visiona-local`**(同 inode),所以 Stat 成功。
|
||
4. 程式碼接著檢查 newDir 是否存在(成功)、是否為空(剛建出來的,當然是空)→ 執行 `os.Remove(newDir)`。
|
||
5. `os.Remove(newDir)` 把那個目錄刪了,但在 case-insensitive FS 上這等於同時刪掉 `visionA-local`(同 inode)。
|
||
6. `os.Rename(visionA-local → newDir)` → 來源不存在 → `ENOENT`。
|
||
7. 更致命:newDir 已經被刪掉了,後續 `acquireSingleInstance` 嘗試在裡面建 lock 檔,`os.OpenFile` 回 `ENOENT`(父目錄不存在)。
|
||
8. `ENOENT` 不是 `IsExist`,函式直接回錯。外層的 handler 把「任何錯誤」都當成「already running」→ `os.Exit(0)`。
|
||
|
||
我用 Python 實測確認:
|
||
```python
|
||
os.makedirs('~/Library/Application Support/visiona-local')
|
||
os.path.exists('~/Library/Application Support/visionA-local') # → True
|
||
```
|
||
|
||
**這個 bug 在任何預設設定的 Mac 上都會觸發(APFS 預設 case-insensitive),等於 M1 在大部分 Mac 上根本跑不起來。**
|
||
|
||
#### 第二次啟動(workaround:預先建立非空 dataDir)
|
||
為了繼續測後面的步驟,我先 `mkdir visiona-local && touch .keep`。這樣 `migrateOldDataDirs` 的「newDir 已有內容 → 拒絕遷移」分支會 kick in,繞過 bug 1。
|
||
|
||
結果:Wails app 進程活著(PID 77828),lock file 建好了,寫了 `visiona-local.ipc-port = 3721`。
|
||
|
||
但 **port 3721 並沒有人 listen**:
|
||
```
|
||
$ lsof -i :3721
|
||
(空)
|
||
$ curl http://127.0.0.1:3721/api/system/health
|
||
(連線失敗)
|
||
```
|
||
|
||
檢查 `~/Library/Application Support/visiona-local/logs/server.stderr.log`:
|
||
```
|
||
flag provided but not defined: -python
|
||
Usage of .../visiona-local-server:
|
||
-python-mode string ...
|
||
(列出所有合法 flag,其中沒有 -python)
|
||
```
|
||
|
||
**P0 Bug #2:** `visiona-local/app.go:234` 組 server 參數時:
|
||
```go
|
||
args := []string{
|
||
"--host", "127.0.0.1",
|
||
"--port", strconv.Itoa(port),
|
||
"--data-dir", a.dataDir,
|
||
"--python-mode", string(pyMode),
|
||
}
|
||
if pyBin != "" {
|
||
args = append(args, "--python", pyBin) // ← server 沒有這個 flag
|
||
}
|
||
```
|
||
|
||
Server binary 只認 `--python-mode`,**不認 `--python`**。當系統有 python3(我這台 `/usr/local/bin/python3`)時,`ensurePythonRuntime` 會回傳 pyBin,於是 `--python /usr/local/bin/python3` 被加到參數列。Server 一啟動就 `flag provided but not defined: -python` → `os.Exit(2)`。
|
||
|
||
Wails app 沒有偵測到 server 掛了(沒有 health check wait,或者 check 失敗但沒有 abort),繼續掛在前景。
|
||
|
||
**組合效應:** 就算使用者手動繞過 bug 1,只要這台 Mac 上有安裝過 python3(絕大多數開發者 / macOS 自帶),就會撞到 bug 2。Mock 模式理論上不該碰 python,但目前的 code path 還是會把 pyBin 傳下去。
|
||
|
||
### Step 8: 主 UI 可訪問 ❌
|
||
因為 server 沒起來,`curl http://127.0.0.1:3721/` 得到 `HTTP 000`(連線拒絕)。**瀏覽器看不到任何東西。**
|
||
|
||
### Step 9: 資料目錄 ⚠️
|
||
在 workaround 下資料目錄內容:
|
||
```
|
||
.keep(我手動建的)
|
||
logs/
|
||
server.stderr.log ← 記錄 server 啟動失敗
|
||
server.stdout.log ← 空
|
||
visiona-local.ipc-port ← 內容 "3721"(但 port 根本沒在 listen)
|
||
visiona-local.lock ← 有 PID
|
||
```
|
||
`.ipc-port` 檔寫的是**預期 port**,不是**實際 port**,這本身也是潛在問題(但相比前兩個 bug 是次要的)。
|
||
|
||
### Step 10: 乾淨退出 ✅
|
||
`pkill -9 -f visiona-local` 後 `pgrep -fl visiona-local` 為空,`.app` 複本、資料目錄都清除乾淨。
|
||
|
||
### Step 11: make clean + make dmg 從頭跑 ⏭️ 略過
|
||
前面已經是阻斷級問題,重新 build 也不會修掉這兩個 bug。略過本步驟,等修好再驗。
|
||
|
||
---
|
||
|
||
## M1 驗收結論
|
||
|
||
| 核心承諾 | 結果 |
|
||
|---------|------|
|
||
| 1. 雙擊 .dmg → mount → .app 存在 | ✅ |
|
||
| 2. 啟動 → Mock 模式跑起來 | ❌ Wails app 秒退(bug 1)或 server 立刻 crash(bug 2) |
|
||
| 3. API (`/api/system/health`、`/api/system/info`) 可訪問 | ❌ port 沒有人 listen |
|
||
| 4. 主 UI 可在瀏覽器看到 | ❌ 同上 |
|
||
| 5. 乾淨退出 | ✅(但因為根本沒真的「運作」過,退出也沒什麼好清的) |
|
||
|
||
**核心承諾 2 / 3 / 4 全部沒達成。M1 未通過。**
|
||
|
||
---
|
||
|
||
## 發現的問題
|
||
|
||
### 🔴 P0 阻斷
|
||
|
||
#### P0-1: `migrateOldDataDirs` 在 case-insensitive APFS 上自我毀滅
|
||
- **位置**:`visiona-local/app.go:572-598` + `oldDataDirCandidates` 清單中的 `visionA-local`
|
||
- **影響**:任何預設 APFS 的 Mac(絕大多數使用者),**第一次啟動必失敗**。
|
||
- **修法建議**:
|
||
- (A) 用 `os.Stat` 比對 inode:如果舊路徑和新路徑指向同一個 inode,代表 case-insensitive FS 且實際上是同一個目錄,直接 `continue`,不要嘗試遷移。
|
||
- (B) 或者,在 candidates 裡**移除** `visionA-local` — 反正新版是 `visiona-local`,如果之前用的也是 `visiona-local`(case-insensitive FS 下等效),根本不需要遷移。只有在真正 case-sensitive FS(少數使用者自選)才需要這個 candidate,那就用 FS 類型偵測決定要不要加。
|
||
- (C) 額外修正:`startup()` 裡對 `acquireSingleInstance` 的錯誤處理要分辨「真的有別的 instance 在跑」vs「其他錯誤(例如資料目錄不見了)」。現在任何錯誤都印 "already running" 會誤導 debug。
|
||
|
||
#### P0-2: Wails 傳給 server 的 `--python` flag 不存在
|
||
- **位置**:`visiona-local/app.go:234`
|
||
- **影響**:只要系統上有 python(macOS 幾乎都有),server 子行程啟動即死。Mock 模式也一樣會死。
|
||
- **修法建議**:
|
||
- 改成 `--python-bin`(並在 server 端加對應 flag),或
|
||
- 把 pyBin 放在環境變數 `VISIONA_PYTHON_BIN` 傳,或
|
||
- 在 Mock 模式下完全不傳 pyBin 相關參數(根本用不到)。
|
||
- **額外**:Wails 端應該在 `startServer()` spawn 後做一次 health check(例如 500ms 內 poll `/api/system/health` 幾次),失敗就 `reportFatal`,不要讓 app 繼續假裝有在運作。
|
||
|
||
### 🟡 需修(非阻斷)
|
||
|
||
#### M-1: `.ipc-port` 寫的是預期 port 不是實際 port
|
||
Server 啟動前就寫好 port file,但如果 server 實際沒起來(bug 2 的情況),前端或 IPC 呼叫者會連到一個不存在的 port。應該在 server 真的 `LISTEN` 成功後才寫 ipc-port 檔(或由 server 自己寫)。
|
||
|
||
#### M-2: 系統級 launchd agent 殘留
|
||
`~/Library/LaunchAgents/com.innovedus.edge-ai-server.plist` 會自動重啟 legacy daemon,搶 3721 port。M1 uninstall / migration 文件應提醒使用者:若曾安裝 edge-ai-platform,需要手動 `launchctl unload` + 刪 plist。或在首次啟動時偵測到同 port 衝突時給出提示。
|
||
|
||
### 🟢 建議
|
||
|
||
- **測試環境 matrix**:目前所有開發測試都在同一台有 python、有舊 edge-ai-platform、有 legacy launchd 的 Mac 上跑。這兩個 bug(尤其 bug 1)在「乾淨 Mac」上才暴露。建議納入「新 macOS VM / 沒有 python 的 Mac / case-sensitive APFS 的 Mac」三種情境做 CI-level 驗證。
|
||
- `codesign` 目前是 ad-hoc/self-signed,上線前需要 Apple Developer ID 簽章 + notarize,否則 Gatekeeper 會擋。本次測試沒觸發這個議題(因為 `.app` 是本機 build 直接 run),但那是因為本機沒有 quarantine attr。從 `.dmg` 真的雙擊打開時會有不同行為,建議補 Gatekeeper 測試。
|
||
- Legacy LaunchAgent 議題(上方 M-2)也要在交付文件 (`07-delivery/launch-checklist.md`) 列成「升級安裝注意事項」。
|
||
|
||
---
|
||
|
||
## 結論:**M1 未通過,需修**
|
||
|
||
兩個 P0 bug 都必須在 ship 前修掉。建議流程:
|
||
|
||
1. **Architect Agent** 決定 bug 1 的修法(inode 比對 vs. 移除候選 vs. FS 偵測),以及 bug 2 的 flag 契約(改名 / 環境變數 / mock 模式不傳)。
|
||
2. **Backend Agent** 實作修復(`app.go` 兩處改動 + 可能的 server flag 新增)。
|
||
3. **Reviewer** 審查。
|
||
4. 重新 `make clean && make dmg`,**我再跑一次完整 E2E**。
|
||
5. 本次的前置清理步驟(unload launchd)也應寫進 launch-checklist,提醒升級使用者。
|
||
|
||
---
|
||
|
||
## 附錄:本次使用的清理指令
|
||
|
||
```bash
|
||
# 卸下 legacy launchd agent(若有)
|
||
launchctl unload ~/Library/LaunchAgents/com.innovedus.edge-ai-server.plist
|
||
pkill -9 -f edge-ai-server
|
||
pkill -9 -f visiona-local
|
||
|
||
# 清資料目錄(模擬全新使用者)
|
||
rm -rf "$HOME/Library/Application Support/visiona-local"
|
||
rm -rf "$HOME/Library/Application Support/visionA-local" # 注意:APFS 上這會和上一行等效
|
||
|
||
# 卸載 .dmg(若還掛著)
|
||
hdiutil detach /Volumes/visionA-local
|
||
```
|