jim800121chen fb7da5d180 chore(autoflow): migrate .autoflow/ 共享層文件至 docs/autoflow/
依 autoflow-agent workspace v2 設計把 PRD / 設計 / 架構 / 交付類
共享文件從個人層 .autoflow/(ignored)搬到 docs/autoflow/(進 git),
讓團隊可共享產品與架構文件,個人層只留 progress / review / testing 等
per-branch 筆記。

- 02-prd/        21 個檔(PRD、features、market-analysis 等)
- 03-design/     18 個檔(design-spec、wireframes、flows 等)
- 04-architecture/ 31 個檔(TDD、design-doc、ADR×14、API 規格等)
- 07-delivery/   3 個檔(project-summary、phase-0.6-handover、stage-deployment-setup)

合計 73 檔。原檔已從 .autoflow/ 移除(migration 工具執行 git mv,
但因 .autoflow/ 在 .gitignore 中、git 將此操作視為新增、無 rename history)。
2026-05-04 16:55:55 +08:00

32 KiB
Raw Permalink Blame History

Technical Design Document — visionA Cloud

Metadata

  • 作者Architect Agent
  • 狀態Draft待三方交叉審閱
  • 最後更新2026-04-22反映三方交叉審閱 M-1M-8 + Minor-2Minor-4
  • 文件角色:給工程師實作時的技術契約(不是 Test-Driven Development
  • 上位文件design-doc.mdadr/adr-*.md
  • 讀者Frontend / Backend / DevOps / Testing Agents

雛形關鍵決策2026-04-22 使用者裁決)

  • 雛形即雙 binarycmd/api-server + cmd/remote-proxy,不做單進程 all-in-one
  • 不引入 RedisPOC 也從未用過;查證 go.mod / source / docker 皆無)
    • Session state 由 remote-proxy 完全持有in-memory
    • api-server 無狀態,透過 internal HTTPremote-proxy 查 session / 轉發請求
    • /internal/session/:token/internal/forward/* 在 Phase 0 雛形就必須實作(不是 Phase 1
  • Local agent 雛形不做tunnel 測試端暫用 POC edge-ai-server 當 tunnel client
  • POC 程式碼複製internal/relay / internal/tunnel / internal/wsconnedge-ai-platform 複製後獨立演進
  • Auth 雛形StaticAuthService 永遠回 demo-user 單一使用者已替換為 OIDCPhase 0.6 / OB5接 Innovedus Member Center詳見 oidc-tdd.md + adr-011
  • Session 覆蓋:沿用 POC — 同 token 後連覆蓋前連Phase 1 再重新設計
  • Pairing Token 顯示API 回傳純 hex前端顯示層加空格
  • 三個 local-tool-only 元件雛形隱藏OnboardingDialogServerStatusDashboardServerLogViewer

索引(本文件為總覽 + 連結)

1. 專案骨架

  • 見 §1 專案骨架(本文件)

2. Backend 模組詳細

  • 見 §2 Backend 模組詳細(本文件)

3. API 規格

4. 資料模型

  • Go struct 定義 + Phase 1 DB schema 對應 → database.md

5. Pairing Token 協定

  • 詳細流程 + 雛形 vs Phase 1 差異 → security.md

6. Tunnel 協定

  • 詳細訊息格式、重連策略 → tunnel.md

7. Session 管理

  • SessionStore interface + 實作策略 → tunnel.md

8. 儲存層介面

9. Converter 整合 API 契約

10. 前端資料流與狀態管理

  • 見 §10本文件

11. 建置與部署

12. 安全考量

13. 測試策略

  • 見 §13本文件

14. TODO 清單

  • 見 §14本文件

§1 專案骨架

1.1 Repo 結構Monorepo

visionA/                              # 既有 repo 根目錄
├── local-tool/                       # 既有,不動
├── visionA-frontend/                 # ⬅ 新增
│   ├── src/
│   │   ├── app/                      # Next.js App Router
│   │   ├── components/
│   │   ├── stores/                   # Zustand
│   │   ├── hooks/
│   │   ├── lib/
│   │   │   ├── api.ts                # 改造base URL = 雲端 API server
│   │   │   ├── auth.ts               # stub未來對接真實 auth
│   │   │   └── ws.ts                 # WebSocket 連接 util
│   │   ├── types/
│   │   └── i18n/
│   ├── public/
│   ├── package.json
│   ├── next.config.ts
│   ├── tsconfig.json
│   ├── tailwind.config.ts
│   └── .env.example                  # VISIONA_* / NEXT_PUBLIC_*
│
├── visionA-backend/                  # ⬅ 新增
│   ├── go.mod                        # module "visiona-backend"
│   ├── go.sum
│   ├── Makefile
│   ├── cmd/
│   │   ├── api-server/
│   │   │   └── main.go               # REST + WebSocket API無狀態
│   │   └── remote-proxy/
│   │       └── main.go               # Tunnel server + internal HTTP API有狀態
│   │   # 註:雛形交付物就是這兩個 binary + docker-compose。
│   │   # 本機開發可用 `make run-dev` 平行跑(純便利工具,非交付物)。
│   │   # 不做 `cmd/dev-all-in-one`Q1 裁決 C + design-doc.md §1.9 N10
│   ├── internal/
│   │   ├── api/                      # API handlers給前端
│   │   │   ├── handlers/
│   │   │   ├── middleware/
│   │   │   ├── ws/
│   │   │   └── router.go
│   │   ├── auth/                     # Auth + Pairing Token
│   │   │   ├── service.go            # AuthService interface
│   │   │   ├── static.go             # StaticAuthService
│   │   │   ├── pairing.go            # PairingStore interface
│   │   │   └── static_pairing.go     # StaticPairingStore
│   │   ├── session/                  # Tunnel session 管理
│   │   │   ├── store.go              # SessionStore interface
│   │   │   ├── inmemory.go
│   │   │   └── types.go
│   │   ├── device/                   # Device domain
│   │   │   ├── types.go
│   │   │   └── repository.go         # interface + InMemory 實作
│   │   ├── model/
│   │   │   ├── types.go
│   │   │   └── repository.go
│   │   ├── cluster/                  # 從 POC 複製Q2 決策,後續獨立演進)
│   │   ├── relay/                    # 從 POC 複製(同上)
│   │   ├── tunnel/                   # 從 POC 複製;雛形給測試/未來 agent 用
│   │   ├── converter/                # Converter client雛形 stub
│   │   │   ├── client.go             # interface
│   │   │   └── stub.go
│   │   ├── storage/                  # S3-compat
│   │   │   ├── store.go              # Store interface
│   │   │   ├── localfs.go
│   │   │   └── s3.go                 # 可選,雛形後期加
│   │   ├── wsconn/                   # yamux over WebSocket從 POC 複製)
│   │   ├── config/                   # env config
│   │   └── logger/
│   ├── pkg/                          # 對外可重用
│   │   └── protocol/                 # 共用的 typesrequest/response schema
│   ├── docker/
│   │   ├── Dockerfile.api-server
│   │   ├── Dockerfile.remote-proxy
│   │   └── docker-compose.yml
│   ├── scripts/
│   │   └── dev.sh
│   └── .env.example
│
├── docs/                             # 共用文件(選配)
├── .autoflow/                        # 文件管理(本文件所在)
└── README.md

1.2 Go module

  • module visiona-backend(簡短、不含 github 路徑,避免未來搬 repo 要改)
  • Go 1.26(與 POC 一致)
  • 主要依賴:
github.com/gin-gonic/gin
github.com/gorilla/websocket
github.com/hashicorp/yamux
github.com/go-playground/validator/v10
github.com/google/uuid
github.com/aws/aws-sdk-go-v2  (可選)

1.3 Frontend Node

  • Node >= 20
  • npm / pnpm沿用 local-tool 偏好)
  • 主要依賴沿用 local-toolNext.js 16、React 19、TypeScript 5、Tailwind 4、Radix UI、Zustand 5、Lucide、Recharts、driver.js、Vitest + RTL

§2 Backend 模組詳細

2.1 internal/apiapi-server 用)

檔案 職責
router.go Gin router 組裝
middleware/auth.go 從 request 解析身分並注入 UserContextgin.Context
middleware/cors.go CORS沿用 local-tool
middleware/logger.go broadcaster logger沿用 local-tool
handlers/device.go /api/devices/* — 大多轉發到 local agent
handlers/model.go /api/models/* — 雲端存模型 + 轉發到 local agent 載入
handlers/cluster.go /api/clusters/* — 從 POC 搬
handlers/inference.go /api/devices/:id/inference/*
handlers/pairing.go /api/pairing/*
handlers/auth.go /api/auth/*(雛形 501
handlers/converter.go /api/converter/*(雛形 stub
handlers/storage.go /storage/*(雛形 LocalFS 代理)
ws/devices.go, ws/inference.go WebSocket upgrade + 轉發到 tunnel
forward.go 核心:把瀏覽器請求透過 tunnel 送到 local agent參考 POC handleProxy

2.2 internal/auth2026-04-22 M-3 修訂:介面雙層 AuthService + AuthProvider

Auth 切分為兩層 interface對應不同呼叫場景詳見 security.md §2.0

介面 層級 檔案
AuthService Middleware 層(每個 request 進來時解析身分) service.go
AuthProvider Handler 層(登入 / 註冊 / 登出 / token 驗證) provider.go
// service.gomiddleware 層 — 既有)
package auth

type AuthService interface {
    Authenticate(ctx context.Context, req *http.Request) (*UserContext, error)
    Authorize(ctx context.Context, userCtx *UserContext, resource, action string) error
}

type UserContext struct {
    UserID string
    Email  string
    Roles  []string
    OrgID  string
}

// static.go — 雛形(⚠️ OB5 已移除;以下程式碼僅作歷史紀錄)
// 取代為 OIDC + cookie session見 oidc-tdd.md §4
type StaticAuthService struct {
    DefaultUser UserContext
}
func (s *StaticAuthService) Authenticate(...) { return &s.DefaultUser, nil }
func (s *StaticAuthService) Authorize(...) { return nil }


// provider.gohandler 層 — 2026-04-22 新增)
package auth

import "time"

type AuthProvider interface {
    Register(ctx context.Context, req *RegisterRequest) (*User, error)
    Login(ctx context.Context, req *LoginRequest) (*LoginResult, error)
    Logout(ctx context.Context, token string) error
    ValidateToken(ctx context.Context, token string) (*UserContext, error)
    GetUser(ctx context.Context, userID string) (*User, error)
}

type LoginRequest    struct { Email, Password string }
type RegisterRequest struct { Email, Password, Name string }
type LoginResult struct {
    User         *User
    AccessToken  string
    RefreshToken string
    ExpiresAt    time.Time
}


// static_provider.go — 雛形(⚠️ OB5 已移除;以下程式碼僅作歷史紀錄)
// 取代為 OIDC + cookie session見 oidc-tdd.md §4
type StaticAuthProvider struct {
    DemoUser *User    // 預設 demo-userUser struct 見 database.md §2.1
}

func (p *StaticAuthProvider) Login(ctx context.Context, req *LoginRequest) (*LoginResult, error) {
    // 無論 email / password 是什麼都通過
    return &LoginResult{
        User:         p.DemoUser,
        AccessToken:  "demo-access-token",
        RefreshToken: "demo-refresh-token",
        ExpiresAt:    time.Now().Add(24 * time.Hour),
    }, nil
}
func (p *StaticAuthProvider) Register(ctx, req) (*User, error) { return nil, ErrNotImplemented }
func (p *StaticAuthProvider) Logout(ctx, token) error          { return nil }
func (p *StaticAuthProvider) ValidateToken(ctx, token) (*UserContext, error) {
    if token == "demo-access-token" {
        return &UserContext{UserID: p.DemoUser.ID, Email: p.DemoUser.Email}, nil
    }
    return nil, ErrInvalidToken
}
func (p *StaticAuthProvider) GetUser(ctx, userID) (*User, error) {
    if userID == p.DemoUser.ID { return p.DemoUser, nil }
    return nil, ErrNotFound
}


// pairing.go不變
type PairingStore interface {
    Validate(ctx context.Context, token string) (*PairingInfo, error)
    MarkUsed(ctx context.Context, token string, deviceID string) error
    Create(ctx context.Context, userID string, ttl time.Duration) (plain string, info *PairingInfo, err error)
    Revoke(ctx context.Context, token string) error
    List(ctx context.Context, userID string) ([]*PairingInfo, error)
}

type PairingInfo struct {
    TokenHash string
    UserID    string
    DeviceID  string
    CreatedAt time.Time
    ExpiresAt *time.Time
    UsedAt    *time.Time
    RevokedAt *time.Time
}

// static_pairing.go — 雛形
type StaticPairingStore struct {
    Token  string   // 格式vAc_[0-9a-f]{32}(見 security.md §1.3
    UserID string
}
func (s *StaticPairingStore) Validate(ctx, token) (*PairingInfo, error) {
    if token != s.Token { return nil, ErrInvalidToken }
    return &PairingInfo{UserID: s.UserID, TokenHash: sha256hex(token)}, nil
}
func (s *StaticPairingStore) Create(...) (..., error) { return "", nil, ErrNotImplemented }

雛形實作

  • StaticAuthServicemiddleware永遠回 demo-userOB5 已移除AuthMiddleware 只走 OIDCcookie session → UserContext詳見 oidc-tdd.md
  • StaticAuthProviderhandlerLogin 任何帳密都通過OB5 已移除POST /api/auth/login 一律 410 Gone使用者改用 GET /api/auth/login redirect 到 Member Center
  • StaticPairingStore 對 env VISIONA_PAIRING_TOKEN 比對(格式必須是 vAc_ + 32 hex— 仍有效

已完成OB1-OB5OIDC 接 Innovedus Member Center 取代 StaticAuth。 未來擴展PostgresPairingStore、Redis-backed user session store取代 in-memory、Member Center webhook 接收 user 刪除/停用通知。


2.2.1 Pairing → Session Token 時序M-2

Phase 1 兩階段 token 完整流程見 security.md §1.2 與 adr/adr-003-pairing-token.md。關鍵原則:

  • Pairing TokenvAc_ + 32 hexWeb UI 產生15 min TTL一次性使用
  • Session TokenvAs_ + 64 hexagent 首次連 tunnel 時由 remote-proxy 換發90 天 TTL可撤銷
  • 換發發生在 WS /tunnel/connect 的 upgrade 階段(一次 DB transaction 完成 insert session + mark pairing used
  • local agent 從首個 yamux control frame 收到 Session Token 後持久化儲存,之後一律用 Session Token

雛形階段為簡化:只用 Pairing Token 單階段比對,無 TTL、無升級、無撤銷僅適合 dev

2.3 internal/session

參見 tunnel.md。核心 interface2026-04-22 Minor-4 新增 CleanupExpired

type SessionStore interface {
    Register(ctx context.Context, token string, sess SessionHandle) error
    Unregister(ctx context.Context, token string) error
    Lookup(ctx context.Context, token string) (SessionHandle, error)
    Exists(ctx context.Context, token string) (bool, error)
    List(ctx context.Context) ([]SessionSummary, error)
    Heartbeat(ctx context.Context, token string) error
    // 清除 LastSeenAt 超過 expireAfter 的 session。
    // 由 remote-proxy background goroutine 每 30s 呼叫。
    CleanupExpired(ctx context.Context, expireAfter time.Duration) (removed int, err error)
}

type SessionHandle interface {
    // remote-proxy 本地LocalHandle wrap *yamux.Session
    // api-server 遠端查詢RemoteHandle wrap internal HTTP client
    OpenStream(ctx context.Context) (net.Conn, error)
    Close() error
    IsClosed() bool
}

雛形部署(不用 Redis

  • remote-proxy 持有 唯一的 InMemoryStoresession 實體就在這個進程的記憶體)
  • api-server 不持有 session state要查 session 時,透過 internal HTTP 呼叫 remote-proxy
    • GET /internal/session/:token — 確認 session 是否在線
    • POST /internal/forward/http / GET /internal/forward/ws — 轉發請求
  • api-server 端,SessionStore 的實作是 ProxyClientStorewrap HTTP client呼叫 remote-proxy
  • remote-proxy 端,SessionStore 的實作是 InMemoryStore(真正持有 *yamux.Session

Phase 1 多節點時再評估:多個 remote-proxy 節點間如何共享 session metadata可能 Redis、可能 gossip、可能 service discovery。雛形不預設方案。參見 ADR-006。

2.4 internal/device, internal/model, internal/cluster

  • types.goGo structdatabase.md
  • repository.gointerface + InMemory 實作
  • 雛形不做 ORMInMemory 用 map + sync.RWMutex
  • Phase 1新增 postgres_repository.go 實作同 interface

2.5 internal/relay

  • 從 POC edge-ai-platform/server/internal/relay/server.go 搬過來
  • 修改:
    • sessions map[string]*yamux.Session 抽到 SessionStore
    • handleTunnel 先呼叫 PairingStore.Validate(token) 驗證後才接受
    • handleProxySessionStore.Lookup(token) 取 session而非 map 直接取)
  • 不修改的部分:
    • proxyWebSocket 的 Hijack 邏輯
    • isWebSocketUpgrade 判斷
    • MJPEG streaming 的 http.Flusher 處理

2.6 internal/tunnel

  • 從 POC edge-ai-platform/server/internal/tunnel/client.go
  • 在雛形中這個模組主要是給測試 / 未來 headless tunnel-only agent 用
  • local-tool 既有的雲端模式 opt-in 也會 import 這個 package未來
  • POC code 不大改;把 localAddr 等參數化

2.7 internal/converter

// client.go
type Client interface {
    SubmitConvert(ctx context.Context, req *ConvertRequest) (jobID string, err error)
    GetJob(ctx context.Context, jobID string) (*Job, error)
    ListJobs(ctx context.Context, userID string) ([]*Job, error)
    DownloadResult(ctx context.Context, jobID string) (io.ReadCloser, error)
}

// stub.go — 雛形
type StubClient struct{}
func (s *StubClient) SubmitConvert(...) (string, error) {
    return "stub-job-" + uuid.NewString(), nil
}
func (s *StubClient) GetJob(ctx, id) (*Job, error) {
    return &Job{ID: id, Status: "queued"}, nil
}

詳細 API 契約見 api/api-converter-contract.md

2.8 internal/storage

storage.md

2.9 internal/wsconn

  • 從 POC server/pkg/wsconn/wsconn.go 搬,無修改
  • 作為 net.Conn adapter讓 yamux 能在 WebSocket 上運作

2.10 internal/config

type Config struct {
    APIServer struct {
        Port int `env:"VISIONA_API_PORT" default:"3001"`
    }
    RemoteProxy struct {
        TunnelPort   int `env:"VISIONA_TUNNEL_PORT" default:"3800"`
        InternalPort int `env:"VISIONA_PROXY_INTERNAL_PORT" default:"3801"`
    }
    Auth struct {
        Mode string `env:"VISIONA_AUTH_MODE" default:"static"` // static / oidc / ...
        // static mode:
        StaticUserID string `env:"VISIONA_STATIC_USER_ID" default:"demo-user"`
    }
    Pairing struct {
        Mode  string `env:"VISIONA_PAIRING_MODE" default:"static"` // static / db
        Token string `env:"VISIONA_PAIRING_TOKEN"`  // for static mode
    }
    Session struct {
        // remote-proxy 端永遠是 "inmemory"(它擁有實體 yamux session
        // api-server 端永遠是 "proxy-client"(透過 internal HTTP 查 remote-proxy
        Backend string `env:"VISIONA_SESSION_BACKEND" default:"inmemory"` // inmemory / proxy-client
        // api-server 要知道 remote-proxy 的 internal HTTP 位址
        ProxyInternalURL string `env:"VISIONA_PROXY_INTERNAL_URL" default:"http://localhost:3801"`
    }
    Storage struct {
        Backend string `env:"VISIONA_STORAGE_BACKEND" default:"localfs"` // localfs / s3
        LocalFSRoot string `env:"VISIONA_STORAGE_LOCALFS_ROOT" default:"./data/storage"`
        LocalFSBaseURL string `env:"VISIONA_STORAGE_LOCALFS_BASE_URL" default:"http://localhost:3001/storage"`
        S3Endpoint string `env:"VISIONA_S3_ENDPOINT"`
        S3Bucket string `env:"VISIONA_S3_BUCKET"`
        S3Region string `env:"VISIONA_S3_REGION"`
        S3AccessKey string `env:"VISIONA_S3_ACCESS_KEY"`
        S3SecretKey string `env:"VISIONA_S3_SECRET_KEY"`
    }
    Converter struct {
        Mode string `env:"VISIONA_CONVERTER_MODE" default:"stub"` // stub / real
        URL  string `env:"VISIONA_CONVERTER_URL"`
    }
}

12-Factor全部走 env雛形 .env.example 提供範本。


§10 前端資料流與狀態管理

10.1 Zustand Stores

從 local-tool 搬來:model-storedevice-preferences-storeinference-storecamera-storeflash-storeactivity-storesettings-storetour-store

新增:

  • auth-storeuser, token, login(), logout()(雛形 stub
  • session-storepairingToken, tunnelStatus, reconnect()

10.2 API Client 改造(src/lib/api.ts

// 雛形base URL 從環境變數讀,不再 hardcode localhost:3721
const API_BASE = process.env.NEXT_PUBLIC_API_BASE ?? 'http://localhost:3001';

export async function apiFetch<T>(path: string, init?: RequestInit): Promise<T> {
  const token = useAuthStore.getState().token;
  const res = await fetch(`${API_BASE}${path}`, {
    ...init,
    headers: {
      'Content-Type': 'application/json',
      ...(token && { Authorization: `Bearer ${token}` }),
      ...init?.headers,
    },
    credentials: 'include', // for cookie-based auth future
  });
  if (!res.ok) throw new ApiError(res);
  return res.json();
}

移除local-tool 原本的 X-Relay-Token header 雲端模式鉤子雲端版不再需要authtoken 是 user tokenpairing 是 backend 內部事)。

10.3 WebSocket Hook 改造(src/hooks/use-websocket.ts

const WS_BASE = (process.env.NEXT_PUBLIC_API_BASE ?? 'http://localhost:3001')
  .replace(/^http/, 'ws');

export function useWebSocket(path: string) {
  // 連 wss://api.visiona.cloud/ws/xxx
  // Auth 透過 first-message 或 cookie 附帶
  // ...
}

10.4 Auth 流程 stub

// src/lib/auth.ts
export const authClient = {
  async login(email: string, password: string) {
    // 雛形:後端回 501前端捕獲顯示「即將推出」
    return apiFetch('/api/auth/login', { method: 'POST', body: JSON.stringify({email, password}) });
  },
  async register(...) { /* 501 */ },
  async logout() { useAuthStore.getState().clear(); },
};

