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>
This commit is contained in:
jim800121chen 2026-08-02 16:31:38 +08:00
parent 6a797d5eb5
commit f6d15b7b14
9 changed files with 1851 additions and 0 deletions

View File

@ -95,6 +95,7 @@ visionA Cloud 是把 edge-ai-platform POC 升格的正式雲端產品,定位
|------|--------|------|------| |------|--------|------|------|
| 裝置管理 | P0 | [feature-device-management.md](features/feature-device-management.md) | 搬自 local-tool走 remote-proxy | | 裝置管理 | P0 | [feature-device-management.md](features/feature-device-management.md) | 搬自 local-tool走 remote-proxy |
| 模型管理 | P0 | [feature-model-management.md](features/feature-model-management.md) | 7 預設 + 上傳,走 S3 介面 | | 模型管理 | P0 | [feature-model-management.md](features/feature-model-management.md) | 7 預設 + 上傳,走 S3 介面 |
| 模型共享 | P0/P1 | [feature-model-sharing.md](features/feature-model-sharing.md) | L 級新功能:在模型管理上加「可見性/權限」維度(私有/指定使用者/組織/公開),含共享模型庫(分頁/排序/filter/搜尋、公開設定、profile 頁;三方討論中 |
| 推論操作 | P0 | [feature-inference.md](features/feature-inference.md) | Camera / Image / Video / Batch | | 推論操作 | P0 | [feature-inference.md](features/feature-inference.md) | Camera / Image / Video / Batch |
| Pairing 流程 | P0 | [feature-pairing.md](features/feature-pairing.md) | 新增,取代 POC 的 MAC 寫死 | | Pairing 流程 | P0 | [feature-pairing.md](features/feature-pairing.md) | 新增,取代 POC 的 MAC 寫死 |
| 工作區 | P0 | [feature-workspace.md](features/feature-workspace.md) | 裝置 → 模型 → 來源 | | 工作區 | P0 | [feature-workspace.md](features/feature-workspace.md) | 裝置 → 模型 → 來源 |

View File

@ -0,0 +1,227 @@
# Feature模型共享Model Sharing— L 級新功能
> 父文件:[PRD.md](../PRD.md) | 相依既有功能:[模型管理](feature-model-management.md)、[會員系統](feature-auth.md)
> 對應 User StoriesUS-31 ~ US-40本檔新增接續 user-stories.md 既有 US-01~US-30
> 狀態三方聯合討論中PM 產出草稿,待 Design / Architect 互審)
> 撰寫日期2026-08-02
---
## 0. 這份 PRD 的定位(先講清楚,避免誤解範圍)
**模型共享不是從零打造的功能,而是在既有「模型管理」功能上,疊加一個「可見性 / 權限」維度。**
反推自既有 code 的現況(重要,這是設計基礎):
| 面向 | 現況(已實作) | 本功能要加什麼 |
|------|---------------|---------------|
| 模型資料模型 | `Model``OwnerUserID``Source`(preset/uploaded/converted)、軟刪。**無 visibility 欄位** | 新增「可見性 / 公開對象」維度 |
| 存取控制 | 嚴格 owner-only`List` 只 filter `OwnerUserID``Get`/`download` 非 owner 回 403+ 系統 preset 公用 | 讓「非 owner 但被授權者」也能看到 / 下載 |
| 模型庫 UI`/models`| 按**來源**分三區preset / converted / uploaded只看自己的 | 新增「共享給我 / 我可見」的維度分頁、排序、搜尋、filter |
| 使用者 / 權限 | OIDCMember Center`sub`=userIDusers 表有 `org_id`(nullable, 未用)、`roles TEXT[]`(未用) | 「公開對象」要對齊這些既有欄位(同租戶 / 群組概念) |
| 下載 | handler 註解已寫明「第一階段 owner-only**B 分享後續階段**)」 | 本功能正是被預留的「B 分享階段」 |
**一句話**:既有系統已經為「分享」預留了骨架(`org_id``roles`、download handler 的註解),本功能把這個骨架填血。
---
## 1. 功能概述與價值主張
**使用者痛點**(反推自 Persona見 user-research.md
- **阿哲FAE**:轉檔 / 調校好一個客戶專用模型後,想給同組 FAE 或客戶直接用,現在只能「下載 → 私訊傳檔 → 對方重新上傳」,模型檔散落、版本混亂。
- **SarahSI**:在多個客戶現場佈署,想把一套驗證過的模型推給整個團隊 workspace現在每個工程師都要自己上傳一份。
- **Mike開發者**:想引用別人分享的公開模型做 A/B test 基準,現在沒有「探索別人模型」的入口。
**核心價值主張**:「**上傳一次,授權共享,團隊即用**」——把模型從「我的私有檔案」升級為「可依身份權限流通的資產」。
**北極星指標關聯**:模型共享降低「取得可用模型」的摩擦 → 提升首次推論成功率與每週推論次數WAD / 每週推論次數 per user見 success-metrics.md
---
## 2. 公開對象(可見性)權限模型 — PM 提案
> ⚠️ **本節是全功能的核心設計決策,需 Architect 確認技術可行性、Design 確認 UI 複雜度。**
### 2.1 設計原則
1. **對齊既有 auth 概念**,不自造平行權限系統。既有已有:`OwnerUserID`(擁有者)、`org_id`(租戶 / 組織nullable`roles TEXT[]`
2. **preset 模型維持不變**:系統 7 預設模型永遠對所有人公開,不納入本功能的可見性控制。
3. **owner 永遠有完整權限**:可設定 / 變更公開對象、可撤銷、可刪除。
4. **漸進式**Phase A 先做最有價值、技術最單純的層級;複雜的群組 / 租戶留 Phase B。
### 2.2 提案可見性層級Visibility Levels
`Model` 上新增 `Visibility` 欄位,四個層級由私到公:
| 層級 | 值(提案) | 誰能看到 / 下載 | 對齊既有概念 | 建議 Phase |
|------|-----------|----------------|-------------|-----------|
| **私有** | `private` | 只有 owner= 現況預設行為) | `OwnerUserID` | **P0預設值** |
| **指定使用者** | `restricted` | owner + 被明確授權的 user 清單 | 新增 share grant 關聯 | **P0** |
| **同租戶 / 組織** | `organization` | owner + 同 `org_id` 的所有 user | 既有 `users.org_id` | **P1**(依賴租戶功能成熟度) |
| **全公開** | `public` | 所有已登入 user | — | **P1** |
**P0MVP先做 `private` + `restricted` 兩級**,理由:
- `private` 是現況行為,成本近乎 0只是把隱含的「無 visibility = 私有」顯性化)。
- `restricted`(指定 user 分享)是**痛點最直接的解**(阿哲要給特定同事 / 客戶),且不依賴「租戶 / 群組」這種尚未成熟的概念。
- `organization` / `public` 依賴 `org_id` 租戶模型與內容治理(濫用、審核),成本與風險高,留 P1。
> **待 Architect 確認 A**`restricted` 的授權清單,建議新增一張 `model_shares` 關聯表(`model_id` × `grantee_user_id` × `permission` × `granted_by` × `created_at`)。這是否與現有 DB migration 策略相容?(既有 migration 到 0005models 表 index 全 owner-scoped共享查詢需要新 index / 新表。)
>
> **待 Architect 確認 B**`organization` 層級依賴 `users.org_id`但該欄位目前是「nullable + 未寫入」的 stub。若 P1 要做,需先有「租戶 / org 指派」機制。此依賴請 Architect 評估。
>
> **待 Design 確認 C**:四層級的 UI 呈現。P0 兩級相對單純(一個下拉 + 指定 user 的 email/名稱輸入);但「指定 user 清單」的 UX怎麼搜尋 user、怎麼移除、怎麼顯示已授權清單需要 Design 設計。
### 2.3 權限粒度permission
P0 先做**唯讀分享**(被授權者可**檢視 + 下載 + 用於推論**,但不能改 / 刪 / 二次分享)。
| 權限 | P0 | 說明 |
|------|-----|------|
| view看得到、看 profile | ✅ | 出現在對方的「共享給我」列表 |
| download下載 .nef | ✅ | 對齊 download handler 的「B 分享階段」 |
| use載入裝置推論 | ✅ | 分享的核心價值:對方能直接用 |
| edit / re-share / delete | ❌P1+ | 只有 owner 能做 |
> **待 Architect 確認 D**`use`(載入裝置推論)牽涉 `load-to-device` 與 download token 簽發。現在 download handler 對非 owner 回 403`restricted` 要放行「被授權者」——授權檢查要插在哪一層middleware / handler / repo filter
---
## 3. User Stories接續既有 US 編號)
> 格式對齊 user-stories.md。RICE 的 Effort 欄標「待 Architect 核對」,本檔先給 PM 相對估值。
### 3.1 模型公開設定owner 視角)
- **US-31P0**:作為模型擁有者,我要**在模型 profile 頁設定它的公開對象(私有 / 指定使用者)**,這樣我就能控制誰能用我的模型。
- **US-32P0**:作為模型擁有者,我要**把模型分享給指定的一位或多位使用者(用 email 或名稱搜尋)**,這樣特定同事 / 客戶就能直接使用。
- **US-33P0**:作為模型擁有者,我要**看到目前這個模型已分享給哪些人,並能移除某個人的存取權**,這樣我能隨時收回授權。
- **US-34P1**:作為模型擁有者,我要**把模型設為「同組織可見」或「全公開」**,這樣整個團隊 / 社群不用我逐一授權。
### 3.2 共享模型庫(被分享者 / 探索者視角)
- **US-35P0**:作為使用者,我要**在模型庫看到「共享給我」的模型(別人授權給我的)**,並清楚看到它是誰分享的、我的權限是什麼。
- **US-36P0**:作為使用者,我要**能對共享模型庫做分頁瀏覽**,這樣模型多時不會一次載入全部、頁面不卡。
- **US-37P0**:作為使用者,我要**對共享模型庫做排序(上傳時間 / 名稱 / 檔案大小)**,這樣我能快速找到最新或最相關的模型。
- **US-38P0**:作為使用者,我要**對共享模型庫做 filter依硬體晶片 / 來源 / 可見性 / 分享者)**,這樣我能縮小範圍。
- **US-39P0**:作為使用者,我要**用關鍵字搜尋模型(名稱 / 描述)**,這樣我能直接找到目標模型。
- **US-40P0**:作為使用者,我要**進入任一可見模型的 profile 頁看到完整資訊metadata、支援硬體、擁有者、分享者、我的權限、下載 / 載入按鈕)**,這樣我在使用前能確認它是我要的。
### 3.3 依身份權限的可見性(貫穿所有 story 的規則)
**共享模型庫「我能看到的模型」= 聯集**
```
我擁有的owner
明確分享給我的restricted grantUS-32
我所屬 org 的 organization 模型P1US-34
全公開模型P1US-34
系統 preset既有永遠可見
```
> **待 Architect 確認 E**:這個「聯集查詢」在現有 `Repository.List(ListFilter)` 介面下無法表達(現在只支援 owner filter。需擴充 `ListFilter`(加 grantee / visibility / org 維度)或新增 method。請評估對現有 in-memory + postgres 兩個實作的衝擊。
---
## 4. RICE 排序 / 優先級
| # | Story | Reach | Impact | Conf. | Effort待 Architect 核對) | RICE | Phase |
|---|-------|-------|--------|-------|------|------|-------|
| US-32 | 分享給指定使用者 | 70 | 3 | 80% | 2 | 84 | **P0** |
| US-35 | 「共享給我」列表 | 70 | 3 | 80% | 1.5 | 112 | **P0** |
| US-31 | 公開設定(私有/指定) | 70 | 2 | 80% | 1 | 112 | **P0** |
| US-40 | 模型 profile 頁 | 80 | 2 | 90% | 1.5 | 96 | **P0** |
| US-39 | 關鍵字搜尋 | 80 | 1 | 90% | 1 | 72 | **P0** |
| US-33 | 檢視 / 撤銷授權清單 | 60 | 2 | 80% | 1 | 96 | **P0** |
| US-38 | filter晶片/來源/可見性/分享者)| 80 | 1 | 80% | 1 | 64 | **P0** |
| US-37 | 排序 | 80 | 1 | 90% | 0.5 | 144 | **P0** |
| US-36 | 分頁 | 80 | 1 | 90% | 0.8 | 90 | **P0** |
| US-34 | org 可見 / 全公開 | 50 | 2 | 60% | 3 | 20 | **P1** |
> RICE 說明沿用 user-stories.md §5.1Reach 為 Phase 相對估值、Impact 對北極星 WAD。Effort 全部標「待 Architect 核對」——尤其 US-32 / US-35 的授權表 + 聯集查詢是技術重點。
### P0 / P1 切分總結
**P0MVP本次做**
- 可見性兩級:`private`(預設)+ `restricted`(指定 user 分享)
- 公開設定 UI在 profile 頁)+ 授權清單管理(新增 / 移除)
- 共享模型庫:「共享給我」維度 + 分頁 + 排序 + filter + 搜尋
- 模型 profile 頁(擴充既有 `/models/[id]`,加分享資訊與權限顯示)
**P1後續**
- `organization`(同租戶可見)— 依賴租戶 / org 指派機制成熟
- `public`(全公開)— 需內容治理(濫用回報、審核)
- 二次分享、edit 權限、分享通知email / in-app
---
## 5. 與既有模型功能的關係(擴充 vs 全新)
| 元件 / 檔案 | 現況 | 本功能 | 類型 |
|------------|------|--------|------|
| `Model` domainmodel.go | 無 visibility | 加 `Visibility` 欄位 + 可能的 share 關聯 | **擴充** |
| `models`migration 0001 | owner-scoped index | 新 migration加 visibility 欄 + `model_shares` 表 + 新 index | **擴充 + 新表** |
| `Repository.List(ListFilter)` | 只 owner filter | 擴充 filtergrantee / visibility或新 method | **擴充** |
| `GET /api/models`models.go | preset + 自己的 | 加「共享給我」聯集 + 分頁 / 排序 / 搜尋 query params | **擴充** |
| `GET /api/models/:id``download` | 非 owner 403 | 放行被授權者 | **擴充** |
| 新 API設定可見性 / 管理授權 | — | `PUT /api/models/:id/visibility``POST/DELETE /api/models/:id/shares` | **全新** |
| `/models`page.tsx | 按 source 三區 | 加「共享給我」維度 + 分頁 / 排序 / 搜尋列 | **擴充** |
| `ModelFilters`model-filters.tsx | 只 targetChip | 加來源 / 可見性 / 分享者 filter + 搜尋框 | **擴充** |
| `/models/[id]` profile 頁 | 基本資訊 + 下載 / 刪除 | 加:公開設定區、授權清單管理、分享者 / 我的權限顯示 | **擴充** |
| 「公開設定」互動 | — | 新 UI可見性下拉 + user 搜尋授權) | **全新Design 重點)** |
**關鍵**:搜尋 / 排序 / 分頁在既有 code 是被標為「Phase 1 TODO」的見 model-filters.tsx 註解、feature-model-management.md TODO-3。本功能把它們正式做出來且**同時服務「我的模型」與「共享模型」兩個維度**——不是只給共享用。
---
## 6. 成功指標
| 類別 | 指標 | Baseline | P0 目標 | 追蹤方式 |
|------|------|----------|---------|---------|
| 採用 | 建立過至少一次分享的 owner 比例 | N/A新功能baseline=0 | 內測 FAE 中 ≥ 40% | 埋點:`model_share_created`(待 Architect 確認埋點清單) |
| 採用 | 「共享給我」列表被點開的 WAU 比例 | N/A | ≥ 30% | 埋點:`shared_library_viewed` |
| 價值 | 用「別人分享的模型」跑過推論的 user 比例 | N/A | ≥ 25% | 埋點:`inference_with_shared_model` |
| 效率 | 共享模型庫首屏載入時間(含分頁) | N/A | P95 < 1.5s | 前端 perf Architect / 非功能需求對齊 nonfunctional.md |
| 效率 | 搜尋 → 找到目標模型的操作步數 | 現況需私訊傳檔(不可量測) | ≤ 3 步(搜尋 / filter → profile → 使用) | 可用性測試 |
| 護欄 | 未授權存取被正確拒絕率 | 現況 owner-only 403 | 100%(不可退化) | 安全測試(待 Security / Architect |
| 護欄 | 既有「我的模型」列表載入時間不退化 | 現況 P95 | 不劣於現況 | 回歸 perf 測試 |
> Baseline 多為 N/A全新功能、無歷史數據。目標值為內測 FAE 群體的估算門檻launch 後 2 週校正。護欄指標「未授權存取拒絕率」是**本功能最重要的把關**——分享功能最大的風險是權限漏洞導致模型外洩。
---
## 7. 待三方確認事項彙總(互審重點)
### 給 Architect技術可行性
| # | 事項 | 位置 |
|---|------|------|
| A | `model_shares` 授權關聯表設計是否相容既有 migration 策略0001~0005 全 owner-scoped index | §2.2 |
| B | `organization` 層級依賴 `users.org_id`(現為未寫入 stubP1 前需先有租戶指派機制 | §2.2 |
| C | `use`(被授權者載入裝置推論)的 download token 簽發 + 授權檢查插入層級middleware / handler / repo | §2.3 |
| D | `Repository.List` 聯集查詢owner granted org public對 in-memory + postgres 兩實作的衝擊 | §3.3 |
| E | 分頁 / 排序 / 搜尋在 List 介面的表達offset/cursor 分頁?搜尋用 LIKE 還是全文索引?) | §3.2 |
| F | 各 RICE Effort 欄核對(尤其 US-32 / US-35 | §4 |
| G | 成功指標的埋點事件清單可行性 | §6 |
### 給 Design體驗面
| # | 事項 | 位置 |
|---|------|------|
| C | 「指定 user 分享」的 UX怎麼搜尋 user、加入 / 移除、已授權清單呈現 | §2.2 |
| — | 可見性層級的 UI下拉分段控制如何讓 owner 一眼看懂「私有 vs 指定 vs 公開」的差異與風險) | §2.2 |
| — | 共享模型庫的資訊架構:「我的模型」與「共享給我」怎麼並存(分頁 tab分區既有已按 source 分三區,再加維度會不會過載) | §3.2 / §5 |
| — | profile 頁如何同時呈現「我是 owner可管理」vs「我是被授權者唯讀」兩種狀態 | US-40 |
| — | 分頁 / 排序 / 搜尋 / filter 控制列的整合設計(既有 filter 只有 targetChip 一個下拉,要擴成完整工具列) | §3.2 |
### 給雙方(需一起拍板)
- **P0 是否確認只做 `private` + `restricted` 兩級**PM 主張是,理由見 §2.2。若 Design / Architect 認為 org 層級成本可控且需求急,可討論上調。)
- **可見性預設值 = `private`**(不主動公開任何東西,安全優先)——三方確認無異議。
---
## 8. 連結
- 回:[PRD 索引](../PRD.md)
- 相依:[模型管理](feature-model-management.md)、[會員系統](feature-auth.md)、[介面契約](../interface-contracts.md)
- 待更新user-stories.mdUS-31~US-40 併入主表、success-metrics.md分享指標

