From dc740f14ef0ddee0cf39a414978408170e3b6119 Mon Sep 17 00:00:00 2001 From: "hanzhi.421" Date: Mon, 7 Sep 2026 02:04:24 +0800 Subject: [PATCH 1/2] feat(studio): update sandbox images from regional catalogs --- docs/content/docs/framework/frontend.mdx | 12 + frontend/README.md | 12 + frontend/server/sandbox_updates.py | 60 ++ frontend/src/adk/client.ts | 55 ++ frontend/src/i18n/resources/en-US/adk.json | 4 + frontend/src/i18n/resources/en-US/ui.json | 7 + frontend/src/i18n/resources/zh-CN/adk.json | 4 + frontend/src/i18n/resources/zh-CN/ui.json | 7 + frontend/src/ui/SystemInfo.css | 13 + frontend/src/ui/SystemInfo.tsx | 153 +++-- frontend/tests/sandboxImageUpdates.test.mjs | 138 +++++ frontend/tests/systemInfo.test.mjs | 14 +- tests/cli/test_studio_sandbox_updates.py | 221 ++++++++ veadk/cli/cli_frontend.py | 19 + veadk/cli/studio_sandbox_updates.py | 209 +++++++ .../{index-BTMjXX7l.js => index-CabUMMxm.js} | 432 +++++++------- ...Dc4zCbz4.js => CodeDiffEditor-BwQXqX7L.js} | 2 +- ...iY.js => MarkdownPromptEditor-Cmx-E7im.js} | 2 +- .../{arc-CtHnki0y.js => arc-WlqfJpSf.js} | 2 +- ...hannel-Dt_8mnHI.js => channel-BdGAp6Bu.js} | 2 +- ...ex.es-CRKLEZzU.js => index.es-BBymJK5V.js} | 2 +- ...n-C-fRYddM.js => jspdf.es.min-CHN3nDIH.js} | 6 +- ...{linear-D8uHbSyw.js => linear-BdKQNUOz.js} | 2 +- ...{index-CFHe9RW3.css => index-COnd7cm-.css} | 2 +- ...3E.js => abnfDiagram-N423BO3Z-otTk03y_.js} | 2 +- ... architectureDiagram-T3A2C74G-HTD-nABp.js} | 2 +- ...4.js => blockDiagram-VBNYF7ZC-D5O01u9u.js} | 2 +- ...-F44.js => c4Diagram-5PPSVZJV-DQgHK4Ym.js} | 2 +- ...CQhzWi6x.js => chunk-2GRJ4B5K-CnAofnNE.js} | 2 +- ...BAW9OQlC.js => chunk-2Q5K7J3B-Day2i5zo.js} | 2 +- ...BU1Yx3mb.js => chunk-5RXB4S5H-xuSDHQPH.js} | 2 +- ...CNmJ5Vnu.js => chunk-5VM5RSS4-DKEilnyR.js} | 2 +- ...CnZtGo2n.js => chunk-6Q2QTUOP-BIObpTu-.js} | 2 +- ...EpguA5Fk.js => chunk-GF5L2VYU-lNR6WpLV.js} | 2 +- ...Ddm4-Uj1.js => chunk-JWPE2WC7-Bbjf4kNX.js} | 2 +- ...D0xof09d.js => chunk-KBJHAD2P-Ec-jWcvG.js} | 2 +- ...ByW8v6x7.js => chunk-RYQCIY6F-DprPcpVU.js} | 2 +- ...1VB0pm6t.js => chunk-XXDRQBXY-fGxqsBab.js} | 2 +- .../mermaid/classDiagram-JCYQIIEL-Bqks8_vs.js | 1 - .../mermaid/classDiagram-JCYQIIEL-t7hoVl-n.js | 1 + .../classDiagram-v2-OCEON4UE-Bqks8_vs.js | 1 - .../classDiagram-v2-OCEON4UE-t7hoVl-n.js | 1 + ...a.js => cose-bilkent-JH36ORCC-CyD8w6Ui.js} | 2 +- ...F5SkDn.js => cynefin-OW5HDTMX-B-cb2KIl.js} | 2 +- ...js => cynefinDiagram-MW4NZA55-ByRifo-3.js} | 2 +- ...CS-CEQLB.js => dagre-VZM6K2ZE-0pfut0iF.js} | 2 +- ...hGUeqm.js => diagram-7IWD3JNH-Bm0xt2c3.js} | 2 +- ...FD9FV2.js => diagram-B4RE2ZJO-DUPsMLSA.js} | 2 +- ...5MzXDx.js => diagram-LBJQPF4R-CjJ4w1e1.js} | 2 +- ...T2VD8V.js => diagram-Q27KOJAE-Bq03nO9z.js} | 2 +- ...hsc4Sm.js => diagram-UB23O5K3-Gci0-hYB.js} | 2 +- ...1W.js => ebnfDiagram-BXEA7PRR-BiAg0dyv.js} | 2 +- ...HrGS.js => erDiagram-JOGREHBK-PmYNKnGJ.js} | 2 +- ...be.js => flowDiagram-UKHOOZJN-CwGqFLE4.js} | 2 +- ...M.js => ganttDiagram-PKOTCBZU-CWkHaGKW.js} | 2 +- ...s => gitGraphDiagram-DS77QQ5N-Cp-2PIhL.js} | 2 +- ...-w.js => infoDiagram-6WML65LV-D7kbZeEZ.js} | 2 +- ...s => ishikawaDiagram-WSZJBQD7-BKsh9FBp.js} | 2 +- ...js => journeyDiagram-NVQOT4AX-DA6QDm-o.js} | 2 +- ...=> kanban-definition-27J2QSJJ-DVvvVCQP.js} | 2 +- ...e-CgG2SnCE.js => mermaid.core-C8O8nWja.js} | 8 +- ...> mindmap-definition-FAOFIHXS-DjOpqiPH.js} | 2 +- ...wCY.js => pegDiagram-VL7TDLO6-D7Kei1hp.js} | 2 +- ...2WV.js => pieDiagram-7S7Q4E2Y-DD26SjDu.js} | 2 +- ...s => quadrantDiagram-CIZ2JOQS-Gen0mzte.js} | 2 +- ...s => railroadDiagram-AXF67PYL-Bh7fUe_W.js} | 2 +- ...> requirementDiagram-LRYGKXZP-3-cQ2cC5.js} | 2 +- ....js => sankeyDiagram-W5VNT64P-UFuFUzXv.js} | 2 +- ...s => sequenceDiagram-SI44F4Z6-RsLKg6FA.js} | 2 +- ...FX.js => sizeCapture-X5ZJPWSS-CB400P_x.js} | 2 +- ...a.js => stateDiagram-OKZ733FA-Cr4A4pCM.js} | 2 +- .../stateDiagram-v2-UEYNNEHI-B_c2Bboe.js | 1 - .../stateDiagram-v2-UEYNNEHI-QSue9JJb.js | 1 + ...o9Vi.js => swimlanes-SLNWSIFB-UKrKtIOj.js} | 4 +- .../swimlanesDiagram-ULZ7WXOC-CJOGkX8K.js | 8 + .../swimlanesDiagram-ULZ7WXOC-D1hhYBjj.js | 8 - ... timeline-definition-Z64GVDOM-s3TJplsm.js} | 2 +- ...70.js => vennDiagram-T6HMQDX7-BVcQInL2.js} | 2 +- ...js => wardleyDiagram-T6FBY63Y-DnSlQqra.js} | 2 +- ...js => xychartDiagram-ELKLHX3M-CKItde_L.js} | 2 +- veadk/webui/index.html | 4 +- veadk/webui/website-integration.js | 526 +++++++++--------- 82 files changed, 1412 insertions(+), 624 deletions(-) create mode 100644 frontend/server/sandbox_updates.py create mode 100644 frontend/tests/sandboxImageUpdates.test.mjs create mode 100644 tests/cli/test_studio_sandbox_updates.py create mode 100644 veadk/cli/studio_sandbox_updates.py rename veadk/webui/assets/app/{index-BTMjXX7l.js => index-CabUMMxm.js} (73%) rename veadk/webui/assets/chunks/{CodeDiffEditor-Dc4zCbz4.js => CodeDiffEditor-BwQXqX7L.js} (99%) rename veadk/webui/assets/chunks/{MarkdownPromptEditor-DTTkA6iY.js => MarkdownPromptEditor-Cmx-E7im.js} (99%) rename veadk/webui/assets/chunks/{arc-CtHnki0y.js => arc-WlqfJpSf.js} (98%) rename veadk/webui/assets/chunks/{channel-Dt_8mnHI.js => channel-BdGAp6Bu.js} (53%) rename veadk/webui/assets/chunks/{index.es-CRKLEZzU.js => index.es-BBymJK5V.js} (99%) rename veadk/webui/assets/chunks/{jspdf.es.min-C-fRYddM.js => jspdf.es.min-CHN3nDIH.js} (99%) rename veadk/webui/assets/chunks/{linear-D8uHbSyw.js => linear-BdKQNUOz.js} (98%) rename veadk/webui/assets/styles/{index-CFHe9RW3.css => index-COnd7cm-.css} (63%) rename veadk/webui/assets/visualizations/mermaid/{abnfDiagram-N423BO3Z--G-I1j3E.js => abnfDiagram-N423BO3Z-otTk03y_.js} (86%) rename veadk/webui/assets/visualizations/mermaid/{architectureDiagram-T3A2C74G-e64hi2HJ.js => architectureDiagram-T3A2C74G-HTD-nABp.js} (99%) rename veadk/webui/assets/visualizations/mermaid/{blockDiagram-VBNYF7ZC-CuI_3Hv4.js => blockDiagram-VBNYF7ZC-D5O01u9u.js} (99%) rename veadk/webui/assets/visualizations/mermaid/{c4Diagram-5PPSVZJV-DVVK-F44.js => c4Diagram-5PPSVZJV-DQgHK4Ym.js} (99%) rename veadk/webui/assets/visualizations/mermaid/{chunk-2GRJ4B5K-CQhzWi6x.js => chunk-2GRJ4B5K-CnAofnNE.js} (93%) rename veadk/webui/assets/visualizations/mermaid/{chunk-2Q5K7J3B-BAW9OQlC.js => chunk-2Q5K7J3B-Day2i5zo.js} (66%) rename veadk/webui/assets/visualizations/mermaid/{chunk-5RXB4S5H-BU1Yx3mb.js => chunk-5RXB4S5H-xuSDHQPH.js} (99%) rename veadk/webui/assets/visualizations/mermaid/{chunk-5VM5RSS4-CNmJ5Vnu.js => chunk-5VM5RSS4-DKEilnyR.js} (83%) rename veadk/webui/assets/visualizations/mermaid/{chunk-6Q2QTUOP-CnZtGo2n.js => chunk-6Q2QTUOP-BIObpTu-.js} (99%) rename veadk/webui/assets/visualizations/mermaid/{chunk-GF5L2VYU-EpguA5Fk.js => chunk-GF5L2VYU-lNR6WpLV.js} (99%) rename veadk/webui/assets/visualizations/mermaid/{chunk-JWPE2WC7-Ddm4-Uj1.js => chunk-JWPE2WC7-Bbjf4kNX.js} (78%) rename veadk/webui/assets/visualizations/mermaid/{chunk-KBJHAD2P-D0xof09d.js => chunk-KBJHAD2P-Ec-jWcvG.js} (87%) rename veadk/webui/assets/visualizations/mermaid/{chunk-RYQCIY6F-ByW8v6x7.js => chunk-RYQCIY6F-DprPcpVU.js} (99%) rename veadk/webui/assets/visualizations/mermaid/{chunk-XXDRQBXY-1VB0pm6t.js => chunk-XXDRQBXY-fGxqsBab.js} (53%) delete mode 100644 veadk/webui/assets/visualizations/mermaid/classDiagram-JCYQIIEL-Bqks8_vs.js create mode 100644 veadk/webui/assets/visualizations/mermaid/classDiagram-JCYQIIEL-t7hoVl-n.js delete mode 100644 veadk/webui/assets/visualizations/mermaid/classDiagram-v2-OCEON4UE-Bqks8_vs.js create mode 100644 veadk/webui/assets/visualizations/mermaid/classDiagram-v2-OCEON4UE-t7hoVl-n.js rename veadk/webui/assets/visualizations/mermaid/{cose-bilkent-JH36ORCC-BsDHpVNa.js => cose-bilkent-JH36ORCC-CyD8w6Ui.js} (99%) rename veadk/webui/assets/visualizations/mermaid/{cynefin-OW5HDTMX-CFF5SkDn.js => cynefin-OW5HDTMX-B-cb2KIl.js} (99%) rename veadk/webui/assets/visualizations/mermaid/{cynefinDiagram-MW4NZA55-xhYuAfwZ.js => cynefinDiagram-MW4NZA55-ByRifo-3.js} (98%) rename veadk/webui/assets/visualizations/mermaid/{dagre-VZM6K2ZE-CS-CEQLB.js => dagre-VZM6K2ZE-0pfut0iF.js} (97%) rename veadk/webui/assets/visualizations/mermaid/{diagram-7IWD3JNH-BshGUeqm.js => diagram-7IWD3JNH-Bm0xt2c3.js} (96%) rename veadk/webui/assets/visualizations/mermaid/{diagram-B4RE2ZJO-BSFD9FV2.js => diagram-B4RE2ZJO-DUPsMLSA.js} (97%) rename veadk/webui/assets/visualizations/mermaid/{diagram-LBJQPF4R-BL5MzXDx.js => diagram-LBJQPF4R-CjJ4w1e1.js} (94%) rename veadk/webui/assets/visualizations/mermaid/{diagram-Q27KOJAE-H7T2VD8V.js => diagram-Q27KOJAE-Bq03nO9z.js} (98%) rename veadk/webui/assets/visualizations/mermaid/{diagram-UB23O5K3-qDhsc4Sm.js => diagram-UB23O5K3-Gci0-hYB.js} (95%) rename veadk/webui/assets/visualizations/mermaid/{ebnfDiagram-BXEA7PRR-DP83Wg1W.js => ebnfDiagram-BXEA7PRR-BiAg0dyv.js} (87%) rename veadk/webui/assets/visualizations/mermaid/{erDiagram-JOGREHBK-C_NDHrGS.js => erDiagram-JOGREHBK-PmYNKnGJ.js} (99%) rename veadk/webui/assets/visualizations/mermaid/{flowDiagram-UKHOOZJN-DHa5Ywbe.js => flowDiagram-UKHOOZJN-CwGqFLE4.js} (99%) rename veadk/webui/assets/visualizations/mermaid/{ganttDiagram-PKOTCBZU-DnaE7egM.js => ganttDiagram-PKOTCBZU-CWkHaGKW.js} (99%) rename veadk/webui/assets/visualizations/mermaid/{gitGraphDiagram-DS77QQ5N-DoNDq9az.js => gitGraphDiagram-DS77QQ5N-Cp-2PIhL.js} (99%) rename veadk/webui/assets/visualizations/mermaid/{infoDiagram-6WML65LV-90pCWD-w.js => infoDiagram-6WML65LV-D7kbZeEZ.js} (69%) rename veadk/webui/assets/visualizations/mermaid/{ishikawaDiagram-WSZJBQD7-Bq-tqJVn.js => ishikawaDiagram-WSZJBQD7-BKsh9FBp.js} (99%) rename veadk/webui/assets/visualizations/mermaid/{journeyDiagram-NVQOT4AX-LGlO43Pc.js => journeyDiagram-NVQOT4AX-DA6QDm-o.js} (98%) rename veadk/webui/assets/visualizations/mermaid/{kanban-definition-27J2QSJJ-CE5fA4Iv.js => kanban-definition-27J2QSJJ-DVvvVCQP.js} (99%) rename veadk/webui/assets/visualizations/mermaid/{mermaid.core-CgG2SnCE.js => mermaid.core-C8O8nWja.js} (98%) rename veadk/webui/assets/visualizations/mermaid/{mindmap-definition-FAOFIHXS-VwD6p6YM.js => mindmap-definition-FAOFIHXS-DjOpqiPH.js} (98%) rename veadk/webui/assets/visualizations/mermaid/{pegDiagram-VL7TDLO6-DAAqkwCY.js => pegDiagram-VL7TDLO6-D7Kei1hp.js} (87%) rename veadk/webui/assets/visualizations/mermaid/{pieDiagram-7S7Q4E2Y-Dkp5z2WV.js => pieDiagram-7S7Q4E2Y-DD26SjDu.js} (96%) rename veadk/webui/assets/visualizations/mermaid/{quadrantDiagram-CIZ2JOQS-5IBhENnF.js => quadrantDiagram-CIZ2JOQS-Gen0mzte.js} (99%) rename veadk/webui/assets/visualizations/mermaid/{railroadDiagram-AXF67PYL-_RNliMR_.js => railroadDiagram-AXF67PYL-Bh7fUe_W.js} (84%) rename veadk/webui/assets/visualizations/mermaid/{requirementDiagram-LRYGKXZP-Cj4V8yCz.js => requirementDiagram-LRYGKXZP-3-cQ2cC5.js} (99%) rename veadk/webui/assets/visualizations/mermaid/{sankeyDiagram-W5VNT64P-bzIgNDhv.js => sankeyDiagram-W5VNT64P-UFuFUzXv.js} (99%) rename veadk/webui/assets/visualizations/mermaid/{sequenceDiagram-SI44F4Z6-B3GdniwZ.js => sequenceDiagram-SI44F4Z6-RsLKg6FA.js} (99%) rename veadk/webui/assets/visualizations/mermaid/{sizeCapture-X5ZJPWSS-9615TpFX.js => sizeCapture-X5ZJPWSS-CB400P_x.js} (87%) rename veadk/webui/assets/visualizations/mermaid/{stateDiagram-OKZ733FA-BcN2g_Ra.js => stateDiagram-OKZ733FA-Cr4A4pCM.js} (96%) delete mode 100644 veadk/webui/assets/visualizations/mermaid/stateDiagram-v2-UEYNNEHI-B_c2Bboe.js create mode 100644 veadk/webui/assets/visualizations/mermaid/stateDiagram-v2-UEYNNEHI-QSue9JJb.js rename veadk/webui/assets/visualizations/mermaid/{swimlanes-SLNWSIFB-DwLXo9Vi.js => swimlanes-SLNWSIFB-UKrKtIOj.js} (99%) create mode 100644 veadk/webui/assets/visualizations/mermaid/swimlanesDiagram-ULZ7WXOC-CJOGkX8K.js delete mode 100644 veadk/webui/assets/visualizations/mermaid/swimlanesDiagram-ULZ7WXOC-D1hhYBjj.js rename veadk/webui/assets/visualizations/mermaid/{timeline-definition-Z64GVDOM-BdS3Tt9W.js => timeline-definition-Z64GVDOM-s3TJplsm.js} (99%) rename veadk/webui/assets/visualizations/mermaid/{vennDiagram-T6HMQDX7-Bvd8D570.js => vennDiagram-T6HMQDX7-BVcQInL2.js} (99%) rename veadk/webui/assets/visualizations/mermaid/{wardleyDiagram-T6FBY63Y-C3ncQS0w.js => wardleyDiagram-T6FBY63Y-DnSlQqra.js} (99%) rename veadk/webui/assets/visualizations/mermaid/{xychartDiagram-ELKLHX3M-DWnakrFd.js => xychartDiagram-ELKLHX3M-CKItde_L.js} (99%) diff --git a/docs/content/docs/framework/frontend.mdx b/docs/content/docs/framework/frontend.mdx index a43e4530e..3596a47c0 100644 --- a/docs/content/docs/framework/frontend.mdx +++ b/docs/content/docs/framework/frontend.mdx @@ -518,3 +518,15 @@ export function RevenueChart({ node, ctx }: ComponentRendererProps) { 未注册渲染器的未知组件会回退到可折叠的 JSON 视图,因此目录与渲染器不匹配也不会导致界面崩溃。 + + +## 沙箱版本更新 + +管理员可在「系统信息 → 沙箱信息」检查沙箱版本。Studio 按云厂商和沙箱实际区域 +查询发布镜像,展示当前版本和目标版本;存在差异时可点击对应沙箱的更新按钮。 +Volcengine 和 BytePlus 使用各自的凭据及接口,镜像目录不会跨厂商、区域复用。 + +Codex 和 DeepSeek Harness 共用 Tool 时,两处同步显示更新状态,只需更新一次。 +快照版使用对应的 Tool 单独检查。Codex 缺失的模型环境变量仍会从原 `CODEX_*` +配置补齐。更新完成表示 Tool 已就绪且镜像匹配目标版本,不代表已有 Session 或 +历史快照已切换镜像。版本查询失败可点击「检查更新」重试。 diff --git a/frontend/README.md b/frontend/README.md index 3815af74f..9a9467625 100644 --- a/frontend/README.md +++ b/frontend/README.md @@ -5,6 +5,18 @@ server that `veadk frontend` launches — no separate backend. ## Features +- **Sandbox updates** in System Information compare each Tool's current image + with `ListToolTypes` for its cloud provider and actual region. Volcengine and + BytePlus use their own credentials and API hosts; catalogs are cached for + 60 seconds and fetched again before an update. Admins can update configured + prebuilt Tools with `UpdateTool.ImageUrl`. Codex and DeepSeek Harness share + update state when they reference the same Tool; snapshot Tools are checked + independently. Missing Codex `MODEL_AGENT_API_KEY` / `MODEL_AGENT_BASE_URL` + values are still backfilled from `CODEX_*`, preserving existing variables. + Completion requires Tool `Ready` and the target image; this does not verify + existing Sessions or rebuild their snapshots. BytePlus has automated coverage, + but its image update has not been verified against a live account. + - **Streaming chat** over the ADK `/run_sse` event stream. While an Agent is generating, the composer exposes a stop control that cancels only the active response, preserves content already received, and immediately enables the diff --git a/frontend/server/sandbox_updates.py b/frontend/server/sandbox_updates.py new file mode 100644 index 000000000..b19ec6887 --- /dev/null +++ b/frontend/server/sandbox_updates.py @@ -0,0 +1,60 @@ +"""Admin-only sandbox image inspection and update routes.""" + +from __future__ import annotations + +import asyncio +from collections.abc import Callable +from typing import Any + +from fastapi import FastAPI, HTTPException, Request + +from veadk.cli.studio_sandbox_updates import SandboxToolUpdates, SandboxUpdateError + + +def register_sandbox_update_routes( + app: FastAPI, + *, + service: SandboxToolUpdates, + require_admin: Callable[[Request], None], + configured_tools: Callable[[], dict[str, str]], +) -> None: + @app.get("/web/system-info/sandbox-tools/updates") + async def inspect_updates(request: Request): + require_admin(request) + # Codex and DSH aliases resolve to one physical resource. + tool_ids = set(configured_tools().values()) - {""} + + async def inspect(tool_id: str) -> dict[str, Any]: + try: + return await asyncio.wait_for( + asyncio.to_thread(service.inspect, tool_id), timeout=15 + ) + except TimeoutError: + return {"toolId": tool_id, "error": "查询沙箱版本超时,请刷新重试"} + except Exception: + # SDK errors can include request bodies or credentials. Keep + # this response separate from the raw control-plane exception. + return { + "toolId": tool_id, + "error": "查询沙箱版本失败,请检查凭据、区域及接口权限后重试", + } + + return {"tools": await asyncio.gather(*(inspect(t) for t in sorted(tool_ids)))} + + @app.post("/web/system-info/sandbox-tools/{kind}/update") + async def update(request: Request, kind: str): + require_admin(request) + tool_id = configured_tools().get(kind) + if tool_id is None: + raise HTTPException(status_code=400, detail="Unsupported Sandbox Tool kind") + if not tool_id: + raise HTTPException(status_code=400, detail="未配置 Sandbox Tool ID") + try: + result = await asyncio.to_thread(service.update, tool_id) + except (SandboxUpdateError, TimeoutError) as error: + raise HTTPException(status_code=409, detail=str(error)) from error + except Exception as error: + raise HTTPException( + status_code=502, detail="Sandbox 更新失败,请刷新检查实际状态及接口权限" + ) from error + return {"kind": kind, "toolId": tool_id, **result} diff --git a/frontend/src/adk/client.ts b/frontend/src/adk/client.ts index e8cd6135a..191b287a6 100644 --- a/frontend/src/adk/client.ts +++ b/frontend/src/adk/client.ts @@ -5040,3 +5040,58 @@ export async function deleteGeneratedAgentTestRun(runId: string): Promise throw new Error(await httpErrorMessage(res, adkT("client.cleanupDebugRunFailed"))); } } + +export interface SandboxImageState { + toolId: string; + error: string; + provider?: CloudProvider; + region?: string; + toolType?: string; + status?: string; + currentImage?: string; + latestImage?: string; + needsImageUpdate?: boolean; + needsModelEnvUpdate?: boolean; + canUpdateModelEnv?: boolean; + modelEnvError?: string; + canUpdate?: boolean; +} + +function sandboxImageState(value: unknown): SandboxImageState { + if (!value || typeof value !== "object") throw new Error(adkT("client.invalidSandboxVersion")); + const row = value as SandboxImageState; + if (typeof row.toolId !== "string" || typeof row.error !== "string") { + throw new Error(adkT("client.invalidSandboxVersion")); + } + if (!row.error && ( + (row.provider !== "volcengine" && row.provider !== "byteplus") || + typeof row.region !== "string" || typeof row.status !== "string" || + typeof row.currentImage !== "string" || typeof row.latestImage !== "string" || + typeof row.needsImageUpdate !== "boolean" || typeof row.canUpdate !== "boolean" || + typeof row.needsModelEnvUpdate !== "boolean" || + typeof row.canUpdateModelEnv !== "boolean" || typeof row.modelEnvError !== "string" + )) throw new Error(adkT("client.invalidSandboxVersion")); + return row; +} + +export async function getSandboxImageUpdates(signal?: AbortSignal): Promise { + const response = await apiFetch("/web/system-info/sandbox-tools/updates", { signal }); + if (!response.ok) throw new Error(await httpErrorMessage(response, adkT("client.loadSandboxVersionsFailed"))); + const payload = await response.json() as { tools?: unknown }; + if (!Array.isArray(payload.tools)) throw new Error(adkT("client.invalidSandboxVersion")); + return payload.tools.map(sandboxImageState); +} + +export async function updateSandboxTool(kind: SandboxToolKind): Promise<{ + updated: boolean; + state: SandboxImageState; +}> { + const response = await apiFetch( + `/web/system-info/sandbox-tools/${encodeURIComponent(kind)}/update`, + { method: "POST" }, {}, 330_000, + ); + if (!response.ok) throw new Error(await httpErrorMessage(response, adkT("client.updateSandboxFailed"))); + const payload = await response.json() as { updated?: unknown; state?: unknown }; + if (typeof payload.updated !== "boolean") throw new Error(adkT("client.invalidSandboxUpdate")); + return { updated: payload.updated, state: sandboxImageState(payload.state) }; +} diff --git a/frontend/src/i18n/resources/en-US/adk.json b/frontend/src/i18n/resources/en-US/adk.json index ce730e1ba..9ad4906ce 100644 --- a/frontend/src/i18n/resources/en-US/adk.json +++ b/frontend/src/i18n/resources/en-US/adk.json @@ -326,6 +326,10 @@ "unsafeToolUrl": "{{label}} returned an unsafe URL." }, "client": { + "invalidSandboxVersion": "Invalid sandbox version response", + "loadSandboxVersionsFailed": "Failed to check sandbox versions", + "updateSandboxFailed": "Failed to update sandbox", + "invalidSandboxUpdate": "Invalid sandbox update response", "errorWithDetailAndRawResponse": "{{context}}\n{{detail}}\nRaw response:\n{{response}}", "errorWithRawResponse": "{{context}}\nRaw response:\n{{response}}", "loadArkApiKeysFailed": "Failed to load Ark API keys", diff --git a/frontend/src/i18n/resources/en-US/ui.json b/frontend/src/i18n/resources/en-US/ui.json index 439cabab5..84037d96d 100644 --- a/frontend/src/i18n/resources/en-US/ui.json +++ b/frontend/src/i18n/resources/en-US/ui.json @@ -48,6 +48,13 @@ "console": "Console" }, "systemInfo": { + "checkUpdates": "Check for updates", + "checkingVersions": "Checking versions…", + "versionCheckError": "Unable to check sandbox versions. Check credentials, region and API permissions, then retry.", + "sandboxUpdateError": "Sandbox update failed. Refresh its status before retrying.", + "modelEnvRepairUnavailable": "Cannot repair model configuration. Check CODEX_API_KEY and CODEX_BASE_URL.", + "updateSandbox": "Update {{variant}}{{name}}", + "updatingSandbox": "Updating", "title": "System information", "description": "View the current Studio version and related infrastructure resources", "general": "General", diff --git a/frontend/src/i18n/resources/zh-CN/adk.json b/frontend/src/i18n/resources/zh-CN/adk.json index 928ec2495..741d2f38d 100644 --- a/frontend/src/i18n/resources/zh-CN/adk.json +++ b/frontend/src/i18n/resources/zh-CN/adk.json @@ -326,6 +326,10 @@ "unsafeToolUrl": "{{label}} 返回了不安全的地址。" }, "client": { + "invalidSandboxVersion": "沙箱版本响应格式无效", + "loadSandboxVersionsFailed": "查询沙箱版本失败", + "updateSandboxFailed": "更新 Sandbox 失败", + "invalidSandboxUpdate": "沙箱更新响应格式无效", "errorWithDetailAndRawResponse": "{{context}}\n{{detail}}\n原始响应:\n{{response}}", "errorWithRawResponse": "{{context}}\n原始响应:\n{{response}}", "loadArkApiKeysFailed": "加载 Ark API Key 失败", diff --git a/frontend/src/i18n/resources/zh-CN/ui.json b/frontend/src/i18n/resources/zh-CN/ui.json index c0047f122..4fa888cb9 100644 --- a/frontend/src/i18n/resources/zh-CN/ui.json +++ b/frontend/src/i18n/resources/zh-CN/ui.json @@ -48,6 +48,13 @@ "console": "控制台" }, "systemInfo": { + "checkUpdates": "检查更新", + "checkingVersions": "正在检查版本…", + "versionCheckError": "查询沙箱版本失败,请检查凭据、区域及接口权限后重试", + "sandboxUpdateError": "Sandbox 更新失败,请刷新检查实际状态后重试", + "modelEnvRepairUnavailable": "无法补齐模型环境变量,请检查 CODEX_API_KEY 和 CODEX_BASE_URL", + "updateSandbox": "更新{{variant}}{{name}}", + "updatingSandbox": "更新中", "title": "系统信息", "description": "查看当前 Studio 版本及关联的基础资源", "general": "通用", diff --git a/frontend/src/ui/SystemInfo.css b/frontend/src/ui/SystemInfo.css index 222ed6657..ab8415d9c 100644 --- a/frontend/src/ui/SystemInfo.css +++ b/frontend/src/ui/SystemInfo.css @@ -390,3 +390,16 @@ opacity: 1; } } + +.system-info-refresh { + border: 0; + background: transparent; + color: hsl(var(--muted-foreground)); + font: inherit; + cursor: pointer; + padding: 4px 0; + margin-bottom: 8px; +} +.system-info-refresh:hover { color: hsl(var(--foreground)); } +.system-info-refresh:disabled { cursor: wait; opacity: 0.6; } +.system-info-refresh:focus-visible { outline: 2px solid hsl(var(--primary)); outline-offset: 2px; } diff --git a/frontend/src/ui/SystemInfo.tsx b/frontend/src/ui/SystemInfo.tsx index b9b33550e..0369649ef 100644 --- a/frontend/src/ui/SystemInfo.tsx +++ b/frontend/src/ui/SystemInfo.tsx @@ -1,16 +1,15 @@ import { useEffect, useRef, useState } from "react"; -import { RefreshCw } from "lucide-react"; import { useTranslation } from "react-i18next"; import { getEnvironmentResources, getSystemInfo, listIdentityUserPools, - updateCodexSandboxToolModelEnv, - type CodexSandboxToolKind, + getSandboxImageUpdates, + updateSandboxTool, + type SandboxImageState, type IdentityUserPool, type EnvironmentResourcesResponse, type SandboxToolInfo, - type SandboxToolKind, type StudioRole, } from "../adk/client"; import type { CloudProvider } from "../adk/cloudProvider"; @@ -65,8 +64,14 @@ function isMissingLocalCredentials(cause: unknown): boolean { ); } -function isCodexSandboxToolKind(kind: SandboxToolKind): kind is CodexSandboxToolKind { - return kind === "codex" || kind === "codex_snapshot"; +function SandboxUpdateIcon({ spinning }: { spinning: boolean }) { + return ( + + ); } interface SandboxToolUpdateState { @@ -105,9 +110,23 @@ export function SystemInfo({ const [environmentResourcesReloadKey, setEnvironmentResourcesReloadKey] = useState(0); const mountedRef = useRef(false); const [sandboxToolUpdates, setSandboxToolUpdates] = useState< - Partial> + Record >({}); + const [imageStates, setImageStates] = useState>({}); + const [imageError, setImageError] = useState(""); + const [imageLoading, setImageLoading] = useState(true); + const pendingTools = useRef(new Set()); + const imageRequestVersion = useRef(0); + const scope = `${provider}:${region}`; + const scopeRef = useRef(scope); + scopeRef.current = scope; + + useEffect(() => { + setImageStates({}); + setSandboxToolUpdates({}); + }, [scope]); + useEffect(() => { mountedRef.current = true; return () => { @@ -116,7 +135,7 @@ export function SystemInfo({ }, []); function patchSandboxToolUpdate( - kind: CodexSandboxToolKind, + kind: string, patch: Partial, ) { setSandboxToolUpdates((current) => ({ @@ -130,44 +149,58 @@ export function SystemInfo({ } async function updateSandboxToolModelEnv(tool: SandboxToolInfo) { - if (!isCodexSandboxToolKind(tool.kind) || !tool.toolId) return; - const currentState = - sandboxToolUpdates[tool.kind] ?? defaultSandboxToolUpdateState(); - if (currentState.busy) return; - patchSandboxToolUpdate(tool.kind, { busy: true, error: "", message: "" }); + if (!tool.toolId || pendingTools.current.has(tool.toolId)) return; + const updateScope = scope; + pendingTools.current.add(tool.toolId); + imageRequestVersion.current += 1; + patchSandboxToolUpdate(tool.toolId, { busy: true, error: "", message: "" }); try { - const result = await updateCodexSandboxToolModelEnv(tool.kind); - if (!mountedRef.current) return; - setSandboxTools((current) => - current.map((item) => - item.kind === tool.kind - ? { - ...item, - needsModelEnvUpdate: false, - canUpdateModelEnv: false, - modelEnvError: "", - modelEnvErrorCode: "", - } - : item, - ), - ); - patchSandboxToolUpdate(tool.kind, { - busy: false, - error: "", - message: result.updated - ? t("systemInfo.modelEnvUpdated") - : t("systemInfo.modelEnvAlreadyCurrent"), + const result = await updateSandboxTool(tool.kind); + if (!mountedRef.current || scopeRef.current !== updateScope) return; + imageRequestVersion.current += 1; + setImageStates((current) => ({ ...current, [tool.toolId]: result.state })); + patchSandboxToolUpdate(tool.toolId, { + busy: false, error: "", + message: result.updated ? t("systemInfo.modelEnvUpdated") : t("systemInfo.modelEnvAlreadyCurrent"), }); } catch (cause) { - if (!mountedRef.current) return; - patchSandboxToolUpdate(tool.kind, { + if (!mountedRef.current || scopeRef.current !== updateScope) return; + imageRequestVersion.current += 1; + patchSandboxToolUpdate(tool.toolId, { busy: false, - error: t("systemInfo.modelEnvUpdateError"), + error: t("systemInfo.sandboxUpdateError"), message: "", }); + } finally { + pendingTools.current.delete(tool.toolId); } } + useEffect(() => { + if (!isAdmin) return; + const controller = new AbortController(); + const version = ++imageRequestVersion.current; + setImageError(""); + setImageLoading(true); + void getSandboxImageUpdates(controller.signal).then((states) => { + if (controller.signal.aborted || version !== imageRequestVersion.current) return; + setImageStates(Object.fromEntries(states.map((state) => [state.toolId, state]))); + }).catch(() => { + if (!controller.signal.aborted && version === imageRequestVersion.current) { + setImageError(t("systemInfo.versionCheckError")); + } + }).finally(() => { + if (!controller.signal.aborted) setImageLoading(false); + }); + return () => controller.abort(); + }, [isAdmin, provider, region, sandboxReloadKey]); + + useEffect(() => { + if (!Object.values(imageStates).some((state) => state.status === "Updating" || state.status === "Creating")) return; + const timer = window.setTimeout(() => setSandboxReloadKey((key) => key + 1), 5000); + return () => window.clearTimeout(timer); + }, [imageStates]); + useEffect(() => { if (!isAdmin) { setTosAddress(""); @@ -181,6 +214,7 @@ export function SystemInfo({ setSandboxError(""); void getSystemInfo(controller.signal) .then((systemInfo) => { + if (controller.signal.aborted) return; setTosAddress(systemInfo.storage.tosAddress); setSandboxTools(systemInfo.sandboxTools); }) @@ -192,7 +226,7 @@ export function SystemInfo({ if (!controller.signal.aborted) setSandboxLoading(false); }); return () => controller.abort(); - }, [isAdmin, sandboxReloadKey]); + }, [isAdmin, provider, region, sandboxReloadKey]); useEffect(() => { if (!isAdmin) { @@ -380,6 +414,10 @@ export function SystemInfo({ aria-labelledby="sandbox-tool-title" >

{t("systemInfo.sandboxInfo")}

+ {sandboxLoading ? (
) : (
+ {imageError ? {imageError} : null} {sandboxTools.map((tool) => { - const codexKind = isCodexSandboxToolKind(tool.kind) - ? tool.kind - : null; - const updateState = codexKind - ? sandboxToolUpdates[codexKind] - : undefined; - const updateVisible = - codexKind !== null && - Boolean(tool.toolId) && - tool.needsModelEnvUpdate && - tool.canUpdateModelEnv; - const inlineError = codexKind - ? updateState?.error || tool.modelEnvError - : ""; + const imageState = imageStates[tool.toolId]; + const updateState = sandboxToolUpdates[tool.toolId]; + const updateVisible = Boolean(tool.toolId) && Boolean(imageState?.canUpdate); + const inlineError = updateState?.error || (imageState?.error ? t("systemInfo.versionCheckError") : imageState?.modelEnvError ? t("systemInfo.modelEnvRepairUnavailable") : ""); return (
@@ -430,7 +459,7 @@ export function SystemInfo({ void updateSandboxToolModelEnv(tool)} > -