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

19 KiB
Raw Permalink Blame History

技術設計文件TDD— 模型共享Model Sharing

狀態Draft待 PM / Design 三方互審) 作者Architect Agent 日期2026-08-02 範圍L 級新功能。在既有「模型庫」owner-only + preset新增「公開對象visibility」+「共享模型庫(依身份權限可見)」兩個維度。 對齊:api/api-model-sharing.mdAPI 規格、ADR-017 決策 3model_shares 點對點分享)、database.md §2.3models 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-onlymodels.owner_user_id + 403 非 owner+ preset7 個公用模型、不在 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_sharesmodel_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 UUIDmigration 0001 有欄);但 user.User domain 未含 OrgIDUserContext.OrgID 有欄位但 OIDC 未必填 migration 0001 line、auth.UserContext.OrgIDuser.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

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/:idget 詳情) 不變(維持 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 visibilityomitempty 或固定輸出,[需 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 == userIDeditor 能否改 [需 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 加 visibilitysnake/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/nonecan_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 依賴圖 + 關鍵路徑

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 審