---
title: "Agent 獨立更新 — 定版交付 (FR-172)"
brand: "Guidant AI · **FR-172** 定版交付"
eyebrow: "FR-172 · 定版交付"
h1: "定版交付：主系統與 Agent 各自更新"
subtitle: "現況：主系統 1.28.3、Agent 1.5.5（內部測試包，測試章簽）；正式版跟 FR-173 出 1.29"
lede: "主系統與 Agent 是兩個安裝包、兩條版本線、各自升級。Agent 不連外網，包一律經主系統轉手；主系統更新中心、Agent 主控台、離線上傳三種方式都能發動 Agent 更新，進度當下回報。給 PM 的問答看 [for-pm-qa.md](for-pm-qa.md)；每一步跑哪支程式看 [upgrade-flow-guide.md](upgrade-flow-guide.md)。"
chips: [
  {text: "主系統 1.28.3", kind: accent},
  {text: "Agent 1.5.5", kind: accent},
  {text: "190 實測通過", kind: ok},
  {text: "正式版跟 FR-173", kind: warn},
]
footer: "FR-172 定版交付 · 設計見 design.md（v1.0）與 design-v1.1.md · 沿革見 handoff/FR-172-LOG.md 與 git log"
---

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

主系統與 Agent 分開出包、分開上更新站、分開升級。按「更新到 X」不必先預先下載：主系統手上沒有最新包就自己先下載再推。Agent 升級中自動進入維護模式，主控台整頁鎖住只顯示進度，完成自動恢復。

---

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

| 項目 | 主系統 | Agent |
|---|---|---|
| 版號（`pyproject.toml`） | 1.28.3 | 1.5.5 |
| 安裝包 | `guidant-ai-<版>.tar.gz`（約 3.5 GB，不含 `agent/`）＋ops 小檔 | `guidant-agent-compose-<版>.tar.gz`（約 1.7 GB） |
| 更新站版本線 | `product=host`，R2 `v1/<版>/` | `product=agent`，R2 `agent/v1/<版>/` |
| 發布清單的章型別 | `release_manifest` | `agent_release_manifest` |
| 最低相容 | `min_agent_version`（主系統要求 Agent 至少幾版） | `min_host_version`（Agent 要求主系統至少幾版，目前 1.28.0） |
| 更新站上（測試章） | 1.28.1、1.28.2、1.28.3 | 1.5.1～1.5.5 |
| 190 測試機 | 1.28.3 | 1.5.5 |
| 189 POC | 1.27.x（未升） | 1.4.x（未升） |

---

## 2. 更新怎麼跑 {#flows nav="流程"}

### 2.1 主系統自己升級

```{.mermaid cap="圖 1 — 主系統升級：主系統容器只寫單，主機的 systemd 單元撿單跑八步。"}
%%{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','titleColor':'#14201F','clusterBkg':'#FBFCFC','clusterBorder':'#E4EAEB','actorBkg':'#E2F0F1','actorTextColor':'#14201F','actorBorder':'#0E7C86','actorLineColor':'#C7D1D2','signalColor':'#4A5A5C','signalTextColor':'#14201F','labelBoxBkgColor':'#EEF2F3','labelBoxBorderColor':'#C7D1D2','labelTextColor':'#14201F','loopTextColor':'#14201F','noteBkgColor':'#F6EBD5','noteTextColor':'#14201F','noteBorderColor':'#9C6B12','activationBkgColor':'#EEF2F3','activationBorderColor':'#C7D1D2','sequenceNumberColor':'#FFFFFF'}}}%%
sequenceDiagram
  autonumber
  participant P as 管理員（更新中心）
  participant M as 主系統 api／worker
  participant U as 更新站＋R2
  participant H as 主機 systemd
  P->>M: 立即檢查（另有每日排程）
  M->>U: /v1/check product=host
  U-->>M: 新版、簽好的清單、三段說明
  P->>M: 更新到 X
  Note over P: 全頁遮罩，只顯示進度
  M->>U: /v1/download-url（15 分鐘預簽網址）
  M->>U: 下載包與 ops 小檔（背景工作、可續傳）
  M->>M: 用本機公鑰驗章、比對雜湊
  M->>H: 寫更新單 upgrade/orders/<job>.json
  H->>H: guidant-upgrade-order.path 撿單 → 重驗包與章
  H->>H: 0 換維運指令 → 1 預檢 → 2 等工作做完 → 3 複製資料庫
  H->>H: 4 在複本套新版 → 5 健康檢查 → 6 換成新版 → 7 清理
  Note over H: 2～6 前門顯示維護頁；任一步失敗自動退回
  H-->>M: 進度檔每步更新
  M-->>P: 八步進度；完成後回更新中心
```

