FR-167 · 技術規格 · 2026-11-01

狀態機

更新紀錄 product_update_jobs、Agent 跟進列 agent_update_rollouts、主機進度檔 upgrade-progress.json

線上更新有三份會變的狀態:主系統資料庫的更新紀錄(一列一次換版)、Agent 跟進列(一台 Agent 一列),以及主機上的進度檔(升級程式唯一的輸出)。主系統不能叫主機做事,只能寫更新單;主機也不能打主系統,只能寫進度檔。所以更新紀錄的後半段(排程之後)全靠主系統每分鐘讀進度檔同步。本頁每個狀態值與轉移條件都對到程式碼行號。

更新紀錄 10 個狀態值,實際會寫 9 個 Agent 跟進列 5 個狀態值 進度檔 status × result 七種組合 第 3 步前就斷(unchanged)一律記 skipped、可重試
§1

三份狀態怎麼連起來

狀態 存在哪 誰寫 誰讀 怎麼跟其他兩份對上
更新紀錄 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):換接剛完成時立刻同步紀錄,不等排程;不打更新站
§2

更新紀錄 product_update_jobs.status

2.1 狀態值

允許值見 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 不會再有新的進度檔,實際上也不會再變。

2.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'}}}%%
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 --> [*]
圖 1 — 更新紀錄的狀態轉移;實線是使用者或主系統動作,虛線是每分鐘同步主機進度檔

2.3 每條轉移的條件與寫入者

從 → 到 觸發 條件 寫入者 位置
∅ → 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)。

2.4 進度檔 → 紀錄的對照

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)。

2.5 畫面才有的狀態 submitted

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」建的下載單剛就緒、後端還沒寫單的空窗)。

2.6 每個狀態在畫面上顯示什麼

標籤取自 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。

§3

Agent 跟進列 agent_update_rollouts.status

3.1 狀態值

允許值見 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(預留,目前沒有程式寫它)。

3.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'}}}%%
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 --> [*]
圖 2 — Agent 跟進列的狀態轉移:先 1 台、成功再放其餘、任一台失敗其餘暫停

3.3 每條轉移的條件與寫入者

從 → 到 觸發 條件 寫入者 位置(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)。

3.4 Agent 那一側:升級單與進度檔怎麼變成結果

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)。

3.5 每個狀態在畫面上顯示什麼

標籤取自 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)。

§4

主機進度檔 upgrade-progress.json

欄位全表見 upgrade-runner-spec.md §8。這裡只講狀態。主系統與 Agent 的進度檔是同一份契約(evidence-agent scripts/install/lib/progress.sh:5-7)。

4.1 頂層 status 與 result

定義在 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

4.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'}}}%%
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 步結束
圖 3 — 進度檔頂層狀態(status/result);只讀預檢是另一條獨立的線

4.3 每個組合怎麼來、畫面顯示什麼

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

4.4 步驟 status

步驟 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。

§5

相關文件

文件 內容
design.md 設計定案 D1~D31
upgrade-runner-spec.md 主機升級程式每一步、進度檔完整欄位、目錄佈局
sequence-diagrams-spec.md 四張循序圖:檢查到就緒、按更新到回報、Agent 跟進、發版
handoff/fr167-STATE.md §3 已落地的契約(與 design 不同處)