FR-167 · 技術規格 · 2026-11-01
更新紀錄 product_update_jobs、Agent 跟進列 agent_update_rollouts、主機進度檔 upgrade-progress.json
線上更新有三份會變的狀態:主系統資料庫的更新紀錄(一列一次換版)、Agent 跟進列(一台 Agent 一列),以及主機上的進度檔(升級程式唯一的輸出)。主系統不能叫主機做事,只能寫更新單;主機也不能打主系統,只能寫進度檔。所以更新紀錄的後半段(排程之後)全靠主系統每分鐘讀進度檔同步。本頁每個狀態值與轉移條件都對到程式碼行號。
| 狀態 | 存在哪 | 誰寫 | 誰讀 | 怎麼跟其他兩份對上 |
|---|---|---|---|---|
| 更新紀錄 | public.product_update_jobs.status |
主系統後端(使用者操作、下載 worker、每分鐘同步排程) | 更新中心畫面 | 寫更新單時記 job_id;同步排程以 job_id 找進度檔 |
| Agent 跟進列 | public.agent_update_rollouts.status |
主系統後端(每分鐘排程、心跳、更新中心 Agent 區) | 更新中心 Agent 區、心跳下發 | 進度檔 status=succeeded 且 to=現行版時建列;host_version=主系統那一版 |
| 進度檔 | 主機 <資料目錄>/upgrade/upgrade-progress.json |
只有主機的 scripts/installer/lib/progress.sh |
維護頁、更新中心、開機檢查、診斷包 | job_id=更新單的 job_id |
Agent 主機上另有一份同契約的進度檔(evidence-agent scripts/install/lib/progress.sh),由 Agent 讀了隨心跳帶回主系統,推進跟進列,見 §3.4。
主系統讀進度檔的地方有兩個(core/scheduler_update.py:39-67、app/product_update/service/product_update_app_service.py:130-150):
| 讀的時機 | 做什麼 |
|---|---|
每分鐘排程 product_update_order_sync |
sync_from_progress 同步紀錄 → after_switch 清舊檢查並重查新版 → 回報更新站(一次)→ 補寫系統紀錄(一次)→ Agent 跟進 tick → Agent 結果回報更新站 |
畫面輪詢 GET /system/product-update/status |
after_switch(recheck=False):換接剛完成時立刻同步紀錄,不等排程;不打更新站 |
允許值見 CHECK 約束(scripts/sql/2026-10-29-fr167-product-update-jobs-failed-status.sql)與 domain/product_update/entity/product_update_job_entity.py:5-6。
| 狀態 | 意思 | 包在不在主機 incoming/ |
終態? |
|---|---|---|---|
checking |
保留值。允許但目前沒有任何程式寫它;「檢查中」是 last_check 系統設定,不建紀錄列 |
— | — |
downloading |
下載 worker 正在下載(線上) | 下載中(.part) |
否 |
ready |
包已下載或上傳、驗過,等管理員按更新(或等自動寫單) | 在 | 否 |
scheduled |
已寫更新單;立即更新的 not_before=寫單當下 |
在 | 否 |
running |
主機進度檔顯示這張單正在跑 | 在 | 否 |
succeeded |
主機換接完成、新版已開放使用 | 在(本版留著) | 是 |
rolled_back |
主機升級失敗、已自動退回 | 在 | 是,但可重試 |
skipped |
這次沒有升級、系統沒動(預檢不過、等不到工作做完、執行前驗包不過、開機檢查或 recover 發現第 3 步前就斷) | 在 | 是,但可重試 |
cancelled |
管理員取消下載或取消就緒的包 | 已刪 | 是 |
failed |
包已刪(下載失敗、驗章或雜湊不過、離線上傳不過),或主機回報退回也失敗(rollback_failed) |
下載失敗時已刪 | 是 |
後端判「已結束、不再同步」的集合是 succeeded、rolled_back、failed、cancelled(app/product_update/service/upgrade_order.py:61)。skipped 不在裡面,但同一個 job_id 不會再有新的進度檔,實際上也不會再變。
%%{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'}}}%%
stateDiagram-v2
[*] --> downloading: 按下載/按更新到 X(包還沒下)
[*] --> ready: 離線上傳驗過
[*] --> failed: 離線上傳驗不過
downloading --> ready: worker 下載、逐段驗過
downloading --> failed: 下載失敗/驗章或雜湊不過
downloading --> cancelled: 按取消下載
ready --> scheduled: 按立即/排程(寫更新單)
ready --> cancelled: 按取消(刪包)
scheduled --> ready: 按取消(刪單或寫取消旗標)
scheduled --> running: 進度檔 running
scheduled --> succeeded: 進度檔已 succeeded(同步晚到)
scheduled --> skipped: 進度檔 skipped
scheduled --> rolled_back: 進度檔 rolled_back
scheduled --> failed: 進度檔 failed/timeout 未退回
running --> ready: 按取消(第 0~2 步)
running --> succeeded: 進度檔 succeeded(最後一段)
running --> skipped: 進度檔 skipped
running --> rolled_back: 進度檔 rolled_back
running --> failed: 進度檔 failed/timeout 未退回
rolled_back --> scheduled: 按重試
skipped --> scheduled: 按重試
succeeded --> [*]
cancelled --> [*]
failed --> [*]
| 從 → 到 | 觸發 | 條件 | 寫入者 | 位置 |
|---|---|---|---|---|
∅ → downloading |
POST /system/product-update/download,或 POST …/apply 時包還沒下 |
最近一次檢查有新版;沒有 downloading/ready 的列,否則 409 PRODUCT_UPDATE_BUSY |
後端(操作者身分) | product_update_flow.py:385-419;upgrade_order.py:279-307 |
∅ → ready |
離線上傳完成 | 清單驗章、大小與 sha256 對得上 | 後端 | product_update_flow.py:618-640 |
∅ → failed |
離線上傳驗不過 | — | 後端 | product_update_flow.py:602-607 |
downloading → ready |
下載 worker 完成 | 路徑每段都下完、驗過 | worker | product_update_flow.py:474-513 |
downloading → failed |
下載 worker 失敗 | 下載失敗、驗章不過、授權照拿不到;ended_at 一起寫 |
worker | :493-501、:432-439 |
downloading/ready → cancelled |
DELETE /system/product-update/download |
最新一列是 downloading 或 ready;包同時刪 |
後端 | :524-550 |
ready → scheduled |
POST …/apply(立即或排程),或下載完成時自動寫單 |
每段重驗包;記 job_id、applied_by、not_before(立即=當下)、with_agents、agent_uids |
後端 | upgrade_order.py:255-276、:218-229、:342-365 |
scheduled → 原狀態 |
寫更新單失敗 | 改紀錄與寫單不在同一個交易,寫單失敗就改回 | 後端 | :232-239、:270-272 |
scheduled/running → ready |
DELETE …/apply |
單還在=刪單;單不在=主機已開跑,最後一步 <3 才寫 .cancel,否則 409 PRODUCT_UPDATE_TOO_LATE_TO_CANCEL;記 cancelled_by |
後端 | :374-385、:247-250 |
scheduled → running/終態 |
每分鐘同步或畫面輪詢 | 進度檔 job_id 對得上(見 §2.4) |
同步排程(系統身分) | :432-473 |
running → 終態 |
同上 | 同上;終態時寫 ended_at、error |
同步排程 | 同上 |
rolled_back/skipped → scheduled |
POST …/retry |
最新一列是這兩種;用 incoming/ 同一個包重驗、寫新 job_id,mode=now;跨版只跑還沒跑的段 |
後端 | :368-371、:166-173 |
已就緒但下載完時已過排定時間:自動寫單不改成立即,紀錄留在 ready 並記 error「下載完成時已超過排定的更新時間,請重新選擇時間」(upgrade_order.py:350-353)。
job_status_from_progress(upgrade_order.py:432-451)。只對 job_id 相同的那一列動;紀錄已在終態或是 ready(已被取消)就不動(:455-476)。
進度檔 status |
進度檔 result |
紀錄改成 |
|---|---|---|
running |
— | running |
succeeded |
— | 跨版且這一段的 to 不是最後一段=running;否則 succeeded |
skipped |
— | skipped |
failed/timeout |
rolled_back |
rolled_back |
failed/timeout |
unchanged |
skipped |
failed/timeout |
其他(rollback_failed、null) |
failed |
preflight 或其他 |
— | 不動 |
陷阱:跨版中間段的 succeeded 不是整張單成功
跨版時每段各寫一份進度檔、job_id 相同。中間那段寫 succeeded 時若把紀錄收成 succeeded,第二段的結果再也寫不回來(upgrade_order.py:435-436)。
陷阱:取消後主機寫的 skipped 不可蓋回去
管理員按了取消,紀錄已回 ready(包還在、可以重排)。主機之後寫的 skipped 要跳過,所以同步時 ready 不動(upgrade_order.py:458、:464)。
陷阱:退回失敗在紀錄上只是 failed
rollback_failed 沒有自己的紀錄狀態,紀錄寫 failed。畫面要分辨「包驗不過、要重新下載」與「退回也失敗、要 IT 處理」,一律再看進度檔 result(FE failureKind)。
display_status(upgrade_order.py:192-204)只換回應、不寫 DB:紀錄是 scheduled、時間已到、主機還沒為這張單寫進度檔(host_step < 0)→ 回 submitted。DB 留 scheduled,取消與重複按的擋法照舊認它。
前端另有兩條判 submitted 的規則(compliance-manager-fe src/views/product-update/productUpdateState.js:73-91):scheduled 且 not_before 已到;ready 且有 applied_by、沒有 job_id、沒有 error、沒有被取消(「更新到 X」建的下載單剛就緒、後端還沒寫單的空窗)。
標籤取自 compliance-manager-fe src/config/locales/i18n/zh-tw/product-update.json,畫面主狀態取自 productUpdateState.js 的 deriveView。
| 紀錄狀態 | 歷史列標籤 | 歷史列顏色 | 更新中心主畫面 | 可按的鈕 |
|---|---|---|---|---|
checking |
檢查中 | info | — | — |
downloading |
下載中 | info | 「下載 {版} 中」+百分比 | 取消下載 |
ready |
已就緒 | info | 「{版} 已就緒」+預檢結果 | 立即更新、排程、取消 |
scheduled(未到) |
已排程 | info | 「{版} 已排程在 {時間} 更新」,30 秒輪詢 | 取消 |
scheduled(已到)/submitted |
已交給主機 | info | 「正在準備更新到 {版}」 | 取消 |
running |
更新中 | info | 「更新中:{舊} → {新}」+步驟列,3 秒輪詢 | 第 0~2 步可取消;第 3 步起沒有取消鈕(canCancelApply) |
succeeded |
成功 | success | 回到「已是最新版本」或下一版可更新 | — |
rolled_back |
已退回 | warning | 頁首「更新到 {新} 失敗,已自動退回 {舊},服務正常」 | 重試、下載診斷包 |
skipped |
這次不更新 | warning | 頁首「這次沒有更新到 {新},系統沒有任何變動」 | 重試 |
cancelled |
已取消 | secondary | 依最近一次檢查顯示可更新 | 下載 |
failed |
失敗 | danger | 頁首「更新沒有開始,系統沒有任何變動」;進度檔 result=rollback_failed 時改為「更新到 {新} 失敗,自動退回也沒有完成」、停輪詢 |
重新下載;退回失敗時下載診斷包 |
輪詢規則(productUpdateState.js:30-35):最新一列不在 succeeded/failed/rolled_back/cancelled/skipped 就 3 秒一次;scheduled 且排定時間還沒到時 30 秒一次;看的是「有沒有還沒結束的單」,不是畫面狀態。
陷阱:進度檔 result=unchanged 一律當「這次不更新」,不可記成 failed
開機檢查或 recover 判「第 3 步之前就斷了」時,主機寫的是 failed/unchanged(upgrade_rollback.sh:306-311),但那時系統沒動、包還在 incoming/。後端同步把 unchanged 一律記 skipped(upgrade_order.py:447-448),前端還沒同步的那一分鐘也照同一條換(productUpdateState.js:62-63),所以畫面顯示「這次不更新」+重試。
重試的判準前後端是同一套:只有 rolled_back、skipped(後端 retry,upgrade_order.py:368-371;前端 canRetry,productUpdateState.js:115-117)。failed 只代表包已刪、要重新下載;把 unchanged 記成 failed,或任一邊放寬成「failed 也能重試」,按下去都會回 412 PRODUCT_UPDATE_NOTHING_TO_RETRY。
允許值見 scripts/sql/2026-10-30-fr167-agent-update-rollouts.sql 與 domain/product_update/entity/agent_update_rollout_entity.py:5-15。一台 Agent 對同一個主系統版本只會有一列(唯一索引 (agent_id, host_version))。
| 狀態 | 意思 | 「開著的列」? | 心跳會下發? |
|---|---|---|---|
pending |
等前一批結果,或等排程時間(not_before) |
是 | 否 |
dispatched |
已下發:心跳回應帶 update,Agent 該去下載 |
是 | 是(且 Agent 仍落後、host_version 不比主系統新、包在) |
succeeded |
Agent 回報升級成功 | 否 | 否 |
failed |
Agent 回報升級失敗 | 否 | 否 |
paused |
前一批有失敗、管理員按暫停,或這台已改跟新版主系統 | 是 | 否 |
「開著的列」=pending/dispatched/paused(OPEN_STATUSES,agent_update_rollout_entity.py:12)。source 三種:following(主系統升完自動跟)、manual(更新中心推選定)、agent-only(預留,目前沒有程式寫它)。
%%{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'}}}%%
stateDiagram-v2
[*] --> dispatched: 建列,該批第一台(心跳最新)
[*] --> pending: 建列,其餘台;或推送帶排程時間
pending --> dispatched: 前一批全部成功/排程時間到
pending --> paused: 前一批有失敗/按暫停
dispatched --> succeeded: 心跳帶回成功
dispatched --> failed: 心跳帶回失敗
dispatched --> paused: 按暫停/這台改跟新版主系統
paused --> succeeded: 已下發過,心跳帶回成功;或按繼續時已是目標版
paused --> failed: 已下發過,心跳帶回失敗
paused --> dispatched: 按繼續(該批第一台)
paused --> pending: 按繼續(其餘台)
failed --> dispatched: 推選定(重排第一台)
failed --> pending: 推選定(重排其餘)
succeeded --> [*]
| 從 → 到 | 觸發 | 條件 | 寫入者 | 位置(app/product_update/service/agent_rollout_service.py) |
|---|---|---|---|---|
∅ → dispatched/pending |
每分鐘排程 tick |
進度檔 status=succeeded、to=現行版、from≠現行版;這一版還沒建過列;更新單 with_agents 不是 false;包內有 Agent 包;Agent active、未停用、落後;有勾 agent_uids 就只取勾的。第 1 批 1 台(最近心跳最新)dispatched,其餘第 2 批 pending |
排程(系統身分) | :461-481、:176-180、:155-166 |
∅ → pending → … |
POST /system/product-update/agents/push |
Agent active、落後、這一版沒有正在下發的列;照「先 1 台再其餘」排在現行版最後一批之後;帶 not_before(10 分鐘後到 60 天內)時第一台先 pending |
更新中心(登入者身分) | :590-609、:526-543、:119-126 |
pending → dispatched |
每分鐘排程 _advance |
前一批全部 succeeded;或這列有 not_before 且已到(排程推送的第一台不看前一批) |
排程 | :496-518、:104-111 |
pending → paused |
每分鐘排程 _advance |
前一批有任一台 failed;error 寫「前一批有 Agent 升級失敗,已自動暫停」 |
排程 | 同上 |
pending/dispatched → paused |
POST …/agents/pause |
現行版的 pending、dispatched 全部 |
更新中心 | :545-553 |
開著的舊列 → paused |
建新列時 | 同一台還開著的列跟的是別的主系統版;error「已改跟主系統 {版}」 |
排程或更新中心 | :483-495 |
dispatched/paused → succeeded/failed |
Agent 心跳帶 upgrade_result |
列是開著的、dispatched_at 有值;結果是新的(同一筆不重寫);結果版號=target_version |
心跳 observer(該 Agent 租戶) | :394-412、:183-200 |
paused → dispatched/pending/succeeded |
POST …/agents/resume |
暫停列中 Agent 仍 active:仍落後的重排「先 1 台再其餘」;已不落後的直接記 succeeded |
更新中心 | :555-580 |
failed(或其他非下發中)→ dispatched/pending |
POST …/agents/push |
同推送條件 | 更新中心 | :590-609 |
狀態改成 dispatched 時寫 dispatched_at、改成 succeeded/failed 時寫 finished_at(:339-348)。交易提交後主系統背景呼叫 Agent POST /update/notify(5 秒逾時)叫它立刻心跳,連不到只記 warning(:350-367;infra/remote_agent/agent_update_notify_client.py:23)。
回報更新站:succeeded/failed 且 reported_at 為空、finished_at 在一天內的列,每分鐘排程向更新站 POST /v1/report(target=agent)各報一次,送達才寫 reported_at(:415-445;infra/product_update/repository/agent_update_rollout_repo_impl.py:44-47)。
陷阱:主系統退回後,還沒升的 Agent 不會被推
心跳下發前要過 host_allows:這一列跟的主系統版不得比主系統現行版新(agent_rollout_service.py:98-101、:369-379)。Agent 與主系統是兩條版號線,「Agent 不比主系統新」只能這樣守。
陷阱:目標版未知時「是否落後」回 None,不是 False
升級目錄沒有這版 Agent 包時比不出來;當 False 會讓落後三版的台顯示「已是最新」(agent_rollout_service.py:85-95)。
Agent 收到 update 後下載包、寫 agent-order/agent-upgrade-order.json,主機 path 單元跑升級。Agent 每次心跳前讀結果(evidence-agent guidant_agent/sync/app/update_sync.py:109-136):
| Agent 讀到 | 判成 | 帶回主系統 |
|---|---|---|
進度檔 job_id=單的 job_id、status=succeeded |
有結果 | upgrade_result={version, status:"succeeded", at} |
同上、status=failed/timeout |
有結果 | status:"failed"、error=進度檔 error |
同上、status=skipped |
這次不升 | 不帶;下次心跳重來 |
| 同上、其他 | 還在跑 | 不帶 |
主機寫的 agent-upgrade-host.json job_id 相同、status=failed |
有結果 | status:"failed" |
同上、status=skipped |
這次不升 | 不帶 |
| 進度檔被別次升級蓋掉、但 Agent 現行版=目標版 | 有結果 | status:"succeeded" |
| 單寫好 7200 秒都沒人接手 | 有結果 | status:"failed"「主機的升級單元可能沒裝或沒啟用」 |
同一包失敗後 1800 秒內不重下(update_sync.py:50)。
標籤取自 product-update.json 的 agent_status_*。Agent 區每台另有「是否落後」與「必須更新」(現行版低於新版的 min_agent_version)。
| 跟進列狀態 | Agent 區標籤 | 一次推送的總結(歷史列) | 可按的鈕 |
|---|---|---|---|
| 沒有列、落後 | 落後 | — | 推選定(立即或排程)、稍後提醒 7 天 |
| 沒有列、不落後 | 已是最新 | — | — |
pending |
等待中 | running |
暫停 |
dispatched |
更新中(主系統另向 Agent 拉 GET /update/progress 顯示下載與步驟進度) |
running |
暫停 |
succeeded |
已完成 | 全部成功=succeeded |
— |
failed |
失敗(顯示 error) |
有失敗=failed |
推選定 |
paused |
已暫停(顯示 error) |
有暫停=paused |
繼續 |
總結規則:還有 pending/dispatched→running;有 paused→paused;有 failed→failed;否則 succeeded(agent_rollout_service.py:274-281)。
欄位全表見 upgrade-runner-spec.md §8。這裡只講狀態。主系統與 Agent 的進度檔是同一份契約(evidence-agent scripts/install/lib/progress.sh:5-7)。
定義在 scripts/installer/lib/progress.sh:7-10。
| 欄位 | 值 | 意思 |
|---|---|---|
status |
running |
升級進行中,或退回進行中 |
succeeded |
換接完成 | |
failed |
失敗 | |
timeout |
某一步逾時 | |
skipped |
這次沒有升級 | |
preflight |
只讀預檢(包就緒後跑一次第 1 步),不是升級 | |
result |
null |
還沒結束 |
upgraded |
系統已是新版 | |
unchanged |
系統完全沒動 | |
rolled_back |
已退回舊版 | |
rollback_failed |
退回也失敗,要人處理 | |
passed/failed |
只出現在 status=preflight |
%%{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'}}}%%
stateDiagram-v2
state "running / null" as R
state "failed或timeout / null(退回中)" as RB
state "succeeded / upgraded" as OK
state "skipped / unchanged" as SK
state "failed / unchanged" as FU
state "failed或timeout / rolled_back" as BACK
state "failed或timeout / rollback_failed" as BF
state "preflight / null" as P
state "preflight / passed或failed" as PD
[*] --> R: progress_init
[*] --> SK: 執行前驗包不過
R --> OK: 第 6 步寫完
R --> SK: 第 0~2 步失敗/時段到/取消
R --> RB: 第 3~6 步失敗或逾時
R --> FU: 開機檢查或 recover,斷在第 3 步之前
R --> RB: 開機檢查或 recover,斷在第 3 步之後
RB --> BACK: 第 8 步成功
RB --> BF: 第 8 步失敗
BF --> RB: sudo guidantai recover
[*] --> P: preflight 單
P --> PD: 第 1 步結束
status / result |
怎麼來 | 位置 | 維護頁 | 更新中心 |
|---|---|---|---|---|
running / null |
progress_init 開新一次 |
progress.sh:147-167 |
步驟列與進度,3 秒輪詢,不放人回去 | 「更新中」+步驟列 |
failed或timeout / null |
runner 第 3~6 步失敗,先改 status 再開始退回;或開機、recover 決定退回 |
upgrade_runner.sh:124;upgrade_rollback.sh:313-314 |
顯示退回中;後端起來就放人 | 依紀錄(同步前仍是 running) |
succeeded / upgraded |
第 6 步在新版 api 健康後寫;runner 結尾再寫一次 | upgrade_switch.sh:145;upgrade_runner.sh:170 |
「已完成」,後端通了帶 ?upgraded=1 回去 |
紀錄 succeeded;Agent 跟進開始建列 |
skipped / unchanged |
第 0~2 步失敗;停接新工作時段到;管理員在第 0~2 步取消;更新單執行前驗包不過 | upgrade_runner.sh:112-117、:162-166;upgrade_order.sh:122-131、:214-222 |
一般維護頁(後端通了就放人) | 紀錄 skipped:「這次沒有更新到 {新},系統沒有任何變動」+重試 |
failed / unchanged |
開機檢查或 recover 發現上次斷在第 3 步之前 | upgrade_rollback.sh:306-311 |
同上 | 紀錄 skipped:「這次沒有更新到 {新},系統沒有任何變動」+重試(見 §2.6 陷阱) |
failed或timeout / rolled_back |
第 8 步成功 | upgrade_runner.sh:126-130;upgrade_rollback.sh:318-322 |
「已退回」,後端通了就放人 | 紀錄 rolled_back:「已自動退回 {舊},服務正常」+重試 |
failed或timeout / rollback_failed |
第 8 步失敗 | upgrade_runner.sh:132-134;upgrade_rollback.sh:324-326 |
紅色、不放人,就算後端起來也不放 | 主狀態 rollback_failed、停輪詢:「自動退回也沒有完成」+下載診斷包 |
preflight / null→passed或failed |
包就緒後的只讀預檢單 | upgrade_order.sh:240-263 |
不影響(status 不是 running) |
就緒卡片顯示預檢結果(只看第 1 步) |
Agent 進度檔多兩種來源(evidence-agent scripts/install/lib/):
status / result |
怎麼來 | 位置 |
|---|---|---|
skipped / unchanged |
互動升級時使用者選取消 | upgrade_runner.sh:487-490 |
failed或skipped / unchanged |
升級單在 runner 開跑前就停下(解包、驗章失敗) | upgrade_order.sh:209-213 |
步驟 status |
意思 | 更新中心步驟列 | 維護頁 |
|---|---|---|---|
running |
進行中 | 進行中(now) | now |
ok |
完成 | 完成(done) | done |
warning |
完成但有警告(只有第 7 步清理) | 完成(done) | — |
failed |
失敗 | 失敗(fail) | fail |
timeout |
逾時 | 失敗(fail),顯示「上限 {秒}」 | fail |
skipped |
略過(第 0 步維運指令已是目標版;第 2 步這次不更新) | 略過(skip) | done |
| (沒有紀錄) | 還沒走到 | 等待(wait);失敗之後的步改列「略過」 | pending |
第 8 步 rollback 成功時在步驟列顯示為「已退回」(back),插在失敗那一步後面(productUpdateState.js:166-200)。維護頁對照在 public/maintenance/maintenance.js:165-170。
| 文件 | 內容 |
|---|---|
| design.md | 設計定案 D1~D31 |
| upgrade-runner-spec.md | 主機升級程式每一步、進度檔完整欄位、目錄佈局 |
| sequence-diagrams-spec.md | 四張循序圖:檢查到就緒、按更新到回報、Agent 跟進、發版 |
handoff/fr167-STATE.md §3 |
已落地的契約(與 design 不同處) |