### 2.2 主系統發動 Agent 更新

```{.mermaid cap="圖 2 — 主系統發動 Agent 更新：手上沒有最新包就先下載，下載完自動推；不走心跳。"}
%%{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','titleColor':'#14201F','clusterBkg':'#FBFCFC','clusterBorder':'#E4EAEB','actorBkg':'#E2F0F1','actorTextColor':'#14201F','actorBorder':'#0E7C86','actorLineColor':'#C7D1D2','signalColor':'#4A5A5C','signalTextColor':'#14201F','labelBoxBkgColor':'#EEF2F3','labelBoxBorderColor':'#C7D1D2','labelTextColor':'#14201F','loopTextColor':'#14201F','noteBkgColor':'#F6EBD5','noteTextColor':'#14201F','noteBorderColor':'#9C6B12','activationBkgColor':'#EEF2F3','activationBorderColor':'#C7D1D2','sequenceNumberColor':'#FFFFFF'}}}%%
sequenceDiagram
  autonumber
  participant P as 管理員（更新中心 Agent 區）
  participant M as 主系統
  participant U as 更新站＋R2
  participant A as Agent 容器
  participant H as Agent 主機 systemd
  P->>M: 勾選 N 台，更新選取的 N 台到 X
  alt 手上的包不是最新（沒有包，或比更新站查到的舊）
    M->>U: 下載 Agent X（背景工作，畫面有進度、可取消）
    M->>M: 驗 agent_release_manifest 章、雜湊，存 agent-bundles/X/
    Note over M: 下載中再有人按，併入待推清單
  end
  M->>M: 建推送列：先 1 台，成功再推其餘
  M->>A: 直推 POST /update {版號, sha256, 大小}（mTLS＋60 秒 JWT）
  Note over A: 進入維護模式：停收新工作
  A->>M: GET /agents/update/bundle 下載包（可續傳）
  A-->>M: 事件 update_download
  A->>A: 核大小與雜湊，寫升級單 agent-upgrade-order.json
  H->>H: guidant-agent-upgrade-order.path 撿單
  H->>H: 用舊版 Agent 驗新版程式樹的章 → 跑升級步驟
  A-->>M: 事件 update_step（每步）
  H->>H: 換新容器、健康檢查
  A-->>M: 事件 update_result＋上傳升級紀錄
  M-->>P: 每台八步進度與結果；成功清用不到的包
```

Agent 當時離線，主系統會在它下次心跳回來時補推一次。心跳本身不帶任何指令。

### 2.3 Agent 主控台發動更新

```{.mermaid cap="圖 3 — Agent 主控台按「更新到 X」：請主系統替這一台做圖 2 的事。"}
%%{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','titleColor':'#14201F','clusterBkg':'#FBFCFC','clusterBorder':'#E4EAEB','actorBkg':'#E2F0F1','actorTextColor':'#14201F','actorBorder':'#0E7C86','actorLineColor':'#C7D1D2','signalColor':'#4A5A5C','signalTextColor':'#14201F','labelBoxBkgColor':'#EEF2F3','labelBoxBorderColor':'#C7D1D2','labelTextColor':'#14201F','loopTextColor':'#14201F','noteBkgColor':'#F6EBD5','noteTextColor':'#14201F','noteBorderColor':'#9C6B12','activationBkgColor':'#EEF2F3','activationBorderColor':'#C7D1D2','sequenceNumberColor':'#FFFFFF'}}}%%
sequenceDiagram
  autonumber
  participant O as 操作者（Agent 主控台 8444）
  participant A as Agent
  participant M as 主系統
  participant U as 更新站
  O->>A: 立即檢查
  A->>M: POST /agents/update/check（通行證＋DPoP）
  M->>U: 查 Agent 線（全機 5 分鐘節流）
  A->>M: GET /agents/update/available
  M-->>A: 有新版 X（手上沒包也照報）
  O->>A: 更新到 X
  Note over O: 按下當下整頁鎖住
  A->>M: POST /agents/update/request（操作者名稱）
  M->>M: 手上不是最新 → 排下載，記「agent-console:操作者」
  M->>A: 下載完直推 POST /update（之後同圖 2 第 7 步起）
```

