從 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>
115 lines
2.8 KiB
Go
115 lines
2.8 KiB
Go
// Package wsconn 將 gorilla/websocket.Conn 包裝成 net.Conn,
|
||
// 讓 hashicorp/yamux 之類的 stream multiplexer 能在 WebSocket 之上運作。
|
||
//
|
||
// 此 package 於 2026-04-22 從 POC (edge-ai-platform) 複製:
|
||
//
|
||
// Source: edge-ai-platform/server/pkg/wsconn/wsconn.go
|
||
// Baseline commit: c9d56a62e23bc45554391123152ca90a07a60bdc
|
||
//
|
||
// 為什麼存兩份 wsconn(visiona-agent/internal/wsconn + server/pkg/wsconn):
|
||
//
|
||
// local-agent/server/ 與 local-agent/visiona-agent/ 是兩個獨立 Go module。
|
||
// server binary 內的 wsconn 由未來的 server 端 tunnel 使用(目前未使用),
|
||
// visiona-agent Wails shell 的 tunnel client 不能跨 module import,所以
|
||
// 複製一份。兩份程式內容相同;未來若 tunnel 改放 server module 可合併。
|
||
//
|
||
// 參考:
|
||
//
|
||
// .autoflow/04-architecture/adr/adr-008-tunnel-client-reuse.md (v2)
|
||
package wsconn
|
||
|
||
import (
|
||
"io"
|
||
"net"
|
||
"sync"
|
||
"time"
|
||
|
||
"github.com/gorilla/websocket"
|
||
)
|
||
|
||
// Conn 將 *websocket.Conn 轉成 net.Conn 介面。
|
||
// 所有訊息以 Binary frame 送收。
|
||
type Conn struct {
|
||
ws *websocket.Conn
|
||
reader io.Reader
|
||
rmu sync.Mutex
|
||
wmu sync.Mutex
|
||
}
|
||
|
||
// New 把 WebSocket 連線包成 net.Conn。
|
||
func New(ws *websocket.Conn) *Conn {
|
||
return &Conn{ws: ws}
|
||
}
|
||
|
||
// Read 讀取下一批 WebSocket binary frame 資料到 p。
|
||
// 若目前 reader 已耗盡,會向底層 websocket 請求下一個 frame。
|
||
func (c *Conn) Read(p []byte) (int, error) {
|
||
c.rmu.Lock()
|
||
defer c.rmu.Unlock()
|
||
|
||
for {
|
||
if c.reader != nil {
|
||
n, err := c.reader.Read(p)
|
||
if err == io.EOF {
|
||
c.reader = nil
|
||
if n > 0 {
|
||
return n, nil
|
||
}
|
||
continue
|
||
}
|
||
return n, err
|
||
}
|
||
|
||
_, reader, err := c.ws.NextReader()
|
||
if err != nil {
|
||
return 0, err
|
||
}
|
||
c.reader = reader
|
||
}
|
||
}
|
||
|
||
// Write 將 p 以 BinaryMessage 送出。
|
||
func (c *Conn) Write(p []byte) (int, error) {
|
||
c.wmu.Lock()
|
||
defer c.wmu.Unlock()
|
||
|
||
err := c.ws.WriteMessage(websocket.BinaryMessage, p)
|
||
if err != nil {
|
||
return 0, err
|
||
}
|
||
return len(p), nil
|
||
}
|
||
|
||
// Close 關閉底層 WebSocket 連線。
|
||
func (c *Conn) Close() error {
|
||
return c.ws.Close()
|
||
}
|
||
|
||
// LocalAddr 回傳底層 WebSocket 連線的本地位址。
|
||
func (c *Conn) LocalAddr() net.Addr {
|
||
return c.ws.LocalAddr()
|
||
}
|
||
|
||
// RemoteAddr 回傳底層 WebSocket 連線的遠端位址。
|
||
func (c *Conn) RemoteAddr() net.Addr {
|
||
return c.ws.RemoteAddr()
|
||
}
|
||
|
||
// SetDeadline 同時設定讀寫超時。
|
||
func (c *Conn) SetDeadline(t time.Time) error {
|
||
if err := c.ws.SetReadDeadline(t); err != nil {
|
||
return err
|
||
}
|
||
return c.ws.SetWriteDeadline(t)
|
||
}
|
||
|
||
// SetReadDeadline 設定讀取超時。
|
||
func (c *Conn) SetReadDeadline(t time.Time) error {
|
||
return c.ws.SetReadDeadline(t)
|
||
}
|
||
|
||
// SetWriteDeadline 設定寫入超時。
|
||
func (c *Conn) SetWriteDeadline(t time.Time) error {
|
||
return c.ws.SetWriteDeadline(t)
|
||
}
|