View File

@ -0,0 +1,461 @@
# 模型共享 設計規格 — visionA Cloud
> **L 級新功能**「模型共享」的 Design 部分三方聯合規劃PM 寫 PRD / Architect 寫 TDD / Design 寫本檔)。
>
> **核心定位**:本功能是**擴充既有模型庫 UI**不是全新模組。100% 沿用既有 Design Tokens`design-tokens.md`)、既有元件(`components.md`)、既有 `/models` 三區版型與 `ModelCard` / `ModelGrid` / `ModelSection` / `ModelFilters` / `ModelDetailClient`。本檔只定義「共享」這個維度帶來的新視覺 / 互動增量。
>
> **不新增 Design Token**。狀態色沿用既有 `chart-*` token 與 §2.1 半語義約定。
>
> 對應頁面:`/models`(列表擴充)+ `/models/[id]`(詳情頁擴充成 owner / 公開兩態)+ 新增「共享對象設定」互動Dialog+ 新增「共享模型庫」瀏覽維度。
>
> 配套格式參考:`pages.md` §8、`flows/flow-model-upload.md``components.md`
---
## 0. 名詞與待確認前提(先給 PM / Architect
本設計基於以下**假設**,凡標 🔶 者為「需 PM 確認需求細節」、標 🔷 者為「需 Architect 確認技術可行性」。設計會標出因假設不同而分歧的 UI 分支。
| # | 名詞 / 假設 | 說明 | 標記 |
|---|-----------|------|------|
| A1 | **公開對象visibility三態** | 假設分 `private`(僅自己)/ `public`(所有登入使用者可見)/ `shared`(指定對象)三種 | 🔶 PM是否只做 private/public 兩態「指定對象」Phase 0 是否需要? |
| A2 | **「指定對象」的粒度** | 假設可指定「個別使用者by email」。是否還有「群組 / 組織 / 團隊」概念? | 🔶 PM + 🔷 Architect有無 org/team 資料模型? |
| A3 | **可見模型的三分類** | 列表要區分「我的owner/ 公開public/ 共享給我的shared-with-me」 | 🔶 PM分類命名與是否需要「我公開出去的」獨立檢視 |
| A4 | **共享 = 唯讀** | 假設別人共享 / 公開給我的模型我**只能檢視 + 下載 + 燒錄**,不能改名 / 刪除 / 再共享 | 🔶 PM可否「再共享」re-share可否下載 |
| A5 | **profile 頁 = 擴充既有 detail** | 假設「模型 profile 頁」就是擴充現有 `/models/[id]`,依 viewer 身份切 owner 版 / 公開版,不另建路由 | 🔷 Architect是否需要獨立 public 路由(如 `/m/[shareId]` 供分享連結)? |
| A6 | **分頁pagination** | 共享模型庫可能量大需分頁。假設用 offset/limit 或 cursor | 🔷 Architect後端分頁機制offset vs cursor決定 UI頁碼 vs 無限捲動 / 載入更多) |
| A7 | **搜尋範圍** | 假設搜尋 = 對「當前可見的所有模型」搜名稱(+ 選填分類 / 晶片) | 🔶 PM搜尋要不要跨到「公開但未共享給我」的全庫探索 |
| A8 | **公開對象變更權限** | 假設只有 owner 能改自己模型的公開設定 | 🔷 Architect權限在 API 層把關(前端只是 UI 便利) |
> **設計原則**:以上假設我採「最完整但可降級」的畫法——先畫三態 visibility + 三分類列表PM 若砍到兩態 / 兩分類UI 直接移除對應分支即可,不需重畫。
---
## 1. User StoryDesign 視角)
> **作為** 一個 Kneron 開發者,
> **我想要** 把我的模型公開或共享給特定同事,並瀏覽別人共享給我的模型,
> **這樣** 團隊就能共用模型,不用每個人重複上傳 / 轉檔。
**體驗成功條件:**
- 使用者一眼能分辨列表中哪些是「我的 / 公開 / 共享給我的」(不只靠顏色,用 badge 文字 + 圖示雙編碼)
- 設定公開對象在 2 次點擊內可達(卡片選單 or 詳情頁按鈕 → Dialog
- 共享模型庫量大時,瀏覽 / 搜尋 / 篩選 / 排序不卡頓、狀態清楚(載入 / 空 / 無結果 / 無權限四態齊備)
- 公開版 profile 頁不洩漏 owner 的私有操作(刪除 / 改公開設定按鈕對非 owner 隱藏)
---
## 2. 範圍與 Phase 0 降級
| 項目 | Phase 0本次 | Phase 1+ |
|------|----------------|---------|
| Visibility 態 | private / public / shared🔶 待 PM 砍) | + 到期共享 / 密碼保護連結 |
| 指定對象粒度 | 個別使用者 by email🔶 | + 群組 / 組織 |
| 列表分類 | 我的 / 公開 / 共享給我的 三分類(🔶) | + 我公開出去的獨立檢視 |
| 分頁 | 「載入更多」按鈕 or 頁碼(依 🔷 A6 | 無限捲動 + 虛擬列表 |
| 排序 | 名稱 / 建立時間 / 共享時間3 選項) | + 熱門度 / 下載數 |
| 篩選 | 既有 targetChip + 新增「共享狀態」維度 | + 分類 / 標籤 / owner |
| 搜尋 | 名稱模糊搜(前端 or 後端依 🔷 A6 | 全文 + tag |
| profile 公開版 | owner 版 / 公開版兩態 | 分享連結 `/m/[shareId]` |
| 再共享 / 轉讓 | 不做(🔶 A4 | 視需求 |
---
## 3. 資訊架構變更IA
沿用既有 `/models``/models/[id]`,不新增頂層導航。共享是「模型」的一個屬性維度,落在既有模型庫內。
```
/models (模型庫 — 擴充:新增「共享」瀏覽維度)
├── 檢視模式切換(新增):
│ ┌─ 依來源分區既有preset / converted / uploaded ← 預設,僅「我的」模型
│ └─ 依共享關係分區(新增):我的 / 公開 / 共享給我的
├── 搜尋框(新增,跨當前檢視)
├── 篩選(擴充 ModelFilterstargetChip + 共享狀態)
├── 排序(新增:名稱 / 建立時間 / 共享時間)
├── 分頁 / 載入更多(新增)
└── /models/[id] (模型 profile 頁 — 擴充成雙態)
├── owner 版:既有全操作(改名 / 刪除 / 下載 / 燒錄 / 【新增】公開設定)
└── 公開 / 共享版:唯讀(檢視 / 下載 / 燒錄),隱藏 owner-only 操作
+ 新增「擁有者」資訊列(誰共享的 / 共享時間 / 公開對象徽章)
```
**🔶 待 PM 確認**:兩種檢視模式(來源分區 vs 共享關係分區)是否都要?還是共享關係分區直接取代來源分區?我建議**保留切換**Tab 或 Segmented Control因為「來源分區」對管理自己的模型仍有用「共享關係分區」才是本功能重點。
---
## 4. 共享模型庫列表設計
### 4.1 檢視模式切換(新增元件 `ModelViewToggle`
`/models` 頁標題列下、`ModelFilters` 上方,新增一個 Segmented Control用既有 `Tabs` 元件實作,`variant` 沿用):
```
┌─────────────────────────────────────────────────────────────┐
│ 模型庫 [上傳模型] │
│ 管理、公開與共享你的 Kneron 模型 │
├─────────────────────────────────────────────────────────────┤
│ ┌──────────────┬──────────────┐ │
│ │ 依來源 ● │ 依共享關係 │ ← Tabs既有元件
│ └──────────────┴──────────────┘ │
├─────────────────────────────────────────────────────────────┤
│ 🔍 [搜尋模型名稱...........] [晶片 ▾] [共享狀態 ▾] [排序 ▾]│ ← 篩選列(擴充)
├─────────────────────────────────────────────────────────────┤
│ (分區內容依所選檢視模式渲染) │
└─────────────────────────────────────────────────────────────┘
```
- **依來源**既有預設不變preset / converted / uploaded 三區,僅顯示「我的」模型。
- **依共享關係**(新增):三區 —
- 🟢 **我的**owner`chart-1` 色點呼應
- 🔵 **公開**public含我公開的 + 別人公開的,🔶 A7 待確認範圍):`chart-2`
- 🟣 **共享給我的**shared-with-me`chart-3`
> 沿用既有 `ModelSection` 的「色點 + 標題 + 計數」樣式,只是換分組維度。`SOURCE_DOT_CLASS` 邏輯延伸出一份 `SHARE_DOT_CLASS`,色 token 一致。
### 4.2 卡片上的共享標示(擴充 `ModelCard`
既有 `ModelCard` 已有 status badge + targetChip badge + source badge。**新增一個 visibility badge**,放在 badge 列,用**圖示 + 文字雙編碼**(不僅靠顏色,滿足無障礙 De1
| Visibility | 圖示Lucide | 文字 | 配色(沿用 token tint 風格) | 顯示條件 |
|-----------|--------------|------|---------------------------|---------|
| private | `Lock` | 私有 | `text-muted-foreground` + `border-border`(中性)| owner 檢視自己模型時 |
| public | `Globe` | 公開 | `border-chart-2/30 bg-chart-2/10 text-chart-2` | 任何人 |
| shared | `Users` | 已共享 / 共享給我 | `border-chart-3/30 bg-chart-3/10 text-chart-3` | owner 看到「已共享給 N 人」receiver 看到「{owner} 共享」|
**owner 額外資訊**private/shared/public 卡片右上角owner 檢視時可在 badge 顯示對象數,如「公開」「共享 · 3 人」。
**receiver 視角**:卡片新增一行**次要資訊**`text-xs text-muted-foreground`):「由 {ownerEmail} 共享 · {relativeTime}」。沿用 `RemoteDeviceBadge` 的相對時間格式化 util剛剛 / X 分鐘前 / 絕對時間)。
**卡片操作差異**
- owner 卡片:既有下載按鈕(若 downloadable+ **新增卡片右上角 `⋮` 選單**Dropdown內含「公開設定」「刪除」等見 §5 觸發點)。
- receiver 卡片:只保留「下載」(若 A4 允許)+ 點擊進 profile 頁。無 `⋮` 選單(無 owner 權限)。
> 🔷 **Architect 確認**`ModelSummary` 需新增欄位 `visibility: 'private' | 'public' | 'shared'``ownerEmail?``sharedAt?``sharedCount?`owner 視角)、`isOwner: boolean`。前端 UI 依這些欄位渲染,實際權限由 API 把關。
### 4.3 篩選擴充(擴充 `ModelFilters`
既有 `ModelFilters` 只有 targetChip。新增「共享狀態」維度第二個 `Select`),僅在「依共享關係」檢視模式下顯示:
```tsx
export interface ModelFilterValue {
targetChip: TargetChip | "all";
visibility?: "all" | "private" | "public" | "shared"; // 新增,僅共享關係檢視用
}
```
- targetChip 篩選跨所有分區作用(沿用既有行為)。
- visibility 篩選:選「公開」時只顯示 public 區,其餘區收合為 0沿用「區內為空仍顯示標題 + 精簡空狀態」的既有慣例)。
### 4.4 搜尋框(新增)
- 位置:篩選列最左,`Input` + 前綴 `Search` icon既有 `Input` 元件 h-9
- 行為:🔷 A6 —
- 若後端分頁搜尋走後端debounce 300ms送 query配合分頁。
- 若前端全量:即時前端 filter無 debounce 需求)。
- Placeholder`搜尋模型名稱...`
- 清除:有輸入時右側顯示 `X` 清除鈕(`ghost` icon button
- 無障礙:`role="searchbox"``aria-label="搜尋模型"`;搜尋無結果時 `aria-live="polite"` 播報結果數。
### 4.5 排序(新增 `ModelSortSelect`
`Select`h-9選項
| key | 文字 | 說明 |
|-----|------|------|
| `name-asc` | 名稱 A→Z | 預設(我的區)|
| `createdAt-desc` | 最新建立 | |
| `sharedAt-desc` | 最近共享 | 僅「共享給我的」區有意義;其他區隱藏此選項 |
🔷 排序在後端還前端由 A6 決定。若後端分頁 → 排序參數送後端。
### 4.6 分頁(新增,依 🔷 A6 二選一)
**方案 P1cursor / 無限捲動友善)→「載入更多」按鈕**Phase 0 建議,實作簡單、無頁碼狀態同步問題):
```
┌────────────────────────────────────────────┐
│ [卡片] [卡片] [卡片] [卡片] │
│ [卡片] [卡片] [卡片] [卡片] │
│ │
│ [ 載入更多 (顯示 24 / 87) ] │ ← Button variant=outlineloading 時 Spinner
└────────────────────────────────────────────┘
```
**方案 P2offset / 傳統頁碼)→ 底部 Pagination 元件**(若後端只支援 offset
```
上一頁 1 2 [3] 4 5 … 12 下一頁
```
> 🔷 **Architect 裁決 A6**:後端分頁是 cursor 還 offset我**建議 P1「載入更多」**——對「瀏覽型」的共享庫體驗較順、行動裝置友善、無「換頁後捲動位置重置」問題。若 Architect 已定 offset則走 P2。**兩案我都出規格,選一即可,不需重畫卡片。**
**分頁 UX 細節**
- 每頁 24 筆3 欄 × 8 列 desktop卡片沿用既有 `grid-cols-1 sm:2 lg:3 xl:4`)。
- 載入更多 loading按鈕內 `Spinner` + 文字「載入中」;同時底部補 4 個 `Skeleton` 卡片(沿用既有 skeleton 樣式)。
- 搜尋 / 篩選 / 排序變更 → 重置分頁到第 1 頁 / 清空已載入。
---
## 5. 模型公開設定 UI新增 `ModelVisibilityDialog`
### 5.1 觸發點兩處owner-only
1. **列表卡片** `⋮` 選單 →「公開設定」§4.2 新增的 Dropdown
2. **profile 頁**owner 版)操作列 → 新增按鈕 `[公開設定]``variant=outline` + `Globe` icon放在既有「下載 / 刪除」旁。
> 🔶 PM觸發點以上兩處是否足夠是否需要在上傳完成後直接引導設定公開對象我建議上傳 Dialog 完成後加一句「模型預設為私有,之後可在詳情頁設定公開」的提示,不強迫當下設定。)
### 5.2 Dialog 版型(沿用既有 `Dialog`
```
┌──────────────────────────────────────────────────┐
│ 公開設定 — {模型名稱} [X] │
├──────────────────────────────────────────────────┤
│ 誰可以看到並使用這個模型? │
│ │
│ ○ 🔒 私有 只有你自己 │
│ ○ 🌐 公開 所有 visionA 使用者 │
│ ● 👥 指定對象 只有你選的人 │ ← RadioGroup
│ │
│ ┌─ 指定對象(僅「指定對象」選中時展開) ──────────┐ │
│ │ [輸入 email 加入........] [加入] │ │
│ │ ┌──────────────────────────────────────────┐ │ │
│ │ │ alice@corp.com 可檢視+下載 [✕] │ │ │
│ │ │ bob@corp.com 可檢視+下載 [✕] │ │ │
│ │ └──────────────────────────────────────────┘ │ │
│ └────────────────────────────────────────────────┘ │
│ │
│ ⚠ 公開後,所有使用者都能下載此模型(若允許下載) │ ← public 選中時的提示
├──────────────────────────────────────────────────┤
│ [取消] [儲存變更] │
└──────────────────────────────────────────────────┘
```
**元件組成(全部沿用既有 shadcn 元件):**
- 三態選擇:`RadioGroup`(🔷 若 components.md 未列 RadioGroup需 Architect / Frontend 確認 shadcn 是否已有,或用 `Select` 降級)。
- 圖示:`Lock` / `Globe` / `Users`(與列表 badge 同圖示,維持一致性)。
- 指定對象加入:`Input`email+ `Button`加入。email 格式即時驗證(失焦時),錯誤走既有 `aria-invalid` + `border-destructive` 樣式。
- 對象清單:每列 = email + 權限標籤Phase 0 固定「可檢視+下載」,🔶 A4+ 移除鈕(`ghost` icon `X`)。
- public 警告:選 public 時顯示 `bg-amber-50 dark:bg-amber-950/30` 提示條(沿用 §2.1 半語義約定)。
- 底部:`[取消]``variant=outline`+ `[儲存變更]``variant=default`loading 時 Spinner
**🔶 待 PM 確認**
- 「指定對象」若 Phase 0 不做Dialog 降為 private / public 兩選RadioGroup 兩項,移除展開區),版型不變。
- 權限粒度Phase 0 是否只有「可檢視+下載」單一權限?還是要分「僅檢視 / 檢視+下載 / 檢視+燒錄」?
### 5.3 互動與回饋
| 動作 | 回饋 |
|------|------|
| 切換 private→public | 顯示 amber 警告條;儲存前不生效 |
| 加入 email格式錯| Input 紅框 + 下方「Email 格式不正確」|
| 加入 email不存在的使用者| 🔷 Architect後端驗證回錯時 toast「找不到使用者 {email}」|
| 加入重複 email | Input 提示「已在清單中」,不重複加入 |
| 儲存成功 | 關 Dialog + toast「已更新公開設定」+ 列表 / 卡片 badge 即時更新 |
| 儲存失敗 | Dialog 內 error banner「儲存失敗請重試」不關閉 |
| public→private曾共享給人| AlertDialog 二次確認「改為私有後,已共享的對象將無法再存取,確定?」|
### 5.4 UX Writing本功能新增文案
| key | 繁中 | English |
|-----|------|---------|
| `models.visibility.title` | 公開設定 | Visibility |
| `models.visibility.question` | 誰可以看到並使用這個模型? | Who can access this model? |
| `models.visibility.private` | 私有 | Private |
| `models.visibility.private.desc` | 只有你自己 | Only you |
| `models.visibility.public` | 公開 | Public |
| `models.visibility.public.desc` | 所有 visionA 使用者 | All visionA users |
| `models.visibility.shared` | 指定對象 | Specific people |
| `models.visibility.shared.desc` | 只有你選的人 | Only people you choose |
| `models.visibility.addEmail` | 輸入 email 加入 | Add by email |
| `models.visibility.publicWarning` | 公開後,所有使用者都能下載此模型 | Once public, anyone can download this model |
| `models.visibility.saved` | 已更新公開設定 | Visibility updated |
| `models.visibility.emailInvalid` | Email 格式不正確 | Invalid email format |
| `models.visibility.emailDuplicate` | 已在清單中 | Already in the list |
| `models.visibility.userNotFound` | 找不到使用者 {email} | User {email} not found |
| `models.visibility.revokeConfirm` | 改為私有後,已共享的對象將無法再存取,確定? | Setting to private revokes access for shared users. Continue? |
| `models.sharedBy` | 由 {email} 共享 | Shared by {email} |
---
## 6. 模型 profile 頁設計(擴充 `/models/[id]`
### 6.1 設計取捨:擴充既有 detail依身份切兩態
**決策:不另建路由**,沿用 `/models/[id]` + `ModelDetailClient`,依 `model.isOwner`(🔷 Architect 提供)分支渲染。理由:
- 既有 detail 頁已有完整資訊展示(描述 / 大小 / framework / inputShape / classes / checksum共享 profile 需要的資訊 90% 重疊,重畫浪費。
- 唯一差異是「操作區權限」與「擁有者資訊」,用條件渲染即可。
> 🔷 **Architect 確認 A5**:若未來要「分享連結給未登入者 / 站外」,才需獨立 public 路由 `/m/[shareId]`免登入、SEO 友善。Phase 0 假設僅站內登入使用者可見,沿用 `/models/[id]` 即可。**若需求包含站外分享連結,請 PM 明確,會影響是否要新路由。**
### 6.2 兩態差異表
| 區塊 | owner 版(既有)| 公開 / 共享版(新增分支)|
|------|---------------|----------------------|
| 返回鈕 | `← 返回`(回 /models| 同 |
| 標題 + badge 列 | name + targetChip + status + source | + **visibility badge**(公開 / 共享)|
| **擁有者資訊列**(新增)| 不顯示(自己就是 owner| 顯示:`由 {ownerEmail} 共享 · {time}` + owner 頭像(首字母圓形,沿用 UserMenu avatar 樣式)|
| 操作列 | 下載 + 刪除 + **【新增】公開設定** | **僅**下載(若 A4 允許);隱藏刪除 / 公開設定 |
| 描述 / metadata 卡片 | 完整 | 完整(唯讀,相同)|
| classes 區塊 | 完整 | 完整(唯讀)|
| 燒錄至裝置(未來 F7+| 列出自己在線裝置 | 同(共享模型也能燒錄到自己的裝置)|
### 6.3 公開版 profile 版型
```
┌──────────────────────────────────────────────────────┐
│ ← 返回 │
│ │
│ ResNet50-KL720 👥 共享給我 │ ← 標題 + visibility badge
│ [KL720] [就緒] [轉檔] │ ← 既有 badge 列
│ [⬇ 下載] │ ← 僅下載(無刪除/公開設定)
│ ┌──────────────────────────────────────────────────┐│
│ │ 👤 由 alice@corp.com 共享 · 3 天前 ││ ← 擁有者資訊列(新增)
│ └──────────────────────────────────────────────────┘│
│ │
│ ┌─ 模型描述 ─────────────────────────────────────┐ │ ← 既有卡片,唯讀
│ │ 一般物件偵測模型... │ │
│ │ 大小: 24.3 MB 建立: 2026/07/20 │ │
│ │ Framework: onnx Input: 1×3×224×224 │ │
│ │ Classes (80): [person][car][dog] +77 │ │
│ └────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────┘
```
### 6.4 擁有者資訊列規格(新增小元件 `ModelOwnerBar`
- 容器:`flex items-center gap-2` + `bg-muted/50 rounded-md px-3 py-2 text-sm`
- 頭像40px→改 24px`h-6 w-6 rounded-full`圓形email 首字母,沿用 UserMenu avatar 配色
- 文字:`由 {ownerEmail} 共享 · {relativeTime}`(相對時間 util 沿用 RemoteDeviceBadge
- 僅在非 owner 檢視時渲染
- 無障礙:`aria-label="模型擁有者資訊"`
---
## 7. 各狀態設計(空 / 載入 / 無權限 / 無結果)
沿用既有 `EmptyState` / `Skeleton` / toast文案走本功能語氣。
| 狀態 | 觸發 | 呈現 | 文案 |
|------|------|------|------|
| **載入中** | 首次載入 / 換頁 / 搜尋 | 既有 `Skeleton` 卡片網格8 個)| — |
| **空 · 共享給我的(無)** | 「共享給我的」區無資料 | `EmptyState` icon=`Users` | 標題「還沒有人共享模型給你」/ 描述「當同事把模型共享給你時,會出現在這裡」|
| **空 · 公開(無)** | 公開區無資料 | 精簡 inline 空狀態(沿用區內空狀態樣式)| 「目前沒有公開的模型」|
| **搜尋無結果** | 搜尋 query 無 match | `EmptyState` icon=`SearchX` + `[清除搜尋]` CTA | 標題「找不到符合『{query}』的模型」/ 描述「試試其他關鍵字或清除篩選」|
| **無權限** | 直接開別人私有模型的 `/models/[id]` | 全頁 `EmptyState` icon=`Lock` + `[返回模型庫]` | 標題「沒有權限檢視此模型」/ 描述「這個模型未公開或未共享給你」|
| **公開設定儲存失敗** | API 錯 | Dialog 內 error banner | 「儲存失敗,請重試」|
| **載入更多失敗** | 分頁 API 錯 | 「載入更多」按鈕轉為 `[重試]` + toast | 「載入失敗,點擊重試」|
**無權限狀態(`/models/[id]` 403版型**
```
┌──────────────────────────────────────┐
│ ← 返回 │
│ │
│ 🔒 │
│ 沒有權限檢視此模型 │
│ 這個模型未公開或未共享給你 │
│ │
│ [ 返回模型庫 ] │
└──────────────────────────────────────┘
```
> 🔷 Architect403 vs 404 的取捨——為避免「模型是否存在」的資訊洩漏,建議私有模型對無權限者回 **404**當作不存在UI 走「找不到模型」而非「無權限」。**請 Architect 確認採 403 揭露存在 or 404 隱藏存在**UI 兩版文案我都備。
---
## 8. 響應式(沿用既有斷點)
沿用 `pages.md` §11 的斷點與 `/models` 既有規則Mobile 單欄 / Tablet 2 欄 / Desktop 34 欄)。本功能新增元素的響應式:
| 元素 | Mobile (<640) | Tablet (6401024) | Desktop (1024) |
|------|--------------|-------------------|-----------------|
| 檢視模式 Tabs | 全寬 2 等分 | 同 | 靠左自然寬 |
| 篩選 / 搜尋 / 排序列 | **堆疊**(搜尋佔滿寬一行 + 篩選鈕 wrap 下一行)| 部分同行 wrap | 單行 |
| visibility badge | 顯示badge 列 `flex-wrap` 既有)| 同 | 同 |
| 擁有者資訊列 | 全寬email 過長 `truncate` | 同 | 同 |
| 公開設定 Dialog | `max-w-[calc(100vw-2rem)]`,對象清單可捲動 | `max-w-lg` | `max-w-lg` |
| 分頁「載入更多」| 全寬按鈕 | 置中 | 置中 |
| 頁碼P2| 精簡(僅 頁 X/Y | 完整頁碼 | 完整頁碼 |
---
## 9. 無障礙(沿用既有 + 本功能新增)
- **visibility badge**:圖示(`aria-hidden`+ 文字雙編碼不僅靠顏色De1。SR 讀得到「公開」「已共享」文字。
- **檢視模式 Tabs**Radix Tabs 內建 `role="tablist"` + Arrow 鍵導航 + `aria-selected`
- **公開設定 RadioGroup**Arrow 鍵切換選項、`aria-checked`、每選項有可見 label。
- **email 加入**`Input` `aria-label`;加入成功後焦點回 input 便於連續加入;對象清單移除鈕 `aria-label="移除 {email}"`
- **搜尋框**`role="searchbox"`;無結果 `aria-live="polite"` 播報「找到 0 個模型」。
- **載入更多**`aria-label` 含當前 / 總數「載入更多,已顯示 24 之 87」。
- **狀態改變**(儲存 / 共享toast 走既有 Sonner`aria-live` 內建)。
- **觸控目標**:所有新增互動元素(`⋮` 選單鈕、移除鈕、Tab、清除鈕≥ 44×44px`⋮``icon` size = 36px 需注意,建議 profile / 卡片上用 `size=icon` 但外圍點擊區補到 44px或用既有 button 慣例)。🔷 Frontend 實作注意。
- **對比度**:所有新 badge 用既有 `chart-*` token tint 風格(`/10` 底 + 純色文字),與既有 source badge 同做法,既有已通過 WCAG AA。Dark Mode 由 token 自動處理。
> **Dark Mode**:本功能不新增顏色,全用既有 token`chart-1/2/3``muted``amber-*` 半語義Dark Mode 自動生效。無需獨立 Dark 版設計。(本任務 A 層 Dark Mode 截圖延到 B 層 / 實作期補,因為零新 token、純沿用。
---
## 10. 沿用 vs 新增總表
### 10.1 沿用既有(不改)
| 既有資產 | 用途 |
|---------|------|
| Design Tokens 全部 | 零新增 |
| `ModelGrid` | 卡片網格 + skeleton + 空狀態(分頁時擴充「載入更多」)|
| `ModelSection` | 分區色點 + 標題 + 計數(共享關係分區沿用)|
| `Card` / `Badge` / `Button` / `Dialog` / `AlertDialog` / `Select` / `Input` / `Tabs` / `EmptyState` / `Skeleton` / `Sonner` | 全部 |
| `RemoteDeviceBadge` 的相對時間 util | 共享時間 / owner 資訊列 |
| UserMenu avatar 樣式 | owner 頭像 |
| §2.1 amber 半語義 | public 警告條 |
### 10.2 新增Design 定義Frontend 實作)
| 新增 | 類型 | 基底 |
|------|------|------|
| `ModelViewToggle` | 元件 | `Tabs` |
| visibility badge含於 ModelCard 擴充)| 元件擴充 | `Badge` + Lucide `Lock`/`Globe`/`Users` |
| ModelCard `⋮` owner 選單 | 元件擴充 | `DropdownMenu`(🔷 確認 shadcn 已有)|
| `ModelFilters` 共享狀態維度 | 元件擴充 | `Select` |
| 搜尋框 | 元件擴充 | `Input` + `Search` icon |
| `ModelSortSelect` | 元件 | `Select` |
| 分頁「載入更多」or Pagination | 元件 | `Button` / 新 Pagination🔷 A6 二選一)|
| `ModelVisibilityDialog` | 元件 | `Dialog` + `RadioGroup` + `Input` + `Button` |
| `ModelOwnerBar` | 元件 | `Card`/div + avatar |
| ModelDetailClient owner/公開雙態分支 | 頁面擴充 | 條件渲染 |
| 無權限 / 搜尋無結果 / 共享空狀態 | 狀態 | `EmptyState` |
---
## 11. 給三方互審的重點清單
### 需 PM 確認需求細節 🔶
1. **A1 Visibility 態數**private/public/shared 三態 vs 只 private/public 兩態「指定對象」Phase 0 要不要做?
2. **A2 指定對象粒度**:個別 email vs 群組/組織/團隊?
3. **A3 列表分類**:「我的 / 公開 / 共享給我的」三分類命名 OK 嗎?要不要「我公開出去的」獨立區?
4. **A4 共享權限**receiver 能否下載能否再共享re-share權限是否分「僅檢視 / 檢視+下載 / 檢視+燒錄」?
5. **A7 搜尋範圍**:搜尋只搜「可見的」還是要能「探索全站公開庫」?
6. **檢視模式**:「依來源」與「依共享關係」兩種 Tab 都保留,還是共享關係取代來源?
7. **觸發引導**:上傳完成後要不要引導設定公開對象?
### 需 Architect 確認可行性 🔷
1. **A5 / 6.1 profile 路由**:沿用 `/models/[id]` 雙態 vs 需獨立 public 路由 `/m/[shareId]`(站外分享連結)?
2. **A6 分頁機制**:後端 cursor→ P1 載入更多)還 offset→ P2 頁碼)?搜尋 / 排序在前端還後端?
3. **`ModelSummary` 新欄位**`visibility` / `ownerEmail` / `sharedAt` / `sharedCount` / `isOwner` 可否由 API 提供?
4. **7 無權限回應碼**:私有模型對無權限者回 403揭露存在還 404隱藏存在影響 UI 文案。
5. **RadioGroup / DropdownMenu**shadcn 元件庫是否已含這兩個?未含則需先補 or 用 Select 降級。
6. **email 對象驗證**:加入指定對象時,後端能否即時驗證使用者存在?
---
## 12. Verification本設計任務自檢
- **De-A1 三斷點**§8 已定義 mobile/tablet/desktop 三斷點行為(純規格 + ASCII 版型;實際截圖於 prototype / 實作期補,本階段為規格文件無渲染產出)。
- **De-A2 對比度**:零新 token全沿用既有 `chart-*` tint + `muted` + `amber` 半語義,既有已過 WCAG AAdesign-tokens.md 已確認)。
- **De-A3 無 hardcode**:本檔所有色彩引用皆為 token 名(`chart-1/2/3``muted-foreground``amber-50` 半語義約定),無裸 hex / rgb。
- **De-A4 三態覆蓋**§7 已定義空 / 載入 / 無結果 / 無權限 / 失敗多態。
- **De-A5 觸控目標**§9 已標註新增互動元素 ≥ 44px 需求 + `⋮` 鈕注意事項。
- **De-B1 互動五狀態 / De-B2 Dark Mode / De-B4 三方互審**:延到 milestone互動狀態沿用既有元件既有態Dark Mode 零新 token 自動生效;三方互審即本檔 §11 待 PM/Architect 回審)。
- **No silent failures**:所有 🔶🔷 未定項已明確標「待 PM/Architect 確認」+ 對應假設無「TBD 無負責人」。
- **Doc 同步**本檔為新增pages.md / components.md 的增補建議見下方回報(本任務未直接改,避免撞名覆蓋,交 Orchestrator 決定增補方式)。