### 2.4 離線上傳

Agent 主控台「離線上傳更新包」：上傳 `.offline.tar.gz`，Agent 依序驗章（用**現行版**的公鑰，不用包裡的）、雜湊、版本、磁碟，過了就寫升級單，主機照圖 2 第 11 步起跑。主系統斷線時主控台只提供這條路。

### 2.5 Agent 什麼時候算「升級中」、什麼時候鎖畫面

| 狀態 | 怎麼判 | 影響 |
|---|---|---|
| 升級中（維護模式） | Agent 在下載主系統推來的包、在等進行中的工作做完、或升級單已寫好主機還在跑 | 停收新工作；主控台狀態回 `in_progress` |
| 主控台鎖畫面 | 主控台問狀態時拿到 `in_progress` | 整頁鎖住，每 3 秒更新進度 |
| 解鎖 | `in_progress` 消失 | 版本換了就整頁重新載入回原頁；沒換（失敗退回）就解鎖顯示結果 |

主控台平常每 30 秒問一次狀態，所以**由主系統發動**時開著的主控台最慢 30 秒才鎖；在主控台自己按的當下就鎖。

---

## 3. 測試機同版重套 {#reapply nav="同版重套"}

已是最新版時，把目前這一版照正式流程重跑一次。只給測試機用，**正式環境沒有這兩個鍵就不會出現按鈕**。

| 開關 | 放哪 | 誰讀 |
|---|---|---|
| `PRODUCT_UPDATE_ALLOW_SAME_VERSION=1` | `/srv/guidant-ai/guidant.env` | 主系統 api（出按鈕、驗章放行同版）、主機升級腳本 `upgrade_order.sh`（放行同版單） |
| `AGENT_UPDATE_ALLOW_SAME_VERSION=1` | Agent 安裝目錄 `.env`（compose 轉進容器） | Agent 容器（接同版推送、離線上傳同版）、Agent 主機升級腳本 `upgrade_order.sh`（放行同版單） |

| 按鈕 | 在哪 | 需要哪個開關 |
|---|---|---|
| 重新套用目前版本 | 主系統更新中心 | 主系統 |
| 重新套用目前 Agent 版本 | 更新中心 Agent 區 | 主系統＋Agent |
| 重新套用 X | Agent 主控台更新頁 | 主系統＋Agent |

改完開關要讓容器重讀：主系統 `sudo guidantai start`、Agent `sudo guidant-agent-compose start`（`restart` 不會重讀設定）。降版、跨主版號不論開關一律擋。目前只開在 190。

---

## 4. 程式在哪 {#programs nav="程式"}

### 4.1 主系統（`compliance-manager-be`）

