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

8.5 KiB
Raw Blame History

Internal API — api-server ↔ remote-proxy

雛形Phase 0就要實作2026-04-22 Q1 裁決)。 雛形採雙 binary 部署:api-serverremote-proxy 各自為獨立 process透過本文件定義的 HTTP 介面溝通。 api-server 無狀態;remote-proxy 持有所有 tunnel sessionin-memory無 Redis見 ADR-006


背景

雛形(單一 remote-proxy instance

[瀏覽器] ─HTTPS─► [api-server]                    [remote-proxy] ←WS+yamux─ [local agent]
                      │                                    ▲
                      │  GET /internal/session/:token      │
                      │  POST /internal/forward/http       │
                      │  GET  /internal/forward/ws         │
                      └────────────────────────────────────┘
                      (internal HTTP走內網 / localhost)
  • api-server 不持有 *yamux.Session;要走 tunnel 的請求一律轉發給 remote-proxy
  • remote-proxy 持有 map[token]*yamux.Session,本機記憶體

Phase 1 當 remote-proxy 水平擴展到多節點時api-server 還需要有「找到對的 proxy」的能力tunnel.md §5.4),但此 HTTP 介面的形狀不變。


端點

雙軌設計B3 Review Major 1 修復 / 2026-04-22

api-server ↔ remote-proxy 的 HTTP forward 提供兩條路徑:

Endpoint 封裝 支援 streaming 支援 WS upgrade 適合場景
POST /internal/forward/http JSON + base64 body 一次性 JSON request/responseGET /healthz、POST /api/devices 等)
POST /internal/forward/raw Hijack 成 raw TCP MJPEG / SSE / chunked (只要 caller 按 HTTP 協定送 upgrade session.ProxyClient.OpenStream(ctx) net.Conn 的底層B4 ProxyClient 預設走這條

兩者並存是刻意為之 — JSON 版對簡單 API 好寫好測raw 版是 OpenStream 語意的真實底層B4 的 session.RemoteHandle 必須走這條)。

POST /internal/forward/httpJSON 版)

用途api-server 把一個一次性的 JSON request 轉發給 proxy 節點,由該節點透過 tunnel 打給 local agent。不支援 streaming;對 MJPEG / SSE / chunked body 應改走 /internal/forward/raw

Request

POST /internal/forward/http?<optional-token-in-query>
Content-Type: application/json

{
  "session_token": "vAc_...",
  "method": "GET",
  "path": "/api/devices",
  "headers": {"X-From-Api-Server": "value"},
  "body": "<base64-encoded request body可省略>"
}

Response200 OK 或 502 Bad Gateway

{
  "status": 200,
  "headers": { "Content-Type": ["application/json"] },
  "body": "<base64-encoded response body>",
  "error": null
}

失敗時:

{
  "error": { "code": "TUNNEL_DISCONNECTED", "message": "session not connected" }
}

實作要點proxy 端):

  • store.Lookup(token) 找 handle
  • handle.OpenStream(ctx) 開 yamux stream
  • http.Requestreq.Write(stream)
  • http.ReadResponse(bufio.NewReader(stream), req)io.ReadAll(body) → base64 encode 回 JSON

POST /internal/forward/rawraw bytes + Hijack 版B3 Review Major 1 修復)

用途:把 api-server 的 HTTP 連線接管成 raw TCP與 tunnel 裡的 yamux stream 雙向 pipe。支援任意 HTTP 流量streaming body、長連線、WS upgradesession.ProxyClient.OpenStream(ctx) net.Conn 語意的真實底層。

Requestapi-server 送):

POST /internal/forward/raw?token=<session-token> HTTP/1.1
Host: <proxy-internal-host>
Content-Length: 0

Response 握手proxy 在 session 找到後寫回):

HTTP/1.1 200 Connected
<空白行>

此時 HTTP 協議結束proxy 呼叫 http.Hijacker.Hijack() 把底層連線交出,與 yamux stream 做雙向 io.Copy。api-server 此後把這條連線當成 net.Conn 直接用:

// api-server 端B4 實作,示意)
conn := dial(/internal/forward/raw?token=xxx)
readHandshake(conn)  // 讀 "HTTP/1.1 200 Connected" + 空白行

// 從這裡開始 conn 就是一條通到 local agent 的 raw TCP stream透過 yamux
req.Write(conn)                                     // 送完整 HTTP request
resp, _ := http.ReadResponse(bufio.NewReader(conn), req)
io.Copy(browserResponseWriter, resp.Body)           // streaming friendly

