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

233 lines
8.5 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Internal API — api-server ↔ remote-proxy
> **雛形Phase 0就要實作**2026-04-22 Q1 裁決)。
> 雛形採雙 binary 部署:`api-server` 與 `remote-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/http`JSON 版)
**用途**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可省略>"
}
```
**Response**200 OK 或 502 Bad Gateway
```json
{
"status": 200,
"headers": { "Content-Type": ["application/json"] },
"body": "<base64-encoded response body>",
"error": null
}
```
失敗時:
```json
{
"error": { "code": "TUNNEL_DISCONNECTED", "message": "session not connected" }
}
```
**實作要點**proxy 端):
- `store.Lookup(token)` 找 handle
- `handle.OpenStream(ctx)` 開 yamux stream
-`http.Request``req.Write(stream)`
- `http.ReadResponse(bufio.NewReader(stream), req)``io.ReadAll(body)` → base64 encode 回 JSON
### POST `/internal/forward/raw`raw bytes + Hijack 版B3 Review Major 1 修復)
**用途**:把 api-server 的 HTTP 連線接管成 raw TCP與 tunnel 裡的 yamux stream 雙向 pipe。支援任意 HTTP 流量streaming body、長連線、WS upgrade`session.ProxyClient.OpenStream(ctx) net.Conn` 語意的真實底層。
**Request**api-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` 直接用:
```go
// 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/ws`WebSocket 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**
```json
{
"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 interface**VPC 私網、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 端的呼叫方式:
```go
// 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/raw`hijack raw TCP不能走 JSON 版的 `/internal/forward/http`
- 對於簡單 JSON API 的同步呼叫(如 internal 健康檢查),仍可使用 JSON 版以簡化程式碼
Phase 1 多節點時:
- 介面完全不變
- `ProxyClientStore` 底層增加「找到正確 proxy 節點」的邏輯(見 `tunnel.md` §5.4
---
**總結**
- ✅ 雛形:`api-server``remote-proxy` 透過本文件定義的 HTTP 介面溝通(必做)
- ✅ Phase 1介面不變底層 routing 策略升級
- ❌ 雛形 **不**走「api-server 與 remote-proxy 同進程共用 map」的捷徑那條路會讓 api-server 變成有狀態,走不到 Phase 1