客戶還沒裝 Agent 時,要有地方拿到安裝包。管理員在主系統點選「取得安裝包」,主系統向原廠更新站換兩條 Cloudflare R2 預簽下載網址(安裝包、.sha256),直接下載或複製給裝機的人。時限由原廠在更新站後台設定,客戶端不能改。
整案一句話:管理員在主系統更新中心 Agent 區、或建立註冊 token 的對話框點選一次,就拿到 Agent 最新版安裝包與 .sha256 兩條 Cloudflare R2 預簽下載網址;網址由原廠更新站簽發,有效時間由原廠在更新站後台設定,主系統只轉交、不存包、不供檔。
| 批 | 卡 | Repo | 做什麼 | 完成判定 |
|---|---|---|---|---|
| 1 | U | update-center | /v1/download-url 加 artifact=sha256、預簽網址帶 response-content-disposition、有效時間改讀 D1 設定、後台加「下載網址有效時間」欄位、/v1/check 讓 Agent 線可不帶 Agent 版號查最新 |
vitest 綠;wrangler dev 打兩種 artifact 拿到兩條可下載網址,時限隨後台設定變 |
| 2 | B | main | 主系統換網址端點改成「向更新站換 R2 網址」;拿掉本機供檔端點、時限設定端點、併發計數;稽核保留 | 接 DEV 更新站(wrangler dev --env dev)打一次拿到兩條 R2 網址、sha256sum -c 通過 |
| 3 | F | fe | 共用元件改顯示 R2 網址與到期時間、拿掉時限欄位與探測/429 處理、拿掉「沒包先預先下載」改成「更新站連不上」提示 | 兩個入口畫面截圖;複製出的網址在另一台機器 curl 下得到 |
| 4 | D | main | FINAL-SPEC、手冊取包句、README/STATE/LOG 收口 | 手冊 grep 不到「本機」「主系統下載」說法 |
依賴:U → B(B 要打真的更新站端點)→ F(F 要打 B 的新形狀)→ D(手冊要新畫面)。四棒嚴格序列。
客戶第一次導入 Guidant AI 時,主系統先裝好,接著要在客戶內網挑一台 Linux 主機裝 Agent。裝 Agent 需要兩個檔:guidant-agent-compose-<版>.tar.gz 和同名 .sha256。這兩個檔目前只在原廠的 Cloudflare R2 上,客戶沒有任何管道自己拿,每次都要原廠人工跑 scripts/r2_presign.py 產 24 小時連結寄過去。
情境的重點是「還沒裝 Agent」。這時主系統上通常也沒有任何 Agent 包(主系統只在推送 Agent 更新時才會預先下載),所以 v1.0「從主系統本機供檔」在最需要它的時候反而沒東西可給,必須先去更新中心按「預先下載」把 1.7 GB 拉進主系統,再從主系統下載一次,多繞一圈且多佔主系統磁碟與頻寬。
R2 本來就是發布物的正本所在地,更新站本來就會簽 R2 預簽網址給主系統下載升級包。讓管理員直接拿到同一種網址,是最短路徑。
更新站(update-center,Cloudflare Worker)
POST /v1/download-url(src/routes/client.ts:67-89):主系統帶授權身分與 product(host/agent)、version,Worker 驗授權、確認這張授權看得到這一版,回 {version, sha256, size, url, expires_in}。artifact 目前只認 ops(主系統維運小檔,Agent 不准帶)。src/releases.ts:203-216 presignDownload(),有效時間是常數 DOWNLOAD_URL_TTL_SECONDS = 15 * 60(:13)。agent/v1/<版>/guidant-agent-compose-<版>.tar.gz、同目錄 .tar.gz.sha256、release.json(releaseObjectKeys(),:55-63)。2026-11-10 實查 1.7.4 三個物件都在 R2 上(包 1737.7 MB)。/admin(src/routes/admin.ts、public/admin.html),Cloudflare Access 保護,只有原廠放行的信箱進得去。現有分頁:版本、內部機器、黑名單、簽章金鑰、CI token。D1 沒有通用設定表。/v1/check 的 Agent 線以「各台 Agent 現行版中最舊」當 current_version 算可升路徑;一台 Agent 都沒有時主系統送 0.0.0(agent_release_service.py:197)。主系統(compliance-manager-be)
app/product_update/service/update_site_client.py:download_url(product, …, version)(:160-165)回 URL,並檢查網址主機在 DOWNLOAD_HOSTS(R2 端點)白名單內才放行。AgentReleaseService:check()(:190-213)問更新站 Agent 最新版、驗簽章清單,結果寫 PRODUCT_UPDATE.agent_last_check;public_last_check() 給畫面用。主機身分 _host_identity()(product_update_flow.py:186-197)取持照租戶的授權檔原文、機器指紋、更新站身分票。api/product_update/routes/agent_bundle_link_route.py、api/product_update/serializers/agent_bundle_link.py、app/product_update/service/agent_bundle_link_service.py、test/test_agent_bundle_link_route.py、DI 一個 Factory、product_update_error_code.py 四個碼。主系統前端(compliance-manager-fe)
src/components/product-update/AgentBundleLinks.vue、AgentBundleButton.vue、AgentBundleLinkTtlField.vue、src/composables/useAgentBundleLinks.js、src/utils/clipboardUtil.js;掛點在 AgentRolloutPanel.vue 與 AgentEnrollTokenCreateDialog.vue。管理員拿到的兩條網址是 https://<account>.r2.cloudflarestorage.com/guidant-ai-release/agent/v1/<版>/…?X-Amz-…,由更新站用 R2 的 S3 相容金鑰簽發。主系統不存包、不供檔、不經手檔案內容。
為什麼:情境是「還沒裝 Agent」,主系統手上通常沒有包;R2 是正本,直接給最短。下載走 Cloudflare 網路,速度與頻寬都不壓在客戶主系統上,v1.0 為了保護主系統執行緒而做的併發上限整套不需要。
排除:主系統本機供檔(v1.0)。要先在更新中心「預先下載」1.7 GB 才有東西給,裝第一台 Agent 時多一道且多一份磁碟;1.7 GB 走主系統 API 串流佔執行緒,因此又長出併發計數與 429。
排除:兩條都給(本機+R2)。兩套來源、兩套時限、兩套錯誤處理,客戶看到兩組網址不知道用哪個;客戶內網沒有外網時,管理員本來就是在自己電腦下載再帶進機房,R2 足夠。
代價:下載者的電腦要能連 Cloudflare。主系統本身連不到更新站(封閉網路、沒設 UPDATE_SITE_URL)時拿不到網址,畫面明講「主系統連不到原廠更新站,請向原廠索取安裝包」。
流程:瀏覽器 → 主系統 POST agents/bundle-links → 主系統以主機授權身分打更新站 /v1/download-url 兩次(安裝包、.sha256)→ 回兩條網址給瀏覽器。
為什麼:更新站只認授權身分(授權檔、機器指紋、身分票),這些只在主系統伺服器端,不能交給瀏覽器。沿用既有 update_site_client.download_url() 與 DOWNLOAD_HOSTS 白名單,主系統拿到的網址主機不是原廠 R2 就拒絕轉交。
主系統換網址時先問 /v1/check product=agent 拿 latest,再對這一版換網址;不依賴主系統本機有沒有包、不依賴更新中心有沒有按過「檢查」。版號與主系統的相容性(min_host_version)沿用 Agent 線既有判斷:最新版要求較新的主系統時,回 412 並說明要先升主系統到哪一版。
為什麼:管理員要的是「現在可以裝的那一版」,不是上次檢查的快照。
查詢基準版:一台 Agent 都沒有時送 0.0.0,更新站回的 latest 就是這條線最新版(agent_release_service.check() 已是這個行為)。為了不覆寫更新中心 Agent 區的「上次檢查」紀錄,換網址時的查詢不寫 agent_last_check,另走一支唯讀查詢。
更新站 D1 新增 site_settings(key/value/updated_at/updated_by),key download_url_ttl_seconds,預設 900(15 分鐘,與現值相同),範圍 300~86400。後台新增「設定」分頁一個數字欄位(單位分鐘)。presignDownload() 改讀這個設定,主系統升級包、ops 小檔、Agent 包全部共用同一個值。
為什麼:決策者 2026-11-10 裁「時限不能讓客戶改,要產品管理員改」。網址是更新站簽的,時限自然由簽發方決定;放在主系統讓客戶管理員改,等於讓客戶延長原廠發布物的公開下載時間。
排除:主系統保留可調欄位。客戶不應有這個權限。排除:只給 Agent 安裝包獨立時限。同一個預簽函式多一個分支、兩個值,目前沒有需求要它們不同。
影響:主系統自己下載升級包時拿到的網址時限也跟著變。主系統下載用的是續傳+每段重拿網址(package_downloader.py),時限變短或變長都不影響正確性;下限 5 分鐘確保單段 50 MB 拿得完。
.sha256 用 artifact=sha256 換,網址帶下載檔名/v1/download-url 的 artifact 從只認 ops 擴成認 ops/sha256。sha256 兩條產品線都可帶(主系統升級包旁也有 .sha256),物件不在 R2 上回 404。
兩種 artifact 的預簽網址都帶 response-content-disposition=attachment; filename="<檔名>"。沒有它的話,瀏覽器點 .sha256 會在分頁裡顯示文字而不是存檔,安裝包則會用 R2 物件路徑的最後一段當檔名(剛好正確,但不保證)。
/v1/download-url 的下載紀錄(release_download_log)只在拿整包時記,.sha256 不記,與 ops 一致。
更新中心 Agent 區「取得安裝包」鈕(讀權限即可見)與建立註冊 token 對話框的「Agent 安裝包」區塊,沿用 v1.0 的 AgentBundleLinks.vue 與 AgentBundleButton.vue。鈕不再因為「主系統沒有包」而停用;改為主系統沒設更新站時停用並提示。
產生網址要 system.product-update.read(v1.0 D5 不變)。每人每分鐘 10 次(v1.0 D11 的發票節流保留),因為每次產生會讓主系統打更新站兩到三次。
每次產生網址寫一筆系統日誌,事件碼 6251(與更新中心「預先下載」共用),action AGENT_BUNDLE_LINK_ISSUED,欄位 actor、version、expires_in、source=r2。
v1.0 的下載前 HEAD 探測、429/401 處理整段拿掉:R2 預簽網址被打 HEAD 也要簽章且簽的是 GET(會 403),而且 R2 沒有併發上限要擋。保留「下載」鈕按下後鎖 10 秒與「請看瀏覽器下載列」提示,防的是使用者以為沒反應再按。到期判斷沿用「收到網址的時刻+expires_in」,不比伺服器時間。
| 拿掉 | 原因 |
|---|---|
本機憑票供檔端點 agents/bundle/<version>/<kind> |
不再從主系統供檔 |
時限設定端點 agents/bundle-link-settings、AgentBundleLinkTtlField.vue |
時限移到更新站後台 |
串流併發計數(StreamSlots、兩個 system_configs key)、PRODUCT_UPDATE_429001 |
R2 不經主系統 |
時限錯誤碼 PRODUCT_UPDATE_400013 |
主系統不再驗時限 |
簽章票 scope agent_bundle_download |
不再發主系統自己的票 |
| FE 探測、429/401 處理 | 見 D9 |
保留:AgentBundleLinks.vue、AgentBundleButton.vue、useAgentBundleLinks.js(改內容)、clipboardUtil.js、兩個掛點、PRODUCT_UPDATE_429002(產生太頻繁)、PRODUCT_UPDATE_404005(改語意為「更新站上沒有可用的 Agent 版本」)。
錯誤碼字串一旦對外就是契約;429001、400013 從 FE error-code.json 拿掉前,B 卡確認沒有其他呼叫端(grep BE 與 FE)。
POST /api/1.0/system/product-update/agents/bundle-links守門:登入、system.product-update.read、每人每分鐘 10 次。Request 無 body(不再接受指定版號:v2.0 只給最新相容版)。
Response data:
{
"version": "1.7.4",
"size": 1822082560,
"sha256": "b771e363…8a4716",
"expires_in": 900,
"expires_at": "2026-11-10T03:15:00+00:00",
"files": [
{"kind": "tarball", "name": "guidant-agent-compose-1.7.4.tar.gz", "url": "https://<account>.r2.cloudflarestorage.com/guidant-ai-release/agent/v1/1.7.4/guidant-agent-compose-1.7.4.tar.gz?X-Amz-…"},
{"kind": "sha256", "name": "guidant-agent-compose-1.7.4.tar.gz.sha256", "url": "https://<account>.r2.cloudflarestorage.com/…/guidant-agent-compose-1.7.4.tar.gz.sha256?X-Amz-…"}
]
}url 是完整網址,前端直接用,不再拼 origin。sha256 與 size 取自更新站 /v1/check 回的簽章清單(主系統驗過簽章),不取 /v1/download-url 回的未簽欄位。
| 情況 | HTTP | 錯誤碼 |
|---|---|---|
| 主系統沒設更新站 | 412 | PRODUCT_UPDATE_412001(既有) |
| 連不到更新站 | 503 | PRODUCT_UPDATE_503001(既有) |
| 沒有授權照 | 412 | PRODUCT_UPDATE_412003(既有) |
| 更新站上沒有這張授權看得到的 Agent 版本 | 404 | PRODUCT_UPDATE_404005 |
| 最新版要求較新的主系統 | 412 | PRODUCT_UPDATE_412014(既有,AGENT_REQUIRES_HOST) |
| 更新站給的網址主機不在白名單 | 503 | PRODUCT_UPDATE_503001 |
| 一分鐘超過 10 次 | 429 | PRODUCT_UPDATE_429002 |
POST /v1/download-url(擴充)artifact:不帶=整包;ops=主系統維運小檔(僅 host);sha256=整包旁的雜湊檔(host、agent 皆可)。物件不在 R2 回 404。回應形狀不變:{version, artifact?, url, expires_in}(整包另帶 sha256、size)。
GET/PUT /admin/settingsCloudflare Access 保護、同源 JSON(沿用 crossSiteWrite)。Body {"download_url_ttl_seconds": 900},範圍 300~86400,整數。PUT 寫 updated_by=Access 身分信箱。後台頁新增「設定」分頁,欄位以分鐘顯示。
%%{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
actor A as 管理員(瀏覽器)
participant B as 主系統 API
participant U as 更新站(Cloudflare Worker)
participant R as Cloudflare R2
actor C as 裝機人員
A->>B: POST agents/bundle-links(bearer、read 權限)
B->>B: 每人每分鐘 10 次節流
B->>U: POST /v1/check product=agent current=0.0.0(主機授權身分)
U-->>B: latest、簽章清單(sha256、size、min_host_version)
B->>B: 用發布公鑰驗清單、比 min_host_version
B->>U: POST /v1/download-url product=agent version=<latest>(整包、artifact=sha256 各一次)
U-->>B: 兩條 R2 預簽網址、expires_in(後台設定)
B-->>A: 版號、大小、SHA-256、兩條網址、到期時間;寫稽核
A->>C: 下載,或複製網址交給裝機人員
C->>R: GET 預簽網址(不需任何帳號)
R-->>C: 檔案(Content-Disposition 帶檔名)
C->>C: sha256sum -c
第 3 步的 current=0.0.0 讓更新站回這條線的最新版,與目前有沒有 Agent、Agent 是幾版無關。第 5 步沿用 Agent 線既有的驗章(read_signed_manifest)與相容判斷;最新版要求較新主系統時在這裡回 412,不給網址。第 6 步的兩次呼叫任一失敗就整個失敗,不回半組網址。第 9 步之後主系統不知道有沒有人下載,也不需要知道。
bundle-links;顯示版號、大小、SHA-256(可複製)、兩列檔名與完整網址、各帶〔下載〕〔複製連結〕、一行「連結 N 分鐘內有效,到 HH:MM」。過期後整塊變灰、出現「重新產生」。錯誤時(412/503/404)整塊換成對應白話訊息,412001/503001 的訊息附一句「請向原廠索取安裝包」。<a href download>,按下鎖 10 秒並顯示「已開始下載,請看瀏覽器下載列」。DOWNLOAD_HOSTS 白名單(既有),防更新站被冒充時把管理員導到別處。release_download_log。| 項目 | v1.0 | v2.0 | 為什麼改 |
|---|---|---|---|
| 檔案來源 | 主系統本機 agent-bundles/<版>/,憑主系統簽章票下載 |
Cloudflare R2,更新站預簽網址 | 情境是還沒裝 Agent,主系統通常沒有包;R2 是正本(決策者 2026-11-10「直接給 R2」) |
| 時限設定者 | 主系統更新中心,客戶管理員可改(1~1440 分) | 原廠在更新站後台改(5~1440 分),所有預簽網址共用 | 決策者 2026-11-10「這應該不能改,要讓產品管理員改」 |
| 前置條件 | 主系統要先「預先下載」 | 主系統能連更新站 | 同第一列 |
| 併發上限 | 同票 2 條、全站 3 條、429 | 拿掉 | 下載不經主系統 |
| 前端探測 | HEAD 探測、處理 429/401 | 拿掉,只留 10 秒連點鎖 | R2 預簽網址不接 HEAD;無併發限制 |
| 版號 | 可指定,預設主系統現行可推的包 | 只給更新站最新相容版 | 管理員要的是「現在能裝的」 |
.sha256 來源 |
主系統本機檔 | R2 物件,artifact=sha256 |
同第一列 |
| 改 update-center | 不改 | 改(artifact、Content-Disposition、TTL 設定、後台分頁) | 時限與 sha256 都在簽發端 |
| 被推翻的 v1.0 決策 | — | D1(本機供檔)、D4(主系統可調時限)、D9(沒包先預先下載)、D11 的併發與探測部分 | 見上 |
| 保留的 v1.0 決策 | — | D2 兩入口共用元件、D5 讀權限、D8 稽核(欄位改)、D10 剪貼簿退路、D11 的發票節流與 10 秒連點鎖 | 與來源無關 |