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>
12 KiB
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:
- 資料目錄遷移邏輯在 case-insensitive APFS 上壞掉 — 預設 Mac 根本啟動不了。
- 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 上會自我毀滅。
實際流程:
startup()先MkdirAll(newDir)建立visiona-local/。migrateOldDataDirs迭代舊候選路徑,其中包含visionA-local(大寫 A)。- 在 case-insensitive APFS 上,
os.Stat(visionA-local)會 hit 剛建立的visiona-local(同 inode),所以 Stat 成功。 - 程式碼接著檢查 newDir 是否存在(成功)、是否為空(剛建出來的,當然是空)→ 執行
os.Remove(newDir)。 os.Remove(newDir)把那個目錄刪了,但在 case-insensitive FS 上這等於同時刪掉visionA-local(同 inode)。os.Rename(visionA-local → newDir)→ 來源不存在 →ENOENT。- 更致命:newDir 已經被刪掉了,後續
acquireSingleInstance嘗試在裡面建 lock 檔,os.OpenFile回ENOENT(父目錄不存在)。 ENOENT不是IsExist,函式直接回錯。外層的 handler 把「任何錯誤」都當成「already running」→os.Exit(0)。
我用 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 參數時:
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。
- (A) 用
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 前修掉。建議流程:
- Architect Agent 決定 bug 1 的修法(inode 比對 vs. 移除候選 vs. FS 偵測),以及 bug 2 的 flag 契約(改名 / 環境變數 / mock 模式不傳)。
- Backend Agent 實作修復(
app.go兩處改動 + 可能的 server flag 新增)。 - Reviewer 審查。
- 重新
make clean && make dmg,我再跑一次完整 E2E。 - 本次的前置清理步驟(unload launchd)也應寫進 launch-checklist,提醒升級使用者。
附錄:本次使用的清理指令
# 卸下 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