| 用途 | 檔案 |
|---|---|
| 更新中心 API 路由 | `api/product_update/__init__.py`、`routes/product_update_route.py` |
| 更新中心總入口 | `app/product_update/service/product_update_app_service.py` |
| 主系統自己升級（檢查、下載、寫單） | `product_update_flow.py`、`update_site_client.py`、`package_downloader.py`、`package_verifier.py`、`upgrade_order.py` |
| Agent 版本線（檢查、下載、判斷手上是否最新） | `agent_release_service.py` |
| Agent 推送（分批、暫停、重推、同版重套） | `agent_rollout_service.py`、`agent_rollout_views.py` |
| Agent 升級事件 | `agent_update_event_handlers.py` |
| 同版重套 | `same_version.py` |
| 直推 `/update` 給 Agent | `core/plugins/_agent_commands.py`（`push_update_soon`） |
| Agent 下載包端點 | `core/plugins/_agent_update.py`、`api/remote_agent/routes/agent_update_bundle_route.py` |
| Agent 主控台請主系統查、報、更新 | `core/plugins/_agent_console_update.py`、`api/remote_agent/routes/agent_update_request_route.py` |
| Agent 事件與升級紀錄上傳 | `api/remote_agent/routes/agent_events_route.py`、`agent_update_log_route.py` |
| 主機升級腳本 | `scripts/installer/lib/upgrade_order.sh`（撿單）、`upgrade_runner.sh`（跑八步）、`upgrade_preflight.sh`、`upgrade_drain.sh`、`upgrade_db_copy.sh`、`upgrade_apply_on_copy.sh`、`upgrade_health.sh`、`upgrade_switch.sh`、`upgrade_cleanup.sh`、`upgrade_rollback.sh`；systemd 單元 `scripts/installer/systemd/guidant-upgrade-order.path`／`.service` |
| 出包 | `scripts/build/build_all.sh`、`build_bundle.sh`、`sbom.sh`、`release_json.sh`、`smoke_auto.sh` |

### 4.2 Agent（`evidence-agent`）

| 用途 | 檔案 |
|---|---|
| 收 `/update` 指令 | `guidant_agent/sync/api/cmd_update.py` |
| 下載包、寫升級單、判斷升級中、同版開關 | `guidant_agent/sync/app/update_sync.py` |
| 升級事件、升級紀錄上傳 | `guidant_agent/sync/app/update_events.py`、`update_log_upload.py` |
| 主控台更新頁 API | `guidant_agent/console/api/update_routes.py`、`console/app/update_console.py` |
| 主控台 8444 伺服器（白名單、TLS 握手） | `guidant_agent/identity/app/allow_cidr.py` |
| 主機升級腳本（隨 Agent 映像出貨，主機每次從映像讀出來跑） | `scripts/install/lib/upgrade_order.sh`、`upgrade_runner.sh`、`upgrade_guard.sh`、`progress.sh`；systemd 單元 `scripts/install/lib/systemd/` |
| 主機維運指令 `guidant-agent-compose` | `deploy/install.sh` 安裝 |
| compose 與開關轉傳 | `deploy/docker-compose.yml` |
| 出包 | `scripts/build/build_all.sh`、`build_agent_release.sh`、`build_agent_bundle.sh`、`build_compose_bundle.sh` |
| 版本說明（出包時讀進 `notes.json`） | `RELEASE_NOTES.md` |

### 4.3 前端（`compliance-manager-fe`）

| 用途 | 檔案 |
|---|---|
| 主系統更新中心 | `src/views/product-update/UpdateCenter.vue`、`UpdateConfirmDialog.vue`、`UpdateHistory.vue`、`productUpdateState.js`；`src/composables/useProductUpdate.js` |
| 更新中心 Agent 區 | `src/views/product-update/AgentRolloutPanel.vue`、`agentRolloutState.js`；`src/composables/useAgentRollout.js` |
| Agent 主控台更新頁 | `console/views/Update.vue`、`console/update.js`、`console/updateApi.js`、`console/useUpdateStatus.js`（輪詢與鎖定）、`console/components/UpdateLockMask.vue`、`OfflineUpdateUpload.vue` |

### 4.4 更新站（`update-center`）

| 用途 | 檔案 |
|---|---|
| 兩條版本線、R2 路徑 | `src/releases.ts`（`r2Key()` 是 R2 路徑唯一拼法） |
| 客戶端三支端點、後台 | `src/routes/client.ts`、`src/routes/admin.ts` |
| 正式發布 CLI | `cli/guidant-release.ts` |

---

## 5. 這一輪做的優化 {#optimizations nav="優化"}

