visionA/docs/autoflow/04-architecture/adr/adr-018-agent-device-model.md
jim800121chen 72544d00ba docs(adr): ADR-018 §8 addendum — C5 實作偏差回填(路由段維持 UUID)
- §5.5 加補註指向、原文一字未動(ADR 不可變原則)
- §8.1 記錄 C5 偏差證據鏈(fetchDevice 純 DB / API 無 GetBySerial /
  帶 serial deep-link 必 404)與最終實作(路由段 UUID、serial 頁內派生、
  入口 disable 全落實);決策本體 FE-A 與 C1–C4/C6–C8 不變
- 裁定來源:wp-c-frontend-serial-routing-review.md(reviewer 獨立驗證)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 08:39:32 +08:00

18 KiB
Raw Permalink Blame History

ADR-018: 一 agent 多 device 資料模型 + serial 路由(走向 A'

狀態

Accepted使用者已裁決同意2026-07-09

日期

2026-07-09

作者

Architect Agent


1. 背景與範圍

visionA 的「個人設備管理」要支援 一個 local agent一條已配對的 tunnel 連線)底下掛多顆實體 USB 裝置Kneron dongle以 kn_number 序號識別)。這與現況資料模型的根本衝突是:現況把 devices 這張表當「一條連線」用exchange 時 uuid.NewString() 自建一筆佔位 device 綁 session token從未表達「一顆實體 USB」的概念。

本 ADR 收斂以下決策,讓後續 task 3三色分色/ task 5註冊/ task 6去重/ task 7實體刪除長在乾淨模型上而非各自打 patch

  • 走向抉擇:如何在資料層表達「一 agent 多 device」且不破壞剛做完的推論 / camera / flash proxy 路由。
  • 路由對應:雲端 devices.idUUID與 local agent 本地 session key合成 kl520-0)從不對齊,多裝置一定路由錯,如何建立跨層穩定識別。
  • 前端識別模型:前端所有 device 操作現在都帶雲端 UUID改用 serial 路由後哪些操作用 UUID、哪些用 serial。

1.1 定案前提(使用者已拍板)

  • device = 實體 USB 裝置,以 Kneron 序號 kn_number 識別;一 agent 多 USB。
  • 註冊軸 registered_atNULL=未註冊)× 連線軸 tunnel_online → 三色狀態。
  • Q3 手動逐顆註冊Q4 kn_number 唯一鍵(多租戶複合鍵留 Phase1Q5 取消註冊 = 真實體刪除 + 清 session_tokens FK。
  • 授權範圍:可動 local-agent/visionA-backend/visionA-frontend/;不動 local-tool/(可複製、不動)。

1.2 讀 code 後推翻的悲觀假設(本 ADR 的核心洞察)

前置盤點(personal-device-mgmt-audit.md曾判定「B 走向硬傷 = session_tokens.device_id 單一 FK」。讀完 proxy.go 後,這個判定被推翻

原假設 讀 code 後的事實 source
「一 agent 多 USB → 一條 tunnel 對應多顆 devicesession_tokens.device_id 單一 FK」 推論路由不經過 session_tokens.device_idproxy.go:278 pickActiveSessionToken 只按 userID 挑該 user 的 active session:iddeviceId原樣透傳給 local agent 當 path 參數,不參與選 tunnel proxy.go:278 / proxy.go:130-134 註解
session_tokens.device_id NOT NULL 一對一是硬傷」 它綁的是 exchange 自建的佔位 devicepairing_exchange.go:115 uuid.NewString()),這筆 device 是「agent 連線」語意、不是任何一顆實體 USB。推論時完全沒用到它 pairing_exchange.go:115

真正的缺口不是 FK是「雲端 device.id ↔ local agent 本地 id」的對應從不存在:雲端 devices.id = DB UUIDlocal agent 路由 key = fmt.Sprintf("%s-%d", chip, idx-1)(合成 kl520-0detector.go:231),兩者從沒對齊。單裝置 demo 能跑純屬 device-store.ts:88-92 記載的寬鬆比對技術債,多裝置一定錯。這個對應的建立,才是本設計的真正重心。


2. 決策

2.1 走向 A':新增 agents 實體 + devices 改實體 USB + session 綁「agent 代表 device」+ serial 路由

採用 走向 A'

  1. 新增 agents(一條已配對的 tunnel 連線 = 一個 agentowner_user_id FK→users
  2. devices 語意改為「一顆實體 USB」,新增 agent_id FK→agentsserial_numberkn_number欄位已存在task 1 填值)、registered_atNULL=未註冊)、is_representativetrue=agent 佔位/代表 device非真 USBagent_local_device_idlocal 合成 id路由除錯輔助
  3. session_tokens.device_id 物理 schema 不動(保留 NOT NULL REFERENCES devices(id)),但改綁「agent 的 representative device」——每個 agent 建一筆 is_representative=trueserial_number=NULL 的代表 device等同現行 exchange 自建的那筆。推論路由既然不靠它,改綁零風險。
  4. 路由統一用 serialkn_numberlocal agent 端新增 serialToLocalID 反查表,GetDevice(id) 改為雙查(先 sessions[id]、查無再 serialToLocalID[id]manager.go:118-126)。純加法、不改 sessions key、不改 proxy——保護剛做完的推論。

