---
title: "Agent 安裝包自助下載 — 定版交付 (FR-191)"
brand: "Guidant AI · **FR-191** 定版交付"
eyebrow: "FR-191 · 定版交付"
h1: "定版交付：Agent 安裝包自助下載"
subtitle: "已上線：正式更新站 80ccaecb（2026-11-10）；主系統、前端、手冊已合回 feature/FR-172，客戶升到含本案的主系統版本即可使用"
lede: "客戶還沒裝 Agent 時，管理員在主系統點「取得安裝包」，拿到原廠 Cloudflare R2 上 Agent 最新相容版安裝包與 .sha256 的預簽下載網址。網址有效時間由原廠在更新站後台設定，客戶畫面沒有這個欄位。"
chips: [
  {text: "正式更新站已部署", kind: ok},
  {text: "正式站實打通過", kind: ok},
  {text: "手冊截圖待補", kind: warn},
]
footer: "FR-191 定版交付 · 設計見 design-v2.1.md · 沿革見 handoff/FR-191-LOG.md 與 git log"
---

## 一句話 {#summary nav="一句話"}

管理員在更新中心 Agent 區按「取得安裝包」，或建完註冊 token 時看對話框下方，就拿到兩條 R2 預簽網址（安裝包、`.sha256`），直接下載或複製給裝機的人。主系統只向原廠更新站換網址、原樣轉交，不存包、不供檔；網址時限由原廠在更新站後台設定。

---

## 1. 交付物 {#deliverables nav="交付物"}

| Repo | 內容 | 位置 |
|---|---|---|
| update-center | `/v1/download-url` 認 `artifact=sha256`；預簽網址帶下載檔名；有效時間讀 D1 `site_settings`（預設 900 秒，範圍 300～86400）；後台「設定」分頁與 `GET／PUT /admin/settings`。**正式站已部署**：版本 `80ccaecb`（2026-11-10），正式 D1 已套 0007 | `src/settings.ts`、`src/routes/client.ts`、`src/routes/admin.ts`、`migrations/0007_site_settings.sql` |
| 主系統 BE | `POST /api/1.0/system/product-update/agents/bundle-links`：節流 → 查最新相容版（唯讀，不寫 `agent_last_check`）→ 換兩條網址 → 稽核 6251 | `api/product_update/routes/agent_bundle_link_route.py`（32 行）、`app/product_update/service/agent_bundle_link_service.py`（101 行）、`update_site_client.py` 的 `download_url_info`、`agent_release_service.py` 的 `query_latest` 與常數 `NO_AGENT_BASE` |
| 主系統 FE | 共用對話框顯示版號、大小、SHA-256、兩列網址與到期時間；兩個入口；沒有時限欄位 | `AgentBundleLinks.vue`、`AgentBundleButton.vue`、`useAgentBundleLinks.js` |
| 手冊 | 快速版第 3 步與 03 章 3.2 的取包標準句；04 章 token 對話框提示；07／10／11／README 的重新取包句 | `docs/user-manual/agent/`、FR-189 `site/index.html` |

---

## 2. 流程 {#flow nav="流程"}

```{.mermaid cap="圖 1　管理員產生 R2 下載網址。主系統以主機授權身分向更新站換網址；檔案下載完全不經主系統。"}
%%{init: {'theme':'base','themeVariables':{'primaryColor':'#E2F0F1','primaryTextColor':'#14201F','primaryBorderColor':'#0E7C86','secondaryColor':'#EEF2F3','secondaryTextColor':'#14201F','tertiaryColor':'#FBFCFC','tertiaryTextColor':'#14201F','lineColor':'#4A5A5C','textColor':'#14201F','mainBkg':'#E2F0F1','nodeBorder':'#0E7C86','nodeTextColor':'#14201F','edgeLabelBackground':'#FBFCFC','actorBkg':'#E2F0F1','actorTextColor':'#14201F','actorBorder':'#0E7C86','actorLineColor':'#C7D1D2','signalColor':'#4A5A5C','signalTextColor':'#14201F','noteBkgColor':'#F6EBD5','noteTextColor':'#14201F','noteBorderColor':'#9C6B12','sequenceNumberColor':'#FFFFFF'}}}%%
sequenceDiagram
  autonumber
  actor A as 管理員（瀏覽器）
  participant B as 主系統 API
  participant U as 更新站（Cloudflare Worker）
  participant R as Cloudflare R2
  actor C as 裝機人員
  A->>B: POST agents/bundle-links
  B->>B: 每人每分鐘 10 次節流
  B->>U: /v1/check product=agent current=1.0.0（主機授權身分）
  U-->>B: latest 與簽章清單
  B->>B: 驗章、比 min_host_version
  B->>U: /v1/download-url 整包＋artifact=sha256
  U-->>B: 兩條 R2 預簽網址、expires_in
  B-->>A: 版號、大小、SHA-256、兩條網址；寫稽核
  A->>C: 下載，或複製網址交給裝機人員
  C->>R: GET 預簽網址（不需帳號）
  R-->>C: 檔案
  C->>C: sha256sum -c
```

第 3 步送 `current=1.0.0`（常數 `NO_AGENT_BASE`）：更新站 Agent 版本的可直升起始版一律是 `<主版>.0.0`，送 `0.0.0` 會把所有版本濾掉、回「沒有新版」。第 6 步任一失敗，或兩條網址只換到一條，整個回失敗，不給半組網址。

