visionA/local-tool/.autoflow/03-design/reviews/architect-review-of-design-spec-v2.md
jim800121chen 8cd5751ce3 feat(local-tool): M8 重構 — Wails 控制台 + 瀏覽器 Web UI(R5 決策)
依 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>
2026-04-15 17:57:54 +08:00

552 lines
35 KiB
Markdown
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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.

# Architect 交叉審閱 Design Spec v22026-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-1A-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**MinorDesign 3 PrimaryStop/Restart Manage dropdownvs TDD 6 顆扁平binding 層一致Design UI 組合方式。**M8-5 實作時 Manage dropdown item 呼叫 TDD 定義的 `StopServer()`/`RestartServer()` bindings**。我會在 M8-5 前補到 TDD `control-panel.md §3` 註記
**A-3**MinorLog 行數上限Design 1000 / TDD 2000。**決案採 2000**Go ring buffer 容量常數~400KB 記憶體可忽略)。**Design Spec §4.4 需改 1000 2000** Footer `Lines: {current} / 1000`)。
**A-4Major**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 storeDesign 誤解現況無任何 settings 存檔機制需新建
- **決案**採用 TDD `preferences.json` + `<dataDir>/` 路徑 + 新建 `visiona-local/preferences.go`
- **Design 需改**:§2.2 檔名改 `preferences.json`刪掉 Wails 既有 settings store一句
**A-5Major**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=ONLinux=OFFxdg-open 極簡 WM 可能失敗)」
- **TDD 需改**`control-panel.md §4.1` 補 `DefaultPreferences()`(我負責 M8-4 前補)
**A-6**MinorOffline Overlay 間隔不同
- Design10s 正常 / 失敗 2 次 / overlay 期間 3s
- TDD5s 正常 / 失敗 3 次(無 active 間隔切換)
- **決案採 Design**10s 省 CPU3s 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**Minori18n 刪除清單微差
- 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) 控制台 bannerDesign §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 notificationR5-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 AppImagei3/xmonadflaky 的 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+ keynamespace `control.*`
- TDD §6.1 範例只 ~15 keynamespace `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 共用 tokenTDD §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.110 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`,路徑 `<dataDir>/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-renameD-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-demand150-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 sidecarbrowser 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 的 URL3721連不上新 port3722→ overlay 永卡;使用者需從新 Wails 控制台 Open in Browser 開新 tab
- **設計正確**TDD §2.3 時序已暗示,不需改
---
## G. 臆測 / 超範圍
**G-1 Export log 按鈕**Design §4.5TDD 沒對應 binding。技術可行`wailsRuntime.SaveFileDialog`**M8-5 補 `ExportLog(path) error` binding**。我負責補到 TDD §4.2。
**G-2 Copy 按鈕**Design §4.5):直接用 `navigator.clipboard.writeText()` 瀏覽器 APIWails 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.4TDD 沒 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=ONLinux=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` @ `<dataDir>/`write-rename
- §11-2 ✅ 10 秒樂觀 ~4s / 悲觀 ~8s / Windows 邊界 ~11sM8-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.12026-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` @ `<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=ONLinux=OFF」依 GOOS | `settings-update.md §2.2`L51「macOS/Windows = ONLinux = 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 FooterL186「`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-6L459| ✅ |
| m-5 | §7.1 「首次」→「每次」 | `control-panel.md §7.1`L310「5. 【每次 / Settings 為 ONmacOS/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-2TDD 側 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-basedStageItem 結構§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 truthM8-4b/M8-5 執行者以 TDD 為準即可,**Design Agent 無需修 startup-progress.md §9**。但 **TDD 需補 `skipped` status**(見 E 節)。
### C-4 Timeout UI 觸發時機對齊
- **20 秒 soft timeout**Design §3.6L189-205「`stage.state === 'running' && (now - stage.startedAt) > 20_000ms`」TDD §4 watcher goroutineL332-346`sinceStage > startupSoftTimeout` + `softTimeoutEmitted` flag 確保只 emit 一次。✅ **觸發時機一致**Go emit event → 前端收到顯示 hint
- **60 秒 hard timeout**Design §3.7L207-224「任一階段 failed 或總計 > 60 秒 → Error mode」TDD §4 watcherL325-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-only` div — Design §6L281已設計vanilla JS `textContent` 更新即可觸發 SR 讀出 ✅
- `⌘0` / `Ctrl+0` focus + `Esc` — Design §6L283vanilla JS `addEventListener('keydown')` 攔截即可 ✅
- `prefers-reduced-motion` — CSS media queryvanilla 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 §7L291-329列出 27 個 i18n keynamespace 一致用 `startup.*`。TDD §2L87-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 已在 `<dataDir>/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-2Restart 強制同 port有衝突需要微調**Retry 情境下 port 可以 fallback**(反正 Error state 下舊 Web UI tab 已顯示 overlay使用者會從新控制台 Open in Browser 開新 tab
4. **階段 6 WebSocket sentinel file 清理**:若 TDD §3 採 sentinel file `<dataDir>/.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(<dataDir>/.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.4L165、§4.1L244-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.1L255「不套 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第二輪