agentengine-sdk-python 0.6.4__py3-none-any.whl → 0.7.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- agentengine_sdk_python-0.7.0.dist-info/METADATA +181 -0
- agentengine_sdk_python-0.7.0.dist-info/RECORD +482 -0
- {agentengine_sdk_python-0.6.4.dist-info → agentengine_sdk_python-0.7.0.dist-info}/WHEEL +1 -1
- agentengine_sdk_python-0.7.0.dist-info/entry_points.txt +3 -0
- agentengine_sdk_python-0.7.0.dist-info/licenses/LICENSE +175 -0
- agentengine_sdk_python-0.7.0.dist-info/top_level.txt +2 -0
- ksadk/__init__.py +13 -0
- ksadk/__main__.py +4 -0
- ksadk/a2a/__init__.py +15 -0
- ksadk/a2a/card_builder.py +64 -0
- ksadk/a2a/client.py +309 -0
- ksadk/a2a/executor.py +201 -0
- ksadk/a2a/server.py +79 -0
- ksadk/agents/__init__.py +20 -0
- ksadk/agents/base.py +370 -0
- ksadk/agents/context.py +38 -0
- ksadk/agents/event.py +35 -0
- ksadk/agents/loop_agent.py +71 -0
- ksadk/agents/parallel_agent.py +109 -0
- ksadk/agents/runner_adapter.py +24 -0
- ksadk/agents/sequential_agent.py +34 -0
- ksadk/api/__init__.py +3 -0
- ksadk/api/client.py +2246 -0
- ksadk/builders/__init__.py +20 -0
- ksadk/builders/base.py +66 -0
- ksadk/builders/code_builder.py +1990 -0
- ksadk/builders/container_builder.py +786 -0
- ksadk/builders/framework_requirements.py +59 -0
- ksadk/builders/ks3_uploader.py +389 -0
- ksadk/builders/mcp_builder.py +476 -0
- ksadk/builders/requirements_utils.py +75 -0
- ksadk/cli/__init__.py +531 -0
- ksadk/cli/__main__.py +4 -0
- ksadk/cli/agent_ref.py +204 -0
- ksadk/cli/cmd_a2a.py +167 -0
- ksadk/cli/cmd_agent.py +201 -0
- ksadk/cli/cmd_build.py +270 -0
- ksadk/cli/cmd_completion.py +304 -0
- ksadk/cli/cmd_config.py +758 -0
- ksadk/cli/cmd_create.py +2304 -0
- ksadk/cli/cmd_dashboard.py +900 -0
- ksadk/cli/cmd_deploy.py +704 -0
- ksadk/cli/cmd_destroy.py +345 -0
- ksadk/cli/cmd_files.py +1227 -0
- ksadk/cli/cmd_hermes.py +1263 -0
- ksadk/cli/cmd_invoke.py +1503 -0
- ksadk/cli/cmd_launch.py +564 -0
- ksadk/cli/cmd_mcp.py +1179 -0
- ksadk/cli/cmd_model.py +302 -0
- ksadk/cli/cmd_openclaw.py +3922 -0
- ksadk/cli/cmd_run.py +239 -0
- ksadk/cli/cmd_status.py +588 -0
- ksadk/cli/cmd_version.py +522 -0
- ksadk/cli/cmd_web.py +288 -0
- ksadk/cli/deploy_utils.py +87 -0
- ksadk/cli/dry_run.py +214 -0
- ksadk/cli/env_options.py +82 -0
- ksadk/cli/error_utils.py +525 -0
- ksadk/cli/global_options.py +31 -0
- ksadk/cli/local_runtime.py +97 -0
- ksadk/cli/model_catalog.py +136 -0
- ksadk/cli/network_options.py +412 -0
- ksadk/cli/resource_common.py +621 -0
- ksadk/cli/storage.py +84 -0
- ksadk/cli/ui.py +431 -0
- ksadk/cli/workflow_common.py +552 -0
- ksadk/common/__init__.py +14 -0
- ksadk/common/aicp_env.py +128 -0
- ksadk/common/auth.py +190 -0
- ksadk/common/constants.py +84 -0
- ksadk/common/llm_utils.py +61 -0
- ksadk/configs/__init__.py +61 -0
- ksadk/configs/env_registry.py +343 -0
- ksadk/configs/global_config.py +200 -0
- ksadk/configs/settings.py +706 -0
- ksadk/conversations/__init__.py +95 -0
- ksadk/conversations/attachment_storage.py +379 -0
- ksadk/conversations/attachments.py +774 -0
- ksadk/conversations/compaction_pipeline.py +387 -0
- ksadk/conversations/compaction_prompt.py +128 -0
- ksadk/conversations/context.py +303 -0
- ksadk/conversations/message_projection.py +269 -0
- ksadk/conversations/model_context.py +370 -0
- ksadk/conversations/model_options.py +136 -0
- ksadk/conversations/normalize.py +446 -0
- ksadk/conversations/reasoning_markup.py +91 -0
- ksadk/conversations/run_kinds.py +61 -0
- ksadk/conversations/run_status.py +53 -0
- ksadk/conversations/runtime.py +5276 -0
- ksadk/conversations/semantic_summary.py +359 -0
- ksadk/conversations/session_title.py +254 -0
- ksadk/deployment/__init__.py +83 -0
- ksadk/deployment/agent_access.py +226 -0
- ksadk/deployment/base.py +229 -0
- ksadk/deployment/manager.py +332 -0
- ksadk/deployment/providers/__init__.py +10 -0
- ksadk/deployment/providers/serverless.py +1115 -0
- ksadk/deployment/registry.py +120 -0
- ksadk/deployment/state.py +78 -0
- ksadk/deployment/ui_config.py +21 -0
- ksadk/detection/__init__.py +7 -0
- ksadk/detection/detector.py +431 -0
- ksadk/detection/mcp_detector.py +250 -0
- ksadk/hermes_terminal.py +467 -0
- ksadk/identity/__init__.py +17 -0
- ksadk/identity/resolver.py +385 -0
- ksadk/knowledge_base/__init__.py +62 -0
- ksadk/knowledge_base/adk_tool.py +47 -0
- ksadk/knowledge_base/client.py +297 -0
- ksadk/knowledge_base/langchain_tool.py +55 -0
- ksadk/knowledge_base/service.py +66 -0
- ksadk/knowledge_base/tool.py +50 -0
- ksadk/markdown.py +169 -0
- ksadk/mcp_runtime/__init__.py +231 -0
- ksadk/memory/__init__.py +57 -0
- ksadk/memory/adk/__init__.py +51 -0
- ksadk/memory/adk/backends/__init__.py +5 -0
- ksadk/memory/adk/backends/base_ltm_backend.py +52 -0
- ksadk/memory/adk/backends/http_ltm_backend.py +183 -0
- ksadk/memory/adk/backends/inmemory_ltm_backend.py +96 -0
- ksadk/memory/adk/backends/sdk_ltm_backend.py +456 -0
- ksadk/memory/adk/long_term_memory.py +273 -0
- ksadk/memory/adk/resilient_session_service.py +268 -0
- ksadk/memory/adk/short_term_memory.py +304 -0
- ksadk/memory/adk_tool.py +31 -0
- ksadk/memory/backends/__init__.py +6 -0
- ksadk/memory/backends/base.py +103 -0
- ksadk/memory/backends/memory.py +112 -0
- ksadk/memory/backends/redis.py +146 -0
- ksadk/memory/langchain_tool.py +39 -0
- ksadk/memory/ltm_backend_factory.py +24 -0
- ksadk/memory/manager.py +215 -0
- ksadk/memory/service.py +172 -0
- ksadk/memory/tool.py +96 -0
- ksadk/model_policy.py +223 -0
- ksadk/openclaw_gateway.py +378 -0
- ksadk/runners/__init__.py +8 -0
- ksadk/runners/adk_runner.py +1994 -0
- ksadk/runners/base_runner.py +305 -0
- ksadk/runners/deepagents_runner.py +13 -0
- ksadk/runners/factory.py +73 -0
- ksadk/runners/langchain_runner.py +751 -0
- ksadk/runners/langgraph_runner.py +985 -0
- ksadk/runners/patch_langchain.py +187 -0
- ksadk/runners/remote_runner.py +792 -0
- ksadk/runners/usage_accumulator.py +38 -0
- ksadk/runners/utils/__init__.py +19 -0
- ksadk/runners/utils/langfuse.py +230 -0
- ksadk/runners/utils/loader.py +62 -0
- ksadk/runtime_context.py +166 -0
- ksadk/runtime_state.py +25 -0
- ksadk/sandbox/__init__.py +28 -0
- ksadk/sandbox/backends/__init__.py +9 -0
- ksadk/sandbox/backends/e2b.py +154 -0
- ksadk/sandbox/backends/local_process.py +131 -0
- ksadk/sandbox/base.py +101 -0
- ksadk/sandbox/factory.py +53 -0
- ksadk/sandbox/registry.py +187 -0
- ksadk/server/__init__.py +7 -0
- ksadk/server/api_models.py +73 -0
- ksadk/server/app.py +3931 -0
- ksadk/server/static/assets/ArtifactsPanel-D-SSn2ee.js +1 -0
- ksadk/server/static/assets/CodeBlock-P16VOLYi.js +9 -0
- ksadk/server/static/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
- ksadk/server/static/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
- ksadk/server/static/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
- ksadk/server/static/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
- ksadk/server/static/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
- ksadk/server/static/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
- ksadk/server/static/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
- ksadk/server/static/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
- ksadk/server/static/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
- ksadk/server/static/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
- ksadk/server/static/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
- ksadk/server/static/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
- ksadk/server/static/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
- ksadk/server/static/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
- ksadk/server/static/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
- ksadk/server/static/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
- ksadk/server/static/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
- ksadk/server/static/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
- ksadk/server/static/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
- ksadk/server/static/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
- ksadk/server/static/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
- ksadk/server/static/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
- ksadk/server/static/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
- ksadk/server/static/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
- ksadk/server/static/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
- ksadk/server/static/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
- ksadk/server/static/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
- ksadk/server/static/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
- ksadk/server/static/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
- ksadk/server/static/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
- ksadk/server/static/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
- ksadk/server/static/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
- ksadk/server/static/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
- ksadk/server/static/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
- ksadk/server/static/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
- ksadk/server/static/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
- ksadk/server/static/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
- ksadk/server/static/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
- ksadk/server/static/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
- ksadk/server/static/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
- ksadk/server/static/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
- ksadk/server/static/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
- ksadk/server/static/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
- ksadk/server/static/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
- ksadk/server/static/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
- ksadk/server/static/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
- ksadk/server/static/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
- ksadk/server/static/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
- ksadk/server/static/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
- ksadk/server/static/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
- ksadk/server/static/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
- ksadk/server/static/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
- ksadk/server/static/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
- ksadk/server/static/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
- ksadk/server/static/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
- ksadk/server/static/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
- ksadk/server/static/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
- ksadk/server/static/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
- ksadk/server/static/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
- ksadk/server/static/assets/MathMessageMarkdown-CdGiQWHR.js +8 -0
- ksadk/server/static/assets/MathMessageMarkdown-DeYh8QPr.css +1 -0
- ksadk/server/static/assets/MermaidBlock-IOcal8cm.js +303 -0
- ksadk/server/static/assets/NativeTerminalPanel-CHOd7Rch.css +1 -0
- ksadk/server/static/assets/NativeTerminalPanel-DGSDWsei.js +1 -0
- ksadk/server/static/assets/addon-fit-DthTIhi3.js +1 -0
- ksadk/server/static/assets/apl-CnwPGSsG.js +1 -0
- ksadk/server/static/assets/arc-BA13sHc9.js +1 -0
- ksadk/server/static/assets/architecture-7EHR7CIX-BvUlsA6l.js +1 -0
- ksadk/server/static/assets/architectureDiagram-3BPJPVTR-DzJzgVoL.js +36 -0
- ksadk/server/static/assets/asciiarmor-qTkVPQu6.js +1 -0
- ksadk/server/static/assets/asn1-Dr8qZg38.js +1 -0
- ksadk/server/static/assets/asterisk-Bja0s6e1.js +1 -0
- ksadk/server/static/assets/blockDiagram-GPEHLZMM-UTdVcZhn.js +132 -0
- ksadk/server/static/assets/brainfuck-D5EjA2JK.js +1 -0
- ksadk/server/static/assets/c4Diagram-AAUBKEIU-Dix0GD4m.js +10 -0
- ksadk/server/static/assets/channel-CTuydARX.js +1 -0
- ksadk/server/static/assets/chunk-2J33WTMH-C2JxuZ42.js +1 -0
- ksadk/server/static/assets/chunk-4BX2VUAB-Be57QPgD.js +1 -0
- ksadk/server/static/assets/chunk-55IACEB6-WjSuy4nv.js +1 -0
- ksadk/server/static/assets/chunk-727SXJPM-BOovP-b5.js +206 -0
- ksadk/server/static/assets/chunk-AQP2D5EJ-DOmZNmWj.js +231 -0
- ksadk/server/static/assets/chunk-FMBD7UC4-DzwlodkU.js +15 -0
- ksadk/server/static/assets/chunk-ND2GUHAM-DMqAikEU.js +1 -0
- ksadk/server/static/assets/chunk-QZHKN3VN-BnCydivS.js +1 -0
- ksadk/server/static/assets/classDiagram-4FO5ZUOK-BNHg45Be.js +1 -0
- ksadk/server/static/assets/classDiagram-v2-Q7XG4LA2-BNHg45Be.js +1 -0
- ksadk/server/static/assets/clike-DHH8Ad3s.js +1 -0
- ksadk/server/static/assets/clojure-BflJGmDX.js +1 -0
- ksadk/server/static/assets/cmake-CyVbuzPu.js +1 -0
- ksadk/server/static/assets/cobol-B_OZ4V-R.js +1 -0
- ksadk/server/static/assets/coffeescript-B2pKbFk8.js +1 -0
- ksadk/server/static/assets/commonlisp-CcllspGY.js +1 -0
- ksadk/server/static/assets/cose-bilkent-S5V4N54A-Zhy2GBnP.js +1 -0
- ksadk/server/static/assets/crystal-M5-qzICs.js +1 -0
- ksadk/server/static/assets/css-CYpP4FRV.js +1 -0
- ksadk/server/static/assets/cypher-p9eYesGn.js +1 -0
- ksadk/server/static/assets/cytoscape.esm-DelgaX4f.js +321 -0
- ksadk/server/static/assets/d-BhMjBjQP.js +1 -0
- ksadk/server/static/assets/dagre-BM42HDAG-CbKrtIUz.js +4 -0
- ksadk/server/static/assets/dagre-Bx709z4p.js +1 -0
- ksadk/server/static/assets/defaultLocale-C8Fc0cco.js +1 -0
- ksadk/server/static/assets/diagram-2AECGRRQ-BRApgQRK.js +43 -0
- ksadk/server/static/assets/diagram-5GNKFQAL-D0xbnDiK.js +10 -0
- ksadk/server/static/assets/diagram-KO2AKTUF-CVugVI9L.js +3 -0
- ksadk/server/static/assets/diagram-LMA3HP47-DozUGgee.js +24 -0
- ksadk/server/static/assets/diagram-OG6HWLK6-ZOYsKlru.js +24 -0
- ksadk/server/static/assets/diff-ChtP43wD.js +1 -0
- ksadk/server/static/assets/dist-2hInLS_J.js +9 -0
- ksadk/server/static/assets/dist-8EEyGgDx.js +1 -0
- ksadk/server/static/assets/dist-8_SfK1IP.js +1 -0
- ksadk/server/static/assets/dist-B0NP_Z0z.js +23 -0
- ksadk/server/static/assets/dist-BIJVDMuB.js +1 -0
- ksadk/server/static/assets/dist-BmlMPsCc.js +2 -0
- ksadk/server/static/assets/dist-BrCcQ47T.js +1 -0
- ksadk/server/static/assets/dist-BssVmjIo.js +6 -0
- ksadk/server/static/assets/dist-C3HIIaN5.js +1 -0
- ksadk/server/static/assets/dist-CPBtqRTW.js +1 -0
- ksadk/server/static/assets/dist-CkEKJFtk.js +1 -0
- ksadk/server/static/assets/dist-D-JMdWPQ.js +1 -0
- ksadk/server/static/assets/dist-D-xOD048.js +1 -0
- ksadk/server/static/assets/dist-D57wnGWd.js +1 -0
- ksadk/server/static/assets/dist-DL_NKc_z.js +1 -0
- ksadk/server/static/assets/dist-DMezvBGJ.js +1 -0
- ksadk/server/static/assets/dist-DmFaq_Iy.js +1 -0
- ksadk/server/static/assets/dist-Dp0k7bWI.js +1 -0
- ksadk/server/static/assets/dist-DpKtt4Bo.js +1 -0
- ksadk/server/static/assets/dist-DyOkZmze.js +11 -0
- ksadk/server/static/assets/dist-DyWsb78Q.js +1 -0
- ksadk/server/static/assets/dist-gX6GOLKq.js +1 -0
- ksadk/server/static/assets/dockerfile-BLkNEvjs.js +1 -0
- ksadk/server/static/assets/dtd-DTF72YNO.js +1 -0
- ksadk/server/static/assets/dylan-C6jEEEk-.js +1 -0
- ksadk/server/static/assets/ebnf-D4c0_ac3.js +1 -0
- ksadk/server/static/assets/ecl-BSZaIHnp.js +1 -0
- ksadk/server/static/assets/eiffel-BEjRio4Q.js +1 -0
- ksadk/server/static/assets/elm-Cshzl8qu.js +1 -0
- ksadk/server/static/assets/erDiagram-TEJ5UH35-Bk-9bgE5.js +85 -0
- ksadk/server/static/assets/erlang-DaB2Rkuy.js +1 -0
- ksadk/server/static/assets/eventmodeling-FCH6USID-DsAgWnlf.js +1 -0
- ksadk/server/static/assets/factor-D7LVTn2l.js +1 -0
- ksadk/server/static/assets/fcl-ClOhbFR7.js +1 -0
- ksadk/server/static/assets/flowDiagram-I6XJVG4X-Dn9kZVuZ.js +162 -0
- ksadk/server/static/assets/forth-Cezjo90N.js +1 -0
- ksadk/server/static/assets/fortran-Bt6PBEDR.js +1 -0
- ksadk/server/static/assets/ganttDiagram-6RSMTGT7-BGxXlJqm.js +292 -0
- ksadk/server/static/assets/gas-BHEdbvp9.js +1 -0
- ksadk/server/static/assets/gherkin-oBAE_ms0.js +1 -0
- ksadk/server/static/assets/gitGraph-WXDBUCRP-BIao9Yjg.js +1 -0
- ksadk/server/static/assets/gitGraphDiagram-PVQCEYII-C7Q0X-wi.js +106 -0
- ksadk/server/static/assets/graphlib-B8gBHxth.js +1 -0
- ksadk/server/static/assets/groovy-BC3IOsC8.js +1 -0
- ksadk/server/static/assets/haskell-BRsoo5mP.js +1 -0
- ksadk/server/static/assets/haxe-DIz0ZZqd.js +1 -0
- ksadk/server/static/assets/http-CLVgA2GD.js +1 -0
- ksadk/server/static/assets/idl-BUZw3wgd.js +1 -0
- ksadk/server/static/assets/index-C00Lwg3L.js +165 -0
- ksadk/server/static/assets/index-DAURbCsP.css +1 -0
- ksadk/server/static/assets/info-J43DQDTF-CyipqGxU.js +1 -0
- ksadk/server/static/assets/infoDiagram-5YYISTIA-BO_CSqq5.js +2 -0
- ksadk/server/static/assets/init-D6jRqBbL.js +1 -0
- ksadk/server/static/assets/ishikawaDiagram-YF4QCWOH-eYLPLd46.js +70 -0
- ksadk/server/static/assets/javascript-9Tg8ixDm.js +1 -0
- ksadk/server/static/assets/journeyDiagram-JHISSGLW-De9geSb0.js +139 -0
- ksadk/server/static/assets/julia-CQfgDRfP.js +1 -0
- ksadk/server/static/assets/kanban-definition-UN3LZRKU-Cs80kQto.js +89 -0
- ksadk/server/static/assets/katex-DSrCFD8n.js +265 -0
- ksadk/server/static/assets/linear-CN9sbKTF.js +1 -0
- ksadk/server/static/assets/livescript-BXxG0Yva.js +1 -0
- ksadk/server/static/assets/lua-CiA1ziua.js +1 -0
- ksadk/server/static/assets/mathematica-u8YMmbU0.js +1 -0
- ksadk/server/static/assets/mbox-BEMVcqjs.js +1 -0
- ksadk/server/static/assets/mermaid-parser.core-DlgSBU8l.js +161 -0
- ksadk/server/static/assets/mindmap-definition-RKZ34NQL-9afz7Oug.js +96 -0
- ksadk/server/static/assets/mirc-C9zpzAZU.js +1 -0
- ksadk/server/static/assets/mllike-B45xgp2S.js +1 -0
- ksadk/server/static/assets/modelica-CqHI_q-D.js +1 -0
- ksadk/server/static/assets/mscgen-qgSLujhx.js +1 -0
- ksadk/server/static/assets/mumps-CW_TBmxz.js +1 -0
- ksadk/server/static/assets/nginx-BMaDbqW3.js +1 -0
- ksadk/server/static/assets/nsis-AQ_alPln.js +1 -0
- ksadk/server/static/assets/ntriples-d28e9m0E.js +1 -0
- ksadk/server/static/assets/octave-D2Q8cUp1.js +1 -0
- ksadk/server/static/assets/ordinal-hYBb2elL.js +1 -0
- ksadk/server/static/assets/oz-BwTct_5T.js +1 -0
- ksadk/server/static/assets/packet-YPE3B663-CGkyRxlg.js +1 -0
- ksadk/server/static/assets/pascal-Dp_KdFgX.js +1 -0
- ksadk/server/static/assets/perl-CdEHsPfU.js +1 -0
- ksadk/server/static/assets/pie-LRSECV5Y-M0uWAf4n.js +1 -0
- ksadk/server/static/assets/pieDiagram-4H26LBE5-DPv_VstN.js +30 -0
- ksadk/server/static/assets/pig-AF4Hwhju.js +1 -0
- ksadk/server/static/assets/powershell-CzG71I4L.js +1 -0
- ksadk/server/static/assets/properties-C357ku5U.js +1 -0
- ksadk/server/static/assets/protobuf-D1ij_kNL.js +1 -0
- ksadk/server/static/assets/pug-Bugr-47h.js +1 -0
- ksadk/server/static/assets/puppet-B4hC2X4m.js +1 -0
- ksadk/server/static/assets/python-BqYw9LYJ.js +1 -0
- ksadk/server/static/assets/q-B9NhS7L_.js +1 -0
- ksadk/server/static/assets/quadrantDiagram-W4KKPZXB-o77wZ1XK.js +7 -0
- ksadk/server/static/assets/r-DADKenSm.js +1 -0
- ksadk/server/static/assets/radar-GUYGQ44K-Bdbm24-x.js +1 -0
- ksadk/server/static/assets/requirementDiagram-4Y6WPE33-BYVw81VC.js +84 -0
- ksadk/server/static/assets/rpm-gpDK5sbt.js +1 -0
- ksadk/server/static/assets/ruby-DsYpTuWg.js +1 -0
- ksadk/server/static/assets/sankeyDiagram-5OEKKPKP-B7nZFMKT.js +40 -0
- ksadk/server/static/assets/sas-C2OyD2Y6.js +1 -0
- ksadk/server/static/assets/scheme-CyDdOh7e.js +1 -0
- ksadk/server/static/assets/sequenceDiagram-3UESZ5HK-BGRRrFw5.js +162 -0
- ksadk/server/static/assets/shell-Du5Qg9im.js +1 -0
- ksadk/server/static/assets/sieve-BAxa3IjF.js +1 -0
- ksadk/server/static/assets/simple-mode-DRpGK0lJ.js +1 -0
- ksadk/server/static/assets/smalltalk-BLp5-jwC.js +1 -0
- ksadk/server/static/assets/solr-BBopId9w.js +1 -0
- ksadk/server/static/assets/sparql-Dz-mlCAC.js +1 -0
- ksadk/server/static/assets/spreadsheet-CTIncYxj.js +1 -0
- ksadk/server/static/assets/sql-DcCJ9jop.js +1 -0
- ksadk/server/static/assets/stateDiagram-AJRCARHV-BUfHCoPs.js +1 -0
- ksadk/server/static/assets/stateDiagram-v2-BHNVJYJU-8xBN_gLM.js +1 -0
- ksadk/server/static/assets/stex-sDrdk2BW.js +1 -0
- ksadk/server/static/assets/stylus-DLLZ8dgm.js +1 -0
- ksadk/server/static/assets/swift-DKB6_1j6.js +1 -0
- ksadk/server/static/assets/tcl-BGfruO1u.js +1 -0
- ksadk/server/static/assets/textile-DWBqthVk.js +1 -0
- ksadk/server/static/assets/tiddlywiki-VlUiK86J.js +1 -0
- ksadk/server/static/assets/tiki-CxVlf5KZ.js +1 -0
- ksadk/server/static/assets/timeline-definition-PNZ67QCA-FbCTGrf9.js +120 -0
- ksadk/server/static/assets/toml-LvoKBwCs.js +1 -0
- ksadk/server/static/assets/treeView-BLDUP644-npMo0ak1.js +1 -0
- ksadk/server/static/assets/treemap-LRROVOQU-CHdzrrdo.js +1 -0
- ksadk/server/static/assets/troff-C2dAgh7P.js +1 -0
- ksadk/server/static/assets/ttcn-BePYh-hG.js +1 -0
- ksadk/server/static/assets/ttcn-cfg-CY0aot7Q.js +1 -0
- ksadk/server/static/assets/turtle-BDLWE2cD.js +1 -0
- ksadk/server/static/assets/vb-BthdM_4N.js +1 -0
- ksadk/server/static/assets/vbscript-D9f6rSsU.js +1 -0
- ksadk/server/static/assets/velocity-CqftRsjg.js +1 -0
- ksadk/server/static/assets/vennDiagram-CIIHVFJN-DWgwtPsJ.js +34 -0
- ksadk/server/static/assets/verilog-D-x6ne0Z.js +1 -0
- ksadk/server/static/assets/vhdl-CMrOq2q1.js +1 -0
- ksadk/server/static/assets/wardley-L42UT6IY-BEQz7wyy.js +1 -0
- ksadk/server/static/assets/wardleyDiagram-YWT4CUSO-2x8G-tTM.js +78 -0
- ksadk/server/static/assets/webidl-D5jiJvOU.js +1 -0
- ksadk/server/static/assets/xquery-fj_R-kMw.js +1 -0
- ksadk/server/static/assets/xterm-DooSxjI5.js +36 -0
- ksadk/server/static/assets/xychartDiagram-2RQKCTM6-Drma3AYl.js +7 -0
- ksadk/server/static/assets/yacas-CXXgtw0p.js +1 -0
- ksadk/server/static/assets/z80-CL1naaMV.js +1 -0
- ksadk/server/static/favicon.svg +1 -0
- ksadk/server/static/icons.svg +24 -0
- ksadk/server/static/index.html +14 -0
- ksadk/server/terminal_sessions.py +551 -0
- ksadk/sessions/__init__.py +279 -0
- ksadk/sessions/base.py +354 -0
- ksadk/sessions/continuity.py +337 -0
- ksadk/sessions/errors.py +7 -0
- ksadk/sessions/in_memory.py +273 -0
- ksadk/sessions/local_service.py +949 -0
- ksadk/sessions/postgres_service.py +901 -0
- ksadk/sessions/resilience.py +52 -0
- ksadk/sessions/resilient.py +360 -0
- ksadk/sessions/sqlite_service.py +13 -0
- ksadk/skills/__init__.py +9 -0
- ksadk/skills/loader.py +46 -0
- ksadk/skills/models.py +120 -0
- ksadk/skills/package_store.py +104 -0
- ksadk/skills/runtime/__init__.py +17 -0
- ksadk/skills/runtime/agent.py +183 -0
- ksadk/skills/runtime/artifacts.py +39 -0
- ksadk/skills/runtime/backends/__init__.py +1 -0
- ksadk/skills/runtime/backends/disabled.py +8 -0
- ksadk/skills/runtime/backends/e2b.py +168 -0
- ksadk/skills/runtime/backends/local.py +95 -0
- ksadk/skills/runtime/base.py +103 -0
- ksadk/skills/runtime/executor.py +219 -0
- ksadk/skills/runtime/factory.py +37 -0
- ksadk/skills/runtime/loader.py +131 -0
- ksadk/skills/runtime/registry.py +150 -0
- ksadk/skills/runtime/request.py +67 -0
- ksadk/skills/service_client.py +239 -0
- ksadk/skills/service_env.py +58 -0
- ksadk/skills/tool_defs.py +221 -0
- ksadk/terminal_client.py +69 -0
- ksadk/terminal_exec_policy.py +229 -0
- ksadk/tools/__init__.py +17 -0
- ksadk/tools/gateway.py +230 -0
- ksadk/tools/result_budget.py +154 -0
- ksadk/toolsets/__init__.py +854 -0
- ksadk/toolsets/_langchain.py +11 -0
- ksadk/toolsets/platform.py +93 -0
- ksadk/toolsets/sandbox.py +310 -0
- ksadk/toolsets/skills.py +217 -0
- ksadk/toolsets/web.py +266 -0
- ksadk/toolsets/workspace.py +917 -0
- ksadk/toolsets/workspace_state.py +44 -0
- ksadk/tracing/__init__.py +37 -0
- ksadk/tracing/exporters/__init__.py +1 -0
- ksadk/tracing/exporters/inmemory_exporter.py +116 -0
- ksadk/tracing/exporters/langfuse_exporter.py +445 -0
- ksadk/tracing/setup.py +892 -0
- ksadk/tracing/span_utils.py +114 -0
- ksadk/tui/__init__.py +27 -0
- ksadk/tui/loop.py +1877 -0
- ksadk/tui/stream_render.py +68 -0
- ksadk/ui_config.py +168 -0
- ksadk/version.py +4 -0
- ksadk_runtime_common/__init__.py +20 -0
- ksadk_runtime_common/memory_backend/__init__.py +29 -0
- ksadk_runtime_common/memory_backend/manifest.py +79 -0
- ksadk_runtime_common/memory_backend/providers/__init__.py +6 -0
- ksadk_runtime_common/memory_backend/providers/lancedb.py +151 -0
- ksadk_runtime_common/memory_backend/providers/mem0.py +74 -0
- ksadk_runtime_common/memory_backend/registry.py +56 -0
- ksadk_runtime_common/memory_backend/render.py +71 -0
- ksadk_runtime_common/schemas/memory_backend_manifest.schema.json +148 -0
- ksadk_runtime_common/workspace_files/__init__.py +29 -0
- ksadk_runtime_common/workspace_files/bootstrap.py +56 -0
- ksadk_runtime_common/workspace_files/constants.py +9 -0
- ksadk_runtime_common/workspace_files/path_utils.py +71 -0
- ksadk_runtime_common/workspace_files/preview.py +96 -0
- ksadk_runtime_common/workspace_files/router.py +254 -0
- agentengine_sdk_python/__init__.py +0 -3
- agentengine_sdk_python-0.6.4.dist-info/METADATA +0 -48
- agentengine_sdk_python-0.6.4.dist-info/RECORD +0 -5
- agentengine_sdk_python-0.6.4.dist-info/top_level.txt +0 -1
|
@@ -0,0 +1,3922 @@
|
|
|
1
|
+
"""
|
|
2
|
+
agentengine openclaw - OpenClaw 资源管理
|
|
3
|
+
|
|
4
|
+
设计目标:
|
|
5
|
+
- 和 Agent 部署完全一致,复用 CreateAgentProduct 接口 (Container 模式)
|
|
6
|
+
- Framework 标记为 "openclaw",区分于普通 Agent
|
|
7
|
+
- 预构建公共镜像,用户无需自行构建
|
|
8
|
+
- 模型配置通过 EnvironmentVariables 传递,自动复用 OPENAI_* 变量
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import copy
|
|
14
|
+
import io
|
|
15
|
+
import os
|
|
16
|
+
import asyncio
|
|
17
|
+
import json
|
|
18
|
+
import re
|
|
19
|
+
import secrets
|
|
20
|
+
import shutil
|
|
21
|
+
import subprocess
|
|
22
|
+
import sys
|
|
23
|
+
import tempfile
|
|
24
|
+
import textwrap
|
|
25
|
+
import time
|
|
26
|
+
import webbrowser
|
|
27
|
+
from datetime import datetime, timedelta, timezone
|
|
28
|
+
from pathlib import Path
|
|
29
|
+
from typing import Optional, Dict, Any
|
|
30
|
+
|
|
31
|
+
import click
|
|
32
|
+
from click.core import ParameterSource
|
|
33
|
+
from rich.measure import Measurement
|
|
34
|
+
from rich.table import Table as RichTable
|
|
35
|
+
|
|
36
|
+
from ksadk.api.client import DryRunExit
|
|
37
|
+
from ksadk.cli.agent_ref import resolve_openclaw_ref
|
|
38
|
+
from ksadk.cli.dry_run import dry_run_option, run_async_with_dry_run, effective_dry_run
|
|
39
|
+
from ksadk.cli.error_utils import abort_with_cli_error, remote_error, resolution_error
|
|
40
|
+
from ksadk.cli.network_options import build_network_payload, network_cli_kwargs, network_options
|
|
41
|
+
from ksadk.cli.storage import build_storage_config
|
|
42
|
+
from ksadk.cli.resource_common import ResourceActionDescriptor
|
|
43
|
+
from ksadk.cli.resource_common import (
|
|
44
|
+
CONTEXT_SETTINGS,
|
|
45
|
+
ResourceActionSet,
|
|
46
|
+
ResourceDescriptor,
|
|
47
|
+
ResourceListSchema,
|
|
48
|
+
ResourceStatusSchema,
|
|
49
|
+
build_dry_run_envelope,
|
|
50
|
+
build_resource_group_help,
|
|
51
|
+
confirm_destructive,
|
|
52
|
+
confirm_options,
|
|
53
|
+
pagination_options,
|
|
54
|
+
print_next_action_hint,
|
|
55
|
+
render_descriptor_list,
|
|
56
|
+
render_descriptor_status,
|
|
57
|
+
region_option,
|
|
58
|
+
)
|
|
59
|
+
from ksadk.cli.ui import (
|
|
60
|
+
emit_json,
|
|
61
|
+
get_console,
|
|
62
|
+
is_json_output,
|
|
63
|
+
is_stdout_tty,
|
|
64
|
+
json_dumps,
|
|
65
|
+
output_option as cli_output_option,
|
|
66
|
+
print_info,
|
|
67
|
+
print_kv,
|
|
68
|
+
print_rule,
|
|
69
|
+
print_success,
|
|
70
|
+
print_title,
|
|
71
|
+
print_warn,
|
|
72
|
+
status_rich_style,
|
|
73
|
+
)
|
|
74
|
+
from ksadk.deployment.agent_access import get_latest_agent_access
|
|
75
|
+
from ksadk.cli.model_catalog import fetch_provider_model_catalog, find_model_in_catalog
|
|
76
|
+
from ksadk.conversations.model_context import normalize_model_metadata
|
|
77
|
+
from ksadk.model_policy import build_runtime_model_policy_env
|
|
78
|
+
from ksadk.builders.container_builder import (
|
|
79
|
+
registry_kind_label,
|
|
80
|
+
resolve_registry_credentials,
|
|
81
|
+
)
|
|
82
|
+
from ksadk.openclaw_gateway import OpenClawGatewayClient, OpenClawGatewayError, OpenClawGatewayRequestError
|
|
83
|
+
from ksadk.terminal_exec_policy import OPENCLAW_TERMINAL_EXEC_POLICY
|
|
84
|
+
from ksadk.terminal_client import run_terminal_session
|
|
85
|
+
|
|
86
|
+
console = get_console()
|
|
87
|
+
# 默认 OpenClaw 镜像。
|
|
88
|
+
DEFAULT_OPENCLAW_NAMESPACE = "agentengine-public"
|
|
89
|
+
DEFAULT_OPENCLAW_REPO = "openclaw"
|
|
90
|
+
DEFAULT_OPENCLAW_VERSION = "2026.6.1"
|
|
91
|
+
DEFAULT_OPENCLAW_REGISTRY = "ghcr.io/kingsoftcloud"
|
|
92
|
+
DEFAULT_OPENCLAW_NAME = "openclaw-gateway"
|
|
93
|
+
DEFAULT_TRUSTED_PROXY_USER_HEADER = "x-forwarded-user"
|
|
94
|
+
DEFAULT_TRUSTED_PROXY_CIDRS = [
|
|
95
|
+
"127.0.0.1",
|
|
96
|
+
"::1",
|
|
97
|
+
"10.0.0.0/8",
|
|
98
|
+
"172.16.0.0/12",
|
|
99
|
+
"192.168.0.0/16",
|
|
100
|
+
"35.0.0.0/8",
|
|
101
|
+
]
|
|
102
|
+
_GLOBAL_ENV_CACHE: Optional[Dict[str, str]] = None
|
|
103
|
+
OPENCLAW_SECURITY_PROFILES = ("relaxed", "strict", "strictest")
|
|
104
|
+
OPENCLAW_CHANNELS = ("weixin", "feishu", "wps-xiezuo")
|
|
105
|
+
OPENCLAW_ENV_KEY_PATTERN = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*$")
|
|
106
|
+
WEIXIN_PLUGIN_ID = "openclaw-weixin"
|
|
107
|
+
FEISHU_PLUGIN_ID = "openclaw-lark"
|
|
108
|
+
FEISHU_CHANNEL_KEY = "feishu"
|
|
109
|
+
WPS_XIEZUO_PLUGIN_ID = "wps-xiezuo"
|
|
110
|
+
WPS_XIEZUO_CHANNEL_KEY = "wps-xiezuo"
|
|
111
|
+
WPS_XIEZUO_DEFAULT_ACCOUNT_ID = "default"
|
|
112
|
+
WEIXIN_REMOTE_LOGIN_ARGV = ["openclaw", "channels", "login", "--channel", WEIXIN_PLUGIN_ID]
|
|
113
|
+
OPENCLAW_CHANNEL_SPECS = {
|
|
114
|
+
"weixin": {
|
|
115
|
+
"plugin_id": WEIXIN_PLUGIN_ID,
|
|
116
|
+
"channel_key": WEIXIN_PLUGIN_ID,
|
|
117
|
+
"default_account_id": "default",
|
|
118
|
+
},
|
|
119
|
+
"feishu": {
|
|
120
|
+
"plugin_id": FEISHU_PLUGIN_ID,
|
|
121
|
+
"channel_key": FEISHU_CHANNEL_KEY,
|
|
122
|
+
"default_account_id": "default",
|
|
123
|
+
},
|
|
124
|
+
"wps-xiezuo": {
|
|
125
|
+
"plugin_id": WPS_XIEZUO_PLUGIN_ID,
|
|
126
|
+
"channel_key": WPS_XIEZUO_CHANNEL_KEY,
|
|
127
|
+
"default_account_id": WPS_XIEZUO_DEFAULT_ACCOUNT_ID,
|
|
128
|
+
},
|
|
129
|
+
}
|
|
130
|
+
OPENCLAW_CHANNEL_CONNECT_HELP = """连接指定 channel。
|
|
131
|
+
|
|
132
|
+
\b
|
|
133
|
+
不同 channel 的接入方式不同:
|
|
134
|
+
微信:扫码登录。
|
|
135
|
+
agentengine openclaw channel connect <id> --channel weixin
|
|
136
|
+
飞书:启动官方 onboarding 流程。
|
|
137
|
+
agentengine openclaw channel connect <id> --channel feishu
|
|
138
|
+
WPS 协作:写入开放平台 appId/appSecret 并启动长连接。
|
|
139
|
+
agentengine openclaw channel connect <id> --channel wps-xiezuo --app-id <appId> --app-secret <appSecret>
|
|
140
|
+
|
|
141
|
+
\b
|
|
142
|
+
WPS 协作说明:
|
|
143
|
+
--app-id / --app-secret 是让 channel 可连接可用的必需凭证。
|
|
144
|
+
--dm-policy=open 表示允许所有用户私聊;pairing 表示未知用户需要配对审批。
|
|
145
|
+
--account-id 当前仅支持 default。
|
|
146
|
+
"""
|
|
147
|
+
OPENCLAW_GATEWAY_READY_STATUSES = {"RUNNING", "READY", "HEALTHY"}
|
|
148
|
+
OPENCLAW_GATEWAY_BLOCKED_STATUSES = {
|
|
149
|
+
"DELETED",
|
|
150
|
+
"DELETING",
|
|
151
|
+
"ERROR",
|
|
152
|
+
"FAILED",
|
|
153
|
+
"STOPPED",
|
|
154
|
+
"STOPPING",
|
|
155
|
+
"TERMINATED",
|
|
156
|
+
"TERMINATING",
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
OPENCLAW_RESOURCE = ResourceDescriptor(
|
|
160
|
+
name="OpenClaw",
|
|
161
|
+
summary="OpenClaw 资源管理。",
|
|
162
|
+
resource_key="openclaw",
|
|
163
|
+
actions=(
|
|
164
|
+
ResourceActionDescriptor(
|
|
165
|
+
name="deploy",
|
|
166
|
+
canonical_command="agentengine openclaw deploy",
|
|
167
|
+
help_text="部署 OpenClaw 到云端",
|
|
168
|
+
kind="write",
|
|
169
|
+
supports_output=True,
|
|
170
|
+
supports_dry_run=True,
|
|
171
|
+
),
|
|
172
|
+
ResourceActionDescriptor(
|
|
173
|
+
name="list",
|
|
174
|
+
canonical_command="agentengine openclaw list",
|
|
175
|
+
help_text="列出已部署的 OpenClaw",
|
|
176
|
+
),
|
|
177
|
+
ResourceActionDescriptor(
|
|
178
|
+
name="status",
|
|
179
|
+
canonical_command="agentengine openclaw status [openclaw_ref]",
|
|
180
|
+
help_text="查看单个 OpenClaw 状态",
|
|
181
|
+
),
|
|
182
|
+
ResourceActionDescriptor(
|
|
183
|
+
name="gateway",
|
|
184
|
+
canonical_command="agentengine openclaw gateway",
|
|
185
|
+
help_text="Gateway 入口、日志与诊断",
|
|
186
|
+
kind="interactive",
|
|
187
|
+
),
|
|
188
|
+
ResourceActionDescriptor(
|
|
189
|
+
name="tui",
|
|
190
|
+
canonical_command="agentengine openclaw tui [openclaw_ref]",
|
|
191
|
+
help_text="连接远端 OpenClaw 原生 TUI",
|
|
192
|
+
kind="interactive",
|
|
193
|
+
supports_output=True,
|
|
194
|
+
supports_dry_run=True,
|
|
195
|
+
),
|
|
196
|
+
ResourceActionDescriptor(
|
|
197
|
+
name="repair",
|
|
198
|
+
canonical_command="agentengine openclaw repair [openclaw_ref]",
|
|
199
|
+
help_text="通过控制面执行 OpenClaw 修复动作",
|
|
200
|
+
kind="write",
|
|
201
|
+
supports_output=True,
|
|
202
|
+
),
|
|
203
|
+
ResourceActionDescriptor(
|
|
204
|
+
name="channel",
|
|
205
|
+
canonical_command="agentengine openclaw channel",
|
|
206
|
+
help_text="Channel 统一入口",
|
|
207
|
+
kind="interactive",
|
|
208
|
+
),
|
|
209
|
+
ResourceActionDescriptor(
|
|
210
|
+
name="delete",
|
|
211
|
+
canonical_command="agentengine openclaw delete [openclaw_ref...]",
|
|
212
|
+
help_text="删除一个或多个 OpenClaw",
|
|
213
|
+
kind="destructive",
|
|
214
|
+
supports_output=True,
|
|
215
|
+
supports_dry_run=True,
|
|
216
|
+
supports_yes=True,
|
|
217
|
+
),
|
|
218
|
+
),
|
|
219
|
+
list_schema=ResourceListSchema(
|
|
220
|
+
title="OpenClaw 列表",
|
|
221
|
+
noun="OpenClaw",
|
|
222
|
+
columns=(
|
|
223
|
+
{"header": "ID", "key": "id", "style": "#58a6ff", "no_wrap": True},
|
|
224
|
+
{"header": "名称", "key": "name", "style": "white"},
|
|
225
|
+
{"header": "状态", "key": "status", "no_wrap": True, "justify": "center"},
|
|
226
|
+
{"header": "Endpoint", "key": "endpoint", "style": "#8b949e", "overflow": "ellipsis"},
|
|
227
|
+
{"header": "区域", "key": "region", "style": "#8b949e"},
|
|
228
|
+
),
|
|
229
|
+
empty_message="没有找到已部署的 OpenClaw",
|
|
230
|
+
),
|
|
231
|
+
status_schema=ResourceStatusSchema(
|
|
232
|
+
title="OpenClaw 状态",
|
|
233
|
+
next_steps=(
|
|
234
|
+
"agentengine invoke <id>",
|
|
235
|
+
"agentengine openclaw tui <id>",
|
|
236
|
+
"agentengine dashboard open <id> --path /chat",
|
|
237
|
+
),
|
|
238
|
+
),
|
|
239
|
+
examples=(
|
|
240
|
+
"agentengine openclaw deploy",
|
|
241
|
+
"agentengine openclaw list",
|
|
242
|
+
"agentengine openclaw status <id>",
|
|
243
|
+
"agentengine openclaw tui <id>",
|
|
244
|
+
"agentengine openclaw gateway open <id>",
|
|
245
|
+
"agentengine openclaw repair <id>",
|
|
246
|
+
"agentengine openclaw channel status <id> --probe",
|
|
247
|
+
"agentengine openclaw channel connect <id> --channel weixin",
|
|
248
|
+
"agentengine openclaw delete <id>",
|
|
249
|
+
),
|
|
250
|
+
missing_ref_message="请指定 OpenClaw ID/名称,或在 OpenClaw 项目目录下运行",
|
|
251
|
+
resolution_commands=("agentengine openclaw list",),
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def _option_was_explicit(ctx: click.Context | None, name: str) -> bool:
|
|
256
|
+
if ctx is None:
|
|
257
|
+
return False
|
|
258
|
+
try:
|
|
259
|
+
return ctx.get_parameter_source(name) != ParameterSource.DEFAULT
|
|
260
|
+
except Exception:
|
|
261
|
+
return False
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def _build_openclaw_update_payload(
|
|
265
|
+
*,
|
|
266
|
+
image_ref: str,
|
|
267
|
+
cpu: str,
|
|
268
|
+
memory: str,
|
|
269
|
+
env_list: list[dict[str, Any]],
|
|
270
|
+
memory_config: dict[str, Any] | None,
|
|
271
|
+
image_credential: dict[str, Any] | None,
|
|
272
|
+
storage_config: dict[str, Any] | None,
|
|
273
|
+
network_payload: dict[str, Any] | None,
|
|
274
|
+
include_env: bool,
|
|
275
|
+
include_storage: bool,
|
|
276
|
+
):
|
|
277
|
+
"""构建已有 OpenClaw 的最小更新请求,默认保留服务端现有配置。"""
|
|
278
|
+
update_payload: dict[str, Any] = {
|
|
279
|
+
"artifact_type": "Container",
|
|
280
|
+
"artifact_path": image_ref,
|
|
281
|
+
"resources": {"cpu": cpu, "memory": memory},
|
|
282
|
+
"auth_type": "None",
|
|
283
|
+
"inbound_identity_auth": "None",
|
|
284
|
+
}
|
|
285
|
+
if include_env:
|
|
286
|
+
update_payload["env_vars"] = env_list
|
|
287
|
+
if memory_config:
|
|
288
|
+
update_payload["memory_config"] = memory_config
|
|
289
|
+
if image_credential:
|
|
290
|
+
update_payload["image_credential"] = image_credential
|
|
291
|
+
if include_storage and storage_config:
|
|
292
|
+
update_payload["storage"] = storage_config
|
|
293
|
+
if network_payload:
|
|
294
|
+
update_payload["network"] = network_payload
|
|
295
|
+
return update_payload
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def _abort_openclaw_error(
|
|
299
|
+
err: Exception,
|
|
300
|
+
*,
|
|
301
|
+
context: str | None = None,
|
|
302
|
+
argv: list[str] | None = None,
|
|
303
|
+
) -> None:
|
|
304
|
+
abort_with_cli_error(err, context=context, argv=argv)
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
def _generate_default_openclaw_name(prefix: str = DEFAULT_OPENCLAW_NAME) -> str:
|
|
308
|
+
"""生成低碰撞默认名称。
|
|
309
|
+
|
|
310
|
+
格式: openclaw-gateway-MMDDHHMMSS-xxxxxx
|
|
311
|
+
- 时间粒度提升到秒
|
|
312
|
+
- 追加 6 位十六进制随机后缀(24-bit)
|
|
313
|
+
"""
|
|
314
|
+
ts = datetime.now().strftime("%m%d%H%M%S")
|
|
315
|
+
suffix = secrets.token_hex(3)
|
|
316
|
+
return f"{prefix}-{ts}-{suffix}"
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
def _load_openclaw_project_name(project_dir: Path) -> Optional[str]:
|
|
320
|
+
"""从 OpenClaw 项目配置读取 init 时指定的项目名。"""
|
|
321
|
+
for file_name in ("agentengine.yaml", "ksadk.yaml"):
|
|
322
|
+
config_path = project_dir / file_name
|
|
323
|
+
if not config_path.exists():
|
|
324
|
+
continue
|
|
325
|
+
try:
|
|
326
|
+
import yaml
|
|
327
|
+
|
|
328
|
+
data = yaml.safe_load(config_path.read_text(encoding="utf-8-sig")) or {}
|
|
329
|
+
except Exception:
|
|
330
|
+
continue
|
|
331
|
+
if not isinstance(data, dict):
|
|
332
|
+
continue
|
|
333
|
+
framework = str(data.get("framework") or "").strip().lower()
|
|
334
|
+
if framework != "openclaw":
|
|
335
|
+
continue
|
|
336
|
+
project_name = str(data.get("name") or "").strip()
|
|
337
|
+
if project_name:
|
|
338
|
+
return project_name
|
|
339
|
+
return None
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
def _get_global_env() -> Dict[str, str]:
|
|
343
|
+
"""读取全局配置并转换为环境变量字典(带进程级缓存)。"""
|
|
344
|
+
global _GLOBAL_ENV_CACHE
|
|
345
|
+
if _GLOBAL_ENV_CACHE is not None:
|
|
346
|
+
return _GLOBAL_ENV_CACHE
|
|
347
|
+
|
|
348
|
+
try:
|
|
349
|
+
from ksadk.configs.global_config import get_env_from_global_config
|
|
350
|
+
_GLOBAL_ENV_CACHE = {
|
|
351
|
+
str(k): str(v).strip()
|
|
352
|
+
for k, v in get_env_from_global_config().items()
|
|
353
|
+
if k and v is not None and str(v).strip() != ""
|
|
354
|
+
}
|
|
355
|
+
except Exception:
|
|
356
|
+
_GLOBAL_ENV_CACHE = {}
|
|
357
|
+
|
|
358
|
+
return _GLOBAL_ENV_CACHE
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
def _resolve_env(*keys: str, default: Optional[str] = None) -> Optional[str]:
|
|
362
|
+
"""按优先级从环境变量和全局配置中获取值。"""
|
|
363
|
+
for key in keys:
|
|
364
|
+
val = os.getenv(key)
|
|
365
|
+
if val is not None and str(val).strip() != "":
|
|
366
|
+
return str(val).strip()
|
|
367
|
+
global_env = _get_global_env()
|
|
368
|
+
for key in keys:
|
|
369
|
+
val = global_env.get(key)
|
|
370
|
+
if val is not None and str(val).strip() != "":
|
|
371
|
+
return str(val).strip()
|
|
372
|
+
return default
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
def _openclaw_registry_env() -> dict[str, str]:
|
|
376
|
+
env = {str(k): str(v) for k, v in os.environ.items()}
|
|
377
|
+
env.update(_get_global_env())
|
|
378
|
+
return env
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
def _resolve_model_base_url(cli_value: Optional[str]) -> Optional[str]:
|
|
382
|
+
"""解析模型 Base URL,缺失时回退到 settings.model.api_base(KSPMAS 自动探测)。"""
|
|
383
|
+
if cli_value and str(cli_value).strip():
|
|
384
|
+
return str(cli_value).strip()
|
|
385
|
+
|
|
386
|
+
from_env = _resolve_env(
|
|
387
|
+
"OPENCLAW_MODEL_BASE_URL",
|
|
388
|
+
"OPENAI_BASE_URL",
|
|
389
|
+
"OPENAI_API_BASE",
|
|
390
|
+
"LLM_API_BASE",
|
|
391
|
+
"MODEL_API_BASE",
|
|
392
|
+
)
|
|
393
|
+
if from_env:
|
|
394
|
+
return from_env
|
|
395
|
+
|
|
396
|
+
try:
|
|
397
|
+
from ksadk.configs.settings import settings
|
|
398
|
+
api_base = settings.model.api_base
|
|
399
|
+
if api_base and str(api_base).strip():
|
|
400
|
+
return str(api_base).strip()
|
|
401
|
+
except Exception:
|
|
402
|
+
pass
|
|
403
|
+
|
|
404
|
+
return None
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
def _summarize_openclaw_account(agents: list[Dict[str, Any]]) -> str:
|
|
408
|
+
"""汇总列表所属账号,优先使用响应字段,缺失时回退当前 CLI 上下文。"""
|
|
409
|
+
accounts = sorted(
|
|
410
|
+
{
|
|
411
|
+
str(item.get("account_id") or "").strip()
|
|
412
|
+
for item in agents
|
|
413
|
+
if str(item.get("account_id") or "").strip()
|
|
414
|
+
}
|
|
415
|
+
)
|
|
416
|
+
if accounts:
|
|
417
|
+
return ",".join(accounts)
|
|
418
|
+
return _resolve_env("KSYUN_ACCOUNT_ID", default="-") or "-"
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
def _summarize_openclaw_region(agents: list[Dict[str, Any]], fallback_region: Optional[str]) -> str:
|
|
422
|
+
"""汇总列表中的 region,缺失时回退命令参数。"""
|
|
423
|
+
regions = sorted(
|
|
424
|
+
{
|
|
425
|
+
str(item.get("region") or "").strip()
|
|
426
|
+
for item in agents
|
|
427
|
+
if str(item.get("region") or "").strip()
|
|
428
|
+
}
|
|
429
|
+
)
|
|
430
|
+
if regions:
|
|
431
|
+
return ",".join(regions)
|
|
432
|
+
return str(fallback_region or "-")
|
|
433
|
+
|
|
434
|
+
|
|
435
|
+
def _print_openclaw_list_summary(table: RichTable, summary_text: str) -> None:
|
|
436
|
+
"""将摘要贴在表格下方;宽度不足时退化成普通单行。"""
|
|
437
|
+
table_width = Measurement.get(console, console.options, table).maximum
|
|
438
|
+
summary_width = Measurement.get(console, console.options, summary_text).maximum
|
|
439
|
+
if table_width >= summary_width:
|
|
440
|
+
summary_grid = RichTable.grid(expand=False)
|
|
441
|
+
summary_grid.add_column(justify="right", width=table_width)
|
|
442
|
+
summary_grid.add_row(f"[muted]{summary_text}[/]")
|
|
443
|
+
console.print(summary_grid)
|
|
444
|
+
return
|
|
445
|
+
console.print(f"[muted]{summary_text}[/]")
|
|
446
|
+
|
|
447
|
+
|
|
448
|
+
def _normalize_ui_locale(raw: Optional[str]) -> str:
|
|
449
|
+
"""标准化 UI 语言代码,默认 zh-CN。"""
|
|
450
|
+
text = str(raw or "").strip()
|
|
451
|
+
if not text:
|
|
452
|
+
return "zh-CN"
|
|
453
|
+
|
|
454
|
+
base = text.split(".", 1)[0].replace("_", "-").strip()
|
|
455
|
+
low = base.lower()
|
|
456
|
+
|
|
457
|
+
if low in {"c", "c-utf-8", "c.utf-8", "posix"}:
|
|
458
|
+
return "zh-CN"
|
|
459
|
+
if low.startswith("zh-tw") or low.startswith("zh-hk") or low.startswith("zh-mo") or low.startswith("zh-hant"):
|
|
460
|
+
return "zh-TW"
|
|
461
|
+
if low.startswith("zh"):
|
|
462
|
+
return "zh-CN"
|
|
463
|
+
if low.startswith("pt"):
|
|
464
|
+
return "pt-BR"
|
|
465
|
+
if low.startswith("de"):
|
|
466
|
+
return "de"
|
|
467
|
+
if low.startswith("en"):
|
|
468
|
+
return "en"
|
|
469
|
+
|
|
470
|
+
return "zh-CN"
|
|
471
|
+
|
|
472
|
+
|
|
473
|
+
def _is_truthy(raw: Optional[str]) -> bool:
|
|
474
|
+
text = str(raw or "").strip().lower()
|
|
475
|
+
return text in {"1", "true", "yes", "on"}
|
|
476
|
+
|
|
477
|
+
|
|
478
|
+
def _resolve_exec_profile_overrides(security_profile: Optional[str]) -> Dict[str, str]:
|
|
479
|
+
"""根据 CLI 安全预设返回 OpenClaw 运行时环境变量覆盖项。"""
|
|
480
|
+
profile = str(security_profile or "").strip().lower()
|
|
481
|
+
if not profile:
|
|
482
|
+
return {}
|
|
483
|
+
|
|
484
|
+
common = {
|
|
485
|
+
"OPENCLAW_EXEC_HOST": "gateway",
|
|
486
|
+
"OPENCLAW_EXEC_AUTO_ALLOW_SKILLS": "false",
|
|
487
|
+
"OPENCLAW_ELEVATED_ENABLED": "false",
|
|
488
|
+
}
|
|
489
|
+
if profile == "relaxed":
|
|
490
|
+
return {
|
|
491
|
+
**common,
|
|
492
|
+
"OPENCLAW_EXEC_STRICT_MODE": "false",
|
|
493
|
+
"OPENCLAW_EXEC_UNSAFE_MODE": "true",
|
|
494
|
+
"OPENCLAW_EXEC_SECURITY": "full",
|
|
495
|
+
"OPENCLAW_EXEC_ASK": "off",
|
|
496
|
+
"OPENCLAW_EXEC_ASK_FALLBACK": "full",
|
|
497
|
+
"OPENCLAW_EXEC_DEFAULT_ALLOWLIST_ENABLED": "false",
|
|
498
|
+
"OPENCLAW_FS_WORKSPACE_ONLY": "false",
|
|
499
|
+
}
|
|
500
|
+
if profile == "strict":
|
|
501
|
+
return {
|
|
502
|
+
**common,
|
|
503
|
+
"OPENCLAW_EXEC_STRICT_MODE": "true",
|
|
504
|
+
"OPENCLAW_EXEC_UNSAFE_MODE": "false",
|
|
505
|
+
"OPENCLAW_EXEC_SECURITY": "allowlist",
|
|
506
|
+
"OPENCLAW_EXEC_ASK": "off",
|
|
507
|
+
"OPENCLAW_EXEC_ASK_FALLBACK": "allowlist",
|
|
508
|
+
"OPENCLAW_EXEC_DEFAULT_ALLOWLIST_ENABLED": "true",
|
|
509
|
+
"OPENCLAW_FS_WORKSPACE_ONLY": "false",
|
|
510
|
+
}
|
|
511
|
+
if profile == "strictest":
|
|
512
|
+
return {
|
|
513
|
+
**common,
|
|
514
|
+
"OPENCLAW_EXEC_STRICT_MODE": "true",
|
|
515
|
+
"OPENCLAW_EXEC_UNSAFE_MODE": "false",
|
|
516
|
+
"OPENCLAW_EXEC_SECURITY": "deny",
|
|
517
|
+
"OPENCLAW_EXEC_ASK": "off",
|
|
518
|
+
"OPENCLAW_EXEC_ASK_FALLBACK": "deny",
|
|
519
|
+
"OPENCLAW_EXEC_DEFAULT_ALLOWLIST_ENABLED": "false",
|
|
520
|
+
"OPENCLAW_FS_WORKSPACE_ONLY": "true",
|
|
521
|
+
}
|
|
522
|
+
raise ValueError(f"unsupported OpenClaw security profile: {security_profile}")
|
|
523
|
+
|
|
524
|
+
|
|
525
|
+
def _strip_provider_prefix(provider_id: str, model_id: str) -> str:
|
|
526
|
+
provider = str(provider_id or "").strip()
|
|
527
|
+
model = str(model_id or "").strip()
|
|
528
|
+
if provider and model.lower().startswith(f"{provider.lower()}/"):
|
|
529
|
+
return model.split("/", 1)[1].strip()
|
|
530
|
+
return model
|
|
531
|
+
|
|
532
|
+
|
|
533
|
+
def _default_openclaw_model_inputs(provider_id: str, model_id: str) -> list[str]:
|
|
534
|
+
if str(provider_id or "").strip().lower() == "ksyun" and str(model_id or "").strip().lower() in {"glm-5.1", "glm-5.2"}:
|
|
535
|
+
return ["text"]
|
|
536
|
+
return ["text", "image"]
|
|
537
|
+
|
|
538
|
+
|
|
539
|
+
def _openclaw_catalog_inputs(
|
|
540
|
+
*,
|
|
541
|
+
provider_id: str,
|
|
542
|
+
model_id: str,
|
|
543
|
+
metadata: Dict[str, Any],
|
|
544
|
+
) -> list[str]:
|
|
545
|
+
raw = metadata.get("_provider_raw_model")
|
|
546
|
+
if isinstance(raw, dict):
|
|
547
|
+
architecture = raw.get("architecture")
|
|
548
|
+
if isinstance(architecture, dict):
|
|
549
|
+
modalities = architecture.get("input_modalities")
|
|
550
|
+
if isinstance(modalities, list):
|
|
551
|
+
normalized = {
|
|
552
|
+
str(item or "").strip().lower()
|
|
553
|
+
for item in modalities
|
|
554
|
+
if str(item or "").strip()
|
|
555
|
+
}
|
|
556
|
+
result = ["text"]
|
|
557
|
+
if normalized & {"image", "图片", "图像"}:
|
|
558
|
+
result.append("image")
|
|
559
|
+
return result
|
|
560
|
+
|
|
561
|
+
raw_capabilities = raw.get("capabilities") if isinstance(raw, dict) else None
|
|
562
|
+
capabilities = raw_capabilities if isinstance(raw_capabilities, dict) else None
|
|
563
|
+
if not capabilities and isinstance(metadata.get("capabilities"), dict):
|
|
564
|
+
metadata_capabilities = metadata["capabilities"]
|
|
565
|
+
if metadata_capabilities.get("multimodal_input_image"):
|
|
566
|
+
capabilities = metadata_capabilities
|
|
567
|
+
if isinstance(capabilities, dict):
|
|
568
|
+
result = ["text"]
|
|
569
|
+
if capabilities.get("multimodal_input_image"):
|
|
570
|
+
result.append("image")
|
|
571
|
+
return result
|
|
572
|
+
return _default_openclaw_model_inputs(provider_id, model_id)
|
|
573
|
+
|
|
574
|
+
|
|
575
|
+
def _openclaw_catalog_item_from_metadata(
|
|
576
|
+
raw_model: Dict[str, Any],
|
|
577
|
+
*,
|
|
578
|
+
provider_id: str,
|
|
579
|
+
provider_api: str,
|
|
580
|
+
) -> Optional[Dict[str, Any]]:
|
|
581
|
+
metadata = normalize_model_metadata(raw_model)
|
|
582
|
+
model_id = _strip_provider_prefix(
|
|
583
|
+
provider_id,
|
|
584
|
+
str(metadata.get("id") or metadata.get("name") or "").strip(),
|
|
585
|
+
)
|
|
586
|
+
if not model_id:
|
|
587
|
+
return None
|
|
588
|
+
|
|
589
|
+
inputs = _openclaw_catalog_inputs(
|
|
590
|
+
provider_id=provider_id,
|
|
591
|
+
model_id=model_id,
|
|
592
|
+
metadata=metadata,
|
|
593
|
+
)
|
|
594
|
+
return {
|
|
595
|
+
"id": model_id,
|
|
596
|
+
"name": str(raw_model.get("name") or metadata.get("display_name") or model_id),
|
|
597
|
+
"api": str(raw_model.get("api") or provider_api or "openai-completions"),
|
|
598
|
+
"reasoning": bool(raw_model.get("reasoning", True)),
|
|
599
|
+
"input": inputs or _default_openclaw_model_inputs(provider_id, model_id),
|
|
600
|
+
"cost": raw_model.get("cost")
|
|
601
|
+
if isinstance(raw_model.get("cost"), dict)
|
|
602
|
+
else {"input": 0, "output": 0, "cacheRead": 0, "cacheWrite": 0},
|
|
603
|
+
"contextWindow": int(metadata.get("context_window_tokens") or 200_000),
|
|
604
|
+
"maxTokens": int(metadata.get("max_output_tokens") or 20_000),
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
|
|
608
|
+
def _apply_openclaw_provider_model_catalog(
|
|
609
|
+
env: Dict[str, str],
|
|
610
|
+
raw_models: list[Any],
|
|
611
|
+
) -> bool:
|
|
612
|
+
if not raw_models:
|
|
613
|
+
return False
|
|
614
|
+
|
|
615
|
+
provider_id = str(env.get("OPENCLAW_MODEL_PROVIDER_ID") or "ksyun").strip() or "ksyun"
|
|
616
|
+
provider_api = str(env.get("OPENCLAW_MODEL_API") or "openai-completions").strip() or "openai-completions"
|
|
617
|
+
raw_catalog = str(env.get("OPENCLAW_MODEL_CATALOG_JSON") or "").strip()
|
|
618
|
+
catalog: list[Dict[str, Any]] = []
|
|
619
|
+
if raw_catalog:
|
|
620
|
+
try:
|
|
621
|
+
parsed_catalog = json.loads(raw_catalog)
|
|
622
|
+
except Exception:
|
|
623
|
+
parsed_catalog = []
|
|
624
|
+
if isinstance(parsed_catalog, list):
|
|
625
|
+
catalog = [dict(item) for item in parsed_catalog if isinstance(item, dict)]
|
|
626
|
+
|
|
627
|
+
seen: set[str] = set()
|
|
628
|
+
for item in catalog:
|
|
629
|
+
model_key = str(item.get("id") or item.get("name") or "").strip()
|
|
630
|
+
if model_key:
|
|
631
|
+
seen.add(model_key)
|
|
632
|
+
|
|
633
|
+
changed = False
|
|
634
|
+
for raw_model in raw_models:
|
|
635
|
+
if not isinstance(raw_model, dict):
|
|
636
|
+
raw_model = {"id": str(raw_model or "").strip()}
|
|
637
|
+
item = _openclaw_catalog_item_from_metadata(
|
|
638
|
+
raw_model,
|
|
639
|
+
provider_id=provider_id,
|
|
640
|
+
provider_api=provider_api,
|
|
641
|
+
)
|
|
642
|
+
if not item:
|
|
643
|
+
continue
|
|
644
|
+
model_key = str(item["id"])
|
|
645
|
+
existing_index = next(
|
|
646
|
+
(
|
|
647
|
+
index
|
|
648
|
+
for index, existing in enumerate(catalog)
|
|
649
|
+
if str(existing.get("id") or existing.get("name") or "").strip() == model_key
|
|
650
|
+
),
|
|
651
|
+
None,
|
|
652
|
+
)
|
|
653
|
+
if existing_index is not None:
|
|
654
|
+
merged = {**catalog[existing_index], **item}
|
|
655
|
+
if merged != catalog[existing_index]:
|
|
656
|
+
catalog[existing_index] = merged
|
|
657
|
+
changed = True
|
|
658
|
+
continue
|
|
659
|
+
seen.add(model_key)
|
|
660
|
+
catalog.append(item)
|
|
661
|
+
changed = True
|
|
662
|
+
|
|
663
|
+
if not catalog or not changed:
|
|
664
|
+
return False
|
|
665
|
+
env["OPENCLAW_MODEL_CATALOG_JSON"] = json.dumps(
|
|
666
|
+
catalog,
|
|
667
|
+
ensure_ascii=False,
|
|
668
|
+
separators=(",", ":"),
|
|
669
|
+
)
|
|
670
|
+
return True
|
|
671
|
+
|
|
672
|
+
|
|
673
|
+
def _apply_openclaw_provider_model_metadata(
|
|
674
|
+
env: Dict[str, str],
|
|
675
|
+
raw_model: Dict[str, Any],
|
|
676
|
+
) -> bool:
|
|
677
|
+
return _apply_openclaw_provider_model_catalog(env, [raw_model])
|
|
678
|
+
|
|
679
|
+
|
|
680
|
+
def _openclaw_requested_model_ids(env: Dict[str, str]) -> list[str]:
|
|
681
|
+
raw_allowlist = str(
|
|
682
|
+
env.get("OPENCLAW_MODEL_ALLOWLIST")
|
|
683
|
+
or env.get("AGENTENGINE_MODEL_ALLOWLIST")
|
|
684
|
+
or ""
|
|
685
|
+
).strip()
|
|
686
|
+
if raw_allowlist:
|
|
687
|
+
return [
|
|
688
|
+
item.strip()
|
|
689
|
+
for item in raw_allowlist.replace(";", ",").split(",")
|
|
690
|
+
if item.strip()
|
|
691
|
+
]
|
|
692
|
+
primary_model = str(
|
|
693
|
+
env.get("OPENCLAW_DEFAULT_MODEL")
|
|
694
|
+
or env.get("OPENAI_MODEL_NAME")
|
|
695
|
+
or ""
|
|
696
|
+
).strip()
|
|
697
|
+
return [primary_model] if primary_model else []
|
|
698
|
+
|
|
699
|
+
|
|
700
|
+
def _filter_openclaw_provider_catalog(
|
|
701
|
+
env: Dict[str, str],
|
|
702
|
+
provider_catalog: list[Any],
|
|
703
|
+
) -> list[Any]:
|
|
704
|
+
requested_models = _openclaw_requested_model_ids(env)
|
|
705
|
+
if not requested_models:
|
|
706
|
+
return []
|
|
707
|
+
raw_models = [
|
|
708
|
+
item.get("_provider_raw_model") or item
|
|
709
|
+
if isinstance(item, dict)
|
|
710
|
+
else item
|
|
711
|
+
for item in provider_catalog
|
|
712
|
+
]
|
|
713
|
+
selected: list[Any] = []
|
|
714
|
+
seen: set[str] = set()
|
|
715
|
+
for model_id in requested_models:
|
|
716
|
+
match = find_model_in_catalog(raw_models, model_id)
|
|
717
|
+
if match is None:
|
|
718
|
+
continue
|
|
719
|
+
identities = sorted(str(value).strip().lower() for value in (model_id, str(match)) if str(value).strip())
|
|
720
|
+
dedupe_key = identities[0] if identities else str(model_id).lower()
|
|
721
|
+
if dedupe_key in seen:
|
|
722
|
+
continue
|
|
723
|
+
seen.add(dedupe_key)
|
|
724
|
+
selected.append(match)
|
|
725
|
+
return selected
|
|
726
|
+
|
|
727
|
+
|
|
728
|
+
def _build_openclaw_env_vars(
|
|
729
|
+
*,
|
|
730
|
+
model_base_url: Optional[str] = None,
|
|
731
|
+
model_api_key: Optional[str] = None,
|
|
732
|
+
default_model: Optional[str] = None,
|
|
733
|
+
model_provider_id: Optional[str] = None,
|
|
734
|
+
gateway_port: Optional[str] = None,
|
|
735
|
+
public_port: Optional[str] = None,
|
|
736
|
+
security_profile: Optional[str] = None,
|
|
737
|
+
) -> dict:
|
|
738
|
+
"""构建 OpenClaw 所需的环境变量,自动复用 OPENAI_* 环境变量"""
|
|
739
|
+
env = {}
|
|
740
|
+
default_provider_id = "ksyun"
|
|
741
|
+
default_model_api = "openai-completions"
|
|
742
|
+
default_model_base_url = "https://kspmas.ksyun.com/v1"
|
|
743
|
+
exec_profile_overrides = _resolve_exec_profile_overrides(security_profile)
|
|
744
|
+
|
|
745
|
+
# 模型配置:客户端只透传用户显式配置和可选的 API Key;
|
|
746
|
+
# 其余默认值交给镜像 bootstrap 兜底,避免创建请求把服务端默认行为短路掉。
|
|
747
|
+
openclaw_explicit_model = default_model or _resolve_env("OPENCLAW_DEFAULT_MODEL")
|
|
748
|
+
generic_model_preference = _resolve_env("OPENAI_MODEL_NAME", "MODEL_NAME", "LLM_MODEL")
|
|
749
|
+
model_preference = openclaw_explicit_model or generic_model_preference
|
|
750
|
+
explicit_base_url = (
|
|
751
|
+
model_base_url
|
|
752
|
+
or _resolve_env("OPENCLAW_MODEL_BASE_URL", "OPENAI_BASE_URL", "OPENAI_API_BASE")
|
|
753
|
+
)
|
|
754
|
+
base_url = _resolve_model_base_url(explicit_base_url)
|
|
755
|
+
api_key = (
|
|
756
|
+
model_api_key
|
|
757
|
+
or _resolve_env("OPENCLAW_MODEL_API_KEY", "OPENAI_API_KEY", "LLM_API_KEY", "MODEL_API_KEY")
|
|
758
|
+
)
|
|
759
|
+
model = model_preference or "glm-5.2"
|
|
760
|
+
explicit_provider_id = model_provider_id or _resolve_env("OPENCLAW_MODEL_PROVIDER_ID")
|
|
761
|
+
inferred_provider_id = explicit_provider_id
|
|
762
|
+
if not inferred_provider_id and model and "/" in model:
|
|
763
|
+
inferred_provider_id = model.split("/", 1)[0].strip()
|
|
764
|
+
provider_id = inferred_provider_id or default_provider_id
|
|
765
|
+
resolved_gateway_port = (
|
|
766
|
+
gateway_port
|
|
767
|
+
or _resolve_env("OPENCLAW_GATEWAY_PORT", "PORT")
|
|
768
|
+
or "8080"
|
|
769
|
+
)
|
|
770
|
+
resolved_public_port = (
|
|
771
|
+
public_port
|
|
772
|
+
or _resolve_env("OPENCLAW_PUBLIC_PORT")
|
|
773
|
+
or "80"
|
|
774
|
+
)
|
|
775
|
+
explicit_model_api = _resolve_env("OPENCLAW_MODEL_API")
|
|
776
|
+
model_api = explicit_model_api or default_model_api
|
|
777
|
+
trusted_proxy_user_header = (
|
|
778
|
+
_resolve_env(
|
|
779
|
+
"OPENCLAW_TRUSTED_PROXY_USER_HEADER",
|
|
780
|
+
"OPENCLAW_GATEWAY_TRUSTED_PROXY_USER_HEADER",
|
|
781
|
+
)
|
|
782
|
+
or DEFAULT_TRUSTED_PROXY_USER_HEADER
|
|
783
|
+
).strip().lower()
|
|
784
|
+
internal_trusted_proxy_user = (
|
|
785
|
+
_resolve_env("OPENCLAW_INTERNAL_TRUSTED_PROXY_USER")
|
|
786
|
+
or "openclaw-backend"
|
|
787
|
+
)
|
|
788
|
+
internal_trusted_proxy_user_header = (
|
|
789
|
+
_resolve_env("OPENCLAW_INTERNAL_TRUSTED_PROXY_USER_HEADER")
|
|
790
|
+
or trusted_proxy_user_header
|
|
791
|
+
or DEFAULT_TRUSTED_PROXY_USER_HEADER
|
|
792
|
+
).strip().lower()
|
|
793
|
+
trusted_proxies = _normalize_csv_list(
|
|
794
|
+
_resolve_env("OPENCLAW_TRUSTED_PROXIES") or "",
|
|
795
|
+
default_items=DEFAULT_TRUSTED_PROXY_CIDRS,
|
|
796
|
+
)
|
|
797
|
+
browser_enabled = _resolve_env("OPENCLAW_BROWSER_ENABLED")
|
|
798
|
+
browser_no_sandbox = _resolve_env("OPENCLAW_BROWSER_NO_SANDBOX") or "true"
|
|
799
|
+
browser_headless = _resolve_env("OPENCLAW_BROWSER_HEADLESS") or "true"
|
|
800
|
+
browser_executable = _resolve_env("OPENCLAW_BROWSER_EXECUTABLE_PATH", "OPENCLAW_BROWSER_EXECUTABLE")
|
|
801
|
+
ui_locale = _normalize_ui_locale(_resolve_env("OPENCLAW_UI_LOCALE", "LANG", "LC_ALL"))
|
|
802
|
+
exec_strict_mode_raw = (
|
|
803
|
+
exec_profile_overrides.get("OPENCLAW_EXEC_STRICT_MODE")
|
|
804
|
+
or _resolve_env("OPENCLAW_EXEC_STRICT_MODE", "OPENCLAW_EXEC_SAFE_MODE")
|
|
805
|
+
or "false"
|
|
806
|
+
)
|
|
807
|
+
exec_strict_mode = _is_truthy(exec_strict_mode_raw)
|
|
808
|
+
|
|
809
|
+
exec_host = exec_profile_overrides.get("OPENCLAW_EXEC_HOST") or _resolve_env("OPENCLAW_EXEC_HOST") or "gateway"
|
|
810
|
+
exec_security = (
|
|
811
|
+
exec_profile_overrides.get("OPENCLAW_EXEC_SECURITY")
|
|
812
|
+
or _resolve_env("OPENCLAW_EXEC_SECURITY")
|
|
813
|
+
or ("allowlist" if exec_strict_mode else "full")
|
|
814
|
+
)
|
|
815
|
+
exec_ask = exec_profile_overrides.get("OPENCLAW_EXEC_ASK") or _resolve_env("OPENCLAW_EXEC_ASK") or "off"
|
|
816
|
+
exec_ask_fallback = (
|
|
817
|
+
exec_profile_overrides.get("OPENCLAW_EXEC_ASK_FALLBACK")
|
|
818
|
+
or _resolve_env("OPENCLAW_EXEC_ASK_FALLBACK")
|
|
819
|
+
or ("allowlist" if exec_strict_mode else "full")
|
|
820
|
+
)
|
|
821
|
+
exec_auto_allow_skills = (
|
|
822
|
+
exec_profile_overrides.get("OPENCLAW_EXEC_AUTO_ALLOW_SKILLS")
|
|
823
|
+
or _resolve_env("OPENCLAW_EXEC_AUTO_ALLOW_SKILLS")
|
|
824
|
+
or "false"
|
|
825
|
+
)
|
|
826
|
+
elevated_enabled = (
|
|
827
|
+
exec_profile_overrides.get("OPENCLAW_ELEVATED_ENABLED")
|
|
828
|
+
or _resolve_env("OPENCLAW_ELEVATED_ENABLED")
|
|
829
|
+
or "false"
|
|
830
|
+
)
|
|
831
|
+
exec_default_allowlist_enabled = (
|
|
832
|
+
exec_profile_overrides.get("OPENCLAW_EXEC_DEFAULT_ALLOWLIST_ENABLED")
|
|
833
|
+
or _resolve_env("OPENCLAW_EXEC_DEFAULT_ALLOWLIST_ENABLED")
|
|
834
|
+
or (
|
|
835
|
+
"true" if exec_strict_mode else "false"
|
|
836
|
+
)
|
|
837
|
+
)
|
|
838
|
+
exec_allowlist = _resolve_env("OPENCLAW_EXEC_ALLOWLIST")
|
|
839
|
+
fs_workspace_only = (
|
|
840
|
+
exec_profile_overrides.get("OPENCLAW_FS_WORKSPACE_ONLY")
|
|
841
|
+
or _resolve_env("OPENCLAW_FS_WORKSPACE_ONLY")
|
|
842
|
+
or "false"
|
|
843
|
+
)
|
|
844
|
+
model_api_key_secret_source = _resolve_env("OPENCLAW_MODEL_API_KEY_SECRET_SOURCE") or "file"
|
|
845
|
+
model_api_key_secret_file_path = _resolve_env("OPENCLAW_MODEL_API_KEY_SECRET_FILE_PATH")
|
|
846
|
+
gateway_auth_mode = _resolve_env("OPENCLAW_GATEWAY_AUTH_MODE")
|
|
847
|
+
gateway_token = _resolve_env("OPENCLAW_GATEWAY_TOKEN")
|
|
848
|
+
gateway_password = _resolve_env("OPENCLAW_GATEWAY_PASSWORD")
|
|
849
|
+
|
|
850
|
+
env["OPENCLAW_GATEWAY_BIND"] = "lan"
|
|
851
|
+
if gateway_auth_mode:
|
|
852
|
+
env["OPENCLAW_GATEWAY_AUTH_MODE"] = gateway_auth_mode
|
|
853
|
+
env["OPENCLAW_TRUSTED_PROXY_USER_HEADER"] = trusted_proxy_user_header or DEFAULT_TRUSTED_PROXY_USER_HEADER
|
|
854
|
+
env["OPENCLAW_INTERNAL_TRUSTED_PROXY_USER"] = internal_trusted_proxy_user
|
|
855
|
+
env["OPENCLAW_INTERNAL_TRUSTED_PROXY_USER_HEADER"] = (
|
|
856
|
+
internal_trusted_proxy_user_header or trusted_proxy_user_header or DEFAULT_TRUSTED_PROXY_USER_HEADER
|
|
857
|
+
)
|
|
858
|
+
env["OPENCLAW_TRUSTED_PROXIES"] = trusted_proxies
|
|
859
|
+
env["OPENCLAW_GATEWAY_PORT"] = str(resolved_gateway_port)
|
|
860
|
+
env["OPENCLAW_PUBLIC_PORT"] = str(resolved_public_port)
|
|
861
|
+
if browser_enabled:
|
|
862
|
+
env["OPENCLAW_BROWSER_ENABLED"] = browser_enabled
|
|
863
|
+
env["OPENCLAW_BROWSER_NO_SANDBOX"] = browser_no_sandbox
|
|
864
|
+
env["OPENCLAW_BROWSER_HEADLESS"] = browser_headless
|
|
865
|
+
if browser_executable:
|
|
866
|
+
env["OPENCLAW_BROWSER_EXECUTABLE_PATH"] = browser_executable
|
|
867
|
+
env["OPENCLAW_UI_LOCALE"] = ui_locale
|
|
868
|
+
env["OPENCLAW_EXEC_HOST"] = exec_host
|
|
869
|
+
env["OPENCLAW_EXEC_STRICT_MODE"] = "true" if exec_strict_mode else "false"
|
|
870
|
+
env["OPENCLAW_EXEC_UNSAFE_MODE"] = "false" if exec_strict_mode else "true"
|
|
871
|
+
env["OPENCLAW_EXEC_SECURITY"] = exec_security
|
|
872
|
+
env["OPENCLAW_EXEC_ASK"] = exec_ask
|
|
873
|
+
env["OPENCLAW_EXEC_ASK_FALLBACK"] = exec_ask_fallback
|
|
874
|
+
env["OPENCLAW_EXEC_AUTO_ALLOW_SKILLS"] = exec_auto_allow_skills
|
|
875
|
+
env["OPENCLAW_ELEVATED_ENABLED"] = elevated_enabled
|
|
876
|
+
env["OPENCLAW_EXEC_DEFAULT_ALLOWLIST_ENABLED"] = exec_default_allowlist_enabled
|
|
877
|
+
env["OPENCLAW_FS_WORKSPACE_ONLY"] = fs_workspace_only
|
|
878
|
+
env["OPENCLAW_MODEL_API_KEY_SECRET_SOURCE"] = model_api_key_secret_source
|
|
879
|
+
if exec_allowlist:
|
|
880
|
+
env["OPENCLAW_EXEC_ALLOWLIST"] = exec_allowlist
|
|
881
|
+
if model_api_key_secret_file_path:
|
|
882
|
+
env["OPENCLAW_MODEL_API_KEY_SECRET_FILE_PATH"] = model_api_key_secret_file_path
|
|
883
|
+
|
|
884
|
+
if explicit_provider_id and provider_id != default_provider_id:
|
|
885
|
+
env["OPENCLAW_MODEL_PROVIDER_ID"] = provider_id
|
|
886
|
+
elif not explicit_provider_id and provider_id and provider_id != default_provider_id:
|
|
887
|
+
env["OPENCLAW_MODEL_PROVIDER_ID"] = provider_id
|
|
888
|
+
if explicit_model_api and model_api != default_model_api:
|
|
889
|
+
env["OPENCLAW_MODEL_API"] = model_api
|
|
890
|
+
if explicit_base_url and base_url and base_url != default_model_base_url:
|
|
891
|
+
env["OPENCLAW_MODEL_BASE_URL"] = base_url
|
|
892
|
+
if api_key:
|
|
893
|
+
env["OPENCLAW_MODEL_API_KEY"] = api_key
|
|
894
|
+
normalized_model = model.strip() if model else None
|
|
895
|
+
catalog_model_id = None
|
|
896
|
+
resolved_model = None
|
|
897
|
+
if normalized_model:
|
|
898
|
+
if "/" in normalized_model:
|
|
899
|
+
_, catalog_model_id = normalized_model.split("/", 1)
|
|
900
|
+
resolved_model = normalized_model
|
|
901
|
+
else:
|
|
902
|
+
catalog_model_id = normalized_model
|
|
903
|
+
resolved_model = f"{provider_id}/{normalized_model}" if provider_id else normalized_model
|
|
904
|
+
if openclaw_explicit_model:
|
|
905
|
+
env["OPENCLAW_DEFAULT_MODEL"] = resolved_model
|
|
906
|
+
elif generic_model_preference:
|
|
907
|
+
env["OPENAI_MODEL_NAME"] = resolved_model
|
|
908
|
+
|
|
909
|
+
# 额外的可选配置
|
|
910
|
+
catalog = _resolve_env("OPENCLAW_MODEL_CATALOG_JSON")
|
|
911
|
+
if catalog:
|
|
912
|
+
env["OPENCLAW_MODEL_CATALOG_JSON"] = catalog
|
|
913
|
+
openclaw_model_allowlist = _resolve_env("OPENCLAW_MODEL_ALLOWLIST")
|
|
914
|
+
agentengine_model_allowlist = _resolve_env("AGENTENGINE_MODEL_ALLOWLIST")
|
|
915
|
+
if openclaw_model_allowlist:
|
|
916
|
+
env["OPENCLAW_MODEL_ALLOWLIST"] = openclaw_model_allowlist
|
|
917
|
+
elif agentengine_model_allowlist:
|
|
918
|
+
env["AGENTENGINE_MODEL_ALLOWLIST"] = agentengine_model_allowlist
|
|
919
|
+
origins = _resolve_env("OPENCLAW_ALLOWED_ORIGINS")
|
|
920
|
+
if origins:
|
|
921
|
+
env["OPENCLAW_ALLOWED_ORIGINS"] = _normalize_allowed_origins(origins)
|
|
922
|
+
else:
|
|
923
|
+
# 统一输出 JSON 数组字符串,兼容旧版 bootstrap(仅支持 JSON.parse)。
|
|
924
|
+
env["OPENCLAW_ALLOWED_ORIGINS"] = json.dumps(["*"])
|
|
925
|
+
allow_insecure_auth = _resolve_env("OPENCLAW_ALLOW_INSECURE_AUTH")
|
|
926
|
+
env["OPENCLAW_ALLOW_INSECURE_AUTH"] = allow_insecure_auth if allow_insecure_auth else "true"
|
|
927
|
+
disable_device_auth = _resolve_env("OPENCLAW_DISABLE_DEVICE_AUTH")
|
|
928
|
+
env["OPENCLAW_DISABLE_DEVICE_AUTH"] = disable_device_auth if disable_device_auth else "true"
|
|
929
|
+
if gateway_token:
|
|
930
|
+
env["OPENCLAW_GATEWAY_TOKEN"] = gateway_token
|
|
931
|
+
if gateway_password:
|
|
932
|
+
env["OPENCLAW_GATEWAY_PASSWORD"] = gateway_password
|
|
933
|
+
for passthrough_key in [
|
|
934
|
+
"OPENCLAW_CHANNEL_BOOTSTRAP_JSON",
|
|
935
|
+
"OPENCLAW_BROWSER_SSRF_POLICY_JSON",
|
|
936
|
+
"OPENCLAW_WEB_FETCH_ENABLED",
|
|
937
|
+
"OPENCLAW_WEB_SEARCH_PROVIDER",
|
|
938
|
+
"OPENCLAW_WEB_SEARCH_BASE_URL",
|
|
939
|
+
"OPENCLAW_WEB_SEARCH_MODEL",
|
|
940
|
+
"OPENCLAW_WEB_SEARCH_API_KEY",
|
|
941
|
+
"OPENCLAW_WEB_SEARCH_API_KEY_SECRET_SOURCE",
|
|
942
|
+
"OPENCLAW_WEB_SEARCH_API_KEY_SECRET_PROVIDER",
|
|
943
|
+
"OPENCLAW_WEB_SEARCH_API_KEY_SECRET_ID",
|
|
944
|
+
]:
|
|
945
|
+
passthrough_value = _resolve_env(passthrough_key)
|
|
946
|
+
if passthrough_value:
|
|
947
|
+
env[passthrough_key] = passthrough_value
|
|
948
|
+
|
|
949
|
+
env = _normalize_openclaw_gateway_auth_env(env)
|
|
950
|
+
return build_runtime_model_policy_env(env, runtime="openclaw")
|
|
951
|
+
|
|
952
|
+
|
|
953
|
+
def _normalize_allowed_origins(raw: str) -> str:
|
|
954
|
+
"""标准化 OPENCLAW_ALLOWED_ORIGINS,统一输出 JSON 数组字符串。"""
|
|
955
|
+
text = (raw or "").strip()
|
|
956
|
+
if not text:
|
|
957
|
+
return ""
|
|
958
|
+
|
|
959
|
+
origins = []
|
|
960
|
+
try:
|
|
961
|
+
parsed = json.loads(text)
|
|
962
|
+
if isinstance(parsed, list):
|
|
963
|
+
origins = [str(x).strip() for x in parsed if str(x).strip()]
|
|
964
|
+
except Exception:
|
|
965
|
+
# Backward compatible: 支持逗号/分号/空白分隔字符串。
|
|
966
|
+
parts = [p.strip() for p in text.replace(";", ",").replace(" ", ",").split(",")]
|
|
967
|
+
origins = [p.strip() for p in parts if p.strip()]
|
|
968
|
+
|
|
969
|
+
if not origins:
|
|
970
|
+
origins = [text]
|
|
971
|
+
|
|
972
|
+
deduped = list(dict.fromkeys(origins))
|
|
973
|
+
return json.dumps(deduped, ensure_ascii=False)
|
|
974
|
+
|
|
975
|
+
|
|
976
|
+
def _normalize_csv_list(raw: str, *, default_items: Optional[list[str]] = None) -> str:
|
|
977
|
+
"""标准化字符串列表为逗号分隔格式。"""
|
|
978
|
+
text = (raw or "").strip()
|
|
979
|
+
items: list[str] = []
|
|
980
|
+
if text:
|
|
981
|
+
try:
|
|
982
|
+
parsed = json.loads(text)
|
|
983
|
+
if isinstance(parsed, list):
|
|
984
|
+
items = [str(x).strip() for x in parsed if str(x).strip()]
|
|
985
|
+
except Exception:
|
|
986
|
+
parts = [p.strip() for p in text.replace(";", ",").replace(" ", ",").split(",")]
|
|
987
|
+
items = [p for p in parts if p]
|
|
988
|
+
|
|
989
|
+
if not items:
|
|
990
|
+
items = [str(x).strip() for x in (default_items or []) if str(x).strip()]
|
|
991
|
+
|
|
992
|
+
return ",".join(list(dict.fromkeys(items)))
|
|
993
|
+
|
|
994
|
+
|
|
995
|
+
def _normalize_openclaw_gateway_auth_env(env: dict[str, str]) -> dict[str, str]:
|
|
996
|
+
"""标准化 OpenClaw gateway 鉴权模式与共享密钥配置。"""
|
|
997
|
+
normalized_env = dict(env or {})
|
|
998
|
+
raw_mode = str(normalized_env.get("OPENCLAW_GATEWAY_AUTH_MODE") or "").strip().lower()
|
|
999
|
+
raw_token = str(normalized_env.get("OPENCLAW_GATEWAY_TOKEN") or "").strip()
|
|
1000
|
+
raw_password = str(normalized_env.get("OPENCLAW_GATEWAY_PASSWORD") or "").strip()
|
|
1001
|
+
|
|
1002
|
+
if raw_mode and raw_mode not in {"trusted-proxy", "token", "none"}:
|
|
1003
|
+
raise ValueError("OPENCLAW_GATEWAY_AUTH_MODE 仅支持 trusted-proxy、token 或 none")
|
|
1004
|
+
|
|
1005
|
+
auth_mode = raw_mode or ("token" if raw_token or raw_password else "trusted-proxy")
|
|
1006
|
+
if auth_mode == "token":
|
|
1007
|
+
if raw_token and raw_password and raw_token != raw_password:
|
|
1008
|
+
raise ValueError("OPENCLAW_GATEWAY_TOKEN 与 OPENCLAW_GATEWAY_PASSWORD 同时提供时必须一致")
|
|
1009
|
+
shared_secret = raw_token or raw_password
|
|
1010
|
+
if not shared_secret:
|
|
1011
|
+
raise ValueError(
|
|
1012
|
+
"OPENCLAW_GATEWAY_AUTH_MODE=token 时必须提供 OPENCLAW_GATEWAY_TOKEN 或 OPENCLAW_GATEWAY_PASSWORD"
|
|
1013
|
+
)
|
|
1014
|
+
normalized_env["OPENCLAW_GATEWAY_AUTH_MODE"] = "token"
|
|
1015
|
+
normalized_env["OPENCLAW_GATEWAY_TOKEN"] = shared_secret
|
|
1016
|
+
normalized_env["OPENCLAW_GATEWAY_PASSWORD"] = shared_secret
|
|
1017
|
+
return normalized_env
|
|
1018
|
+
|
|
1019
|
+
if raw_token or raw_password:
|
|
1020
|
+
raise ValueError(
|
|
1021
|
+
"仅在 OPENCLAW_GATEWAY_AUTH_MODE=token 时支持 OPENCLAW_GATEWAY_TOKEN 或 OPENCLAW_GATEWAY_PASSWORD"
|
|
1022
|
+
)
|
|
1023
|
+
|
|
1024
|
+
normalized_env["OPENCLAW_GATEWAY_AUTH_MODE"] = auth_mode
|
|
1025
|
+
normalized_env.pop("OPENCLAW_GATEWAY_TOKEN", None)
|
|
1026
|
+
normalized_env.pop("OPENCLAW_GATEWAY_PASSWORD", None)
|
|
1027
|
+
return normalized_env
|
|
1028
|
+
|
|
1029
|
+
|
|
1030
|
+
def _parse_extra_openclaw_env_pairs(items: tuple[str, ...] | list[str] | None) -> dict[str, str]:
|
|
1031
|
+
"""解析 deploy --env 传入的自定义环境变量。"""
|
|
1032
|
+
parsed: dict[str, str] = {}
|
|
1033
|
+
for raw_item in items or ():
|
|
1034
|
+
item = str(raw_item or "").strip()
|
|
1035
|
+
if not item or "=" not in item:
|
|
1036
|
+
raise ValueError(f"自定义环境变量格式错误: {raw_item!r},应为 KEY=VALUE")
|
|
1037
|
+
key, value = item.split("=", 1)
|
|
1038
|
+
key = key.strip()
|
|
1039
|
+
if not OPENCLAW_ENV_KEY_PATTERN.fullmatch(key):
|
|
1040
|
+
raise ValueError(f"自定义环境变量名不合法: {key!r},请使用合法的环境变量名")
|
|
1041
|
+
if key == "OPENCLAW_GATEWAY_AUTH_MODE":
|
|
1042
|
+
normalized = value.strip().lower()
|
|
1043
|
+
if normalized not in {"trusted-proxy", "token", "none"}:
|
|
1044
|
+
raise ValueError("OPENCLAW_GATEWAY_AUTH_MODE 仅支持 trusted-proxy、token 或 none")
|
|
1045
|
+
value = normalized
|
|
1046
|
+
parsed[key] = value
|
|
1047
|
+
return parsed
|
|
1048
|
+
|
|
1049
|
+
|
|
1050
|
+
def _build_openclaw_memory_config(
|
|
1051
|
+
*,
|
|
1052
|
+
memory_system: str | None,
|
|
1053
|
+
mem0_instance_id: str | None,
|
|
1054
|
+
mem0_instance_name: str | None,
|
|
1055
|
+
mem0_region: str | None,
|
|
1056
|
+
) -> dict[str, str] | None:
|
|
1057
|
+
normalized_system = str(memory_system or "").strip().lower()
|
|
1058
|
+
normalized_mem0_instance_id = str(mem0_instance_id or "").strip()
|
|
1059
|
+
normalized_mem0_instance_name = str(mem0_instance_name or "").strip()
|
|
1060
|
+
normalized_mem0_region = str(mem0_region or "").strip()
|
|
1061
|
+
|
|
1062
|
+
has_mem0_detail = any(
|
|
1063
|
+
[
|
|
1064
|
+
normalized_mem0_instance_id,
|
|
1065
|
+
normalized_mem0_instance_name,
|
|
1066
|
+
normalized_mem0_region,
|
|
1067
|
+
]
|
|
1068
|
+
)
|
|
1069
|
+
if not normalized_system and has_mem0_detail:
|
|
1070
|
+
normalized_system = "mem0"
|
|
1071
|
+
if not normalized_system:
|
|
1072
|
+
return None
|
|
1073
|
+
|
|
1074
|
+
if normalized_system == "openclaw_default":
|
|
1075
|
+
if has_mem0_detail:
|
|
1076
|
+
raise click.UsageError("使用 --memory-system openclaw_default 时不能再传入 mem0 参数。")
|
|
1077
|
+
return {"memory_system": "openclaw_default"}
|
|
1078
|
+
|
|
1079
|
+
if normalized_system == "mem0":
|
|
1080
|
+
if not normalized_mem0_instance_id:
|
|
1081
|
+
raise click.UsageError("使用 --memory-system mem0 时必须传入 --mem0-instance-id。")
|
|
1082
|
+
payload = {
|
|
1083
|
+
"memory_system": "mem0",
|
|
1084
|
+
"mem0_instance_id": normalized_mem0_instance_id,
|
|
1085
|
+
}
|
|
1086
|
+
if normalized_mem0_instance_name:
|
|
1087
|
+
payload["mem0_instance_name"] = normalized_mem0_instance_name
|
|
1088
|
+
if normalized_mem0_region:
|
|
1089
|
+
payload["mem0_region"] = normalized_mem0_region
|
|
1090
|
+
return payload
|
|
1091
|
+
|
|
1092
|
+
raise click.UsageError(f"不支持的记忆后端: {normalized_system}")
|
|
1093
|
+
|
|
1094
|
+
|
|
1095
|
+
def _parse_image(image: Optional[str]) -> tuple[str, str, str]:
|
|
1096
|
+
"""解析镜像地址为 (namespace, repo, version)
|
|
1097
|
+
|
|
1098
|
+
支持格式:
|
|
1099
|
+
- ghcr.io/kingsoftcloud/ns/repo:tag → (ns, repo, tag)
|
|
1100
|
+
- ns/repo:tag → (ns, repo, tag)
|
|
1101
|
+
- 无输入 → 使用默认值
|
|
1102
|
+
"""
|
|
1103
|
+
if not image:
|
|
1104
|
+
return DEFAULT_OPENCLAW_NAMESPACE, DEFAULT_OPENCLAW_REPO, DEFAULT_OPENCLAW_VERSION
|
|
1105
|
+
|
|
1106
|
+
# 去掉 registry 域名前缀。
|
|
1107
|
+
path = image
|
|
1108
|
+
if "/" in path:
|
|
1109
|
+
parts = path.split("/")
|
|
1110
|
+
if "." in parts[0]:
|
|
1111
|
+
# 有 registry 域名,去掉
|
|
1112
|
+
parts = parts[1:]
|
|
1113
|
+
if len(parts) >= 2:
|
|
1114
|
+
ns = parts[0]
|
|
1115
|
+
repo_version = "/".join(parts[1:])
|
|
1116
|
+
else:
|
|
1117
|
+
ns = "default"
|
|
1118
|
+
repo_version = parts[0]
|
|
1119
|
+
else:
|
|
1120
|
+
ns = "default"
|
|
1121
|
+
repo_version = path
|
|
1122
|
+
|
|
1123
|
+
# 拆分 repo:version
|
|
1124
|
+
if ":" in repo_version:
|
|
1125
|
+
repo, version = repo_version.rsplit(":", 1)
|
|
1126
|
+
else:
|
|
1127
|
+
repo = repo_version
|
|
1128
|
+
version = "latest"
|
|
1129
|
+
|
|
1130
|
+
return ns, repo, version
|
|
1131
|
+
|
|
1132
|
+
|
|
1133
|
+
def _resolve_image_ref(image: Optional[str]) -> str:
|
|
1134
|
+
"""解析并返回完整镜像地址。"""
|
|
1135
|
+
if image and str(image).strip():
|
|
1136
|
+
img = str(image).strip()
|
|
1137
|
+
first = img.split("/", 1)[0]
|
|
1138
|
+
if "." in first or ":" in first or first == "localhost":
|
|
1139
|
+
return img
|
|
1140
|
+
return f"{DEFAULT_OPENCLAW_REGISTRY}/{img}"
|
|
1141
|
+
return (
|
|
1142
|
+
f"{DEFAULT_OPENCLAW_REGISTRY}/"
|
|
1143
|
+
f"{DEFAULT_OPENCLAW_NAMESPACE}/{DEFAULT_OPENCLAW_REPO}:{DEFAULT_OPENCLAW_VERSION}"
|
|
1144
|
+
)
|
|
1145
|
+
|
|
1146
|
+
|
|
1147
|
+
def _version_tuple(raw: str) -> tuple[int, ...]:
|
|
1148
|
+
"""将版本字符串转为可比较的整数元组。"""
|
|
1149
|
+
parts = []
|
|
1150
|
+
for token in str(raw or "").strip().split("."):
|
|
1151
|
+
m = re.match(r"^(\d+)", token)
|
|
1152
|
+
if not m:
|
|
1153
|
+
break
|
|
1154
|
+
parts.append(int(m.group(1)))
|
|
1155
|
+
return tuple(parts)
|
|
1156
|
+
|
|
1157
|
+
|
|
1158
|
+
def _is_version_newer(candidate: str, current: str) -> bool:
|
|
1159
|
+
"""判断 candidate 是否高于 current。"""
|
|
1160
|
+
cand = _version_tuple(candidate)
|
|
1161
|
+
cur = _version_tuple(current)
|
|
1162
|
+
if not cand or not cur:
|
|
1163
|
+
return False
|
|
1164
|
+
n = max(len(cand), len(cur))
|
|
1165
|
+
cand = cand + (0,) * (n - len(cand))
|
|
1166
|
+
cur = cur + (0,) * (n - len(cur))
|
|
1167
|
+
return cand > cur
|
|
1168
|
+
|
|
1169
|
+
|
|
1170
|
+
async def _fetch_bootstrap_config(region: str) -> Optional[Dict[str, Any]]:
|
|
1171
|
+
"""从服务端获取客户端启动配置。失败时返回 None。"""
|
|
1172
|
+
from ksadk.api import AgentEngineClient
|
|
1173
|
+
from ksadk.version import VERSION as CLI_VERSION
|
|
1174
|
+
|
|
1175
|
+
try:
|
|
1176
|
+
async with AgentEngineClient(region=region) as client:
|
|
1177
|
+
return await client.get_client_bootstrap_config(
|
|
1178
|
+
product="openclaw",
|
|
1179
|
+
framework="openclaw",
|
|
1180
|
+
region=region,
|
|
1181
|
+
client_type="cli",
|
|
1182
|
+
client_version=CLI_VERSION,
|
|
1183
|
+
locale=_resolve_env("OPENCLAW_UI_LOCALE", "LANG", "LC_ALL"),
|
|
1184
|
+
ignore_dry_run=True,
|
|
1185
|
+
)
|
|
1186
|
+
except Exception as e:
|
|
1187
|
+
print_warn(f"拉取服务端默认配置失败,回退本地默认镜像: {e}")
|
|
1188
|
+
return None
|
|
1189
|
+
|
|
1190
|
+
|
|
1191
|
+
def _extract_bootstrap_image(bootstrap_cfg: Optional[Dict[str, Any]]) -> Optional[str]:
|
|
1192
|
+
"""从 bootstrap 配置中提取默认镜像。"""
|
|
1193
|
+
if not isinstance(bootstrap_cfg, dict):
|
|
1194
|
+
return None
|
|
1195
|
+
configs = bootstrap_cfg.get("configs")
|
|
1196
|
+
if not isinstance(configs, dict):
|
|
1197
|
+
return None
|
|
1198
|
+
value = configs.get("bootstrap.default_image") or configs.get("openclaw.default_image")
|
|
1199
|
+
if value is None:
|
|
1200
|
+
return None
|
|
1201
|
+
text = str(value).strip()
|
|
1202
|
+
return text or None
|
|
1203
|
+
|
|
1204
|
+
|
|
1205
|
+
def _print_bootstrap_hints(bootstrap_cfg: Optional[Dict[str, Any]]) -> None:
|
|
1206
|
+
"""打印升级提示和公告(如果服务端下发)。"""
|
|
1207
|
+
if not isinstance(bootstrap_cfg, dict):
|
|
1208
|
+
return
|
|
1209
|
+
|
|
1210
|
+
from ksadk.version import VERSION as CLI_VERSION
|
|
1211
|
+
|
|
1212
|
+
configs = bootstrap_cfg.get("configs")
|
|
1213
|
+
if isinstance(configs, dict):
|
|
1214
|
+
latest = str(configs.get("upgrade.latest_cli_version") or "").strip()
|
|
1215
|
+
min_required = str(configs.get("upgrade.min_cli_version") or "").strip()
|
|
1216
|
+
upgrade_msg = str(configs.get("upgrade.message") or "").strip()
|
|
1217
|
+
|
|
1218
|
+
if latest and _is_version_newer(latest, CLI_VERSION):
|
|
1219
|
+
hint = f"检测到 CLI 新版本: {latest} (当前 {CLI_VERSION})"
|
|
1220
|
+
if upgrade_msg:
|
|
1221
|
+
hint = f"{hint},{upgrade_msg}"
|
|
1222
|
+
print_warn(hint)
|
|
1223
|
+
|
|
1224
|
+
if min_required and _is_version_newer(min_required, CLI_VERSION):
|
|
1225
|
+
print_warn(
|
|
1226
|
+
f"当前 CLI 版本 {CLI_VERSION} 低于服务端建议最低版本 {min_required},"
|
|
1227
|
+
"建议尽快升级后继续使用。"
|
|
1228
|
+
)
|
|
1229
|
+
|
|
1230
|
+
notices = bootstrap_cfg.get("notices")
|
|
1231
|
+
if isinstance(notices, list):
|
|
1232
|
+
for item in notices:
|
|
1233
|
+
if not isinstance(item, dict):
|
|
1234
|
+
continue
|
|
1235
|
+
message = str(item.get("message") or "").strip()
|
|
1236
|
+
if not message:
|
|
1237
|
+
continue
|
|
1238
|
+
level = str(item.get("level") or "info").lower()
|
|
1239
|
+
if level in {"warn", "warning", "error"}:
|
|
1240
|
+
print_warn(f"平台公告: {message}")
|
|
1241
|
+
else:
|
|
1242
|
+
print_info(f"平台公告: {message}")
|
|
1243
|
+
break
|
|
1244
|
+
|
|
1245
|
+
|
|
1246
|
+
def _flatten_agent_detail(agent: dict) -> dict:
|
|
1247
|
+
"""将 GetAgent 响应转换为扁平结构,兼容旧字段和嵌套字段。"""
|
|
1248
|
+
basic = agent.get("basic", {}) if isinstance(agent, dict) else {}
|
|
1249
|
+
quick = agent.get("quick_access", {}) if isinstance(agent, dict) else {}
|
|
1250
|
+
deploy = agent.get("deployment", {}) if isinstance(agent, dict) else {}
|
|
1251
|
+
|
|
1252
|
+
return {
|
|
1253
|
+
"agent_id": basic.get("agent_id") or agent.get("agent_id") or "",
|
|
1254
|
+
"name": basic.get("name") or agent.get("name") or "",
|
|
1255
|
+
"status": (basic.get("status") or agent.get("status") or "UNKNOWN").upper(),
|
|
1256
|
+
"framework": basic.get("framework") or deploy.get("framework") or agent.get("framework") or "",
|
|
1257
|
+
"region": basic.get("region") or deploy.get("region") or agent.get("region") or "",
|
|
1258
|
+
"endpoint": quick.get("public_endpoint") or quick.get("private_endpoint") or agent.get("endpoint") or "",
|
|
1259
|
+
"artifact_path": deploy.get("artifact_path") or agent.get("artifact_path") or "",
|
|
1260
|
+
"created_at": basic.get("created_at") or agent.get("created_at") or "",
|
|
1261
|
+
"updated_at": basic.get("updated_at") or agent.get("updated_at") or "",
|
|
1262
|
+
"api_key": quick.get("api_key") or agent.get("api_key"),
|
|
1263
|
+
"langfuse_url": (agent.get("advanced") or {}).get("observability_url") or agent.get("langfuse_trace_url") or "",
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
|
|
1267
|
+
async def _get_openclaw_detail_with_client(
|
|
1268
|
+
client,
|
|
1269
|
+
agent_ref: str,
|
|
1270
|
+
*,
|
|
1271
|
+
include_api_key: bool = False,
|
|
1272
|
+
) -> dict[str, Any]:
|
|
1273
|
+
if str(agent_ref).startswith("ar-"):
|
|
1274
|
+
agent = await client.get_agent(agent_id=agent_ref, include_api_key=include_api_key)
|
|
1275
|
+
else:
|
|
1276
|
+
agent = await client.get_agent(name=agent_ref, include_api_key=include_api_key)
|
|
1277
|
+
detail = _flatten_agent_detail(agent)
|
|
1278
|
+
framework = str(detail.get("framework") or "").strip().lower()
|
|
1279
|
+
if framework and framework != "openclaw":
|
|
1280
|
+
raise resolution_error(f"目标 Agent 不是 OpenClaw: {agent_ref}", hints=["agentengine openclaw list"])
|
|
1281
|
+
return detail
|
|
1282
|
+
|
|
1283
|
+
|
|
1284
|
+
def _format_cli_timestamp(value: Optional[str], *, never_text: str = "-") -> str:
|
|
1285
|
+
raw = str(value or "").strip()
|
|
1286
|
+
if not raw:
|
|
1287
|
+
return never_text
|
|
1288
|
+
try:
|
|
1289
|
+
dt = datetime.fromisoformat(raw.replace("Z", "+00:00"))
|
|
1290
|
+
if dt.tzinfo is None:
|
|
1291
|
+
dt = dt.replace(tzinfo=timezone.utc)
|
|
1292
|
+
beijing = dt.astimezone(timezone(timedelta(hours=8)))
|
|
1293
|
+
beijing_text = beijing.strftime("%Y-%m-%d %H:%M:%S CST")
|
|
1294
|
+
utc_text = dt.astimezone(timezone.utc).strftime("%Y-%m-%d %H:%M:%S UTC")
|
|
1295
|
+
return f"{beijing_text} ({utc_text})"
|
|
1296
|
+
except Exception:
|
|
1297
|
+
return raw
|
|
1298
|
+
|
|
1299
|
+
|
|
1300
|
+
def _resolve_region(
|
|
1301
|
+
cli_region: Optional[str],
|
|
1302
|
+
state: Optional[dict],
|
|
1303
|
+
) -> str:
|
|
1304
|
+
"""解析 region: 显式参数 > state > 环境变量 > 默认值。"""
|
|
1305
|
+
return (
|
|
1306
|
+
cli_region
|
|
1307
|
+
or (state or {}).get("region")
|
|
1308
|
+
or _resolve_env("KSYUN_REGION")
|
|
1309
|
+
or "cn-beijing-6"
|
|
1310
|
+
)
|
|
1311
|
+
|
|
1312
|
+
|
|
1313
|
+
async def _resolve_openclaw_detail_or_raise(
|
|
1314
|
+
agent_ref: Optional[str],
|
|
1315
|
+
*,
|
|
1316
|
+
region: Optional[str],
|
|
1317
|
+
) -> tuple[str, dict[str, Any]]:
|
|
1318
|
+
from ksadk.api import AgentEngineClient
|
|
1319
|
+
from ksadk.deployment.state import load_state
|
|
1320
|
+
|
|
1321
|
+
state = load_state(Path(".").resolve())
|
|
1322
|
+
resolved_region = _resolve_region(region, state)
|
|
1323
|
+
resolved = resolve_openclaw_ref(agent_ref, cwd=Path(".").resolve(), include_state=True)
|
|
1324
|
+
if not resolved:
|
|
1325
|
+
raise resolution_error(
|
|
1326
|
+
OPENCLAW_RESOURCE.missing_ref_message or "请指定 OpenClaw。",
|
|
1327
|
+
hints=list(OPENCLAW_RESOURCE.resolution_commands),
|
|
1328
|
+
)
|
|
1329
|
+
|
|
1330
|
+
async with AgentEngineClient(region=resolved_region) as client:
|
|
1331
|
+
detail = await _get_openclaw_detail_with_client(client, resolved.value)
|
|
1332
|
+
|
|
1333
|
+
if not detail:
|
|
1334
|
+
raise resolution_error(f"未找到 OpenClaw: {resolved.value}", hints=["agentengine openclaw list"])
|
|
1335
|
+
return resolved_region, detail
|
|
1336
|
+
|
|
1337
|
+
|
|
1338
|
+
def _build_gateway_client(region: str, detail: dict[str, Any]) -> OpenClawGatewayClient:
|
|
1339
|
+
return OpenClawGatewayClient(
|
|
1340
|
+
region=region,
|
|
1341
|
+
agent_id=str(detail.get("agent_id") or "").strip(),
|
|
1342
|
+
agent_name=str(detail.get("name") or "").strip() or None,
|
|
1343
|
+
)
|
|
1344
|
+
|
|
1345
|
+
|
|
1346
|
+
def _gateway_status_value(detail: dict[str, Any]) -> str:
|
|
1347
|
+
return str(detail.get("status") or "UNKNOWN").upper()
|
|
1348
|
+
|
|
1349
|
+
|
|
1350
|
+
def _build_gateway_instance_check(detail: dict[str, Any]) -> dict[str, Any]:
|
|
1351
|
+
status_val = _gateway_status_value(detail)
|
|
1352
|
+
ready = status_val in OPENCLAW_GATEWAY_READY_STATUSES
|
|
1353
|
+
blocked = status_val in OPENCLAW_GATEWAY_BLOCKED_STATUSES
|
|
1354
|
+
payload: dict[str, Any] = {
|
|
1355
|
+
"name": "instance",
|
|
1356
|
+
"ok": not blocked,
|
|
1357
|
+
"status": status_val,
|
|
1358
|
+
"ready": ready,
|
|
1359
|
+
}
|
|
1360
|
+
if not blocked and not ready:
|
|
1361
|
+
payload["note"] = "控制面状态尚未进入 RUNNING,继续按网关实际连通性探测"
|
|
1362
|
+
return payload
|
|
1363
|
+
|
|
1364
|
+
|
|
1365
|
+
async def _ensure_openclaw_gateway_available(
|
|
1366
|
+
region: str,
|
|
1367
|
+
detail: dict[str, Any],
|
|
1368
|
+
*,
|
|
1369
|
+
timeout_seconds: float = 60.0,
|
|
1370
|
+
) -> None:
|
|
1371
|
+
status_val = str(detail.get("status") or "UNKNOWN").upper()
|
|
1372
|
+
if status_val in OPENCLAW_GATEWAY_BLOCKED_STATUSES:
|
|
1373
|
+
raise remote_error(
|
|
1374
|
+
f"目标 OpenClaw 当前状态为 {status_val},暂不支持 gateway/channel 操作",
|
|
1375
|
+
details={"status": status_val},
|
|
1376
|
+
)
|
|
1377
|
+
if status_val not in OPENCLAW_GATEWAY_READY_STATUSES:
|
|
1378
|
+
await _wait_for_gateway_ready(region, detail, timeout_seconds=timeout_seconds)
|
|
1379
|
+
|
|
1380
|
+
|
|
1381
|
+
def _emit_data_payload(title: str, payload: dict[str, Any], *, subtitle: Optional[str] = None) -> None:
|
|
1382
|
+
if is_json_output():
|
|
1383
|
+
emit_json(payload)
|
|
1384
|
+
return
|
|
1385
|
+
print_title(title, subtitle)
|
|
1386
|
+
console.print(json_dumps(payload), markup=False)
|
|
1387
|
+
|
|
1388
|
+
|
|
1389
|
+
def _render_openclaw_dry_run(action: str, request: dict[str, Any], hints: tuple[str, ...] = ()) -> None:
|
|
1390
|
+
if is_json_output():
|
|
1391
|
+
emit_json(
|
|
1392
|
+
build_dry_run_envelope(
|
|
1393
|
+
resource="openclaw",
|
|
1394
|
+
action=action,
|
|
1395
|
+
request=request,
|
|
1396
|
+
hints=list(hints),
|
|
1397
|
+
)
|
|
1398
|
+
)
|
|
1399
|
+
return
|
|
1400
|
+
print_title("OpenClaw Dry Run", f"action: {action}")
|
|
1401
|
+
for key, value in request.items():
|
|
1402
|
+
if isinstance(value, (list, tuple)):
|
|
1403
|
+
rendered = " ".join(str(item) for item in value) or "-"
|
|
1404
|
+
elif isinstance(value, dict):
|
|
1405
|
+
rendered = json_dumps(value)
|
|
1406
|
+
else:
|
|
1407
|
+
rendered = str(value if value is not None else "-")
|
|
1408
|
+
print_kv(key, rendered)
|
|
1409
|
+
for hint in hints:
|
|
1410
|
+
print_info(hint)
|
|
1411
|
+
|
|
1412
|
+
|
|
1413
|
+
def _build_openclaw_tui_options(
|
|
1414
|
+
*,
|
|
1415
|
+
message: str | None = None,
|
|
1416
|
+
thinking: str | None = None,
|
|
1417
|
+
history_limit: int | None = None,
|
|
1418
|
+
timeout_ms: int | None = None,
|
|
1419
|
+
deliver: bool = False,
|
|
1420
|
+
) -> dict[str, Any]:
|
|
1421
|
+
options: dict[str, Any] = {}
|
|
1422
|
+
if message:
|
|
1423
|
+
options["message"] = message
|
|
1424
|
+
if thinking:
|
|
1425
|
+
options["thinking"] = thinking
|
|
1426
|
+
if history_limit is not None:
|
|
1427
|
+
options["history_limit"] = int(history_limit)
|
|
1428
|
+
if timeout_ms is not None:
|
|
1429
|
+
options["timeout_ms"] = int(timeout_ms)
|
|
1430
|
+
if deliver:
|
|
1431
|
+
options["deliver"] = True
|
|
1432
|
+
return options
|
|
1433
|
+
|
|
1434
|
+
|
|
1435
|
+
def _select_openclaw_gateway_secret(
|
|
1436
|
+
*,
|
|
1437
|
+
gateway_token: str | None = None,
|
|
1438
|
+
gateway_password: str | None = None,
|
|
1439
|
+
state: dict[str, Any] | None = None,
|
|
1440
|
+
detail: dict[str, Any] | None = None,
|
|
1441
|
+
) -> tuple[str | None, str | None]:
|
|
1442
|
+
state = state or {}
|
|
1443
|
+
detail = detail or {}
|
|
1444
|
+
token = str(
|
|
1445
|
+
gateway_token
|
|
1446
|
+
or os.getenv("OPENCLAW_GATEWAY_TOKEN")
|
|
1447
|
+
or detail.get("openclaw_gateway_token")
|
|
1448
|
+
or state.get("openclaw_gateway_token")
|
|
1449
|
+
or ""
|
|
1450
|
+
).strip()
|
|
1451
|
+
password = str(
|
|
1452
|
+
gateway_password
|
|
1453
|
+
or os.getenv("OPENCLAW_GATEWAY_PASSWORD")
|
|
1454
|
+
or detail.get("openclaw_gateway_password")
|
|
1455
|
+
or state.get("openclaw_gateway_password")
|
|
1456
|
+
or ""
|
|
1457
|
+
).strip()
|
|
1458
|
+
if token and password and token != password:
|
|
1459
|
+
raise click.ClickException("OPENCLAW_GATEWAY_TOKEN 与 OPENCLAW_GATEWAY_PASSWORD 同时提供时必须一致")
|
|
1460
|
+
if token:
|
|
1461
|
+
return "token", token
|
|
1462
|
+
if password:
|
|
1463
|
+
return "password", password
|
|
1464
|
+
return None, None
|
|
1465
|
+
|
|
1466
|
+
|
|
1467
|
+
def _mask_openclaw_secret(secret: str | None) -> str:
|
|
1468
|
+
if not secret:
|
|
1469
|
+
return ""
|
|
1470
|
+
text = str(secret)
|
|
1471
|
+
if len(text) <= 4:
|
|
1472
|
+
return "****"
|
|
1473
|
+
return f"{text[:4]}****"
|
|
1474
|
+
|
|
1475
|
+
|
|
1476
|
+
def _openclaw_auth_mode_from_sources(detail: dict[str, Any], state: dict[str, Any] | None = None) -> str:
|
|
1477
|
+
return str(
|
|
1478
|
+
detail.get("openclaw_auth_mode")
|
|
1479
|
+
or detail.get("gateway_auth_mode")
|
|
1480
|
+
or (state or {}).get("openclaw_auth_mode")
|
|
1481
|
+
or (state or {}).get("gateway_auth_mode")
|
|
1482
|
+
or ""
|
|
1483
|
+
).strip().lower()
|
|
1484
|
+
|
|
1485
|
+
|
|
1486
|
+
def _openclaw_state_gateway_token(env_vars: dict[str, str]) -> str | None:
|
|
1487
|
+
"""返回需要持久化到本地 state 的 OpenClaw Gateway token。"""
|
|
1488
|
+
if str(env_vars.get("OPENCLAW_GATEWAY_AUTH_MODE") or "").strip().lower() != "token":
|
|
1489
|
+
return None
|
|
1490
|
+
token = str(
|
|
1491
|
+
env_vars.get("OPENCLAW_GATEWAY_TOKEN")
|
|
1492
|
+
or env_vars.get("OPENCLAW_GATEWAY_PASSWORD")
|
|
1493
|
+
or ""
|
|
1494
|
+
).strip()
|
|
1495
|
+
return token or None
|
|
1496
|
+
|
|
1497
|
+
|
|
1498
|
+
def _openclaw_runtime_endpoint(detail: dict[str, Any]) -> str:
|
|
1499
|
+
return str(detail.get("endpoint") or "").strip()
|
|
1500
|
+
|
|
1501
|
+
|
|
1502
|
+
def _openclaw_terminal_api_key(detail: dict[str, Any]) -> str | None:
|
|
1503
|
+
return str(detail.get("api_key") or "").strip() or None
|
|
1504
|
+
|
|
1505
|
+
|
|
1506
|
+
def _is_weixin_web_login_unavailable(exc: Exception) -> bool:
|
|
1507
|
+
message = str(exc).lower()
|
|
1508
|
+
return (
|
|
1509
|
+
"web login provider is not available" in message
|
|
1510
|
+
or "method not found" in message
|
|
1511
|
+
or "unknown method" in message
|
|
1512
|
+
or "unsupported method" in message
|
|
1513
|
+
)
|
|
1514
|
+
|
|
1515
|
+
|
|
1516
|
+
async def _run_weixin_remote_cli_login(
|
|
1517
|
+
region: str,
|
|
1518
|
+
detail: dict[str, Any],
|
|
1519
|
+
*,
|
|
1520
|
+
reason: str,
|
|
1521
|
+
) -> dict[str, Any]:
|
|
1522
|
+
endpoint = _openclaw_runtime_endpoint(detail)
|
|
1523
|
+
if not endpoint:
|
|
1524
|
+
raise OpenClawGatewayError("OpenClaw runtime endpoint 为空,无法通过远端 CLI 执行微信登录")
|
|
1525
|
+
|
|
1526
|
+
print_info(f"当前 OpenClaw 未暴露微信 web login RPC,改用远端 OpenClaw CLI 登录流程({reason})")
|
|
1527
|
+
exit_code = await run_terminal_session(
|
|
1528
|
+
endpoint=endpoint,
|
|
1529
|
+
api_key=_openclaw_terminal_api_key(detail),
|
|
1530
|
+
mode="exec",
|
|
1531
|
+
argv=WEIXIN_REMOTE_LOGIN_ARGV,
|
|
1532
|
+
exec_policy=OPENCLAW_TERMINAL_EXEC_POLICY,
|
|
1533
|
+
)
|
|
1534
|
+
if exit_code:
|
|
1535
|
+
raise OpenClawGatewayError(f"微信远端登录流程执行失败,exit_code={exit_code}")
|
|
1536
|
+
|
|
1537
|
+
snapshot = await _fetch_channel_snapshot_with_retry(
|
|
1538
|
+
region,
|
|
1539
|
+
detail,
|
|
1540
|
+
probe=True,
|
|
1541
|
+
)
|
|
1542
|
+
return {
|
|
1543
|
+
"ok": True,
|
|
1544
|
+
"agent_id": detail.get("agent_id"),
|
|
1545
|
+
"name": detail.get("name"),
|
|
1546
|
+
"region": region,
|
|
1547
|
+
"channel": "weixin",
|
|
1548
|
+
"mode": "remote_cli",
|
|
1549
|
+
"reason": reason,
|
|
1550
|
+
"argv": list(WEIXIN_REMOTE_LOGIN_ARGV),
|
|
1551
|
+
"status": _extract_channel_snapshot(snapshot, "weixin"),
|
|
1552
|
+
}
|
|
1553
|
+
|
|
1554
|
+
|
|
1555
|
+
async def _wait_for_gateway_ready(
|
|
1556
|
+
region: str,
|
|
1557
|
+
detail: dict[str, Any],
|
|
1558
|
+
*,
|
|
1559
|
+
timeout_seconds: float = 60.0,
|
|
1560
|
+
interval_seconds: float = 2.0,
|
|
1561
|
+
) -> None:
|
|
1562
|
+
deadline = time.monotonic() + timeout_seconds
|
|
1563
|
+
last_error: Optional[Exception] = None
|
|
1564
|
+
while time.monotonic() < deadline:
|
|
1565
|
+
gateway = _build_gateway_client(region, detail)
|
|
1566
|
+
try:
|
|
1567
|
+
await gateway.connect()
|
|
1568
|
+
return
|
|
1569
|
+
except Exception as exc:
|
|
1570
|
+
last_error = exc
|
|
1571
|
+
finally:
|
|
1572
|
+
await gateway.close()
|
|
1573
|
+
await asyncio.sleep(interval_seconds)
|
|
1574
|
+
|
|
1575
|
+
message = f"gateway 在 {int(timeout_seconds)} 秒内未恢复可用"
|
|
1576
|
+
if last_error:
|
|
1577
|
+
message = f"{message}: {last_error}"
|
|
1578
|
+
raise OpenClawGatewayError(message)
|
|
1579
|
+
|
|
1580
|
+
|
|
1581
|
+
async def _wait_for_gateway_reload_after_config_apply(
|
|
1582
|
+
gateway: OpenClawGatewayClient,
|
|
1583
|
+
region: str,
|
|
1584
|
+
detail: dict[str, Any],
|
|
1585
|
+
*,
|
|
1586
|
+
disconnect_timeout_seconds: float = 5.0,
|
|
1587
|
+
ready_timeout_seconds: float = 90.0,
|
|
1588
|
+
) -> None:
|
|
1589
|
+
await gateway.wait_for_disconnect(timeout_ms=int(disconnect_timeout_seconds * 1000))
|
|
1590
|
+
await _wait_for_gateway_ready(region, detail, timeout_seconds=ready_timeout_seconds)
|
|
1591
|
+
|
|
1592
|
+
|
|
1593
|
+
def _is_gateway_reload_disconnect_error(exc: Exception) -> bool:
|
|
1594
|
+
message = str(exc).lower()
|
|
1595
|
+
return (
|
|
1596
|
+
"websocket receive failed" in message
|
|
1597
|
+
and ("1011" in message or "bad gateway" in message or "going away" in message)
|
|
1598
|
+
)
|
|
1599
|
+
|
|
1600
|
+
|
|
1601
|
+
async def _config_apply_and_wait_for_reload(
|
|
1602
|
+
gateway: OpenClawGatewayClient,
|
|
1603
|
+
region: str,
|
|
1604
|
+
detail: dict[str, Any],
|
|
1605
|
+
*,
|
|
1606
|
+
config: dict[str, Any],
|
|
1607
|
+
base_hash: str,
|
|
1608
|
+
note: str,
|
|
1609
|
+
) -> None:
|
|
1610
|
+
try:
|
|
1611
|
+
await gateway.config_apply(config=config, base_hash=base_hash, note=note)
|
|
1612
|
+
except OpenClawGatewayRequestError:
|
|
1613
|
+
raise
|
|
1614
|
+
except OpenClawGatewayError as exc:
|
|
1615
|
+
if not _is_gateway_reload_disconnect_error(exc):
|
|
1616
|
+
raise
|
|
1617
|
+
print_warn("配置已提交,gateway reload 期间连接短暂中断;等待恢复后继续验证")
|
|
1618
|
+
await _wait_for_gateway_ready(region, detail, timeout_seconds=90.0)
|
|
1619
|
+
return
|
|
1620
|
+
await _wait_for_gateway_reload_after_config_apply(gateway, region, detail)
|
|
1621
|
+
|
|
1622
|
+
|
|
1623
|
+
async def _fetch_channel_snapshot_with_retry(
|
|
1624
|
+
region: str,
|
|
1625
|
+
detail: dict[str, Any],
|
|
1626
|
+
*,
|
|
1627
|
+
probe: bool,
|
|
1628
|
+
timeout_seconds: float = 30.0,
|
|
1629
|
+
interval_seconds: float = 2.0,
|
|
1630
|
+
) -> dict[str, Any]:
|
|
1631
|
+
deadline = time.monotonic() + timeout_seconds
|
|
1632
|
+
last_error: Optional[Exception] = None
|
|
1633
|
+
while time.monotonic() < deadline:
|
|
1634
|
+
gateway = _build_gateway_client(region, detail)
|
|
1635
|
+
try:
|
|
1636
|
+
await gateway.connect()
|
|
1637
|
+
return await gateway.channels_status(probe=probe, timeout_ms=8_000 if probe else None)
|
|
1638
|
+
except Exception as exc:
|
|
1639
|
+
last_error = exc
|
|
1640
|
+
finally:
|
|
1641
|
+
await gateway.close()
|
|
1642
|
+
await asyncio.sleep(interval_seconds)
|
|
1643
|
+
|
|
1644
|
+
message = f"channel 状态在 {int(timeout_seconds)} 秒内未恢复可用"
|
|
1645
|
+
if last_error:
|
|
1646
|
+
message = f"{message}: {last_error}"
|
|
1647
|
+
raise OpenClawGatewayError(message)
|
|
1648
|
+
|
|
1649
|
+
|
|
1650
|
+
def _channel_aliases(channel: str) -> tuple[str, ...]:
|
|
1651
|
+
spec = OPENCLAW_CHANNEL_SPECS.get(channel) or {}
|
|
1652
|
+
aliases = [channel]
|
|
1653
|
+
for candidate in (spec.get("channel_key"), spec.get("plugin_id")):
|
|
1654
|
+
if candidate and candidate not in aliases:
|
|
1655
|
+
aliases.append(candidate)
|
|
1656
|
+
return tuple(aliases)
|
|
1657
|
+
|
|
1658
|
+
|
|
1659
|
+
def _channel_config_section(config: dict[str, Any], channel: str) -> dict[str, Any]:
|
|
1660
|
+
channels = config.get("channels") if isinstance(config.get("channels"), dict) else {}
|
|
1661
|
+
if not isinstance(channels, dict):
|
|
1662
|
+
return {}
|
|
1663
|
+
spec = OPENCLAW_CHANNEL_SPECS.get(channel) or {}
|
|
1664
|
+
section = channels.get(spec.get("channel_key") or channel)
|
|
1665
|
+
return section if isinstance(section, dict) else {}
|
|
1666
|
+
|
|
1667
|
+
|
|
1668
|
+
def _channel_default_account_id(channel: str) -> str:
|
|
1669
|
+
spec = OPENCLAW_CHANNEL_SPECS.get(channel) or {}
|
|
1670
|
+
return str(spec.get("default_account_id") or "default")
|
|
1671
|
+
|
|
1672
|
+
|
|
1673
|
+
def _is_channel_configured(
|
|
1674
|
+
channel: str,
|
|
1675
|
+
*,
|
|
1676
|
+
config: dict[str, Any],
|
|
1677
|
+
snapshot: Any,
|
|
1678
|
+
) -> bool:
|
|
1679
|
+
if isinstance(snapshot, dict) and isinstance(snapshot.get("configured"), bool):
|
|
1680
|
+
return bool(snapshot.get("configured"))
|
|
1681
|
+
|
|
1682
|
+
channel_cfg = _channel_config_section(config, channel)
|
|
1683
|
+
if channel == "weixin":
|
|
1684
|
+
accounts = channel_cfg.get("accounts")
|
|
1685
|
+
return isinstance(accounts, dict) and any(str(key).strip() for key in accounts.keys())
|
|
1686
|
+
if channel == "feishu":
|
|
1687
|
+
return bool(str(channel_cfg.get("appId") or "").strip() and str(channel_cfg.get("appSecret") or "").strip())
|
|
1688
|
+
if channel == "wps-xiezuo":
|
|
1689
|
+
return bool(str(channel_cfg.get("appId") or "").strip() and str(channel_cfg.get("appSecret") or "").strip())
|
|
1690
|
+
return bool(channel_cfg)
|
|
1691
|
+
|
|
1692
|
+
|
|
1693
|
+
def _build_channel_doctor_availability_check(
|
|
1694
|
+
*,
|
|
1695
|
+
name: str,
|
|
1696
|
+
available: bool,
|
|
1697
|
+
configured: bool,
|
|
1698
|
+
connect_required_message: str,
|
|
1699
|
+
connect_required_ok: bool = True,
|
|
1700
|
+
**extra: Any,
|
|
1701
|
+
) -> dict[str, Any]:
|
|
1702
|
+
if available:
|
|
1703
|
+
state = "ready"
|
|
1704
|
+
ok = True
|
|
1705
|
+
message = None
|
|
1706
|
+
elif configured:
|
|
1707
|
+
state = "missing"
|
|
1708
|
+
ok = False
|
|
1709
|
+
message = None
|
|
1710
|
+
else:
|
|
1711
|
+
state = "connect_required"
|
|
1712
|
+
ok = connect_required_ok
|
|
1713
|
+
message = connect_required_message
|
|
1714
|
+
|
|
1715
|
+
payload = {
|
|
1716
|
+
"name": name,
|
|
1717
|
+
"ok": ok,
|
|
1718
|
+
"configured": configured,
|
|
1719
|
+
"state": state,
|
|
1720
|
+
**extra,
|
|
1721
|
+
}
|
|
1722
|
+
if message:
|
|
1723
|
+
payload["message"] = message
|
|
1724
|
+
return payload
|
|
1725
|
+
|
|
1726
|
+
|
|
1727
|
+
def _extract_channel_snapshot(snapshot: Any, channel: Optional[str]) -> Any:
|
|
1728
|
+
if channel is None:
|
|
1729
|
+
return snapshot
|
|
1730
|
+
aliases = set(_channel_aliases(channel))
|
|
1731
|
+
if isinstance(snapshot, dict):
|
|
1732
|
+
for key in aliases:
|
|
1733
|
+
if key in snapshot:
|
|
1734
|
+
return snapshot[key]
|
|
1735
|
+
channels = snapshot.get("channels")
|
|
1736
|
+
if isinstance(channels, dict):
|
|
1737
|
+
for key in aliases:
|
|
1738
|
+
if key in channels:
|
|
1739
|
+
return channels[key]
|
|
1740
|
+
if isinstance(channels, list):
|
|
1741
|
+
for item in channels:
|
|
1742
|
+
if not isinstance(item, dict):
|
|
1743
|
+
continue
|
|
1744
|
+
candidates = {
|
|
1745
|
+
str(item.get("id") or "").strip(),
|
|
1746
|
+
str(item.get("channel") or "").strip(),
|
|
1747
|
+
str(item.get("name") or "").strip(),
|
|
1748
|
+
str(item.get("pluginId") or "").strip(),
|
|
1749
|
+
}
|
|
1750
|
+
if candidates & aliases:
|
|
1751
|
+
return item
|
|
1752
|
+
return None
|
|
1753
|
+
|
|
1754
|
+
|
|
1755
|
+
def _parse_time_arg(raw: Optional[str]) -> Optional[int]:
|
|
1756
|
+
text = str(raw or "").strip()
|
|
1757
|
+
if not text:
|
|
1758
|
+
return None
|
|
1759
|
+
if text.isdigit():
|
|
1760
|
+
return int(text)
|
|
1761
|
+
normalized = text.replace("Z", "+00:00")
|
|
1762
|
+
try:
|
|
1763
|
+
parsed = datetime.fromisoformat(normalized)
|
|
1764
|
+
except ValueError as exc:
|
|
1765
|
+
raise click.BadParameter("时间格式必须是 Unix 毫秒时间戳或 ISO-8601") from exc
|
|
1766
|
+
if parsed.tzinfo is None:
|
|
1767
|
+
parsed = parsed.replace(tzinfo=timezone.utc)
|
|
1768
|
+
return int(parsed.timestamp() * 1000)
|
|
1769
|
+
|
|
1770
|
+
|
|
1771
|
+
def _render_terminal_qr(qr_url: str) -> None:
|
|
1772
|
+
if not is_stdout_tty():
|
|
1773
|
+
return
|
|
1774
|
+
try:
|
|
1775
|
+
import qrcode
|
|
1776
|
+
except Exception:
|
|
1777
|
+
print_warn("未检测到 Python `qrcode` 依赖,已退化为仅输出二维码 URL")
|
|
1778
|
+
return
|
|
1779
|
+
|
|
1780
|
+
qr = qrcode.QRCode(border=1)
|
|
1781
|
+
qr.add_data(qr_url)
|
|
1782
|
+
qr.make(fit=True)
|
|
1783
|
+
out = io.StringIO()
|
|
1784
|
+
qr.print_ascii(out=out, tty=False)
|
|
1785
|
+
console.print(out.getvalue(), markup=False)
|
|
1786
|
+
|
|
1787
|
+
|
|
1788
|
+
def _ensure_local_node_tools() -> dict[str, str]:
|
|
1789
|
+
node_path = shutil.which("node")
|
|
1790
|
+
npx_path = shutil.which("npx")
|
|
1791
|
+
if not node_path or not npx_path:
|
|
1792
|
+
raise resolution_error(
|
|
1793
|
+
"本地缺少 `node` 或 `npx`,飞书接入依赖官方 onboarding 工具",
|
|
1794
|
+
hints=["先安装 Node.js,然后重试 `agentengine openclaw channel connect --channel feishu`"],
|
|
1795
|
+
)
|
|
1796
|
+
return {"node": node_path, "npx": npx_path}
|
|
1797
|
+
|
|
1798
|
+
|
|
1799
|
+
async def _resolve_npx_cached_package_file(package_spec: str, relative_path: str) -> Path:
|
|
1800
|
+
tools = _ensure_local_node_tools()
|
|
1801
|
+
package_name = package_spec.strip()
|
|
1802
|
+
if package_name.startswith("@"):
|
|
1803
|
+
slash_idx = package_name.find("/")
|
|
1804
|
+
version_idx = package_name.rfind("@")
|
|
1805
|
+
if slash_idx > 0 and version_idx > slash_idx:
|
|
1806
|
+
package_name = package_name[:version_idx]
|
|
1807
|
+
elif "@" in package_name:
|
|
1808
|
+
package_name = package_name.split("@", 1)[0]
|
|
1809
|
+
warmup_cmd = [
|
|
1810
|
+
tools["npx"],
|
|
1811
|
+
"-y",
|
|
1812
|
+
"--package",
|
|
1813
|
+
package_spec,
|
|
1814
|
+
"-c",
|
|
1815
|
+
"true",
|
|
1816
|
+
]
|
|
1817
|
+
completed = await asyncio.to_thread(
|
|
1818
|
+
subprocess.run,
|
|
1819
|
+
warmup_cmd,
|
|
1820
|
+
check=False,
|
|
1821
|
+
capture_output=True,
|
|
1822
|
+
text=True,
|
|
1823
|
+
)
|
|
1824
|
+
if completed.returncode != 0:
|
|
1825
|
+
raise OpenClawGatewayError(
|
|
1826
|
+
f"无法准备官方 npm 包缓存: {completed.stderr.strip() or completed.stdout.strip() or package_spec}"
|
|
1827
|
+
)
|
|
1828
|
+
|
|
1829
|
+
npm_cache_root = Path(
|
|
1830
|
+
os.getenv("NPM_CONFIG_CACHE")
|
|
1831
|
+
or os.getenv("npm_config_cache")
|
|
1832
|
+
or str(Path.home() / ".npm")
|
|
1833
|
+
)
|
|
1834
|
+
pattern = f"_npx/**/node_modules/{package_name}/{relative_path}"
|
|
1835
|
+
matches = list(npm_cache_root.glob(pattern))
|
|
1836
|
+
if not matches:
|
|
1837
|
+
raise OpenClawGatewayError(f"未找到官方 npm 包缓存文件: {package_name}/{relative_path}")
|
|
1838
|
+
return max(matches, key=lambda item: item.stat().st_mtime)
|
|
1839
|
+
|
|
1840
|
+
|
|
1841
|
+
async def _run_feishu_onboarding(existing_app_id: Optional[str]) -> dict[str, Any]:
|
|
1842
|
+
tools = _ensure_local_node_tools()
|
|
1843
|
+
script_path = ""
|
|
1844
|
+
result_path = ""
|
|
1845
|
+
try:
|
|
1846
|
+
install_prompts_path = await _resolve_npx_cached_package_file(
|
|
1847
|
+
"@larksuite/openclaw-lark-tools@latest",
|
|
1848
|
+
"dist/utils/install-prompts.js",
|
|
1849
|
+
)
|
|
1850
|
+
script_body = textwrap.dedent(
|
|
1851
|
+
"""
|
|
1852
|
+
const fs = require("fs");
|
|
1853
|
+
const { runInstallAuthFlow } = require(__INSTALL_PROMPTS_PATH__);
|
|
1854
|
+
|
|
1855
|
+
(async () => {
|
|
1856
|
+
try {
|
|
1857
|
+
const result = await runInstallAuthFlow(
|
|
1858
|
+
process.env.KSADK_FEISHU_APP_ID || undefined,
|
|
1859
|
+
undefined,
|
|
1860
|
+
{},
|
|
1861
|
+
false,
|
|
1862
|
+
);
|
|
1863
|
+
fs.writeFileSync(process.env.KSADK_FEISHU_RESULT_PATH, JSON.stringify(result), "utf8");
|
|
1864
|
+
} catch (error) {
|
|
1865
|
+
console.error(error);
|
|
1866
|
+
process.exit(1);
|
|
1867
|
+
}
|
|
1868
|
+
})();
|
|
1869
|
+
"""
|
|
1870
|
+
).replace("__INSTALL_PROMPTS_PATH__", json.dumps(str(install_prompts_path)))
|
|
1871
|
+
with tempfile.NamedTemporaryFile("w", suffix=".cjs", delete=False, encoding="utf-8") as script_file:
|
|
1872
|
+
script_file.write(script_body.strip())
|
|
1873
|
+
script_path = script_file.name
|
|
1874
|
+
|
|
1875
|
+
with tempfile.NamedTemporaryFile("w", suffix=".json", delete=False, encoding="utf-8") as result_file:
|
|
1876
|
+
result_path = result_file.name
|
|
1877
|
+
|
|
1878
|
+
env = os.environ.copy()
|
|
1879
|
+
if existing_app_id:
|
|
1880
|
+
env["KSADK_FEISHU_APP_ID"] = existing_app_id
|
|
1881
|
+
env["KSADK_FEISHU_RESULT_PATH"] = result_path
|
|
1882
|
+
cmd = [tools["node"], script_path]
|
|
1883
|
+
completed = await asyncio.to_thread(subprocess.run, cmd, env=env, check=False)
|
|
1884
|
+
if completed.returncode != 0:
|
|
1885
|
+
raise OpenClawGatewayError("飞书官方 onboarding 流程执行失败")
|
|
1886
|
+
raw = Path(result_path).read_text(encoding="utf-8").strip()
|
|
1887
|
+
if not raw:
|
|
1888
|
+
raise OpenClawGatewayError("飞书官方 onboarding 未返回结果")
|
|
1889
|
+
result = json.loads(raw)
|
|
1890
|
+
if not isinstance(result, dict):
|
|
1891
|
+
raise OpenClawGatewayError("飞书官方 onboarding 返回了无效结果")
|
|
1892
|
+
return result
|
|
1893
|
+
finally:
|
|
1894
|
+
for temp_path in (script_path, result_path):
|
|
1895
|
+
if temp_path and Path(temp_path).exists():
|
|
1896
|
+
try:
|
|
1897
|
+
Path(temp_path).unlink()
|
|
1898
|
+
except Exception:
|
|
1899
|
+
pass
|
|
1900
|
+
|
|
1901
|
+
|
|
1902
|
+
def _ensure_plugin_enabled(config: dict[str, Any], plugin_id: str) -> bool:
|
|
1903
|
+
changed = False
|
|
1904
|
+
plugins = config.setdefault("plugins", {})
|
|
1905
|
+
allow = plugins.setdefault("allow", [])
|
|
1906
|
+
if plugin_id not in allow:
|
|
1907
|
+
allow.append(plugin_id)
|
|
1908
|
+
changed = True
|
|
1909
|
+
entries = plugins.setdefault("entries", {})
|
|
1910
|
+
entry = entries.setdefault(plugin_id, {})
|
|
1911
|
+
if entry.get("enabled") is not True:
|
|
1912
|
+
entry["enabled"] = True
|
|
1913
|
+
changed = True
|
|
1914
|
+
return changed
|
|
1915
|
+
|
|
1916
|
+
|
|
1917
|
+
def _extract_config_state(snapshot: dict[str, Any]) -> tuple[dict[str, Any], str]:
|
|
1918
|
+
config = snapshot.get("config")
|
|
1919
|
+
if not isinstance(config, dict):
|
|
1920
|
+
raise OpenClawGatewayError("config.get 未返回可编辑的配置快照")
|
|
1921
|
+
base_hash = str(snapshot.get("hash") or "").strip()
|
|
1922
|
+
if not base_hash and snapshot.get("exists", True):
|
|
1923
|
+
raise OpenClawGatewayError("config.get 未返回 base hash,请稍后重试")
|
|
1924
|
+
return copy.deepcopy(config), base_hash
|
|
1925
|
+
|
|
1926
|
+
|
|
1927
|
+
def _resolve_weixin_account_id(
|
|
1928
|
+
config: dict[str, Any],
|
|
1929
|
+
*,
|
|
1930
|
+
account_id: Optional[str],
|
|
1931
|
+
create_if_missing: bool,
|
|
1932
|
+
) -> str:
|
|
1933
|
+
if account_id:
|
|
1934
|
+
return str(account_id).strip()
|
|
1935
|
+
channels = config.get("channels") or {}
|
|
1936
|
+
weixin_cfg = channels.get(WEIXIN_PLUGIN_ID) or {}
|
|
1937
|
+
accounts = weixin_cfg.get("accounts")
|
|
1938
|
+
if isinstance(accounts, dict):
|
|
1939
|
+
account_keys = [str(key).strip() for key in accounts.keys() if str(key).strip()]
|
|
1940
|
+
if "default" in account_keys:
|
|
1941
|
+
return "default"
|
|
1942
|
+
if len(account_keys) == 1:
|
|
1943
|
+
return account_keys[0]
|
|
1944
|
+
if len(account_keys) > 1:
|
|
1945
|
+
raise click.ClickException("检测到多个微信账号,请显式传入 --account-id")
|
|
1946
|
+
if create_if_missing:
|
|
1947
|
+
return "default"
|
|
1948
|
+
raise click.ClickException("尚未检测到微信账号,请先执行 `agentengine openclaw channel connect --channel weixin`")
|
|
1949
|
+
|
|
1950
|
+
|
|
1951
|
+
def _mutate_weixin_account_enabled(
|
|
1952
|
+
config: dict[str, Any],
|
|
1953
|
+
*,
|
|
1954
|
+
enabled: bool,
|
|
1955
|
+
account_id: Optional[str],
|
|
1956
|
+
) -> tuple[bool, str]:
|
|
1957
|
+
changed = _ensure_plugin_enabled(config, WEIXIN_PLUGIN_ID) if enabled else False
|
|
1958
|
+
channels = config.setdefault("channels", {})
|
|
1959
|
+
weixin_cfg = channels.setdefault(WEIXIN_PLUGIN_ID, {})
|
|
1960
|
+
accounts = weixin_cfg.setdefault("accounts", {})
|
|
1961
|
+
resolved_account_id = _resolve_weixin_account_id(
|
|
1962
|
+
config,
|
|
1963
|
+
account_id=account_id,
|
|
1964
|
+
create_if_missing=enabled,
|
|
1965
|
+
)
|
|
1966
|
+
account_cfg = accounts.setdefault(resolved_account_id, {})
|
|
1967
|
+
if account_cfg.get("enabled") is not enabled:
|
|
1968
|
+
account_cfg["enabled"] = enabled
|
|
1969
|
+
changed = True
|
|
1970
|
+
return changed, resolved_account_id
|
|
1971
|
+
|
|
1972
|
+
|
|
1973
|
+
def _mutate_feishu_enabled(
|
|
1974
|
+
config: dict[str, Any],
|
|
1975
|
+
*,
|
|
1976
|
+
enabled: bool,
|
|
1977
|
+
account_id: Optional[str],
|
|
1978
|
+
) -> bool:
|
|
1979
|
+
normalized_account = str(account_id or "default").strip()
|
|
1980
|
+
if normalized_account not in {"", "default"}:
|
|
1981
|
+
raise click.ClickException("V1 仅支持飞书默认账号,`--account-id` 仅支持 default")
|
|
1982
|
+
changed = _ensure_plugin_enabled(config, FEISHU_PLUGIN_ID) if enabled else False
|
|
1983
|
+
channels = config.setdefault("channels", {})
|
|
1984
|
+
feishu_cfg = channels.setdefault(FEISHU_CHANNEL_KEY, {})
|
|
1985
|
+
if feishu_cfg.get("enabled") is not enabled:
|
|
1986
|
+
feishu_cfg["enabled"] = enabled
|
|
1987
|
+
changed = True
|
|
1988
|
+
return changed
|
|
1989
|
+
|
|
1990
|
+
|
|
1991
|
+
def _mutate_feishu_connect_config(config: dict[str, Any], onboarding: dict[str, Any]) -> bool:
|
|
1992
|
+
changed = _ensure_plugin_enabled(config, FEISHU_PLUGIN_ID)
|
|
1993
|
+
channels = config.setdefault("channels", {})
|
|
1994
|
+
feishu_cfg = channels.setdefault(FEISHU_CHANNEL_KEY, {})
|
|
1995
|
+
|
|
1996
|
+
desired_pairs = {
|
|
1997
|
+
"enabled": True,
|
|
1998
|
+
"appId": str(onboarding.get("appId") or "").strip(),
|
|
1999
|
+
"appSecret": str(onboarding.get("appSecret") or "").strip(),
|
|
2000
|
+
"domain": str(onboarding.get("domain") or "feishu").strip() or "feishu",
|
|
2001
|
+
}
|
|
2002
|
+
if not desired_pairs["appId"] or not desired_pairs["appSecret"]:
|
|
2003
|
+
raise OpenClawGatewayError("飞书 onboarding 结果缺少 appId/appSecret")
|
|
2004
|
+
|
|
2005
|
+
defaults = {
|
|
2006
|
+
"connectionMode": "websocket",
|
|
2007
|
+
"requireMention": True,
|
|
2008
|
+
}
|
|
2009
|
+
for key, value in {**defaults, **desired_pairs}.items():
|
|
2010
|
+
if feishu_cfg.get(key) != value:
|
|
2011
|
+
feishu_cfg[key] = value
|
|
2012
|
+
changed = True
|
|
2013
|
+
|
|
2014
|
+
user_info = onboarding.get("userInfo") if isinstance(onboarding.get("userInfo"), dict) else {}
|
|
2015
|
+
open_id = str(user_info.get("openId") or "").strip()
|
|
2016
|
+
if open_id:
|
|
2017
|
+
if feishu_cfg.get("dmPolicy") != "allowlist":
|
|
2018
|
+
feishu_cfg["dmPolicy"] = "allowlist"
|
|
2019
|
+
changed = True
|
|
2020
|
+
allow_from = feishu_cfg.setdefault("allowFrom", [])
|
|
2021
|
+
if open_id not in allow_from:
|
|
2022
|
+
allow_from.append(open_id)
|
|
2023
|
+
changed = True
|
|
2024
|
+
if feishu_cfg.get("groupPolicy") != "allowlist":
|
|
2025
|
+
feishu_cfg["groupPolicy"] = "allowlist"
|
|
2026
|
+
changed = True
|
|
2027
|
+
group_allow_from = feishu_cfg.setdefault("groupAllowFrom", [])
|
|
2028
|
+
if open_id not in group_allow_from:
|
|
2029
|
+
group_allow_from.append(open_id)
|
|
2030
|
+
changed = True
|
|
2031
|
+
groups = feishu_cfg.get("groups")
|
|
2032
|
+
if not isinstance(groups, dict) or "*" not in groups:
|
|
2033
|
+
feishu_cfg["groups"] = {"*": {"enabled": True}}
|
|
2034
|
+
changed = True
|
|
2035
|
+
else:
|
|
2036
|
+
if feishu_cfg.get("dmPolicy") not in {"pairing", "allowlist", "open"}:
|
|
2037
|
+
feishu_cfg["dmPolicy"] = "pairing"
|
|
2038
|
+
changed = True
|
|
2039
|
+
if not feishu_cfg.get("groupPolicy"):
|
|
2040
|
+
feishu_cfg["groupPolicy"] = "open"
|
|
2041
|
+
changed = True
|
|
2042
|
+
|
|
2043
|
+
return changed
|
|
2044
|
+
|
|
2045
|
+
|
|
2046
|
+
def _should_auto_open_browser() -> bool:
|
|
2047
|
+
if is_json_output():
|
|
2048
|
+
return False
|
|
2049
|
+
if str(os.getenv("SSH_TTY") or os.getenv("SSH_CONNECTION") or "").strip():
|
|
2050
|
+
return False
|
|
2051
|
+
if sys.platform == "darwin" or os.name == "nt":
|
|
2052
|
+
return True
|
|
2053
|
+
return bool(
|
|
2054
|
+
str(os.getenv("DISPLAY") or "").strip()
|
|
2055
|
+
or str(os.getenv("WAYLAND_DISPLAY") or "").strip()
|
|
2056
|
+
or str(os.getenv("BROWSER") or "").strip()
|
|
2057
|
+
)
|
|
2058
|
+
|
|
2059
|
+
|
|
2060
|
+
def _mutate_wps_xiezuo_connect_config(
|
|
2061
|
+
config: dict[str, Any],
|
|
2062
|
+
*,
|
|
2063
|
+
app_id: str,
|
|
2064
|
+
app_secret: str,
|
|
2065
|
+
account_id: str,
|
|
2066
|
+
agent_id: str,
|
|
2067
|
+
dm_policy: str,
|
|
2068
|
+
group_policy: str,
|
|
2069
|
+
base_url: str,
|
|
2070
|
+
) -> bool:
|
|
2071
|
+
changed = _ensure_plugin_enabled(config, WPS_XIEZUO_PLUGIN_ID)
|
|
2072
|
+
channels = config.setdefault("channels", {})
|
|
2073
|
+
channel_cfg = channels.setdefault(WPS_XIEZUO_CHANNEL_KEY, {})
|
|
2074
|
+
channel_cfg.pop("accounts", None)
|
|
2075
|
+
channel_cfg.pop("defaultAccountId", None)
|
|
2076
|
+
desired_channel = {
|
|
2077
|
+
"enabled": True,
|
|
2078
|
+
"appId": app_id,
|
|
2079
|
+
"appSecret": app_secret,
|
|
2080
|
+
"baseUrl": base_url,
|
|
2081
|
+
"sdk": {
|
|
2082
|
+
"enabled": True,
|
|
2083
|
+
"logLevel": "info",
|
|
2084
|
+
},
|
|
2085
|
+
"dmPolicy": dm_policy,
|
|
2086
|
+
"allowFrom": ["*"] if dm_policy == "open" else [],
|
|
2087
|
+
"groupPolicy": group_policy,
|
|
2088
|
+
"instantAck": {
|
|
2089
|
+
"enabled": True,
|
|
2090
|
+
"text": "内容处理中,请稍候...",
|
|
2091
|
+
},
|
|
2092
|
+
"mcp": {
|
|
2093
|
+
"enabled": True,
|
|
2094
|
+
"mode": "app",
|
|
2095
|
+
},
|
|
2096
|
+
}
|
|
2097
|
+
for key, value in desired_channel.items():
|
|
2098
|
+
if channel_cfg.get(key) != value:
|
|
2099
|
+
channel_cfg[key] = value
|
|
2100
|
+
changed = True
|
|
2101
|
+
session_cfg = config.setdefault("session", {})
|
|
2102
|
+
if session_cfg.get("dmScope") != "per-account-channel-peer":
|
|
2103
|
+
session_cfg["dmScope"] = "per-account-channel-peer"
|
|
2104
|
+
changed = True
|
|
2105
|
+
bindings = config.setdefault("bindings", [])
|
|
2106
|
+
if isinstance(bindings, list):
|
|
2107
|
+
next_bindings = [
|
|
2108
|
+
item for item in bindings
|
|
2109
|
+
if not (isinstance(item, dict) and item.get("match", {}).get("channel") == WPS_XIEZUO_CHANNEL_KEY)
|
|
2110
|
+
]
|
|
2111
|
+
desired_binding = {
|
|
2112
|
+
"type": "route",
|
|
2113
|
+
"agentId": agent_id,
|
|
2114
|
+
"match": {
|
|
2115
|
+
"channel": WPS_XIEZUO_CHANNEL_KEY,
|
|
2116
|
+
},
|
|
2117
|
+
}
|
|
2118
|
+
next_bindings.append(desired_binding)
|
|
2119
|
+
if next_bindings != bindings:
|
|
2120
|
+
config["bindings"] = next_bindings
|
|
2121
|
+
changed = True
|
|
2122
|
+
return changed
|
|
2123
|
+
|
|
2124
|
+
|
|
2125
|
+
def _mutate_wps_xiezuo_enabled(
|
|
2126
|
+
config: dict[str, Any],
|
|
2127
|
+
*,
|
|
2128
|
+
enabled: bool,
|
|
2129
|
+
account_id: Optional[str],
|
|
2130
|
+
) -> bool:
|
|
2131
|
+
normalized_account = str(account_id or WPS_XIEZUO_DEFAULT_ACCOUNT_ID).strip()
|
|
2132
|
+
if normalized_account and normalized_account != WPS_XIEZUO_DEFAULT_ACCOUNT_ID:
|
|
2133
|
+
raise click.ClickException("WPS 协作插件使用扁平 channel 配置,`--account-id` 仅支持 default")
|
|
2134
|
+
changed = _ensure_plugin_enabled(config, WPS_XIEZUO_PLUGIN_ID) if enabled else False
|
|
2135
|
+
channels = config.setdefault("channels", {})
|
|
2136
|
+
channel_cfg = channels.setdefault(WPS_XIEZUO_CHANNEL_KEY, {})
|
|
2137
|
+
channel_cfg.pop("accounts", None)
|
|
2138
|
+
channel_cfg.pop("defaultAccountId", None)
|
|
2139
|
+
if channel_cfg.get("enabled") is not enabled:
|
|
2140
|
+
channel_cfg["enabled"] = enabled
|
|
2141
|
+
changed = True
|
|
2142
|
+
return changed
|
|
2143
|
+
|
|
2144
|
+
|
|
2145
|
+
def _check_wps_xiezuo_local_deps() -> dict[str, Any]:
|
|
2146
|
+
node_path = shutil.which("node")
|
|
2147
|
+
npm_path = shutil.which("npm")
|
|
2148
|
+
return {
|
|
2149
|
+
"ok": bool(node_path and npm_path),
|
|
2150
|
+
"node": node_path,
|
|
2151
|
+
"npm": npm_path,
|
|
2152
|
+
}
|
|
2153
|
+
|
|
2154
|
+
|
|
2155
|
+
@click.group("openclaw", context_settings=CONTEXT_SETTINGS, help=build_resource_group_help(OPENCLAW_RESOURCE))
|
|
2156
|
+
def openclaw():
|
|
2157
|
+
pass
|
|
2158
|
+
|
|
2159
|
+
|
|
2160
|
+
@openclaw.group(
|
|
2161
|
+
"gateway",
|
|
2162
|
+
context_settings=CONTEXT_SETTINGS,
|
|
2163
|
+
help="OpenClaw gateway 入口、日志与诊断。",
|
|
2164
|
+
)
|
|
2165
|
+
def openclaw_gateway():
|
|
2166
|
+
pass
|
|
2167
|
+
|
|
2168
|
+
|
|
2169
|
+
async def _run_openclaw_repair_action(
|
|
2170
|
+
agent_ref: Optional[str],
|
|
2171
|
+
*,
|
|
2172
|
+
region: Optional[str],
|
|
2173
|
+
repair_action: str = "doctor-fix",
|
|
2174
|
+
) -> dict[str, Any]:
|
|
2175
|
+
from ksadk.api import AgentEngineClient
|
|
2176
|
+
|
|
2177
|
+
resolved_region, detail = await _resolve_openclaw_detail_or_raise(agent_ref, region=region)
|
|
2178
|
+
async with AgentEngineClient(region=resolved_region) as client:
|
|
2179
|
+
repair_payload = await client.run_openclaw_repair(
|
|
2180
|
+
str(detail.get("agent_id") or ""),
|
|
2181
|
+
repair_action=repair_action,
|
|
2182
|
+
)
|
|
2183
|
+
|
|
2184
|
+
return {
|
|
2185
|
+
"ok": bool(repair_payload.get("ok", False)),
|
|
2186
|
+
"agent_id": detail.get("agent_id"),
|
|
2187
|
+
"name": detail.get("name"),
|
|
2188
|
+
"region": resolved_region,
|
|
2189
|
+
"current_status": detail.get("status"),
|
|
2190
|
+
**repair_payload,
|
|
2191
|
+
}
|
|
2192
|
+
|
|
2193
|
+
|
|
2194
|
+
@openclaw.command("tui", context_settings=CONTEXT_SETTINGS)
|
|
2195
|
+
@click.argument("agent_ref", required=False)
|
|
2196
|
+
@region_option(default=None, envvar=None, help_text="区域 (默认优先读取 .agentengine.state)")
|
|
2197
|
+
@click.option("--endpoint", "-e", default=None, help="OpenClaw Endpoint URL (覆盖自动获取)")
|
|
2198
|
+
@click.option("--api-key", default=None, help="AgentEngine API Key (trusted-proxy/none 场景可用)")
|
|
2199
|
+
@click.option(
|
|
2200
|
+
"--gateway-token",
|
|
2201
|
+
envvar="OPENCLAW_GATEWAY_TOKEN",
|
|
2202
|
+
default=None,
|
|
2203
|
+
help="OpenClaw Gateway token(token 模式必需,不是 ak-*)",
|
|
2204
|
+
)
|
|
2205
|
+
@click.option(
|
|
2206
|
+
"--gateway-password",
|
|
2207
|
+
envvar="OPENCLAW_GATEWAY_PASSWORD",
|
|
2208
|
+
default=None,
|
|
2209
|
+
help="OpenClaw Gateway password(password 模式必需)",
|
|
2210
|
+
)
|
|
2211
|
+
@click.option("--session", "-s", default=None, help="OpenClaw Session key")
|
|
2212
|
+
@click.option("--message", "-m", default=None, help="连接后发送的初始消息")
|
|
2213
|
+
@click.option("--thinking", default=None, help="Thinking level override")
|
|
2214
|
+
@click.option("--history-limit", type=click.IntRange(1, 10000), default=None, help="历史条数 (默认使用 OpenClaw 默认值)")
|
|
2215
|
+
@click.option("--timeout-ms", type=click.IntRange(1, 86400000), default=None, help="Agent timeout ms")
|
|
2216
|
+
@click.option("--deliver", is_flag=True, help="Deliver assistant replies")
|
|
2217
|
+
@click.option("--insecure", "-k", is_flag=True, help="跳过 SSL 证书验证")
|
|
2218
|
+
@dry_run_option()
|
|
2219
|
+
@cli_output_option()
|
|
2220
|
+
def tui_openclaw(
|
|
2221
|
+
agent_ref: Optional[str],
|
|
2222
|
+
region: Optional[str],
|
|
2223
|
+
endpoint: Optional[str],
|
|
2224
|
+
api_key: Optional[str],
|
|
2225
|
+
gateway_token: Optional[str],
|
|
2226
|
+
gateway_password: Optional[str],
|
|
2227
|
+
session: Optional[str],
|
|
2228
|
+
message: Optional[str],
|
|
2229
|
+
thinking: Optional[str],
|
|
2230
|
+
history_limit: Optional[int],
|
|
2231
|
+
timeout_ms: Optional[int],
|
|
2232
|
+
deliver: bool,
|
|
2233
|
+
insecure: bool,
|
|
2234
|
+
dry_run: bool,
|
|
2235
|
+
output_mode: str | None,
|
|
2236
|
+
):
|
|
2237
|
+
"""连接远端 OpenClaw 原生 TUI(不需要本机安装 OpenClaw CLI)。
|
|
2238
|
+
|
|
2239
|
+
该命令不依赖本机安装 OpenClaw CLI;AgentEngine 会连接远端 runtime proxy,
|
|
2240
|
+
由容器内的 OpenClaw CLI 执行 `openclaw tui`。
|
|
2241
|
+
"""
|
|
2242
|
+
_ = output_mode
|
|
2243
|
+
dry_run = effective_dry_run(dry_run)
|
|
2244
|
+
options = _build_openclaw_tui_options(
|
|
2245
|
+
message=message,
|
|
2246
|
+
thinking=thinking,
|
|
2247
|
+
history_limit=history_limit,
|
|
2248
|
+
timeout_ms=timeout_ms,
|
|
2249
|
+
deliver=deliver,
|
|
2250
|
+
)
|
|
2251
|
+
if dry_run:
|
|
2252
|
+
secret_kind, gateway_secret = _select_openclaw_gateway_secret(
|
|
2253
|
+
gateway_token=gateway_token,
|
|
2254
|
+
gateway_password=gateway_password,
|
|
2255
|
+
)
|
|
2256
|
+
_render_openclaw_dry_run(
|
|
2257
|
+
"tui",
|
|
2258
|
+
{
|
|
2259
|
+
"agent_ref": agent_ref,
|
|
2260
|
+
"endpoint": endpoint,
|
|
2261
|
+
"mode": "tui",
|
|
2262
|
+
"session": session,
|
|
2263
|
+
"insecure": insecure,
|
|
2264
|
+
"gateway_auth_kind": secret_kind,
|
|
2265
|
+
"gateway_token_provided": bool(gateway_secret),
|
|
2266
|
+
"options": options,
|
|
2267
|
+
},
|
|
2268
|
+
hints=("dry-run 未解析远端 OpenClaw,也未建立 websocket。",),
|
|
2269
|
+
)
|
|
2270
|
+
return
|
|
2271
|
+
|
|
2272
|
+
from ksadk.deployment.state import load_state
|
|
2273
|
+
|
|
2274
|
+
state = load_state(Path(".").resolve())
|
|
2275
|
+
resolved_region = _resolve_region(region, state)
|
|
2276
|
+
detail: dict[str, Any] = dict(state or {})
|
|
2277
|
+
if endpoint:
|
|
2278
|
+
resolved_endpoint = endpoint
|
|
2279
|
+
elif not agent_ref and str(state.get("endpoint") or "").strip():
|
|
2280
|
+
resolved_endpoint = str(state.get("endpoint") or "").strip()
|
|
2281
|
+
else:
|
|
2282
|
+
resolved_region, detail = asyncio.run(_resolve_openclaw_detail_or_raise(agent_ref, region=region))
|
|
2283
|
+
resolved_endpoint = str(detail.get("endpoint") or "").strip()
|
|
2284
|
+
if not resolved_endpoint:
|
|
2285
|
+
raise click.ClickException("未解析到 OpenClaw Endpoint,请传入 --endpoint 或指定 OpenClaw ID/名称")
|
|
2286
|
+
|
|
2287
|
+
secret_kind, gateway_secret = _select_openclaw_gateway_secret(
|
|
2288
|
+
gateway_token=gateway_token,
|
|
2289
|
+
gateway_password=gateway_password,
|
|
2290
|
+
state=state,
|
|
2291
|
+
detail=detail,
|
|
2292
|
+
)
|
|
2293
|
+
auth_mode = _openclaw_auth_mode_from_sources(detail, state)
|
|
2294
|
+
if auth_mode in {"token", "password"} and not gateway_secret:
|
|
2295
|
+
raise click.ClickException(
|
|
2296
|
+
"当前 OpenClaw Gateway 为 token/password 模式,agentengine openclaw tui 需要 OpenClaw Gateway token/password。\n"
|
|
2297
|
+
"请使用: agentengine openclaw tui --gateway-token <token>\n"
|
|
2298
|
+
"或设置: OPENCLAW_GATEWAY_TOKEN=<token> agentengine openclaw tui\n"
|
|
2299
|
+
"如果部署时传过 OPENCLAW_GATEWAY_TOKEN,请重新运行部署让本地 .agentengine.state 记录该 token。\n"
|
|
2300
|
+
"注意:这里不是 AgentEngine API Key(ak-*)。"
|
|
2301
|
+
)
|
|
2302
|
+
|
|
2303
|
+
terminal_api_key = gateway_secret or api_key or str(detail.get("api_key") or state.get("api_key") or "").strip() or None
|
|
2304
|
+
click.secho("🖥️ OpenClaw Native Remote TUI", fg="blue", bold=True)
|
|
2305
|
+
click.echo(f" Endpoint: {resolved_endpoint}")
|
|
2306
|
+
if gateway_secret:
|
|
2307
|
+
click.echo(f" Runtime Auth: OpenClaw Gateway {secret_kind} {_mask_openclaw_secret(gateway_secret)}")
|
|
2308
|
+
elif terminal_api_key:
|
|
2309
|
+
click.echo(f" Runtime Auth: Bearer {_mask_openclaw_secret(terminal_api_key)}")
|
|
2310
|
+
else:
|
|
2311
|
+
click.echo(" Runtime Auth: anonymous/trusted-proxy")
|
|
2312
|
+
click.echo(" 退出: Ctrl-D 或 Ctrl-C")
|
|
2313
|
+
try:
|
|
2314
|
+
exit_code = asyncio.run(
|
|
2315
|
+
run_terminal_session(
|
|
2316
|
+
endpoint=resolved_endpoint,
|
|
2317
|
+
api_key=terminal_api_key,
|
|
2318
|
+
session_id=session,
|
|
2319
|
+
insecure=insecure,
|
|
2320
|
+
mode="tui",
|
|
2321
|
+
argv=[],
|
|
2322
|
+
options=options,
|
|
2323
|
+
)
|
|
2324
|
+
)
|
|
2325
|
+
except (KeyboardInterrupt, asyncio.CancelledError):
|
|
2326
|
+
raise SystemExit(130)
|
|
2327
|
+
except Exception as exc:
|
|
2328
|
+
click.secho(f"\n❌ OpenClaw 终端连接失败: {exc}", fg="red")
|
|
2329
|
+
click.echo(" 浏览器聊天页请改用: agentengine dashboard open --path /chat")
|
|
2330
|
+
raise SystemExit(1)
|
|
2331
|
+
if exit_code:
|
|
2332
|
+
raise SystemExit(exit_code)
|
|
2333
|
+
|
|
2334
|
+
|
|
2335
|
+
@openclaw_gateway.command("open", context_settings=CONTEXT_SETTINGS)
|
|
2336
|
+
@click.argument("agent_ref", required=False)
|
|
2337
|
+
@region_option(default=None, envvar=None, help_text="区域 (默认优先读取 .agentengine.state)")
|
|
2338
|
+
@click.option("--no-open", is_flag=True, help="仅打印 URL,不自动打开浏览器")
|
|
2339
|
+
@cli_output_option()
|
|
2340
|
+
def gateway_open(agent_ref: Optional[str], region: Optional[str], no_open: bool, output_mode: str | None):
|
|
2341
|
+
"""打开 OpenClaw gateway Dashboard。"""
|
|
2342
|
+
_ = output_mode
|
|
2343
|
+
no_open = no_open or is_json_output()
|
|
2344
|
+
|
|
2345
|
+
async def _run():
|
|
2346
|
+
resolved_region, detail = await _resolve_openclaw_detail_or_raise(agent_ref, region=region)
|
|
2347
|
+
await _ensure_openclaw_gateway_available(resolved_region, detail)
|
|
2348
|
+
gateway = _build_gateway_client(resolved_region, detail)
|
|
2349
|
+
try:
|
|
2350
|
+
info = await gateway.build_access_info()
|
|
2351
|
+
finally:
|
|
2352
|
+
await gateway.close()
|
|
2353
|
+
|
|
2354
|
+
payload = {
|
|
2355
|
+
"ok": True,
|
|
2356
|
+
"agent_id": detail.get("agent_id"),
|
|
2357
|
+
"name": detail.get("name"),
|
|
2358
|
+
"region": resolved_region,
|
|
2359
|
+
"status": detail.get("status"),
|
|
2360
|
+
"dashboard_url": info.access_url,
|
|
2361
|
+
"ws_url": info.ws_url,
|
|
2362
|
+
"auth_mode": "cookie-session",
|
|
2363
|
+
"link_id": info.link_id,
|
|
2364
|
+
"expires_at": info.expires_at,
|
|
2365
|
+
}
|
|
2366
|
+
if is_json_output():
|
|
2367
|
+
emit_json(payload)
|
|
2368
|
+
return
|
|
2369
|
+
|
|
2370
|
+
print_success("OpenClaw gateway 短链已生成")
|
|
2371
|
+
print_kv("OpenClaw", str(detail.get("name") or detail.get("agent_id") or "-"))
|
|
2372
|
+
print_kv("Dashboard", info.access_url, value_style="#58a6ff")
|
|
2373
|
+
print_kv("WebSocket", info.ws_url, value_style="#58a6ff")
|
|
2374
|
+
print_kv("认证方式", "cookie-session")
|
|
2375
|
+
if not no_open:
|
|
2376
|
+
webbrowser.open(info.access_url)
|
|
2377
|
+
|
|
2378
|
+
try:
|
|
2379
|
+
asyncio.run(_run())
|
|
2380
|
+
except Exception as e:
|
|
2381
|
+
_abort_openclaw_error(e, context="打开 gateway 失败", argv=["openclaw", "gateway", "open"])
|
|
2382
|
+
|
|
2383
|
+
|
|
2384
|
+
@openclaw_gateway.command("ws-url", context_settings=CONTEXT_SETTINGS)
|
|
2385
|
+
@click.argument("agent_ref", required=False)
|
|
2386
|
+
@region_option(default=None, envvar=None, help_text="区域 (默认优先读取 .agentengine.state)")
|
|
2387
|
+
@cli_output_option()
|
|
2388
|
+
def gateway_ws_url(agent_ref: Optional[str], region: Optional[str], output_mode: str | None):
|
|
2389
|
+
"""打印 gateway 短链与推导后的 websocket 地址。"""
|
|
2390
|
+
_ = output_mode
|
|
2391
|
+
|
|
2392
|
+
async def _run():
|
|
2393
|
+
resolved_region, detail = await _resolve_openclaw_detail_or_raise(agent_ref, region=region)
|
|
2394
|
+
await _ensure_openclaw_gateway_available(resolved_region, detail)
|
|
2395
|
+
gateway = _build_gateway_client(resolved_region, detail)
|
|
2396
|
+
try:
|
|
2397
|
+
info = await gateway.build_access_info()
|
|
2398
|
+
finally:
|
|
2399
|
+
await gateway.close()
|
|
2400
|
+
|
|
2401
|
+
payload = {
|
|
2402
|
+
"ok": True,
|
|
2403
|
+
"agent_id": detail.get("agent_id"),
|
|
2404
|
+
"name": detail.get("name"),
|
|
2405
|
+
"region": resolved_region,
|
|
2406
|
+
"dashboard_url": info.access_url,
|
|
2407
|
+
"ws_url": info.ws_url,
|
|
2408
|
+
"auth_mode": "cookie-session",
|
|
2409
|
+
"note": "该 ws-url 依赖短链 cookie session,不承诺长期复用。",
|
|
2410
|
+
}
|
|
2411
|
+
_emit_data_payload("OpenClaw Gateway 连接信息", payload, subtitle=str(detail.get("name") or "-"))
|
|
2412
|
+
|
|
2413
|
+
try:
|
|
2414
|
+
asyncio.run(_run())
|
|
2415
|
+
except Exception as e:
|
|
2416
|
+
_abort_openclaw_error(e, context="获取 gateway ws-url 失败", argv=["openclaw", "gateway", "ws-url"])
|
|
2417
|
+
|
|
2418
|
+
|
|
2419
|
+
@openclaw_gateway.command("logs", context_settings=CONTEXT_SETTINGS)
|
|
2420
|
+
@click.argument("agent_ref", required=False)
|
|
2421
|
+
@region_option(default=None, envvar=None, help_text="区域 (默认优先读取 .agentengine.state)")
|
|
2422
|
+
@click.option("--instance", default=None, help="实例名;不填则查询全部实例")
|
|
2423
|
+
@click.option("--log-type", type=click.Choice(["stdout", "log"], case_sensitive=False), default="stdout", show_default=True, help="日志类型")
|
|
2424
|
+
@click.option("--start-time", default=None, help="开始时间,支持 Unix 毫秒或 ISO-8601")
|
|
2425
|
+
@click.option("--end-time", default=None, help="结束时间,支持 Unix 毫秒或 ISO-8601")
|
|
2426
|
+
@cli_output_option()
|
|
2427
|
+
def gateway_logs(
|
|
2428
|
+
agent_ref: Optional[str],
|
|
2429
|
+
region: Optional[str],
|
|
2430
|
+
instance: Optional[str],
|
|
2431
|
+
log_type: str,
|
|
2432
|
+
start_time: Optional[str],
|
|
2433
|
+
end_time: Optional[str],
|
|
2434
|
+
output_mode: str | None,
|
|
2435
|
+
):
|
|
2436
|
+
"""读取 OpenClaw gateway 日志。"""
|
|
2437
|
+
_ = output_mode
|
|
2438
|
+
|
|
2439
|
+
async def _run():
|
|
2440
|
+
from ksadk.api import AgentEngineClient
|
|
2441
|
+
|
|
2442
|
+
resolved_region, detail = await _resolve_openclaw_detail_or_raise(agent_ref, region=region)
|
|
2443
|
+
async with AgentEngineClient(region=resolved_region) as client:
|
|
2444
|
+
resp = await client.get_agent_logs(
|
|
2445
|
+
agent_id=str(detail.get("agent_id") or ""),
|
|
2446
|
+
instance=instance,
|
|
2447
|
+
log_type=log_type,
|
|
2448
|
+
start_time=_parse_time_arg(start_time),
|
|
2449
|
+
end_time=_parse_time_arg(end_time),
|
|
2450
|
+
page=1,
|
|
2451
|
+
page_size=200,
|
|
2452
|
+
)
|
|
2453
|
+
|
|
2454
|
+
payload = {
|
|
2455
|
+
"ok": True,
|
|
2456
|
+
"agent_id": detail.get("agent_id"),
|
|
2457
|
+
"name": detail.get("name"),
|
|
2458
|
+
"region": resolved_region,
|
|
2459
|
+
"instance": resp.get("instance"),
|
|
2460
|
+
"log_type": resp.get("log_type"),
|
|
2461
|
+
"total": resp.get("total"),
|
|
2462
|
+
"logs": resp.get("logs", []),
|
|
2463
|
+
}
|
|
2464
|
+
if is_json_output():
|
|
2465
|
+
emit_json(payload)
|
|
2466
|
+
return
|
|
2467
|
+
|
|
2468
|
+
print_title("OpenClaw Gateway 日志", str(detail.get("name") or detail.get("agent_id") or "-"))
|
|
2469
|
+
print_kv("实例", str(resp.get("instance") or "all"))
|
|
2470
|
+
print_kv("日志类型", str(resp.get("log_type") or log_type))
|
|
2471
|
+
print_kv("日志条数", str(resp.get("total") or 0))
|
|
2472
|
+
logs = resp.get("logs", []) or []
|
|
2473
|
+
if not logs:
|
|
2474
|
+
print_warn("没有查询到日志")
|
|
2475
|
+
return
|
|
2476
|
+
for line in logs:
|
|
2477
|
+
console.print(str(line), markup=False)
|
|
2478
|
+
|
|
2479
|
+
try:
|
|
2480
|
+
asyncio.run(_run())
|
|
2481
|
+
except Exception as e:
|
|
2482
|
+
_abort_openclaw_error(e, context="获取 gateway 日志失败", argv=["openclaw", "gateway", "logs"])
|
|
2483
|
+
|
|
2484
|
+
|
|
2485
|
+
@openclaw_gateway.command("doctor", context_settings=CONTEXT_SETTINGS)
|
|
2486
|
+
@click.argument("agent_ref", required=False)
|
|
2487
|
+
@region_option(default=None, envvar=None, help_text="区域 (默认优先读取 .agentengine.state)")
|
|
2488
|
+
@click.option("--fix", "do_fix", is_flag=True, help="不走 gateway 内诊断,改为通过控制面执行 openclaw doctor --fix")
|
|
2489
|
+
@cli_output_option()
|
|
2490
|
+
def gateway_doctor(
|
|
2491
|
+
agent_ref: Optional[str],
|
|
2492
|
+
region: Optional[str],
|
|
2493
|
+
do_fix: bool,
|
|
2494
|
+
output_mode: str | None,
|
|
2495
|
+
):
|
|
2496
|
+
"""检查 gateway 短链、cookie 与 websocket 链路。"""
|
|
2497
|
+
_ = output_mode
|
|
2498
|
+
|
|
2499
|
+
async def _run() -> dict[str, Any]:
|
|
2500
|
+
if do_fix:
|
|
2501
|
+
return await _run_openclaw_repair_action(
|
|
2502
|
+
agent_ref,
|
|
2503
|
+
region=region,
|
|
2504
|
+
repair_action="doctor-fix",
|
|
2505
|
+
)
|
|
2506
|
+
checks: list[dict[str, Any]] = []
|
|
2507
|
+
resolved_region, detail = await _resolve_openclaw_detail_or_raise(agent_ref, region=region)
|
|
2508
|
+
checks.append(_build_gateway_instance_check(detail))
|
|
2509
|
+
|
|
2510
|
+
if checks[-1]["ok"]:
|
|
2511
|
+
gateway = _build_gateway_client(resolved_region, detail)
|
|
2512
|
+
try:
|
|
2513
|
+
info = await gateway.build_access_info()
|
|
2514
|
+
checks.append(
|
|
2515
|
+
{
|
|
2516
|
+
"name": "dashboard_short_link",
|
|
2517
|
+
"ok": True,
|
|
2518
|
+
"dashboard_url": info.access_url,
|
|
2519
|
+
"ws_url": info.ws_url,
|
|
2520
|
+
}
|
|
2521
|
+
)
|
|
2522
|
+
hello = await gateway.connect()
|
|
2523
|
+
checks.append(
|
|
2524
|
+
{
|
|
2525
|
+
"name": "cookie_ws_handshake",
|
|
2526
|
+
"ok": True,
|
|
2527
|
+
"methods": len(gateway.methods),
|
|
2528
|
+
}
|
|
2529
|
+
)
|
|
2530
|
+
cfg = await gateway.config_get()
|
|
2531
|
+
checks.append(
|
|
2532
|
+
{
|
|
2533
|
+
"name": "gateway_rpc",
|
|
2534
|
+
"ok": isinstance(cfg, dict),
|
|
2535
|
+
"config_path": cfg.get("path"),
|
|
2536
|
+
}
|
|
2537
|
+
)
|
|
2538
|
+
except Exception as exc:
|
|
2539
|
+
checks.append({"name": "gateway_connectivity", "ok": False, "error": str(exc)})
|
|
2540
|
+
finally:
|
|
2541
|
+
await gateway.close()
|
|
2542
|
+
|
|
2543
|
+
return {
|
|
2544
|
+
"ok": all(bool(item.get("ok")) for item in checks),
|
|
2545
|
+
"agent_id": detail.get("agent_id"),
|
|
2546
|
+
"name": detail.get("name"),
|
|
2547
|
+
"region": resolved_region,
|
|
2548
|
+
"checks": checks,
|
|
2549
|
+
}
|
|
2550
|
+
|
|
2551
|
+
try:
|
|
2552
|
+
payload = asyncio.run(_run())
|
|
2553
|
+
title = "OpenClaw Gateway Doctor Fix" if do_fix else "OpenClaw Gateway Doctor"
|
|
2554
|
+
_emit_data_payload(title, payload, subtitle=str(payload.get("name") or "-"))
|
|
2555
|
+
except Exception as e:
|
|
2556
|
+
argv = ["openclaw", "gateway", "doctor"]
|
|
2557
|
+
if do_fix:
|
|
2558
|
+
argv.append("--fix")
|
|
2559
|
+
_abort_openclaw_error(e, context="gateway doctor 执行失败", argv=argv)
|
|
2560
|
+
|
|
2561
|
+
|
|
2562
|
+
@openclaw.command("repair", context_settings=CONTEXT_SETTINGS)
|
|
2563
|
+
@click.argument("agent_ref", required=False)
|
|
2564
|
+
@region_option(default=None, envvar=None, help_text="区域 (默认优先读取 .agentengine.state)")
|
|
2565
|
+
@click.option(
|
|
2566
|
+
"--action",
|
|
2567
|
+
"repair_action",
|
|
2568
|
+
type=click.Choice(["doctor-fix"], case_sensitive=False),
|
|
2569
|
+
default="doctor-fix",
|
|
2570
|
+
show_default=True,
|
|
2571
|
+
help="控制面修复动作",
|
|
2572
|
+
)
|
|
2573
|
+
@cli_output_option()
|
|
2574
|
+
def openclaw_repair(
|
|
2575
|
+
agent_ref: Optional[str],
|
|
2576
|
+
region: Optional[str],
|
|
2577
|
+
repair_action: str,
|
|
2578
|
+
output_mode: str | None,
|
|
2579
|
+
):
|
|
2580
|
+
"""通过控制面执行 OpenClaw 修复动作。"""
|
|
2581
|
+
_ = output_mode
|
|
2582
|
+
|
|
2583
|
+
async def _run() -> dict[str, Any]:
|
|
2584
|
+
return await _run_openclaw_repair_action(
|
|
2585
|
+
agent_ref,
|
|
2586
|
+
region=region,
|
|
2587
|
+
repair_action=str(repair_action or "doctor-fix").strip().lower(),
|
|
2588
|
+
)
|
|
2589
|
+
|
|
2590
|
+
try:
|
|
2591
|
+
payload = asyncio.run(_run())
|
|
2592
|
+
_emit_data_payload("OpenClaw Repair", payload, subtitle=str(payload.get("name") or "-"))
|
|
2593
|
+
except Exception as e:
|
|
2594
|
+
_abort_openclaw_error(e, context="OpenClaw repair 执行失败", argv=["openclaw", "repair"])
|
|
2595
|
+
|
|
2596
|
+
|
|
2597
|
+
@openclaw.group(
|
|
2598
|
+
"channel",
|
|
2599
|
+
context_settings=CONTEXT_SETTINGS,
|
|
2600
|
+
help="OpenClaw Channel 统一入口。",
|
|
2601
|
+
)
|
|
2602
|
+
def openclaw_channel():
|
|
2603
|
+
pass
|
|
2604
|
+
|
|
2605
|
+
|
|
2606
|
+
@openclaw_channel.command("status", context_settings=CONTEXT_SETTINGS)
|
|
2607
|
+
@click.argument("agent_ref", required=False)
|
|
2608
|
+
@region_option(default=None, envvar=None, help_text="区域 (默认优先读取 .agentengine.state)")
|
|
2609
|
+
@click.option("--channel", type=click.Choice(OPENCLAW_CHANNELS, case_sensitive=False), default=None, help="指定 channel")
|
|
2610
|
+
@click.option("--probe", is_flag=True, help="触发远端 probe 刷新 channel 快照")
|
|
2611
|
+
@cli_output_option()
|
|
2612
|
+
def channel_status(
|
|
2613
|
+
agent_ref: Optional[str],
|
|
2614
|
+
region: Optional[str],
|
|
2615
|
+
channel: Optional[str],
|
|
2616
|
+
probe: bool,
|
|
2617
|
+
output_mode: str | None,
|
|
2618
|
+
):
|
|
2619
|
+
"""查看远端 channel 状态快照。"""
|
|
2620
|
+
_ = output_mode
|
|
2621
|
+
normalized_channel = str(channel).lower() if channel else None
|
|
2622
|
+
|
|
2623
|
+
async def _run():
|
|
2624
|
+
resolved_region, detail = await _resolve_openclaw_detail_or_raise(agent_ref, region=region)
|
|
2625
|
+
await _ensure_openclaw_gateway_available(resolved_region, detail)
|
|
2626
|
+
gateway = _build_gateway_client(resolved_region, detail)
|
|
2627
|
+
try:
|
|
2628
|
+
await gateway.connect()
|
|
2629
|
+
snapshot = await gateway.channels_status(probe=probe, timeout_ms=8_000 if probe else None)
|
|
2630
|
+
finally:
|
|
2631
|
+
await gateway.close()
|
|
2632
|
+
|
|
2633
|
+
payload = {
|
|
2634
|
+
"ok": True,
|
|
2635
|
+
"agent_id": detail.get("agent_id"),
|
|
2636
|
+
"name": detail.get("name"),
|
|
2637
|
+
"region": resolved_region,
|
|
2638
|
+
"channel": normalized_channel,
|
|
2639
|
+
"probe": probe,
|
|
2640
|
+
"snapshot": snapshot,
|
|
2641
|
+
"selected": _extract_channel_snapshot(snapshot, normalized_channel),
|
|
2642
|
+
}
|
|
2643
|
+
_emit_data_payload("OpenClaw Channel 状态", payload, subtitle=str(detail.get("name") or "-"))
|
|
2644
|
+
|
|
2645
|
+
try:
|
|
2646
|
+
asyncio.run(_run())
|
|
2647
|
+
except Exception as e:
|
|
2648
|
+
_abort_openclaw_error(e, context="获取 channel 状态失败", argv=["openclaw", "channel", "status"])
|
|
2649
|
+
|
|
2650
|
+
|
|
2651
|
+
@openclaw_channel.command("connect", context_settings=CONTEXT_SETTINGS, help=OPENCLAW_CHANNEL_CONNECT_HELP)
|
|
2652
|
+
@click.argument("agent_ref", required=False)
|
|
2653
|
+
@region_option(default=None, envvar=None, help_text="区域 (默认优先读取 .agentengine.state)")
|
|
2654
|
+
@click.option("--channel", "channel_name", type=click.Choice(OPENCLAW_CHANNELS, case_sensitive=False), required=True, help="目标 channel")
|
|
2655
|
+
@click.option("--open-qr", is_flag=True, help="仅微信:在本地浏览器额外打开二维码链接")
|
|
2656
|
+
@click.option("--app-id", "wps_xiezuo_app_id", default=None, help="仅 WPS 协作:开放平台应用 ID")
|
|
2657
|
+
@click.option("--app-secret", "wps_xiezuo_app_secret", default=None, help="仅 WPS 协作:开放平台应用密钥")
|
|
2658
|
+
@click.option("--account-id", "wps_xiezuo_account_id", default="default", help="仅 WPS 协作:账号 ID;当前只支持 default")
|
|
2659
|
+
@click.option("--agent-id", "wps_xiezuo_agent_id", default="main", help="仅 WPS 协作:消息路由到的 OpenClaw agentId")
|
|
2660
|
+
@click.option("--dm-policy", "wps_xiezuo_dm_policy", type=click.Choice(("disabled", "open", "pairing", "allowlist")), default="pairing", help="仅 WPS 协作:私聊策略,默认 pairing")
|
|
2661
|
+
@click.option("--group-policy", "wps_xiezuo_group_policy", type=click.Choice(("open", "allowlist")), default="open", help="仅 WPS 协作:群聊策略,默认 open")
|
|
2662
|
+
@click.option("--base-url", "wps_xiezuo_base_url", default="https://openapi.wps.cn", help="仅 WPS 协作:WPS OpenAPI 基础地址")
|
|
2663
|
+
def channel_connect(
|
|
2664
|
+
agent_ref: Optional[str],
|
|
2665
|
+
region: Optional[str],
|
|
2666
|
+
channel_name: str,
|
|
2667
|
+
open_qr: bool,
|
|
2668
|
+
wps_xiezuo_app_id: Optional[str],
|
|
2669
|
+
wps_xiezuo_app_secret: Optional[str],
|
|
2670
|
+
wps_xiezuo_account_id: str,
|
|
2671
|
+
wps_xiezuo_agent_id: str,
|
|
2672
|
+
wps_xiezuo_dm_policy: str,
|
|
2673
|
+
wps_xiezuo_group_policy: str,
|
|
2674
|
+
wps_xiezuo_base_url: str,
|
|
2675
|
+
):
|
|
2676
|
+
normalized_channel = channel_name.lower()
|
|
2677
|
+
|
|
2678
|
+
async def _run():
|
|
2679
|
+
resolved_region, detail = await _resolve_openclaw_detail_or_raise(agent_ref, region=region)
|
|
2680
|
+
await _ensure_openclaw_gateway_available(resolved_region, detail)
|
|
2681
|
+
|
|
2682
|
+
if normalized_channel == "weixin":
|
|
2683
|
+
preflight_changed = False
|
|
2684
|
+
preflight_gateway = _build_gateway_client(resolved_region, detail)
|
|
2685
|
+
try:
|
|
2686
|
+
await preflight_gateway.connect()
|
|
2687
|
+
cfg_snapshot = await preflight_gateway.config_get()
|
|
2688
|
+
config, base_hash = _extract_config_state(cfg_snapshot)
|
|
2689
|
+
preflight_changed, _ = _mutate_weixin_account_enabled(
|
|
2690
|
+
config,
|
|
2691
|
+
enabled=True,
|
|
2692
|
+
account_id="default",
|
|
2693
|
+
)
|
|
2694
|
+
if preflight_changed:
|
|
2695
|
+
await _config_apply_and_wait_for_reload(
|
|
2696
|
+
preflight_gateway,
|
|
2697
|
+
resolved_region,
|
|
2698
|
+
detail,
|
|
2699
|
+
config=config,
|
|
2700
|
+
base_hash=base_hash,
|
|
2701
|
+
note="ksadk seed weixin channel config",
|
|
2702
|
+
)
|
|
2703
|
+
finally:
|
|
2704
|
+
await preflight_gateway.close()
|
|
2705
|
+
|
|
2706
|
+
gateway = _build_gateway_client(resolved_region, detail)
|
|
2707
|
+
fallback_reason: str | None = None
|
|
2708
|
+
qr_url = ""
|
|
2709
|
+
wait_result: dict[str, Any] = {}
|
|
2710
|
+
try:
|
|
2711
|
+
await gateway.connect()
|
|
2712
|
+
methods = set(getattr(gateway, "methods", []) or [])
|
|
2713
|
+
missing_methods = [
|
|
2714
|
+
method
|
|
2715
|
+
for method in ("web.login.start", "web.login.wait")
|
|
2716
|
+
if method not in methods
|
|
2717
|
+
]
|
|
2718
|
+
if missing_methods:
|
|
2719
|
+
fallback_reason = f"missing gateway methods: {', '.join(missing_methods)}"
|
|
2720
|
+
else:
|
|
2721
|
+
try:
|
|
2722
|
+
start = await gateway.web_login_start(force=False, timeout_ms=30_000)
|
|
2723
|
+
except OpenClawGatewayRequestError as exc:
|
|
2724
|
+
if not _is_weixin_web_login_unavailable(exc):
|
|
2725
|
+
raise
|
|
2726
|
+
fallback_reason = str(exc)
|
|
2727
|
+
else:
|
|
2728
|
+
qr_url = str(start.get("qrDataUrl") or "").strip()
|
|
2729
|
+
session_key = str(start.get("sessionKey") or "").strip()
|
|
2730
|
+
if not qr_url:
|
|
2731
|
+
raise OpenClawGatewayError("微信扫码登录未返回二维码 URL")
|
|
2732
|
+
|
|
2733
|
+
print_success("请使用微信扫码完成连接")
|
|
2734
|
+
print_kv("二维码链接", qr_url, value_style="#58a6ff")
|
|
2735
|
+
_render_terminal_qr(qr_url)
|
|
2736
|
+
if open_qr:
|
|
2737
|
+
webbrowser.open(qr_url)
|
|
2738
|
+
|
|
2739
|
+
try:
|
|
2740
|
+
wait_result = await gateway.web_login_wait(
|
|
2741
|
+
account_id=session_key or None,
|
|
2742
|
+
timeout_ms=120_000,
|
|
2743
|
+
)
|
|
2744
|
+
except OpenClawGatewayRequestError as exc:
|
|
2745
|
+
if not _is_weixin_web_login_unavailable(exc):
|
|
2746
|
+
raise
|
|
2747
|
+
fallback_reason = str(exc)
|
|
2748
|
+
finally:
|
|
2749
|
+
await gateway.close()
|
|
2750
|
+
|
|
2751
|
+
if fallback_reason:
|
|
2752
|
+
payload = await _run_weixin_remote_cli_login(
|
|
2753
|
+
resolved_region,
|
|
2754
|
+
detail,
|
|
2755
|
+
reason=fallback_reason,
|
|
2756
|
+
)
|
|
2757
|
+
_emit_data_payload("OpenClaw Channel Connect", payload, subtitle="weixin")
|
|
2758
|
+
return
|
|
2759
|
+
|
|
2760
|
+
snapshot = await _fetch_channel_snapshot_with_retry(
|
|
2761
|
+
resolved_region,
|
|
2762
|
+
detail,
|
|
2763
|
+
probe=True,
|
|
2764
|
+
)
|
|
2765
|
+
|
|
2766
|
+
payload = {
|
|
2767
|
+
"ok": True,
|
|
2768
|
+
"agent_id": detail.get("agent_id"),
|
|
2769
|
+
"name": detail.get("name"),
|
|
2770
|
+
"region": resolved_region,
|
|
2771
|
+
"channel": normalized_channel,
|
|
2772
|
+
"qr_url": qr_url,
|
|
2773
|
+
"login": wait_result,
|
|
2774
|
+
"status": _extract_channel_snapshot(snapshot, normalized_channel),
|
|
2775
|
+
}
|
|
2776
|
+
_emit_data_payload("OpenClaw Channel Connect", payload, subtitle="weixin")
|
|
2777
|
+
return
|
|
2778
|
+
|
|
2779
|
+
if normalized_channel == "wps-xiezuo":
|
|
2780
|
+
app_id = str(wps_xiezuo_app_id or "").strip()
|
|
2781
|
+
app_secret = str(wps_xiezuo_app_secret or "").strip()
|
|
2782
|
+
account_id = str(wps_xiezuo_account_id or WPS_XIEZUO_DEFAULT_ACCOUNT_ID).strip() or WPS_XIEZUO_DEFAULT_ACCOUNT_ID
|
|
2783
|
+
if not app_id:
|
|
2784
|
+
raise click.ClickException("连接 WPS 协作 channel 必须提供 --app-id")
|
|
2785
|
+
if not app_secret:
|
|
2786
|
+
raise click.ClickException("连接 WPS 协作 channel 必须提供 --app-secret")
|
|
2787
|
+
if account_id != WPS_XIEZUO_DEFAULT_ACCOUNT_ID:
|
|
2788
|
+
raise click.ClickException("WPS 协作插件使用扁平 channel 配置,`--account-id` 仅支持 default")
|
|
2789
|
+
apply_gateway = _build_gateway_client(resolved_region, detail)
|
|
2790
|
+
changed = False
|
|
2791
|
+
try:
|
|
2792
|
+
await apply_gateway.connect()
|
|
2793
|
+
fresh_cfg_snapshot = await apply_gateway.config_get()
|
|
2794
|
+
fresh_config, fresh_base_hash = _extract_config_state(fresh_cfg_snapshot)
|
|
2795
|
+
changed = _mutate_wps_xiezuo_connect_config(
|
|
2796
|
+
fresh_config,
|
|
2797
|
+
app_id=app_id,
|
|
2798
|
+
app_secret=app_secret,
|
|
2799
|
+
account_id=account_id,
|
|
2800
|
+
agent_id=str(wps_xiezuo_agent_id or "main").strip() or "main",
|
|
2801
|
+
dm_policy=wps_xiezuo_dm_policy,
|
|
2802
|
+
group_policy=wps_xiezuo_group_policy,
|
|
2803
|
+
base_url=str(wps_xiezuo_base_url or "https://openapi.wps.cn").strip() or "https://openapi.wps.cn",
|
|
2804
|
+
)
|
|
2805
|
+
if changed:
|
|
2806
|
+
await _config_apply_and_wait_for_reload(
|
|
2807
|
+
apply_gateway,
|
|
2808
|
+
resolved_region,
|
|
2809
|
+
detail,
|
|
2810
|
+
config=fresh_config,
|
|
2811
|
+
base_hash=fresh_base_hash,
|
|
2812
|
+
note="ksadk configure wps-xiezuo channel",
|
|
2813
|
+
)
|
|
2814
|
+
finally:
|
|
2815
|
+
await apply_gateway.close()
|
|
2816
|
+
|
|
2817
|
+
snapshot = await _fetch_channel_snapshot_with_retry(
|
|
2818
|
+
resolved_region,
|
|
2819
|
+
detail,
|
|
2820
|
+
probe=True,
|
|
2821
|
+
)
|
|
2822
|
+
payload = {
|
|
2823
|
+
"ok": True,
|
|
2824
|
+
"agent_id": detail.get("agent_id"),
|
|
2825
|
+
"name": detail.get("name"),
|
|
2826
|
+
"region": resolved_region,
|
|
2827
|
+
"channel": normalized_channel,
|
|
2828
|
+
"configured": True,
|
|
2829
|
+
"changed": changed,
|
|
2830
|
+
"app_id": app_id,
|
|
2831
|
+
"account_id": account_id,
|
|
2832
|
+
"status": _extract_channel_snapshot(snapshot, normalized_channel),
|
|
2833
|
+
}
|
|
2834
|
+
_emit_data_payload("OpenClaw Channel Connect", payload, subtitle="wps-xiezuo")
|
|
2835
|
+
return
|
|
2836
|
+
|
|
2837
|
+
bootstrap_gateway = _build_gateway_client(resolved_region, detail)
|
|
2838
|
+
try:
|
|
2839
|
+
await bootstrap_gateway.connect()
|
|
2840
|
+
cfg_snapshot = await bootstrap_gateway.config_get()
|
|
2841
|
+
finally:
|
|
2842
|
+
await bootstrap_gateway.close()
|
|
2843
|
+
|
|
2844
|
+
config, _ = _extract_config_state(cfg_snapshot)
|
|
2845
|
+
existing_app_id = str(
|
|
2846
|
+
((config.get("channels") or {}).get(FEISHU_CHANNEL_KEY) or {}).get("appId") or ""
|
|
2847
|
+
).strip() or None
|
|
2848
|
+
onboarding = await _run_feishu_onboarding(existing_app_id)
|
|
2849
|
+
changed_config = copy.deepcopy(config)
|
|
2850
|
+
changed = _mutate_feishu_connect_config(changed_config, onboarding)
|
|
2851
|
+
base_hash = str(cfg_snapshot.get("hash") or "").strip()
|
|
2852
|
+
if changed:
|
|
2853
|
+
apply_gateway = _build_gateway_client(resolved_region, detail)
|
|
2854
|
+
try:
|
|
2855
|
+
await apply_gateway.connect()
|
|
2856
|
+
await _config_apply_and_wait_for_reload(
|
|
2857
|
+
apply_gateway,
|
|
2858
|
+
resolved_region,
|
|
2859
|
+
detail,
|
|
2860
|
+
config=changed_config,
|
|
2861
|
+
base_hash=base_hash,
|
|
2862
|
+
note="ksadk configure feishu channel",
|
|
2863
|
+
)
|
|
2864
|
+
finally:
|
|
2865
|
+
await apply_gateway.close()
|
|
2866
|
+
|
|
2867
|
+
snapshot = await _fetch_channel_snapshot_with_retry(
|
|
2868
|
+
resolved_region,
|
|
2869
|
+
detail,
|
|
2870
|
+
probe=True,
|
|
2871
|
+
)
|
|
2872
|
+
|
|
2873
|
+
payload = {
|
|
2874
|
+
"ok": True,
|
|
2875
|
+
"agent_id": detail.get("agent_id"),
|
|
2876
|
+
"name": detail.get("name"),
|
|
2877
|
+
"region": resolved_region,
|
|
2878
|
+
"channel": normalized_channel,
|
|
2879
|
+
"configured": True,
|
|
2880
|
+
"changed": changed,
|
|
2881
|
+
"status": _extract_channel_snapshot(snapshot, normalized_channel),
|
|
2882
|
+
}
|
|
2883
|
+
_emit_data_payload("OpenClaw Channel Connect", payload, subtitle="feishu")
|
|
2884
|
+
|
|
2885
|
+
try:
|
|
2886
|
+
asyncio.run(_run())
|
|
2887
|
+
except Exception as e:
|
|
2888
|
+
_abort_openclaw_error(e, context="channel connect 失败", argv=["openclaw", "channel", "connect"])
|
|
2889
|
+
|
|
2890
|
+
|
|
2891
|
+
def _run_channel_toggle_command(
|
|
2892
|
+
*,
|
|
2893
|
+
action: str,
|
|
2894
|
+
enabled: bool,
|
|
2895
|
+
agent_ref: Optional[str],
|
|
2896
|
+
region: Optional[str],
|
|
2897
|
+
channel_name: str,
|
|
2898
|
+
account_id: Optional[str],
|
|
2899
|
+
) -> None:
|
|
2900
|
+
normalized_channel = channel_name.lower()
|
|
2901
|
+
|
|
2902
|
+
async def _run():
|
|
2903
|
+
resolved_region, detail = await _resolve_openclaw_detail_or_raise(agent_ref, region=region)
|
|
2904
|
+
await _ensure_openclaw_gateway_available(resolved_region, detail)
|
|
2905
|
+
|
|
2906
|
+
gateway = _build_gateway_client(resolved_region, detail)
|
|
2907
|
+
try:
|
|
2908
|
+
await gateway.connect()
|
|
2909
|
+
cfg_snapshot = await gateway.config_get()
|
|
2910
|
+
config, base_hash = _extract_config_state(cfg_snapshot)
|
|
2911
|
+
if normalized_channel == "weixin":
|
|
2912
|
+
changed, resolved_account_id = _mutate_weixin_account_enabled(
|
|
2913
|
+
config,
|
|
2914
|
+
enabled=enabled,
|
|
2915
|
+
account_id=account_id,
|
|
2916
|
+
)
|
|
2917
|
+
elif normalized_channel == "wps-xiezuo":
|
|
2918
|
+
changed = _mutate_wps_xiezuo_enabled(
|
|
2919
|
+
config,
|
|
2920
|
+
enabled=enabled,
|
|
2921
|
+
account_id=account_id,
|
|
2922
|
+
)
|
|
2923
|
+
resolved_account_id = _channel_default_account_id(normalized_channel)
|
|
2924
|
+
else:
|
|
2925
|
+
changed = _mutate_feishu_enabled(config, enabled=enabled, account_id=account_id)
|
|
2926
|
+
resolved_account_id = _channel_default_account_id(normalized_channel)
|
|
2927
|
+
|
|
2928
|
+
if changed:
|
|
2929
|
+
await _config_apply_and_wait_for_reload(
|
|
2930
|
+
gateway,
|
|
2931
|
+
resolved_region,
|
|
2932
|
+
detail,
|
|
2933
|
+
config=config,
|
|
2934
|
+
base_hash=base_hash,
|
|
2935
|
+
note=f"ksadk {action} {normalized_channel} channel",
|
|
2936
|
+
)
|
|
2937
|
+
finally:
|
|
2938
|
+
await gateway.close()
|
|
2939
|
+
|
|
2940
|
+
snapshot = await _fetch_channel_snapshot_with_retry(
|
|
2941
|
+
resolved_region,
|
|
2942
|
+
detail,
|
|
2943
|
+
probe=True,
|
|
2944
|
+
)
|
|
2945
|
+
|
|
2946
|
+
payload = {
|
|
2947
|
+
"ok": True,
|
|
2948
|
+
"agent_id": detail.get("agent_id"),
|
|
2949
|
+
"name": detail.get("name"),
|
|
2950
|
+
"region": resolved_region,
|
|
2951
|
+
"channel": normalized_channel,
|
|
2952
|
+
"account_id": resolved_account_id,
|
|
2953
|
+
"changed": changed,
|
|
2954
|
+
"enabled": enabled,
|
|
2955
|
+
"status": _extract_channel_snapshot(snapshot, normalized_channel),
|
|
2956
|
+
}
|
|
2957
|
+
_emit_data_payload(f"OpenClaw Channel {action.title()}", payload, subtitle=normalized_channel)
|
|
2958
|
+
|
|
2959
|
+
try:
|
|
2960
|
+
asyncio.run(_run())
|
|
2961
|
+
except Exception as e:
|
|
2962
|
+
_abort_openclaw_error(e, context=f"channel {action} 失败", argv=["openclaw", "channel", action])
|
|
2963
|
+
|
|
2964
|
+
|
|
2965
|
+
@openclaw_channel.command("enable", context_settings=CONTEXT_SETTINGS)
|
|
2966
|
+
@click.argument("agent_ref", required=False)
|
|
2967
|
+
@region_option(default=None, envvar=None, help_text="区域 (默认优先读取 .agentengine.state)")
|
|
2968
|
+
@click.option("--channel", "channel_name", type=click.Choice(OPENCLAW_CHANNELS, case_sensitive=False), required=True, help="目标 channel")
|
|
2969
|
+
@click.option("--account-id", default=None, help="账号 ID;飞书 V1 仅支持 default")
|
|
2970
|
+
def channel_enable(
|
|
2971
|
+
agent_ref: Optional[str],
|
|
2972
|
+
region: Optional[str],
|
|
2973
|
+
channel_name: str,
|
|
2974
|
+
account_id: Optional[str],
|
|
2975
|
+
):
|
|
2976
|
+
"""启用远端 channel 配置。"""
|
|
2977
|
+
_run_channel_toggle_command(
|
|
2978
|
+
action="enable",
|
|
2979
|
+
enabled=True,
|
|
2980
|
+
agent_ref=agent_ref,
|
|
2981
|
+
region=region,
|
|
2982
|
+
channel_name=channel_name,
|
|
2983
|
+
account_id=account_id,
|
|
2984
|
+
)
|
|
2985
|
+
|
|
2986
|
+
|
|
2987
|
+
@openclaw_channel.command("disable", context_settings=CONTEXT_SETTINGS)
|
|
2988
|
+
@click.argument("agent_ref", required=False)
|
|
2989
|
+
@region_option(default=None, envvar=None, help_text="区域 (默认优先读取 .agentengine.state)")
|
|
2990
|
+
@click.option("--channel", "channel_name", type=click.Choice(OPENCLAW_CHANNELS, case_sensitive=False), required=True, help="目标 channel")
|
|
2991
|
+
@click.option("--account-id", default=None, help="账号 ID;飞书 V1 仅支持 default")
|
|
2992
|
+
def channel_disable(
|
|
2993
|
+
agent_ref: Optional[str],
|
|
2994
|
+
region: Optional[str],
|
|
2995
|
+
channel_name: str,
|
|
2996
|
+
account_id: Optional[str],
|
|
2997
|
+
):
|
|
2998
|
+
"""禁用远端 channel 配置。"""
|
|
2999
|
+
_run_channel_toggle_command(
|
|
3000
|
+
action="disable",
|
|
3001
|
+
enabled=False,
|
|
3002
|
+
agent_ref=agent_ref,
|
|
3003
|
+
region=region,
|
|
3004
|
+
channel_name=channel_name,
|
|
3005
|
+
account_id=account_id,
|
|
3006
|
+
)
|
|
3007
|
+
|
|
3008
|
+
|
|
3009
|
+
@openclaw_channel.command("doctor", context_settings=CONTEXT_SETTINGS)
|
|
3010
|
+
@click.argument("agent_ref", required=False)
|
|
3011
|
+
@region_option(default=None, envvar=None, help_text="区域 (默认优先读取 .agentengine.state)")
|
|
3012
|
+
@click.option("--channel", type=click.Choice(OPENCLAW_CHANNELS, case_sensitive=False), default=None, help="指定 channel")
|
|
3013
|
+
@cli_output_option()
|
|
3014
|
+
def channel_doctor(agent_ref: Optional[str], region: Optional[str], channel: Optional[str], output_mode: str | None):
|
|
3015
|
+
"""检查 channel 接入前置条件。"""
|
|
3016
|
+
_ = output_mode
|
|
3017
|
+
normalized_channel = str(channel).lower() if channel else None
|
|
3018
|
+
|
|
3019
|
+
async def _run():
|
|
3020
|
+
checks: list[dict[str, Any]] = []
|
|
3021
|
+
resolved_region, detail = await _resolve_openclaw_detail_or_raise(agent_ref, region=region)
|
|
3022
|
+
checks.append(_build_gateway_instance_check(detail))
|
|
3023
|
+
|
|
3024
|
+
snapshot = None
|
|
3025
|
+
methods: list[str] = []
|
|
3026
|
+
config: dict[str, Any] = {}
|
|
3027
|
+
if checks[-1]["ok"]:
|
|
3028
|
+
gateway = _build_gateway_client(resolved_region, detail)
|
|
3029
|
+
try:
|
|
3030
|
+
info = await gateway.build_access_info()
|
|
3031
|
+
checks.append({"name": "dashboard_short_link", "ok": True, "dashboard_url": info.access_url})
|
|
3032
|
+
await gateway.connect()
|
|
3033
|
+
methods = gateway.methods
|
|
3034
|
+
checks.append({"name": "cookie_ws_handshake", "ok": True, "methods": len(methods)})
|
|
3035
|
+
snapshot = await gateway.channels_status(probe=False)
|
|
3036
|
+
config_snapshot = await gateway.config_get()
|
|
3037
|
+
config = config_snapshot.get("config") if isinstance(config_snapshot.get("config"), dict) else {}
|
|
3038
|
+
except Exception as exc:
|
|
3039
|
+
checks.append({"name": "gateway_connectivity", "ok": False, "error": str(exc)})
|
|
3040
|
+
finally:
|
|
3041
|
+
await gateway.close()
|
|
3042
|
+
|
|
3043
|
+
channels_to_check = [normalized_channel] if normalized_channel else list(OPENCLAW_CHANNELS)
|
|
3044
|
+
plugin_entries = ((config.get("plugins") or {}).get("entries") or {}) if isinstance(config, dict) else {}
|
|
3045
|
+
for item in channels_to_check:
|
|
3046
|
+
selected_snapshot = _extract_channel_snapshot(snapshot, item)
|
|
3047
|
+
configured = _is_channel_configured(item, config=config, snapshot=selected_snapshot)
|
|
3048
|
+
plugin_id = str((OPENCLAW_CHANNEL_SPECS.get(item) or {}).get("plugin_id") or "")
|
|
3049
|
+
if item == "weixin":
|
|
3050
|
+
checks.append(
|
|
3051
|
+
{
|
|
3052
|
+
"name": "weixin_plugin_visible",
|
|
3053
|
+
"ok": plugin_id in plugin_entries or selected_snapshot is not None,
|
|
3054
|
+
"plugin_id": plugin_id,
|
|
3055
|
+
}
|
|
3056
|
+
)
|
|
3057
|
+
checks.append(
|
|
3058
|
+
_build_channel_doctor_availability_check(
|
|
3059
|
+
name="weixin_status_snapshot",
|
|
3060
|
+
available=selected_snapshot is not None,
|
|
3061
|
+
configured=configured,
|
|
3062
|
+
connect_required_message="首次连接前微信状态快照可能为空,执行 channel connect 后会自动补齐",
|
|
3063
|
+
)
|
|
3064
|
+
)
|
|
3065
|
+
checks.append(
|
|
3066
|
+
_build_channel_doctor_availability_check(
|
|
3067
|
+
name="weixin_qr_rpc",
|
|
3068
|
+
available="web.login.start" in methods and "web.login.wait" in methods,
|
|
3069
|
+
configured=configured,
|
|
3070
|
+
connect_required_message="首次连接会先自动启用 bundled weixin plugin,然后再暴露扫码 RPC",
|
|
3071
|
+
connect_required_ok=False,
|
|
3072
|
+
required_methods=["web.login.start", "web.login.wait"],
|
|
3073
|
+
)
|
|
3074
|
+
)
|
|
3075
|
+
elif item == "feishu":
|
|
3076
|
+
checks.append(
|
|
3077
|
+
{
|
|
3078
|
+
"name": "feishu_plugin_visible",
|
|
3079
|
+
"ok": plugin_id in plugin_entries or selected_snapshot is not None,
|
|
3080
|
+
"plugin_id": plugin_id,
|
|
3081
|
+
}
|
|
3082
|
+
)
|
|
3083
|
+
checks.append(
|
|
3084
|
+
_build_channel_doctor_availability_check(
|
|
3085
|
+
name="feishu_status_snapshot",
|
|
3086
|
+
available=selected_snapshot is not None,
|
|
3087
|
+
configured=configured,
|
|
3088
|
+
connect_required_message="飞书尚未完成 connect/onboarding,首次接入前不会出现在 channel snapshot 中",
|
|
3089
|
+
)
|
|
3090
|
+
)
|
|
3091
|
+
node_path = shutil.which("node")
|
|
3092
|
+
npx_path = shutil.which("npx")
|
|
3093
|
+
checks.append(
|
|
3094
|
+
{
|
|
3095
|
+
"name": "feishu_local_node",
|
|
3096
|
+
"ok": bool(node_path and npx_path),
|
|
3097
|
+
"node": node_path,
|
|
3098
|
+
"npx": npx_path,
|
|
3099
|
+
}
|
|
3100
|
+
)
|
|
3101
|
+
elif item == "wps-xiezuo":
|
|
3102
|
+
checks.append(
|
|
3103
|
+
{
|
|
3104
|
+
"name": "wps_xiezuo_plugin_visible",
|
|
3105
|
+
"ok": plugin_id in plugin_entries or selected_snapshot is not None,
|
|
3106
|
+
"plugin_id": plugin_id,
|
|
3107
|
+
}
|
|
3108
|
+
)
|
|
3109
|
+
checks.append(
|
|
3110
|
+
_build_channel_doctor_availability_check(
|
|
3111
|
+
name="wps_xiezuo_status_snapshot",
|
|
3112
|
+
available=selected_snapshot is not None,
|
|
3113
|
+
configured=configured,
|
|
3114
|
+
connect_required_message="WPS 协作尚未完成配置,首次 connect 前不会出现在 channel snapshot 中",
|
|
3115
|
+
)
|
|
3116
|
+
)
|
|
3117
|
+
dep_check = _check_wps_xiezuo_local_deps()
|
|
3118
|
+
checks.append(
|
|
3119
|
+
{
|
|
3120
|
+
"name": "wps_xiezuo_local_deps",
|
|
3121
|
+
**dep_check,
|
|
3122
|
+
}
|
|
3123
|
+
)
|
|
3124
|
+
|
|
3125
|
+
return {
|
|
3126
|
+
"ok": all(bool(item.get("ok")) for item in checks),
|
|
3127
|
+
"agent_id": detail.get("agent_id"),
|
|
3128
|
+
"name": detail.get("name"),
|
|
3129
|
+
"region": resolved_region,
|
|
3130
|
+
"channel": normalized_channel,
|
|
3131
|
+
"checks": checks,
|
|
3132
|
+
"snapshot": snapshot,
|
|
3133
|
+
}
|
|
3134
|
+
|
|
3135
|
+
try:
|
|
3136
|
+
payload = asyncio.run(_run())
|
|
3137
|
+
_emit_data_payload("OpenClaw Channel Doctor", payload, subtitle=str(payload.get("name") or "-"))
|
|
3138
|
+
except Exception as e:
|
|
3139
|
+
_abort_openclaw_error(e, context="channel doctor 执行失败", argv=["openclaw", "channel", "doctor"])
|
|
3140
|
+
|
|
3141
|
+
|
|
3142
|
+
@openclaw.command("deploy", context_settings=CONTEXT_SETTINGS)
|
|
3143
|
+
@click.option("--name", "-n", default=None, help="OpenClaw 名称 (默认: openclaw-gateway)")
|
|
3144
|
+
@click.option(
|
|
3145
|
+
"--region", "-r",
|
|
3146
|
+
default="cn-beijing-6",
|
|
3147
|
+
envvar="KSYUN_REGION",
|
|
3148
|
+
help="部署区域 (默认: cn-beijing-6)",
|
|
3149
|
+
)
|
|
3150
|
+
@click.option(
|
|
3151
|
+
"--security-profile",
|
|
3152
|
+
type=click.Choice(OPENCLAW_SECURITY_PROFILES, case_sensitive=False),
|
|
3153
|
+
default=None,
|
|
3154
|
+
help="安全预设: relaxed | strict | strictest (安全测试建议 strictest)",
|
|
3155
|
+
)
|
|
3156
|
+
@click.option("--strict-mode", "security_profile", flag_value="strict", help="快捷开启严格模式")
|
|
3157
|
+
@click.option("--strictest", "security_profile", flag_value="strictest", help="快捷开启最严格安全模式")
|
|
3158
|
+
@click.option(
|
|
3159
|
+
"--image",
|
|
3160
|
+
default=None,
|
|
3161
|
+
help="OpenClaw 镜像地址 (默认: 内置公共镜像;也可用 OPENCLAW_IMAGE/OPENCLAW_DOCKER_IMAGE)",
|
|
3162
|
+
)
|
|
3163
|
+
@click.option("--model-base-url", default=None, help="模型 Base URL (默认复用 OPENAI_BASE_URL)")
|
|
3164
|
+
@click.option("--model-api-key", default=None, help="模型 API Key (可选;默认复用 OPENAI_API_KEY)")
|
|
3165
|
+
@click.option("--default-model", default=None, help="默认模型名 (默认复用 OPENAI_MODEL_NAME)")
|
|
3166
|
+
@click.option(
|
|
3167
|
+
"--memory-system",
|
|
3168
|
+
type=click.Choice(["openclaw_default", "mem0"], case_sensitive=False),
|
|
3169
|
+
default=None,
|
|
3170
|
+
help="记忆后端类型(默认不显式变更当前服务端配置)",
|
|
3171
|
+
)
|
|
3172
|
+
@click.option("--mem0-instance-id", default=None, help="mem0 实例 ID")
|
|
3173
|
+
@click.option("--mem0-instance-name", default=None, help="mem0 实例名称(可选)")
|
|
3174
|
+
@click.option("--mem0-region", default=None, help="mem0 实例区域(可选)")
|
|
3175
|
+
@click.option(
|
|
3176
|
+
"--env",
|
|
3177
|
+
"extra_env",
|
|
3178
|
+
multiple=True,
|
|
3179
|
+
help="额外透传自定义环境变量,格式 KEY=VALUE,可重复传入",
|
|
3180
|
+
)
|
|
3181
|
+
@click.option("--storage-size-gi", type=int, default=20, show_default=True, help="PVC 容量(Gi)")
|
|
3182
|
+
@click.option("--storage-mount-path", default=None, help="PVC 挂载目录(默认: /home/node/.openclaw)")
|
|
3183
|
+
@click.option("--no-storage", is_flag=True, help="禁用默认 PVC 挂载")
|
|
3184
|
+
@network_options
|
|
3185
|
+
@dry_run_option("仅显示请求,不实际部署")
|
|
3186
|
+
def deploy(
|
|
3187
|
+
name: Optional[str],
|
|
3188
|
+
region: str,
|
|
3189
|
+
security_profile: Optional[str],
|
|
3190
|
+
image: Optional[str],
|
|
3191
|
+
model_base_url: Optional[str],
|
|
3192
|
+
model_api_key: Optional[str],
|
|
3193
|
+
default_model: Optional[str],
|
|
3194
|
+
memory_system: Optional[str],
|
|
3195
|
+
mem0_instance_id: Optional[str],
|
|
3196
|
+
mem0_instance_name: Optional[str],
|
|
3197
|
+
mem0_region: Optional[str],
|
|
3198
|
+
extra_env: tuple[str, ...],
|
|
3199
|
+
storage_size_gi: int,
|
|
3200
|
+
storage_mount_path: Optional[str],
|
|
3201
|
+
no_storage: bool,
|
|
3202
|
+
enable_public_access: Optional[bool],
|
|
3203
|
+
enable_vpc_access: bool,
|
|
3204
|
+
vpc_id: Optional[str],
|
|
3205
|
+
subnet_id: Optional[str],
|
|
3206
|
+
security_group_id: Optional[str],
|
|
3207
|
+
availability_zone: Optional[str],
|
|
3208
|
+
dry_run: bool,
|
|
3209
|
+
):
|
|
3210
|
+
"""部署 OpenClaw 到云端
|
|
3211
|
+
|
|
3212
|
+
\b
|
|
3213
|
+
通过 CreateAgentProduct (Container 模式) 部署预构建的 OpenClaw 镜像。
|
|
3214
|
+
模型配置自动复用 OPENAI_* 环境变量。
|
|
3215
|
+
|
|
3216
|
+
\b
|
|
3217
|
+
示例:
|
|
3218
|
+
# 默认部署 (自动复用 .env 中的 OPENAI_* 变量)
|
|
3219
|
+
agentengine openclaw deploy
|
|
3220
|
+
# 显式开启严格模式
|
|
3221
|
+
agentengine openclaw deploy --strict-mode
|
|
3222
|
+
# 一键创建最严格实例(适合安全测试)
|
|
3223
|
+
agentengine openclaw deploy --security-profile strictest
|
|
3224
|
+
# 显式指定模型
|
|
3225
|
+
agentengine openclaw deploy --model-base-url https://api.example.com/v1 --model-api-key sk-xxx
|
|
3226
|
+
# 使用自定义镜像
|
|
3227
|
+
agentengine openclaw deploy --image ghcr.io/my-org/openclaw:v2
|
|
3228
|
+
# 透传业务自定义环境变量
|
|
3229
|
+
agentengine openclaw deploy --env APP_MODE=prod --env API_BASE=https://example.com
|
|
3230
|
+
"""
|
|
3231
|
+
dry_run = effective_dry_run(dry_run)
|
|
3232
|
+
ctx = click.get_current_context(silent=True)
|
|
3233
|
+
include_env_on_update = any(
|
|
3234
|
+
(
|
|
3235
|
+
_option_was_explicit(ctx, "security_profile"),
|
|
3236
|
+
_option_was_explicit(ctx, "model_base_url"),
|
|
3237
|
+
_option_was_explicit(ctx, "model_api_key"),
|
|
3238
|
+
_option_was_explicit(ctx, "default_model"),
|
|
3239
|
+
bool(extra_env),
|
|
3240
|
+
)
|
|
3241
|
+
)
|
|
3242
|
+
include_storage_on_update = any(
|
|
3243
|
+
(
|
|
3244
|
+
_option_was_explicit(ctx, "storage_size_gi"),
|
|
3245
|
+
_option_was_explicit(ctx, "storage_mount_path"),
|
|
3246
|
+
_option_was_explicit(ctx, "no_storage"),
|
|
3247
|
+
)
|
|
3248
|
+
)
|
|
3249
|
+
_build_openclaw_memory_config(
|
|
3250
|
+
memory_system=memory_system,
|
|
3251
|
+
mem0_instance_id=mem0_instance_id,
|
|
3252
|
+
mem0_instance_name=mem0_instance_name,
|
|
3253
|
+
mem0_region=mem0_region,
|
|
3254
|
+
)
|
|
3255
|
+
try:
|
|
3256
|
+
run_async_with_dry_run(
|
|
3257
|
+
_deploy_openclaw(
|
|
3258
|
+
name=name,
|
|
3259
|
+
region=region,
|
|
3260
|
+
security_profile=security_profile,
|
|
3261
|
+
image=image,
|
|
3262
|
+
model_base_url=model_base_url,
|
|
3263
|
+
model_api_key=model_api_key,
|
|
3264
|
+
default_model=default_model,
|
|
3265
|
+
memory_system=memory_system,
|
|
3266
|
+
mem0_instance_id=mem0_instance_id,
|
|
3267
|
+
mem0_instance_name=mem0_instance_name,
|
|
3268
|
+
mem0_region=mem0_region,
|
|
3269
|
+
extra_env=extra_env,
|
|
3270
|
+
storage_size_gi=storage_size_gi,
|
|
3271
|
+
storage_mount_path=storage_mount_path,
|
|
3272
|
+
no_storage=no_storage,
|
|
3273
|
+
include_env_on_update=include_env_on_update,
|
|
3274
|
+
include_storage_on_update=include_storage_on_update,
|
|
3275
|
+
**network_cli_kwargs(
|
|
3276
|
+
enable_public_access=enable_public_access,
|
|
3277
|
+
enable_vpc_access=enable_vpc_access,
|
|
3278
|
+
vpc_id=vpc_id,
|
|
3279
|
+
subnet_id=subnet_id,
|
|
3280
|
+
security_group_id=security_group_id,
|
|
3281
|
+
availability_zone=availability_zone,
|
|
3282
|
+
),
|
|
3283
|
+
dry_run=dry_run,
|
|
3284
|
+
),
|
|
3285
|
+
dry_run=dry_run,
|
|
3286
|
+
)
|
|
3287
|
+
except Exception as e:
|
|
3288
|
+
_abort_openclaw_error(e, context="部署失败", argv=["openclaw", "deploy"])
|
|
3289
|
+
|
|
3290
|
+
|
|
3291
|
+
async def _deploy_openclaw(
|
|
3292
|
+
*,
|
|
3293
|
+
name: Optional[str],
|
|
3294
|
+
region: str,
|
|
3295
|
+
security_profile: Optional[str],
|
|
3296
|
+
image: Optional[str],
|
|
3297
|
+
model_base_url: Optional[str],
|
|
3298
|
+
model_api_key: Optional[str],
|
|
3299
|
+
default_model: Optional[str],
|
|
3300
|
+
memory_system: Optional[str],
|
|
3301
|
+
mem0_instance_id: Optional[str],
|
|
3302
|
+
mem0_instance_name: Optional[str],
|
|
3303
|
+
mem0_region: Optional[str],
|
|
3304
|
+
extra_env: tuple[str, ...] = (),
|
|
3305
|
+
storage_size_gi: int = 20,
|
|
3306
|
+
storage_mount_path: Optional[str] = None,
|
|
3307
|
+
no_storage: bool = False,
|
|
3308
|
+
include_env_on_update: bool = False,
|
|
3309
|
+
include_storage_on_update: bool = False,
|
|
3310
|
+
enable_public_access: Optional[bool] = None,
|
|
3311
|
+
enable_vpc_access: bool = False,
|
|
3312
|
+
vpc_id: Optional[str] = None,
|
|
3313
|
+
subnet_id: Optional[str] = None,
|
|
3314
|
+
security_group_id: Optional[str] = None,
|
|
3315
|
+
availability_zone: Optional[str] = None,
|
|
3316
|
+
dry_run: bool,
|
|
3317
|
+
):
|
|
3318
|
+
"""异步部署 OpenClaw"""
|
|
3319
|
+
from ksadk.api import AgentEngineClient
|
|
3320
|
+
from ksadk.deployment.state import load_state, save_state, clear_state
|
|
3321
|
+
from dotenv import dotenv_values
|
|
3322
|
+
|
|
3323
|
+
# 自动加载当前目录 .env(仅补充未导出的变量,不覆盖已导出的 shell 环境)
|
|
3324
|
+
project_dir = Path(".").resolve()
|
|
3325
|
+
env_file = project_dir / ".env"
|
|
3326
|
+
if env_file.exists():
|
|
3327
|
+
try:
|
|
3328
|
+
loaded = 0
|
|
3329
|
+
for k, v in dotenv_values(env_file).items():
|
|
3330
|
+
if not k or v is None:
|
|
3331
|
+
continue
|
|
3332
|
+
if os.getenv(k) is None:
|
|
3333
|
+
os.environ[k] = str(v)
|
|
3334
|
+
loaded += 1
|
|
3335
|
+
if loaded:
|
|
3336
|
+
print_info(f"已从 .env 注入环境变量: {loaded} 项")
|
|
3337
|
+
except Exception as e:
|
|
3338
|
+
print_warn(f"读取 .env 失败,将继续使用当前 shell 环境: {e}")
|
|
3339
|
+
|
|
3340
|
+
# 读取本地状态 (判断创建 vs 更新)
|
|
3341
|
+
state = load_state(project_dir)
|
|
3342
|
+
existing_agent_id = None
|
|
3343
|
+
state_name = None
|
|
3344
|
+
state_kind = str(state.get("type") or state.get("framework") or "").strip().lower()
|
|
3345
|
+
if state_kind == "openclaw":
|
|
3346
|
+
existing_agent_id = state.get("agent_id")
|
|
3347
|
+
state_name = str(state.get("name") or "").strip() or None
|
|
3348
|
+
|
|
3349
|
+
if name:
|
|
3350
|
+
openclaw_name = name
|
|
3351
|
+
elif state_name:
|
|
3352
|
+
openclaw_name = state_name
|
|
3353
|
+
elif project_name := _load_openclaw_project_name(project_dir):
|
|
3354
|
+
openclaw_name = project_name
|
|
3355
|
+
else:
|
|
3356
|
+
openclaw_name = _generate_default_openclaw_name()
|
|
3357
|
+
resolved_image = image or _resolve_env("OPENCLAW_IMAGE", "OPENCLAW_DOCKER_IMAGE")
|
|
3358
|
+
bootstrap_cfg: Optional[Dict[str, Any]] = None
|
|
3359
|
+
if not resolved_image:
|
|
3360
|
+
bootstrap_cfg = await _fetch_bootstrap_config(region)
|
|
3361
|
+
server_default_image = _extract_bootstrap_image(bootstrap_cfg)
|
|
3362
|
+
if server_default_image:
|
|
3363
|
+
resolved_image = server_default_image
|
|
3364
|
+
print_info(f"未指定镜像,使用服务端默认镜像: {resolved_image}")
|
|
3365
|
+
_print_bootstrap_hints(bootstrap_cfg)
|
|
3366
|
+
image_ref = _resolve_image_ref(resolved_image)
|
|
3367
|
+
|
|
3368
|
+
# 构建环境变量
|
|
3369
|
+
env_vars = _build_openclaw_env_vars(
|
|
3370
|
+
model_base_url=model_base_url,
|
|
3371
|
+
model_api_key=model_api_key,
|
|
3372
|
+
default_model=default_model,
|
|
3373
|
+
security_profile=security_profile,
|
|
3374
|
+
)
|
|
3375
|
+
custom_env_vars = _parse_extra_openclaw_env_pairs(extra_env)
|
|
3376
|
+
if custom_env_vars:
|
|
3377
|
+
env_vars.update(custom_env_vars)
|
|
3378
|
+
env_vars = _normalize_openclaw_gateway_auth_env(env_vars)
|
|
3379
|
+
if not str(env_vars.get("OPENCLAW_MODEL_CATALOG_JSON") or "").strip():
|
|
3380
|
+
catalog_api_base = (
|
|
3381
|
+
model_base_url
|
|
3382
|
+
or _resolve_env("OPENCLAW_MODEL_BASE_URL", "OPENAI_BASE_URL", "OPENAI_API_BASE", "LLM_API_BASE", "MODEL_API_BASE")
|
|
3383
|
+
)
|
|
3384
|
+
catalog_api_key = (
|
|
3385
|
+
model_api_key
|
|
3386
|
+
or _resolve_env("OPENCLAW_MODEL_API_KEY", "OPENAI_API_KEY", "LLM_API_KEY", "MODEL_API_KEY")
|
|
3387
|
+
)
|
|
3388
|
+
provider_catalog = await fetch_provider_model_catalog(
|
|
3389
|
+
api_base=catalog_api_base,
|
|
3390
|
+
api_key=catalog_api_key,
|
|
3391
|
+
)
|
|
3392
|
+
selected_catalog = _filter_openclaw_provider_catalog(env_vars, provider_catalog)
|
|
3393
|
+
if _apply_openclaw_provider_model_catalog(env_vars, selected_catalog):
|
|
3394
|
+
print_info(f"已从模型服务 /v1/models 同步 OpenClaw 模型元数据: {len(selected_catalog)} 个")
|
|
3395
|
+
memory_config = _build_openclaw_memory_config(
|
|
3396
|
+
memory_system=memory_system,
|
|
3397
|
+
mem0_instance_id=mem0_instance_id,
|
|
3398
|
+
mem0_instance_name=mem0_instance_name,
|
|
3399
|
+
mem0_region=mem0_region,
|
|
3400
|
+
)
|
|
3401
|
+
|
|
3402
|
+
print_title("OpenClaw 云端部署", f"region: {region}")
|
|
3403
|
+
print_kv("名称", openclaw_name)
|
|
3404
|
+
print_kv("镜像", image_ref)
|
|
3405
|
+
print_kv("区域", region, value_style="#58a6ff")
|
|
3406
|
+
if security_profile:
|
|
3407
|
+
print_kv("安全预设", security_profile.lower())
|
|
3408
|
+
if memory_config:
|
|
3409
|
+
print_kv("记忆后端", str(memory_config.get("memory_system") or "").strip())
|
|
3410
|
+
if memory_config.get("mem0_instance_id"):
|
|
3411
|
+
print_kv("mem0 实例", str(memory_config["mem0_instance_id"]))
|
|
3412
|
+
|
|
3413
|
+
# 构建环境变量列表
|
|
3414
|
+
env_list = [
|
|
3415
|
+
{"Key": k, "Value": v, "IsSensitive": "KEY" in k or "TOKEN" in k or "SECRET" in k}
|
|
3416
|
+
for k, v in env_vars.items()
|
|
3417
|
+
]
|
|
3418
|
+
# 资源规格(支持通过环境变量覆盖)
|
|
3419
|
+
cpu = _resolve_env("OPENCLAW_CPU") or "2"
|
|
3420
|
+
memory = _resolve_env("OPENCLAW_MEMORY") or "4Gi"
|
|
3421
|
+
|
|
3422
|
+
# 构建请求数据
|
|
3423
|
+
request_data = {
|
|
3424
|
+
"name": openclaw_name,
|
|
3425
|
+
"description": "OpenClaw Gateway (managed by AgentEngine)",
|
|
3426
|
+
"framework": "openclaw",
|
|
3427
|
+
"artifact_type": "Container",
|
|
3428
|
+
"artifact_path": image_ref,
|
|
3429
|
+
"region": region,
|
|
3430
|
+
"resources": {"cpu": cpu, "memory": memory},
|
|
3431
|
+
"scaling": {"min_replicas": 1, "max_replicas": 1, "concurrency": 1000},
|
|
3432
|
+
"env_vars": env_list,
|
|
3433
|
+
# OpenClaw UI 需要浏览器直开;默认关闭平台层 ApiKey 保护,避免 dashboard 401
|
|
3434
|
+
"auth_type": "None",
|
|
3435
|
+
"inbound_identity_auth": "None",
|
|
3436
|
+
}
|
|
3437
|
+
if memory_config:
|
|
3438
|
+
request_data["memory_config"] = memory_config
|
|
3439
|
+
storage_config = build_storage_config(
|
|
3440
|
+
"openclaw",
|
|
3441
|
+
no_storage=no_storage,
|
|
3442
|
+
mount_path=storage_mount_path,
|
|
3443
|
+
size_gi=storage_size_gi,
|
|
3444
|
+
)
|
|
3445
|
+
if storage_config:
|
|
3446
|
+
request_data["storage"] = storage_config
|
|
3447
|
+
if existing_agent_id and include_storage_on_update and no_storage:
|
|
3448
|
+
print_warn("更新已有 OpenClaw 时 `--no-storage` 不会删除服务端既有挂盘配置;默认保留已有配置。")
|
|
3449
|
+
network_payload = build_network_payload(
|
|
3450
|
+
enable_public_access=enable_public_access,
|
|
3451
|
+
enable_vpc_access=enable_vpc_access,
|
|
3452
|
+
vpc_id=vpc_id,
|
|
3453
|
+
subnet_id=subnet_id,
|
|
3454
|
+
security_group_id=security_group_id,
|
|
3455
|
+
availability_zone=availability_zone,
|
|
3456
|
+
region=region,
|
|
3457
|
+
dry_run=dry_run,
|
|
3458
|
+
)
|
|
3459
|
+
# create 默认开公网(network_payload 未显式 enable_public_access 时补 True);update 分支用原始 network_payload(None=保留现有配置)
|
|
3460
|
+
create_network_payload = dict(network_payload) if network_payload is not None else {}
|
|
3461
|
+
if "enable_public_access" not in create_network_payload:
|
|
3462
|
+
create_network_payload["enable_public_access"] = True
|
|
3463
|
+
if create_network_payload:
|
|
3464
|
+
request_data["network"] = create_network_payload
|
|
3465
|
+
|
|
3466
|
+
# 镜像凭证:按目标镜像地址判断仓库类型,避免企业版/第三方误用 KSYUN_ACCOUNT_ID。
|
|
3467
|
+
image_credential = None
|
|
3468
|
+
kcr_username, kcr_password, registry_kind = resolve_registry_credentials(
|
|
3469
|
+
image_ref,
|
|
3470
|
+
environ=_openclaw_registry_env(),
|
|
3471
|
+
)
|
|
3472
|
+
if kcr_username and kcr_password:
|
|
3473
|
+
image_credential = {
|
|
3474
|
+
"username": kcr_username,
|
|
3475
|
+
"password": kcr_password,
|
|
3476
|
+
}
|
|
3477
|
+
request_data["image_credential"] = image_credential
|
|
3478
|
+
elif kcr_password and not kcr_username and registry_kind != "personal_kcr":
|
|
3479
|
+
print_warn(
|
|
3480
|
+
f"检测到 KCR_PASSWORD 但缺少 KCR_USERNAME,已忽略{registry_kind_label(registry_kind)}镜像凭证;"
|
|
3481
|
+
"企业版 KCR 和第三方镜像仓库必须配置 KCR_USERNAME + KCR_PASSWORD"
|
|
3482
|
+
)
|
|
3483
|
+
elif "/agentengine-public/" not in image_ref:
|
|
3484
|
+
if registry_kind == "personal_kcr":
|
|
3485
|
+
print_warn("未配置个人版 KCR 镜像凭证 (KSYUN_ACCOUNT_ID/KCR_PASSWORD),私有镜像可能无法拉取")
|
|
3486
|
+
else:
|
|
3487
|
+
print_warn(
|
|
3488
|
+
f"未配置{registry_kind_label(registry_kind)}镜像凭证 "
|
|
3489
|
+
"(KCR_USERNAME/KCR_PASSWORD),私有镜像可能无法拉取"
|
|
3490
|
+
)
|
|
3491
|
+
|
|
3492
|
+
if dry_run:
|
|
3493
|
+
async with AgentEngineClient(region=region, dry_run=True) as client:
|
|
3494
|
+
if existing_agent_id:
|
|
3495
|
+
update_payload = _build_openclaw_update_payload(
|
|
3496
|
+
image_ref=image_ref,
|
|
3497
|
+
cpu=cpu,
|
|
3498
|
+
memory=memory,
|
|
3499
|
+
env_list=env_list,
|
|
3500
|
+
memory_config=memory_config,
|
|
3501
|
+
image_credential=image_credential,
|
|
3502
|
+
storage_config=storage_config,
|
|
3503
|
+
network_payload=network_payload,
|
|
3504
|
+
include_env=include_env_on_update,
|
|
3505
|
+
include_storage=include_storage_on_update,
|
|
3506
|
+
)
|
|
3507
|
+
await client.update_agent(existing_agent_id, update_payload)
|
|
3508
|
+
else:
|
|
3509
|
+
await client.create_agent(request_data)
|
|
3510
|
+
return
|
|
3511
|
+
|
|
3512
|
+
# 调用 API
|
|
3513
|
+
print_rule("部署 OpenClaw")
|
|
3514
|
+
try:
|
|
3515
|
+
latest_status = None
|
|
3516
|
+
updated_existing_agent = False
|
|
3517
|
+
async with AgentEngineClient(region=region) as client:
|
|
3518
|
+
if existing_agent_id:
|
|
3519
|
+
print_info(f"检测到本地状态: {existing_agent_id},执行更新...")
|
|
3520
|
+
try:
|
|
3521
|
+
update_payload = _build_openclaw_update_payload(
|
|
3522
|
+
image_ref=image_ref,
|
|
3523
|
+
cpu=cpu,
|
|
3524
|
+
memory=memory,
|
|
3525
|
+
env_list=env_list,
|
|
3526
|
+
memory_config=memory_config,
|
|
3527
|
+
image_credential=image_credential,
|
|
3528
|
+
storage_config=storage_config,
|
|
3529
|
+
network_payload=network_payload,
|
|
3530
|
+
include_env=include_env_on_update,
|
|
3531
|
+
include_storage=include_storage_on_update,
|
|
3532
|
+
)
|
|
3533
|
+
res = await client.update_agent(existing_agent_id, update_payload)
|
|
3534
|
+
agent_id = existing_agent_id
|
|
3535
|
+
endpoint = res.get("endpoint") or state.get("endpoint")
|
|
3536
|
+
api_key = state.get("api_key")
|
|
3537
|
+
updated_existing_agent = True
|
|
3538
|
+
except Exception as update_err:
|
|
3539
|
+
err_msg = str(update_err)
|
|
3540
|
+
not_found = (
|
|
3541
|
+
"code: 404" in err_msg.lower()
|
|
3542
|
+
or "agent not found" in err_msg.lower()
|
|
3543
|
+
)
|
|
3544
|
+
if not not_found:
|
|
3545
|
+
raise
|
|
3546
|
+
print_warn(f"本地状态失效 ({existing_agent_id}),将自动回退为新建: {update_err}")
|
|
3547
|
+
cleared = clear_state(project_dir, key=existing_agent_id)
|
|
3548
|
+
if cleared:
|
|
3549
|
+
print_info("已清理失效的 .agentengine.state")
|
|
3550
|
+
existing_agent_id = None
|
|
3551
|
+
|
|
3552
|
+
if not existing_agent_id:
|
|
3553
|
+
res = await client.create_agent(request_data)
|
|
3554
|
+
if not res:
|
|
3555
|
+
raise Exception("Server 返回空响应,请查看 Server 日志")
|
|
3556
|
+
|
|
3557
|
+
# CreateAgentProduct 返回 order_id,需要轮询获取 agent_id
|
|
3558
|
+
order_id = res.get("order_id")
|
|
3559
|
+
agent_id = res.get("agent_id")
|
|
3560
|
+
endpoint = res.get("endpoint")
|
|
3561
|
+
api_key = res.get("api_key")
|
|
3562
|
+
|
|
3563
|
+
if order_id and not agent_id:
|
|
3564
|
+
print_info(f"订单已创建: {order_id},等待实例创建...")
|
|
3565
|
+
latest = await get_latest_agent_access(
|
|
3566
|
+
client,
|
|
3567
|
+
agent_name=openclaw_name,
|
|
3568
|
+
attempts=12,
|
|
3569
|
+
interval_seconds=5,
|
|
3570
|
+
include_api_key=True,
|
|
3571
|
+
detail_fetcher=lambda agent_ref, include_api_key: _get_openclaw_detail_with_client(
|
|
3572
|
+
client,
|
|
3573
|
+
agent_ref,
|
|
3574
|
+
include_api_key=include_api_key,
|
|
3575
|
+
),
|
|
3576
|
+
suppress_transient_not_found_log=True,
|
|
3577
|
+
)
|
|
3578
|
+
if latest:
|
|
3579
|
+
agent_id = latest.get("agent_id") or agent_id
|
|
3580
|
+
endpoint = latest.get("endpoint") or endpoint
|
|
3581
|
+
api_key = latest.get("api_key") or api_key
|
|
3582
|
+
print_success(f"实例已创建: {agent_id}")
|
|
3583
|
+
else:
|
|
3584
|
+
print_warn("实例创建中,稍后使用 'agentengine openclaw list' 查看")
|
|
3585
|
+
elif agent_id and (
|
|
3586
|
+
not str(endpoint or "").strip()
|
|
3587
|
+
or not str(api_key or "").strip()
|
|
3588
|
+
):
|
|
3589
|
+
latest = await get_latest_agent_access(
|
|
3590
|
+
client,
|
|
3591
|
+
agent_id=str(agent_id).strip() or None,
|
|
3592
|
+
attempts=5,
|
|
3593
|
+
interval_seconds=1,
|
|
3594
|
+
initial_delay_seconds=2,
|
|
3595
|
+
require_complete_access=True,
|
|
3596
|
+
include_api_key=True,
|
|
3597
|
+
detail_fetcher=lambda agent_ref, include_api_key: _get_openclaw_detail_with_client(
|
|
3598
|
+
client,
|
|
3599
|
+
agent_ref,
|
|
3600
|
+
include_api_key=include_api_key,
|
|
3601
|
+
),
|
|
3602
|
+
suppress_transient_not_found_log=True,
|
|
3603
|
+
)
|
|
3604
|
+
if latest:
|
|
3605
|
+
agent_id = latest.get("agent_id") or agent_id
|
|
3606
|
+
endpoint = latest.get("endpoint") or endpoint
|
|
3607
|
+
api_key = latest.get("api_key") or api_key
|
|
3608
|
+
|
|
3609
|
+
# 保存状态
|
|
3610
|
+
saved_name = openclaw_name if not state.get("name") else state.get("name")
|
|
3611
|
+
if not existing_agent_id:
|
|
3612
|
+
saved_name = openclaw_name
|
|
3613
|
+
|
|
3614
|
+
state_payload = {
|
|
3615
|
+
"type": "openclaw",
|
|
3616
|
+
"framework": "openclaw",
|
|
3617
|
+
"agent_id": agent_id,
|
|
3618
|
+
"name": saved_name,
|
|
3619
|
+
"region": region,
|
|
3620
|
+
"endpoint": endpoint,
|
|
3621
|
+
"api_key": api_key,
|
|
3622
|
+
"image": image_ref,
|
|
3623
|
+
"openclaw_auth_mode": env_vars.get("OPENCLAW_GATEWAY_AUTH_MODE"),
|
|
3624
|
+
}
|
|
3625
|
+
state_gateway_token = _openclaw_state_gateway_token(env_vars)
|
|
3626
|
+
if state_gateway_token:
|
|
3627
|
+
state_payload["openclaw_gateway_token"] = state_gateway_token
|
|
3628
|
+
save_state(project_dir, state_payload)
|
|
3629
|
+
|
|
3630
|
+
# 仅在更新已有实例时回读一次状态;新建时底层可能尚未落库,立即按 ID 查询会产生误导性报错。
|
|
3631
|
+
if updated_existing_agent and agent_id:
|
|
3632
|
+
try:
|
|
3633
|
+
latest = await client.get_agent(agent_id=agent_id, include_api_key=False)
|
|
3634
|
+
latest_status = str(((latest.get("basic") or {}).get("status") or "")).upper() or None
|
|
3635
|
+
except Exception:
|
|
3636
|
+
latest_status = None
|
|
3637
|
+
|
|
3638
|
+
print_success("OpenClaw 已提交部署")
|
|
3639
|
+
print_kv("Agent ID", agent_id or "(创建中)")
|
|
3640
|
+
if latest_status:
|
|
3641
|
+
print_kv("当前状态", latest_status)
|
|
3642
|
+
if endpoint:
|
|
3643
|
+
print_kv("Endpoint", endpoint, value_style="#58a6ff")
|
|
3644
|
+
if api_key:
|
|
3645
|
+
print_kv("API Key", api_key, value_style="#d29922")
|
|
3646
|
+
print_info("已保存状态到 .agentengine.state")
|
|
3647
|
+
print_info("建议先确认实例状态:")
|
|
3648
|
+
print_info(" agentengine openclaw status")
|
|
3649
|
+
if latest_status != "RUNNING":
|
|
3650
|
+
print_info("实例进入 RUNNING 后再打开 Dashboard:")
|
|
3651
|
+
else:
|
|
3652
|
+
print_info("可直接打开 Dashboard:")
|
|
3653
|
+
if agent_id:
|
|
3654
|
+
print_info(f" agentengine dashboard {agent_id}")
|
|
3655
|
+
else:
|
|
3656
|
+
print_info(" agentengine dashboard")
|
|
3657
|
+
|
|
3658
|
+
except DryRunExit:
|
|
3659
|
+
raise
|
|
3660
|
+
|
|
3661
|
+
|
|
3662
|
+
@openclaw.command("list", context_settings=CONTEXT_SETTINGS)
|
|
3663
|
+
@region_option()
|
|
3664
|
+
@pagination_options(default_page=1, default_size=20)
|
|
3665
|
+
@dry_run_option()
|
|
3666
|
+
@cli_output_option()
|
|
3667
|
+
def list_openclaws(region: str, page: int, size: int, dry_run: bool, output_mode: str | None):
|
|
3668
|
+
"""列出已部署的 OpenClaw 实例"""
|
|
3669
|
+
_ = output_mode
|
|
3670
|
+
dry_run = effective_dry_run(dry_run)
|
|
3671
|
+
from ksadk.api import AgentEngineClient
|
|
3672
|
+
|
|
3673
|
+
async def _list():
|
|
3674
|
+
async with AgentEngineClient(region=region, dry_run=dry_run) as client:
|
|
3675
|
+
resp = await client.list_agents(region=region, framework="openclaw", page=page, page_size=size)
|
|
3676
|
+
agents = resp.get("agents", []) or []
|
|
3677
|
+
total = int(resp.get("total") or len(agents))
|
|
3678
|
+
rows = []
|
|
3679
|
+
items = []
|
|
3680
|
+
for a in agents:
|
|
3681
|
+
status = (a.get("status") or "UNKNOWN").upper()
|
|
3682
|
+
rows.append(
|
|
3683
|
+
(
|
|
3684
|
+
str(a.get("agent_id", "-")),
|
|
3685
|
+
str(a.get("name", "-")),
|
|
3686
|
+
f"[{status_rich_style(status)}]{status}[/]",
|
|
3687
|
+
str(a.get("endpoint", "N/A")),
|
|
3688
|
+
str(a.get("region", "-")),
|
|
3689
|
+
)
|
|
3690
|
+
)
|
|
3691
|
+
items.append(
|
|
3692
|
+
{
|
|
3693
|
+
"id": str(a.get("agent_id", "-")),
|
|
3694
|
+
"name": str(a.get("name", "-")),
|
|
3695
|
+
"status": status,
|
|
3696
|
+
"endpoint": str(a.get("endpoint", "N/A")),
|
|
3697
|
+
"region": str(a.get("region", "-")),
|
|
3698
|
+
}
|
|
3699
|
+
)
|
|
3700
|
+
|
|
3701
|
+
if not render_descriptor_list(
|
|
3702
|
+
OPENCLAW_RESOURCE,
|
|
3703
|
+
rows=rows,
|
|
3704
|
+
total=total,
|
|
3705
|
+
page=page,
|
|
3706
|
+
size=size,
|
|
3707
|
+
items=items,
|
|
3708
|
+
):
|
|
3709
|
+
return
|
|
3710
|
+
|
|
3711
|
+
account_summary = _summarize_openclaw_account(agents)
|
|
3712
|
+
region_summary = _summarize_openclaw_region(agents, region)
|
|
3713
|
+
print_info(f"账号: {account_summary} region: {region_summary} 总计: {total}")
|
|
3714
|
+
|
|
3715
|
+
try:
|
|
3716
|
+
run_async_with_dry_run(
|
|
3717
|
+
_list(),
|
|
3718
|
+
dry_run=dry_run,
|
|
3719
|
+
dry_run_resource="openclaw",
|
|
3720
|
+
dry_run_action="list",
|
|
3721
|
+
)
|
|
3722
|
+
except Exception as e:
|
|
3723
|
+
_abort_openclaw_error(e, context="获取列表失败", argv=["openclaw", "list"])
|
|
3724
|
+
|
|
3725
|
+
|
|
3726
|
+
@openclaw.command("status", context_settings=CONTEXT_SETTINGS)
|
|
3727
|
+
@click.argument("agent_ref", required=False, default=None)
|
|
3728
|
+
@region_option(default=None, envvar=None, help_text="区域 (默认优先读取 .agentengine.state)")
|
|
3729
|
+
@dry_run_option()
|
|
3730
|
+
@cli_output_option()
|
|
3731
|
+
def status(agent_ref: Optional[str], region: Optional[str], dry_run: bool, output_mode: str | None):
|
|
3732
|
+
"""查看 OpenClaw 状态
|
|
3733
|
+
|
|
3734
|
+
\b
|
|
3735
|
+
AGENT_REF: Agent ID 或名称 (可选,默认从 .agentengine.state 读取)
|
|
3736
|
+
"""
|
|
3737
|
+
_ = output_mode
|
|
3738
|
+
dry_run = effective_dry_run(dry_run)
|
|
3739
|
+
from ksadk.deployment.state import load_state
|
|
3740
|
+
|
|
3741
|
+
state = load_state(Path(".").resolve())
|
|
3742
|
+
region = _resolve_region(region, state)
|
|
3743
|
+
resolved = resolve_openclaw_ref(agent_ref, cwd=Path(".").resolve(), include_state=True)
|
|
3744
|
+
|
|
3745
|
+
# 无参数时从本地状态读取
|
|
3746
|
+
if not resolved:
|
|
3747
|
+
_abort_openclaw_error(
|
|
3748
|
+
resolution_error(
|
|
3749
|
+
OPENCLAW_RESOURCE.missing_ref_message or "请指定 OpenClaw。",
|
|
3750
|
+
hints=list(OPENCLAW_RESOURCE.resolution_commands),
|
|
3751
|
+
),
|
|
3752
|
+
argv=["openclaw", "status"],
|
|
3753
|
+
)
|
|
3754
|
+
return
|
|
3755
|
+
agent_ref = resolved.value
|
|
3756
|
+
if resolved.source != "cli":
|
|
3757
|
+
print_info(f"未显式指定 OpenClaw,使用 {resolved.source_text}: {agent_ref}")
|
|
3758
|
+
from ksadk.api import AgentEngineClient
|
|
3759
|
+
|
|
3760
|
+
async def _get():
|
|
3761
|
+
async with AgentEngineClient(region=region, dry_run=dry_run) as client:
|
|
3762
|
+
if agent_ref.startswith("ar-"):
|
|
3763
|
+
agent = await client.get_agent(agent_id=agent_ref)
|
|
3764
|
+
else:
|
|
3765
|
+
agent = await client.get_agent(name=agent_ref)
|
|
3766
|
+
|
|
3767
|
+
if not agent:
|
|
3768
|
+
raise resolution_error(f"未找到 OpenClaw: {agent_ref}", hints=["agentengine openclaw list"])
|
|
3769
|
+
|
|
3770
|
+
detail = _flatten_agent_detail(agent)
|
|
3771
|
+
status_val = detail.get("status", "UNKNOWN")
|
|
3772
|
+
created_at_display = _format_cli_timestamp(detail.get("created_at"))
|
|
3773
|
+
updated_at_display = _format_cli_timestamp(detail.get("updated_at"))
|
|
3774
|
+
resolved_agent_id = str(detail.get("agent_id") or agent_ref)
|
|
3775
|
+
render_descriptor_status(
|
|
3776
|
+
OPENCLAW_RESOURCE,
|
|
3777
|
+
subtitle=str(detail.get("name") or agent_ref),
|
|
3778
|
+
fields=[
|
|
3779
|
+
("ID", str(detail.get("agent_id") or "-"), None),
|
|
3780
|
+
("状态", str(status_val), status_rich_style(status_val)),
|
|
3781
|
+
("框架", str(detail.get("framework") or "-"), None),
|
|
3782
|
+
("区域", str(detail.get("region") or region), None),
|
|
3783
|
+
("Endpoint", str(detail.get("endpoint") or "N/A"), "#58a6ff"),
|
|
3784
|
+
("Langfuse", str(detail.get("langfuse_url") or "-"), "#58a6ff" if detail.get("langfuse_url") else None),
|
|
3785
|
+
("镜像", str(detail.get("artifact_path") or "-"), None),
|
|
3786
|
+
("创建时间", created_at_display, None),
|
|
3787
|
+
("更新时间", updated_at_display, None),
|
|
3788
|
+
],
|
|
3789
|
+
item={
|
|
3790
|
+
"id": str(detail.get("agent_id") or "-"),
|
|
3791
|
+
"name": str(detail.get("name") or agent_ref),
|
|
3792
|
+
"status": str(status_val),
|
|
3793
|
+
"framework": str(detail.get("framework") or "-"),
|
|
3794
|
+
"region": str(detail.get("region") or region),
|
|
3795
|
+
"endpoint": str(detail.get("endpoint") or "N/A"),
|
|
3796
|
+
"langfuse_url": str(detail.get("langfuse_url") or ""),
|
|
3797
|
+
"image": str(detail.get("artifact_path") or "-"),
|
|
3798
|
+
"created_at": str(detail.get("created_at") or "-"),
|
|
3799
|
+
"updated_at": str(detail.get("updated_at") or "-"),
|
|
3800
|
+
},
|
|
3801
|
+
next_steps=(
|
|
3802
|
+
f"agentengine invoke {resolved_agent_id}",
|
|
3803
|
+
f"agentengine openclaw tui {resolved_agent_id}",
|
|
3804
|
+
f"agentengine dashboard open {resolved_agent_id} --path /chat",
|
|
3805
|
+
"agentengine openclaw list",
|
|
3806
|
+
),
|
|
3807
|
+
)
|
|
3808
|
+
|
|
3809
|
+
try:
|
|
3810
|
+
run_async_with_dry_run(
|
|
3811
|
+
_get(),
|
|
3812
|
+
dry_run=dry_run,
|
|
3813
|
+
dry_run_resource="openclaw",
|
|
3814
|
+
dry_run_action="status",
|
|
3815
|
+
)
|
|
3816
|
+
except Exception as e:
|
|
3817
|
+
_abort_openclaw_error(e, context="获取状态失败", argv=["openclaw", "status"])
|
|
3818
|
+
|
|
3819
|
+
|
|
3820
|
+
def _delete_impl(agent_refs: tuple[str, ...], region: str, assume_yes: bool, dry_run: bool):
|
|
3821
|
+
"""删除 OpenClaw 实例。
|
|
3822
|
+
|
|
3823
|
+
AGENT_REF: Agent ID
|
|
3824
|
+
"""
|
|
3825
|
+
dry_run = effective_dry_run(dry_run)
|
|
3826
|
+
from ksadk.api import AgentEngineClient
|
|
3827
|
+
|
|
3828
|
+
if not confirm_destructive(
|
|
3829
|
+
assume_yes=assume_yes,
|
|
3830
|
+
dry_run=dry_run,
|
|
3831
|
+
prompt=f"确定要删除这 {len(agent_refs)} 个 OpenClaw 实例吗?",
|
|
3832
|
+
):
|
|
3833
|
+
return
|
|
3834
|
+
|
|
3835
|
+
async def _delete():
|
|
3836
|
+
async with AgentEngineClient(region=region, dry_run=dry_run) as client:
|
|
3837
|
+
failed_refs: list[str] = []
|
|
3838
|
+
deleted_refs: list[str] = []
|
|
3839
|
+
for agent_ref in agent_refs:
|
|
3840
|
+
success = await client.delete_agent(agent_ref)
|
|
3841
|
+
if success:
|
|
3842
|
+
deleted_refs.append(agent_ref)
|
|
3843
|
+
print_success(f"OpenClaw 已删除: {agent_ref}")
|
|
3844
|
+
|
|
3845
|
+
# 清理本地状态
|
|
3846
|
+
from ksadk.deployment.state import clear_state
|
|
3847
|
+
try:
|
|
3848
|
+
removed = clear_state(Path("."), key=agent_ref)
|
|
3849
|
+
if removed:
|
|
3850
|
+
print_info("本地状态文件已清理")
|
|
3851
|
+
else:
|
|
3852
|
+
print_warn("未清理本地状态文件: 当前目录状态与目标 ID 不匹配")
|
|
3853
|
+
except Exception:
|
|
3854
|
+
pass
|
|
3855
|
+
else:
|
|
3856
|
+
failed_refs.append(agent_ref)
|
|
3857
|
+
|
|
3858
|
+
if failed_refs:
|
|
3859
|
+
raise remote_error(
|
|
3860
|
+
f"以下 OpenClaw 删除失败: {', '.join(failed_refs)}",
|
|
3861
|
+
details={"deleted": deleted_refs, "failed": failed_refs},
|
|
3862
|
+
)
|
|
3863
|
+
return {
|
|
3864
|
+
"targets": list(agent_refs),
|
|
3865
|
+
"deleted": deleted_refs,
|
|
3866
|
+
"failed": failed_refs,
|
|
3867
|
+
}
|
|
3868
|
+
|
|
3869
|
+
dry_run_kwargs = {"dry_run": dry_run}
|
|
3870
|
+
if is_json_output():
|
|
3871
|
+
dry_run_kwargs.update(
|
|
3872
|
+
dry_run_resource="openclaw",
|
|
3873
|
+
dry_run_action="delete",
|
|
3874
|
+
)
|
|
3875
|
+
try:
|
|
3876
|
+
result = run_async_with_dry_run(_delete(), **dry_run_kwargs)
|
|
3877
|
+
except Exception as e:
|
|
3878
|
+
_abort_openclaw_error(e, context="删除失败", argv=["openclaw", "delete"])
|
|
3879
|
+
return
|
|
3880
|
+
if result is not None:
|
|
3881
|
+
deleted_text = ", ".join(result["deleted"]) or "-"
|
|
3882
|
+
failed_text = ", ".join(result["failed"]) or "-"
|
|
3883
|
+
render_descriptor_status(
|
|
3884
|
+
OPENCLAW_RESOURCE,
|
|
3885
|
+
title="OpenClaw 删除结果",
|
|
3886
|
+
subtitle=", ".join(result["targets"]) if result["targets"] else "-",
|
|
3887
|
+
fields=[
|
|
3888
|
+
("目标数量", str(len(result["targets"])), None),
|
|
3889
|
+
("已删除", deleted_text, "ok" if result["deleted"] else "muted"),
|
|
3890
|
+
("失败", failed_text, "err" if result["failed"] else "muted"),
|
|
3891
|
+
],
|
|
3892
|
+
next_steps=(
|
|
3893
|
+
"agentengine openclaw list",
|
|
3894
|
+
"agentengine openclaw deploy",
|
|
3895
|
+
),
|
|
3896
|
+
action="delete",
|
|
3897
|
+
item=result,
|
|
3898
|
+
)
|
|
3899
|
+
|
|
3900
|
+
|
|
3901
|
+
@openclaw.command("delete", context_settings=CONTEXT_SETTINGS)
|
|
3902
|
+
@click.argument("agent_refs", nargs=-1, required=True)
|
|
3903
|
+
@region_option()
|
|
3904
|
+
@confirm_options()
|
|
3905
|
+
@dry_run_option()
|
|
3906
|
+
@cli_output_option()
|
|
3907
|
+
def delete(agent_refs: tuple[str, ...], region: str, assume_yes: bool, dry_run: bool, output_mode: str | None):
|
|
3908
|
+
"""删除 OpenClaw 实例。"""
|
|
3909
|
+
_ = output_mode
|
|
3910
|
+
_delete_impl(agent_refs=agent_refs, region=region, assume_yes=assume_yes, dry_run=dry_run)
|
|
3911
|
+
|
|
3912
|
+
|
|
3913
|
+
@openclaw.command("destroy", context_settings=CONTEXT_SETTINGS, hidden=True)
|
|
3914
|
+
@click.argument("agent_refs", nargs=-1, required=True)
|
|
3915
|
+
@region_option()
|
|
3916
|
+
@confirm_options()
|
|
3917
|
+
@dry_run_option()
|
|
3918
|
+
@cli_output_option()
|
|
3919
|
+
def destroy(agent_refs: tuple[str, ...], region: str, assume_yes: bool, dry_run: bool, output_mode: str | None):
|
|
3920
|
+
"""删除 OpenClaw 实例。"""
|
|
3921
|
+
_ = output_mode
|
|
3922
|
+
_delete_impl(agent_refs=agent_refs, region=region, assume_yes=assume_yes, dry_run=dry_run)
|