---
title: "Agent 獨立更新 — 升級執行流程 (FR-172)"
brand: "Guidant AI · **FR-172** 升級執行流程"
eyebrow: "FR-172 · 說明 · 循序圖"
h1: "升級執行流程"
subtitle: "主系統升級、主系統發動 Agent 升級、Agent 主控台發動"
lede: "三張循序圖加兩張逐步表，說明按下更新後每一步誰在跑、跑哪支程式、畫面看到什麼。與 FR-175 機制問答「線上升級執行流程」同一份內容；那邊另有補問「升級觸發機制：更新單與主機監看服務」。"
chips: [{text: "主系統 1.28.3／Agent 1.5.5", kind: plain}, {text: "3 張循序圖", kind: accent}]
footer: "FR-172 升級執行流程 · 同步自 FR-175 qa/mechanism-qa.html#flow · 現況見 FINAL-SPEC.md"
---

## 設計原則 {#principles nav="原則"}

- **容器不動 docker，主機才動**：主系統後端與 Agent 都只寫一張「升級單」交給自己那台主機，停容器、換版、退回全部由主機上的 systemd 單元以 root 執行。容器在升級中會被換掉，不能當執行者。
- **狀態只有一個來源**：主機每步改寫進度檔，畫面、維護頁、開機檢查都讀這一份，所以容器停了進度照樣會動。
- **Agent 不連外網**：Agent 的包一律由主系統向更新站下載、驗章，再交給 Agent。
- **不靠心跳**：主系統直接連 Agent 的 8443 推指令；Agent 主動送事件回報進度。心跳只表示「活著」。

## 主系統升級 {#host nav="主系統升級"}

```{.mermaid cap="圖 1 — 主系統升級。編號為時間順序；灰色區段後端與資料庫停機，畫面由前門 nginx 的維護頁提供，維護頁直接讀進度檔。190 實測 1.28.2→1.28.3 整次 6 分 42 秒，停機約 4.5 分鐘。"}
%%{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 B as 瀏覽器（更新中心）
    participant N as 前門 guidant-fe（nginx）
    participant M as 後端 guidant-api／worker
    participant S as 更新站＋R2
    participant H as 主機 systemd（root）
    participant P as 進度檔 upgrade-progress.json
    B->>M: 立即檢查（另有每日排程）
    M->>S: /v1/check product=host
    S-->>M: 新版、簽好的清單、三段說明
    B->>M: POST …/apply「更新到 X」
    Note over B: 全頁遮罩，只顯示進度
    M->>S: /v1/download-url（15 分鐘預簽網址）
    M->>S: 下載整包與 ops 小檔（背景工作、可續傳）
    M->>M: 本機公鑰驗章、比對雜湊
    M->>H: 寫更新單 upgrade/orders/<job>.json
    H->>H: guidant-upgrade-order.path 看到檔 → .service → guidantai upgrade --from-order
    H->>H: 重驗包與章（容器寫的單不全信）
    H->>P: 第 0 步 換新版維運指令與維護頁
    H->>P: 第 1 步 預檢、第 2 步 放 draining 旗標等工作歸零
    Note over M: worker 不撿新單、不推新工作給 Agent；API 照常
    rect rgb(236,236,236)
    Note over B,P: 第 3～6 步：後端與資料庫停機，畫面由前門維護頁提供
    H->>H: 第 3 步 停 api／worker／socketio／db，複製資料庫
    B->>N: 輪詢拿到 502 → 整頁 reload
    N-->>B: 探後端不通 → 送維護頁
    B->>N: 每 3 秒 GET /upgrade-progress.json
    N->>P: 直接讀唯讀掛載的檔（不經後端）
    H->>H: 第 4 步 在複本 load 映像、跑 migration
    H->>H: 第 5 步 用複本起新版，打 version／login／三支 API
    H->>H: 第 6 步 改 .env、換目錄、依序起服務，最後重建 fe
    end
    H->>P: 第 7 步 清理舊映像、舊快照；status=succeeded
    N-->>B: 探後端通了 → 跳回更新中心 ?upgraded=1
    M->>P: 排程每分鐘讀：同步單終態、補寫系統日誌、回報更新站
    M->>M: agent-bundles 有相容新版 → 自動建 Agent 推送列（接圖 2）
    rect rgb(253,236,236)
    Note over B,P: 任一步失敗或逾時
    H->>H: 第 8 步 rollback：.env 寫回舊值、起舊版
    H->>P: rolled_back（退回也失敗寫 rollback_failed）
    end
```

