# Design Doc — visionA Cloud ## Metadata - **作者**:Architect Agent - **狀態**:Draft(待三方交叉審閱) - **最後更新**:2026-04-21 - **範圍**:visionA-frontend + visionA-backend 的雲端版架構;定位為「POC 升格為產品」的雛形骨架 - **相關文件**: - 健檢:`.autoflow/00-onboarding/health-check.md` - PRD:`.autoflow/02-prd/PRD.md`(PM 主導,平行產出中) - 設計規格:`.autoflow/03-design/`(Design 主導,平行產出中) - TDD:`.autoflow/04-architecture/TDD.md`(本文檔的姊妹檔,實作細節) - ADRs:`.autoflow/04-architecture/adr/adr-*.md` --- ## 1. 系統總覽(High-Level Overview) visionA Cloud 把「使用者的 Kneron 裝置」透過雲端反向代理提供給瀏覽器操作。核心流向: ``` ┌────────────────────────────────────────────────────────────────────────┐ │ 使用者瀏覽器 │ │ visionA-frontend (Next.js) │ └─────────────────┬────────────────────────────┬────────────────────────┘ │ HTTPS REST │ WSS (訂閱) │ │ ┌─────────────────▼────────────────┐ ┌─────────▼──────────────────────┐ │ visionA-backend / api-server │ │ visionA-backend / api-server │ │ (無狀態, 水平擴展) │ │ (WebSocket 訂閱, 同 binary) │ └─────────────────┬────────────────┘ └─────────┬──────────────────────┘ │ Session 查詢(in-mem 同進程 / Redis 跨進程) │ ▼ ▼ ┌────────────────────────────────────────────────────────────────────────┐ │ 共享 Session Store(routing table) │ │ 雛形:in-memory(單進程) / Phase 1:Redis │ └─────────────────────────────┬──────────────────────────────────────────┘ │ 查到該 token 的 tunnel 在哪個 proxy 節點 ▼ ┌────────────────────────────────────────────────────────────────────────┐ │ visionA-backend / remote-proxy (有狀態) │ │ 持有 yamux.Session map[token]→Session │ └────────────────────────────┬───────────────────────────────────────────┘ │ WebSocket + yamux(長連線) ▼ ┌────────────────────────────────────────────────────────────────────────┐ │ 使用者電腦 local agent │ │ (local-tool 或 tunnel-only client;連 remote-proxy 出站) │ │ 本機 HTTP server :3721 │ └────────────────────────────┬───────────────────────────────────────────┘ │ USB ▼ ┌──────────────────┐ │ Kneron 裝置 │ │ KL520 / KL720 │ └──────────────────┘ 外部依賴(均透過介面,雛形 stub): ├─ kneron_model_converter (`/api/converter/*` stub, TODO) ├─ S3-compatible 物件儲存 (LocalFSStore for prototype) └─ PostgreSQL + Auth provider (in-memory for prototype, ADR-005) ``` **關鍵概念**: - **Session key = pairing token**,一個 token 對應一個 yamux tunnel。 - **API Server 把收到的請求**,查 session store 找到對應 token 的 tunnel,**轉發**到該 tunnel 上的 remote-proxy 節點,**由 proxy 打 yamux stream** 送進 local agent,拿到 response 再回傳瀏覽器。 - **WebSocket 訂閱**同理:api-server 升級 WS 後,在 session store 裡找到 tunnel,把 WS 幀透過 yamux stream 送給 local agent 的 `/ws/*`。 --- ## 1.9 Non-Goals(雛形明確不做的事,2026-04-22 M-8) 以下項目在 **Phase 0 雛形階段刻意不支援**,列出以避免誤解: | # | Non-Goal | 原因 | 對應 Phase | |---|----------|------|-----------| | N1 | **多 instance 部署** — 雛形僅支援單一 `api-server` instance + 單一 `remote-proxy` instance | 無共享 state 機制(無 Redis、無 DB session metadata);開兩個 `api-server` 會因為 session lookup 散到不同節點而壞掉;開兩個 `remote-proxy` 會讓 tunnel 註冊到 A 節點、但 api-server 的 `ProxyClientStore` 配置只連 B,查無 session | Phase 1 | | N2 | **真實使用者註冊 / 登入** — `StaticAuthProvider` 無論帳密都回 demo-user | Auth 不是 POC → 產品驗證的核心風險;整合 Clerk / 自建 OIDC 需 2-4 週,放 Phase 1 | Phase 1 | | N3 | **真實 DB 持久化** — 所有 repository 走 in-memory map | ADR-005 明定;Phase 1 實作 `Postgres*Repository` | Phase 1 | | N4 | **真實 Converter 整合** — `/api/converter/*` 走 Stub | Converter 團隊 API 規格未定;見 `api/api-converter-contract.md` | Phase 1 | | N5 | **Pairing Token 兩階段升級** — 雛形 Pairing 與 Session 合併為單一 token | 單 token 比對流程足以驗證 tunnel;兩階段 DB transaction 需 Postgres | Phase 1(必須在公開 release 前完成)| | N6 | **HTTPS / WSS** — 雛形走明文 HTTP / WS | TLS termination 由 Phase 1 LB / reverse proxy 處理 | Phase 1 | | N7 | **Rate limiting / Audit log** — 雛形無 | 雛形只跑 dev / 內測 | Phase 1 | | N8 | **Observability(metrics / trace / alert)** — 雛形只有 stdout log | 雛形不追求 SLO | Phase 1 | | N9 | **Local agent 自己的 binary(visionA/local-agent/)** — 雛形用 POC `edge-ai-server` 當 tunnel client 暫代 | 讓雛形專注在 backend 協定正確性;Phase 1 從 local-tool 複製起步 | Phase 1 | | N10 | **`cmd/dev-all-in-one` 單進程合併模式** | 會讓部署拓撲與 Production 不一致,反而掩蓋 bug | 永不做 | **重要後果**: - 開第二個 `api-server`(例如為了測試無狀態性)會因 `ProxyClientStore` 只配一個 `VISIONA_PROXY_INTERNAL_URL`,導致兩個 api-server 都指向同一個 remote-proxy,這**可以**運作(真實多 instance 場景)。但開兩個 `remote-proxy` 會壞 — tunnel 連上的是 A,但 api-server 可能查到 B(視 LB 分配),B 沒有 session 就回 404。 - 雛形的部署指引明確:**恰好 1 個 api-server + 恰好 1 個 remote-proxy**。 - Phase 1 解多 `remote-proxy` 節點時會補 session metadata 共享機制(見 `tunnel.md` §5.4 + 待產出的新 ADR)。 --- ## 2. 系統邊界與組件 ### 2.1 visionA-frontend(客戶端) | 屬性 | 值 | |------|-----| | 技術 | Next.js 16 App Router + React 19 + TypeScript + Tailwind 4 + Radix + Zustand 5 + Lucide + Recharts + driver.js | | 部署 | 靜態打包 + CDN(雛形用 Next.js 本地 dev server;Phase 1 上 Vercel / Cloudflare Pages / S3+CloudFront)| | 狀態 | 無(純前端,所有資料 via API)| | Auth | JWT / session cookie(雛形 stub;Phase 1 真實) | | 連線對象 | **只連 `visionA-backend/api-server`**,**不直連** remote-proxy 或 local agent | **從 local-tool 搬來的內容**:所有頁面、components、stores、i18n、UI styling;改 API base URL,移除 localhost hardcode。 **新增**: - `/login`、`/register`(雛形骨架,Auth stub) - `/account`(雛形骨架) - `/devices/pair`(輸入 / 產生 pairing token) - `/clusters`(從 POC 搬) ### 2.2 visionA-backend / api-server(cmd/api-server) | 屬性 | 值 | |------|-----| | Binary | `cmd/api-server/main.go` | | 狀態 | **無狀態**(所有狀態在 DB / session store / 物件儲存)| | 水平擴展 | ✅ 多實例 + 前置 L7 load balancer | | 對外 | HTTP :3001(雛形預設);正式上 443 | | 職責 | REST API、WebSocket 升級、Auth、權限、把請求 proxy 到 remote-proxy、Storage presigned URL、Converter 呼叫 | ### 2.3 visionA-backend / remote-proxy(cmd/remote-proxy) | 屬性 | 值 | |------|-----| | Binary | `cmd/remote-proxy/main.go` | | 狀態 | **有狀態**(持有 yamux.Session)| | 水平擴展 | 可多實例;session 路由交給 session store | | 對外(local agent) | WS :3800(POC 預設)| | 對內(api-server) | HTTP :3801(內部 API,不對外)| | 職責 | 接受 local agent 的 tunnel 連線、維護 yamux session、轉發 api-server 送來的請求 | ### 2.4 共享狀態:Session Store(2026-04-22 Q1 裁決 C + ADR-006:雛形即雙 binary,無 Redis) | 雛形實作 | Phase 1 實作 | |---------|-------------| | Session state **完全由 remote-proxy 持有**(in-memory);api-server 無狀態,透過 internal HTTP 向 remote-proxy 查詢 | 多 remote-proxy 節點間的 metadata 共享機制**待 Phase 1 評估**(Redis / gossip / sticky LB 等 — 不預設採用 Redis) | **雛形設計**: - `cmd/remote-proxy`:**唯一**持有 `*yamux.Session` 的 process;以 `InMemoryStore` 儲存 - `cmd/api-server`:無狀態;`internal/session` 載入 `ProxyClientStore`(internal HTTP client) - 兩 binary 透過 `internal/forward/*` 和 `internal/session/*` endpoints 溝通(詳見 `api/api-internal.md`) **Non-Goal(刻意不做)**: - **不做 `cmd/dev-all-in-one` 單進程合併模式** — 會讓部署拓撲與 Production 不一致,反而讓 bug 在雛形階段無法暴露 - **不引入 Redis** — POC 從未用過(見 ADR-006) 詳細見 TDD §2 與 `tunnel.md` §5。 ### 2.5 local agent(使用者電腦) 三種形態: 1. **local-tool**(桌面版 Wails 應用):原本離線用,加一個 config 選項「啟用雲端模式」後,額外啟動 tunnel client 連 remote-proxy。**local-tool 本身不動**;雲端模式是新增 opt-in 模組,不影響離線使用。 2. **tunnel-only client**(未來):專為「headless / server 使用者」提供的輕量 Go binary,只跑 tunnel client + 最小 HTTP server,不含 Wails / UI。雛形不做。 3. **既有 edge-ai-platform**:POC 的 tunnel client 可直接繼續用來做技術驗證。 ### 2.6 外部依賴 | 依賴 | 雛形 | Phase 1 | |------|------|--------| | `kneron_model_converter` | Stub:`/api/converter/*` 回假資料 | 真實打 converter API | | 物件儲存 | `LocalFSStore`(本地檔案)| `S3Store`(AWS S3 / Cloudflare R2 / MinIO)| | 資料庫 | in-memory repositories | PostgreSQL | | Auth | `StaticAuthService`(永遠 demo-user)| 真實 Auth | | Observability | stdout log | Prometheus + Loki + OpenTelemetry | --- ## 3. 12-Factor 合規策略 | # | Factor | 雛形做法 | Phase 1 做法 | |---|--------|---------|-------------| | 1 | Codebase | ✅ Monorepo (`visionA/visionA-frontend` + `visionA/visionA-backend`) | 同 | | 2 | Dependencies | ✅ `go.mod` + `package.json` 明確宣告;無系統隱式依賴 | 同 | | 3 | Config | ✅ 全走 env vars(`VISIONA_*` prefix);`internal/config` 讀取 | 同 + 密鑰用 Secrets Manager | | 4 | Backing services | ✅ Storage / DB / Auth 全走 interface | 同(實作 swap)| | 5 | Build / Release / Run | ⚠️ 雛形只有 `make build` + `docker-compose` | Build:CI 產出 image;Release:tag + manifest;Run:K8s / ECS deploy | | 6 | Processes | ✅ api-server 無狀態;state 全走 Store interface | 同 | | 7 | Port binding | ✅ api-server `:3001`,remote-proxy 對 agent `:3800`,對 api `:3801` | 同(port 由 env 指定)| | 8 | Concurrency | ✅ 單進程多 goroutine;跨進程靠 SessionStore | 多實例 + auto-scale | | 9 | Disposability | ⚠️ 雛形 SIGTERM graceful shutdown;tunnel 斷線有重連 | 同 + drain period | | 10 | Dev/Prod Parity | ✅ Docker 一致化 | 同 | | 11 | Logs | ⚠️ 雛形 stdout(已有 broadcaster WebSocket 看 log)| 結構化 JSON log + 集中收集 | | 12 | Admin processes | ⚠️ 雛形手動 | 後台 CLI / admin API | **雛形妥協**:log 未結構化、缺 metrics、缺 trace。記錄為 TODO,不影響功能驗證。 --- ## 4. 水平擴展策略 ### 4.1 api-server(無狀態) - 多實例,前置 L7 load balancer(ALB / nginx / Cloud Run 自動) - Sticky session **不需要**(無狀態) - 擴展訊號:CPU > 60% 或 p95 延遲 > 300ms ### 4.2 remote-proxy(有狀態) remote-proxy 的狀態是 `map[token]*yamux.Session`。擴展的挑戰: - Local agent 連 proxy 時,會連到某一個**具體節點**(由 LB 分派,例:ALB + IP hash 或 DNS round-robin) - 後續 api-server 要送請求到該 token 的 tunnel 時,需要**知道這條 tunnel 在哪個 proxy 節點** **解決方案:Session Store 記錄 routing table** ``` Session Store 儲存(Redis Hash): session:{token} = { proxy_node_id: "proxy-2", proxy_internal_url: "http://proxy-2.internal:3801", connected_at: "...", last_seen: "..." } ``` api-server 送請求的流程: ``` 1. 收到 API 請求(例:GET /api/devices + X-Pairing-Token: xxx) 2. 查 Session Store:token → proxy_internal_url 3. 呼叫該 proxy 的 internal endpoint:POST http://proxy-2.internal:3801/forward - body = 原請求(method, path, headers, body) 4. proxy-2 收到後,從自己 in-memory map 取 yamux.Session 5. 開 stream,傳 HTTP request 6. 等 response → 回給 api-server → 回給瀏覽器 ``` ### 4.3 雛形簡化(單節點) 雛形期 api-server 與 remote-proxy **同進程**,SessionStore 就是本機 map,**跳過跨節點轉發**: ``` 1. 查 SessionStore(local map)取 yamux.Session 2. 直接開 stream 送請求 ``` Phase 1 時把 SessionStore 換 Redis、引入「proxy internal HTTP API」,不改 API handler 程式碼。 ### 4.4 擴展邊界 | 資源 | 估算 | |------|------| | remote-proxy 單節點 tunnel 數上限 | ~10K(受檔案描述符 / 記憶體限制,每個 tunnel ~100KB)| | api-server 單節點 QPS | ~5K(Gin + Go 典型值)| | Session Store(Redis)單節點 QPS | ~100K | 雛形單節點即可撐 demo / 早期內測。Phase 1 再引入多節點。 --- ## 5. 資料流 ### 5.1 前端呼叫 REST API(最常見情境) ``` [瀏覽器] GET /api/devices ↓ HTTPS + Cookie/JWT [api-server] authMiddleware 驗 user ↓ 從 user 找對應 pairing token(雛形 env 寫死;Phase 1 查 DB) [api-server] 查 SessionStore(token) 取 proxy location ↓ (單節點雛形:直接拿到 yamux.Session) (多節點:轉發 http://proxy-X.internal:3801/forward) ↓ [remote-proxy] 從 session open yamux stream ↓ 把 HTTP request 寫進 stream(沿用 POC 方法) [local agent] tunnel client accept stream ↓ 轉發到本機 127.0.0.1:3721 [local HTTP server] 處理 /api/devices → 回 response ↓ 經 yamux stream 回到 remote-proxy ↓ remote-proxy 回 api-server ↓ api-server 回瀏覽器 [瀏覽器] 得到裝置列表 ``` ### 5.2 前端開 WebSocket 訂閱(例:裝置事件串流) ``` [瀏覽器] WS /ws/devices/events (connect) ↓ [api-server] authMiddleware → 升級 WS ↓ 查 SessionStore → 取 yamux session ↓ 開 stream,把 HTTP upgrade request 寫進 stream(沿用 POC proxyWebSocket 邏輯) [local agent] handleStream 偵測 upgrade → 連本機 raw TCP → 雙向 copy [local HTTP server] /ws/devices/events 升級 → 開始推事件 ↓ 事件 frame → yamux stream → api-server → 瀏覽器 ``` POC 已完整實作此流程,visionA 直接沿用。 ### 5.3 模型上傳(**不走 tunnel**) ``` [瀏覽器] 點「上傳模型」 ↓ [api-server] POST /api/models/upload-url ↓ 呼叫 Storage.PresignedPutURL → 回給瀏覽器 [瀏覽器] 直接 PUT 檔案到 S3 / LocalFS(不走 tunnel,省 tunnel 頻寬) ↓ 上傳完成後 [瀏覽器] POST /api/models/register (告知 api-server 上傳完成 + metadata) ↓ [api-server] 寫入 model repository ``` **為何不走 tunnel?** - 模型檔可達百 MB,走 tunnel = 占用使用者本地頻寬兩次(上傳 → 雲端 → 下載) - S3 presigned URL 讓瀏覽器直連,scalable - 需要時 api-server 再叫 local agent「下載這個 model 的 URL」,由 local agent 用自己網路下載 ### 5.4 轉檔呼叫(Phase 1) ``` [瀏覽器] POST /api/converter/convert {source_url, target_chip: "kl520"} ↓ [api-server] 驗 user → 呼叫 kneron_model_converter API ↓ (非同步)取得 job_id [api-server] 回 job_id ↓ 輪詢 /api/converter/jobs/{id} 或 WS 訂閱進度 ``` 雛形 api-server 提供端點,但回 stub(job_id 假的,狀態永遠 `queued`)。 --- ## 6. 安全架構 ### 6.1 通訊加密 - **瀏覽器 ↔ api-server**:HTTPS(Phase 1);雛形開發用 HTTP - **api-server ↔ remote-proxy 內部**:雛形同進程無需加密;多節點 Phase 1 考慮 mTLS 或 VPC-only - **local agent ↔ remote-proxy**:WSS(Phase 1);雛形 WS。TLS 由前端反代(ALB / nginx)終止 ### 6.2 認證 / 授權 | 物件 | 雛形 | Phase 1 | |------|------|--------| | 使用者登入 | `StaticAuthService` 永遠過 | OIDC / SAML / 自建 | | 前端 call API | 無 token 也接受(stub)| JWT / session cookie | | local agent 連 proxy | `VISIONA_PAIRING_TOKEN` env 寫死 | DB-backed Pairing Token(見 ADR-003)| | API handler 授權 | 一律通過 | RBAC:`user 只能看自己的 device` | ### 6.3 CORS / CSRF - **CORS**:api-server 雛形設 `Access-Control-Allow-Origin: *`(dev);Phase 1 白名單 - **CSRF**:用 JWT in header(非 cookie)可天然免疫;若用 session cookie 則需 CSRF token - **WebSocket Origin**:本 repo 的 `local-tool/server/internal/api/ws/origin.go` 已有成熟 allowlist,搬過來用 ### 6.4 Pairing Token 安全 詳見 ADR-003。重點: - 雛形:env 寫死 - Phase 1:DB 存 `sha256(token)`,不存明文;15min expiry;可 revoke;綁 user_id + device_id ### 6.5 輸入驗證 - 模型檔案格式驗證(雛形僅 extension;Phase 1 加 magic bytes + file size limit) - API body schema 驗證(用 struct tags `binding:"required"` 或 `go-playground/validator`) ### 6.6 STRIDE 分析(重點項目) | 威脅 | 防護 | 雛形 / Phase 1 | |------|------|--------------| | 偽冒(Spoofing):他人冒用 token | `sha256(token)` 存 DB + rate limit;雛形 env 隔離 | Phase 1 必做 | | 竄改(Tampering):中間人改 request | TLS | Phase 1 | | 否認(Repudiation) | Audit log | Phase 1 | | 資訊洩露(Information Disclosure) | TLS + least privilege log | Phase 1 | | DoS | rate limit on `/tunnel/connect` + API | Phase 1 | | 提權(EoP) | RBAC | Phase 1 | **雛形期驗證技術,不預期公開部署**,以上多數 Phase 1 才補全。 --- ## 7. 部署架構 ### 7.1 雛形(開發 / 內測) **方案 A:本機一鍵啟動** ``` docker-compose up ``` `docker-compose.yml`: ``` services: api-server: build: { context: ., dockerfile: docker/Dockerfile.api-server } ports: ["3001:3001"] environment: VISIONA_SESSION_BACKEND: inmemory VISIONA_STORAGE_BACKEND: localfs VISIONA_AUTH_MODE: static VISIONA_PAIRING_TOKEN: dev-token-abc123 volumes: ["./data:/data"] remote-proxy: build: { context: ., dockerfile: docker/Dockerfile.remote-proxy } ports: ["3800:3800", "3801:3801"] environment: VISIONA_SESSION_BACKEND: inmemory VISIONA_PAIRING_TOKEN: vAc_<32 hex> # 格式見 security.md §1.3 frontend: build: { context: visionA-frontend } ports: ["3000:3000"] environment: NEXT_PUBLIC_API_BASE: http://localhost:3001 ``` **雛形交付物(Phase 0)**:上述雙 binary + docker-compose 即完整雛形。**不提供** `cmd/dev-all-in-one`(見 §2.4 Non-Goal)。 ### 7.2 Phase 1(正式部署,草圖) ``` ┌──────────┐ │ CDN │ (Cloudflare / CloudFront) └────┬─────┘ │ static frontend ┌───────────────┴────────────────┐ │ │ ┌────▼─────┐ ┌──────▼────┐ │ ALB │ │ NLB │ (for WebSocket, TCP pass-through) └────┬─────┘ └──────┬────┘ │ HTTPS /api/*, /ws/* │ WSS /tunnel/connect │ │ ┌────▼───────────────┐ ┌────▼──────────────────┐ │ api-server cluster │◄──────────►│ remote-proxy cluster │ │ (K8s / ECS) │ Redis │ (K8s / ECS, StatefulSet│ │ stateless, N pods │ Session │ or DaemonSet) │ └────┬───────────────┘ Store └────┬──────────────────┘ │ │ ├─► PostgreSQL (RDS) │ ├─► S3 / R2 (object store) │ └─► Converter API │ │ WSS + yamux ▼ 使用者電腦 local agent ``` **Cloud-agnostic**:所有組件(Redis、PG、S3)都有對應的 interface;可用 AWS / GCP / 自建。 ### 7.3 CI/CD(雛形目標) - `make build` → 兩個 binary - `make docker-build` → 兩個 image - `make docker-compose-up` → 本機跑起來 - GitHub Actions / GitLab CI(Phase 1)→ 自動 build + push registry + deploy --- ## 8. 觀測(Observability) ### 8.1 雛形 | 項目 | 做法 | |------|------| | Log | stdout;沿用 local-tool 的 log broadcaster(WS 看 log)| | Metrics | 無;`/api/system/health` 回 ok 即可 | | Trace | 無 | | Alert | 無 | ### 8.2 Phase 1(TODO) | 項目 | 做法 | |------|------| | Log | 結構化 JSON → Loki / CloudWatch Logs | | Metrics | Prometheus export:`qps`, `p95_latency`, `tunnel_count`, `session_active_count` | | Trace | OpenTelemetry SDK → Tempo / X-Ray | | SLO | API 可用性 99.9%,p95 < 500ms;Tunnel 連線穩定度 99%(7 天滾動)| | Alert | Grafana / PagerDuty | --- ## 9. ADR 索引 | 編號 | 標題 | 狀態 | |------|------|------| | [ADR-001](adr/adr-001-two-binaries.md) | 單一 Go 專案 / 雙 binary 結構 | Accepted | | [ADR-002](adr/adr-002-tunnel-protocol.md) | 沿用 POC 的 Tunnel 協定(WebSocket + yamux)| Accepted | | [ADR-003](adr/adr-003-pairing-token.md) | 以 Pairing Token 取代 SHA256(MAC) Token | Accepted | | [ADR-004](adr/adr-004-storage-interface.md) | 模型儲存採用 S3-Compatible 介面 | Accepted | | [ADR-005](adr/adr-005-no-db-auth-in-prototype.md) | 雛形階段不接真實 DB 與 Auth | Accepted | --- ## 10. 三方交叉審閱檢核清單 ### 給 PM 審閱 - [ ] 所有 PRD 中列出的 P0 功能,是否在本 Design Doc 都有對應的技術路徑? - [ ] 「雲端能操作使用者本地裝置」的核心價值主張是否由架構支撐? - [ ] 雛形 vs Phase 1 的差異,PM 能否向使用者解釋「現在能做什麼、未來會做什麼」? ### 給 Design 審閱 - [ ] 「pairing token 輸入」頁面是否技術可行(API 已定義)? - [ ] 模型上傳流程的 presigned URL 對 UX 是否清楚? - [ ] tunnel 斷線時瀏覽器要怎麼顯示?(已在 §5.2 指出走 POC 同機制;需 Design 規劃 UI) ### 給 Architect 自我檢查 - [x] 每個 ADR 都有 Context / Decision / Consequences - [x] 所有雛形實作都有明確的 Phase 1 替換計畫 - [x] 擴展策略說明完整(§4) - [x] 安全架構覆蓋 STRIDE(§6.6) --- **下一步**:閱讀 `TDD.md`(實作細節),以及各 ADR。