從 local-tool 複製出獨立的「visionA Agent」桌面應用(A3 純橋樑: tunnel client + 配對 UI + 設定,不開 HTTP port、不做本機裝置/推論 UI)。 Bundle ID 與 local-tool 不同(com.innovedus.visiona-agent vs visiona-local), 雙 app 可共存。fork 後不主動 sync,需要時手動 cherry-pick。 Backend / Wails Go(AB1-AB13): - internal/tunnel:6 狀態機(Idle/Connecting/Connected/Reconnecting/Failed/Stopped) + Pair/Unpair/Reconnect/Disconnect binding + ClientHooks event - internal/auth:encrypted file token store(AES-GCM + scrypt + machineID fallback salt + 13 tests) - internal/config:YAML validation + atomic write + 11 tests - internal/log:ring buffer + ExportLog 升級 zip - visionA-backend /api/pairing/exchange:SessionTokenStore + 17 new tests - 三平台 build 驗證(macOS DMG 160 MB / Windows EXE / Linux AppImage) - end-to-end 5 milestone 全綠(pairing → tunnel → forward → reuse 防護 → tunnel drop failover) Frontend / Next.js(AF1-AF7,沿用 visionA-frontend 基礎): - AppShell + Header + TabNav(StatusView / PairView / SettingsView 三 tab) - ConnectionStatusBadge 5 種狀態 - TokenInput regex 驗證 + 7 種錯誤 + 0.5s auto-switch 到狀態頁 - 設定頁 4 區塊(含重新配對 AlertDialog) - agent-api.ts 封裝 Wails bindings(mock/real 雙實作)+ 90 tests Phase 0.7 review-driven fix(Round 2): - A1 Session fixation 防護(RotateSessionID) - A3 mock pairing 預設改 false(必須明確 opt-in)+ startup log - A4 Pair 失敗後 state 清理矩陣(exchange/Save/Start fail 各自終態) - A5 Pair/Unpair/Reconnect lifecycleMu + 50 goroutine race test - F1 重新配對次按鈕 / F2 PairView Esc cancel / F3 Wails BrowserOpenURL / F4 Settings draft 持久 + 未儲存 badge 驗證:agent backend go test -race -count=3 ./... 4 packages 全綠 / agent frontend pnpm test 119 tests 全綠 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
59 lines
2.1 KiB
Go
59 lines
2.1 KiB
Go
package main
|
||
|
||
// shutdown_notify.go — MAJ-4 補丁
|
||
//
|
||
// 對應 TDD v2/server-lifecycle.md §2.3(Minor 4)與 v2/web-ui-offline-overlay.md §3.2a。
|
||
//
|
||
// 在 Wails OnBeforeClose / Restart 實際對 server 下 SIGTERM 之前,先打一個 POST 到
|
||
// server 的 /api/system/shutdown-notify,讓 server 透過 /ws/system WebSocket 廣播
|
||
// server:shutdown-imminent 給所有已連線的瀏覽器 tab,觸發 Offline Overlay 立即顯示。
|
||
//
|
||
// 設計原則:best-effort,不阻塞 shutdown 流程。
|
||
// - 1 秒 timeout:server 沒起來、卡住、network error 都視為失敗,不重試
|
||
// - 失敗完全忽略(log 也不寫,避免 shutdown 期間噴 stderr)
|
||
// - port <= 0 時直接 return(server 根本沒起來)
|
||
|
||
import (
|
||
"context"
|
||
"fmt"
|
||
"net/http"
|
||
"time"
|
||
)
|
||
|
||
// notifyShutdownImminentTimeout 控制 HTTP request 的總 timeout。
|
||
// server 端 ShutdownNotify handler 會 sleep 100 ms 後才回,留 900 ms buffer 給網路與 scheduler。
|
||
// 變數化以便測試注入。
|
||
var notifyShutdownImminentTimeout = 1 * time.Second
|
||
|
||
// notifyShutdownImminent 通知 server 廣播 shutdown-imminent 給所有 WebSocket client。
|
||
//
|
||
// 參數:
|
||
//
|
||
// ctx — caller context(會被 timeout 包裹,避免 ctx 卡住整個 shutdown)
|
||
// port — server 正在聽的 port;<= 0 代表 server 還沒起來,直接 skip
|
||
// reason — "quit" 或 "restart",對應 Offline Overlay 的 reason 映射
|
||
//
|
||
// 錯誤容忍:全部錯誤靜默處理,絕不阻塞呼叫端。
|
||
func notifyShutdownImminent(ctx context.Context, port int, reason string) {
|
||
if port <= 0 {
|
||
return
|
||
}
|
||
if ctx == nil {
|
||
ctx = context.Background()
|
||
}
|
||
reqCtx, cancel := context.WithTimeout(ctx, notifyShutdownImminentTimeout)
|
||
defer cancel()
|
||
|
||
url := fmt.Sprintf("http://127.0.0.1:%d/api/system/shutdown-notify?reason=%s", port, reason)
|
||
req, err := http.NewRequestWithContext(reqCtx, http.MethodPost, url, nil)
|
||
if err != nil {
|
||
return
|
||
}
|
||
resp, err := http.DefaultClient.Do(req)
|
||
if err != nil {
|
||
return
|
||
}
|
||
// 不需讀 body,關閉即可(釋放底層連線)
|
||
_ = resp.Body.Close()
|
||
}
|