2.2 前端 FE-A 混合識別模型(併入 R-Route 查證結論)

R-Route 前端查證(讀 device-store.ts / workspace-client.tsx / media.ts / camera_handler.go 等)發現:前端帶 device id 的方式不統一

  • path 類/api/devices/{id}/.../ws/devices/{id}/inferenceconnect / disconnect / unpair / 詳情 fetchDevice / inference WS。
  • body / FormData 類{ deviceId } / form.append("deviceId", …)camera start/stop、media uploadimage/video/batch

因此原設計「用 serial 當 path 識別」只對一半。正確落地 = 前端把 device 識別「值來源」從 device.idUUID換成 device.serialNumber傳輸位置path vs body完全不動。且採 FE-A 混合模型

操作類別 用哪個識別值 理由
純雲端 DB 操作(列表 / 詳情 fetchDevice / unpair UUID 在 api-server 直接查 DBUUID 是主鍵最自然;且 serial 空的 device 也要能看詳情 / 被移除
路由到 local agent 的操作camera / media / inference WS / flash / connect serial 才能對上 local agent sessions(雙查反查表)

被否決的 FE-B全部用 serial:會讓 serial 空(未串通 / 假序號)的 device 連移除都做不到,不可接受。

serial 空的 device:無法路由,「路由類」操作按鈕須 disable(新增 hasSerial 條件,疊加現有 isOnline),並 tooltip 說明「此裝置尚未取得序號,無法操作」。假序號 0x00000000macOS 無 SDK視同無序號、寫 NULL。


3. 考慮過的替代方案

走向 schema 動作 session_tokens.device_id device.id ↔ local id 對應 task 1 能否先行 未來債 排除原因
A新增 agents + session 改綁 agent 大:新表 + devices 加 FK + session_tokens 的 FK targetdevices→agents REFERENCES agents(id) 要建 否(連帶動一堆) 破壞性 migration:既有 session token 的 device_id 值要重新 map而推論根本不靠它改它純為「乾淨」承擔風險ROI 低
B沿用 devices 改語意 + 加 registered devices 加 registered_at 維持綁「第一顆 device」→ N>1 時語意崩 要建 勉強 多 USB 時 FK 語意崩、是半套 A留技術債
A'(採用)新增 agents + devices 掛 agent_id + session 綁代表 device + serial 路由 中:新表 agents + devices 加欄位 + 不改 session_tokens 物理 schema(改語意) 維持 NOT NULL、綁 agent 的代表 device 用 serialkn_number建立 task 1 只填 serial、agents 表可後補) 採用

前端識別模型的替代方案

方案 前端改動 排除原因
FE-A採用混合DB 操作用 UUID、路由操作用 serial 識別值來源分流,語意最正確 採用
FE-B 全部用 serial 連 unpair / 詳情都用 serialapi-server 要改 DB 層支援 GetBySerial serial 空的 device 連詳情 / 移除都進不去,不可接受
B2 前端維持 UUID、local agent 建 UUID→本地 key map 前端零改動 local agent 要在配對時取得雲端 UUID 並建 map耦合重等同原設計選項 A已否決

4. 後果

4.1 正面

  • 語意乾淨agents(一條 tunnel 連線)與 devices(一顆實體 USB分離正是使用者要的模型。task 3/5/6/7 長在乾淨模型上。
  • 不動 session_tokens 物理 schema:避開改 FK target 的高風險 migration只改語意綁代表 device
  • 路由對應用 serial 建立serial 是硬體穩定值、跨雲端↔local 一致。local agent 端純加法(serialToLocalID + GetDevice 雙查),舊呼叫(本地 id與新呼叫serial都通。
  • task 1序號地基可先行:只需 devices.serial_number 填值(欄位已存在),agents 表與 agent_id 可在 task 5/6 補migration 分兩支task 1 不動 schema、0005 建 agents 模型)。序號本身可獨立交付、模型另案,兩者不衝突。
  • 保護剛做完的推論proxy.go 一行都不用改(路由機制天然支援多 USBlocal agent sessions map 的 key 不改;GetDevice 雙查向下相容。

4.2 負面(接受的取捨)

  • 多一張 agents 表 + 一次資料遷移現有「device=連線」的舊 row 要拆成「1 agent + N devicerepresentative遷移邏輯見 §5.4)。
  • local agent 端新增「serial ↔ 本地 sessions key」對應:這是 A/A' 共同的必要工作B 走向也躲不掉。
  • session_tokens.device_id 綁「代表 device」是語意上的 workaround:它不是真正的 USB。若未來要「session 綁具體某顆 USB」例如 per-device token要再改但目前無此需求一條 tunnel 服務該 agent 底下所有 USB可接受。
  • 前端 FE-A 混合的認知成本:工程師須清楚區分「哪個操作用 UUID、哪個用 serial」R-Route 查證已列出逐檔清單 C1C8

4.3 風險

  • R-Route路由對應serial 空的 device未串通 / 假序號)無法路由,「路由類」操作須 disable。若使用者期待「所有 device 都能操作」,需先補齊 serial 串通task 1
  • connect/disconnect 歸屬待查connect/disconnect 是否 forward 到 local agent決定歸 serial 或 UUID需在 WP-C 開工第一件事查 backend devicesConnectHandler 是否 proxy 到 local agent。目前 device-store 只打 API 看不出。
  • 資料遷移正確性:現有 device 全部 serial=NULL、全部視為 representative本來就是「連線」不是 USB遷移後繼續綁原 session_tokens現有 tunnel/推論流程須驗證零影響。真實 USB device 遷移後還不存在,等 agent 下次 exchange 帶 serial 上來或 task 5 註冊才建立。
  • dbtest 陷阱:本機無 docker跑 migration 測試用 192.168.0.130 跑 testcontainersgo test -run xxx -list 先確認 case 數再實跑避免「pool 參數混入 migration」「檔名沒 _test.go」兩種假綠。0005 up/down migration 須有對應 *_test.go 驗證 apply/rollback 對稱。

5. 落地方向schema 形狀,細節由 backend / frontend agent 落地)