View File

@ -62,6 +62,18 @@
- **下載對接權威規格v1.3query-string token + redirect、推翻舊「fetch + Bearer」、解 CORS 405** → [`adr/adr-017-model-library-access.md` §11](adr/adr-017-model-library-access.md#11-v13瀏覽器下載改-query-string-token--redirect推翻決策-2--104-的fetch--bearer) - **下載對接權威規格v1.3query-string token + redirect、推翻舊「fetch + Bearer」、解 CORS 405** → [`adr/adr-017-model-library-access.md` §11](adr/adr-017-model-library-access.md#11-v13瀏覽器下載改-query-string-token--redirect推翻決策-2--104-的fetch--bearer)
- CORS 405 根因定位(個人層)→ `.autoflow/04-architecture/download-cors-405-diagnosis.md` - CORS 405 根因定位(個人層)→ `.autoflow/04-architecture/download-cors-405-diagnosis.md`
### 9.6 模型共享Model Sharing — 公開對象 + 共享模型庫)
- **TDD**(資料模型 visibility 欄 / 權限可見性 query / 相容性 / 並行化計畫) → [`feature-model-sharing-tdd.md`](feature-model-sharing-tdd.md)
- **API 規格**`GET /api/models/library` 共享庫列表 / `GET /api/models/:id/profile` / `PATCH /api/models/:id/visibility` → [`api/api-model-sharing.md`](api/api-model-sharing.md)
- 沿用 ADR-017 決策 3 的 `model_shares`(點對點分享);本功能新增 `models.visibility` enumprivate/tenant/public作正交的「廣播」維度
- ⚠️ 放寬既有 owner-only 邊界download 端點權限改動 + enumeration 防護)— 建議整體送 security agent 審
### 9.7 個人設備管理 — 設備註冊 / 取消註冊P0
- **TDD**(註冊 API / 取消註冊 vs unpair 區分 / 三態分色 / 排序 filter / 安全 / 並行化計畫) → [`feature-device-mgmt-tdd.md`](feature-device-mgmt-tdd.md)
- **API 規格**`POST /api/devices/:id/register` / `POST /api/devices/:id/unregister`UUID 識別) → [`api/api-device-mgmt.md`](api/api-device-mgmt.md)
- 沿用 ADR-018 走向 A' 模型(`registered_at` 註冊軸、欄位已存在 → **不需 migration**
- ⚠️ **取消註冊 = 退回未註冊(清 registered_at、保留列與 unpair軟刪+撤 token分開**;使用者 2026-08-02 重新拍板,覆蓋 ADR-018 §1.1 Q5「實體刪除」
### 10. 前端資料流與狀態管理 ### 10. 前端資料流與狀態管理
- 見 §10本文件 - 見 §10本文件

View File

@ -0,0 +1,190 @@
# ADR-020: vendor ffmpeg 加回 camera input deviceindev— 三平台 avfoundation / dshow / v4l2
## 狀態
Proposed。
> 待「三平台 ffmpeg rebuild / 驗證 + `buildCaptureArgs` 補 Linux 分支 + 三平台實機驗(需實體攝影機)」全數完成後,轉 Accepted。
> 使用者目前僅有 macOS 機器可測Windows / Linux 標為「待實機驗證」。
## 日期
2026-08-02
## 作者
Architect Agent
## 相關
- 根因評估:`local-tool/.autoflow/05-implementation/camera-ffmpeg-avfoundation-eval.md`
- 打破的前提v2 TDD §2decoder-only ffmpeg 決策)、`vendor/ffmpeg/macos/BUILD.md`
- 相關實作:`local-tool/server/internal/camera/ffmpeg_camera.go``ffmpeg_detect.go`
---
## 1. 背景與範圍 (Context)
### 1.1 觸發問題
camera 即時推論在真機報錯camera 修 bug 讓「吞錯誤」不再發生後,底層錯誤終於浮現):
```
failed to open camera (index=0): camera did not start:
ffmpeg exited before producing a frame: ffmpeg stream ended: EOF
...
Unknown input format: 'avfoundation'
Error opening input file 0:none
```
`Unknown input format: 'avfoundation'` = 這顆 ffmpeg binary **沒有編進 avfoundation input deviceindev**
### 1.2 根因(已 100% 確認,非推測)
camera 抓實體攝影機是 **Go 端 `os/exec` 起 ffmpeg subprocess**(非 Python指令形如 `ffmpeg -f avfoundation -i "0:none" ... -f image2pipe -vcodec mjpeg -`ffmpeg 把攝影機輸出成連續 MJPEG stream 到 stdoutGo 端掃 JPEG SOI/EOI marker 切 frame。
問題在 vendor 的 macOS ffmpeg 是「decoder-only」自 buildv2 TDD §2 決策、`vendor/ffmpeg/macos/BUILD.md`configure flags 為縮體積 `--disable-everything`,白名單只 re-enable 了「解碼既有檔案」需要的元件:
```
--disable-everything
--enable-protocol=file,pipe
--enable-demuxer=mov,avi,mpegps,mpegts,matroska,image2
--enable-decoder=h264,hevc,...,mjpeg,...
--enable-parser=... --enable-filter=... --enable-muxer=image2pipe,image2,null
--enable-encoder=mjpeg
--disable-network
```
**白名單裡完全沒有任何 `--enable-indev=...`。** `--disable-everything` 一併關掉所有 input device而白名單沒把 camera 需要的 indev 加回來 → `avfoundation` 認不得 → camera 開不了。
### 1.3 為什麼只有 camera 壞、影片 / 圖片 / 批次正常
| 功能 | 用到的 ffmpeg 能力 | 白名單有無 | 結果 |
|------|-------------------|-----------|------|
| 影片 / 圖片 / 批次推論 | demuxer + decoder解碼**既有檔案** | ✅ 有 | 正常 |
| camera 即時推論 | **indev**(從實體裝置抓 raw frame | ❌ 沒有 | 壞 |
這是純「build 白名單漏了 indev」的問題**不是程式碼邏輯錯**。
### 1.4 跨平台現況(讀 code 確認)
camera 三平台共用同一套 MJPEG pipe 抓取架構,但每平台用不同 indev
| 平台 | 抓取 indev | ffmpeg 來源 | ffmpeg indev 現況 | camera code 現況 |
|------|-----------|------------|------------------|-----------------|
| macOS | `avfoundation` | 自 build decoder-only | ❌ 缺 avfoundation根因 | ✅ 完整 |
| Windows | `dshow` | BtbN n7.1 完整 LGPL build | ⚠️ 大概率已含 dshow**待實機驗** | ✅ 完整dshow 路徑齊全) |
| Linux | `v4l2`(應為) | BtbN n7.1 完整 LGPL build | ⚠️ 大概率已含 v4l2**待實機驗** | ❌ 缺 Linux 分支 |
**附帶 code bug獨立於 ffmpeg**`buildCaptureArgs``ffmpeg_camera.go`)的 `switch runtime.GOOS` 只有 `windows``default``default` 分支用 avfoundation。**Linux 會誤落 `default` → 對 Linux 攝影機用 avfoundation → 必錯**。即使 macOS ffmpeg 修好Linux camera 仍會壞。
---
## 2. 決策 (Decision)
採**方案 Arebuild / 驗證三平台 ffmpeg 加回對應 indev並補 Linux camera code 分支**。
### 2.1 三平台 ffmpeg indev
| 平台 | 動作 | 具體 |
|------|------|------|
| macOS | rebuild 加 indev | `vendor-ffmpeg-macos-build` 的 configure 加 `--enable-indev=avfoundation`rebuild重算 sha256更新 `BUILD.md`commit 新 binary |
| Windows | 驗證(大概率零改動) | 實機 `ffmpeg -f dshow -list_devices true -i dummy` 確認 BtbN build 已含 dshow**若缺**才需換含 dshow 的 build |
| Linux | 驗證(大概率零改動) | 實機 `ffmpeg -devices` 確認 BtbN build 已含 v4l2**若缺**才需換 build 或補 `--enable-indev=v4l2` |
> BtbN 官方 LGPL 完整 build`win64-lgpl` / `linux64-lgpl`)預設含 dshow / v4l2 indev故 Windows / Linux **預期零 ffmpeg 改動、只需實機驗證**。macOS 是自 build 精簡版,是唯一確定要 rebuild 的。
### 2.2 補 Linux camera code 分支(獨立必補項)
`buildCaptureArgs``ffmpeg_camera.go`)與 `ListFFmpegDevices``ffmpeg_detect.go`)補 Linux 分支,三平台 capture args 對照:
| 平台 | 列裝置 | 抓 framecapture args |
|------|--------|-------------------------|
| macOS | `-f avfoundation -list_devices true -i ""` | `-f avfoundation -i "<index>:none"` |
| Windows | `-f dshow -list_devices true -i dummy` | `-f dshow -i video="<name>"` |
| Linux新增 | `-f v4l2 -list_devices true -i ""` 或列舉 `/dev/video*` | `-f v4l2 -i /dev/video<N>` |
> 三平台後段皆接 `-f image2pipe -vcodec mjpeg -q:v 5 -an -`MJPEG pipe 架構不動。
---
## 3. 考慮過的替代方案 (Alternatives Considered)
| 方案 | 優點 | 缺點 | 排除原因 |
|------|------|------|---------|
| **A. rebuild 加 indev採用** | 成本最低macOS 加一行 flag不推翻 decoder-onlyLGPL 乾淨;架構不動 | 需 rebuild macOS binary + 補 Linux code 分支 | — |
| B. 換完整版 ffmpeg | 省事、一次到位所有格式 | 體積爆增4070MB vs 現 5.7MB);推翻 v2 TDD §2 decoder-onlymacOS 無現成 LGPL static 完整 build正是當初自 build 的理由) | 唯一好處「省事」在 A 只加一個 flag 前提下不成立 |
| C. camera 改用平台原生 APIAVFoundation / Media Foundation / V4L2 ioctl + cgo | 不依賴 ffmpeg subprocess | 三平台各寫一套原生 + cgo複雜度爆炸 = 重寫 camera 子系統 | 解「build flag 少一行」不該砍掉可用的抓取架構 |
| D. macOS 專用第二顆含 avfoundation 的 ffmpeg | 主 binary 維持純解碼 | 多一顆 binary + 兩套 build 維護 | A 加一個 flag 就能讓同一顆 binary 兼顧indev 增量 < 0.5MBD 無意義 |
---
## 4. 後果 (Consequences)
### 4.1 打破的既有前提
本 ADR **打破 v2 TDD §2 decoder-only「不含 indev」的前提**。原決策的假設是「local-tool 只處理本地檔案解碼」,但 camera 即時推論需要從實體裝置抓 frameindev該假設對 camera 情境不成立。本 ADR 確立新契約:
> **camera indev 是 ffmpeg build 白名單的必要一部分。** 未來升級 ffmpeg 版本 rebuild 時,三平台的 indevavfoundation / dshow / v4l2不得再遺漏。
### 4.2 體積影響
| 平台 | 增量 | 說明 |
|------|------|------|
| macOS | **< 0.5 MB** 5.7MB | avfoundation indev 是薄封裝呼叫系統 AVFoundation / CoreMedia framework`otool -L` 已顯示 binary link 這些 framework不自帶 codec |
| Windows / Linux | 0若零改動 | BtbN build 已含 indev不重 build |
### 4.3 LGPL 合規
- avfoundation / dshow / v4l2 **皆為 LGPL-safe indev不引入任何 GPL 元件**
- macOS 維持 `--enable-version3`LGPL v3rebuild 後仍須通過既有驗證(`ffmpeg -version` 不含 `--enable-gpl` / `libx264` / `libx265`)。
- Windows / Linux 沿用 BtbN LGPL build合規不變。
### 4.4 三平台 indev 對照(契約,供未來 rebuild 參照)
| 平台 | indev | ffmpeg 來源 | build 需求 |
|------|-------|------------|-----------|
| macOS | avfoundation | 自 build decoder-only | configure 白名單須含 `--enable-indev=avfoundation` |
| Windows | dshow | BtbN 完整 LGPL build | 沿用 upstream含 dshow |
| Linux | v4l2 | BtbN 完整 LGPL build | 沿用 upstream含 v4l2 |
### 4.5 風險
- 低。macOS 改的是 build flag + 補一個 OS 分支Linux code不動 MJPEG pipe 抓取架構。
- 主要不確定性在「Windows / Linux BtbN build 是否已含 indev」透過實機 `-list_devices` / `-devices` 驗證即可消除;若不含則退化為「換 build」+1h
---
## 5. 合規性 Checklist
實作與轉 Accepted 前須全數完成:
- [ ] **macOS ffmpeg rebuild**configure 加 `--enable-indev=avfoundation`、rebuild、重算 sha256、更新 `vendor/ffmpeg/macos/BUILD.md`(含新 configure line / sha256 / 大小、commit 新 binary
- [ ] **macOS LGPL 驗證**rebuild 後 `ffmpeg -version` 不含 `--enable-gpl` / `libx264` / `libx265``ffmpeg -devices` 列出 avfoundation
- [ ] **Windows dshow 驗證**:實機 `ffmpeg -f dshow -list_devices true -i dummy` 有列裝置(若缺 → 換含 dshow 的 build
- [ ] **Linux v4l2 驗證**:實機 `ffmpeg -devices` 含 v4l2若缺 → 換 build 或補 flag
- [ ] **`buildCaptureArgs` 補 Linux 分支**`ffmpeg_camera.go` + `ffmpeg_detect.go``case "linux": -f v4l2`(列裝置 + 抓 frame
- [ ] **三平台實機驗**各平台接實體攝影機camera 即時推論從「開不了」變「出 frame」
- [ ] **體積 / LGPL 確認**macOS binary 增量 < 0.5MBLGPL v3 合規未破
- [ ] **v2 TDD ffmpeg 章節交叉引用**增補不覆蓋註記「camera indev 為 build 白名單必要部分、三平台 indev 對照見 ADR-020」
- [ ] 與 Tech Lead / 使用者確認
- [ ] 成本影響已評估(體積增量微、無新增基礎設施成本)
---
## 6. WP 拆解(供 Orchestrator 排期)
| WP | 內容 | 派誰 | 需實機 | 前置 | 估時 |
|----|------|------|--------|------|------|
| WP-1 | macOSconfigure 加 `--enable-indev=avfoundation`、rebuild、重算 sha256、更新 BUILD.md、commit binary | devopsbuild/vendor| macOS使用者有 | — | 11.5h(含 ~3 分 rebuild |
| WP-2 | `buildCaptureArgs` / `ListFFmpegDevices` 補 Linux v4l2 分支(三平台 capture args 對照) | backendGo camera code | 否可先寫、Linux 驗證階段測) | — | 11.5h |
| WP-3 | Windows實機 `-f dshow -list_devices` 驗證 dshow若缺才換 build | devops | Windows**待實機** | — | 0.5h(順利)/ +1h換 build |
| WP-4 | Linux實機驗證 v4l2 + camera 即時推論測試 | devops + backend | Linux**待實機** | WP-2 | 11.5h |
| WP-5 | v2 TDD ffmpeg 章節交叉引用增補ADR-020 轉 Accepted | architect | 否(待 WP-1~4 完成) | WP-1~4 | 0.5h |
> **平行 / 接力**WP-1macOS ffmpeg與 WP-2Linux code可**平行**互不相依、不同檔。WP-3Windows 驗獨立可平行。WP-4Linux 驗)**接力 WP-2**。WP-5 收尾,等前四個。
> **關鍵路徑**WP-2 → WP-4 → WP-5Linux 驗需先有 code 分支)。
> **使用者只有 macOS**WP-1 可立即做並驗證WP-3 / WP-4 的實機驗證需 Windows / Linux 機器,標為「待實機」,先寫好 code / build 再擇機驗。
---
## 7. 等級與後續
- **等級**M 級(跨多檔 + 跨三平台 + 動 vendor build 策略,但非新 user story / 新架構)。
- **後續**ADR 定稿後由 Orchestrator 派 devopsffmpeg rebuild / 驗證)+ backendLinux camera code 分支落地。camera 不阻擋 B+C 主線,可獨立排期。

