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:
parent
6a797d5eb5
commit
f6d15b7b14
@ -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) | 裝置 → 模型 → 來源 |
|
||||||
|
|||||||
227
docs/autoflow/02-prd/features/feature-model-sharing.md
Normal file
227
docs/autoflow/02-prd/features/feature-model-sharing.md
Normal file
@ -0,0 +1,227 @@
|
|||||||
|
# Feature:模型共享(Model Sharing)— L 級新功能
|
||||||
|
|
||||||
|
> 父文件:[PRD.md](../PRD.md) | 相依既有功能:[模型管理](feature-model-management.md)、[會員系統](feature-auth.md)
|
||||||
|
> 對應 User Stories:US-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 |
|
||||||
|
| 使用者 / 權限 | OIDC(Member Center)`sub`=userID;users 表有 `org_id`(nullable, 未用)、`roles TEXT[]`(未用) | 「公開對象」要對齊這些既有欄位(同租戶 / 群組概念) |
|
||||||
|
| 下載 | handler 註解已寫明「第一階段 owner-only(**B 分享後續階段**)」 | 本功能正是被預留的「B 分享階段」 |
|
||||||
|
|
||||||
|
**一句話**:既有系統已經為「分享」預留了骨架(`org_id`、`roles`、download handler 的註解),本功能把這個骨架填血。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 功能概述與價值主張
|
||||||
|
|
||||||
|
**使用者痛點**(反推自 Persona,見 user-research.md):
|
||||||
|
|
||||||
|
- **阿哲(FAE)**:轉檔 / 調校好一個客戶專用模型後,想給同組 FAE 或客戶直接用,現在只能「下載 → 私訊傳檔 → 對方重新上傳」,模型檔散落、版本混亂。
|
||||||
|
- **Sarah(SI)**:在多個客戶現場佈署,想把一套驗證過的模型推給整個團隊 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** |
|
||||||
|
|
||||||
|
**P0(MVP)先做 `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 到 0005,models 表 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-31(P0)**:作為模型擁有者,我要**在模型 profile 頁設定它的公開對象(私有 / 指定使用者)**,這樣我就能控制誰能用我的模型。
|
||||||
|
- **US-32(P0)**:作為模型擁有者,我要**把模型分享給指定的一位或多位使用者(用 email 或名稱搜尋)**,這樣特定同事 / 客戶就能直接使用。
|
||||||
|
- **US-33(P0)**:作為模型擁有者,我要**看到目前這個模型已分享給哪些人,並能移除某個人的存取權**,這樣我能隨時收回授權。
|
||||||
|
- **US-34(P1)**:作為模型擁有者,我要**把模型設為「同組織可見」或「全公開」**,這樣整個團隊 / 社群不用我逐一授權。
|
||||||
|
|
||||||
|
### 3.2 共享模型庫(被分享者 / 探索者視角)
|
||||||
|
|
||||||
|
- **US-35(P0)**:作為使用者,我要**在模型庫看到「共享給我」的模型(別人授權給我的)**,並清楚看到它是誰分享的、我的權限是什麼。
|
||||||
|
- **US-36(P0)**:作為使用者,我要**能對共享模型庫做分頁瀏覽**,這樣模型多時不會一次載入全部、頁面不卡。
|
||||||
|
- **US-37(P0)**:作為使用者,我要**對共享模型庫做排序(上傳時間 / 名稱 / 檔案大小)**,這樣我能快速找到最新或最相關的模型。
|
||||||
|
- **US-38(P0)**:作為使用者,我要**對共享模型庫做 filter(依硬體晶片 / 來源 / 可見性 / 分享者)**,這樣我能縮小範圍。
|
||||||
|
- **US-39(P0)**:作為使用者,我要**用關鍵字搜尋模型(名稱 / 描述)**,這樣我能直接找到目標模型。
|
||||||
|
- **US-40(P0)**:作為使用者,我要**進入任一可見模型的 profile 頁,看到完整資訊(metadata、支援硬體、擁有者、分享者、我的權限、下載 / 載入按鈕)**,這樣我在使用前能確認它是我要的。
|
||||||
|
|
||||||
|
### 3.3 依身份權限的可見性(貫穿所有 story 的規則)
|
||||||
|
|
||||||
|
**共享模型庫「我能看到的模型」= 聯集**:
|
||||||
|
```
|
||||||
|
我擁有的(owner)
|
||||||
|
∪ 明確分享給我的(restricted grant,US-32)
|
||||||
|
∪ 我所屬 org 的 organization 模型(P1,US-34)
|
||||||
|
∪ 全公開模型(P1,US-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.1(Reach 為 Phase 相對估值、Impact 對北極星 WAD)。Effort 全部標「待 Architect 核對」——尤其 US-32 / US-35 的授權表 + 聯集查詢是技術重點。
|
||||||
|
|
||||||
|
### P0 / P1 切分總結
|
||||||
|
|
||||||
|
**P0(MVP,本次做)**:
|
||||||
|
- 可見性兩級:`private`(預設)+ `restricted`(指定 user 分享)
|
||||||
|
- 公開設定 UI(在 profile 頁)+ 授權清單管理(新增 / 移除)
|
||||||
|
- 共享模型庫:「共享給我」維度 + 分頁 + 排序 + filter + 搜尋
|
||||||
|
- 模型 profile 頁(擴充既有 `/models/[id]`,加分享資訊與權限顯示)
|
||||||
|
|
||||||
|
**P1(後續)**:
|
||||||
|
- `organization`(同租戶可見)— 依賴租戶 / org 指派機制成熟
|
||||||
|
- `public`(全公開)— 需內容治理(濫用回報、審核)
|
||||||
|
- 二次分享、edit 權限、分享通知(email / in-app)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 與既有模型功能的關係(擴充 vs 全新)
|
||||||
|
|
||||||
|
| 元件 / 檔案 | 現況 | 本功能 | 類型 |
|
||||||
|
|------------|------|--------|------|
|
||||||
|
| `Model` domain(model.go) | 無 visibility | 加 `Visibility` 欄位 + 可能的 share 關聯 | **擴充** |
|
||||||
|
| `models` 表(migration 0001) | owner-scoped index | 新 migration:加 visibility 欄 + `model_shares` 表 + 新 index | **擴充 + 新表** |
|
||||||
|
| `Repository.List(ListFilter)` | 只 owner filter | 擴充 filter(grantee / 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`(現為未寫入 stub),P1 前需先有租戶指派機制 | §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.md(US-31~US-40 併入主表)、success-metrics.md(分享指標)
|
||||||
461
docs/autoflow/03-design/feature-model-sharing-design.md
Normal file
461
docs/autoflow/03-design/feature-model-sharing-design.md
Normal 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 Story(Design 視角)
|
||||||
|
|
||||||
|
> **作為** 一個 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) ← 預設,僅「我的」模型
|
||||||
|
│ └─ 依共享關係分區(新增):我的 / 公開 / 共享給我的
|
||||||
|
│
|
||||||
|
├── 搜尋框(新增,跨當前檢視)
|
||||||
|
├── 篩選(擴充 ModelFilters:targetChip + 共享狀態)
|
||||||
|
├── 排序(新增:名稱 / 建立時間 / 共享時間)
|
||||||
|
├── 分頁 / 載入更多(新增)
|
||||||
|
│
|
||||||
|
└── /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 二選一)
|
||||||
|
|
||||||
|
**方案 P1(cursor / 無限捲動友善)→「載入更多」按鈕**(Phase 0 建議,實作簡單、無頁碼狀態同步問題):
|
||||||
|
|
||||||
|
```
|
||||||
|
┌────────────────────────────────────────────┐
|
||||||
|
│ [卡片] [卡片] [卡片] [卡片] │
|
||||||
|
│ [卡片] [卡片] [卡片] [卡片] │
|
||||||
|
│ │
|
||||||
|
│ [ 載入更多 (顯示 24 / 87) ] │ ← Button variant=outline,loading 時 Spinner
|
||||||
|
└────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
**方案 P2(offset / 傳統頁碼)→ 底部 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)版型:**
|
||||||
|
```
|
||||||
|
┌──────────────────────────────────────┐
|
||||||
|
│ ← 返回 │
|
||||||
|
│ │
|
||||||
|
│ 🔒 │
|
||||||
|
│ 沒有權限檢視此模型 │
|
||||||
|
│ 這個模型未公開或未共享給你 │
|
||||||
|
│ │
|
||||||
|
│ [ 返回模型庫 ] │
|
||||||
|
└──────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
> 🔷 Architect:403 vs 404 的取捨——為避免「模型是否存在」的資訊洩漏,建議私有模型對無權限者回 **404**(當作不存在),UI 走「找不到模型」而非「無權限」。**請 Architect 確認採 403 揭露存在 or 404 隱藏存在**,UI 兩版文案我都備。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. 響應式(沿用既有斷點)
|
||||||
|
|
||||||
|
沿用 `pages.md` §11 的斷點與 `/models` 既有規則(Mobile 單欄 / Tablet 2 欄 / Desktop 3–4 欄)。本功能新增元素的響應式:
|
||||||
|
|
||||||
|
| 元素 | Mobile (<640) | Tablet (640–1024) | 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 AA(design-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 決定增補方式)。
|
||||||
@ -62,6 +62,18 @@
|
|||||||
- **下載對接權威規格(v1.3:query-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.3:query-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` enum(private/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(本文件)
|
||||||
|
|
||||||
|
|||||||
190
docs/autoflow/04-architecture/adr/adr-020-ffmpeg-camera-indev.md
Normal file
190
docs/autoflow/04-architecture/adr/adr-020-ffmpeg-camera-indev.md
Normal file
@ -0,0 +1,190 @@
|
|||||||
|
# ADR-020: vendor ffmpeg 加回 camera input device(indev)— 三平台 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 §2(decoder-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 device(indev)**。
|
||||||
|
|
||||||
|
### 1.2 根因(已 100% 確認,非推測)
|
||||||
|
|
||||||
|
camera 抓實體攝影機是 **Go 端 `os/exec` 起 ffmpeg subprocess**(非 Python),指令形如 `ffmpeg -f avfoundation -i "0:none" ... -f image2pipe -vcodec mjpeg -`,ffmpeg 把攝影機輸出成連續 MJPEG stream 到 stdout,Go 端掃 JPEG SOI/EOI marker 切 frame。
|
||||||
|
|
||||||
|
問題在 vendor 的 macOS ffmpeg 是「decoder-only」自 build(v2 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)
|
||||||
|
|
||||||
|
採**方案 A:rebuild / 驗證三平台 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 對照:
|
||||||
|
|
||||||
|
| 平台 | 列裝置 | 抓 frame(capture 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-only;LGPL 乾淨;架構不動 | 需 rebuild macOS binary + 補 Linux code 分支 | — |
|
||||||
|
| B. 換完整版 ffmpeg | 省事、一次到位所有格式 | 體積爆增(40–70MB vs 現 5.7MB);推翻 v2 TDD §2 decoder-only;macOS 無現成 LGPL static 完整 build(正是當初自 build 的理由) | 唯一好處「省事」在 A 只加一個 flag 前提下不成立 |
|
||||||
|
| C. camera 改用平台原生 API(AVFoundation / Media Foundation / V4L2 ioctl + cgo) | 不依賴 ffmpeg subprocess | 三平台各寫一套原生 + cgo,複雜度爆炸 = 重寫 camera 子系統 | 解「build flag 少一行」不該砍掉可用的抓取架構 |
|
||||||
|
| D. macOS 專用第二顆含 avfoundation 的 ffmpeg | 主 binary 維持純解碼 | 多一顆 binary + 兩套 build 維護 | A 加一個 flag 就能讓同一顆 binary 兼顧(indev 增量 < 0.5MB),D 無意義 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 後果 (Consequences)
|
||||||
|
|
||||||
|
### 4.1 打破的既有前提
|
||||||
|
|
||||||
|
本 ADR **打破 v2 TDD §2 decoder-only「不含 indev」的前提**。原決策的假設是「local-tool 只處理本地檔案解碼」,但 camera 即時推論需要從實體裝置抓 frame(indev),該假設對 camera 情境不成立。本 ADR 確立新契約:
|
||||||
|
|
||||||
|
> **camera indev 是 ffmpeg build 白名單的必要一部分。** 未來升級 ffmpeg 版本 rebuild 時,三平台的 indev(avfoundation / 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 v3),rebuild 後仍須通過既有驗證(`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.5MB、LGPL v3 合規未破
|
||||||
|
- [ ] **v2 TDD ffmpeg 章節交叉引用**(增補不覆蓋):註記「camera indev 為 build 白名單必要部分、三平台 indev 對照見 ADR-020」
|
||||||
|
- [ ] 與 Tech Lead / 使用者確認
|
||||||
|
- [ ] 成本影響已評估(體積增量微、無新增基礎設施成本)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. WP 拆解(供 Orchestrator 排期)
|
||||||
|
|
||||||
|
| WP | 內容 | 派誰 | 需實機 | 前置 | 估時 |
|
||||||
|
|----|------|------|--------|------|------|
|
||||||
|
| WP-1 | macOS:configure 加 `--enable-indev=avfoundation`、rebuild、重算 sha256、更新 BUILD.md、commit binary | devops(build/vendor)| macOS(使用者有) | — | 1–1.5h(含 ~3 分 rebuild) |
|
||||||
|
| WP-2 | `buildCaptureArgs` / `ListFFmpegDevices` 補 Linux v4l2 分支(三平台 capture args 對照) | backend(Go camera code) | 否(可先寫、Linux 驗證階段測) | — | 1–1.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 | 1–1.5h |
|
||||||
|
| WP-5 | v2 TDD ffmpeg 章節交叉引用(增補);ADR-020 轉 Accepted | architect | 否(待 WP-1~4 完成) | WP-1~4 | 0.5h |
|
||||||
|
|
||||||
|
> **平行 / 接力**:WP-1(macOS ffmpeg)與 WP-2(Linux code)可**平行**(互不相依、不同檔)。WP-3(Windows 驗)獨立可平行。WP-4(Linux 驗)**接力 WP-2**。WP-5 收尾,等前四個。
|
||||||
|
> **關鍵路徑**:WP-2 → WP-4 → WP-5(Linux 驗需先有 code 分支)。
|
||||||
|
> **使用者只有 macOS**:WP-1 可立即做並驗證;WP-3 / WP-4 的實機驗證需 Windows / Linux 機器,標為「待實機」,先寫好 code / build 再擇機驗。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. 等級與後續
|
||||||
|
|
||||||
|
- **等級**:M 級(跨多檔 + 跨三平台 + 動 vendor build 策略,但非新 user story / 新架構)。
|
||||||
|
- **後續**:ADR 定稿後由 Orchestrator 派 devops(ffmpeg rebuild / 驗證)+ backend(Linux camera code 分支)落地。camera 不阻擋 B+C 主線,可獨立排期。
|
||||||
131
docs/autoflow/04-architecture/api/api-device-mgmt.md
Normal file
131
docs/autoflow/04-architecture/api/api-device-mgmt.md
Normal 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-A:DB 操作用 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 撤 token(device 從清單消失);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 + DeviceListItem(registered_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 | 非 owner(IDOR) | 既有 |
|
||||||
|
| `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 範式)
|
||||||
|
|
||||||
|
```
|
||||||
|
// 皆用 UUID(DB 操作);不帶 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。
|
||||||
186
docs/autoflow/04-architecture/api/api-model-sharing.md
Normal file
186
docs/autoflow/04-architecture/api/api-model-sharing.md
Normal 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 session(BFF,同 `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 | 每頁筆數,1–100,超出 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 200(envelope `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 200(envelope `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 內部 key,ADR-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 200(envelope `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 |
|
||||||
356
docs/autoflow/04-architecture/feature-device-mgmt-tdd.md
Normal file
356
docs/autoflow/04-architecture/feature-device-mgmt-tdd.md
Normal 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 列 | 對 token(pairing/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-A):register 是純雲端 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 是全欄 upsert,register 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_test(testcontainers 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,並列 registerDeviceRoutes,UUID 識別。
|
||||||
|
- **Request body**:無。
|
||||||
|
- **行為**(與 register 對稱、但更寬鬆):
|
||||||
|
1. userID / `:id` / Get / owner / representative 檢查同 §3.1 步驟 1-5。
|
||||||
|
2. **不做「已註冊才可取消」的硬擋**:採冪等——若 `registered_at` 已是 NULL(未註冊),unregister 回 200(no-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 8601,nil=未註冊)。
|
||||||
|
- `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-161,Light/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
|
||||||
|
|
||||||
|
新增 key(zh-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 || name)localeCompare,A→Z |
|
||||||
|
| 註冊時間 | `registeredAt` desc(新註冊在前);null(未註冊)排最後 |
|
||||||
|
|
||||||
|
- 排序狀態存元件 local state(`useState`)即可;不需持久化(P0)。若要記住偏好可存 localStorage(可選、非必要)。
|
||||||
|
|
||||||
|
### 6.3 Filter(依三態)
|
||||||
|
|
||||||
|
提供 filter(chips / 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 → 500(auth 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(前端拿不到其 UUID),handler 仍要自己擋(攻擊者可能猜 UUID 或從其他管道拿到)。repo 的 `SetRegistered` 的 WHERE 也帶 `is_representative = false`(§3.3),三層防護。
|
||||||
|
|
||||||
|
### 7.4 其他
|
||||||
|
|
||||||
|
- register/unregister 是狀態變更操作,走既有 auth middleware(JWT/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-memory:set→get 回值、set nil→清空、representative 拒絕(ErrNotFound)、已刪除拒絕。
|
||||||
|
- postgres db_test:testcontainers(本機無 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-first,single 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` omitempty,snake_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-BE:register/unregister handler + route + SetRegistered repo(in-mem+pg)+ handler test | backend | WS-FE, WS-TEST | 契約定稿 | 整合 join |
|
||||||
|
| WS-FE:store 補 registeredAt + normalize + deriveTriState + 三態配色 + 排序/filter UI + i18n | frontend | WS-BE, WS-TEST | 契約定稿 | 整合 join |
|
||||||
|
| WS-TEST:backend 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 key(zh/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.1–8.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-BE(backend)+ WS-FE(frontend)+ WS-TEST(testing)三線並行——契約已定、各自可對 mock/stub 開工。
|
||||||
|
3. 各線完成 → Reviewer 審 → 收斂到**整合 join**(真後端接前端)。
|
||||||
|
4. **E2E join** + unpair 回歸全綠 → P0 收尾。
|
||||||
|
5. effort scaling 判斷:本任務中等(2 個清楚分離模組 BE/FE + 測試),3 線平行是甜蜜點,不過度切分。
|
||||||
287
docs/autoflow/04-architecture/feature-model-sharing-tdd.md
Normal file
287
docs/autoflow/04-architecture/feature-model-sharing-tdd.md
Normal 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)
|
||||||
|
)
|
||||||
|
-- + filter(target_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,量大時考慮拆 UNION(owner 子查 ∪ public 子查 ∪ shared 子查)避免全表掃。 |
|
||||||
|
| **model_shares EXISTS 子查** | `idx model_shares (grantee_user_id)` 已在 ADR-017 定義,子查走此 index。 |
|
||||||
|
| **tenant join users** | owner.org_id 比對需 join;users 主鍵 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-breaker,base64 不透明(前端當黑箱)。
|
||||||
|
- `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 對稱 test(130 testcontainers,避免假綠)|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. 並行化工作流計畫(Parallelization Plan)
|
||||||
|
|
||||||
|
L 級、跨 backend + frontend + testing,適用本節。**contract-first**:§8.1 契約定死後三條 work-stream 可平行。
|
||||||
|
|
||||||
|
### 8.1 模組間契約(single source of truth)
|
||||||
|
|
||||||
|
| 契約項 | 定義處 | 鎖定內容 |
|
||||||
|
|--------|--------|---------|
|
||||||
|
| API schema(library / 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 test(130 testcontainers)。行為符合 §4.1 predicate + api 契約。
|
||||||
|
- 來源指引:本 TDD §3/§4/§6 + `api/api-model-sharing.md` + ADR-017 決策 3(model_shares)。
|
||||||
|
- 邊界:**只做 model sharing。不碰前端、不改 model_shares schema(ADR-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_key(DTO 本來就沒有)。**
|
||||||
|
|
||||||
|
**WS-3 testing**:
|
||||||
|
- Objective:權限可見性 matrix(owner/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 審**。
|
||||||
Loading…
x
Reference in New Issue
Block a user