此為決策方向。schema DDL 細節、migration 檔、前端逐檔改動由後續實作落地。schema 併入 database.md、TDD 由 Orchestrator 另案處理,本 ADR 只定模型形狀與決策歷史。

5.1 目標 ERA' 模型)

users (已存在)  1─N  agents ★新表(一條 tunnel 連線 = 一個 agent
                       id / owner_user_id FK→users / name / platform / agent_version / last_paired_at

agents  1─N  devices語意改為「實體 USB」
                id (UUID PK雲端穩定 id)
                owner_user_id FK→users
                agent_id FK→agents ★新增NULL=代表 device / 舊資料)
                serial_numberkn_number已存在task 1 填值)
                agent_local_device_id ★新增local 合成 id路由除錯輔助
                registered_at ★新增NULL=未註冊)
                is_representative ★新增true=agent 佔位/代表 device
                remote_status / status / last_seen_at / deleted_at已存在task7 改實體刪除)

session_tokens物理 schema 不動)
                device_id NOT NULL FK→devices ← 改綁「agent 的 representative device」語意變、schema 不變)

5.2 migration 分支

  • task 1序號地基無 migration 檔serial_number 欄、SaveTx 空→NULL、GetBySerial、partial unique 全已存在。工作純在 Go 層exchange 收 serial 填入 + 防唯一約束炸裂exchange 前 GetBySerial(owner, serial) 已存在則復用既有 device_id
  • 0005agents 模型)CREATE TABLE agents + devices 加 4 欄 + indexidx_devices_agent_active / idx_devices_registered+ data migration舊 device → agent + 標 representative。環境PostgreSQL 14.23gen_random_uuid() 可直接用。

5.3 唯一約束task 6 去重基礎)

現有 uq_devices_owner_serial_active (owner_user_id, serial_number) WHERE deleted_at IS NULL 不需改representative device serial=NULLNULL 互不相等、天然共存);假序號寫 NULL 不撞Q4「多租戶複合鍵留 Phase1」指跨租戶全域唯一現階段 owner-scoped 已足夠。只要 serial 有值task 1就自動生效。

5.4 資料遷移

每筆現有 device皆 serial=NULL 的連線佔位)→ 建對應 agent + UPDATE device SET agent_id, is_representative=trueSQL 無法一句對應,建議 migration 程式碼逐筆。List devices 時 filter is_representative=false(只列真 USB且加 WHERE ... AND is_representative=false

5.5 前端 WP-C 逐檔FE-A 混合)

