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

更新中心 API

主系統「系統設定 → 更新中心」的後端端點、status 回傳形狀、權限、排程、下載與更新單、錯誤碼與事件碼、容器掛載,以及前端畫面怎麼讀它們

這份寫給要改更新中心前後端的工程師。每個欄位與數字都對回程式檔與行號(BE api/product_update/、app/product_update/service/、core/scheduler_update.py;FE src/views/product-update/)。Agent 跟進的細節另見 agent-rollout-spec.md,主機上的升級指令另見 upgrade-runner-spec.md。

1. 範圍與分工

更新中心是主系統的一頁設定頁,做五件事:問原廠更新站有沒有新版、下載並驗證更新包、寫「更新單」交給主機、顯示主機升級進度、管 Agent 跟進。

🔴 主系統不碰 docker、不跑升級。主系統容器只做兩件事:把更新包放進 incoming/、把更新單寫進 orders/。真正換版的是主機上的 systemd 單元與 guidantai upgrade;主機把進度寫成檔案,主系統每分鐘讀一次回寫紀錄。主機不打主系統 API,主系統也不呼叫主機(單向:主系統寫單、主機寫檔)。

%%{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'}}}%%
flowchart LR
    FE["FE 更新中心頁<br/>UpdateCenter.vue"] -->|"REST /system/product-update/*"| API["guidant-api"]
    API -->|"入列 product_update_download"| WK["guidant-worker"]
    SITE["原廠更新站<br/>/v1/check · /v1/download-url · /v1/report"]
    API -->|"立即檢查"| SITE
    WK -->|"預簽網址下載"| SITE
    WK -->|"驗章+雜湊後改名"| INC[("upgrade/incoming/<br/>容器 rw")]
    API -->|"寫 &lt;job_id&gt;.json"| ORD[("upgrade/orders/<br/>容器 rw、730")]
    ORD -->|"PathChanged"| UNIT["guidant-upgrade-order.path<br/>→ guidantai upgrade --from-order"]
    UNIT -->|"讀包"| INC
    UNIT -->|"寫"| PROG[("upgrade/upgrade-progress.json<br/>draining · last-switch.json · diag/")]
    PROG -->|"唯讀掛載,每分鐘同步"| API
    PROG -->|"維護頁直讀"| NGX["guidant-fe nginx"]
圖 1 — 更新中心的資料通道:主系統只寫 incoming/ 與 orders/,主機只寫進度檔
層 檔案 職責
Route api/product_update/__init__.py:28-47、api/product_update/routes/product_update_route.py 18 個 Resource、能力點守門、解 request、序列化
Schema api/product_update/serializers/product_update.py request/response 欄位(下文各表照此)
App service app/product_update/service/product_update_app_service.py 守門(SaaS 要平台管理員)、組 status、合併紀錄
發現與下載 app/product_update/service/product_update_flow.py 檢查、通知、背景下載、取消、離線上傳
更新單 app/product_update/service/upgrade_order.py 立即/排程/取消/重試/預檢/診斷包、進度檔回寫、回報更新站、補寫 system_logs
驗章與下載 package_verifier.py、package_downloader.py、offline_bundle.py、update_site_client.py 發布清單驗章、續傳、離線檔拆解、更新站呼叫
排程 core/scheduler_update.py 每 6 小時檢查+啟動一次;每分鐘同步進度檔
Domain/Infra domain/product_update/、infra/product_update/ product_update_jobs、agent_update_rollouts 兩表
DI di_containers/product_update/product_update_containers.py ProductUpdateAppService 只注入 job domain service 與 identity;Agent 跟進的 service 由 core/plugins/_agent_update.py:19 就地組

2. 權限

能力點 誰預設有 能做什麼
system.product-update.read 所有 is_admin=1 的非系統、非範本角色(migration 2026-10-29-fr167-product-update-capabilities.sql) 進頁、GET status/history/agents
system.product-update.update 同上 其餘所有寫入端點與 GET diag/<id>
(只驗登入) 任何登入者 GET notice(全站排程提醒,只回時間與版號)

守門分兩層:

  1. Route:@jwt_required → @require_capability(...)(product_update_route.py:31-32)。不掛 require_license——product-update 列在 common/authz/license.py:127 的基礎設施模組,授權過期仍能升級。
  2. App service:所有寫入先 _assert_may_write()(product_update_app_service.py:102-105)——非落地版(SaaS)一律要平台管理員,因為換版是整台機器的事,任何租戶都不准換掉全機共用的版本。

status.can_update(product_update_app_service.py:107-116)=持 update 能力點 且 通過上面第 2 層。前端用它藏寫入鈕;按鈕本身另掛 v-can="CAP.SYSTEM_PRODUCT_UPDATE_UPDATE"(src/config/capabilities.js:212-213)。

選單:ui_routes.name='product-update'、/system/product-update、掛在 group-system-config 下、sort 43;route_capabilities 綁 read(ALL)與 update(ANY)。

3. 端點總覽

前綴一律 /api/1.0。信封照全站:成功 {"status": true, "data": …};失敗 HTTP 4xx/5xx、{"error_code", "msg"}。

# 方法 路徑 能力點 FE 常數(src/config/api/api.js:550-566)
1 GET /system/product-update/status read PRODUCT_UPDATE_STATUS
2 GET /system/product-update/history read PRODUCT_UPDATE_HISTORY
3 POST /system/product-update/check update PRODUCT_UPDATE_CHECK
4 PUT /system/product-update/auto-check update (FE 目前沒有呼叫)
5 POST /system/product-update/download update PRODUCT_UPDATE_DOWNLOAD
6 DELETE /system/product-update/download update 同上
7 POST /system/product-update/uploads update PRODUCT_UPDATE_UPLOADS
8 PUT /system/product-update/uploads/<upload_id>/chunks/<index> update PRODUCT_UPDATE_UPLOAD_CHUNK
9 POST /system/product-update/uploads/<upload_id>/complete update PRODUCT_UPDATE_UPLOAD_COMPLETE
10 POST /system/product-update/apply update PRODUCT_UPDATE_APPLY
11 DELETE /system/product-update/apply update 同上
12 POST /system/product-update/retry update PRODUCT_UPDATE_RETRY
13 POST /system/product-update/diag update PRODUCT_UPDATE_DIAG
14 GET /system/product-update/diag/<diag_id> update PRODUCT_UPDATE_DIAG_FILE
15 GET /system/product-update/notice 只驗登入 PRODUCT_UPDATE_NOTICE
16~20 GET agents、POST agents/pause、agents/resume、agents/push、agents/remind-later read/update PRODUCT_UPDATE_AGENTS*

16~20 是 Agent 跟進,契約寫在 agent-rollout-spec.md §5。

4. GET status

ProductUpdateStatusRoute(product_update_route.py:37)→ ProductUpdateAppService.status()(product_update_app_service.py:131-151)。這支有副作用:先呼叫 order.after_switch(progress, recheck=False)——主機剛換接完成時,同步紀錄、清掉舊版本那次檢查,不等每分鐘排程(不重問更新站,免得卡住請求)。

4.1 頂層欄位

欄位 型別 含義 來源
current_version string|null 主系統現行版號 get_version_info()
last_check object|null 最近一次檢查紀錄(§4.2),已拿掉簽好的清單原文 ROOT 租戶 system_configs PRODUCT_UPDATE/last_check,product_update_flow.py:272-278
available object|null 以目前版本問到的新版;沒有新版、或那次檢查是升級前做的回 null product_update_flow.py:281-289
download object|null 下載中才有 {version, bytes_done, bytes_total};bytes_total 不知道為 null product_update_flow.py:299-306
auto_check_enabled bool 自動檢查開關,預設 true;false 時畫面要一直提醒 product_update_flow.py:93-98
latest_job object|null product_update_jobs 最新一列(§4.3),status 已換成畫面狀態 upgrade_order.display_status
progress object|null 主機進度檔 upgrade-progress.json 原樣;沒有、超過 256KB、不是 JSON 物件都回 null product_update_app_service.py:44-64
draining bool 主機放了停接新工作旗標 upgrade/draining;worker 不撿新單、Agent 不派新任務 core/maintenance_gate.py:23
in_flight object|null 只在 draining=true 時有:{background_jobs, agent_tasks},取自進度檔第 2 步的 running_jobs/running_agent_tasks(主機以超管身分數,不在這裡重查 DB 免得被 RLS 少算) product_update_app_service.py:74-82
can_update bool 見 §2
scheduled_at string|null 最新一列是 scheduled 時的 not_before(ISO 帶時區) product_update_app_service.py:67-71
last_upgraded_at datetime|null 最近一筆 succeeded 的 ended_at product_update_app_service.py:153-156
agent_rollout object {host_version, counts{pending,dispatched,succeeded,failed,paused}, total, paused},只算現行版那一組 agent_rollout_service.py:694-697

4.2 last_check 與 available

last_check(product_update_flow.py:241-269):

欄位 含義
at、by 檢查時間與觸發者(排程為 system)
current_version 檢查當下的現行版;與 current_version 不同時 available 一律回 null
ok 這次有沒有成功問到
status none(沒有新版)/available/error/cleared(換版後被清掉)
error、error_code 失敗原因白話與錯誤碼(PRODUCT_UPDATE_412001、412003、503001、400001),只寫在這裡、不當 HTTP 錯誤拋
latest、path[] 最新版與必經版本路徑
releases[] 每段 {version, size, has_security_fix, min_base_version, issued_at, notes[], security_impact, security_action}
last_success_at 最近一次成功檢查時間(失敗時沿用前一次)
notified_version 已寄過新版通知的版號;同一版不重寄

available 形狀:{latest, path[], releases[], found_at},releases 同上。

更新站回的每一段清單都要用本機印樣(/etc/guidant/release-keys)驗過才採信,並檢查每段都能從前一段直升(min_base_version)。任一段驗不過整個不認,記 error_code=PRODUCT_UPDATE_400001(product_update_flow.py:217-231)。

4.3 latest_job(紀錄列)

欄位照 ProductUpdateJobSchema(serializers/product_update.py:8-42):uid、job_id、from_version、to_version、path[]、status、source(online/offline)、downloaded_by/applied_by/cancelled_by(login)與對應 *_name(暱稱)、*_at、not_before、started_at、ended_at、error、with_agents、agent_uids[]、審計四欄與 *_name。

status 的畫面換算(upgrade_order.py:192-204):DB 是 scheduled、時間已到、主機還沒為這個 job_id 寫進度檔時,回應換成 submitted。只換回應不寫 DB,取消與重複按的擋法照舊認 scheduled。

%%{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: POST download/apply(還沒下載)
    [*] --> ready: 離線上傳 complete
    downloading --> ready: worker 驗章+雜湊通過
    downloading --> failed: 下載失敗/驗不過(包已刪)
    downloading --> cancelled: DELETE download
    ready --> cancelled: DELETE download
    ready --> scheduled: apply/下載完自動寫單
    scheduled --> ready: DELETE apply(第 0~2 步前)
    scheduled --> running: 進度檔 status=running
    running --> succeeded: 最後一段 succeeded
    running --> rolled_back: failed+result=rolled_back
    running --> failed: failed/timeout
    running --> skipped: 主機這次不更新(包還在)
    rolled_back --> scheduled: POST retry
    skipped --> scheduled: POST retry
圖 2 — product_update_jobs.status 的狀態流轉(submitted 只出現在回應)
  • checking 在 DB CHECK 內但程式不寫入。
  • failed 與 skipped 的分界:failed=包已刪、下一步是重新下載;skipped=這次沒動、包還在、可重試(product_update_flow.py:49-51)。
  • 跨版時每段各寫一份進度檔、job_id 相同;中間段的 succeeded 仍當 running,直到 to 等於最後一段(upgrade_order.py:432-447)。
  • 已回 ready 的列(管理員取消)不會被主機事後寫的 skipped 蓋掉(upgrade_order.py:450-472)。

4.4 progress(主機進度檔)

原樣轉交,欄位由主機 scripts/installer/lib/progress.sh:116 決定:job_id、from、to、path、segment{n,of}(跨版第幾段,非跨版為 null)、triggered_by{kind,user,mode,at}、started_at、status(running/succeeded/failed/timeout/skipped/preflight)、db、prev_images、result(upgraded/rolled_back/rollback_failed/unchanged/passed/failed)、error、rolled_back_at、updated_at、steps[]。步驟編號與名稱見 upgrade-runner-spec.md。

5. 各端點契約

5.1 GET history

項目 內容
Query page(預設 1,≥1)、page_size(預設 20,1~100,超過回 400,不靜默夾小)
Response data[]=紀錄列(§4.3 欄位)+target(host/agent);target=agent 多 agent_total/agent_succeeded/agent_failed、host_version。meta 為標準分頁
合併規則 主系統列在 DB 分頁,取前 page×page_size 筆;Agent 列(同 host_version、同 created_at 的一次推送彙整成一列,agent_rollout_service.py:284-313)全取;兩邊依 created_at 新到舊合併(沒時區當 UTC)後切這一頁。total=主系統總數+Agent 彙整列數(product_update_app_service.py:158-165)
Agent 列狀態 還有 pending/dispatched → running;有 paused → paused;有 failed → failed;全成功 → succeeded(agent_rollout_service.py:274-281)

5.2 POST check

項目 內容
Request 無 body
行為 同步打更新站 /v1/check(連線 10 秒、讀取 30 秒,update_site_client.py:20-21),驗清單後寫 last_check;有新版且 latest ≠ notified_version 就寄信給 ROOT 租戶管理員(收件人沿用授權到期通知的 TenantAdminDirectory),至少寄出一封才記 notified_version
Response last_check(§4.2)
錯誤 連不到站、沒設站、沒授權、清單驗不過都不拋,寫進 last_check.error_code

送出的身分:有更新站身分票只送 {ticket, fingerprint, current_version};沒有才送授權檔原文(update_site_client.py:99-104)。

5.3 PUT auto-check

項目 內容
Request {"enabled": bool}(必填)
Response {"enabled": bool}
錯誤 寫設定失敗 → 500 PRODUCT_UPDATE_500001

5.4 POST/DELETE download

POST:只下最近一次檢查驗過、且是以目前版本問到的那條路徑。建一列 downloading、入列 product_update_download 背景工作(max_attempts=1,斷線重試在 handler 內做)。回 {uid, version, path[], status}(product_update_flow.py:406-418)。

前置 錯誤
last_check.status=available 且 current_version 相符 412 PRODUCT_UPDATE_412002
沒有 downloading/ready 的列 409 PRODUCT_UPDATE_409001

DELETE:downloading 或 ready(還沒寫單)的列改 cancelled、刪 incoming/ 裡這條路徑的所有檔。worker 每收 64MB 看一次狀態,看到 cancelled 自己停手刪檔。回 {uid, status:"cancelled"}。沒有可取消的 → 404 PRODUCT_UPDATE_404001。

下載流程(worker,product_update_flow.py:474-513):

  1. 以 last_check 存的簽好清單逐段重驗:每段比前一段新、前一段夠格直升它(不信更新站排的順序)。
  2. 空間檢查:剩餘總大小+2GB(package_downloader.py:41、95-100)。
  3. 每段向 /v1/download-url 拿短效預簽網址,只接受 DOWNLOAD_HOSTS 白名單主機;下到 incoming/guidant-ai-<版>.tar.gz.part,Range 續傳,斷線最多重試 5 次、每次重拿網址(package_downloader.py:39、190-227)。
  4. 下完驗 sha256 與大小;先寫 .release.json(簽好的信封原文)與 .tar.gz.sha256,最後才把 .part 改名成 .tar.gz——主機看到 .tar.gz 時旁邊兩檔一定在。
  5. 清單有 ops_sha256 時另下維運指令小檔 guidant-ai-<版>.ops.tar.gz;拿不到不算失敗。
  6. 驗不過、連線用盡、授權缺 → 列改 failed、包已刪(驗不過)或 .part 保留(斷線)。
  7. 成功 → 列改 ready、記實測下載速率(PRODUCT_UPDATE/download_rate),接著:這列是「更新到 X」建的就自動寫更新單(§5.6);否則寫一張 type=preflight 預檢單,就緒卡片就有預檢結果。

5.5 離線上傳(uploads 三端點)

主機連不到外網時,客戶向原廠取得單一離線檔 guidant-ai-<版>-offline.tar(檔內恰好是同版號的更新包與發布清單兩個檔)上傳。

端點 Request Response 錯誤
POST uploads {total_size ≥1, total_chunks ≥1, manifest?};manifest 不送=單一離線檔;送 release.json 原文(≤64KB)=兩檔送法,先驗清單才收 {upload_id, version, chunk_max_bytes} 400001/400003/400004(清單驗不過)、400006(total_size 不等於清單記的大小)、412 412004(空間不足)、409 409001(已有下載中或就緒)
PUT uploads/<id>/chunks/<n> body=原始位元組,n 從 0 起;同一塊可重送覆蓋 {index, size} 400 400007(編號或大小不對)、404 404002(上傳不存在)
POST uploads/<id>/complete 無 {uid, version, status:"ready"} 404002、400 400008(還有塊沒傳)、400005(雜湊不符)、400 400012(不是原廠離線檔)、驗章類 400001/3/4、409001

限制(product_update_flow.py:569-573):單塊 ≤50MB、總量 ≤8GB、塊數上限 ceil(8GB/50MB)。暫存在 incoming/.uploads/pkg-*。空間:兩檔送法要 1 倍包大小,單一離線檔要 2 倍(合併檔與拆出的包同時在磁碟上),再加 2GB 餘裕。

單一離線檔(product_update_flow.py:660-680):先只讀檔內清單驗章,過了才把包抽到系統自產的檔名,再走跟線上下載同一段核雜湊。檔內附的維運指令小檔不符只丟掉它、不拒整份。上傳完但驗不過會留一列 failed(紀錄頁看得到、畫面給「重新上傳」而不是重試)。成功後同樣寫一張預檢單。

5.6 POST apply(寫更新單)

欄位 型別 說明
mode now|scheduled 必填
at AwareDateTime scheduled 必帶,要有時區,1 分鐘後到 60 天內(upgrade_order.py:53-55、134-143)
with_agents bool 預設 true:主系統升完接著更新 Agent
agent_uids string[](每個 1~50 字、最多 200 個) with_agents=true 時只跟這幾台;空=全部落後的

Response(ProductUpdateOrderSchema):{uid, job_id, version, path[], status, mode, not_before, with_agents, agent_uids}。

判斷順序(upgrade_order.py:279-307):

最新一列 行為 錯誤
ready、由「更新到 X」建、還沒寫單、沒取消沒錯誤 擋:worker 正要自動寫單 412 412013
ready 驗包 → 改 scheduled 記 job_id/applied_by → 寫單;寫單失敗改回 ready 412006、400001/3/4/5、500 500002
downloading/scheduled/running 擋 412 412005
其他(沒下載過) 有可用新版就先建下載單並記下誰按的、排程時間、Agent 勾選,就緒後 worker 接著寫單;排程時間要晚於「現在+剩餘大小÷最近一次下載速率」(預設 1MB/s) 412005(沒新版)、412 412011(排太近)、409001
— at 不合法 400 400009

下載途中排定時間已過,不改成立即:包留在就緒、error 記原因讓管理員重選(upgrade_order.py:342-365)。

更新單(upgrade_order.py:83-95,主機端 scripts/installer/lib/upgrade_order.sh:13-14 同欄位):

{"job_id": "20261101-1430-a1b2", "type": "upgrade", "mode": "now",
 "version": "1.27.11", "path": ["1.27.11"], "not_before": null,
 "requested_by": "admin", "requested_at": "2026-11-01T06:30:00+00:00"}
  • 寫到 upgrade/orders/<job_id>.json,先寫 .<名>.tmp 再 os.replace——path 單元一看到檔就觸發,不能讓主機讀到半份。
  • job_id 形狀 YYYYmmdd-HHMM-<4~8 位 hex>,主機 ord_valid_job_id 認同一個樣式。
  • type:upgrade/preflight/diag。
  • 主機只信三件事:job_id 與版號的格式、包在本機 incoming/ 且用本機印樣驗得過、版號真的比現行新。單上不接受任何路徑。
  • 主機端單檔上限 8192 bytes(ORD_MAX_BYTES),超過不讀。
  • orders/ 對容器是 730(root:1000):能建檔、能刪知道名字的檔,但列不出目錄——取消只能靠紀錄上的 job_id 拼檔名。
  • 主機 guidant-upgrade-order.path(PathChanged=<資料目錄>/upgrade/orders)觸發 guidant-upgrade-order.service → guidantai upgrade --from-order。立即單撿到就刪單、開跑;排程單用 systemd-run --on-calendar 排一次性 timer,到點以 --run 回來(單留在 orders/ 等)。

5.7 DELETE apply(取消)

狀況 行為
單還在 orders/(主機還沒撿) 刪單
單不在、主機進度檔這個 job_id 的最大步驟 < 3 寫取消旗標 orders/<job_id>.cancel;主機每秒看一次,看到就 TERM runner,進度檔寫 skipped/unchanged
已到第 3 步(停服務、複製資料庫)以後 409 PRODUCT_UPDATE_409002
最新一列不是 scheduled/running 404 404001

成功後列改 ready、記 cancelled_by,包保留可重排。回 {uid, status:"ready"}。

5.8 POST retry

只對 rolled_back、skipped 有效(否則 412 412007)。用 incoming/ 裡同一個包從預檢重跑,不重下載;只跑比現行版新的那幾段(跨版第二段失敗停在中間版,重試從中間版接著跑)。沒有剩下的段 → 412005。mode 固定 now,Agent 跟進的勾選沿用上次。

5.9 診斷包

端點 行為 錯誤
POST diag 寫 type=diag 更新單,回 {diag_id}(=job_id) 500002
GET diag/<diag_id> 主機把 guidantai diag 產到 upgrade/diag/<job_id>/guidant-diag-*.tar.gz 並 chmod 644;有檔回 application/gzip 附件,還沒好回 HTTP 202 {"ready": false} diag_id 不是 job_id 形狀 → 404 404003;檔是 root 600 讀不到 → 412 412012

FE 每 3 秒問一次、最多 100 次(src/composables/useProductUpdate.js:14-15)。

5.10 GET notice(全站提醒)

項目 內容
守門 只 @jwt_required,不掛能力點——同 /version 是全站公開的系統狀態
Response {scheduled_at, version, remind_minutes:[30,5]};沒排程時前兩欄 null
陷阱 在 system_context 內讀最新一列,不可包 @transaction:session 開在外面就沿用登入者的 RLS,子租戶的人看不到排在根租戶的那一列(product_update_app_service.py:228-239)

FE UpdateScheduleReminder.vue 每 60 秒問一次,開始前 30 分與 5 分各提醒一次(src/views/product-update/updateNoticeState.js:34-48)。

6. 排程

core/scheduler_update.py。gunicorn preload_app=True,排程在 master fork 前建立,只跑一份。

job id 觸發 做什麼
product_update_check 每 6 小時(CHECK_INTERVAL_HOURS=6,:10) run_scheduled_check():開關關了或沒設 UPDATE_SITE_URL 就不問、不寫 last_check;其餘同立即檢查(check_now("system"))。只問有沒有新版、有就通知,不下載
product_update_check_startup 啟動時跑一次(date trigger 不給 run_date) 同上;新裝或改了更新站網址重啟後當下就有結果
product_update_order_sync 每分鐘 依序:sync_from_progress(進度檔結果寫回紀錄)→ after_switch(recheck=True)(換接後清舊檢查並以新版重查)→ report_final_once(終態回報更新站一次)→ log_steps_once(每步補寫 system_logs)→ Agent 跟進 tick 與回報。各段各自 try,一段出錯不擋其他段(:39-108)

ROOT 租戶 system_configs 群組 PRODUCT_UPDATE 的鍵:

key 用途
auto_check_enabled 自動檢查開關
last_check 最近一次檢查(§4.2,含簽好的清單原文)
download_rate {bytes_per_sec, at},估排程夠不夠下完
last_reported_job 已回報更新站的 job_id;送不到每分鐘重試,結束超過 1 天放棄
upgrade_steps_logged_job 已補寫 system_logs 的 job_id
agent_remind Agent 稍後提醒 {version, until}

回報更新站(upgrade_order.py:486-512):POST /v1/report,body 為身分+{result, from_version, to_version},result 只收 succeeded/rolled_back/failed/skipped。不掛在「狀態有變」那一刻——畫面輪詢的 status 請求也會同步紀錄,它先收成終態,排程就看不到變化、永遠不報;所以改看「單是終態且沒報過」。

7. 前端畫面對應

7.1 deriveView 的畫面狀態

src/views/product-update/productUpdateState.js:73-91,由上往下第一個成立的勝出:

view 後端狀態組合 畫面
loading 還沒拿到 status 載入中
rollback_failed progress.result=rollback_failed 紅色失敗,顯示 sudo guidantai recover,停輪詢
running progress.status=running;或 latest_job.status=running 且進度檔沒收手(不是 failed/timeout/skipped) 「更新中:{from} → {to}」+步驟列,排空中顯示進行中件數
submitted latest_job.status=scheduled 且 not_before 已到(後端回 submitted 也落這裡);或 ready+applied_by 有值+job_id 空+沒 error+沒 cancelled_at(下載完 worker 正要寫單) 「正在準備更新到 {to}」五格:已送出 → 驗證更新包 → 已交給主機 → 主機驗包 → 開始升級
downloading/ready/scheduled/checking latest_job.status 原值 下載進度條(download)/就緒卡+預檢結果/排程時間
available available.latest 有值且不等於 current_version 版本說明、「更新到 X」「預先下載」
up_to_date 其餘 已是最新,顯示 last_upgraded_at

effectiveJob(:58-64):紀錄還寫 scheduled/submitted/running、但同 job_id 的進度檔已是 failed/timeout/skipped 的那一分鐘,以進度檔為準換成結果,畫面不會卡在轉圈。成功不在前端換(跨版中間段也寫 succeeded)。

畫面另有一層 screen(UpdateCenter.vue:55-58):按下更新後、後端回應前是 sending。

失敗另判、不佔主狀態:lastFailure 回最新一列是 rolled_back/skipped/failed 的那筆,頁首放失敗說明;canRetry 只對 rolled_back、skipped、或 failed 且進度檔 result=unchanged 給重試鈕(:104-117)。

7.2 輪詢

來源 間隔 規則
更新中心 status pollIntervalMs(productUpdateState.js:30-35) 沒有紀錄或紀錄已終態(succeeded/failed/rolled_back/cancelled/skipped)→ 0(不輪詢);scheduled 且 not_before 在未來 → 30 秒;其餘 3 秒。看「有沒有沒結束的單」不看畫面狀態——下載完那一刻後端先寫 ready、一秒後才自己寫單,看畫面會停在下載完那格
主機忙時連不到 — view 屬 scheduled/submitted/running 時請求靜默;拿到 502/503/504 或無回應就整頁重載,讓前門送維護頁(useProductUpdate.js:47-75)
Agent 區 3 秒 有任一台 pending/dispatched(agentRolloutState.js:20、useAgentRollout.js:9)
全站提醒 60 秒 notice 端點
診斷包 3 秒 × 100 次

7.3 步驟名

STEP_NAMES(productUpdateState.js:15)=進度檔步驟 0~7:self_replace、preflight、drain、db_copy、apply_on_copy、health、switch、cleanup;第 8 步 rollback 插在失敗那步之後。白話名走 i18n lang.product_update.step_<代號>,與後端 upgrade_order.STEP_EVENTS、主機 progress_step_label 是同一套,改要三邊一起改。可取消:view 為 scheduled/submitted,或 running 且目前步驟在 db_copy 之前(:212-216)。

7.4 i18n key 對照(src/config/locales/i18n/zh-tw/product-update.json)

畫面文字 key
頁首標籤:更新中/更新失敗/有新版本/已是最新 tag_updating/tag_failed/tag_has_update/tag_latest
連不到更新站的提示 check_failed
下載 {version} 中、{version} 已就緒、已排程在 {at} 更新 downloading_title、ready_title、scheduled_title
正在準備更新到 {to}、更新中:{from} → {to} submitted_title、running_title
更新到 {version}、預先下載 btn_update_to、btn_predownload
確認視窗「接著更新 Agent」 with_agents_label、with_agents_on、with_agents_off
必須更新 Agent 的頁首警示 required_banner

錯誤碼訊息在 src/config/locales/i18n/zh-tw/error-code.json(PRODUCT_UPDATE_* 共 34 條,與後端一一對應)。

8. 錯誤碼

common/code/product_update_error_code.py。HTTP 欄寫「—」的只出現在 last_check.error_code,不當 HTTP 錯誤回。

代碼 HTTP 常數 白話
PRODUCT_UPDATE_400001 400 BAD_SIGNATURE 發布清單無法用原廠印章驗過(壞檔或不是原廠的);也記在 last_check 表示更新站回的清單驗不過
PRODUCT_UPDATE_400002 — MANIFEST_EXPIRED 保留不拋(發布清單不設有效期),碼不可挪用
PRODUCT_UPDATE_400003 400 NOT_NEWER 版本沒比目前新,不可降版或重裝;或路徑缺了必經版本
PRODUCT_UPDATE_400004 400 MAJOR_MISMATCH 主版號不同,換主版號要重新授權
PRODUCT_UPDATE_400005 400 CHECKSUM_MISMATCH 包的內容與清單記的雜湊不符
PRODUCT_UPDATE_400006 400 UPLOAD_SIZE_MISMATCH 上傳宣告的大小與清單不符(選錯版的包)
PRODUCT_UPDATE_400007 400 UPLOAD_CHUNK_INVALID 分塊編號或大小不對
PRODUCT_UPDATE_400008 400 UPLOAD_INCOMPLETE 還有分塊沒傳完就按完成
PRODUCT_UPDATE_400009 400 SCHEDULE_INVALID 主系統排程時間沒帶時區,或不在 1 分鐘後~60 天內
PRODUCT_UPDATE_400010 400 AGENT_NONE_LAGGING 勾選的 Agent 都已最新或更新中,或一次超過 200 台
PRODUCT_UPDATE_400011 400 AGENT_SCHEDULE_INVALID Agent 排程沒帶時區,或不在 10 分鐘後~60 天內
PRODUCT_UPDATE_400012 400 OFFLINE_BUNDLE_INVALID 上傳的不是原廠離線檔(檔內要恰好是同版號的包與清單)
PRODUCT_UPDATE_404001 404 NOTHING_TO_CANCEL 沒有可取消的下載或更新
PRODUCT_UPDATE_404002 404 UPLOAD_NOT_FOUND 上傳不存在或已過期
PRODUCT_UPDATE_404003 404 DIAG_NOT_FOUND 診斷包編號不對
PRODUCT_UPDATE_404004 404 AGENT_BUNDLE_NOT_FOUND 這台 Agent 現在沒有要下載的包(Agent 下載端點,不區分「沒有」與「不准」)
PRODUCT_UPDATE_409001 409 BUSY 已有下載中或就緒的包,要先取消
PRODUCT_UPDATE_409002 409 TOO_LATE_TO_CANCEL 服務已停、正在升級,不能取消(失敗會自動退回)
PRODUCT_UPDATE_412001 — SITE_NOT_CONFIGURED 沒設原廠更新站,請改用離線更新
PRODUCT_UPDATE_412002 412 NO_AVAILABLE_RELEASE 沒有可下載的新版,請先檢查
PRODUCT_UPDATE_412003 — LICENSE_MISSING 這台主機還沒開通落地版授權,無法問更新站
PRODUCT_UPDATE_412004 412 NO_SPACE 更新包存放位置空間不足
PRODUCT_UPDATE_412005 412 NOT_READY 沒有已就緒的包;或正在下載/排程/升級中
PRODUCT_UPDATE_412006 412 PACKAGE_MISSING 就緒的包或清單在 incoming/ 找不到(被刪了)
PRODUCT_UPDATE_412007 412 NOTHING_TO_RETRY 只有退回或這次不更新的能重試
PRODUCT_UPDATE_412008 412 AGENT_BUNDLE_MISSING 現行版主系統包沒附 Agent 安裝包,無法推
PRODUCT_UPDATE_412009 412 AGENT_NOTHING_TO_PAUSE 沒有正在推送的 Agent
PRODUCT_UPDATE_412010 412 AGENT_NOTHING_TO_RESUME 沒有已暫停且仍落後的 Agent
PRODUCT_UPDATE_412011 412 SCHEDULE_TOO_SOON 排程太近,依最近下載速率估算來不及下載完
PRODUCT_UPDATE_412012 412 DIAG_UNREADABLE 診斷包產好了但檔案權限讀不到,請在主機 sudo guidantai diag
PRODUCT_UPDATE_412013 412 ALREADY_SUBMITTING 下載完正在自動寫單,請稍候
PRODUCT_UPDATE_500001 500 SETTING_SAVE_FAILED 更新中心設定(自動檢查、稍後提醒)存不進去
PRODUCT_UPDATE_500002 500 ORDER_WRITE_FAILED 更新單寫不進 orders/,請查主機目錄權限
PRODUCT_UPDATE_503001 — SITE_UNREACHABLE 連不到原廠更新站(封閉網路的常態)

9. system_logs 事件碼

common/enum/event_code.py:239-251。

碼 名稱 何時寫 寫在哪
6237 AGENT_UPDATE_BUNDLE_FETCH Agent 每次成功取用 Agent 包(agent_uid、version、size_bytes) core/plugins/_agent_update.py:101
6238 AGENT_UPDATE_ROLLOUT_CHANGED 管理員暫停/繼續/推選定/稍後提醒(action=pause/resume/push/remind_later、actor、結果) product_update_app_service.py:275-282
6239~6247 PRODUCT_UPDATE_STEP_SELF_REPLACE、_PREFLIGHT、_DRAIN、_DB_COPY、_APPLY_ON_COPY、_HEALTH、_SWITCH、_CLEANUP、_ROLLBACK 主系統升級每一步一筆(狀態、秒數、起訖、說明),順序=進度檔步驟 0~8 upgrade_order.py:518-559
6248 PRODUCT_UPDATE_RESULT 整次升級結果(job_id、from → to、結果、主機狀態、觸發者、錯誤) upgrade_order.py:580-583

6239~6248 不是即時寫:升級期間服務是停的,主機的 log 只在主機檔案裡。服務回來後由每分鐘排程在紀錄收成終態時補寫,每個 job_id 只寫一次(upgrade_steps_logged_job 記著)。跨版升級只看得到最後一段的步驟。

10. 容器掛載

docker/production/docker-compose.yml。api、socketio、worker 共用 x-guidant-common(:151),volumes 清單無法在單一 service 追加,所以三個都掛(socketio 掛了不用)。

主機路徑 容器路徑 api/socketio/worker fe 用途
${GUIDANT_DATA_DIR}/upgrade /guidant/upgrade ro(:252) ro(:525) 進度檔、draining 旗標、last-switch.json、bundles/、diag/;fe 的維護頁直讀 /upgrade-progress.json
${GUIDANT_DATA_DIR}/upgrade/orders /guidant/upgrade/orders rw(:253) — 更新單;主機目錄 730 root:1000
${GUIDANT_DATA_DIR}/upgrade/incoming /guidant/upgrade/incoming rw(:255) — 下載與上傳落點;主機目錄 750 1000:1000
/etc/guidant/release-keys 同路徑 ro(:257) — 發布印樣;主系統驗清單只信這裡

🔴 upgrade:ro 與 orders:rw 兩行都要在:只掛 orders/ 時 api 讀不到進度檔與旗標,且不報錯(狀態永遠 null、永遠不停接單)。目錄由主機 scripts/installer/lib/systemd_units.sh:20-29 建。

設定(config/config.py:204-209):GUIDANT_UPGRADE_DIR(預設 /guidant/upgrade)、GUIDANT_RELEASE_KEYS_DIR(預設 /etc/guidant/release-keys)、UPDATE_SITE_URL(預設空字串=沒設站,只能離線更新)。

11. 資料表 product_update_jobs

建表 scripts/sql/2026-10-29-fr167-product-update-jobs.sql,加 failed 狀態 2026-10-29-fr167-product-update-jobs-failed-status.sql,加 Agent 跟進欄 2026-10-30-fr167-agent-follow-optional.sql。ORM infra/product_update/model/product_update_job.py。

欄位 型別 說明
id/uid bigserial/varchar(36)
tenant_id/org_unit_id int RLS 前綴比對+超管放行,FORCE;紀錄列建在按下載那個人的租戶
job_id varchar(64) unique 寫更新單時才有;用來把進度檔結果對回這一列
from_version/to_version varchar(32)
path jsonb 必經版本路徑,最後一個=to_version。🔴 entity 預設 [],更新時一定要帶回原值,否則會被蓋成空清單(product_update_flow.py:426-428)
status varchar(16) CHECK:checking/downloading/ready/scheduled/running/succeeded/rolled_back/skipped/cancelled/failed
source varchar(16) online/offline
downloaded_by/at、applied_by/at、cancelled_by/at 各動作的 login 與時間
not_before、started_at、ended_at、error error 取自進度檔,不含密碼與金鑰
with_agents bool,預設 true
agent_uids jsonb,預設 []

cm_app 只有 SELECT/INSERT/UPDATE,沒有 DELETE。

相關文件