| 步驟 | 誰在跑、跑哪支 | 做什麼 | 使用者看到 |
|---|---|---|---|
| 按下「更新到 X」 | 後端 `product_update_flow.py`、`package_downloader.py`、`package_verifier.py`、`upgrade_order.py` | 沒包就先下載（背景工作），用本機公鑰驗章與雜湊，寫更新單 `upgrade/orders/<job>.json`。後端到此為止，不碰 docker | 全頁遮罩、八步清單，每 3 秒問 `GET …/status` |
| 單落地 | 主機 `guidant-upgrade-order.path` → `.service` → `lib/upgrade_order.sh` → `lib/upgrade_runner.sh` | 撿單就刪單，重驗包與章、確認版號比現行新（測試機開同版開關時同版也放行） | 「已送出」 |
| 0 換維運指令 | 主機 `self_replace.sh` | 從 ops 小檔取新版 `guidantai`、`lib/`、維護頁；exec 新版續跑 | 照常 |
| 1 預檢 | 主機 `upgrade_preflight.sh` | 簽章、雜湊、docker、資料庫、磁碟等；不過就停，系統沒動 | 照常 |
| 2 停接新工作 | 主機 `upgrade_drain.sh`；後端 `core/maintenance_gate.py` | 放 `draining` 旗標：worker 不撿新單、不推新工作給 Agent；等進行中的歸零 | 照常操作，新工作先排隊 |
| 3 停服務、複製資料庫 | 主機 `upgrade_db_copy.sh` | 停 api／worker／socketio／db（fe、redis、seaweedfs 不停），複製 `pgdata` → `pgdata-next` | 維護頁（前門 nginx 直接讀進度檔） |
| 4 在複本上升級 | 主機 `upgrade_apply_on_copy.sh` | 解包、load 映像並核雜湊、db 指到複本、init 容器跑 migration、補設定 | 維護頁第 4 步 |
| 5 健康檢查 | 主機 `upgrade_health.sh` | 用複本起新版 api，打 version、login、三支 API | 維護頁第 5 步 |
| 6 換接 | 主機 `upgrade_switch.sh` | 改 `.env` 版號與資料庫目錄、搬目錄、依序起服務，最後重建 fe；寫 `last-switch.json` | fe 重建那幾秒看不到畫面，之後跳回更新中心 |
| 7 清理 | 主機 `upgrade_cleanup.sh` | 刪舊映像、只留一代資料庫快照；升級成功後 `incoming/` 只留清單 | 新版畫面 |
| 換接後一分鐘內 | 後端排程 `core/scheduler_update.py` | 同步單終態、補寫系統日誌 6239～6248、回報更新站；有相容 Agent 新版就自動建推送列 | 現行版與「上次更新完成時間」 |
| 失敗或逾時 | 主機 `upgrade_rollback.sh` | 停新版、`.env` 寫回、起舊版；第 0～2 步失敗系統沒動只記 skipped | 維護頁「已退回」或「需主機管理員」 |

## 主系統發動 Agent 升級 {#agent-from-host nav="主系統發動"}

```{.mermaid cap="圖 2 — 主系統發動 Agent 升級。按更新不必先預先下載；Agent 當時離線，主系統會在它下次心跳回來時補推。開著的主控台最慢 30 秒內鎖住（平常輪詢間隔），鎖住後每 3 秒更新進度。"}
%%{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 B as 瀏覽器（更新中心 Agent 區）
    participant M as 主系統後端
    participant S as 更新站＋R2
    participant A as Agent 容器（8443）
    participant H as Agent 主機 systemd（root）
    participant C as Agent 主控台（8444）
    B->>M: 勾選 N 台，「更新選取的 N 台到 X」
    alt 手上沒有包，或手上的包比更新站查到的舊
    M->>S: 下載 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: 進入維護模式：停收新工作
    C->>A: 每 30 秒問狀態 → 拿到 in_progress → 整頁鎖住
    A->>M: GET /agents/update/bundle 下載包（可續傳）
    A-->>M: 事件 update_download
    A->>A: 核大小與雜湊，寫升級單 agent-upgrade-order.json
    H->>H: guidant-agent-upgrade-order.path 看到單 → upgrade-host order
    H->>H: 從現行 Agent 映像讀出 upgrade_order.sh，用舊版 Agent 驗新版程式樹的章
    H->>H: 預檢 → 等工作做完 → 換上新版設定 → 重建容器 → 健康檢查 → 清理
    A-->>M: 事件 update_step（每一步，失敗帶錯誤輸出末 20 行）
    A-->>M: 新容器起來：事件 update_result＋上傳升級紀錄
    M-->>B: 每台八步進度與結果；整批成功後清用不到的包
    C->>A: 版本換了 → 整頁重新載入回原頁（不必重登）
```