C1 device-store.tsserialNumber type + normalizeC2 workspace-client.tsx camera/media/inference 識別改 serial + disableC3 use-inference-stream.ts 註解C4 lib/media.ts 註解C5 workspace/[deviceId] 連結改帶 serialC6 device-card.tsx 工作區按鈕加 hasSerialC7 device-detail-client.tsx 工作區用 serial詳情頁本身 fetchDevice 維持 UUIDC8 flashF8 接時用 serial不改(保持 UUIDfetchDevices/fetchDevice/unpair/connect/disconnect、詳情頁路由 /devices/[id]

⚠️ 補註2026-07-11上列 C5「連結改帶 serial」一句於 WP-C 實作時證實與 §2.2 FE-A 自相矛盾,實作偏差已裁定接受——路由段維持 UUID、serial 頁內派生。詳見 §8 補註(本段原文依 ADR 不可變原則保留、不改寫)。


6. 合規性

  • 與使用者確認走向 A'(新增 agents + devices 改實體 USB + session 綁代表 device + serial 路由)— 已裁決同意
  • 與使用者確認前端 FE-A 混合模型DB 操作用 UUID、路由操作用 serial、serial 空 disable 已裁決同意
  • 與 backend agent 確認 schema 併入 database.mdagents 表 + devices 新欄位 + migration 0005— 另案,本 ADR 不做
  • 與 backend agent 確認 WP-C 開工前置:devicesConnectHandler 是否 proxy 到 local agent決定 connect/disconnect 歸 serial 或 UUID
  • 與 testing agent 確認回歸:既有推論 E2ETestE2E_FullFlow_PairingToForward 等)在 serial 反查加入後仍全綠 + 0005 up/down 對稱測試130 testcontainers
  • 成本影響:無新雲端資源(沿用現有 DB / tunnel主要是 visionA 三 modulebackend / local-agent / frontend開發工時粗估全量 2036 人天、僅 task 1 則 36 人天

7. 一句話總結

讀 code 後推翻「session_tokens.device_id 單一 FK 是硬傷」的假設——推論路由根本不靠它,真正缺口是「雲端 device.id ↔ local agent 本地 id」從不對齊。故採 走向 A':新增 agents 表、devices 改為實體 USB 掛 agent_idsession_tokens.device_id 保留物理 schema 但改綁「agent 代表 device」避開破壞性 FK migration、路由統一用 serialkn_number並在 local agent 端加 serial→本地 key 雙查(純加法、保護推論)。前端採 FE-A 混合模型DB 操作用 UUID、路由到 local agent 的操作用 serialserial 值替換 UUID、傳輸位置不動serial 空的 device 路由類操作 disable。


8. 補註Addendum

ADR 為不可變決策紀錄以下補註只增不刪§1§7 原文一字未動。

8.1 C5 實作偏差回填2026-07-10 實作定案、2026-07-11 回填)

偏差內容§5.5 原句「C5 workspace/[deviceId] 連結改帶 serial」於 WP-C 前端實作時證實與本 ADR §2.2 FE-A 混合模型自相矛盾

  1. workspace 進頁第一件事是 fetchDevice(路由段值),屬「純雲端 DB 操作」——依 FE-A 分流表本應歸 UUID 組。
  2. api-server 只有 GET /devices/:id 一個讀取端點、handler 直接按 DB 主鍵UUIDvisionA-backend/internal/api/devices.go:26-42不存在任何 GetBySerial 端點
  3. 路由段若帶 serial直接輸入 URL / 重新整理 / 書籤進入 workspace → fetchDevice(serial) → 404 → 整頁卡占位態。要救就得 api-server 加 serial 查 DB——正是本 ADR §3 否決 FE-B 時列出的理由。

R-Route 查證報告 §3.2 第 4 點當時已標注此張力,但 §4 C5 與本 ADR §5.5 C5 的行文未回頭同步,屬文件內部殘留矛盾、非實作漏做

最終實作(經 reviewer 獨立驗證、裁定偏差成立並接受)

  • workspace 路由段 [deviceId] 維持 UUIDdevice-card.tsx / device-detail-client.tsx / workspace/page.tsx 連結仍帶 device.id)。
  • serial 由頁內 selectedDevice.serialNumber 派生,供 camera / media / inference WS 等路由類操作使用。FE-A 精神(「路由到 local agent 的操作識別值 = serial」100% 達成——路由段只是「取 device 資料的 key」、本身不路由到 local agent。
  • C5 的「serial 空時工作區入口 disable + tooltip」行為全數落實另加頁內 no-serial banner。

決策本體不變§2.2 FE-A 混合模型、§5.5 其餘各條C1C4 / C6C8均維持原決策本補註僅將 C5 中「連結 href 改帶 serial」一句更正為「路由段維持 UUID、serial 頁內派生」。

參照

  • 裁定證據鏈:.autoflow/05-implementation/review/wp-c-frontend-serial-routing-review.md §二C5 偏差獨立驗證)與 Minor #2本回填的來源指令
  • 同步修正文件:.autoflow/04-architecture/personal-device-mgmt-R-Route-frontend-verification.md §4 C5該檔非 ADR、直接於原條目修正 + 表下加修正註記)。