10.5 Pairing 流程 UI

新頁面 /devices/pair

  1. 頁面顯示「請在本機 local-tool 輸入以下 Pairing Token」
  2. 從 env NEXT_PUBLIC_DEV_PAIRING_TOKEN(雛形)或 POST /api/pairing/tokenPhase 1取得 token 並顯示
  3. 右側顯示即時連線狀態(訂閱 GET /api/pairing/status 或 WS /ws/pairing/status

Token 顯示格式Q7 裁決)

  • API 回傳值永遠是純 hex / base64url(無空格、無分隔符)— 例:pk_a1b2c3d4e5f6g7h8...
  • 前端顯示層為了可讀性,每 8 個字元插入空格 — 例:pk_a1b2c3d4 e5f6g7h8 ...
  • 使用者複製時UI 提供「Copy」按鈕複製的是原始無空格 token
  • 使用者貼上時:前端允許任何空白字元,送出前正規化(.replace(/\s/g, '')
  • 這保證 API 契約乾淨,同時 UI 可讀性好

10.5.1 Device 雙狀態消費2026-04-22 Minor-3

Device struct 現在有兩組狀態欄位(見 database.md §2.2

interface Device {
  id: string;
  name: string;
  deviceType: string;

  // tunnel-level雲端觀察
  remoteStatus: 'online' | 'offline' | 'reconnecting' | 'error';
  lastSeenAt?: string;         // ISO 8601
  lastConnectedAt?: string;

  // USB-levellocal agent 上報)
  status: 'online' | 'offline' | 'unknown';

  // ...
}

前端顯示邏輯:

畫面位置 顯示依據
Device 卡片主狀態 badge remoteStatus(決定「雲端能不能操作它」)
Device 卡片副狀態 statusUSB 是否插著)— 僅在 remoteStatus === 'online' 時顯示有意義
「最後上線」時間 lastSeenAttunnel 最後心跳)
Inference / Flash 按鈕啟用條件 remoteStatus === 'online' && status === 'online'
「重連中」loading remoteStatus === 'reconnecting'
「離線,請檢查 local-tool」提示 remoteStatus === 'offline'