---

## 3. 規則 {#rules nav="規則"}

- 版號＝更新站上這張授權看得到的 Agent 最新相容版，管理員不能選。查詢時一律送基準版 `1.0.0`；更新中心 Agent 區「檢查」與「預先下載」在一台 Agent 都沒有時也送同一個值。最新版要求較新的主系統時回 412（`PRODUCT_UPDATE_412014`），不給網址。
- 時限由原廠在更新站後台「設定」分頁調整，所有預簽網址（升級包、維運小檔、Agent 包）共用。陷阱：主系統不再有任何時限欄位，客戶端問時限一律請原廠改後台。
- `sha256`、`size` 取自驗過章的簽章清單，不取 `/v1/download-url` 回的未簽欄位。
- 網址主機必須在 `DOWNLOAD_HOSTS` 白名單（R2 端點），否則拒絕轉交（503001）。
- 產生網址要 `system.product-update.read`；每人每分鐘 10 次，超過回 429（`PRODUCT_UPDATE_429002`）。
- 稽核事件碼 6251，action `AGENT_BUNDLE_LINK_ISSUED`，欄位 actor、version、expires_in、source=r2。
- 錯誤：沒設更新站 412001、連不到或清單驗不過 503001、沒授權照 412003、站上沒有 Agent 版本 404005。
- 前端只保留「下載」鈕按下後鎖 10 秒；到期判斷用收到網址的本機時刻加 `expires_in`；不做 HEAD 探測（R2 預簽網址簽的是 GET）。
- 下載者的電腦要能連 Cloudflare；主系統連不到更新站時畫面提示向原廠索取。

---

## 4. 驗證結果 {#verify nav="驗證結果"}

**正式更新站實打**（2026-11-10，部署 `80ccaecb` 後，用 190 測試機主機授權身分；190 本身未改動）：

| 項目 | 結果 |
|---|---|
| 健康檢查 | `{"ok":true}` |
| 主系統「檢查更新」（現有功能） | 正常 |
| 主系統升級包下載網址（現有功能） | 正常；`expires_in`＝900；真 R2 下載與續傳（206）成功，檔名正確 |
| Agent 線 `current=1.0.0` | `latest`＝1.7.4（`current=0.0.0` 回 `none`，見第 2 節） |
| Agent 1.7.4 安裝包與 `.sha256` 網址 | 兩條都拿得到，檔名正確；`.sha256` 實際下載內容與更新站清單 sha256 一致 |
| Agent 線帶 `artifact=ops` | 400，正確擋下 |
| 主系統程式（修正後常數＋真 `update_site_client`）打正式站 | `latest`＝1.7.4，兩條網址正常，sha256 一致 |

**自動測試**：

| 範圍 | 結果 |
|---|---|
| update-center | `typecheck` 綠；`npm test` Worker 187、CLI 101 全綠（含 sha256、content-disposition 簽進章內、設定範圍、CI／GitLab token 403） |
| 主系統 BE | FR-191、更新中心、FR-167／172 相關測試與守衛 `test_module_boundaries.py` 共 495 條綠 |
| 主系統 FE | vitest 相關 12 檔 145 條綠；全套 86 檔 663 條綠；`vite build` 過；改動檔無 `border-left` |
| 手冊 | grep「主系統下載／預先下載／30 分鐘內有效」只剩 07 章 7.8 一句（講 Agent 升級，與取包無關） |

**畫面**：本機接模擬更新站（用綁本機的 DEV 身分票）時，後端 API 與無頭瀏覽器都打通；對話框顯示版本 1.7.4、1.7 GB、SHA-256、兩列 R2 網址、「連結 15 分鐘內有效」，沒有時限欄位。模擬環境的 R2 金鑰是假的，網址本身下載不到；真實下載已由上方正式站實打涵蓋。

---

## 5. 沒做完的 {#todo nav="沒做完的"}

| 事項 | 說明 | 誰接 |
|---|---|---|
| 手冊截圖 | 快速版第 3 步與 03 章 3.2 的畫面截圖。現有兩張是模擬環境畫面，不採用 | 決策者 2026-11-10 裁先不做；有真網址畫面時重截 |
| 客戶主系統升級 | 客戶要升到含 FR-191 v2 與 CM-3819 的主系統版本，才會有新的「取得安裝包」行為。舊版主系統打新版更新站不受影響 | 隨下一次主系統發版 |

---

## 6. 文件定位 {#docs nav="文件定位"}

| 你想知道 | 看哪份 |
|---|---|
| 要做成什麼樣、為什麼、排除了什麼 | [design-v2.1.md](design-v2.1.md)（現行）；[design-v2.0.md](design-v2.0.md)（被 v2.1 取代，只差查詢基準版） |
| v1.0 當初怎麼設計（稽核用，已推翻） | [design.md](design.md) |
| 進度、決策者表態、Notion 卡號 | [README.md](README.md) |
| 每一棒做了什麼、v1.0 為什麼被推翻 | [handoff/FR-191-LOG.md](handoff/FR-191-LOG.md) |
| 現在停在哪 | [handoff/FR-191-STATE.md](handoff/FR-191-STATE.md) |
