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

23 KiB
Raw Permalink Blame History

Reviewer 審查 M8-4 ServerController + Log Ring Buffer + Preferences + Notify + Boot-ID2026-04-15

摘要

  • 總結論⚠️ 需小改(核心架構正確,有 2 個 Major 阻擋後續 milestone
  • 阻擋情況
    • 阻擋 M8-5/M8-7/M8-9watchServerV2 在 Stop 後仍存活會誤觸 Error state誤發崩潰通知→ 直接破壞 M8-5 控制台 UXserver 端 server:shutdown-imminent WebSocket 廣播未實作會直接讓 M8-9 的 race 風險回來
    • 不阻擋 M8-4bM8-4b 是擴充 ServerController 加 pipeline hook本層 state machine 介面已就緒,可平行進行
  • 親跑驗證build / vet / test / race detector 全綠smoke test boot-id + SkipPaths 完全符合預期

A. Log Ring Bufferlog_buffer.go

檢查項 結論 備註
容量 2000 行 logBufferCap = 2000L26
Thread-safe Append/Snapshot/Reset/Size/Dropped 全 mutex
Wrap-around head 環形遞增、size 飽和、dropped 計數正確test 驗證 line-100..2099
Snapshot 回 copy out := make([]LogLine, 0, n) + appendL96-105
Rate limit 200/sec burst ⚠️ Minor 演算法本身正確(固定視窗 CAS呼叫單位有偏差(見 D 段)
parseLogLevel bracket / 冒號 / Gin 三種格式齊備;空字串、非 ASCII 不 panic
5 個 unit test TestLogBuffer_*、TestParseLogLevel 全綠

Minor 1A.1Snapshoti := 0 走到 b.size,前 skip 行只是 continue可以直接從 (start + skip) % logBufferCap 開始走 n 步,效率較好。當 size = 2000、n = 100 時白跑 1900 圈。非阻擋,效能影響小於 0.1 ms。

Minor 2A.2ShouldEmit 內 CAS 與 Store(0) 的微 race — A 視窗內 CAS 成功還沒 Store(0) 之前B 已先 Add(1),可能讓新視窗第一行被舊 count 計算。實際只會多放 ≤ 1 行 emit不影響功能正確性,可不修。


B. Preferencespreferences.go

檢查項 結論 備註
DefaultPreferences() 平台分支 runtime.GOOS != "linux" → trueL42
Atomic write-rename tmp → os.Rename → cleanupL78-95
讀取失敗 fallback 缺檔 / JSON 損毀 → 回 default + warningL57-71
JSON 欄位齊全 autoOpenBrowser / locale / logRingSize
5 個 unit test 平台預設 / missing file / corrupt JSON / roundtrip / 殘留 tmp

極佳SavePreferences 失敗時 os.Remove(tmpPath) 把垃圾 tmp 清掉L92符合 atomic 持久化最佳實踐。

Minor 3B.1SavePreferences 路徑在跨檔案系統 rename 時會失敗(罕見邊界,例如 dataDir 是 NFS / overlayfs。建議將來加 io.Copy + os.Remove(tmp) fallback但本 milestone 不必處理。


C. ServerController State Machineserver_control.go

C.1 雙鎖機制

檢查項 結論 備註
State enum 完整 Idle / Starting / Running / Stopping / Stopped / ErrorL43-50
txMu 序列化 transition Start/Stop/ForceKill 都 c.txMu.Lock(); defer c.txMu.Unlock()
mu 保護欄位讀寫 state / proc / startedAt / lastError 全走 mu
setState emit Wails event L120 EventsEmit("server:state-change", status)
鎖順序避免 deadlock a.mu → c.mu 全部單向
race detector go test -race ./... 全綠

C.2 Start / Stop / Restart / ForceKill 行為

行為 結論 備註
Start 允許 cold start fallback startInternal(0)pickPort(defaultPreferredPort)
Restart 強制保留 port L233-244 讀 oldPort → Stop → StartWithPort(oldPort)
StartWithPort 對佔 port 不 fallback L408-411 直接 return error
killStaleServerOnPort 嘗試清乾淨 L405-408500 ms wait
ForceKill 不發 crash 通知 直接呼叫 proc.forceKill(),不走 handleWatchFailure
Shutdown 7 + 1 秒 modal shutdownGraceV2 = 7s、modalTimer = 1sL319-339
Windows 沒 SIGTERM 直接 Kill L307 runtime.GOOS == "windows" 分支
watchServerV2 連 3 次失敗進 Error L644-649 handleWatchFailure
不 os.Exit watchServerV2 完全沒呼叫 reportFatal / os.Exit

C.3 Major 與 Minor 問題(針對 ServerController

Major 1C.MAJ-1Stop() / ForceKill() 不會 cancel watchCancel,導致 watchServerV2 在 server 死後仍存活30 秒3 × 10s會把 state 從 Stopped 翻成 Error,並發 OS 崩潰通知**。

檔案/行server_control.go:198-229Stopserver_control.go:251-265ForceKillserver_control.go:617-652watchServerV2

重現

  1. Start → Running
  2. Stop → state = Stopped、proc = nil
  3. watchServerV2 仍持有舊 sp 與舊 port下一個 ticker 到 → health check fail → failures++
  4. 30 秒後 failures = 3 → handleWatchFailuresetState(Error) + EventsEmit("server:error") + sendCrashNotification("Server 崩潰")

後果:使用者按 Stop 後 30 秒看到「visionA Local — Server 崩潰」OS 通知M8-5 控制台 UI 會把 Stopped 翻紅變 Error。這是嚴重 UX bug

修法Stop()ForceKill() 在持有 txMu 期間先 cancel a.watchCancel

a.mu.Lock()
if a.watchCancel != nil { a.watchCancel(); a.watchCancel = nil }
a.mu.Unlock()

一行集中到 helper 函式更乾淨。

Major 2C.MAJ-2handleWatchFailure 不持 txMu,會與正在進行的 Stop / ForceKill / Restart race

檔案/行server_control.go:269-291

重現

  1. watchServerV2 連續失敗 2 次failures = 2
  2. 使用者按 Stop → ctrl.Stop() 取得 txMu → setState(Stopping) → proc.stopGraceful() → setState(Stopped)
  3. 同時 watchServerV2 第 3 次失敗 → handleWatchFailure → setState(Error)
  4. 競態結果state 可能是 Error覆蓋 Stopped即使使用者剛剛主動 Stop

修法handleWatchFailure 進場先取 txMu並在 setState 前重新檢查 state已是 Stopped/Stopping/Idle 就 return。或更穩watchServerV2 在 ticker 失敗時也檢查 c.State() != ServerStateRunning 直接退出 goroutine。

Minor 4C.MIN-1Restart() 拆成獨立的 Stop()StartWithPort() 兩段 txMu 持有,中間有窗口期讓另一個 binding 呼叫插隊。實務上前端按鈕在 Stopping 時 disable 應該擋住,但仍是潛在 race。建議 Restart() 自己持 txMu 整段(或加 restartInternal 不再呼叫 Stop / StartWithPort而是內部 inline 邏輯)。

Minor 5C.MIN-2stopGraceful 走完 done 路徑後立刻 closeLogFiles(),但 logPump goroutine 還在處理 lineCh 中 buffered 的最後幾行(會寫 fileWriter。pipe EOF 與 stopGraceful 的時序可能有極窄的 racerace detector 沒抓到是因為 unit test 沒實際 spawn process建議stopGraceful 等待 logPump 結束(加 done channel再 close file或讓 logPump 持有 file ownership 自己 defer close。


D. Log Pump goroutine

檢查項 結論 備註
每個 pipe 一個 goroutine go a.logPump(stdoutPipe, ...) × 2L491-492
10 ms micro-batch time.NewTicker(10 * time.Millisecond)
Rate limit fallback超量只寫檔不 emit ⚠️ 偏差 見下方 D.MIN-1
pipe EOF 後 goroutine 自行退出 scanner.Scan() 結束 → close(scanDone) → main loop 收到 → flush + return
lineCh 沒被 close ⚠️ Minor 由 GC 處理scanDone 訊號替代

D.MIN-1rate limit 計數單位)TDD §4.5 + Reviewer prompt 描述「1 秒 200 行」,但 logPump 是每個 batch 呼叫一次 ShouldEmit()server_control.go:572),不是每行。因為 batch 大小 1~數十不等,實際 burst 容許行數可能達 200 × 平均 batch size。

不算 bug限流仍能擋住災難級瀑布單元測試 TestLogBuffer_RateLimitline 108-127的假設與真實使用方式不符 — test 模擬「每行一次 ShouldEmit」實際 logPump 是「每 flush 一次 ShouldEmit」。

建議:要嘛把 ShouldEmit(n int) 改成接行數參數一次扣 n要嘛在 logPump flush 時 for i := 0; i < len(batch); i++ { if !ShouldEmit() { drop } }不阻擋本 milestone

D.MAJ-1(雖然影響不大,但屬於資料丟失 → 標 MajorlogPump 在 server 子程序剛 exit 時可能丟掉最後幾行 stderr崩潰原因

檔案/行server_control.go:579-608

原因select 同時看 lineCh 與 scanDone。當 scanner goroutine 結束 → close(scanDone)lineCh buffer 內可能仍有最多 128 行未處理。Go select 在多個 ready case 隨機選擇,可能直接走 <-scanDone flush 後 return把 lineCh 中剩餘的行全數丟棄,包括寫檔與 ring buffer。

後果server crash 時最後幾行 stderr最有價值的 stack trace有機率丟。

修法scanner goroutine 結束時 close(lineCh) 而不是 close(scanDone)main loop 用 case line, ok := <-lineCh: if !ok { return } 作為唯一退出條件。或更簡單scanDone 收到後先 drain lineCh 再 return

case <-scanDone:
    for {
        select {
        case line := <-lineCh:
            // 處理寫檔 + ring buffer + batch
        default:
            flush(); return
        }
    }

Minor 6D.MIN-2scanner goroutine 內 select { case lineCh <- text: default: lineCh <- text } 寫法奇怪 — default 之後又 blocking send 同一個值,等同沒 default。建議直接 lineCh <- text 一行。


E. OnBeforeClose handlermain.go

檢查項 結論 備註
呼叫關閉時觸發 graceful shutdown return false → Wails OnShutdown → app.shutdown → ctrl.Stop
return false 允許關閉 L34
不彈 confirm 對話框 無對話框邏輯,符合 R5-2

潛在 UX 風險(非 bugWails 視窗一旦開始 close前端 JS 是否還能渲染 shutdown:modal-show 是不確定的(不同 OS 行為不同。M8-5 控制台 UI 改寫時應實機驗證;現階段不阻擋。


F. Notify 三平台

檔案 build tag 機制 結論
notify_darwin.go //go:build darwin osascript display notification + escape
notify_linux.go //go:build linux notify-send -u critical -i dialog-error
notify_windows.go //go:build windows PowerShell BurntToast + msg * fallback
檢查項 結論 備註
三平台 build tag 齊全 沒重複定義 / 沒缺失
AppleScript escape 反斜線 + 雙引號
PowerShell escape 單引號 → ''
fire-and-forget 呼叫 呼叫端用 go sendCrashNotification(...)
失敗只 log 不 fail 三平台都 fmt.Fprintf stderr

Minor 7F.MIN-1:三平台都用 cmd.Run() 同步等子程序結束。雖然呼叫端用 go wrapper但 goroutine 內若 osascript / powershell hang 住會 leak極罕見建議:包一個 context.WithTimeout(5*time.Second) + exec.CommandContext,過期 Kill。本 milestone 可不修。

Minor 8F.MIN-2notify_linux.go 假設 notify-send 一定在 PATH 上。雖然 prompt 提到「若不存在 log warning 不 fail」實作確實不會 crash exec.Command.Run() 在 binary not found 時回的是 *exec.Error,輸出的 warning 對使用者不夠友善(純 path not found。可接受不修。


G. Boot-ID + SkipPaths

檢查項 結論 備註
crypto/rand.Read + hex.EncodeToString system_handler.go:27-32
32 字元 hex smoke test 實測 bd94ae7f976a6e526bd9840fc385b92d
process-scoped每次啟動新 ID NewSystemHandler 內呼叫 newBootID()
GET /api/system/boot-id 註冊 router.go:59
Response shape {success, data: {bootId, startedAt}}
broadcasterLoggerSkipPaths 含 boot-id + health router.go:120-123
skip 邏輯只比對 path router.go:140-142
Smoke test兩個 endpoint 不寫 access log 實測 stdout 只有 [GIN] 200 ... GET /api/devices沒有 boot-id 或 health 的 GIN log

Minor 9G.MIN-1boot-id 產生位置偏離 TDD §9.1。TDD 寫的是「server/main.go 在啟動時產生 boot-id傳入 SystemHandler」實作把 newBootID() 放在 NewSystemHandler 內部呼叫(system_handler.go:42)。功能完全等價,但讓單元測試難以注入固定 boot-id 做 deterministic test。Minor 偏離規格,可接受。

Major 3G.MAJ-1TDD §8.1 + milestone-plan 要求 server/main.goshutdownFn timeout 從 10 秒改成 6 秒,未改

檔案/行server/main.go:166

ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)

應改為 6*time.Second

後果Wails ctrl.Stop 在第 7 秒會 SIGKILL但 server 自己內部還在 10 秒 graceful cleanup window 中 — 可能出現「server 還在 sync 檔案到一半被打斷」。對 v2 「7 + 1 秒 modal」UX 設計直接破壞。


H. v1/v2 並存策略評估

實作保留了 ServerProcess.stop() / kill() / watchServer v1 路徑(app.go:583-685+ v1 shutdownGracePeriod = 5s 常數宣稱為「fallback」 + 「M8-5 前端改寫期間相容」。

評估結論⚠️ 策略合理但需立即標記砍除時程

問題 風險 結論
v1/v2 路徑會被同時呼叫嗎? 不會 startup 只走 ctrl.Start → startServerV2shutdown 只走 ctrl.Stop → stopGraceful。app.stopServer()App.startServer 已被 dead code
v1 常數 shutdownGracePeriod = 5s 會被誤用嗎? 只有 v1 ServerProcess.stop() 用到,已是 dead path
v1 watchServer 會被誤啟動嗎? 只在 v1 startServer 的 dead path 啟動;新 startServerV2 走 watchServerV2
reportFatal v1 路徑仍存在 startup 失敗 / single-instance 失敗仍會走這合理startup 完全失敗時控制台還沒就緒,無 fallback 只能原生對話框)
未來何時砍 未明確 必須在 progress.md「未解決問題」記錄M8-5 完成後立即砍全部 v1ServerProcess.stop/killwatchServerstopServershutdownGracePeriodstartServer

Minor 10H.MIN-1shutdown()app.go:222的 fallback 路徑 a.stopServer() 是 dead code因為 ctrl 在 startup 一定會被建立 — 直接刪 else 分支,避免讓未來的開發者誤以為兩條路徑都有效。

Minor 11H.MIN-2v1 watchServerv2 watchServerV2 內容 90% 相同,只差最後失敗的 reportFatal vs handleWatchFailure。等 M8-5 砍 v1 即可消除這份重複。


I. Build / Test / Race detector 結果

cd server && go build ./...                  ✅ PASS
cd server && go vet ./...                    ✅ PASS
cd server && go test ./...                   ✅ PASSdevice + model 既有 test
cd server && go test -race ./...             ✅ PASS

cd visiona-local && go build .               ✅ PASS
cd visiona-local && go vet ./...             ✅ PASS
cd visiona-local && go test -count=1 ./...   ✅ PASS20 tests 全綠)
cd visiona-local && go test -race ./...      ✅ PASS

20 個 unit test 明細

  • log_buffer_test.go5 個Append / Wrap / Reset / RateLimit / parseLogLevel
  • preferences_test.go5 個platform default / missing / corrupt / roundtrip / atomic rename
  • server_control_test.go10 個Initial / setState / Stop×2 / ForceKill / snapshotStatus / GetRecentLogs / GetSystemInfo / ClearLogs / GetServerStatusV2

race detector 沒抓到任何 race,但要注意 unit test 沒有實際 spawn server binary,因此 logPump↔stopGraceful 的 file handle raceC.MIN-2 / D.MAJ-1 / Major 1必須靠人工 review 與 integration test 才能驗證。


J. Smoke test 結果

執行:go run server 啟動真實 servercurl 各 endpoint。

步驟 結果
1. curl /api/system/boot-id {"data":{"bootId":"bd94ae7f976a6e526bd9840fc385b92d","startedAt":1776213642971},"success":true}32 字元 hex
2. curl /api/system/health {"status":"ok"}
3. curl /api/devices 正常回應
4. stdout 內 GIN access log 只有 [GIN] 200 | 137.674µs | GET /api/devices沒有 boot-id / health 的 access log
5. 連續 4 次 poll boot-id 都回同一 IDprocess-scoped

SkipPaths 機制完全符合 TDD §9.1a 與 milestone-plan 預期。


K. 遺漏 / 誤解

Major 4K.MAJ-1未實作 server:shutdown-imminent WebSocket 廣播milestone-plan #157「Minor 4」明確要求

檔案/行server_control.go:302-340stopGraceful 內應在 SIGTERM 前先廣播server 端應有 /ws/system 或擴充 /ws/server-logs 來廣播 server:shutdown-imminent

影響M8-9 的 web UI offline overlay race condition 防護機制完全缺席。M8-9 將會被迫補做、或 fall back 到「3 次 poll 失敗 = 15s 才顯示 overlay」的舊行為。

狀態阻擋 M8-9建議在 M8-4b 一併補做或拆獨立小 task

Minor 12K.MIN-1milestone-plan #153 要求「shutdownGracePeriod 5 s → 7 s」,實作改用新常數 shutdownGraceV2 = 7s、舊常數 5s 還在。功能等效但字面違反 milestone-plan。可接受作為 v1/v2 並存策略的副作用,但要在 H 段提到的「砍 v1」清單中一併處理。

Minor 13K.MIN-2ForceKillServer 確認發崩潰通知 — 走 proc.forceKill() 路徑直接 SIGKILL不經過 handleWatchFailure符合 prompt 要求。

Minor 14K.MIN-3preferences 讀取失敗的 fallback 行為符合 TDD §11不 fail、用 default

Minor 15K.MIN-4notify 三平台 timeout 處理F.MIN-1— 已在 F 段提,不重複列。


L. 問題清單

Major阻擋後續 milestone

# 檔案:行 問題 影響的 milestone 修法
MAJ-1 server_control.go:198-229 (Stop), 251-265 (ForceKill) Stop() / ForceKill() 不 cancel watchCancel30 秒後誤觸 Error state + 發 OS 崩潰通知 M8-5、M8-7 Stop/ForceKill 進場後加 if a.watchCancel != nil { a.watchCancel(); a.watchCancel = nil }
MAJ-2 server_control.go:269-291 handleWatchFailure 不取 txMu與 Stop/ForceKill race可能把 Stopped 翻成 Error M8-5 進場取 txMusetState 前重新檢查 state或 watchServerV2 ticker 內檢查 c.State() != ServerStateRunning 直接退出
MAJ-3 server/main.go:166 shutdownFn timeout 仍是 10s未對齊 TDD §8.1 + milestone-plan 的 6s M8-5、M8-9 6*time.Second
MAJ-4 server_control.go:302-340 + 缺 server WebSocket 廣播 未實作 server:shutdown-imminent 廣播milestone-plan Minor 4 M8-9 server 端加 /ws/system 或擴充 /ws/server-logsctrl.Stop SIGTERM 前 HTTP/WS 觸發
MAJ-5 server_control.go:579-608 (logPump main loop) scanDone 觸發後 lineCh 仍有 buffered 行未處理 → 資料丟失(含崩潰時最後幾行 stderr M8-5 (debug experience) scanDone case 加 drain loop 或讓 scanner goroutine 自己 close lineCh

Minor建議修不阻擋

# 檔案 問題
MIN-1 log_buffer.go:99-105 Snapshot 從 i=0 走 size 圈,可改為從 (start+skip) 走 n 圈
MIN-2 log_buffer.go:139-151 ShouldEmit CAS raceA 視窗 Store(0) 前 B 已 Add(1),極小機率多放 1 行
MIN-3 preferences.go:91 跨 filesystem rename 失敗時無 io.Copy fallback罕見邊界
MIN-4 server_control.go:232-245 Restart 拆兩段 txMu中間有插隊窗口
MIN-5 server_control.go:302-340 stopGraceful 結束時關 file 與 logPump goroutine 還在寫的時序 race
MIN-6 server_control.go:556-562 scanner goroutine select { case lineCh<-text: default: lineCh<-text } 寫法奇怪、等同 blocking
MIN-7 notify_*.go 三平台都用 cmd.Run() 無 timeout極罕見情況下 hang 會 leak goroutine
MIN-8 notify_linux.go notify-send not in PATH 時 warning 不夠友善
MIN-9 system_handler.go:42 newBootID 放在 handler 內呼叫,偏離 TDD §9.1 「main.go 注入」設計,不利於 deterministic test
MIN-10 app.go:222-227 shutdown() 的 v1 fallback 是 dead code建議刪
MIN-11 app.go:583-622 watchServer v1 與 watchServerV2 高度重複,待 M8-5 後砍
MIN-12 app.go:46 shutdownGracePeriod = 5s 仍在,違反 milestone-plan #153 字面
MIN-13 log_buffer_test.go:108-127 TestLogBuffer_RateLimit 假設「每行一次 ShouldEmit」與 logPump 實際「每 batch 一次」不一致

M. 結論

M8-4 的核心架構正確且品質高

做得好的地方(值得讚許):

  • 雙鎖機制 txMu + mu 設計清晰、race detector 全綠
  • LogBuffer 的 ring buffer + atomic rate limit 結構漂亮
  • Preferences atomic write-rename 含 tmp cleanup
  • 三平台 notify 用 build tag 隔離乾淨、escape 處理到位
  • boot-id 用 crypto/rand 不引 google/uuid符合 TDD 定版決策
  • broadcasterLogger SkipPaths 實測完美過 smoke test
  • 20 unit test 全綠、race detector 全綠
  • v1/v2 並存策略雖然增加維護成本,但邏輯上沒有衝突
  • 文件註解極詳細,每個函式都標 TDD 出處

必須處理的問題(阻擋後續 milestone

  • 5 個 Major — MAJ-1 ~ MAJ-5。其中 MAJ-1watch leak 誤觸 Error與 MAJ-2handleWatchFailure race是直接破壞 M8-5 UX 的 bug第二輪修改前必須處理。MAJ-3shutdownFn 6s與 MAJ-4shutdown-imminent 廣播是純遺漏補就好。MAJ-5logPump 丟最後幾行)影響 debug 體驗。
  • 15 個 Minor — 大多可以放到 v1 砍除時一併處理,或當作技術債記錄。

對 Orchestrator 的建議:

  1. 第二輪修改範圍(必須):

    • MAJ-1Stop/ForceKill cancel watchCancel
    • MAJ-2handleWatchFailure 取 txMu + 二次檢查 state
    • MAJ-3server/main.go shutdownFn 6s
    • MAJ-5logPump scanDone 加 drain loop
  2. MAJ-4 處理時機M8-4b 一併做,因為它需要 server 端新增 /ws/system endpoint與 startup-pipeline 的 event 設計可一起規劃。或拆獨立 micro-task 立刻補做,不要等到 M8-9 才發現缺

  3. v1 砍除追蹤:在 progress.md「未解決問題」加一條「M8-4 v1 路徑ServerProcess.stop/kill、watchServer、stopServer、shutdownGracePeriod、startServer保留作為相容過渡M8-5 完成後立即砍除(含相關 dead code 清理 MIN-10/11/12」。

  4. 整合測試M8-4 的 unit test 沒實際 spawn server binary因此 logPump file handle raceMIN-5、stopGraceful timing、preferences 跨檔案系統等只能靠 manual / integration test 驗證。建議在 M8-5 完成後安排一次完整 integration smoke testStart → 噴 log → Stop → Restart → Force Kill → 模擬 crash → 各事件都要正確)。

結論⚠️ 需小改後通過。修完 5 個 Major 即可進 M8-4b / M8-5。Minor 不阻擋。