visionA/docs/autoflow/04-architecture/feature-model-sharing-tdd.md
jim800121chen f6d15b7b14 docs(arch): B 設備管理 + C 模型共享 + camera ADR-020 規劃文件
- feature-device-mgmt-tdd.md + api-device-mgmt.md(B 設備管理 TDD)
- feature-model-sharing-tdd.md + api-model-sharing.md + PRD feature + 設計規格(C 模型共享三方規劃)
- adr-020-ffmpeg-camera-indev.md(camera 三平台 indev)
- PRD.md / TDD.md 索引增補

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-02 16:31:38 +08:00

288 lines
19 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.

# 技術設計文件TDD— 模型共享Model Sharing
> 狀態Draft待 PM / Design 三方互審)
> 作者Architect Agent
> 日期2026-08-02
> 範圍L 級新功能。在既有「模型庫」owner-only + preset新增「公開對象visibility」+「共享模型庫(依身份權限可見)」兩個維度。
> 對齊:`api/api-model-sharing.md`API 規格、ADR-017 決策 3`model_shares` 點對點分享)、`database.md §2.3`models schema
>
> **不動 production code、不寫 migration 實檔**——本檔只給設計方向migration DDL / handler 由 backend agent 落地。
---
## 1. 系統概述與定位
### 1.1 功能拆解(對齊使用者截圖)
| 大項 | 細項 | 對應本 TDD |
|------|------|-----------|
| 模型公開設定 | 設定公開對象 | §3 資料模型visibility 欄)+ API §3 PATCH visibility |
| 共享模型庫 | 依身份權限可見列表 / 分頁 / 排序 filter / 搜尋 | §4 權限查詢 + API §1 `GET /api/models/library` |
| 共享模型庫 | 模型 profile 頁 | §5 + API §2 `GET /api/models/:id/profile` |
### 1.2 兩個正交維度(核心設計判斷)
現況只有 **owner-only**`models.owner_user_id` + 403 非 owner+ **preset**7 個公用模型、不在 DB。本功能加兩個**正交維度**
| 維度 | 語意 | 資料落點 | 來源 |
|------|------|---------|------|
| **visibility廣播** | 「對一群人公開」private / tenant / public | `models.visibility` 欄(本 TDD 新增) | 本功能 |
| **model_shares點對點** | 「分享給特定某人」by user | `model_shares` 表 | ADR-017 決策 3 已定義,本功能**沿用不重造** |
兩者是「或」的關係:一個 user 能看到 model ⟺ 是我的 **OR** visibility 命中我的身份 **OR** 被 share 給我 **OR** preset。這是共享模型庫列表的核心 predicate§4
> **為何不把 visibility 塞進 model_shares**model_shares 是 per-(model, user) 列舉表達「public/tenant」要對每個 user 塞一列、不可行。visibility 是「對一整群」的廣播屬性,天然是 model 上的 enum 欄,查詢時展開成 OR 條件。兩者資料結構本質不同、故分開。
---
## 2. 「依身份權限」的基礎:既有 identity 模型盤點
設計前先確認「身份」有什麼可用(避免腦補一套不存在的 RBAC
| 事實 | source | 對本功能的意義 |
|------|--------|--------------|
| `users.roles TEXT[] NOT NULL DEFAULT '{}'` | migration 0001 + `user.User.Roles` | 有 role 欄位,但目前 OIDC provision 只寫最小欄位、role 多為空。**本功能第一階段不依賴 role 做可見性**(避免建在空資料上),只用 owner / org / share / visibility。role-based 可見性列為後續。`[需 PM 確認]` |
| `users.org_id UUID`migration 0001 有欄);但 `user.User` domain **未含 OrgID**`UserContext.OrgID` 有欄位但 OIDC 未必填 | migration 0001 line、`auth.UserContext.OrgID``user.User`(無 OrgID | **`tenant` visibility 依賴 org_id**。第一階段需確認 OIDC 是否帶 org claim若無`tenant` 可見性暫時等同「沒有人」(安全預設)。`[需 PM 確認]` org_id 來源。 |
| `UserContext{ UserID, Email, Roles, OrgID }` 已是 handler 可信賴身份 | `auth.auth.go` | 可見性查詢的 input = `UserContext`。handler 從它取 userID + orgId。 |
| 無 tenant / group / workspace 表 | grep migrations | **本功能不新增 workspace 表**ADR-017 決策 3 B2 workspace 留後續。tenant = org_id 直接比對,不建關聯表。 |
**設計結論**:第一階段「身份」= `userID` + `org_id`。可見性維度 = private / tenant(org) / public + 點對點 share。**不建 RBAC engine、不建 workspace 表**effort scaling與現況落差最小、避免建在空 role 資料上)。
---
## 3. 資料模型變更(給 migration 設計方向,不寫實檔)
### 3.1 `models` 表加 `visibility` 欄
```
-- 方向DDL 由 backend agent 落地 migration 0006 或後續編號)
ALTER TABLE models ADD COLUMN visibility TEXT NOT NULL DEFAULT 'private';
-- 'private' | 'tenant' | 'public'
-- 既有資料預設 'private':不破壞現況(既有 model 維持只有 owner 看得到)。★關鍵相容性
ALTER TABLE models ADD CONSTRAINT chk_models_visibility
CHECK (visibility IN ('private','tenant','public'));
```
**為何用 enum 欄而非獨立 sharing 表**
- 「公開對象」是 model 的**單值屬性**(一個 model 一個 visibility不是 1-N 關聯 → enum 欄最自然、查詢無需 join。
- 若未來要「同時公開給多個特定 group」才需要關聯表第一階段 private/tenant/public 三選一enum 足夠YAGNI
**既有資料預設值(關鍵相容性)**`DEFAULT 'private'` → 既有所有 model 遷移後維持 owner-only 語意,**零行為改變**。使用者要主動 PATCH 才會公開。
### 3.2 `model_shares` 表(沿用 ADR-017 決策 3本功能不重造
ADR-017 決策 3 已定義(此處只引用、確認欄位對齊):
```
CREATE TABLE model_shares (
model_id UUID NOT NULL REFERENCES models(id),
grantee_user_id UUID NOT NULL REFERENCES users(id),
role TEXT NOT NULL, -- 'viewer' | 'editor'
granted_by UUID NOT NULL REFERENCES users(id),
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
PRIMARY KEY (model_id, grantee_user_id)
);
CREATE INDEX ON model_shares (grantee_user_id);
```
> 本功能第一階段的**寫入 model_shares 的 API分享給特定人不在使用者截圖範圍內**(截圖是「公開對象」+「共享庫瀏覽」。model_shares 在本功能中主要作為**讀取端**(共享庫 predicate 的一部分),確保未來點對點分享上線時共享庫已支援。`[需 PM 確認]` 本期是否需要「分享給特定人」的寫入 UI或只做 visibility 廣播。
### 3.3 index 設計(效能,見 §4
```
-- 共享庫「public 全平台可見」列表high-selectivity 掃描
CREATE INDEX idx_models_public_active ON models (visibility, created_at DESC)
WHERE deleted_at IS NULL AND visibility = 'public' AND uploaded_at IS NOT NULL;
-- tenant 可見:需 join users 取 owner.org_id故 index 在 owner_user_id既有 idx_models_owner_active 已可用)
-- 搜尋 q見 §5 效能考量(第一階段 ILIKE + trigram量大再上 FTS
```
**沿用 ADR-018 慣例**partial index `WHERE deleted_at IS NULL`(既有 models index 全用此模式)、`gen_random_uuid()`PG14 內建、migration 需有對應 `*_test.go` 驗 up/down 對稱130 testcontainers避免假綠——見 memory「DB 接入 dbtest 陷阱」)。
---
## 4. 權限可見性查詢設計(核心,效能重點)
### 4.1 可見性 predicate
「共享模型庫列表」= 一個 query 過濾出當前 user 可見的所有 model
```
可見(model, user) =
model.owner_user_id = :userID -- 我的
OR model.visibility = 'public' -- 全平台
OR (model.visibility = 'tenant'
AND owner.org_id = :userOrgID AND :userOrgID IS NOT NULL) -- 同租戶
OR EXISTS (SELECT 1 FROM model_shares s -- 分享給我
WHERE s.model_id = model.id AND s.grantee_user_id = :userID)
-- preset 模型不在 DB由 handler 層 union沿用既有 modelsListHandler 做法)
```
### 4.2 query 形狀(給 backend方向非最終 SQL
第一階段建議 **單一 query + LEFT JOIN users取 owner.org_id / owner.name**
```sql
SELECT m.*, u.name AS owner_name, u.org_id AS owner_org_id
FROM models m
JOIN users u ON u.id = m.owner_user_id
WHERE m.deleted_at IS NULL
AND m.uploaded_at IS NOT NULL -- 只列 ready
AND (
m.owner_user_id = $userID
OR m.visibility = 'public'
OR (m.visibility = 'tenant' AND u.org_id = $userOrgID AND $userOrgID <> '')
OR EXISTS (SELECT 1 FROM model_shares s
WHERE s.model_id = m.id AND s.grantee_user_id = $userID)
)
-- + filtertarget_chip / source / q+ sort + cursor見 §5
```
### 4.3 效能考量
| 考量 | 對策 |
|------|------|
| **OR 條件讓 planner 難用 index** | public 是最常見的大集合、用 `idx_models_public_active`§3.3覆蓋。owner 用既有 `idx_models_owner_active`。PG 對多 OR 可能走 BitmapOr——`[需 backend 驗]` EXPLAIN量大時考慮拆 UNIONowner 子查 public 子查 shared 子查)避免全表掃。 |
| **model_shares EXISTS 子查** | `idx model_shares (grantee_user_id)` 已在 ADR-017 定義,子查走此 index。 |
| **tenant join users** | owner.org_id 比對需 joinusers 主鍵 join 成本低。 |
| **N+1 owner name** | 用 JOIN 一次帶出 owner_name不在 handler loop 查。 |
| **共享庫規模** | 全平台 public model 數量可能大 → **必須分頁 + index**,不可一次撈全部(現況 owner-only list 無此問題)。 |
---
## 5. API 規格摘要(詳見 `api/api-model-sharing.md`
| 端點 | 用途 | 權限 |
|------|------|------|
| `GET /api/models/library` | 共享庫列表(分頁 cursor + limit + sort + order + q + filter | 登入即可,回傳依身份裁剪可見範圍 |
| `GET /api/models/:id/profile` | 模型 profile公開版詳情 | 可見性檢查,不命中回 404 |
| `PATCH /api/models/:id/visibility` | 設公開對象 | owner-only |
### 5.1 分頁契約cursor vs offset — 決策)
**採 cursor-based不透明 base64 游標),不用 offset**
- 理由:共享庫是「跨擁有者、可能很大、常按 created_at 排序」的 feed 型列表offset 在深分頁效能差且有「插入導致跳頁/重複」問題。
- cursor 編碼 `(sort_value, id)` tie-breakerbase64 不透明(前端當黑箱)。
- `has_more` + `next_cursor` 回傳(見 API §1
- **與既有 `GET /api/models` 不一致是可接受的**:既有是「我的模型」小列表、無分頁需求;共享庫是新端點、獨立契約。`[需 Design 確認]` 前端是否用無限捲動cursor 適合或頁碼offng 需 offset但不建議
### 5.2 既有端點的連帶變更(相容性關鍵)
| 既有端點 | 變更 | 相容性 |
|---------|------|--------|
| `GET /api/models` | **不變**(維持「我的 + preset」語意owner dashboard 用) | 零改變 |
| `GET /api/models/:id`get 詳情) | **不變**(維持 owner-only 403 | 零改變profile 是**新端點**、不動這個 |
| `GET /api/models/:id/download` | **owner-only → 共享權限檢查**(有可見性 + can_download 才放行) | ⚠️ 行為改變、涉及 auth。與 profile 可見性**共用同一判斷函式**single source of truth`[需 security 審]` |
| `POST /api/models/init` / `finalize` / `DELETE` | **不變**(新 model 預設 private見 §3.1 | 零改變 |
| `toModelResponse` DTO | 加 `visibility`omitempty 或固定輸出,`[需 Design 確認]` | 加欄不破壞既有前端snake/camel 雙吃 + 未知欄忽略) |
---
## 6. 安全性設計(多處 `[需 security 審]`
| # | 風險 | 對策 | 需 security 審 |
|---|------|------|:---:|
| SEC-1 | **IDOR / enumeration**:非 owner 猜 model id 看不該看的 profile/download | 可見性檢查在 handler 第一步;不命中回 **404 而非 403**(不揭露 id 存在性) | ✅ |
| SEC-2 | **下載端點權限繞過**download 從 owner-only 放寬後放行過頭 | profile 可見性 + download 授權**共用同一 `canAccessModel(uc, model) → accessLevel` 函式**,杜絕兩處邏輯漂移 | ✅ |
| SEC-3 | **資訊洩漏**profile 回傳 owner email / storage_key / faa_object_key | DTO 白名單;內部 key `json:"-"`(沿用 `FAAObjectKey` 既有做法owner 只揭露 id/name | ✅ |
| SEC-4 | **tenant 邊界誤放**org_id 為空時誤判「同租戶」 | tenant 命中要求 `owner.org_id == user.org_id` 且**兩者皆非空**;空 org 一律不落 tenant 可見 | ✅ |
| SEC-5 | **越權改 visibility**:非 owner 改他人 model 公開對象 | PATCH visibility 強制 `owner_user_id == userID`editor 能否改 `[需 PM 確認]` | ✅ |
| SEC-6 | **公開後可下載即等於檔案外流** | visibility=public 的 download 仍走 ADR-017 (a) FAA delegated token短 TTL + boundary非直接曝露 storage | 併 ADR-017 R4 |
> **建議**:本功能的權限模型(可見性判斷 + download 放寬 + enumeration 防護)**整體送 security agent 審一輪**。這是「auth 的複雜點」——放寬既有 owner-only 邊界,任何判斷漏洞都是資料外洩。
---
## 7. 對既有功能的相容性總結
| 面向 | 影響 | 結論 |
|------|------|------|
| 既有 model 資料 | `visibility DEFAULT 'private'` | 零行為改變,既有 model 維持 owner-only |
| 既有 `GET /api/models` | 不動 | 相容 |
| 既有上傳/下載/刪除 | 上傳/刪除不動download 加共享權限檢查 | download 需回歸測試(既有 owner 下載仍 work|
| 前端 model-store | DTO 加 `visibility`snake/camel 雙吃 + 未知欄忽略 | 加欄相容;共享庫是新頁面/新 store slice |
| migration | 新增 `visibility` 欄 + index+ model_shares 若尚未建) | 需 up/down 對稱 test130 testcontainers避免假綠|
---
## 8. 並行化工作流計畫Parallelization Plan
L 級、跨 backend + frontend + testing適用本節。**contract-first**§8.1 契約定死後三條 work-stream 可平行。
### 8.1 模組間契約single source of truth
| 契約項 | 定義處 | 鎖定內容 |
|--------|--------|---------|
| API schemalibrary / profile / visibility | `api/api-model-sharing.md` | request/response 欄位、分頁 cursor 形狀、錯誤碼 |
| visibility enum | 本檔 §3.1 | `private`/`tenant`/`public`(三方一致,前後端不各自定義字串)|
| accessLevel enum | 本檔 §6 SEC-2 | `owner`/`editor`/`viewer`/`none``can_download = access != none`|
| 可見性 predicate | 本檔 §4.1 | 唯一真實來源backend 與 testing 都對這份 |
| DTO 欄位owner 裁剪、內部 key 不揭露) | api §1/§2 | `owner{id,name,is_me}`、不含 email/storage_key/faa_object_key |
### 8.2 依賴圖 + 關鍵路徑
```mermaid
graph LR
C[契約定稿 §8.1<br/>critical] --> M[migration: visibility 欄+index<br/>backend, critical]
C --> FE[前端: 共享庫頁+profile<br/>對 mock schema]
M --> Q[權限 query + canAccessModel<br/>backend, critical]
Q --> LIB[library/profile/visibility handler<br/>backend]
Q --> DL[download 端點權限放寬<br/>backend]
C --> TS[測試腳本設計<br/>testing 對契約]
LIB --> INT[整合 join]
DL --> INT
FE --> INT
INT --> SEC[security 審 + E2E join]
TS --> SEC
```
**關鍵路徑critical path**`契約定稿 → migration → 權限 query + canAccessModel → handler → 整合 → security審/E2E`。這條無法平行、決定總工期。前端可對 mock schema 平行、不在關鍵路徑上。
### 8.3 work-stream 清單 + 同步點
| Work-stream | 派給 | 可與誰平行 | 阻擋於(前置) | 同步點join |
|-------------|------|-----------|---------------|----------------|
| WS-1 migration + 權限 query + canAccessModel + 三個 handler + download 放寬 | backend | WS-2, WS-3 | 契約定稿 | 整合 join |
| WS-2 共享庫列表頁 + profile 頁(對 mock | frontend | WS-1, WS-3 | 契約定稿 | 整合 join |
| WS-3 測試腳本設計(權限 matrix + 分頁 + enumeration | testing | WS-1, WS-2 | 契約定稿 | E2E join |
| WS-4 security 審(權限模型 / IDOR / 邊界) | security | — | WS-1 整合後 | security join |
> backend 的權限 query 與 canAccessModel 在關鍵路徑、內部**串行**query → handler → download不硬拆。coding 本質難平行的部分誠實標串行。
### 8.4 任務卡Anthropic 四要素,摘要)
**WS-1 backend**
- Objective落地 visibility 欄 migration + 可見性 query + `canAccessModel(uc, model)→accessLevel` 單一函式 + library/profile/visibility 三 handler + download 端點改用 canAccessModel。
- Output可運行 API + 單元測試(權限 matrix+ migration up/down test130 testcontainers。行為符合 §4.1 predicate + api 契約。
- 來源指引:本 TDD §3/§4/§6 + `api/api-model-sharing.md` + ADR-017 決策 3model_shares
- 邊界:**只做 model sharing。不碰前端、不改 model_shares schemaADR-017 已定)、不動既有 `GET /api/models``/:id` 語意。visibility/accessLevel 字串照契約、不自造。download 放寬必須用 canAccessModel、不得複製一份可見性邏輯。**
**WS-2 frontend**
- Objective共享庫列表頁分頁/排序/filter/搜尋)+ profile 頁 + visibility 設定 UI。
- Output頁面 + 元件測試,對 §8.1 契約的 mock。
- 邊界:**只做 UI + api client。不碰 backend。visibility enum / 錯誤碼 / 分頁 cursor 當黑箱照契約。不揭露/不依賴 storage_key/faa_object_keyDTO 本來就沒有)。**
**WS-3 testing**
- Objective權限可見性 matrixowner/tenant同org/tenant異org/public/shared/無關 × library/profile/download+ enumeration猜 id 回 404+ 分頁 + 既有 owner download 回歸。
- 邊界:**對契約與 §4.1 predicate 設計,不改 production code。**
---
## 9. 待三方確認清單(彙整,交互審用)
| # | 項目 | 待誰 | 出處 |
|---|------|------|------|
| P1 | org_id 來源OIDC 是否帶 org claim+ tenant=org 語意是否正確;跨 org 可能性 | PM | §2 |
| P2 | 本期是否要「分享給特定人」寫入 UI或只做 visibility 廣播 | PM | §3.2 |
| P3 | editor 能否改 visibility / 未 ready 能否公開 / checksum 對非 owner 是否公開 | PM | api §3/§5、§6 SEC-5 |
| P4 | 第一階段是否需要 role-based 可見性(現況 role 資料多為空) | PM | §2 |
| D1 | 共享庫 UI 入口(與「我的模型」分頁 or 合併 tab、無限捲動 vs 頁碼 | Design | §5.1、api §0 |
| D2 | `shared_with_me` / visibility badge 標示方式、profile 頁欄位與版面 | Design | api §1/§2 |
| SEC1 | 整體權限模型送審IDOR/404 策略、download 放寬、tenant 邊界、owner email 不揭露 | security | §6 全節 |
---
## 10. 一句話總結
在既有 owner-only 模型庫上,加一個 **model 上的 `visibility` enum 欄private/tenant/public預設 private 保證零相容性衝擊)** 作為「公開對象」廣播維度,與 ADR-017 決策 3 的 `model_shares`(點對點分享)正交;共享模型庫用**單一可見性 predicate**(我的 public 同 org tenant share preset+ cursor 分頁 + 對應 index 查詢profile/download 的權限用**同一 `canAccessModel` 函式**杜絕邏輯漂移enumeration 一律回 404——整個放寬 owner-only 邊界的權限模型建議**整體送 security 審**。