View File

@ -0,0 +1,131 @@
# API 規格 — 設備註冊 / 取消註冊(個人設備管理 P0
- **上位**[`../feature-device-mgmt-tdd.md`](../feature-device-mgmt-tdd.md)、[`api-spec.md`](api-spec.md) §3 Devices
- **狀態**Draft契約contract-first single source of truth
- **最後更新**2026-08-02
> 本檔為 register / unregister 兩個新端點的權威契約。前端對此形狀寫 normalize、後端對此形狀保證回應。既有 `GET /api/devices``GET /api/devices/:id``POST /api/devices/:id/unpair``api-spec.md` §3、**本 P0 不改**。
---
## 通用
- **認證**JWT/OIDC bearer同既有 `/api/devices/*` route group
- **識別值**`:id` = device UUID雲端 DB 主鍵。register/unregister 是純雲端 DB 操作、不路由到 local agent故用 UUID對齊 ADR-018 FE-ADB 操作用 UUID、路由操作才用 serial
- **回應信封**:沿用既有 `{ "success": true, "data": ... }`(前端 api client 已 unwrap `data`)。錯誤為 `{ "success": false, "error": { "code", "message" } }`
### DeviceListItem回應 data 形狀沿用既有snake_case
register/unregister 成功皆回**更新後的單筆 DeviceListItem**(與 `GET /api/devices/:id` 同形狀):
```json
{
"id": "uuid",
"name": "KL520 #A1B2",
"device_type": "kl520",
"serial_number": "0x1234ABCD",
"agent_id": "uuid",
"registered_at": "2026-08-02T10:00:00Z",
"remote_status": "online",
"last_seen_at": "2026-08-02T10:00:00Z",
"last_connected_at": null,
"status": "connected",
"tunnel_online": true,
"created_at": "2026-08-01T00:00:00Z",
"updated_at": "2026-08-02T10:00:00Z"
}
```
- `registered_at`register 後為 ISO 8601 時間unregister 後為 `null`omitempty → 欄位可能不出現,前端 normalize 皆視為 null=未註冊)。
---
## 1. `POST /api/devices/:id/register` — 註冊設備
把裝置從「未註冊」翻成「已註冊」(`registered_at` NULL → now())。
- **Request body**:無。
- **Success**`200 OK` + 更新後 DeviceListItem`registered_at` 非 null
### 行為順序(後端)
| 步驟 | 條件 | 回應 |
|------|------|------|
| 1 | 缺 UserContext | 500 `INTERNAL_ERROR` |
| 2 | `:id` 空 | 400 `VALIDATION_FAILED` |
| 3 | device 不存在 / 已軟刪 | 404 `NOT_FOUND` |
| 4 | `owner_user_id != caller` | 403 `FORBIDDEN`IDOR 防護) |
| 5 | `is_representative == true` | 409 `CONFLICT`(代表 device 不可註冊) |
| 6 | `registered_at != null`(已註冊) | 409 `ALREADY_REGISTERED` |
| 7 | 正常 | 200 + DeviceListItem |
### 範例
```
POST /api/devices/7f3a.../register
→ 200 { "success": true, "data": { ...,"registered_at":"2026-08-02T10:00:00Z" } }
(已註冊再打)
→ 409 { "success": false, "error": { "code":"ALREADY_REGISTERED","message":"device already registered" } }
```
---
## 2. `POST /api/devices/:id/unregister` — 取消註冊(退回未註冊)
把裝置退回「未註冊」(`registered_at` → NULL**保留裝置列**、不軟刪、不撤 token。
> ⚠️ **與 `unpair` 完全不同**unpair 是軟刪整台 + cascade 撤 tokendevice 從清單消失unregister 只清 `registered_at`device 仍在清單、顯示為未註冊)。兩端點各走各的,不可合併。詳見 TDD §1。
- **Request body**:無。
- **Success**`200 OK` + 更新後 DeviceListItem`registered_at` = null
### 行為順序(後端)
| 步驟 | 條件 | 回應 |
|------|------|------|
| 1 | 缺 UserContext | 500 `INTERNAL_ERROR` |
| 2 | `:id` 空 | 400 `VALIDATION_FAILED` |
| 3 | device 不存在 / 已軟刪 | 404 `NOT_FOUND` |
| 4 | `owner_user_id != caller` | 403 `FORBIDDEN` |
| 5 | `is_representative == true` | 409 `CONFLICT` |
| 6 | 已是未註冊(`registered_at == null` | **200**(冪等 no-op非錯誤 |
| 7 | 正常 | 200 + DeviceListItemregistered_at=null |
### 範例
```
POST /api/devices/7f3a.../unregister
→ 200 { "success": true, "data": { ...,"registered_at": null } }
device 仍存在於 GET /api/devices
```
---
## 3. 錯誤碼
| code | HTTP | 情境 | 新增? |
|------|------|------|-------|
| `VALIDATION_FAILED` | 400 | id 空 | 既有 |
| `FORBIDDEN` | 403 | 非 ownerIDOR | 既有 |
| `NOT_FOUND` | 404 | device 不存在/軟刪 | 既有 |
| `CONFLICT` | 409 | representative device 不可註冊/取消 | 既有(或沿用既有 CONFLICT 碼) |
| `ALREADY_REGISTERED` | 409 | register 時已註冊 | **新增** |
| `INTERNAL_ERROR` | 500 | 缺 UserContext / DB 錯 | 既有DB down 經 errors.go 映射 503 |
- `ALREADY_REGISTERED` 為本功能新增錯誤碼,需在後端 errors 常數 + 前端 i18n`devices.register.error.alreadyRegistered`)同步登記。
- representative 衝突若不想新增碼,可沿用既有 `CONFLICT` + message 區分;由 backend agent 依既有錯誤碼慣例定,前端據 message/context 提示。
---
## 4. 前端呼叫(對齊 device-store.ts 範式)
```
// 皆用 UUIDDB 操作);不帶 serial。
registerDevice(id): POST /api/devices/{id}/register → 更新 store 該筆 registeredAt
unregisterDevice(id): POST /api/devices/{id}/unregister → 清 store 該筆 registeredAt保留列
```
- 成功後就地更新 store 對應 device 的 `registeredAt`(避免 refetch 延遲,比照既有 unpairDevice 就地移除的範式),或 refetch 該筆。
- 409 `ALREADY_REGISTERED` → toast「已註冊」+ refetch。
- **不像 unpair 從 list 移除**——unregister 只改欄、device 留在 list。

View File

@ -0,0 +1,186 @@
# API Spec — 模型共享Model Sharing
> 對齊 `feature-model-sharing-tdd.md`。base URL / envelope / 認證同 `api-spec.md`§0
> 通用回應:`{ "success": true, "data": {...} }` / `{ "success": false, "error": { "code", "message" } }`
> 認證cookie sessionBFF`api-spec.md`handler 從 `UserContext``userID` / `roles` / `orgId`
>
> **狀態**Draft待三方互審。標 `[需 PM 確認]` / `[需 Design 確認]` / `[需 security 審]` 的點見各節。
---
## 0. 名詞與前提
| 名詞 | 定義 |
|------|------|
| **visibility** | 模型的「公開對象」維度(廣播式):`private`(僅擁有者)/ `tenant`(同租戶可見)/ `public`(全平台可見)。存在 `models.visibility` 欄。 |
| **model_shares** | 「點對點分享給特定人」維度ADR-017 決策 3 已定義,本功能沿用、不重造)。與 visibility 正交。 |
| **共享模型庫** | 「依身份權限的可見模型列表」= 我的 公開(public) 同租戶(tenant 且同 org) 分享給我(model_shares) preset。 |
| **tenant / org** | 沿用既有 `users.org_id`migration 0001 已有欄位、domain 尚未使用,見 TDD §2`tenant` visibility = 同 `org_id` 可見。`[需 PM 確認]` org 邊界語意。 |
> **與既有 `/api/models` 的分工**:既有 `GET /api/models``api-spec.md §4`)維持「**我的模型 + preset**」語意不變owner dashboard 用),**不動**。共享模型庫是**新端點** `GET /api/models/library`(跨擁有者、依權限可見)。兩者刻意分開,避免破壞既有 owner dashboard 行為。`[需 Design 確認]` UI 是否分兩個入口。
---
## 1. GET `/api/models/library` — 共享模型庫列表(依身份權限可見)
依當前 user 身份,回「可見模型」的分頁列表。可見範圍 = 我的 public (tenant 且同 org) 分享給我 preset。
### Query 參數
| 參數 | 型別 | 預設 | 說明 |
|------|------|------|------|
| `cursor` | string | — | 分頁游標(不透明 base64。首頁不帶。見 §1.3 分頁契約。 |
| `limit` | int | 20 | 每頁筆數1100超出 clamp。 |
| `sort` | enum | `created_at` | 排序欄位:`created_at` / `name` / `file_size`。 |
| `order` | enum | `desc` | `asc` / `desc`。 |
| `q` | string | — | 搜尋關鍵字(比對 `name` + `description`ILIKE 前綴/包含,見 TDD §5 效能)。 |
| `target_chip` | enum | — | filter`kl520`/`kl720`/`kl630`/`kl730`。 |
| `source` | enum | — | filter`uploaded`/`converted`/`preset`。 |
| `visibility` | enum | — | filter`public`/`tenant`(僅過濾廣播類;`private` 不在共享庫語意內、傳了視為忽略)。 |
| `owned` | bool | — | `true`=只看我的、`false`=只看別人分享/公開給我的;不帶=全部可見。 |
### Response 200envelope `data`
```json
{
"items": [
{
"id": "uuid",
"name": "yolov5s-kl520",
"description": "...",
"target_chip": "kl520",
"file_size": 10485760,
"source": "converted",
"status": "ready",
"visibility": "public",
"owner": { "id": "uuid", "name": "Alice", "is_me": false },
"shared_with_me": false,
"my_access": "viewer",
"created_at": "2026-07-01T00:00:00Z",
"updated_at": "2026-07-02T00:00:00Z"
}
],
"next_cursor": "eyJ...",
"has_more": true
}
```
**欄位說明(權限相關,重要)**
- `owner`:只揭露 `id` / `name` / `is_me`**不揭露 owner email**——非擁有者不該看到他人 email見 §4 安全)。`[需 security 審]`
- `my_access`:當前 user 對此模型的有效權限(`owner`/`editor`/`viewer`。由「owner_user_id 命中 / model_shares.role / visibility 落 viewer」計算取最高。
- `shared_with_me`:是否因 model_shares 命中(供 UI 標「已分享給我」)。`[需 Design 確認]` 標示方式。
- `status`:沿用既有 DTO`pending`/`ready``uploaded_at` 決定,見 `models.go toModelResponse`)。**共享庫只列 `ready`**(未 finalize 的不進共享庫,見 TDD §5
### 錯誤碼
| HTTP | code | 情境 |
|------|------|------|
| 400 | `VALIDATION_FAILED` | limit / cursor / sort 非法。 |
| 401 | middleware | 未登入。 |
| 500/503 | `INTERNAL_ERROR` | DB 錯誤(經 `WriteDBError` 映射,不洩漏 raw。 |
---
## 2. GET `/api/models/:id/profile` — 模型 profile 頁(公開版詳情)
非擁有者也可看的模型詳情頁。**權限檢查為核心**:只有「對此 model 有可見性」的 user 能看,且回傳欄位依身份裁剪。
### 可見性判斷handler 第一步)
user 能看此 profile ⟺ 命中任一:
1. `owner_user_id == userID`(我的)
2. `visibility == public`
3. `visibility == tenant` 且 model.owner.org_id == user.org_id
4. `model_shares` 命中grantee == userID
5. preset 模型(公用)
不命中 → **404**(不是 403不揭露「這個 id 存在但你沒權限」,避免 enumeration 洩漏存在性。`[需 security 審]` 404 vs 403 策略。
### Response 200envelope `data`
```json
{
"id": "uuid",
"name": "...",
"description": "...",
"target_chip": "kl520",
"file_size": 10485760,
"source": "converted",
"status": "ready",
"visibility": "public",
"input_shape": [1, 3, 224, 224],
"classes": ["cat", "dog"],
"framework": "onnx",
"owner": { "id": "uuid", "name": "Alice", "is_me": false },
"my_access": "viewer",
"can_download": true,
"created_at": "...",
"updated_at": "...",
"uploaded_at": "..."
}
```
**依身份裁剪的欄位(重要,`[需 security 審]`**
- **絕不揭露**(無論身份):`storage_key``faa_object_key`FAA 內部 keyADR-017 決策 2 防曝露、owner email、`file_checksum``[需 PM 確認]` checksum 是否對非 owner 公開)。
- `can_download``my_access != none` 時 true。前端據此決定是否顯示下載鈕。實際下載仍走既有 `GET /api/models/:id/download`(該端點需同步加共享權限檢查,見 §3
### 錯誤碼
| HTTP | code | 情境 |
|------|------|------|
| 404 | `NOT_FOUND` | model 不存在 **或** 無可見性(合併,防 enumeration。 |
---
## 3. PATCH `/api/models/:id/visibility` — 設定公開對象
擁有者設定模型的公開對象。**只有 owner或 editor`[需 PM 確認]`)能改**。
### Request
```json
{ "visibility": "public" }
```
- `visibility`required`private` / `tenant` / `public`
- `tenant` 但 user 無 `org_id` → 400無租戶歸屬不能設 tenant 可見)。`[需 PM 確認]` demo/無 org user 的行為。
### Response 200envelope `data`
```json
{ "id": "uuid", "visibility": "public", "updated_at": "..." }
```
### 錯誤碼
| HTTP | code | 情境 |
|------|------|------|
| 400 | `VALIDATION_FAILED` | visibility 非法 / 設 tenant 但無 org。 |
| 403 | `FORBIDDEN` | 非 owner。 |
| 404 | `NOT_FOUND` | model 不存在。 |
| 409 | `CONFLICT` | model 未 `ready`(未 finalize 不允許公開,`[需 PM 確認]`)。 |
> **既有 `GET /api/models/:id/download` 的連帶變更(見 TDD §5**:目前是 owner-only`m.OwnerUserID != userID → 403`)。共享後需改為「有可見性且 `can_download` → 放行」。此變更**涉及 auth 繞過風險**`[需 security 審]`
---
## 4. 安全考量摘要(詳見 TDD §6
| 風險 | 對策 |
|------|------|
| IDOR / enumeration猜 id 看他人 model | profile / download 的可見性檢查在 handler 第一步;不命中回 404不回 403 揭露存在性)。`[需 security 審]` |
| 越權改 visibility | PATCH visibility 強制 owner 檢查。 |
| 資訊洩漏email / storage_key / faa_object_key | DTO 白名單序列化,內部 key 用 `json:"-"`(沿用 `model.Model.FAAObjectKey` 既有做法)。 |
| tenant 邊界誤放 | tenant 可見性需 `model.owner.org_id == user.org_id` 且兩者皆非空;空 org 不落 tenant 可見。 |
| 下載端點權限繞過 | `GET /api/models/:id/download` 的 owner-only 改共享權限檢查,須與 profile 可見性邏輯**共用同一函式**single source of truth避免兩處判斷不一致`[需 security 審]` |
---
## 5. 待三方確認清單(本檔)
| # | 項目 | 待誰 |
|---|------|------|
| S1 | org/tenant 邊界語意org_id 是否等於租戶;跨 org 是否可能) | PM |
| S2 | editor 是否能改 visibility / 未 ready 能否公開 / checksum 是否對非 owner 公開 | PM |
| S3 | 共享庫 UI 入口(與既有「我的模型」分頁 or 合併 tab`shared_with_me` 標示、profile 頁欄位 | Design |
| S4 | 404 vs 403 防 enumeration、download 端點權限改動、owner email 不揭露 | security |

View File

@ -0,0 +1,356 @@
# 技術設計文件TDD— 個人設備管理 / 設備與註冊P0
- **作者**Architect Agent
- **狀態**Draft待實作 + 三方交叉審閱)
- **最後更新**2026-08-02
- **上位文件**[`adr/adr-018-agent-device-model.md`](adr/adr-018-agent-device-model.md)(走向 A' 模型)、[`database.md`](database.md)、[`api/api-spec.md`](api/api-spec.md)
- **權威輸入**`.autoflow/05-implementation/device-mgmt-gap-analysis.md`(缺口盤點)
- **讀者**Backend / Frontend / Testing Agents
- **API 規格**[`api/api-device-mgmt.md`](api/api-device-mgmt.md)
---
## 0. 一句話與範圍
「取序號 → 上報 → 存 serial」地基ADR-018 WP-0/WP-B已完整落地`registered_at` 欄可讀寫、List/Get API 已回傳,但**整條「註冊」語意軸沒接上**:沒有任何路徑能把 `registered_at` 由 NULL 翻成有值,前端 store 也把後端回的 `registered_at` 丟掉。本 TDD 補完 P0 範圍:
| P0 細項 | 對應 work-stream | 一句話 |
|---------|-----------------|--------|
| 1. 註冊 API | WS-BE | `POST /api/devices/:id/register``registered_at` NULL→now() |
| 2. 已註冊檢查 | WS-BE | register 端點對「已註冊」回衝突(冪等取捨見 §3.2 |
| 3. 取消註冊(退回未註冊) | WS-BE | `POST /api/devices/:id/unregister` → 清 `registered_at`、**保留列**,與 unpair 分開 |
| 4. 三態分色 | WS-FE | 前端 store 補 `registeredAt` 欄 + 三態運算 + 配色 |
| 5. 排序 + filter | WS-FE | 排序切換(名稱/狀態/註冊時間)+ 依三態 filter + UI |
| 6. 資料模型 | WS-BE | 確認欄位現況、**判定不需 migration** |
| 7. 安全 | WS-BE | register/unregister 的 owner 檢查 + IDOR 防護 |
**非目標(本 TDD 不做)**
- 不改既有 `unpair`devices.go:256-346 軟刪 + cascade的任何行為。
- 不做批次註冊 / 自動註冊Q3 已定手動逐顆註冊)。
- 不動 serial 路由 / agents 表 / session_tokens。
- 不寫 production code、不寫 migration 實檔(本文件只給設計方向與契約)。
---
## 1. 關鍵決策:**取消註冊 ≠ unpair**(使用者已拍板,覆蓋 ADR-018 §1.1 Q5
> ⚠️ **這是本 TDD 最容易做壞的地方,工程師務必先讀懂再動手。**
系統裡有**兩個語意不同、絕不可合併**的「移除類」動作:
| 動作 | 端點 | 對 `registered_at` | 對 device 列 | 對 tokenpairing/session | 語意 |
|------|------|-------------------|-------------|---------------------------|------|
| **取消註冊**(本 TDD 新增) | `POST /api/devices/:id/unregister` | 清成 NULL | **保留**(列還在、仍在清單顯示為「未註冊」) | **不動** | 「這顆 USB 退回未註冊狀態,但我還看得到它」 |
| **unpair / 移除裝置**(既有,不動) | `POST /api/devices/:id/unpair` | 不特別處理(連列都軟刪了) | **軟刪**`deleted_at=now()`、從清單消失) | **cascade 撤銷**DeviceUnpairer | 「把整顆已配對裝置移除、斷開連線」 |
### 1.1 為什麼與 ADR-018 §1.1 不同(決策沿革)
ADR-018 §1.1 記載的 Q5 是「取消註冊 = 真實體刪除 + 清 session_tokens FK」。**使用者在本次2026-08-02重新拍板為「取消註冊 = 退回未註冊、保留裝置列、不硬刪」**,理由:實體刪除會讓使用者「取消註冊後就再也看不到那顆已插著的 USB」體驗不合理而「退回未註冊」讓已連接的 USB 仍留在清單、可再次註冊,符合三態模型(未註冊態就是要能看到的第三態)。
- ADR-018 為不可變決策紀錄、其 §1.1 原文不改;本 TDD 於此明載新決策為準。**實作以本 TDD 為準。**
- 「真實體刪除 + 清 session_tokens」的語意已由**既有 unpair** 覆蓋(雖是軟刪、但那是既有行為、本 TDD 不動)。取消註冊是全新的第三種較輕動作。
### 1.2 工程師落地時的三條紅線
1. **不要改 `devicesUnpairHandler`**devices.go:256-346。unregister 是**新 handler、新端點**,與 unpair 各走各的。
2. **unregister 不呼叫 `DeviceUnpairer`、不呼叫 `Delete/DeleteTx`、不碰 token。** 它只做一件事:把該列 `registered_at` set NULL。
3. **register / unregister 對 representative device 一律拒絕**(見 §7.3)——只有真實 USB`is_representative=false`)能被註冊 / 取消註冊。
---
## 2. 現況地基盤點(實作前必讀,避免重造輪子)
| 元件 | 現況 | 檔案:行 | 對本 TDD 的意義 |
|------|------|--------|----------------|
| `Device.RegisteredAt *time.Time` | 已存在domain | `device.go:83` | 直接用,不加欄 |
| DB 欄 `registered_at TIMESTAMPTZ`nullable | 已存在0005 | `migrations/0005_create_agents.up.sql:38` | 不需 migration |
| index `idx_devices_registered` | 已存在partial未刪除 | 同上:44 | filter「未註冊」可用本 P0 前端 client-side filter 為主index 供未來後端 filter |
| `scanDevice``registered_at` | 已接nullable *time.Time | `postgres_repository.go:391` | 讀取路徑完整 |
| `Save` upsert 寫 `registered_at` | 已接(`$15 = d.RegisteredAt` | `postgres_repository.go:268,286` | **register 可直接走 Save/SaveTx**、不必新寫 SQL但見 §3.3 建議加專用 UPDATE |
| List/Get API 回 `registered_at` | 已接 | `devices.go:65,129,220` | 後端→前端資料已備妥 |
| `is_representative=false` filter | List 已濾(只列真 USB | `device.go:190` / postgres List | register/unregister 端點另需在 handler 再擋一次 representative縱深 |
| owner 檢查範式 | 既有 handler 皆用 `UserContextFrom` + `d.OwnerUserID != userID → 403` | `devices.go:197-201,299-302` | register/unregister 照抄這個範式 |
| 前端 `DeviceSummary.registeredAt` | **不存在**(資料到前端被丟) | `device-store.ts:74-96` | WS-FE 第一件事:補欄 + normalize |
**結論**:後端 register/unregister 是「加 2 個 handler + 2 條 route + 1 個 repo 專用方法(可選)」;前端三態是「補 1 個欄位 + 三態運算 + 配色」。地基全在,只差翻轉開關與前端消費。
---
## 3. 註冊 API細項 1 + 2 + 6— WS-BE
### 3.1 端點:`POST /api/devices/:id/register`
- **路由註冊位置**`registerDeviceRoutes`devices.go:27-49與既有 unpair 並列(都是純雲端 DB 操作、用 UUID `:id`、不 proxy
- **識別值**UUID`:id`)。理由(對齊 ADR-018 FE-Aregister 是純雲端 DB 操作(只翻 `registered_at`、不路由到 local agent用主鍵 UUID 最自然;且序號可能為空的 device 也要能被查詢與擋掉。
- **Request body**:無(空 body
- **行為**
1. `UserContextFrom` 取 userID缺 → 500照既有範式 devices.go:95-100
2. `:id` 空 → 400 `VALIDATION_FAILED`
3. `DeviceRepo.Get(ctx, id)``ErrNotFound` → 404其他 DB error → `WriteDBError`
4. **owner 檢查**`d.OwnerUserID != userID` → 403 `FORBIDDEN`§7
5. **representative 檢查**`d.IsRepresentative == true` → 409 `CONFLICT`representative 不是真 USB、不可註冊§7.3)。
6. **已註冊檢查(細項 2**`d.RegisteredAt != nil` → 見 §3.2。
7. 翻轉set `registered_at = now()`、回 200 + 更新後的 DeviceListItem`registered_at`)。
### 3.2 已註冊檢查(細項 2— 取「409 衝突」,理由如下
兩個方案:
| 方案 | 行為 | 取捨 |
|------|------|------|
| **A. 409 CONFLICT採用** | 已註冊再 register → 回 409 `ALREADY_REGISTERED` | 語意明確、前端可提示「已註冊」;符合需求「避免重複註冊」的字面 |
| B. 冪等 200 | 已註冊再 register → 回 200不改 registered_at | 對 retry 友善,但掩蓋「使用者以為沒註冊卻已註冊」的狀態 |
**採 A**需求明確要「已註冊檢查避免重複註冊409 最貼合。前端收到 409 顯示 toast「此裝置已註冊」並 refetch。錯誤碼新增 `ALREADY_REGISTERED`(見 api 規格 §錯誤碼)。
> 註:`registered_at` 一旦設值,不因後續 exchange 覆寫——`upsertUSBDeviceTx` 復用分支pairing_exchange.go:329-341只更新 PairedAt/AgentID/type**不碰 RegisteredAt**,故重配對不會清掉註冊態(現況已正確、不需改)。
### 3.3 Repo 落地建議:新增專用 `SetRegisteredAtTx`(優於走 Save
雖然 `Save`/`SaveTx` 已能寫 `registered_at`upsert 全欄),但 register/unregister 建議**新增精準的單欄 UPDATE**,理由:
- Save 是全欄 upsertregister handler 得先 Get 整個 device 再 Save 回去,有「讀到寫之間被其他請求改動」的競態面(雖 P0 單裝置風險低,但語意不乾淨)。
- 專用 `UPDATE devices SET registered_at = $2, updated_at = now() WHERE id = $1 AND deleted_at IS NULL AND is_representative = false` 一句到位、天然只影響該欄、`RowsAffected()==0` 可轉 404/409 判定。
**建議簽名**domain `Repository` interface 加一法、in-memory + postgres 各實作):
```
// SetRegistered 設定/清除註冊時間。at=nil 表示取消註冊(清 NULL
// 只作用於未刪除、非 representative 的 device不符則回 ErrNotFound。
SetRegistered(ctx context.Context, id string, at *time.Time) error
```
- register`SetRegistered(ctx, id, &now)`handler 已先擋 already-registered故這裡不再判
- unregister`SetRegistered(ctx, id, nil)`
- 若不想動 interface退路是 handler 內 Get→改欄→Save可接受但較不乾淨。**推薦加 interface 方法**,並補 in-memory 對稱實作 + postgres db_testtestcontainers 130見 §8
### 3.4 資料模型(細項 6**不需 migration**
- `registered_at` 欄、`idx_devices_registered` index、domain 欄位、scan/save 讀寫路徑**全部已存在**§2 表。register/unregister 純粹是「翻轉既有欄」+「加 handler」**不新增欄、不改 schema、不寫 migration 檔**。
- 唯一的 code 面新增是可選的 `SetRegistered` repo 方法Go 層,非 schema
---
## 4. 取消註冊 API細項 3— WS-BE
### 4.1 端點:`POST /api/devices/:id/unregister`
- **路由**:同 register並列 registerDeviceRoutesUUID 識別。
- **Request body**:無。
- **行為**(與 register 對稱、但更寬鬆):
1. userID / `:id` / Get / owner / representative 檢查同 §3.1 步驟 1-5。
2. **不做「已註冊才可取消」的硬擋**:採冪等——若 `registered_at` 已是 NULL未註冊unregister 回 200no-op 成功),避免使用者連點兩次第二次報錯。(`SetRegistered(id, nil)` 對已 NULL 的列 UPDATE 到相同值、`RowsAffected` 仍為 1因 WHERE 命中;語意上「取消一個未註冊的 = 已達成目標」。)
3. `SetRegistered(ctx, id, nil)` → 清 `registered_at`
4. 回 200 + 更新後 DeviceListItem`registered_at` 為 null
- **絕不做**:不軟刪、不呼叫 DeviceUnpairer、不撤 token、不動 session§1.2 紅線)。
### 4.2 與 unpair 的並存驗證testing 重點)
- unregister 後device 仍 `GET /api/devices` 列得到、`registered_at=null`、pairing/session token 不變、tunnel 狀態不變。
- unpair 後device 從 List 消失軟刪、token 被撤。
- 兩端點互不影響:對同一 device 先 unregister 再 unpair 應正常unregister 只清欄、unpair 照樣軟刪)。
---
## 5. 三態分色(細項 4— WS-FE
### 5.1 前端資料斷點修復(第一步,最關鍵)
`device-store.ts``DeviceSummary`74-96**沒有 `registeredAt` 欄**`normalizeDevice`128-164也沒接後端回的 `registered_at` 在前端被丟棄。必須先補:
- `DeviceSummary``registeredAt?: string | null;`ISO 8601nil=未註冊)。
- `Device`extends Summary自動繼承。
- `normalizeDevice``registeredAt: pick<string>("registered_at", "registeredAt") ?? null,`(沿用既有 `pick` snake/camel 相容範式)。
### 5.2 三態運算規則(給前端明確定義)
三態 = **連線軸remoteStatus× 註冊軸registeredAt** 的組合:
| 態 | 條件 | 語意 | 配色 token |
|----|------|------|-----------|
| **已連接(已註冊在線)** | `remoteStatus === "online"` **且** `registeredAt != null` | 正常可用的個人設備 | `--status-online`(綠,既有) |
| **已連接未註冊** | `remoteStatus === "online"` **且** `registeredAt == null` | 插著、連線中但還沒註冊 → **第三態** | `--warning`(黃,既有 warning token 組) |
| **未連接** | `remoteStatus !== "online"`offline/reconnecting/error/unknown | 離線(不論註冊與否) | 沿用既有 RemoteDeviceBadge 各狀態色 |
**運算實作建議**:在 `device-store.ts` 或一支 `lib/device-state.ts` 加純函式:
```
type DeviceTriState = "online-registered" | "online-unregistered" | "offline";
function deriveTriState(d: Pick<DeviceSummary,"remoteStatus"|"registeredAt">): DeviteTriState
// online + registeredAt != null → "online-registered"
// online + registeredAt == null → "online-unregistered"
// 其餘 → "offline"
```
- 純函式便於 testing 單測(真值表 6 格)。
### 5.3 配色落地(沿用既有 design tokens不新增裸色
第三態「已連接未註冊」用**既有 warning token 組**`--warning` / `--warning-foreground` / `--warning-subtle`globals.css:159-161Light/Dark 都有定義),與 pairing 頁 / login 頁 / flash-dialog 的警示 UI 同色系。**禁止**寫 `bg-yellow-*` 裸色(對齊 flash-progress.tsx:11 的既有慣例)。
改動點:
- `remote-device-badge.tsx`:目前 badge 只吃 `remoteStatus`63-101。**方案:不改 badge 的內部語意**badge 仍表達連線狀態),而是在 `DeviceCard` 層疊加「未註冊」標記——online 且未註冊時,卡片額外顯示一個 warning 色的「未註冊」pill/badge新增小元件或用既有 warning classes。理由連線狀態與註冊狀態是正交兩軸硬塞進同一個 badge 會讓「online 但未註冊」的顏色語意打架。
- `device-card.tsx``isOnline`44旁加 `isRegistered = !!device.registeredAt`online && !registered → 卡片邊框 / 角落 pill 走 warning 色 + i18n 文案「未註冊」。
- **不只靠顏色**(沿用 design-review M2 無障礙原則):第三態要有文字「未註冊」+ icon不能只有黃色。
### 5.4 i18n
新增 keyzh-Hant + en 同步,對齊 `dictionaries/``devices.state.unregistered`(「未註冊」/ "Unregistered")、`devices.register.action`(「註冊」)、`devices.unregister.action`(「取消註冊」)、`devices.register.error.alreadyRegistered``devices.register.confirm` 等(實際 key 由 frontend agent 補全、testing 驗證存在)。
---
## 6. 排序 + filter細項 5— WS-FE
### 6.1 現況與範圍
- 現況:`device-list.tsx:31-37,77-79` 寫死一條「在線優先」排序,**無 filter、無切換 UI**。
- **不需後端改動、不需分頁**:個人設備數量級小(一個使用者的 USB 通常 < 20全部 client-side 排序 / 過濾即可。**不做 cursor/offset 分頁**資料量不成立徒增複雜度)。
### 6.2 排序(可自選鍵)
提供排序切換(下拉或 segmented control
| 排序鍵 | 規則 |
|--------|------|
| 狀態(預設,保留既有行為) | 沿用 `STATUS_ORDER`online→reconnecting→unknown→offline→error同狀態內次比名稱 |
| 名稱 | `displayName`alias || namelocaleCompareA→Z |
| 註冊時間 | `registeredAt` desc新註冊在前null未註冊排最後 |
- 排序狀態存元件 local state`useState`即可不需持久化P0。若要記住偏好可存 localStorage可選、非必要
### 6.3 Filter依三態
提供 filterchips / checkbox group選項
| filter | 條件(用 §5.2 deriveTriState |
|--------|------------------------------|
| 全部(預設) | 不過濾 |
| 已連接 | triState === "online-registered" |
| 未連接 | triState === "offline" |
| 已連接未註冊 | triState === "online-unregistered" |
- 排序與 filter 組合:先 filter 再 sort。
- 空結果狀態filter 後 0 筆 → 顯示「無符合條件的裝置」空狀態(與既有 EmptyState 區隔——這不是「完全沒裝置」)。
### 6.4 UI 落點
- 排序 + filter 控制列放 `devices/page.tsx``device-list.tsx` 頂部。
- `device-list.tsx` 目前接 `devices` prop 後自己排序;改為接「已排序 / 已過濾的 devices」或把排序 filter state 提到 list 內。由 frontend agent 決定放置層級(保持既有 EmptyState / skeleton / pair CTA 行為不變)。
---
## 7. 安全(細項 7— WS-BE
### 7.1 Owner 權限檢查
register / unregister **都必須**照既有範式devices.go:197-201`UserContextFrom` 取 userID → `Get` device → `d.OwnerUserID != userID` → 403 `FORBIDDEN`。缺 UserContext → 500auth middleware 沒配好,不可 fallthrough
### 7.2 IDOR 防護
- **威脅**:攻擊者用別人的 device UUID 打 `POST /api/devices/{別人的id}/register`(或 unregister若無 owner 檢查就能改別人裝置的註冊態。
- **防護**§7.1 的 owner 檢查即 IDOR 防線——先 Get 拿到 device 的 `OwnerUserID`、與 caller userID 比對、不符回 403。**不可**因「反正只是翻個 flag」而省略。
- **列舉防護取捨**owner 不符回 403既有 handler 慣例,如 devices.go:198。這會洩漏「該 UUID 存在但不屬於你」vs「不存在404」的差異。既有 unpair/get 都用 403、本 TDD **沿用一致**(不特別改成 404 混淆),維持 codebase 一致性;若 security agent 要求統一改 404-混淆,另案處理、不在 P0 範圍。
### 7.3 Representative device 防護
- register/unregister 前檢查 `d.IsRepresentative`;為 true → 409 `CONFLICT`representative 是 agent 連線佔位、非真 USB、註冊語意不適用
- 縱深:即使 List 已濾掉 representative前端拿不到其 UUIDhandler 仍要自己擋(攻擊者可能猜 UUID 或從其他管道拿到。repo 的 `SetRegistered` 的 WHERE 也帶 `is_representative = false`§3.3),三層防護。
### 7.4 其他
- register/unregister 是狀態變更操作,走既有 auth middlewareJWT/OIDC與 unpair 同一 route group
- 無新增 PII、無新增對外資料揭露。
- 建議整體register/unregister/IDOR納入 testing 的 API 安全測試;若 Orchestrator 判定需深度威脅建模,可送 security agent但 P0 owner+representative+IDOR 三檢查已覆蓋主要面。
---
## 8. 測試策略(給 Testing Agent
### 8.1 Backend 單元 / handler 測試
- register未認證(500) / 空 id(400) / device 不存在(404) / 非 owner(403) / representative(409) / 已註冊(409) / 正常(200 且 registered_at 非 null)。
- unregister非 owner(403) / representative(409) / 已註冊→取消(200 且 registered_at=null) / 未註冊→取消(200 冪等 no-op)。
- **並存回歸**unregister 後 device 仍在 List、token 不變unpair 行為完全不變(跑既有 unpair_test.go / unpair_db_test.go 確認全綠)。
### 8.2 Repo 測試SetRegistered若採 §3.3
- in-memoryset→get 回值、set nil→清空、representative 拒絕ErrNotFound、已刪除拒絕。
- postgres db_testtestcontainers本機無 docker → `192.168.0.130`,見 ADR-018 §4.3 dbtest 陷阱);`go test -run xxx -list` 先確認 case 數再跑避免假綠。UPDATE `RowsAffected` 對 representative / deleted 列為 0 → ErrNotFound。
### 8.3 Frontend 測試
- `normalizeDevice``registered_at` 有值/缺值 → `registeredAt` 正確 / null。
- `deriveTriState` 真值表online×registered / online×null / offline×registered / offline×null / reconnecting / unknown≥6 格)。
- 三態配色online 未註冊卡片有 warning 標記 + 「未註冊」文字(不只靠色)。
- 排序三種鍵各驗序filter四選項各驗集合 + 空結果狀態。
### 8.4 E2E可選P0 視情況)
- 註冊一顆在線未註冊 device → 卡片由黃轉綠 → 取消註冊 → 轉回黃 → device 仍在清單。
---
## 9. 並行化工作流計畫Parallelization Plan
### 9.1 契約contract-firstsingle source of truth
跨 BE/FE 必須一致的契約,全部定死於 [`api/api-device-mgmt.md`](api/api-device-mgmt.md)
- 端點:`POST /api/devices/:id/register``POST /api/devices/:id/unregister`UUID 識別)。
- Response既有 `DeviceListItem` 形狀(含 `registered_at` omitemptysnake_case。前端對這個形狀寫 normalize。
- 錯誤碼:`ALREADY_REGISTERED`(409)、`CONFLICT`(409, representative)、`FORBIDDEN`(403)、`NOT_FOUND`(404)、`VALIDATION_FAILED`(400)。
- 三態運算規則§5.2 真值表)= 前後端共識,前端據此算色、後端據此保證 `registered_at` 語意。
### 9.2 依賴圖 + 關鍵路徑
```mermaid
graph LR
C[契約定稿<br/>api-device-mgmt.md<br/>critical] --> BE[WS-BE: register/unregister<br/>+ SetRegistered repo]
C --> FE1[WS-FE: store 補 registeredAt<br/>+ deriveTriState 純函式<br/>對 mock 契約]
FE1 --> FE2[WS-FE: 三態配色 + 排序 filter UI]
C --> TS[WS-TEST: 測試設計<br/>對契約先寫]
BE --> INT[整合 join<br/>真後端接前端]
FE2 --> INT
TS --> INT
INT --> E2E[E2E join]
```
- **關鍵路徑**`契約定稿 → WS-BE → 整合 join → E2E`。契約是所有線的前置、必須先鎖死。
- WS-FE 可對著契約 mock 先開工store 補欄 + deriveTriState + UI不必等後端。
### 9.3 Work-stream 清單 + 同步點
| Work-stream | 派給 | 可與誰平行 | 阻擋於 | 同步點 |
|-------------|------|-----------|--------|--------|
| WS-BEregister/unregister handler + route + SetRegistered repoin-mem+pg+ handler test | backend | WS-FE, WS-TEST | 契約定稿 | 整合 join |
| WS-FEstore 補 registeredAt + normalize + deriveTriState + 三態配色 + 排序/filter UI + i18n | frontend | WS-BE, WS-TEST | 契約定稿 | 整合 join |
| WS-TESTbackend handler/repo 測試設計 + frontend 真值表/配色測試 + 並存回歸 + E2E 腳本 | testing | WS-BE, WS-FE | 契約定稿 | 整合 join / E2E join |
**同步點**
- **整合 join**:真後端接上前端,驗「註冊→卡片轉綠 / 取消→轉黃、device 保留」端到端。
- **E2E join**:跑 §8.4,並確認 unpair 回歸全綠。
### 9.4 任務卡Anthropic 四要素)
**WS-BE**
- Objective實作 register/unregister 兩端點 + (建議)`SetRegistered` repo 方法,符合 api-device-mgmt.md 契約。
- Output可運行 handler + route 註冊 + repo 方法in-mem + pg+ 對應 Go 測試(含 pg db_test 走 130 testcontainers
- 來源指引:讀本 TDD §1/§3/§4/§7、`api/api-device-mgmt.md`、既有 `devices.go`(照 owner 檢查範式)、`unpair.go`**只讀不改**、理解語意差異)、`postgres_repository.go`Save/scan 範式)。
- 邊界:**只加 register/unregister絕不改 unpair / Delete / DeviceUnpairer / token / session**。錯誤碼照契約、不自創。representative 一律擋。不寫 migration欄已存在
**WS-FE**
- Objective補前端註冊軸消費——store 接 `registeredAt`、三態運算與配色、排序/filter UI、register/unregister 呼叫。
- Output`DeviceSummary.registeredAt` + normalize + `deriveTriState` 純函式 + DeviceCard 三態配色warning token+ device-list 排序/filter 控制 + store action `registerDevice`/`unregisterDevice` + i18n keyzh/en
- 來源指引:讀本 TDD §5/§6、`api/api-device-mgmt.md`、既有 `device-store.ts`pick 範式、unpairDevice action 範式)、`device-card.tsx` / `remote-device-badge.tsx` / `device-list.tsx``globals.css`warning token
- 邊界識別值——register/unregister 用 **UUID**DB 操作、對齊 FE-A**不改** serial 路由類操作connect/camera/media。配色只用既有 design token、禁裸色。第三態不只靠顏色加文字+icon。不動 unpairDevice 行為。
**WS-TEST**
- Objective對契約設計 backend + frontend 測試 + 並存回歸 + E2E 腳本。
- Output§8.18.4 測試清單落為可執行測試 / 腳本。
- 來源指引:讀本 TDD §8、`api/api-device-mgmt.md`、既有 unpair 測試。
- 邊界:只寫測試、不改 production code。特別覆蓋「unregister vs unpair 語意不混」與「unpair 回歸不破」。
---
## 10. 給 Orchestrator 的實作排程建議
1. **先鎖契約**:確認 `api/api-device-mgmt.md`(本 TDD 附)定稿。
2. **同一輪平行 invoke**WS-BEbackend+ WS-FEfrontend+ WS-TESTtesting三線並行——契約已定、各自可對 mock/stub 開工。
3. 各線完成 → Reviewer 審 → 收斂到**整合 join**(真後端接前端)。
4. **E2E join** + unpair 回歸全綠 → P0 收尾。
5. effort scaling 判斷本任務中等2 個清楚分離模組 BE/FE + 測試3 線平行是甜蜜點,不過度切分。

View File

@ -0,0 +1,287 @@
# 技術設計文件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 審**。