依 R5 五輪決策把 visionA-local 從「Wails 內嵌 Next.js」重構為「Wails
本機伺服器控制台 + 瀏覽器 Web UI」模式(類比 Docker Desktop / Ollama)。
程式碼變動
- M8-1 砍 yt-dlp 全套(後端 resolver / URL handler / 前端 URL tab /
Makefile vendor / installer / bootstrap / CI workflow,-555 行)
- M8-2 砍 Mock 模式全套(driver/mock、mock_camera、Settings runtimeMode、
VISIONA_MOCK 環境變數,-528 行)
- M8-3 ffmpeg 從 GPL 切換到 LGPL 混合方案:Windows/Linux 用 BtbN 現成
LGPL binary,macOS 自 build minimal decoder-only 進 git
(vendor/ffmpeg/macos/ffmpeg 5.7MB + ffprobe 5.6MB,比 GPL 版省 85% 空間)
- M8-4 Wails Server Controller:state machine、log ring buffer 2000 行、
preferences.json atomic write、boot-id、Gin SkipPaths、shutdown 7+1 秒、
notify_*.go 三平台 OS 通知、watchServer 改 Error state 不 os.Exit
- M8-4b 啟動階段管線 R5-E:6 階段進度 event、20s soft / 60s hard timeout、
stage 5/6 skip 規則、sentinel file、RestartStartupSequence 5 步驟
- M8-5 Wails 控制台 vanilla HTML/JS/CSS(9 檔 ~2012 行)取代 M7-B splash:
state 視覺、log panel、startup progress panel、Stage 6 manual CTA
pulse、shutdown modal、Settings、Dark Mode、i18n 中英雙語
- M8-6 上傳影片副檔名擴充(mp4/avi/mov/mpeg/mpg)
- M8-7 Web UI Server Offline Overlay(role=alertdialog + focus trap +
wsEverConnected 容錯 + Page Visibility)
- M8-8 CORS middleware(127.0.0.1/localhost only + suffix attack 防護)+
ws/origin.go 獨立 WebSocket CheckOrigin 避 package cycle
- MAJ-4 server:shutdown-imminent WebSocket broadcast 機制
(/ws/system endpoint + notifyShutdownImminent helper)
- M8-9 Boot-ID + 瀏覽器 tab 自動重連(sessionStorage loop guard)
品質
- ~105+ 新 unit test + race detector (-count=2) 全綠
- 10 個 milestone 全部通過 Reviewer 審查
- 三方 v2 + v2.1 文件(PRD / Design Spec / TDD)+ 交叉互審紀錄
收錄在 .autoflow/
交付前待處理(M8-10)
- 重跑 make payload-macos 把舊 GPL 77MB binary 換成新 LGPL
- 三平台 end-to-end build 驗證
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
35 KiB
Architect 交叉審閱 Design Spec v2(2026-04-14)
審閱者:Architect Agent 對象:
03-design/design-spec-v2.md+03-design/v2/*.md基準:04-architecture/TDD-v2.md+04-architecture/v2/*.md
摘要
- 結論:有條件通過(需小改)
- Major:2(A-4 Settings 儲存 spec + A-5 Linux 預設值)
- Minor:12
- 是否阻擋進開發:不阻擋,Major 都是 < 10 行 spec 改動,可在 M8-4/M8-5 前補
A. Design ↔ TDD 實作一致性
| Design v2 | TDD v2 | 一致性 | 備註 |
|---|---|---|---|
control-panel.md(5 態 + 元件 + i18n) |
control-panel.md + server-lifecycle.md §5 |
⚠️ 部分 | A-1~A-5 |
server-offline-overlay.md(role=alertdialog + focus trap + 3s active polling) |
web-ui-offline-overlay.md |
⚠️ 間隔差 | A-6 |
source-selector-update.md(砍 URL + .mpeg/.mpg + i18n) |
deletions.md §1 §5 |
✅ 大致 | A-7 |
first-run-update.md(2 步 + 硬體可略過) |
未描述(屬 Next.js onboarding) | ✅ 無衝突 | — |
settings-update.md(auto-open toggle) |
control-panel.md §4.1 + server-lifecycle.md §2.1 |
⚠️ 命名差 | A-4 |
A-1(Minor)狀態名大小寫 Design 用首大寫 / TDD 用全小寫 — 語意同,i18n key 對齊即可。不需改。
A-2(Minor)Design 3 顆 Primary(Stop/Restart 收 Manage dropdown)vs TDD 6 顆扁平。binding 層一致,Design 是 UI 組合方式。M8-5 實作時 Manage dropdown item 呼叫 TDD 定義的 StopServer()/RestartServer() bindings。我會在 M8-5 前補到 TDD control-panel.md §3 註記。
A-3(Minor)Log 行數上限:Design 1000 / TDD 2000。決案採 2000(Go ring buffer 容量常數,~400KB 記憶體可忽略)。Design Spec §4.4 需改 1000 → 2000(含 Footer 的 Lines: {current} / 1000)。
**A-4(Major)**Settings 儲存 spec 不一致
- Design Spec
settings-update.md §2.2:settings.json+ 「走 Wails 既有 settings store」 - TDD v2
control-panel.md §4.1:preferences.go+<dataDir>/preferences.json - 問題:Wails v2 沒有內建 settings store(Design 誤解);現況無任何 settings 存檔機制,需新建
- 決案:採用 TDD 的
preferences.json+<dataDir>/路徑 + 新建visiona-local/preferences.go - Design 需改:§2.2 檔名改
preferences.json,刪掉「走 Wails 既有 settings store」一句
**A-5(Major)**R5-D2 Linux 預設 OFF 兩份 spec 都未落地
- Design 寫「預設值:ON」三平台一致
- TDD
Preferences{OpenBrowserOnStart bool}預設填true - 決案:新增
DefaultPreferences()依runtime.GOOS回傳:func DefaultPreferences() Preferences { return Preferences{OpenBrowserOnStart: runtime.GOOS != "linux"} } - Design 需改:§2.2「預設值」那格加註「macOS/Windows=ON,Linux=OFF(xdg-open 極簡 WM 可能失敗)」
- TDD 需改:
control-panel.md §4.1補DefaultPreferences()(我負責 M8-4 前補)
A-6(Minor)Offline Overlay 間隔不同
- Design:10s 正常 / 失敗 2 次 / overlay 期間 3s
- TDD:5s 正常 / 失敗 3 次(無 active 間隔切換)
- 決案採 Design(10s 省 CPU,3s active 更貼合 R5-D3 重啟救援體驗)
- TDD 需改:
web-ui-offline-overlay.md §3.1POLL_INTERVAL_MS=10000/FAILURE_THRESHOLD=2+ overlay active 切換至3000(我負責 M8-7 前補)
A-7(Minor)i18n 刪除清單微差
- Design 砍
camera.uploadFile(TDD 漏列) - TDD 砍
cannotOpenVideoUrl(Design 漏列) - M8-1 執行者取聯集即可
B. R5-D 補充決策技術落地
B-1 R5-D1 Server 崩潰保留 OS 原生通知
- 現況
visiona-local/app.go:240-272showNativeError()已有三平台實作(osascript display dialog / PowerShell MessageBox / zenity / kdialog),但這是給reportFatal()用的 modal dialog - TDD v2
control-panel.md §4.7把 watchServer 3 次失敗從reportFatal改為setState(Error),同時等於砍掉 OS 通知 → 違反 R5-D1 - 決案:Error state 時並存兩件事:(1) 控制台 banner(Design §6),(2) 新的 non-blocking toast notification(不是 modal)
- macOS:
osascript -e 'display notification "..." with title "..."'(toast,非 dialog) - Windows:優先
wailsRuntime.SendNotification(見frontend/wailsjs/runtime/runtime.d.ts:275-310已自動生成),fallbackmsg *命令列 - Linux:
notify-send,fallbackzenity --notification
- macOS:
- 新增
visiona-local/notify.go放sendCrashNotification(title, body) - 保留
showNativeError()/reportFatal()僅給 startup 致命錯誤用(data dir 建不起來等) - TDD 需改:
control-panel.md §4.7補sendCrashNotification呼叫(我負責 M8-4 前補) - Design 建議加:§6.2/§6.3 加一句「Error state 時另發 non-blocking OS notification(R5-D1)」
B-2 R5-D2 Linux 預設 OFF — 見 A-5。
B-3 R5-D3 每次啟動都自動開
- TDD
control-panel.md §4.6:if prefs.OpenBrowserOnStart && !autoOpenedThisSession autoOpenedThisSession是 per-App-process flag,每次 Wails 新啟動會重建 → 每次啟動都會開 ✅- Restart Server(同一 App process 內)不會重開瀏覽器 ✅
- 技術落地正確
- Design 建議改:
control-panel.md §7.1第 5 步「[首次 / Settings 為 ON]」→「[每次 / Settings 為 ON]」
C. 體驗 ↔ 技術紅線
C-1 OnBeforeClose:Design Footer 持久提示取代 modal 確認 ↔ TDD server-lifecycle.md §7 OnBeforeClose return false → ✅ 一致。但 Linux AppImage(i3/xmonad)flaky 的 edge case TDD §10 已記錄,M8-10 驗收時實測。
C-2 Offline Overlay focus trap:Design 要求 Tab 只能在「重試 / 了解更多」間循環,TDD web-ui-offline-overlay.md §4.4 沒 focus trap 實作。決案:M8-7 Frontend Agent 手動實作(useEffect 裡 onKeyDown 擋 Tab + 強制 focus,~15 行,不引入 react-focus-lock)。SSR 相容性:'use client' + Zustand + effect 只在 client 跑,✅ 無問題。
C-3 3s active polling 對 log pump 的壓力:0.33 行/秒 vs R-v2-3 的推論 30-100 行/秒差兩個量級,✅ 無壓力。額外建議:server Gin logger 加入 SkipPaths: []string{"/api/system/boot-id"} 避免 boot-id probe 洗版控制台 log panel。我負責 M8-9 前補到 TDD。
C-4 Wails 控制台 i18n:
- Design Spec §9.1 列 30+ key,namespace
control.* - TDD §6.1 範例只 ~15 key,namespace
statusCard.*/actions.* - 決案:以 Design 為準(
control.*更語意化,對齊 Web UI 風格)。M8-5 執行者依 Design Spec §9.1 填visiona-local/frontend/i18n/{zh-TW,en-US}.json
C-5 Dark Mode:Design 要求跟隨系統 + 與 Web UI 共用 token,TDD §2 用 CSS vars + prefers-color-scheme → ✅ 方向一致。建議做法:M8-5 執行者直接從 frontend/src/app/globals.css 複製 :root + [data-theme='dark'] token block 到 visiona-local/frontend/style.css,不重造一份。
D. Architect 7 懸念決策
Q1 NewVideoSourceFromURL 呼叫者(grep 驗證):
camera_handler.go:435在StartFromURL內(即將砍)camera_handler.go:731在 seek handler 的if h.videoIsURL分支;砍StartFromURL後h.videoIsURL永遠 false → dead branch- 決案:
NewVideoSourceFromURL/NewVideoSourceFromURLWithSeek/h.videoIsURLfield 一起砍。M8-1 執行時套用。我負責補到 TDDdeletions.md §1.2末段。
Q3 uuid vs crypto/rand:採 crypto/rand(~10 行 hex encode,不引入 google/uuid 依賴)。TDD server-lifecycle.md §9.1/§10 Q3 以此為準。
Q4 shutdownGracePeriod 5s vs 10s:blocked-on-pm-review(我的意見:建議 10s 對齊 server shutdownFn timeout,使用者等 10s 是罕見 edge case)
Q5 navigator.language fallback:
const raw = (navigator.language || navigator.userLanguage || 'en').toLowerCase();
if (raw.startsWith('zh')) return 'zh-TW';
if (raw.startsWith('en')) return 'en-US';
return 'zh-TW'; // 預設繁中(visionA 團隊內部工具)
TDD §6.2/§8 Q4 以此為準。
Q6 window.close() 限制:blocked-on-design-review(我的意見:Chrome/Safari/Edge 禁止非 window.open() 開啟的 tab 呼叫 window.close()。建議 Design 把「關閉這個頁面」按鈕改為「我知道了」純視覺收斂,或改為提示「請手動關閉此分頁」)
Q7 Preferences JSON atomic write:write-rename pattern
os.WriteFile(path+".tmp", data, 0o644); os.Rename(path+".tmp", path)
POSIX/Windows 都原子。TDD §8 Q3 以此為準。
E. PM 3 技術懸念回答
§11-1 Settings 資料落地位置:
- 檔名
preferences.json,路徑<dataDir>/preferences.json- macOS
~/Library/Application Support/visiona-local/preferences.json - Windows
%APPDATA%\visiona-local\preferences.json - Linux
~/.local/share/visiona-local/preferences.json
- macOS
- 以
migrateOldDataDirs()/ensureDataDir()算出的路徑(visiona-local/app.go:103-155) - JSON 格式:
{"openBrowserOnStart": true} - 原子寫入:write-rename(D-Q7)
- 讀取失敗 fallback:
DefaultPreferences()(平台差異,見 A-5)
§11-2 US-1 AC-1.3 10 秒預算:
| 步驟 | 估時 |
|---|---|
| Wails 載入 + OnStartup | 0.3-0.5s |
| migrate/lock/ipc/seed | 0.1-0.2s |
| ensurePythonRuntime | 0.2-0.5s |
| pickPort + spawn server | 0.05-0.1s |
| Server cold start | 1.5-2.5s |
| waitHealthy 首次成功 | 0.5-1.5s |
| OpenInBrowser + OS open | 0.3-0.8s |
| Next.js SPA 載入 | 0.8-1.5s |
| boot-id + WS ready | 0.3-0.5s |
| 樂觀 | ~4.1s ✅ |
| 悲觀(Intel Mac + system Python) | ~8.1s ✅ |
| Linux AppImage 首次解壓 | +1-2s,仍達標 |
| Windows + Defender 掃描 | +2-3s,最壞 ~11s可能超時 |
- 風險:Windows 首次啟動可能超過 10s。建議 M8-10 驗收實測,超時則 PRD AC-1.3 放寬到 12s
§11-3 idle RAM ≤ 450 MB:
| 程序 | v1 | v2 |
|---|---|---|
| Wails shell | 150-200 MB | 120-160 MB(無 Next.js bundle) |
| Go server | 80-120 MB | 70-100 MB(砍 Mock + yt-dlp) |
| Python sidecar | 150-250 MB | 0 MB 若 on-demand;150-250 MB 若 always spawn |
| Browser tab (Next.js Web UI) | — | 180-240 MB(新項目) |
| 樂觀 | — | ~370 MB ✅ |
| 悲觀 | — | ~500 MB ❌ 超 50 MB |
- 建議:PRD v2 §NFR 明示「450 MB 不含 browser tab,只含 Wails + Go server + Python sidecar;browser tab 視為使用者瀏覽器自己的資源」—— 這個 clarify 符合常識且達標
- 或:放寬到 ≤ 550 MB
- 另需確認:v1
visiona-local/app.go:441的ensureDriverInstalled是否在無硬體時仍 spawn python。若是,v2 建議改為 on-demand 節省 idle RAM
F. 技術衝突
F-1 CORS ↔ 瀏覽器 tab origin:
- Wails「Open in Browser」→
http://127.0.0.1:<port>→ 白名單內 ✅ - 使用者手改
localhost→ 白名單也接受 ✅ - Same-origin fetch(不送 Origin)→
CORSMiddleware()第一個 if 放行 ✅ - 無衝突
F-2 Restart Server port 保留(Major 隱含議題):
- TDD
v2/server-lifecycle.md §3:Restart 後重新pickPort(3721),可能 fallback 到 3722 - Boot-ID 機制假設 tab URL 的 port 不變 → Restart 後 port 變 → 舊 tab
window.location.reload()仍連舊 port → overlay 永久卡死 - 決案:
ServerController.Restart()強制保留舊 port,若 old port 不可用 → 進 Error state 並發 OS notification「port 被占用」,不 fallback。cold start 則仍允許 3721→3722 fallback - TDD 需改:
server-lifecycle.md §3新增 Restart 同 port 規則(我負責 M8-4 前補) - Design Spec
control-panel.md §7.3的 fallback 情境只適用 cold start,不影響 Design,但 Frontend Agent 需知
F-3 Boot-ID 機制在「關 Wails = 結束 server」場景:
- server 沒了 → boot-id 不會變 → 由 overlay 接手(連續失敗 → 顯示)
- 使用者重開 app 若走 cold start port fallback 到新 port → 舊 tab 的 URL(3721)連不上新 port(3722)→ overlay 永卡;使用者需從新 Wails 控制台 Open in Browser 開新 tab
- 設計正確,TDD §2.3 時序已暗示,不需改
G. 臆測 / 超範圍
G-1 Export log 按鈕(Design §4.5):TDD 沒對應 binding。技術可行(wailsRuntime.SaveFileDialog),M8-5 補 ExportLog(path) error binding。我負責補到 TDD §4.2。
G-2 Copy 按鈕(Design §4.5):直接用 navigator.clipboard.writeText() 瀏覽器 API,Wails WebView 支援。不需新 binding。
G-3 Report 按鈕(Design §6.2 Error banner):需要 GitHub Issue URL,目前沒公開 repo。降級:M8-5 若 PM 沒提供 URL → 不實作 Report 按鈕,Error banner 只留 Restart + View details。Design 需加:§6.2 hold 註記「Report 按鈕待 PM 提供 repo URL,暫不實作」。
G-4 Log rotate 7 天/10MB(Design §4.4):TDD 沒 rotate 實作,v1 也無。降級為 append 模式,不做 rotate(rotate 需要 lumberjack.v2 或自刻定時掃描 + size 比較,非 M8 scope)。Design 需改:§4.4 「rotate 7 天/10MB」改為「append 模式;使用者可 Clear Logs 清畫面、Open Log Folder 手動刪檔」。未來 M9+ 再加。
H. 問題清單
Major(阻擋,需修正):
| # | 位置 | 問題 | 修正 |
|---|---|---|---|
| M-1 | Design settings-update.md §2.2 |
檔名 settings.json + 「走 Wails settings store」誤解 |
改 preferences.json,刪掉 Wails settings store 那句 |
| M-2 | Design settings-update.md §2.2 + TDD control-panel.md §4.1 |
R5-D2 Linux 預設 OFF 未落地 | Design 加「macOS/Windows=ON,Linux=OFF」;TDD 加 DefaultPreferences() 依 GOOS |
Minor(不阻擋):
| # | 位置 | 處理者 |
|---|---|---|
| m-1 | Design control-panel.md §4.4 + Footer |
1000 → 2000 |
| m-2 | TDD web-ui-offline-overlay.md §3.1 |
10s/2 次/3s active(我 M8-7 前補) |
| m-3 | M8-1 執行者 | i18n 刪除清單取聯集 |
| m-4 | TDD control-panel.md §4.7 + Design control-panel.md §6 |
Error state 補 OS notification(我 M8-4 前補 TDD) |
| m-5 | Design control-panel.md §7.1 第 5 步 |
「首次」→「每次」對齊 R5-D3 |
| m-6 | M8-7 Frontend Agent | Offline Overlay focus trap 實作 |
| m-7 | TDD server-lifecycle.md §9 |
Gin SkipPaths + crypto/rand(我 M8-9 前補) |
| m-8 | M8-5 執行者 | i18n 以 Design §9.1 為準 |
| m-9 | TDD server-lifecycle.md §3 |
Restart 強制同 port(我 M8-4 前補) |
| m-10 | TDD control-panel.md §4.2 |
補 ExportLog binding(我 M8-5 前補) |
| m-11 | Design control-panel.md §6.2 |
Report 按鈕 hold 註記 |
| m-12 | Design control-panel.md §4.4 |
Log rotate 降級為無 rotate |
I. 結論
✅ 有條件通過 — Design Spec v2 方向正確,與 TDD v2 無不可修復衝突。
Design Agent 必改(Major + 關鍵 Minor):
settings-update.md §2.2(M-1 + M-2)control-panel.md §4.4(m-1 + m-12)control-panel.md §6.2(m-4 + m-11)control-panel.md §7.1(m-5)
TDD 必改(我負責,M8- 執行前補)*:
control-panel.md §4.1(DefaultPreferences()平台差異)control-panel.md §4.2(ExportLogbinding)control-panel.md §4.7(sendCrashNotification+ 新增notify.go)control-panel.md §3(Manage dropdown 註記)server-lifecycle.md §3(Restart 同 port)server-lifecycle.md §9(Gin SkipPaths +crypto/rand)web-ui-offline-overlay.md §3.1(10s/2 次/3s active)deletions.md §1.2(videoIsURLfield 直接砍)
Q 決案狀態:Q1 ✅ Q3 ✅ Q4 ⏳ PM Q5 ✅ Q6 ⏳ Design Q7 ✅
PM 3 技術懸念:
- §11-1 ✅
preferences.json@<dataDir>/,write-rename - §11-2 ✅ 10 秒樂觀 ~4s / 悲觀 ~8s / Windows 邊界 ~11s(M8-10 驗收時實測,可能需放寬到 12s)
- §11-3 ⚠️ idle RAM 需 clarify「不含 browser tab」或放寬到 550 MB;另需確認 Python sidecar 是否 on-demand
不阻擋進開發:所有修正都是 < 10 行 spec 改動,可在 M8-4 / M8-5 執行前 1-2 小時內完成。
審閱日期:2026-04-14 下一步:交 Orchestrator 彙整三方 + R5-D 對齊 + Q4/Q6 收斂 → 使用者最終確認 → M8-1
Architect 第二輪審閱 Design Spec v2.1(2026-04-14)
對象:
design-spec-v2.mdv2.1 索引 +v2/settings-update.mdv2.1 +v2/control-panel.mdv2.1 + 新檔v2/startup-progress.mdv2.1 基準:TDD v2.1v2/startup-pipeline.md(518 行)+ 第一輪審閱結論
摘要
- 總結論:通過,需小改(3 個 Minor,都在 TDD 側補齊)
- 第一輪 Major × 2 / Minor × 12 修復:Major 2/2 修好;Minor 12/12 修好或已落到 TDD/執行者待辦
- R5-E startup-progress.md 技術可行性:可行,event schema 7 成對齊,有 3 處 TDD 側需補小改
- 是否阻擋進 M8 開發:不阻擋,TDD 小改可在 M8-4b 執行前 30 分鐘內補完
A. 第一輪 Major 修復檢查
| # | 第一輪意見 | v2.1 現況 | 結果 |
|---|---|---|---|
| M-1 Settings 落地檔案 | 改 preferences.json + 刪 Wails settings store |
settings-update.md §2.2(L48-56)已明文「preferences.json @ <dataDir>/」+「Go server 端負責讀寫(非 Wails 內建機制 — Wails v2 沒有 settings store)」+「write-rename pattern」;全檔 grep 無 settings.json 殘留,無「Wails settings store」殘留 |
✅ 修好 |
| M-2 R5-D2 Linux 預設 OFF | 加「macOS/Windows=ON,Linux=OFF」依 GOOS | settings-update.md §2.2(L51)「macOS/Windows = ON;Linux = OFF」+ L55「DefaultPreferences() 依 runtime.GOOS」+ §2.2 Linux 首次使用說明(L59-63)+ §7 遷移策略(L180-190)全部落地 |
✅ 修好 |
B. 第一輪 Minor 修復檢查
| # | 第一輪意見 | v2.1 現況 | 結果 |
|---|---|---|---|
| m-1 | Log 上限 1000→2000 | control-panel.md §4.4(L152)「最大行數 2000」、§4.6 Footer(L186)「Lines: {current} / 2000」 |
✅ |
| m-2 | Offline Overlay 10s/2 次/3s active → TDD 側 | 非 Design 修,我負責 M8-7 前補 TDD | ⏳ 我的 TODO(未阻擋) |
| m-3 | i18n 刪除清單聯集 → 執行者 | M8-1 執行者責任 | ⏳ 執行者 TODO |
| m-4 | Error state OS notification | control-panel.md §6.3(L269-275)「R5-D1 OS 原生通知並存」完整三平台實作表;Design Diff §12-6(L459) |
✅ |
| m-5 | §7.1 「首次」→「每次」 | control-panel.md §7.1(L310)「5. 【每次 / Settings 為 ON(macOS/Windows 預設;Linux 預設 OFF)】」;L331「每次啟動(每次 Wails App process 新啟動)都會跑完整 6 階段流程」 |
✅ |
| m-6 | Offline Overlay focus trap | M8-7 Frontend 責任 | ⏳ 執行者 TODO |
| m-7 | Gin SkipPaths + crypto/rand → TDD 側 | 我負責 M8-9 前補 | ⏳ 我的 TODO |
| m-8 | i18n 以 Design §9.1 為準 | Design control-panel.md §9(L387-408)完整 control.* 清單 |
✅ |
| m-9 | Restart 強制同 port → TDD 側 | 我負責 M8-4 前補 | ⏳ 我的 TODO |
| m-10 | ExportLog binding → TDD 側 |
我負責 M8-5 前補 | ⏳ 我的 TODO |
| m-11 | Report 按鈕 hold | control-panel.md §6.2(L265)「【hold】 現階段先不實作」;startup-progress.md §3.7(L221)「Report Issue 【hold】」 |
✅ |
| m-12 | Log rotate 降級 | control-panel.md §4.4(L152-162)「無落地寫檔(in-memory ring buffer)」+「為什麼取消落地寫檔與 rotate」整段說明 |
✅ |
修復統計:Design 側 8/8 已修(m-1、m-4、m-5、m-8、m-11、m-12 + M-1、M-2);TDD 側 4 個由我自己負責(m-2、m-7、m-9、m-10);執行者側 2 個(m-3、m-6)。無 Design 側遺漏。
C. R5-E startup-progress.md 技術可行性(重中之重)
C-1 Wireframe DOM/CSS 難度
startup-progress.md §2(L25-101)三個 wireframe 全部 text-based,StageItem 結構(§3.3 L141-154)明確示範 vanilla HTML + CSS class data-state,可直接對齊 TDD startup-pipeline.md §6(L407-491)的 vanilla JS 實作。無 framework-specific 要求。✅
C-2 6 階段定義對應 TDD §3 階段表
| # | Design label | TDD §3 Go 實作點 |
對應 |
|---|---|---|---|
| 1 | 初始化控制台 | app.go:startup 首行 → seedUserDataDir() 返回 |
✅ |
| 2 | 檢查 Python 執行環境 | startServerV2 → ensurePythonRuntime() 返回 |
✅ |
| 3 | 啟動本機伺服器 | waitHealthy(port, 30s) 返回 |
✅ |
| 4 | 偵測 Kneron 裝置 | GET /api/devices 第一次 response |
✅ |
| 5 | 開啟瀏覽器 | OpenInBrowser("") 命令 return |
✅ |
| 6 | 等待 Web UI 連線 | WebSocket hub OnClientConnected 首次 |
✅ |
完全對齊。Design §8 的訊號來源(L361-367)和 TDD §3 表格用詞一致。✅
C-3 Event schema 一致性(發現 3 個小衝突)
Design startup-progress.md §9(L386-393)預設的 event 名:
startup:stage {id, state, errorMessage?}
startup:complete / startup:error
TDD startup-pipeline.md §1(L28-84)定義的 event 名:
startup:progress (StartupProgressEvent: stage, totalStages, labelKey, status, startedAt)
startup:stage-timeout (StartupStageTimeoutEvent: stage, softTimeoutSeconds)
startup:error (StartupErrorEvent: stage, error, cause)
startup:ready (無 payload)
衝突 1:事件名 startup:stage vs startup:progress 不一致。決案:採 TDD 的 startup:progress(更語義化;stage 單字太模糊)。Design Agent 不必改 startup-progress.md §9(備忘性質),由 M8-4b / M8-5 執行者以 TDD 為準。
衝突 2:Design StageState 列舉(§8 L335-341)有 pending | running | running-slow | done | failed | skipped,TDD StartupProgressEvent.Status(§1.1 L41)只定義 pending | running | completed | failed。
差異分析:
running-slow— Design 明確標註「UI 派生狀態」(L338 註解),由前端根據now - startedAt > 20s計算,Go 端不需 emit → 無衝突donevscompleted— 語義同,用詞差。決案:Go 端 emitcompleted,前端 Design class name 用done即可(CSS 層面對應)skippedGo 端完全沒有 — 這是 TDD 側 Minor 問題,要補
衝突 3:soft timeout 事件名不同。Design §9 預設「前端自己 timer 檢查 20 秒 → 派生 running-slow」,TDD §1.2 定義「Go 端 watcher goroutine emit startup:stage-timeout」。兩種實作都可行,決案以 TDD 為準(後端 source of truth,避免兩邊各自計時導致時間漂移),Design 前端僅負責接 startup:stage-timeout event 更新 UI。
小結:事件協議 Design 預設是草稿,TDD 是 ground truth;M8-4b/M8-5 執行者以 TDD 為準即可,Design Agent 無需修 startup-progress.md §9。但 TDD 需補 skipped status(見 E 節)。
C-4 Timeout UI 觸發時機對齊
- 20 秒 soft timeout:Design §3.6(L189-205)「
stage.state === 'running' && (now - stage.startedAt) > 20_000ms」;TDD §4 watcher goroutine(L332-346)sinceStage > startupSoftTimeout+softTimeoutEmittedflag 確保只 emit 一次。✅ 觸發時機一致(Go emit event → 前端收到顯示 hint) - 60 秒 hard timeout:Design §3.7(L207-224)「任一階段 failed 或總計 > 60 秒 → Error mode」;TDD §4 watcher(L325-330)
sinceTotal > startupHardTimeout→Fail(cur, ...)+emitError(cur, ..., "total-timeout")。✅ 一致
C-5 無障礙可行性
role="progressbar"+aria-valuemin="0"+aria-valuemax="6"+aria-valuenow— vanilla HTML<section>直接標即可 ✅aria-live="polite"搭配.sr-onlydiv — Design §6(L281)已設計,vanilla JStextContent更新即可觸發 SR 讀出 ✅⌘0/Ctrl+0focus +Esc— Design §6(L283):vanilla JSaddEventListener('keydown')攔截即可 ✅prefers-reduced-motion— CSS media query,vanilla CSS 支援 ✅
可行性:100%。不需引入任何 third-party library。
C-6 Linux / Toggle OFF 分支(重要)
Design 規格(§4.1 L244-256):
- 階段 5:
pending → skipped(不經過running),顯示 ⏭ icon - 階段 6:
pending → running,description 改 manualHint,不套 20 秒 retry hint - 當使用者手動點 Open in Browser 並建立 WebSocket →
done
TDD §3 表(L117)階段 5:「若 AutoOpenBrowser=false 則立即 complete 跳過」— TDD 是 completed,Design 是 skipped,語義不同。Design 要顯示「跳過(依偏好設定)」⏭ icon,不是綠勾 ✅。
TDD 也沒有處理階段 6 不套 soft timeout 的情境:§4 watcher 對所有階段一視同仁,Toggle OFF 時階段 6 會在 20 秒後觸發 startup:stage-timeout,前端會顯示「正在重試...」—— 這是錯的,因為在等人為動作。
兩個問題都是 TDD 側需補(見 E 節 E-2 / E-3)。
C-7 i18n key 30+ 條
Design §7(L291-329)列出 27 個 i18n key,namespace 一致用 startup.*。TDD §2(L87-103)只列 11 個 key 作為骨架,註明「文案由 Design Spec v2.1 敲定」。
key 名稱微差:
- Design:
startup.panel.title/ TDD:startup.title— 統一採 Design 的startup.panel.title(更完整的 namespace 樹狀結構) - Design:
startup.timeout.message/ TDD:startup.retrying— 統一採 TDD 的startup.retrying(對應 TDD eventstartup:stage-timeout的 copy,更精準) - Design:
startup.error.description.timeout/ TDD:startup.error.totalTimeout— 統一採 Design 的樹狀版
這些微差不影響可行性,M8-4b/M8-5 執行者統一用 Design §7 清單 + TDD 補上 labelKey path 即可。載入時機:Wails 控制台前端 app.js 在 main() 最前呼叫 i18n/loader.js 載入 JSON,早於 initStartupPanel(),順序正確。✅
可行性:可行。
7 子項總結:全部可實作。發現的 3 個技術衝突都在 TDD 側(不在 Design 側)— skipped status、階段 6 不套 soft timeout、RestartStartupSequence binding — 由我在 M8-4b 執行前補 TDD 修復。
D. Design v2.1 新增 3 個懸念的技術回答
D-Q1「正在重試」vs「正在處理中」文案
技術面:不影響實作。Go 端只 emit startup:stage-timeout event,文案在前端 i18n JSON,改字面 0 行程式碼成本。
Architect 決案:交由 Design 最終定稿即可,我不干涉文案選擇。若要技術建議:採 Design 原提案「正在重試...」—— event 名本身是 stage-timeout,副文字用「重試」與 icon ⚠ 搭配更有「系統知道有狀況且在努力」的語感,對齊 Nielsen Norman perceived-performance 原則。不阻擋。
D-Q2 WebSocket 被安全軟體攔截的偵測
技術面評估:不可行。
- Windows Defender SmartScreen 攔截瀏覽器連線時,Go server 端
OnClientConnected根本不會被呼叫,server 無法區分「使用者還沒打開瀏覽器」、「瀏覽器正在載入 Next.js」、「瀏覽器被擋」這三種情境 - 唯一能辨識的訊號是「階段 5 已 complete 但階段 6 > 30 秒未 complete」,但這個時間閾值和正常冷啟動(悲觀 ~8 秒 Next.js + WS)無法可靠區分
- 若硬要辨識,需要辨識多種安全軟體的特徵(Defender SmartScreen / McAfee / Kaspersky / 企業 MDM),過度設計
- 接受 Design 建議:不做特殊偵測,Error 說明用通用文案「階段 6 超時,請檢查瀏覽器是否能開啟 http://127.0.0.1:{port}」+ 引導看 log details 即可
Architect 決案:同意 Design 建議,不做特殊偵測。M8-5 前端 i18n 的 startup.error.description.timeout 文案建議補上「請確認瀏覽器可存取 127.0.0.1:{port}」作為通用提示。
D-Q3 Retry 按鈕語意:重置整個啟動 vs 重試當前階段(關鍵技術決策)
技術面評估:「重置整個啟動流程」可行且建議採用。
實作細節(RestartStartupSequence() 新 function,位於 startup_pipeline.go):
// RestartStartupSequence 停掉當前 pipeline,清理 server 狀態,重新跑 pipeline 從階段 1 開始。
// 只能在 Error state 時呼叫(Starting/Running 時 noop)。
func (a *App) RestartStartupSequence() error {
if a.ctrl.State() != ServerStateError {
return errors.New("RestartStartupSequence only allowed in Error state")
}
// 1. 停掉舊 watcher(若還活著)
if a.pipeline != nil {
a.pipeline.stopWatcher()
}
// 2. 強制 kill 舊 server process(若還存在)
// ctrl.Stop() 會走 graceful → 這裡用 ForceKill 因為 server 很可能已處於壞狀態
a.ctrl.ForceKill()
// 3. 重置 ServerController state machine 回 Stopped
a.ctrl.setState(ServerStateStopped, "")
// 4. 重建 StartupPipeline(清掉所有 stage state)
a.pipeline = NewStartupPipeline(a)
a.pipeline.Start(a.ctx)
// 5. 重跑 ctrl.Start() — 會依序觸發階段 2-6
return a.ctrl.Start()
}
關鍵考量:
- 階段 1 不需重跑:Wails
OnStartup已經跑過,seedUserDataDir/migrateOldDataDirs等只需跑一次。建議:Retry 時pipeline.Start(ctx)後直接Complete(1)(0.0 秒完成,視覺上瞬過) - 階段 2
ensurePythonRuntime()重跑成本:若 Python venv 已在<dataDir>/python/解壓完成,ensurePythonRuntime()內部 check 應該 < 100ms return;若 Python extract 到一半失敗需重跑,成本較高但仍有邊界(Architect §11-2 估悲觀 0.5s)。可接受 - 階段 3 server 全砍重起:必須全砍。舊 server process 若處於半掛狀態(port 卡著、goroutine leak),保留只會讓新 pipeline 再卡一次。
ForceKill→pickPort重新選 port — 但這和第一輪 F-2(Restart 強制同 port)有衝突,需要微調:Retry 情境下 port 可以 fallback(反正 Error state 下舊 Web UI tab 已顯示 overlay,使用者會從新控制台 Open in Browser 開新 tab) - 階段 6 WebSocket sentinel file 清理:若 TDD §3 採 sentinel file
<dataDir>/.first-ws-connected,Retry 時必須 先os.Remove()sentinel file,否則新階段 6 會瞬間completed錯誤 - Wails binding:需新增
RestartStartupSequence() error到app.gobindings,前端 Design §3.7 Retry 按鈕onclick呼叫window.go.main.App.RestartStartupSequence()
Architect 決案:可行,採用「重置整個啟動流程」語意。我會在 TDD v2/startup-pipeline.md 新增 §9「Retry 機制」小節落地上述實作細節(M8-4b 執行前補)。Design Agent 無需修 startup-progress.md,維持 §3.7 Retry 按鈕「重置進度面板,重新跑階段 1」的 wireframe 文字即可。
E. 新發現的 Design ↔ TDD 衝突(TDD 側 Minor)
E-1 WebSocket OnClientConnected → Wails event 路徑未敲定
- Design
startup-progress.md §9-4(L391)明確要求「Go WebSocket hubOnConnect事件中 emitstartup:stage {id: 6, state: 'done'}」 - TDD
startup-pipeline.md §3(L122-132)提出兩個選項:long-poll endpoint 或 sentinel file,尚未敲定 - Architect 補決案:採 sentinel file 方案(long-poll endpoint 要改 server 路由、rg ctx 維護成本較高)。server WebSocket hub 的
OnClientConnectedcallback 首次觸發時os.WriteFile(<dataDir>/.first-ws-connected, []byte{}, 0o644);Wails 階段 5 complete 後 goroutinefor { if exists { pipeline.Complete(6); return }; time.Sleep(200ms) },30 秒逾時pipeline.Fail(6, ...) - TDD 需改:
startup-pipeline.md §3表格階段 6 列敲定 sentinel 方案(我負責 M8-4b 前補)
E-2 skipped status Go 端未定義
- Design §3.4(L165)、§4.1(L244-248)明確需要
skippedstatus(階段 5 Toggle OFF 時) - TDD §1.1
StartupProgressEvent.Status只有pending | running | completed | failed,缺skipped - Architect 補決案:TDD
startup-pipeline.md §1.1新增"skipped"到 Status 枚舉;§4StartupPipeline新增Skip(stage int)method,內部p.stages[stage].status = "skipped"+emitProgress+ 自動進入下一階段(不Fail) - TDD 需改:§1.1 補 status、§4 補
Skip()method(我負責 M8-4b 前補)
E-3 階段 6 AutoOpenBrowser=false 時不觸發 soft timeout
- Design §4.1(L255)「不套 20 秒 retry hint(因為是等待人為動作,不是系統卡住)」
- TDD §4 watcher goroutine 對所有階段一視同仁,未排除階段 6 Toggle OFF 情境
- Architect 補決案:TDD §4 watcher 新增條件
if cur == 6 && !p.app.prefs.OpenBrowserOnStart { continue }(跳過 soft timeout 檢查);hard timeout 仍照常檢查(使用者 60 秒內不點 Open in Browser 仍應進 Error state,合理) - TDD 需改:§4 watcher goroutine 條件補上(我負責 M8-4b 前補)
F. 第二輪新發現問題
Major(阻擋)
無。
Minor(不阻擋)
| # | 位置 | 問題 | 處理 |
|---|---|---|---|
| 2-m1 | TDD startup-pipeline.md §1.1 |
StartupProgressEvent.Status 缺 "skipped" |
我 M8-4b 前補 |
| 2-m2 | TDD startup-pipeline.md §3 |
階段 6 WebSocket sentinel 方案未敲定 | 我 M8-4b 前補(採 sentinel file) |
| 2-m3 | TDD startup-pipeline.md §4 + 新增 §9 |
watcher 階段 6 Toggle OFF 跳過 soft timeout;新增 RestartStartupSequence() Retry 小節 |
我 M8-4b 前補 |
G. 第二輪結論
✅ 通過(需小改,TDD 側 3 個 Minor)
Design Spec v2.1 側:
- Major 2/2 修好 —
settings-update.md §2.2檔名 + Linux 預設 OFF 全部落地 - Design 側 Minor 8/8 修好(m-1/m-4/m-5/m-8/m-11/m-12 + M-1/M-2)
- 無新發現 Design 側問題,Design Agent 無需再動 v2.1 任何檔案
TDD v2.1 側(我自己負責,M8-4b 執行前補):
- 第一輪遺留 4 項(m-2/m-7/m-9/m-10)
- 第二輪新增 3 項(2-m1/2-m2/2-m3)
- 合計 7 項小改,估計 1-2 小時完成
R5-E startup-progress.md 通過:Design 定義完整且技術可行,與 TDD startup-pipeline.md 7 成對齊,3 處差異全部是 TDD 側可小改。
3 個 Design v2.1 懸念已技術回答:
- D-Q1 文案:不影響技術,Design 自由定稿
- D-Q2 安全軟體偵測:不做(過度設計),採通用文案
- D-Q3 Retry 按鈕語意:採「重置整個啟動流程」,
RestartStartupSequence()可行,實作細節見 D-Q3 區塊
不阻擋進 M8 開發。三方 v2.1 已收斂。
下一步:Orchestrator 可將此第二輪結論 + Design v2.1 現況 + TDD 7 項待補 → 呈報使用者最終確認 → 我補 TDD 小改 → 進 M8-1。
審閱日期:2026-04-14(第二輪)