邏輯總結:前端永遠先看 remoteStatus(雲端是否可達),再看 statusUSB 是否接著)。兩者獨立更新,前端要訂閱兩種事件:

  • remoteStatus 變化 → WS /ws/devices/:id/remote-status(來自 remote-proxy 轉發)
  • status 變化 → WS /ws/devices/:id/status(來自 local agent 的 USB 監聽事件)

10.6 local-tool-only 元件雛形隱藏Q6 裁決)

以下元件在 local-tool 有意義(它就是本機 server在 visionA Cloud 無意義。雛形階段前端隱藏

元件 原因
OnboardingDialog local-tool 首次啟動引導本機 server 設定;雲端版不需要
ServerStatusDashboard 顯示本機 server 健康port / process雲端版換成 tunnel 連線狀態
ServerLogViewer 顯示本機 server log雲端版換成 device log透過 tunnel 抓)

實作方式

  • 沿用 local-tool 同名元件但用 feature flag 控制:NEXT_PUBLIC_APP_MODE=cloud 時不掛載
  • Phase 1 視需求決定要替換、重寫,還是徹底移除

§13 測試策略

13.1 雛形期

類型 範圍 工具
Unit internal/* 各 package go test
Integration api-server + remote-proxy兩 binary+ stub tunnel client go test + httptest
E2E 瀏覽器 → api-server → remote-proxy ← POC edge-ai-servertunnel client 手動 + 簡易 Playwright

雛形端對端驗證路徑Q3 決策):

┌────────────────────┐    HTTPS        ┌────────────────┐  internal HTTP  ┌────────────────┐
│ visionA-frontend   │ ──────────────► │ api-server     │ ──────────────► │ remote-proxy   │
│ (browser)          │                 │ (stateless)    │ ◄────────────── │ (stateful)     │
└────────────────────┘                 └────────────────┘                 └────────┬───────┘
                                                                                   │ WS + yamux
                                                                                   ▼
                                                                          ┌────────────────┐
                                                                          │ POC            │
                                                                          │ edge-ai-server │
                                                                          │ (暫代 tunnel    │
                                                                          │  client)       │
                                                                          └────────────────┘

雛形 不實作 visionA/local-agent/。未來Phase 1建立時將從 local-tool/ 複製起步。

雛形 MVP 覆蓋目標:

  • internal/auth, internal/session(含 InMemoryStore + ProxyClientStore, internal/storageLocalFS80% 行覆蓋
  • internal/relaytunnel accept + internal forwardinternal/tunnel:整合測試為主
  • api handlers至少 happy path + error path
  • Internal HTTP API/internal/session/:token/internal/forward/*):雛形必須有整合測試

13.2 Phase 1

  • E2EPlaywright 跑主要使用者旅程
  • 壓測vegeta 打 api-server自製 tunnel client 模擬 N 條連線打 remote-proxy
  • 混沌測試:隨機斷開 tunnel 驗證重連

§14 TODO 清單(雛形暫不實作,但已記錄)

Auth / Security

  • 真實 AuthOIDC / 自建)
  • JWT / refresh token 機制
  • /tunnel/connect IP + token rate limit
  • Pairing Token DB-backed 實作(PostgresPairingStore
  • 兩階段 tokenpairing short-lived + session long-lived
  • Audit log
  • TLS 終止策略決定ALB / Caddy / nginx

Data / Persistence

  • PostgreSQL + migration 機制golang-migrate
  • 所有 InMemory*RepositoryPostgres*Repository 實作
  • 資料備援策略
  • Soft delete 行為標準化(deleted_at

Storage

  • S3Store 實作 + interface conformance tests
  • Multipart upload for > 100MB 檔案
  • Server-side encryption

ScalingPhase 1

  • remote-proxy 節點間的 session metadata 共享機制(評估 Redis / gossip / service discovery — 雛形不預設方案;見 ADR-006
  • api-server → proxy 跨節點路由邏輯(當一個 api-server 面對多個 proxy 節點時,需找到 session 所在節點)
  • mTLS / shared secret 保護 internal HTTP endpoints目前僅靠內網隔離

雛形 Phase 0 必做(為清楚起見明列)

  • 雙 binary 骨架:cmd/api-server + cmd/remote-proxy
  • api-serverProxyClientStoreSessionStore 透過 internal HTTP 查 remote-proxy
  • remote-proxyInMemoryStore + internal HTTP endpoints/internal/session/:token/internal/forward/http/internal/forward/ws

Converter 整合

  • 確認真實 converter API spec 後,替換 StubClientHTTPClient
  • Job queue / webhook 機制(等 converter 團隊確認)

Observability

  • 結構化 log (JSON)
  • Prometheus metrics export
  • OpenTelemetry trace
  • SLO dashboards
  • Alert rules

Local Agent

  • 建立 visionA/local-agent/:從 local-tool/ 複製起步,精簡為 tunnel-only 輕量代理Q3 決策)
  • local-tool 的「雲端模式」opt-in 設定 UI另一條線讓既有 local-tool 也能當 tunnel client
  • 輕量 tunnel-only headless agent binary未來針對 server / Linux 使用者)
  • local agent 自動更新機制(搬 POC 的 update/

雛形 tunnel 驗證過渡方案Q3 裁決)

  • 雛形 tunnel 驗證暫用 POC 的 edge-ai-server(來自 edge-ai-platform)當 tunnel client
  • 端對端測試路徑:visionA-frontend → api-server → remote-proxy ← POC edge-ai-server
  • visionA/local-agent/ 建立後替換

DevOps

  • CI pipelinelint, test, build, push
  • Kubernetes Helm chart / Terraform module
  • Staging 環境
  • Blue/green or canary deploy
  • 備份與災難恢復演練

Product

  • Billing / 訂閱PRD 範疇)
  • 使用量統計
  • 多組織Org / Team支援
  • API key for programmatic access

待 PM / Design 確認

  • pairing token UX 細節QR code複製貼上WebUSB
  • 未連線時前端呈現方式
  • 模型上傳上限(容量 / 個數)
  • 裝置離線時「離線模式」fallback 設計

版本記錄

日期 版本 變更
2026-04-21 0.1 Architect Agent 初稿(待三方交叉審閱)
2026-04-22 0.2 反映三方審閱 7 項使用者裁決 + ADR-006
2026-04-22 0.3 反映三方交叉審閱結果M-1 Token 格式統一 vAc_/vAs_ hex、M-2 Token 兩階段 TTL 明確化 + 時序圖、M-3 Auth 介面雙層AuthService + AuthProvider、M-4 Converter API 確認 REST、M-5 心跳 10s/30s 統一、M-7 Q5 vs Multi-tab 澄清、M-8 Non-Goal 章節、Minor-2 dev-all-in-one 定位修正、Minor-3 Device 雙狀態 struct、Minor-4 SessionStore.CleanupExpired + ObjectStorage.Exists