失敗處理

  • Session 不存在:在 hijack 之前502 JSON { error: TUNNEL_DISCONNECTED }(一般 HTTP client 可讀)
  • Hijacking 不支援:回 500 JSON { error: HIJACK_UNSUPPORTED }
  • Hijack 後 OpenStream 失敗:在 hijacked 連線上寫回 HTTP/1.1 502 Bad Gateway + JSON body 再關閉caller 拿到的仍是合法 HTTP response

為何用 CONNECT-style 握手而非直接雙向 pipe

  • 需要一個明確的「session ready」訊號避免 caller 不知道 session 是否存在就開始 write
  • 與既有 HTTP 基礎設施相容middleware / LB 能正確路由與紀錄一次 request
  • api-server 端 B4 實作時可以用標準 http.Client 發第一個 request拿到 underlying conn 後再切換成 raw 模式

Flusher 支援:不需要 — raw 模式下 io.Copy 天然支援任意 chunk / streaming 語意,由 yamux 負責在 TCP 上傳輸。


GET /internal/forward/wsWebSocket upgrade

用途:與 /http 同,但用於 WebSocket。api-server 收到瀏覽器 WS → 升級 → 在內部開一個到 proxy 的 HTTP Connection Upgrade。

Request

GET /internal/forward/ws?token=<pairing-token>
Upgrade: websocket
... (原瀏覽器 WS upgrade request headers)

Response

101 Switching Protocols
... (yamux stream 裡 local agent 回來的 101 response)

proxy 拿到 upgrade request 後:

  1. session.Open() 拿 stream
  2. 把 upgrade request 寫進 stream給 local agent
  3. 讀 stream 回來的 101從 local agent
  4. Hijack caller 連線,與 stream 雙向 pipe

邏輯同 POC proxyWebSocket只是「caller」換成 api-serverinternal


GET /internal/session/:token

用途proxy 自我查詢某個 token 的 session 狀態debug / health 用。

Response

{
  "token": "pk_xxx",
  "connected": true,
  "connected_at": "...",
  "last_seen_at": "...",
  "stream_open_count": 3
}

POST /internal/session/:token/close

用途:後台運維強制斷某條 tunnel使用者 revoke token 後)。


安全

  • 內部 API 只監聽 internal network interfaceVPC 私網、K8s ClusterIP
  • 防禦:
    • 網路層面security group / NetworkPolicy 只允許 api-server pod 連
    • 應用層面Phase 1+mTLS 或 shared secret header X-Internal-Auth: <bearer>
  • 絕不對外暴露(公網 LB 白名單)

觀測

Metrics

  • proxy_internal_forward_total{endpoint, result}
  • proxy_internal_forward_duration_seconds
  • proxy_stream_open_total
  • proxy_stream_error_total{reason}

Log

  • event=forward path=/api/devices token_prefix=pk_abcd duration_ms=45 status=200

雛形行為Phase 0 必做Q1 裁決)

雛形就是雙 binary,本文件的所有 endpoints 都是 Phase 0 必須實作:

  • remote-proxy 提供這些 internal HTTP endpoints
  • api-server 透過 ProxyClientStore 呼叫這些 endpoints

雛形 API handler 端的呼叫方式:

// api-server 端
handle, err := sessionStore.Lookup(ctx, token)  // 實際走 GET /internal/session/:token
if err != nil { return ErrTunnelDisconnected }
stream, err := handle.OpenStream(ctx)           // RemoteHandle 實作POST /internal/forward/rawB3 Major 1 修復後)
// 寫 request、讀 response — 語義跟直接拿 yamux stream 一致

B4 實作提醒

  • RemoteHandle.OpenStream 必須走 POST /internal/forward/rawhijack raw TCP不能走 JSON 版的 /internal/forward/http
  • 對於簡單 JSON API 的同步呼叫(如 internal 健康檢查),仍可使用 JSON 版以簡化程式碼

Phase 1 多節點時:

  • 介面完全不變
  • ProxyClientStore 底層增加「找到正確 proxy 節點」的邏輯(見 tunnel.md §5.4

總結

  • 雛形:api-serverremote-proxy 透過本文件定義的 HTTP 介面溝通(必做)
  • Phase 1介面不變底層 routing 策略升級
  • 雛形 走「api-server 與 remote-proxy 同進程共用 map」的捷徑那條路會讓 api-server 變成有狀態,走不到 Phase 1