| 畫面那一格 | 誰在跑、跑哪支 | 做什麼 |
|---|---|---|
| 等 Agent 接手 | 主系統 `agent_rollout_service.py`、`core/plugins/_agent_commands.py`；Agent `sync/api/cmd_update.py` | 主系統直推 `POST /update`；Agent 收下即進入維護模式（停收新工作） |
| 下載更新包 | Agent `sync/app/update_sync.py`；主系統 `GET /agents/update/bundle` | 向主系統下載包、核大小與雜湊，送 `update_download` 事件 |
| 解開更新包 | Agent 主機 `upgrade_order.sh`（從現行 Agent 映像讀出來跑） | 撿升級單、預檢版號（同版只有開關開才放行）、磁碟、解包、用舊版 Agent 驗新版程式樹的章 |
| 預檢 | Agent 主機 `upgrade_runner.sh` | 主系統位址必須 https、工具差異提醒、空間 |
| 停接新工作，等進行中的做完 | 同上、`upgrade_guard.sh` | 等執行中的掃描做完 |
| 換上新版 | 同上 | 先備份 `.env` 與 compose 檔，再換 compose 檔與映像版本 |
| 啟動新版與健康檢查 | 同上 | 重建容器、確認版本與資料 |
| 清理 | 同上 | 維運指令換新；只記警告，不影響結果 |
| （結果） | Agent `update_events.py`、`update_log_upload.py` | 新容器起來送 `update_result`、上傳升級紀錄；換版後失敗自動還原 `.env` 與 compose 檔 |

## Agent 主控台發動 {#agent-from-console nav="主控台發動"}

```{.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 S as 更新站
    O->>A: 立即檢查
    A->>M: POST /agents/update/check（通行證＋DPoP）
    M->>S: 查 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 步起）
    rect rgb(236,236,236)
    Note over O,S: 與主系統斷線時
    O->>A: 離線上傳 .offline.tar.gz
    A->>A: 用現行版公鑰驗章 → 雜湊 → 版本 → 磁碟 → 寫升級單（同圖 2 第 12 步起）
    end
```

## 常見問題 {#faq nav="常見問題"}

- **主系統停機時，畫面是誰在服務？**前門 `guidant-fe`（nginx）從不停。它探到後端不通就改送維護頁，維護頁每 3 秒直接讀進度檔，不經後端、不經資料庫。
- **Agent 升級時還能派掃描嗎？**不能。從收到 `/update` 起 Agent 就停收新工作，直到換版完成。
- **Agent 主控台為什麼有時慢一點才鎖？**主控台平常每 30 秒問一次狀態；由主系統發動時最慢 30 秒才鎖，在主控台自己按的當下就鎖。這段時間 Agent 已經停收新工作。
- **Agent 不連外網，包怎麼來？**一律由主系統向更新站下載、驗章後交給 Agent；離線時才由現場人員上傳離線包，驗章用 Agent 現行版的公鑰，不用包裡附的。
- **升級中有人又按更新？**主系統更新單同時只跑一張；Agent 下載中再有人按，會併入同一份待推清單，不覆蓋先按的人。
- **測試機想重看升級流程？**兩個環境變數都開才出現「重新套用目前版本」：主系統 `PRODUCT_UPDATE_ALLOW_SAME_VERSION=1`、Agent `AGENT_UPDATE_ALLOW_SAME_VERSION=1`。出貨範本不帶這兩個鍵，正式環境不會出現。

## 相關文件 {#refs nav="相關"}

| 想看 | 去哪 |
|---|---|
| 升級觸發機制（更新單、主機 systemd 監看服務） | [FR-175 機制問答 · 線上升級執行流程](../FR-175-2611-mechanism-qa-hub/qa/mechanism-qa.html#flow) 補問 |
| 程式清單、優化項目、交付物現況 | [FINAL-SPEC.md](FINAL-SPEC.md) |
| PM 常問的 16 題 | [for-pm-qa.md](for-pm-qa.md) |
