# 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` + `/preferences.json` - **問題**:Wails v2 **沒有**內建 settings store(Design 誤解);現況無任何 settings 存檔機制,需新建 - **決案**:採用 TDD 的 `preferences.json` + `/` 路徑 + 新建 `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` 回傳: ```go 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.1` `POLL_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-272` `showNativeError()` 已有三平台實作(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` 已自動生成),fallback `msg *` 命令列 - Linux:`notify-send`,fallback `zenity --notification` - 新增 `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.videoIsURL` field 一起砍。M8-1 執行時套用。我負責補到 TDD `deletions.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**: ```javascript 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** ```go os.WriteFile(path+".tmp", data, 0o644); os.Rename(path+".tmp", path) ``` POSIX/Windows 都原子。TDD §8 Q3 以此為準。 --- ## E. PM 3 技術懸念回答 **§11-1 Settings 資料落地位置**: - 檔名 `preferences.json`,路徑 `/preferences.json` - macOS `~/Library/Application Support/visiona-local/preferences.json` - Windows `%APPDATA%\visiona-local\preferences.json` - Linux `~/.local/share/visiona-local/preferences.json` - 以 `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:` → 白名單內 ✅ - 使用者手改 `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`(`ExportLog` binding) - `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`(`videoIsURL` field 直接砍) **Q 決案狀態**:Q1 ✅ Q3 ✅ Q4 ⏳ PM Q5 ✅ Q6 ⏳ Design Q7 ✅ **PM 3 技術懸念**: - §11-1 ✅ `preferences.json` @ `/`,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.md` v2.1 索引 + `v2/settings-update.md` v2.1 + `v2/control-panel.md` v2.1 + **新檔** `v2/startup-progress.md` v2.1 > 基準:TDD v2.1 `v2/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` @ `/`」+「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 → **無衝突** - `done` vs `completed` — 語義同,用詞差。**決案:Go 端 emit `completed`**,前端 Design class name 用 `done` 即可(CSS 層面對應) - **`skipped` Go 端完全沒有** — 這是 **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` + `softTimeoutEmitted` flag 確保只 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 `
` 直接標即可 ✅ - `aria-live="polite"` 搭配 `.sr-only` div — Design §6(L281)已設計,vanilla JS `textContent` 更新即可觸發 SR 讀出 ✅ - `⌘0` / `Ctrl+0` focus + `Esc` — Design §6(L283):vanilla JS `addEventListener('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 event `startup: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`): ```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. **階段 1 不需重跑**:Wails `OnStartup` 已經跑過,`seedUserDataDir` / `migrateOldDataDirs` 等只需跑一次。**建議:Retry 時 `pipeline.Start(ctx)` 後直接 `Complete(1)`**(0.0 秒完成,視覺上瞬過) 2. **階段 2 `ensurePythonRuntime()` 重跑成本**:若 Python venv 已在 `/python/` 解壓完成,`ensurePythonRuntime()` 內部 check 應該 < 100ms return;若 Python extract 到一半失敗需重跑,成本較高但仍有邊界(Architect §11-2 估悲觀 0.5s)。**可接受** 3. **階段 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) 4. **階段 6 WebSocket sentinel file 清理**:若 TDD §3 採 sentinel file `/.first-ws-connected`,Retry 時必須 **先 `os.Remove()` sentinel file**,否則新階段 6 會瞬間 `completed` 錯誤 5. **Wails binding**:需新增 `RestartStartupSequence() error` 到 `app.go` bindings,前端 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 hub `OnConnect` 事件中 emit `startup: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 的 `OnClientConnected` callback 首次觸發時 `os.WriteFile(/.first-ws-connected, []byte{}, 0o644)`;Wails 階段 5 complete 後 goroutine `for { 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)明確需要 `skipped` status(階段 5 Toggle OFF 時) - TDD §1.1 `StartupProgressEvent.Status` 只有 `pending | running | completed | failed`,**缺 `skipped`** - **Architect 補決案**:TDD `startup-pipeline.md §1.1` 新增 `"skipped"` 到 Status 枚舉;§4 `StartupPipeline` 新增 `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(第二輪)