| 優化 | 效果 |
|---|---|
| Agent 區一步更新 | 不必先按「預先下載」；手上沒有或只有舊包都會自動下載最新版再推 |
| 升級中維護模式 | 升級時不再收新工作；主控台鎖畫面、換容器後不必重新登入 |
| 主控台立即檢查真的查 | 新版發布後主控台立刻看得到 |
| 更新包自動清理 | 整批推完清 Agent 舊包；主系統升級成功只留清單，各省約 1.7～3.5 GB |
| SeaweedFS 殭屍行程 | 不再每 3 小時要重啟 |
| 8444 強制重新整理不再掉 CSS | 一條卡住的連線不影響其他連線 |
| 同版重套 | 測試機重看升級流程不必再出一版 |
| 出包：測試包略過 SBOM＋弱點掃描 | Agent 包 19 分 30 秒 → 15 分 52 秒；主系統約省 5 分 |
| 出包：主系統封包多核壓縮 | 預估省約 4 分（188 有 24 核） |
| 出包：釘版第三方 image 匯出檔快取 | 主系統 5 顆（約 3 GB）、Agent 1 顆不再每版重匯 |

主系統完整出包約 51 分鐘：Nuitka 編譯約 19 分、封包壓縮約 13 分、SBOM＋弱點掃描約 9 分、其他約 10 分。Nuitka 在 188 本來就沒開 LTO，`--lto=no` 不會更快。

---

## 6. 這一輪沒做完的 {#not-done nav="沒做完"}

| 事 | 白話說明 | 誰接 |
|---|---|---|
| 正式版出版 | 目前上站的都是測試章簽的內部測試包；正式章／Vault 簽的第一版是 1.29 | FR-173 |
| 189 POC 升級 | 仍 1.27.x；升 1.28.x 時 Agent 要手動升一次（1.4.x 沒有 `/update`） | 待決策者裁時機 |
| release note 1.27.x～1.28.x | 還沒寫 | 待決策者裁 |
| 主控台背景輪詢寫稽核 | 主控台每次問更新狀態都記一筆「看過更新頁」，數量不實 | 未開卡 |
| 主系統發動時主控台最慢 30 秒才鎖 | 平常輪詢間隔 | 決策者裁先這樣 |
| 只改主機腳本也得出版 | 主機每次從現行 Agent 映像讀升級腳本，不能只換 `.sh` | 未開卡，另議 |
| 守衛測試 8 條紅 | 乾淨 HEAD 一樣紅，非本案造成 | 未開卡 |

---

## 7. 延伸出去的 FR {#spinoffs nav="延伸 FR"}

| FR | 在做什麼 | 為什麼會長出來 | 狀態 |
|---|---|---|---|
| [FR-171](../FR-171-2610-agent-command-channel/) | 主系統直推指令給 Agent、事件端點，心跳不再帶訊息 | 172 的「叫 Agent 升級」「回報進度」要用這條通道 | 結案 |
| [FR-173](../FR-173-2611-release-pipeline/) | BE／Agent／FE 各一條 CI，蓋章改用自架 Vault | 172 把出包分家後，出版要自動化、私鑰不放在個人電腦 | 開卡未派 |
| [FR-174](../FR-174-2611-url-profile-snapshot-sync/) | 網址型檢測基準匯入時存成檔案 | 原 172 的 H-5 卡範圍太大，移出 | 結案 |

閱讀順序：先看本頁 → FR-171 FINAL-SPEC（通道）→ FR-173（出版自動化）。

---

## 8. 這份文件的定位 {#positioning nav="定位"}

| 你想知道 | 看哪份 |
|---|---|
| 現在長什麼樣、程式在哪 | 本頁 |
| PM 常問的問題 | [for-pm-qa.md](for-pm-qa.md) |
| 每一步誰在跑、跑哪支（循序圖＋逐步表） | [upgrade-flow-guide.md](upgrade-flow-guide.md)；FR-175 [機制問答 · 線上升級執行流程](../FR-175-2611-mechanism-qa-hub/qa/mechanism-qa.html#flow) |
| 為什麼這樣設計 | [design.md](design.md)（v1.0）→ [design-v1.1.md](design-v1.1.md)（190 實測後的異動） |
| 190 實測回饋原文 | [handoff/190-test-feedback.md](handoff/190-test-feedback.md) |
| 哪一棒做了什麼 | [handoff/FR-172-LOG.md](handoff/FR-172-LOG.md) |
| 怎麼手動出版 | FR-167 `release-runbook.md` |
