agentengine-sdk-python 0.6.2__py3-none-any.whl → 0.6.9__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.6.9.dist-info/METADATA +179 -0
- agentengine_sdk_python-0.6.9.dist-info/RECORD +487 -0
- {agentengine_sdk_python-0.6.2.dist-info → agentengine_sdk_python-0.6.9.dist-info}/WHEEL +1 -1
- agentengine_sdk_python-0.6.9.dist-info/entry_points.txt +3 -0
- agentengine_sdk_python-0.6.9.dist-info/licenses/LICENSE +175 -0
- agentengine_sdk_python-0.6.9.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 +2227 -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 +2277 -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 +1259 -0
- ksadk/cli/cmd_invoke.py +1336 -0
- ksadk/cli/cmd_launch.py +564 -0
- ksadk/cli/cmd_mcp.py +1174 -0
- ksadk/cli/cmd_model.py +302 -0
- ksadk/cli/cmd_openclaw.py +3918 -0
- ksadk/cli/cmd_run.py +232 -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 +410 -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 +342 -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/model_context.py +360 -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 +5208 -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 +1105 -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/short_term_memory.py +289 -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 +1135 -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 +539 -0
- ksadk/runners/langgraph_runner.py +852 -0
- ksadk/runners/patch_langchain.py +187 -0
- ksadk/runners/remote_runner.py +667 -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 +3789 -0
- ksadk/server/static/assets/ArtifactsPanel-C0meuOdi.js +1 -0
- ksadk/server/static/assets/CodeBlock-BbI4HPtJ.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-DMLQYoLp.js +8 -0
- ksadk/server/static/assets/MathMessageMarkdown-DeYh8QPr.css +1 -0
- ksadk/server/static/assets/MermaidBlock-DCR8WZ17.js +303 -0
- ksadk/server/static/assets/NativeTerminalPanel-CHOd7Rch.css +1 -0
- ksadk/server/static/assets/NativeTerminalPanel-CKUvWhUz.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-bj2rZnP5.js +1 -0
- ksadk/server/static/assets/architecture-7EHR7CIX-Cr6Xz1_c.js +1 -0
- ksadk/server/static/assets/architectureDiagram-3BPJPVTR-2u1ltDIj.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-D6fvdK20.js +132 -0
- ksadk/server/static/assets/brainfuck-D5EjA2JK.js +1 -0
- ksadk/server/static/assets/c4Diagram-AAUBKEIU-BK82L5n5.js +10 -0
- ksadk/server/static/assets/channel-BSSOwFIb.js +1 -0
- ksadk/server/static/assets/chunk-2J33WTMH-DLVdkeyQ.js +1 -0
- ksadk/server/static/assets/chunk-4BX2VUAB-BbwsYcbW.js +1 -0
- ksadk/server/static/assets/chunk-55IACEB6-DOairxzw.js +1 -0
- ksadk/server/static/assets/chunk-727SXJPM-hCO487gs.js +206 -0
- ksadk/server/static/assets/chunk-AQP2D5EJ-XU8vDOuG.js +231 -0
- ksadk/server/static/assets/chunk-FMBD7UC4-PrapyfMu.js +15 -0
- ksadk/server/static/assets/chunk-ND2GUHAM-CboKhTZ3.js +1 -0
- ksadk/server/static/assets/chunk-QZHKN3VN-XXpdiwUL.js +1 -0
- ksadk/server/static/assets/classDiagram-4FO5ZUOK-C5HvGiyK.js +1 -0
- ksadk/server/static/assets/classDiagram-v2-Q7XG4LA2-C5HvGiyK.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-_N8bJ-EU.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-FoDlo5J2.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-BBp6ebBw.js +43 -0
- ksadk/server/static/assets/diagram-5GNKFQAL-CqOvG1PR.js +10 -0
- ksadk/server/static/assets/diagram-KO2AKTUF-O9T3t6ba.js +3 -0
- ksadk/server/static/assets/diagram-LMA3HP47-CGG8ekBO.js +24 -0
- ksadk/server/static/assets/diagram-OG6HWLK6-4BmH1EEg.js +24 -0
- ksadk/server/static/assets/diff-ChtP43wD.js +1 -0
- ksadk/server/static/assets/dist-6kSZvCZh.js +1 -0
- ksadk/server/static/assets/dist-B67uF2uK.js +1 -0
- ksadk/server/static/assets/dist-BMKM66B4.js +1 -0
- ksadk/server/static/assets/dist-BVzcWa6D.js +1 -0
- ksadk/server/static/assets/dist-Bk1l05Ni.js +1 -0
- ksadk/server/static/assets/dist-BkJjNih3.js +1 -0
- ksadk/server/static/assets/dist-BmuTWcm7.js +9 -0
- ksadk/server/static/assets/dist-C2DKRFVK.js +1 -0
- ksadk/server/static/assets/dist-CWiTDN_V.js +6 -0
- ksadk/server/static/assets/dist-CZ6C3x8B.js +23 -0
- ksadk/server/static/assets/dist-CiZHB6ZW.js +1 -0
- ksadk/server/static/assets/dist-Cp1_jKwf.js +1 -0
- ksadk/server/static/assets/dist-Cwhk4lwI.js +2 -0
- ksadk/server/static/assets/dist-D5JGcxBx.js +11 -0
- ksadk/server/static/assets/dist-D99wf6_X.js +1 -0
- ksadk/server/static/assets/dist-DCpZrjaK.js +1 -0
- ksadk/server/static/assets/dist-DI-oVAXw.js +1 -0
- ksadk/server/static/assets/dist-DLMjL01k.js +1 -0
- ksadk/server/static/assets/dist-DLeM1OCK.js +1 -0
- ksadk/server/static/assets/dist-DM0rEQPk.js +1 -0
- ksadk/server/static/assets/dist-DbaH3p5h.js +1 -0
- ksadk/server/static/assets/dist-S4klkK5W.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-oJhaci7S.js +85 -0
- ksadk/server/static/assets/erlang-DaB2Rkuy.js +1 -0
- ksadk/server/static/assets/eventmodeling-FCH6USID-CIgwHJLL.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-DtTmUan-.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-BSsb_eij.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-BVAJ7uo0.js +1 -0
- ksadk/server/static/assets/gitGraphDiagram-PVQCEYII-BR5s6Pws.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-BH7-pC0B.js +165 -0
- ksadk/server/static/assets/index-DAURbCsP.css +1 -0
- ksadk/server/static/assets/info-J43DQDTF-MT80V4ge.js +1 -0
- ksadk/server/static/assets/infoDiagram-5YYISTIA-DYPc5cvh.js +2 -0
- ksadk/server/static/assets/init-D6jRqBbL.js +1 -0
- ksadk/server/static/assets/ishikawaDiagram-YF4QCWOH-MoBdALDg.js +70 -0
- ksadk/server/static/assets/javascript-9Tg8ixDm.js +1 -0
- ksadk/server/static/assets/journeyDiagram-JHISSGLW-DgLiQT8l.js +139 -0
- ksadk/server/static/assets/julia-CQfgDRfP.js +1 -0
- ksadk/server/static/assets/kanban-definition-UN3LZRKU-CsFx112F.js +89 -0
- ksadk/server/static/assets/katex-DRsiwpM2.js +265 -0
- ksadk/server/static/assets/linear-TNRQ5Orv.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-D2eMVOgo.js +161 -0
- ksadk/server/static/assets/mindmap-definition-RKZ34NQL-BtByYFhL.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-BLKhbXZB.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-DFOikjSY.js +1 -0
- ksadk/server/static/assets/pieDiagram-4H26LBE5-DPnuCB8c.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-miFpCPv4.js +7 -0
- ksadk/server/static/assets/r-DADKenSm.js +1 -0
- ksadk/server/static/assets/radar-GUYGQ44K-095b1Ikw.js +1 -0
- ksadk/server/static/assets/requirementDiagram-4Y6WPE33-mjB_yYkW.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-D7hSlZZZ.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-D9Cx6hC3.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-D_zrg47o.js +1 -0
- ksadk/server/static/assets/stateDiagram-v2-BHNVJYJU-J9kCb0nC.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-CwSec0Wa.js +120 -0
- ksadk/server/static/assets/toml-LvoKBwCs.js +1 -0
- ksadk/server/static/assets/treeView-BLDUP644-yinfJEY6.js +1 -0
- ksadk/server/static/assets/treemap-LRROVOQU-DosUqN1Q.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-agCVobiF.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-Bvp7-5uH.js +1 -0
- ksadk/server/static/assets/wardleyDiagram-YWT4CUSO-TmrlhWNt.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-DV71V-ah.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 +283 -0
- ksadk/sessions/base.py +354 -0
- ksadk/sessions/continuity.py +313 -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 +838 -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 +852 -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 +878 -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 +829 -0
- ksadk/tracing/span_utils.py +114 -0
- ksadk/tui/__init__.py +12 -0
- ksadk/tui/app.py +400 -0
- ksadk/tui/clipboard.py +100 -0
- ksadk/tui/widgets/__init__.py +22 -0
- ksadk/tui/widgets/assistant.py +89 -0
- ksadk/tui/widgets/base.py +54 -0
- ksadk/tui/widgets/chat_input.py +246 -0
- ksadk/tui/widgets/history.py +156 -0
- ksadk/tui/widgets/system.py +87 -0
- ksadk/tui/widgets/thinking.py +110 -0
- ksadk/tui/widgets/tool_call.py +161 -0
- ksadk/tui/widgets/user.py +37 -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-0.6.2.dist-info/METADATA +0 -46
- agentengine_sdk_python-0.6.2.dist-info/RECORD +0 -4
- agentengine_sdk_python-0.6.2.dist-info/top_level.txt +0 -1
ksadk/api/client.py
ADDED
|
@@ -0,0 +1,2227 @@
|
|
|
1
|
+
"""
|
|
2
|
+
AgentEngine Server API 客户端
|
|
3
|
+
|
|
4
|
+
支持 AWS V4 签名认证,用于通过 KOP 网关访问 AgentEngine Server。
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import os
|
|
8
|
+
import re
|
|
9
|
+
import json
|
|
10
|
+
import uuid
|
|
11
|
+
import socket
|
|
12
|
+
import logging
|
|
13
|
+
import mimetypes
|
|
14
|
+
from contextlib import contextmanager
|
|
15
|
+
from dataclasses import dataclass
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
from typing import Optional, Dict, Any, Sequence, Callable, Iterator
|
|
18
|
+
from urllib.parse import quote, unquote, urlparse, urlsplit
|
|
19
|
+
|
|
20
|
+
import requests
|
|
21
|
+
import urllib3
|
|
22
|
+
|
|
23
|
+
from ksadk.common.auth import AWSV4Auth
|
|
24
|
+
|
|
25
|
+
logger = logging.getLogger(__name__)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
HttpErrorLogSuppressor = Callable[..., bool]
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass(frozen=True)
|
|
32
|
+
class AttachmentContent:
|
|
33
|
+
data: bytes
|
|
34
|
+
content_type: str
|
|
35
|
+
display_name: str
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class DryRunExit(Exception):
|
|
39
|
+
"""DryRun 模式退出异常。"""
|
|
40
|
+
|
|
41
|
+
def __init__(self, message: str = "Dry Run finished.", *, payload: Optional[Dict[str, Any]] = None):
|
|
42
|
+
self.payload = payload or {}
|
|
43
|
+
super().__init__(message)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class AgentEngineAPIError(Exception):
|
|
47
|
+
"""服务端 Action API 返回非零 Code 时抛出的结构化异常。"""
|
|
48
|
+
|
|
49
|
+
def __init__(
|
|
50
|
+
self,
|
|
51
|
+
code: Any,
|
|
52
|
+
message: Optional[str] = None,
|
|
53
|
+
*,
|
|
54
|
+
details: Optional[Dict[str, Any]] = None,
|
|
55
|
+
):
|
|
56
|
+
self.raw_code = code
|
|
57
|
+
try:
|
|
58
|
+
self.code = int(code)
|
|
59
|
+
except (TypeError, ValueError):
|
|
60
|
+
self.code = None
|
|
61
|
+
self.message = (message or "Unknown API error").strip() or "Unknown API error"
|
|
62
|
+
self.details = dict(details or {})
|
|
63
|
+
super().__init__(self.__str__())
|
|
64
|
+
|
|
65
|
+
def __str__(self) -> str:
|
|
66
|
+
code_text = self.code if self.code is not None else self.raw_code
|
|
67
|
+
return f"Server API Error (Code: {code_text}): {self.message}"
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class AgentEngineClient:
|
|
71
|
+
"""AgentEngine Server API 客户端
|
|
72
|
+
|
|
73
|
+
使用 AWS V4 签名认证访问 KOP 网关后的 AgentEngine Server。
|
|
74
|
+
|
|
75
|
+
凭证来源 (优先级从高到低):
|
|
76
|
+
1. 构造函数参数
|
|
77
|
+
2. 环境变量 KSYUN_ACCESS_KEY / KSYUN_SECRET_KEY
|
|
78
|
+
3. 环境变量 KS3_ACCESS_KEY / KS3_SECRET_KEY
|
|
79
|
+
"""
|
|
80
|
+
|
|
81
|
+
_DEFAULT_PERMISSION_ROLE = "KsyunAgentEngineDefaultRole"
|
|
82
|
+
_PUBLIC_AICP_BASE_URL = "https://aicp.api.ksyun.com"
|
|
83
|
+
_INNER_AICP_BASE_URL = "http://aicp.inner.api.ksyun.com"
|
|
84
|
+
_PERMISSION_PROBE_ACTIONS = {"CreateAgentProduct", "CreateAgent", "ListAgents", "GetAgent"}
|
|
85
|
+
_permission_probe_cache: dict[tuple[str, str, str], bool] = {}
|
|
86
|
+
|
|
87
|
+
def __init__(
|
|
88
|
+
self,
|
|
89
|
+
base_url: Optional[str] = None,
|
|
90
|
+
access_key: Optional[str] = None,
|
|
91
|
+
secret_key: Optional[str] = None,
|
|
92
|
+
region: str = "cn-beijing-6",
|
|
93
|
+
service: Optional[str] = None,
|
|
94
|
+
timeout: float = 60.0,
|
|
95
|
+
dry_run: bool = False,
|
|
96
|
+
extra_headers: Optional[Dict[str, str]] = None,
|
|
97
|
+
):
|
|
98
|
+
self.base_url = (
|
|
99
|
+
base_url
|
|
100
|
+
or os.getenv("AGENTENGINE_SERVER_URL")
|
|
101
|
+
)
|
|
102
|
+
if not self.base_url:
|
|
103
|
+
self.base_url = self._detect_default_base_url()
|
|
104
|
+
|
|
105
|
+
# 本地调试覆盖 (如果需要)
|
|
106
|
+
# self.base_url = "http://localhost:8081"
|
|
107
|
+
self.timeout = timeout
|
|
108
|
+
self.logical_region = region
|
|
109
|
+
self.region = self._normalize_control_region(region)
|
|
110
|
+
self.custom_source = self._resolve_custom_source(region)
|
|
111
|
+
self.dry_run = bool(dry_run or self._is_global_dry_run_enabled())
|
|
112
|
+
self.extra_headers = extra_headers or {}
|
|
113
|
+
# 签名 service 可通过环境变量覆盖(例如 aicp)
|
|
114
|
+
self.service = service or os.getenv("AGENTENGINE_SIGN_SERVICE", "aicp")
|
|
115
|
+
|
|
116
|
+
# AWS V4 签名
|
|
117
|
+
self._auth = AWSV4Auth(
|
|
118
|
+
access_key_id=access_key or "",
|
|
119
|
+
secret_access_key=secret_key or "",
|
|
120
|
+
region=self.region,
|
|
121
|
+
service=self.service,
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
if self._auth.is_enabled:
|
|
125
|
+
logger.debug(f"AgentEngineClient initialized with V4Auth: {self.base_url}")
|
|
126
|
+
else:
|
|
127
|
+
logger.debug("AgentEngineClient: No credentials, signing disabled")
|
|
128
|
+
|
|
129
|
+
self._session: Optional[requests.Session] = None
|
|
130
|
+
self._http_error_log_suppressors: list[HttpErrorLogSuppressor] = []
|
|
131
|
+
# 反查身份的实例缓存(避免同会话重复调 IAM);None=未尝试,ResolvedIdentity|None=已反查
|
|
132
|
+
self._resolved_identity: Any = None
|
|
133
|
+
self._identity_resolve_attempted: bool = False
|
|
134
|
+
|
|
135
|
+
@staticmethod
|
|
136
|
+
def _ssl_verify_enabled() -> bool:
|
|
137
|
+
insecure = (
|
|
138
|
+
os.getenv("AGENTENGINE_SSL_INSECURE")
|
|
139
|
+
or os.getenv("CURL_SSL_INSECURE")
|
|
140
|
+
or ""
|
|
141
|
+
).strip().lower()
|
|
142
|
+
if insecure in {"1", "true", "yes", "on"}:
|
|
143
|
+
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
|
|
144
|
+
return False
|
|
145
|
+
return True
|
|
146
|
+
|
|
147
|
+
@staticmethod
|
|
148
|
+
def _is_global_dry_run_enabled() -> bool:
|
|
149
|
+
"""根命令 --dry-run 开关:支持未显式透传 dry_run 参数的命令。"""
|
|
150
|
+
return os.getenv("AGENTENGINE_GLOBAL_DRY_RUN", "").strip().lower() in {
|
|
151
|
+
"1",
|
|
152
|
+
"true",
|
|
153
|
+
"yes",
|
|
154
|
+
"on",
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
@staticmethod
|
|
158
|
+
def _is_connectable(url: str, timeout: float = 1.0) -> bool:
|
|
159
|
+
"""检查目标地址 TCP 连通性。"""
|
|
160
|
+
try:
|
|
161
|
+
parsed = urlparse(url)
|
|
162
|
+
host = parsed.hostname
|
|
163
|
+
if not host:
|
|
164
|
+
return False
|
|
165
|
+
port = parsed.port or (443 if parsed.scheme == "https" else 80)
|
|
166
|
+
with socket.create_connection((host, port), timeout=timeout):
|
|
167
|
+
return True
|
|
168
|
+
except OSError:
|
|
169
|
+
return False
|
|
170
|
+
|
|
171
|
+
def _detect_default_base_url(self) -> str:
|
|
172
|
+
"""默认地址自动探测: inner 优先,失败回落公网。"""
|
|
173
|
+
inner_url = self._INNER_AICP_BASE_URL
|
|
174
|
+
public_url = self._PUBLIC_AICP_BASE_URL
|
|
175
|
+
|
|
176
|
+
if self._is_connectable(inner_url):
|
|
177
|
+
logger.info(f"AgentEngineClient endpoint selected (inner): {inner_url}")
|
|
178
|
+
return inner_url
|
|
179
|
+
|
|
180
|
+
logger.warning(
|
|
181
|
+
f"AgentEngineClient endpoint fallback to public: {public_url} "
|
|
182
|
+
f"(inner unreachable: {inner_url})"
|
|
183
|
+
)
|
|
184
|
+
return public_url
|
|
185
|
+
|
|
186
|
+
def _get_session(self) -> requests.Session:
|
|
187
|
+
if self._session is None:
|
|
188
|
+
self._session = requests.Session()
|
|
189
|
+
return self._session
|
|
190
|
+
|
|
191
|
+
def _get_host(self) -> str:
|
|
192
|
+
"""提取 Host (用于签名)"""
|
|
193
|
+
host = self.base_url
|
|
194
|
+
if host.startswith("http://"):
|
|
195
|
+
host = host[7:]
|
|
196
|
+
elif host.startswith("https://"):
|
|
197
|
+
host = host[8:]
|
|
198
|
+
return host.split("/")[0].rstrip("/")
|
|
199
|
+
|
|
200
|
+
def _is_kop_mode(self) -> bool:
|
|
201
|
+
"""是否使用 KOP 协议模式(按 base_url 是否包含 aicp 判断)。"""
|
|
202
|
+
return "aicp" in (self.base_url or "").lower()
|
|
203
|
+
|
|
204
|
+
@staticmethod
|
|
205
|
+
def _normalize_control_region(region: Optional[str]) -> str:
|
|
206
|
+
"""归一化控制面 Region(KOP 头部与签名使用)。"""
|
|
207
|
+
region = (region or "cn-beijing-6").strip()
|
|
208
|
+
if region.lower() == "pre-online":
|
|
209
|
+
return os.getenv("AGENTENGINE_PRE_CONTROL_REGION", "cn-beijing-6")
|
|
210
|
+
return region
|
|
211
|
+
|
|
212
|
+
@staticmethod
|
|
213
|
+
def _resolve_custom_source(region: Optional[str]) -> Optional[str]:
|
|
214
|
+
"""根据逻辑环境决定是否附加 X-KSC-CUSTOM-SOURCE。"""
|
|
215
|
+
if (region or "").strip().lower() == "pre-online":
|
|
216
|
+
return os.getenv("AGENTENGINE_PRE_CUSTOM_SOURCE", "pre")
|
|
217
|
+
return None
|
|
218
|
+
|
|
219
|
+
@staticmethod
|
|
220
|
+
def _should_fallback_get_agent_with_legacy_id(error: Exception) -> bool:
|
|
221
|
+
"""仅在明确识别为字段兼容问题时,才回退到旧控制面的 Id 字段。"""
|
|
222
|
+
text = str(error or "").lower()
|
|
223
|
+
if not text:
|
|
224
|
+
return False
|
|
225
|
+
|
|
226
|
+
# 404 / not found / 缺少入参都属于正常业务错误,不应被误判为字段兼容问题。
|
|
227
|
+
non_compat_markers = (
|
|
228
|
+
"http 404",
|
|
229
|
+
"status=404",
|
|
230
|
+
"not found",
|
|
231
|
+
"未找到对应的 agent",
|
|
232
|
+
"请至少传入 agentid 或 name",
|
|
233
|
+
"请至少传入 agent_id 或 name",
|
|
234
|
+
)
|
|
235
|
+
if any(marker in text for marker in non_compat_markers):
|
|
236
|
+
return False
|
|
237
|
+
|
|
238
|
+
agent_id_markers = (
|
|
239
|
+
"agentid",
|
|
240
|
+
"agent_id",
|
|
241
|
+
'"agentid"',
|
|
242
|
+
"'agentid'",
|
|
243
|
+
'"agent_id"',
|
|
244
|
+
"'agent_id'",
|
|
245
|
+
)
|
|
246
|
+
if not any(marker in text for marker in agent_id_markers):
|
|
247
|
+
return False
|
|
248
|
+
|
|
249
|
+
compat_markers = (
|
|
250
|
+
"422",
|
|
251
|
+
"invalid",
|
|
252
|
+
"validation error",
|
|
253
|
+
"field required",
|
|
254
|
+
"extra inputs are not permitted",
|
|
255
|
+
"extra fields not permitted",
|
|
256
|
+
"unexpected field",
|
|
257
|
+
"unknown field",
|
|
258
|
+
"unrecognized field",
|
|
259
|
+
"not permitted",
|
|
260
|
+
"not allowed",
|
|
261
|
+
)
|
|
262
|
+
return any(marker in text for marker in compat_markers)
|
|
263
|
+
|
|
264
|
+
def _normalize_payload_region(self, region: Optional[str]) -> str:
|
|
265
|
+
"""归一化请求体中的 Region 字段。"""
|
|
266
|
+
return self._normalize_control_region(region or self.logical_region or "cn-beijing-6")
|
|
267
|
+
|
|
268
|
+
@staticmethod
|
|
269
|
+
def _extract_http_error_details(resp_text: str) -> Dict[str, Any]:
|
|
270
|
+
text = str(resp_text or "").strip()
|
|
271
|
+
details: Dict[str, Any] = {}
|
|
272
|
+
if not text:
|
|
273
|
+
return details
|
|
274
|
+
try:
|
|
275
|
+
payload = json.loads(text)
|
|
276
|
+
except Exception:
|
|
277
|
+
return details
|
|
278
|
+
if not isinstance(payload, dict):
|
|
279
|
+
return details
|
|
280
|
+
|
|
281
|
+
request_id = str(payload.get("RequestId") or payload.get("RequestID") or "").strip()
|
|
282
|
+
if request_id:
|
|
283
|
+
details["request_id"] = request_id
|
|
284
|
+
|
|
285
|
+
error = payload.get("Error")
|
|
286
|
+
if isinstance(error, dict):
|
|
287
|
+
remote_code = str(error.get("Code") or "").strip()
|
|
288
|
+
remote_message = str(error.get("Message") or "").strip()
|
|
289
|
+
remote_type = str(error.get("Type") or "").strip()
|
|
290
|
+
if remote_code:
|
|
291
|
+
details["remote_error_code"] = remote_code
|
|
292
|
+
if remote_message:
|
|
293
|
+
details["remote_error_message"] = remote_message
|
|
294
|
+
if remote_type:
|
|
295
|
+
details["remote_error_type"] = remote_type
|
|
296
|
+
|
|
297
|
+
message = str(payload.get("Message") or "").strip()
|
|
298
|
+
if message:
|
|
299
|
+
details["message"] = message
|
|
300
|
+
|
|
301
|
+
return details
|
|
302
|
+
|
|
303
|
+
@staticmethod
|
|
304
|
+
def _is_auth_related_error_details(details: Dict[str, Any]) -> bool:
|
|
305
|
+
remote_code = str(details.get("remote_error_code") or "").strip().lower()
|
|
306
|
+
remote_message = str(details.get("remote_error_message") or details.get("message") or "").strip().lower()
|
|
307
|
+
if remote_code in {
|
|
308
|
+
"missingaccesskey",
|
|
309
|
+
"missingsecretkey",
|
|
310
|
+
"invalidaccesskey",
|
|
311
|
+
"signaturedoesnotmatch",
|
|
312
|
+
"invalidsignature",
|
|
313
|
+
"signaturemismatch",
|
|
314
|
+
"accessdenied",
|
|
315
|
+
"accessdeniedexception",
|
|
316
|
+
"unauthorized",
|
|
317
|
+
"unauthorizedoperation",
|
|
318
|
+
"invalidclienttokenid",
|
|
319
|
+
"authfailure",
|
|
320
|
+
}:
|
|
321
|
+
return True
|
|
322
|
+
markers = (
|
|
323
|
+
"access key is missing",
|
|
324
|
+
"secret key is missing",
|
|
325
|
+
"invalid access key",
|
|
326
|
+
"access key id you provided does not exist",
|
|
327
|
+
"signature",
|
|
328
|
+
"access denied",
|
|
329
|
+
"unauthorized",
|
|
330
|
+
"没有",
|
|
331
|
+
"权限",
|
|
332
|
+
"permission",
|
|
333
|
+
)
|
|
334
|
+
return any(marker in remote_message for marker in markers)
|
|
335
|
+
|
|
336
|
+
@staticmethod
|
|
337
|
+
def _is_inner_account_intranet_error(details: Dict[str, Any]) -> bool:
|
|
338
|
+
remote_code = str(details.get("remote_error_code") or "").strip().lower()
|
|
339
|
+
remote_message = str(details.get("remote_error_message") or details.get("message") or "").strip().lower()
|
|
340
|
+
return (
|
|
341
|
+
remote_code == "inneraccountcanonlyaccessthroughintranet"
|
|
342
|
+
or "inner account can only access through intranet" in remote_message
|
|
343
|
+
or "只能通过内网" in remote_message
|
|
344
|
+
)
|
|
345
|
+
|
|
346
|
+
def _can_retry_with_inner_aicp_endpoint(self, details: Dict[str, Any]) -> bool:
|
|
347
|
+
if not self._is_inner_account_intranet_error(details):
|
|
348
|
+
return False
|
|
349
|
+
parsed = urlparse(self.base_url or "")
|
|
350
|
+
return parsed.scheme == "https" and parsed.netloc.lower() == "aicp.api.ksyun.com"
|
|
351
|
+
|
|
352
|
+
def _switch_to_inner_aicp_endpoint(self) -> None:
|
|
353
|
+
logger.warning(
|
|
354
|
+
"AgentEngineClient switching to intranet endpoint after inner-account access error: %s",
|
|
355
|
+
self._INNER_AICP_BASE_URL,
|
|
356
|
+
)
|
|
357
|
+
self.base_url = self._INNER_AICP_BASE_URL
|
|
358
|
+
|
|
359
|
+
def _build_action_request_target(self, path: str, action: str) -> tuple[bool, Dict[str, str], str]:
|
|
360
|
+
kop_mode = self._is_kop_mode()
|
|
361
|
+
headers = self._build_headers(action=action, kop_mode=kop_mode)
|
|
362
|
+
if kop_mode:
|
|
363
|
+
version = os.getenv("AGENTENGINE_API_VERSION", "2024-06-12")
|
|
364
|
+
full_url = f"{self.base_url.rstrip('/')}/?Action={action}&Version={version}"
|
|
365
|
+
else:
|
|
366
|
+
full_url = f"{self.base_url}{path}"
|
|
367
|
+
return kop_mode, headers, full_url
|
|
368
|
+
|
|
369
|
+
def _build_raw_action_request_target(self, action: str, accept: str, has_files: bool) -> tuple[bool, Dict[str, str], str]:
|
|
370
|
+
kop_mode = self._is_kop_mode()
|
|
371
|
+
headers = self._build_headers(action=action, kop_mode=kop_mode)
|
|
372
|
+
headers["Accept"] = accept
|
|
373
|
+
if has_files:
|
|
374
|
+
headers.pop("Content-Type", None)
|
|
375
|
+
full_url = (
|
|
376
|
+
f"{self.base_url.rstrip('/')}/?Action={action}&Version={os.getenv('AGENTENGINE_API_VERSION', '2024-06-12')}"
|
|
377
|
+
if kop_mode
|
|
378
|
+
else f"{self.base_url}/agentengine/api/v1/{action}"
|
|
379
|
+
)
|
|
380
|
+
return kop_mode, headers, full_url
|
|
381
|
+
|
|
382
|
+
@contextmanager
|
|
383
|
+
def suppress_http_error_logging(
|
|
384
|
+
self,
|
|
385
|
+
predicate: Optional[HttpErrorLogSuppressor] = None,
|
|
386
|
+
) -> Iterator[None]:
|
|
387
|
+
suppressor = predicate or (
|
|
388
|
+
lambda *, method, full_url, status_code, resp_text, details: True
|
|
389
|
+
)
|
|
390
|
+
self._http_error_log_suppressors.append(suppressor)
|
|
391
|
+
try:
|
|
392
|
+
yield
|
|
393
|
+
finally:
|
|
394
|
+
if self._http_error_log_suppressors:
|
|
395
|
+
self._http_error_log_suppressors.pop()
|
|
396
|
+
|
|
397
|
+
def _log_http_error(self, *, method: str, full_url: str, status_code: int, details: Dict[str, Any]) -> None:
|
|
398
|
+
for suppressor in reversed(self._http_error_log_suppressors):
|
|
399
|
+
try:
|
|
400
|
+
if suppressor(
|
|
401
|
+
method=method,
|
|
402
|
+
full_url=full_url,
|
|
403
|
+
status_code=status_code,
|
|
404
|
+
resp_text="",
|
|
405
|
+
details=details,
|
|
406
|
+
):
|
|
407
|
+
return
|
|
408
|
+
except Exception:
|
|
409
|
+
continue
|
|
410
|
+
log_fn = logger.debug if self._is_auth_related_error_details(details) else logger.error
|
|
411
|
+
log_fn(
|
|
412
|
+
"Request failed: method=%s, status=%s",
|
|
413
|
+
method,
|
|
414
|
+
status_code,
|
|
415
|
+
)
|
|
416
|
+
|
|
417
|
+
@staticmethod
|
|
418
|
+
def _safe_log_target(raw_url: str) -> str:
|
|
419
|
+
parsed = urlsplit(str(raw_url or ""))
|
|
420
|
+
return parsed.path or "/"
|
|
421
|
+
|
|
422
|
+
@staticmethod
|
|
423
|
+
def _safe_log_error_summary(*, details: Dict[str, Any]) -> str:
|
|
424
|
+
code = str(details.get("remote_error_code") or details.get("code") or "").strip()
|
|
425
|
+
message = str(details.get("remote_error_message") or details.get("message") or "").strip()
|
|
426
|
+
if not message:
|
|
427
|
+
message = "response body omitted"
|
|
428
|
+
message = re.sub(
|
|
429
|
+
r"(?i)(password|passwd|secret|token|access[_-]?key|api[_-]?key)\s*[:=]\s*[^,\s}\"]+",
|
|
430
|
+
r"\1=<redacted>",
|
|
431
|
+
message,
|
|
432
|
+
)
|
|
433
|
+
message = " ".join(message.split())[:200]
|
|
434
|
+
return f"{code}: {message}" if code else message
|
|
435
|
+
|
|
436
|
+
def _build_headers(self, request_id: str = "", action: str = "", kop_mode: bool = False) -> Dict[str, str]:
|
|
437
|
+
if not request_id:
|
|
438
|
+
request_id = str(uuid.uuid4())
|
|
439
|
+
headers = {
|
|
440
|
+
"Accept": "application/json",
|
|
441
|
+
"Content-Type": "application/json",
|
|
442
|
+
"Host": self._get_host(),
|
|
443
|
+
"X-Ksc-Request-Id": request_id,
|
|
444
|
+
"X-Ksc-Region": self.region,
|
|
445
|
+
"X-Ksc-Source": "ksadk-cli",
|
|
446
|
+
}
|
|
447
|
+
if kop_mode and action:
|
|
448
|
+
headers["X-Action"] = action
|
|
449
|
+
headers["X-Version"] = os.getenv("AGENTENGINE_API_VERSION", "2024-06-12")
|
|
450
|
+
if self.custom_source:
|
|
451
|
+
headers["X-KSC-CUSTOM-SOURCE"] = self.custom_source
|
|
452
|
+
|
|
453
|
+
# 先合并 extra_headers(key 归一为 Title-Case,避免大小写重复 header)
|
|
454
|
+
if self.extra_headers:
|
|
455
|
+
for ek, ev in self.extra_headers.items():
|
|
456
|
+
# 归一常见 identity header 大小写,避免 X-Ksc-User-uuid 与 x-ksc-user-uuid 共存
|
|
457
|
+
normalized = ek
|
|
458
|
+
lower = str(ek or "").lower()
|
|
459
|
+
if lower == "x-ksc-user-uuid":
|
|
460
|
+
normalized = "X-Ksc-User-uuid"
|
|
461
|
+
elif lower == "x-ksc-account-id":
|
|
462
|
+
normalized = "X-Ksc-Account-Id"
|
|
463
|
+
headers[normalized] = ev
|
|
464
|
+
|
|
465
|
+
# 注入子账号身份:extra_headers 显式 > 反查(反查结果不覆盖已显式设置的值)
|
|
466
|
+
if "X-Ksc-User-uuid" not in headers:
|
|
467
|
+
user_uuid = self._resolve_user_uuid()
|
|
468
|
+
if user_uuid:
|
|
469
|
+
headers["X-Ksc-User-uuid"] = user_uuid
|
|
470
|
+
if "X-Ksc-Account-Id" not in headers:
|
|
471
|
+
account_id = self._resolve_account_id()
|
|
472
|
+
if account_id:
|
|
473
|
+
headers["X-Ksc-Account-Id"] = account_id
|
|
474
|
+
return headers
|
|
475
|
+
|
|
476
|
+
def _resolve_user_uuid(self) -> Optional[str]:
|
|
477
|
+
"""解析子账号 user uuid(X-Ksc-User-uuid 值)。
|
|
478
|
+
|
|
479
|
+
优先级:extra_headers 显式 > 实例缓存 > 反查(dry-run 只读缓存)。
|
|
480
|
+
反查失败返回 None(不抛异常,不阻塞主流程)。
|
|
481
|
+
"""
|
|
482
|
+
# 1. extra_headers 显式覆盖
|
|
483
|
+
for key, value in self.extra_headers.items():
|
|
484
|
+
if key.lower() == "x-ksc-user-uuid" and str(value or "").strip():
|
|
485
|
+
return str(value).strip()
|
|
486
|
+
# 2. 实例缓存(同会话已反查过)
|
|
487
|
+
if self._identity_resolve_attempted:
|
|
488
|
+
identity = self._resolved_identity
|
|
489
|
+
return identity.user_uuid if identity else None
|
|
490
|
+
# 3. 反查
|
|
491
|
+
identity = self._get_resolved_identity()
|
|
492
|
+
return identity.user_uuid if identity else None
|
|
493
|
+
|
|
494
|
+
def _get_resolved_identity(self) -> Any:
|
|
495
|
+
"""反查身份并缓存到实例。dry-run 只读文件缓存不联网。"""
|
|
496
|
+
if self._identity_resolve_attempted:
|
|
497
|
+
return self._resolved_identity
|
|
498
|
+
self._identity_resolve_attempted = True
|
|
499
|
+
ak = getattr(self._auth, "access_key_id", "") or ""
|
|
500
|
+
sk = getattr(self._auth, "secret_access_key", "") or ""
|
|
501
|
+
if not ak or not sk:
|
|
502
|
+
self._resolved_identity = None
|
|
503
|
+
return None
|
|
504
|
+
try:
|
|
505
|
+
from ksadk.identity import get_cached_identity, resolve_identity
|
|
506
|
+
|
|
507
|
+
if self.dry_run:
|
|
508
|
+
# dry-run 不联网,只读缓存
|
|
509
|
+
self._resolved_identity = get_cached_identity(ak)
|
|
510
|
+
else:
|
|
511
|
+
self._resolved_identity = resolve_identity(
|
|
512
|
+
access_key=ak, secret_key=sk
|
|
513
|
+
)
|
|
514
|
+
except Exception as e:
|
|
515
|
+
logger.warning("反查子账号身份失败: %s", e)
|
|
516
|
+
self._resolved_identity = None
|
|
517
|
+
return self._resolved_identity
|
|
518
|
+
|
|
519
|
+
def _resolve_account_id(self) -> Optional[str]:
|
|
520
|
+
# 1. extra_headers 显式
|
|
521
|
+
for key, value in self.extra_headers.items():
|
|
522
|
+
if key.lower() == "x-ksc-account-id" and str(value or "").strip():
|
|
523
|
+
return str(value).strip()
|
|
524
|
+
# 2. env KSYUN_ACCOUNT_ID
|
|
525
|
+
account_id = os.getenv("KSYUN_ACCOUNT_ID", "").strip()
|
|
526
|
+
if account_id:
|
|
527
|
+
return account_id
|
|
528
|
+
# 3. 反查主账号 ID(复用 _get_resolved_identity 的反查,不重复调)
|
|
529
|
+
identity = self._get_resolved_identity()
|
|
530
|
+
if identity and getattr(identity, "main_account_id", None):
|
|
531
|
+
return str(identity.main_account_id).strip() or None
|
|
532
|
+
return None
|
|
533
|
+
|
|
534
|
+
def _resolve_permission_role_name(self, action: str, params: Dict[str, Any]) -> str:
|
|
535
|
+
if action in {"CreateAgentProduct", "CreateAgent"}:
|
|
536
|
+
access = params.get("Access") if isinstance(params, dict) else None
|
|
537
|
+
if isinstance(access, dict):
|
|
538
|
+
explicit_role = (
|
|
539
|
+
access.get("IamRole")
|
|
540
|
+
or access.get("iamRole")
|
|
541
|
+
or access.get("iam_role")
|
|
542
|
+
)
|
|
543
|
+
if str(explicit_role or "").strip():
|
|
544
|
+
return str(explicit_role).strip()
|
|
545
|
+
return self._DEFAULT_PERMISSION_ROLE
|
|
546
|
+
|
|
547
|
+
def _maybe_precheck_permission(self, action: str, params: Dict[str, Any]) -> None:
|
|
548
|
+
if action not in self._PERMISSION_PROBE_ACTIONS or action == "CheckIamRole":
|
|
549
|
+
return
|
|
550
|
+
|
|
551
|
+
account_id = self._resolve_account_id()
|
|
552
|
+
if not account_id:
|
|
553
|
+
logger.warning("Skip permission precheck for %s: missing KSYUN_ACCOUNT_ID", action)
|
|
554
|
+
return
|
|
555
|
+
|
|
556
|
+
role_name = self._resolve_permission_role_name(action, params)
|
|
557
|
+
cache_key = (account_id, self.region, role_name)
|
|
558
|
+
cached = self._permission_probe_cache.get(cache_key)
|
|
559
|
+
if cached is True:
|
|
560
|
+
return
|
|
561
|
+
if cached is False:
|
|
562
|
+
raise AgentEngineAPIError(403, f"当前账号没有 {role_name} 权限")
|
|
563
|
+
|
|
564
|
+
try:
|
|
565
|
+
result = self._request(
|
|
566
|
+
"POST",
|
|
567
|
+
"/agentengine/api/v1/CheckIamRole",
|
|
568
|
+
{"RoleName": role_name},
|
|
569
|
+
)
|
|
570
|
+
except Exception as exc:
|
|
571
|
+
details = getattr(exc, "details", {}) if isinstance(exc, AgentEngineAPIError) else {}
|
|
572
|
+
if self._is_auth_related_error_details(details):
|
|
573
|
+
logger.debug("Permission probe auth failure for %s: %s", action, exc)
|
|
574
|
+
else:
|
|
575
|
+
logger.warning("Permission probe failed for %s: %s", action, exc)
|
|
576
|
+
return
|
|
577
|
+
|
|
578
|
+
code = int(result.get("Code", 0) or 0)
|
|
579
|
+
data = result.get("Data") or {}
|
|
580
|
+
has_permission = data.get("HasPermission")
|
|
581
|
+
if has_permission is False or code in {401, 403}:
|
|
582
|
+
self._permission_probe_cache[cache_key] = False
|
|
583
|
+
raise AgentEngineAPIError(
|
|
584
|
+
code=code or 403,
|
|
585
|
+
message=result.get("Message") or f"当前账号没有 {role_name} 权限",
|
|
586
|
+
)
|
|
587
|
+
|
|
588
|
+
if code != 0:
|
|
589
|
+
logger.warning(
|
|
590
|
+
"Permission probe unavailable for %s: code=%s message=%s",
|
|
591
|
+
action,
|
|
592
|
+
code,
|
|
593
|
+
result.get("Message", ""),
|
|
594
|
+
)
|
|
595
|
+
return
|
|
596
|
+
|
|
597
|
+
self._permission_probe_cache[cache_key] = True
|
|
598
|
+
|
|
599
|
+
def _request(
|
|
600
|
+
self,
|
|
601
|
+
method: str,
|
|
602
|
+
path: str,
|
|
603
|
+
body: Optional[Dict[str, Any]] = None,
|
|
604
|
+
*,
|
|
605
|
+
ignore_dry_run: bool = False,
|
|
606
|
+
) -> Dict[str, Any]:
|
|
607
|
+
"""同步 HTTP 请求"""
|
|
608
|
+
action = path.rstrip("/").split("/")[-1] if path else ""
|
|
609
|
+
_kop_mode, headers, full_url = self._build_action_request_target(path, action)
|
|
610
|
+
body_str = json.dumps(body, ensure_ascii=False) if body else ""
|
|
611
|
+
|
|
612
|
+
# DryRun 模式
|
|
613
|
+
if self.dry_run and not ignore_dry_run:
|
|
614
|
+
signed_headers = headers
|
|
615
|
+
if self._auth.is_enabled:
|
|
616
|
+
# Dry-run 展示真实即将发送的签名头(不暴露明文 SK)
|
|
617
|
+
signed_headers = self._auth.sign_headers(
|
|
618
|
+
method=method,
|
|
619
|
+
url=full_url,
|
|
620
|
+
headers=headers.copy(),
|
|
621
|
+
body=body_str,
|
|
622
|
+
)
|
|
623
|
+
|
|
624
|
+
# 生成 Curl 命令
|
|
625
|
+
curl_cmd = f"curl -X {method} \"{full_url}\" \\\n"
|
|
626
|
+
for k, v in signed_headers.items():
|
|
627
|
+
curl_cmd += f" -H \"{k}: {v}\" \\\n"
|
|
628
|
+
if body_str:
|
|
629
|
+
curl_cmd += f" -d '{body_str}'"
|
|
630
|
+
|
|
631
|
+
# 抛出异常以中断流程 (CLI 应捕获此异常)
|
|
632
|
+
raise DryRunExit(
|
|
633
|
+
"Dry Run finished.",
|
|
634
|
+
payload={
|
|
635
|
+
"method": method,
|
|
636
|
+
"url": full_url,
|
|
637
|
+
"headers": signed_headers,
|
|
638
|
+
"body": body,
|
|
639
|
+
"curl": curl_cmd,
|
|
640
|
+
},
|
|
641
|
+
)
|
|
642
|
+
|
|
643
|
+
session = self._get_session()
|
|
644
|
+
|
|
645
|
+
retried_inner_endpoint = False
|
|
646
|
+
while True:
|
|
647
|
+
logger.debug("Request: %s", method)
|
|
648
|
+
response = session.request(
|
|
649
|
+
method=method,
|
|
650
|
+
url=full_url,
|
|
651
|
+
data=body_str.encode("utf-8") if body_str else None,
|
|
652
|
+
headers=headers,
|
|
653
|
+
auth=self._auth.get_auth(), # AWS V4 签名
|
|
654
|
+
timeout=self.timeout,
|
|
655
|
+
verify=self._ssl_verify_enabled(),
|
|
656
|
+
)
|
|
657
|
+
|
|
658
|
+
logger.debug(f"Response: {response.status_code}")
|
|
659
|
+
|
|
660
|
+
if response.status_code < 400:
|
|
661
|
+
break
|
|
662
|
+
|
|
663
|
+
resp_text = response.text or ""
|
|
664
|
+
details = self._extract_http_error_details(resp_text)
|
|
665
|
+
details.setdefault("http_status", response.status_code)
|
|
666
|
+
if not retried_inner_endpoint and self._can_retry_with_inner_aicp_endpoint(details):
|
|
667
|
+
retried_inner_endpoint = True
|
|
668
|
+
self._switch_to_inner_aicp_endpoint()
|
|
669
|
+
_kop_mode, headers, full_url = self._build_action_request_target(path, action)
|
|
670
|
+
continue
|
|
671
|
+
|
|
672
|
+
self._log_http_error(
|
|
673
|
+
method=method,
|
|
674
|
+
full_url=full_url,
|
|
675
|
+
status_code=response.status_code,
|
|
676
|
+
details=details,
|
|
677
|
+
)
|
|
678
|
+
message = (
|
|
679
|
+
str(details.get("remote_error_message") or details.get("message") or "").strip()
|
|
680
|
+
or resp_text
|
|
681
|
+
)
|
|
682
|
+
if details:
|
|
683
|
+
raise AgentEngineAPIError(response.status_code, message, details=details)
|
|
684
|
+
raise Exception(f"HTTP {response.status_code} {method} {full_url}: {resp_text}")
|
|
685
|
+
|
|
686
|
+
if response.text:
|
|
687
|
+
try:
|
|
688
|
+
return response.json()
|
|
689
|
+
except Exception as e:
|
|
690
|
+
raise Exception(f"Invalid JSON response from {full_url}: {response.text}") from e
|
|
691
|
+
return {}
|
|
692
|
+
|
|
693
|
+
# Async 包装 (保持兼容性)
|
|
694
|
+
async def close(self):
|
|
695
|
+
if self._session:
|
|
696
|
+
self._session.close()
|
|
697
|
+
self._session = None
|
|
698
|
+
|
|
699
|
+
async def __aenter__(self):
|
|
700
|
+
return self
|
|
701
|
+
|
|
702
|
+
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
|
703
|
+
await self.close()
|
|
704
|
+
|
|
705
|
+
# ===== Action API (推荐使用) =====
|
|
706
|
+
|
|
707
|
+
@staticmethod
|
|
708
|
+
def _pascal_key(key: str) -> str:
|
|
709
|
+
"""PascalCase/camelCase 键名转 snake_case"""
|
|
710
|
+
# "MCPs" -> "mcps", "AgentId" -> "agent_id", "QuickAccess" -> "quick_access"
|
|
711
|
+
s1 = re.sub('([A-Z]+)([A-Z][a-z])', r'\1_\2', key)
|
|
712
|
+
s2 = re.sub('([a-z0-9])([A-Z])', r'\1_\2', s1)
|
|
713
|
+
return s2.lower()
|
|
714
|
+
|
|
715
|
+
@classmethod
|
|
716
|
+
def _to_snake_case(cls, data):
|
|
717
|
+
"""递归将字典的 PascalCase 键名转为 snake_case"""
|
|
718
|
+
if isinstance(data, dict):
|
|
719
|
+
return {cls._pascal_key(k): cls._to_snake_case(v) for k, v in data.items()}
|
|
720
|
+
elif isinstance(data, list):
|
|
721
|
+
return [cls._to_snake_case(item) for item in data]
|
|
722
|
+
return data
|
|
723
|
+
|
|
724
|
+
@classmethod
|
|
725
|
+
def _fix_endpoints_protocol(cls, data):
|
|
726
|
+
"""递归将 endpoint 相关字段的 https:// 替换为 http:// (预发环境)"""
|
|
727
|
+
_ENDPOINT_KEYS = {"endpoint", "public_endpoint", "private_endpoint"}
|
|
728
|
+
if isinstance(data, dict):
|
|
729
|
+
result = {}
|
|
730
|
+
for k, v in data.items():
|
|
731
|
+
if k in _ENDPOINT_KEYS and isinstance(v, str) and v.startswith("https://"):
|
|
732
|
+
result[k] = "http://" + v[8:]
|
|
733
|
+
else:
|
|
734
|
+
result[k] = cls._fix_endpoints_protocol(v)
|
|
735
|
+
return result
|
|
736
|
+
elif isinstance(data, list):
|
|
737
|
+
return [cls._fix_endpoints_protocol(item) for item in data]
|
|
738
|
+
return data
|
|
739
|
+
|
|
740
|
+
@staticmethod
|
|
741
|
+
def _parse_container_image_ref(image_ref: str) -> tuple[str, str, str]:
|
|
742
|
+
"""解析容器镜像地址,返回 (namespace, repo, tag)。"""
|
|
743
|
+
raw = (image_ref or "").strip()
|
|
744
|
+
if not raw:
|
|
745
|
+
return "default", "", "latest"
|
|
746
|
+
|
|
747
|
+
image = raw
|
|
748
|
+
for prefix in ("http://", "https://"):
|
|
749
|
+
if image.startswith(prefix):
|
|
750
|
+
image = image[len(prefix):]
|
|
751
|
+
break
|
|
752
|
+
|
|
753
|
+
image_no_tag = image
|
|
754
|
+
tag = "latest"
|
|
755
|
+
last_slash = image.rfind("/")
|
|
756
|
+
last_colon = image.rfind(":")
|
|
757
|
+
if last_colon > last_slash:
|
|
758
|
+
image_no_tag = image[:last_colon]
|
|
759
|
+
tag = image[last_colon + 1 :] or "latest"
|
|
760
|
+
|
|
761
|
+
path = image_no_tag
|
|
762
|
+
first = image_no_tag.split("/", 1)[0].strip()
|
|
763
|
+
has_registry = "." in first or ":" in first or first == "localhost"
|
|
764
|
+
if has_registry and "/" in image_no_tag:
|
|
765
|
+
path = image_no_tag.split("/", 1)[1]
|
|
766
|
+
|
|
767
|
+
if "/" in path:
|
|
768
|
+
namespace, repo = path.split("/", 1)
|
|
769
|
+
else:
|
|
770
|
+
namespace, repo = "default", path
|
|
771
|
+
|
|
772
|
+
return namespace or "default", repo, tag
|
|
773
|
+
|
|
774
|
+
@staticmethod
|
|
775
|
+
def _enterprise_instance_from_image_ref(image_ref: str) -> str | None:
|
|
776
|
+
"""Infer enterprise KCR instance name from a full image reference."""
|
|
777
|
+
image = (image_ref or "").strip()
|
|
778
|
+
parsed = urlsplit(image if "://" in image else f"//{image}", allow_fragments=False)
|
|
779
|
+
host = (parsed.hostname or "").strip().lower()
|
|
780
|
+
vpc_match = re.fullmatch(r"([a-z0-9][a-z0-9-]*)-vpc\.ksyunkcr\.com", host)
|
|
781
|
+
if vpc_match:
|
|
782
|
+
return vpc_match.group(1) or None
|
|
783
|
+
public_match = re.fullmatch(r"([a-z0-9][a-z0-9-]*)\.ksyunkcr\.com", host)
|
|
784
|
+
if public_match:
|
|
785
|
+
return public_match.group(1) or None
|
|
786
|
+
return None
|
|
787
|
+
|
|
788
|
+
@classmethod
|
|
789
|
+
def _build_container_config_payload(
|
|
790
|
+
cls,
|
|
791
|
+
*,
|
|
792
|
+
artifact: str,
|
|
793
|
+
image_credential: Optional[Dict[str, Any]] = None,
|
|
794
|
+
container: Optional[Dict[str, Any]] = None,
|
|
795
|
+
) -> Dict[str, Any]:
|
|
796
|
+
container = container or {}
|
|
797
|
+
artifact = str(artifact or container.get("image_addr") or "").strip()
|
|
798
|
+
img_ns, img_repo, img_ver = cls._parse_container_image_ref(artifact)
|
|
799
|
+
|
|
800
|
+
inferred_enterprise_instance = cls._enterprise_instance_from_image_ref(artifact)
|
|
801
|
+
image_type = (
|
|
802
|
+
container.get("image_type")
|
|
803
|
+
or ("Enterprise" if inferred_enterprise_instance else "Personal")
|
|
804
|
+
)
|
|
805
|
+
params: Dict[str, Any] = {
|
|
806
|
+
"ImageType": image_type,
|
|
807
|
+
"NameSpace": container.get("name_space") or img_ns,
|
|
808
|
+
"ImageRepo": container.get("image_repo") or img_repo,
|
|
809
|
+
"ImageVersion": container.get("image_version") or img_ver,
|
|
810
|
+
"ImageAddr": container.get("image_addr") or artifact,
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
enterprise_instance = container.get("enterprise_instance") or inferred_enterprise_instance
|
|
814
|
+
if enterprise_instance:
|
|
815
|
+
params["EnterpriseInstance"] = enterprise_instance
|
|
816
|
+
if container.get("enterprise_instance_id"):
|
|
817
|
+
params["EnterpriseInstanceId"] = container.get("enterprise_instance_id")
|
|
818
|
+
|
|
819
|
+
ic = image_credential or {}
|
|
820
|
+
username = (container.get("username") or ic.get("username") or "").strip()
|
|
821
|
+
password = (container.get("password") or ic.get("password") or "").strip()
|
|
822
|
+
if username and password:
|
|
823
|
+
params["UserName"] = username
|
|
824
|
+
params["Password"] = password
|
|
825
|
+
return params
|
|
826
|
+
|
|
827
|
+
@staticmethod
|
|
828
|
+
def _normalize_framework_name(framework: Optional[str]) -> str:
|
|
829
|
+
"""规范化 framework 名称,默认 langgraph。"""
|
|
830
|
+
normalized = (framework or "langgraph").strip().lower()
|
|
831
|
+
return normalized or "langgraph"
|
|
832
|
+
|
|
833
|
+
@staticmethod
|
|
834
|
+
def _normalize_framework_filters(framework: Optional[str | Sequence[str]]) -> str | None:
|
|
835
|
+
"""规范化 ListAgents 的 framework 过滤,支持 CSV 和字符串序列。"""
|
|
836
|
+
if framework is None:
|
|
837
|
+
return None
|
|
838
|
+
|
|
839
|
+
raw_values: list[str]
|
|
840
|
+
if isinstance(framework, str):
|
|
841
|
+
raw_values = [framework]
|
|
842
|
+
else:
|
|
843
|
+
raw_values = [str(item) for item in framework if str(item).strip()]
|
|
844
|
+
|
|
845
|
+
normalized_values: list[str] = []
|
|
846
|
+
seen: set[str] = set()
|
|
847
|
+
for raw in raw_values:
|
|
848
|
+
for part in raw.split(","):
|
|
849
|
+
normalized = part.strip().lower()
|
|
850
|
+
if normalized and normalized not in seen:
|
|
851
|
+
seen.add(normalized)
|
|
852
|
+
normalized_values.append(normalized)
|
|
853
|
+
|
|
854
|
+
if not normalized_values:
|
|
855
|
+
return None
|
|
856
|
+
return ",".join(normalized_values)
|
|
857
|
+
|
|
858
|
+
@staticmethod
|
|
859
|
+
def _extract_runtime_access(detail: Dict[str, Any]) -> Dict[str, Any]:
|
|
860
|
+
if not isinstance(detail, dict):
|
|
861
|
+
return {}
|
|
862
|
+
|
|
863
|
+
basic = detail.get("basic") if isinstance(detail.get("basic"), dict) else {}
|
|
864
|
+
quick = detail.get("quick_access") if isinstance(detail.get("quick_access"), dict) else {}
|
|
865
|
+
deployment = detail.get("deployment") if isinstance(detail.get("deployment"), dict) else {}
|
|
866
|
+
framework = (
|
|
867
|
+
deployment.get("framework")
|
|
868
|
+
or basic.get("framework")
|
|
869
|
+
or detail.get("framework")
|
|
870
|
+
)
|
|
871
|
+
return {
|
|
872
|
+
"agent_id": basic.get("agent_id") or detail.get("agent_id"),
|
|
873
|
+
"name": basic.get("name") or detail.get("name"),
|
|
874
|
+
"endpoint": quick.get("public_endpoint")
|
|
875
|
+
or quick.get("private_endpoint")
|
|
876
|
+
or detail.get("endpoint"),
|
|
877
|
+
"api_key": quick.get("api_key") or detail.get("api_key"),
|
|
878
|
+
"framework": str(framework or "").strip().lower() or None,
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
@staticmethod
|
|
882
|
+
def _encode_workspace_runtime_path(remote_path: str) -> str:
|
|
883
|
+
raw = str(remote_path or "").strip().replace("\\", "/")
|
|
884
|
+
if not raw or raw in {".", "/"}:
|
|
885
|
+
raise ValueError("workspace file path must not be empty")
|
|
886
|
+
segments = [segment for segment in raw.split("/") if segment not in {"", "."}]
|
|
887
|
+
if not segments:
|
|
888
|
+
raise ValueError("workspace file path must not be empty")
|
|
889
|
+
return "/".join(quote(segment, safe="") for segment in segments)
|
|
890
|
+
|
|
891
|
+
def _workspace_runtime_error(self, response: requests.Response) -> AgentEngineAPIError:
|
|
892
|
+
message = (response.text or "").strip() or f"HTTP {response.status_code}"
|
|
893
|
+
try:
|
|
894
|
+
payload = response.json()
|
|
895
|
+
except Exception:
|
|
896
|
+
payload = None
|
|
897
|
+
if isinstance(payload, dict):
|
|
898
|
+
message = (
|
|
899
|
+
str(payload.get("detail") or payload.get("Message") or message).strip()
|
|
900
|
+
or message
|
|
901
|
+
)
|
|
902
|
+
return AgentEngineAPIError(response.status_code, message)
|
|
903
|
+
|
|
904
|
+
@staticmethod
|
|
905
|
+
def _compact_params(params: Dict[str, Any] | None) -> Dict[str, Any]:
|
|
906
|
+
return {
|
|
907
|
+
key: value
|
|
908
|
+
for key, value in (params or {}).items()
|
|
909
|
+
if value is not None
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
@staticmethod
|
|
913
|
+
def _workspace_prefers_action_proxy(detail: Dict[str, Any] | None) -> bool:
|
|
914
|
+
if not isinstance(detail, dict):
|
|
915
|
+
return False
|
|
916
|
+
access = AgentEngineClient._extract_runtime_access(detail)
|
|
917
|
+
framework = str(access.get("framework") or "").strip().lower()
|
|
918
|
+
return framework == "openclaw"
|
|
919
|
+
|
|
920
|
+
async def _resolve_workspace_transport(
|
|
921
|
+
self,
|
|
922
|
+
*,
|
|
923
|
+
agent_id: str | None = None,
|
|
924
|
+
name: str | None = None,
|
|
925
|
+
endpoint: str | None = None,
|
|
926
|
+
api_key: str | None = None,
|
|
927
|
+
) -> Dict[str, Any]:
|
|
928
|
+
endpoint_value = str(endpoint or "").strip()
|
|
929
|
+
api_key_value = str(api_key or "").strip() or None
|
|
930
|
+
if endpoint_value:
|
|
931
|
+
return {
|
|
932
|
+
"mode": "runtime",
|
|
933
|
+
"agent_id": agent_id,
|
|
934
|
+
"name": name,
|
|
935
|
+
"access": {
|
|
936
|
+
"endpoint": endpoint_value.rstrip("/"),
|
|
937
|
+
"api_key": api_key_value,
|
|
938
|
+
},
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
detail = await self.get_agent(agent_id=agent_id, name=name, include_api_key=True)
|
|
942
|
+
access = self._extract_runtime_access(detail)
|
|
943
|
+
resolved_agent_id = str(access.get("agent_id") or agent_id or "").strip() or None
|
|
944
|
+
resolved_name = str(access.get("name") or name or "").strip() or None
|
|
945
|
+
resolved_api_key = api_key_value or (
|
|
946
|
+
str(access.get("api_key") or "").strip() or None
|
|
947
|
+
)
|
|
948
|
+
if self._workspace_prefers_action_proxy(detail):
|
|
949
|
+
return {
|
|
950
|
+
"mode": "action",
|
|
951
|
+
"agent_id": resolved_agent_id,
|
|
952
|
+
"name": resolved_name,
|
|
953
|
+
"access": access,
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
resolved_endpoint = str(access.get("endpoint") or "").strip()
|
|
957
|
+
if not resolved_endpoint:
|
|
958
|
+
raise AgentEngineAPIError(404, "Agent runtime endpoint is not ready")
|
|
959
|
+
return {
|
|
960
|
+
"mode": "runtime",
|
|
961
|
+
"agent_id": resolved_agent_id,
|
|
962
|
+
"name": resolved_name,
|
|
963
|
+
"access": {
|
|
964
|
+
"endpoint": resolved_endpoint.rstrip("/"),
|
|
965
|
+
"api_key": resolved_api_key,
|
|
966
|
+
},
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
def _action_raw_request(
|
|
970
|
+
self,
|
|
971
|
+
method: str,
|
|
972
|
+
action: str,
|
|
973
|
+
*,
|
|
974
|
+
params: Dict[str, Any] | None = None,
|
|
975
|
+
data: Dict[str, Any] | None = None,
|
|
976
|
+
files: Dict[str, Any] | None = None,
|
|
977
|
+
accept: str = "application/json",
|
|
978
|
+
) -> requests.Response:
|
|
979
|
+
_kop_mode, headers, full_url = self._build_raw_action_request_target(
|
|
980
|
+
action,
|
|
981
|
+
accept,
|
|
982
|
+
files is not None,
|
|
983
|
+
)
|
|
984
|
+
session = self._get_session()
|
|
985
|
+
|
|
986
|
+
retried_inner_endpoint = False
|
|
987
|
+
while True:
|
|
988
|
+
response = session.request(
|
|
989
|
+
method=method,
|
|
990
|
+
url=full_url,
|
|
991
|
+
params=self._compact_params(params),
|
|
992
|
+
data=self._compact_params(data) if data is not None else None,
|
|
993
|
+
files=files,
|
|
994
|
+
headers=headers,
|
|
995
|
+
auth=self._auth.get_auth(),
|
|
996
|
+
timeout=self.timeout,
|
|
997
|
+
verify=self._ssl_verify_enabled(),
|
|
998
|
+
)
|
|
999
|
+
if response.status_code < 400:
|
|
1000
|
+
return response
|
|
1001
|
+
|
|
1002
|
+
resp_text = response.text or ""
|
|
1003
|
+
details = self._extract_http_error_details(resp_text)
|
|
1004
|
+
details.setdefault("http_status", response.status_code)
|
|
1005
|
+
if not retried_inner_endpoint and self._can_retry_with_inner_aicp_endpoint(details):
|
|
1006
|
+
retried_inner_endpoint = True
|
|
1007
|
+
self._switch_to_inner_aicp_endpoint()
|
|
1008
|
+
_kop_mode, headers, full_url = self._build_raw_action_request_target(
|
|
1009
|
+
action,
|
|
1010
|
+
accept,
|
|
1011
|
+
files is not None,
|
|
1012
|
+
)
|
|
1013
|
+
continue
|
|
1014
|
+
|
|
1015
|
+
self._log_http_error(
|
|
1016
|
+
method=method,
|
|
1017
|
+
full_url=full_url,
|
|
1018
|
+
status_code=response.status_code,
|
|
1019
|
+
details=details,
|
|
1020
|
+
)
|
|
1021
|
+
message = (
|
|
1022
|
+
str(details.get("remote_error_message") or details.get("message") or "").strip()
|
|
1023
|
+
or resp_text
|
|
1024
|
+
)
|
|
1025
|
+
raise AgentEngineAPIError(response.status_code, message, details=details or None)
|
|
1026
|
+
|
|
1027
|
+
async def _resolve_workspace_runtime_access(
|
|
1028
|
+
self,
|
|
1029
|
+
*,
|
|
1030
|
+
agent_id: str | None = None,
|
|
1031
|
+
name: str | None = None,
|
|
1032
|
+
endpoint: str | None = None,
|
|
1033
|
+
api_key: str | None = None,
|
|
1034
|
+
) -> Dict[str, Any]:
|
|
1035
|
+
transport = await self._resolve_workspace_transport(
|
|
1036
|
+
agent_id=agent_id,
|
|
1037
|
+
name=name,
|
|
1038
|
+
endpoint=endpoint,
|
|
1039
|
+
api_key=api_key,
|
|
1040
|
+
)
|
|
1041
|
+
if transport["mode"] != "runtime":
|
|
1042
|
+
raise AgentEngineAPIError(400, "Workspace runtime direct access is unavailable for this agent")
|
|
1043
|
+
return transport["access"]
|
|
1044
|
+
|
|
1045
|
+
def _workspace_runtime_request(
|
|
1046
|
+
self,
|
|
1047
|
+
*,
|
|
1048
|
+
access: Dict[str, Any],
|
|
1049
|
+
method: str,
|
|
1050
|
+
path: str,
|
|
1051
|
+
params: Dict[str, Any] | None = None,
|
|
1052
|
+
files: Dict[str, Any] | None = None,
|
|
1053
|
+
) -> requests.Response:
|
|
1054
|
+
url = f"{access['endpoint'].rstrip('/')}/_ksadk/workspace/v1/{path.lstrip('/')}"
|
|
1055
|
+
headers: Dict[str, str] = {}
|
|
1056
|
+
if access.get("api_key"):
|
|
1057
|
+
headers["Authorization"] = f"Bearer {access['api_key']}"
|
|
1058
|
+
response = self._get_session().request(
|
|
1059
|
+
method=method,
|
|
1060
|
+
url=url,
|
|
1061
|
+
headers=headers or None,
|
|
1062
|
+
params=params,
|
|
1063
|
+
files=files,
|
|
1064
|
+
stream=False,
|
|
1065
|
+
timeout=self.timeout,
|
|
1066
|
+
verify=self._ssl_verify_enabled(),
|
|
1067
|
+
)
|
|
1068
|
+
setattr(response, "_ksadk_workspace_url", url)
|
|
1069
|
+
if response.status_code >= 400:
|
|
1070
|
+
raise self._workspace_runtime_error(response)
|
|
1071
|
+
return response
|
|
1072
|
+
|
|
1073
|
+
def _workspace_runtime_json(self, response: requests.Response) -> Dict[str, Any]:
|
|
1074
|
+
try:
|
|
1075
|
+
payload = response.json()
|
|
1076
|
+
except Exception as exc:
|
|
1077
|
+
url = str(
|
|
1078
|
+
getattr(response, "_ksadk_workspace_url", "")
|
|
1079
|
+
or getattr(response, "url", "")
|
|
1080
|
+
).strip()
|
|
1081
|
+
body = (response.text or "").strip()
|
|
1082
|
+
body_preview = body[:200] if body else "<empty response body>"
|
|
1083
|
+
raise AgentEngineAPIError(
|
|
1084
|
+
502,
|
|
1085
|
+
(
|
|
1086
|
+
"workspace runtime returned invalid JSON"
|
|
1087
|
+
+ (f" from {url}" if url else "")
|
|
1088
|
+
+ f": {body_preview}"
|
|
1089
|
+
),
|
|
1090
|
+
details={
|
|
1091
|
+
"workspace_runtime_url": url or None,
|
|
1092
|
+
"workspace_runtime_status": getattr(response, "status_code", None),
|
|
1093
|
+
"workspace_runtime_body_preview": body_preview,
|
|
1094
|
+
},
|
|
1095
|
+
) from exc
|
|
1096
|
+
return self._to_snake_case(payload)
|
|
1097
|
+
|
|
1098
|
+
@staticmethod
|
|
1099
|
+
def _annotate_workspace_payload(payload: Dict[str, Any], *, transport_mode: str) -> Dict[str, Any]:
|
|
1100
|
+
annotated = dict(payload or {})
|
|
1101
|
+
annotated["transport_mode"] = transport_mode
|
|
1102
|
+
return annotated
|
|
1103
|
+
|
|
1104
|
+
@staticmethod
|
|
1105
|
+
def _display_name_from_content_disposition(value: str) -> str:
|
|
1106
|
+
header = str(value or "").strip()
|
|
1107
|
+
if not header:
|
|
1108
|
+
return ""
|
|
1109
|
+
filename_star_match = re.search(r"filename\*=UTF-8''([^;]+)", header, flags=re.IGNORECASE)
|
|
1110
|
+
if filename_star_match:
|
|
1111
|
+
return Path(unquote(filename_star_match.group(1).strip().strip('"'))).name
|
|
1112
|
+
filename_match = re.search(r'filename="?([^";]+)"?', header, flags=re.IGNORECASE)
|
|
1113
|
+
if filename_match:
|
|
1114
|
+
return Path(filename_match.group(1).strip()).name
|
|
1115
|
+
return ""
|
|
1116
|
+
|
|
1117
|
+
def _action(
|
|
1118
|
+
self,
|
|
1119
|
+
action: str,
|
|
1120
|
+
params: Dict[str, Any] = None,
|
|
1121
|
+
*,
|
|
1122
|
+
ignore_dry_run: bool = False,
|
|
1123
|
+
) -> Dict[str, Any]:
|
|
1124
|
+
"""通用 Action API 调用"""
|
|
1125
|
+
body = params or {}
|
|
1126
|
+
self._maybe_precheck_permission(action, body)
|
|
1127
|
+
request_kwargs = {"ignore_dry_run": True} if ignore_dry_run else {}
|
|
1128
|
+
result = self._request("POST", f"/agentengine/api/v1/{action}", body, **request_kwargs)
|
|
1129
|
+
|
|
1130
|
+
# 检查错误 (统一返回格式 {"Code": 0, ...})
|
|
1131
|
+
code = result.get("Code", 0)
|
|
1132
|
+
if code != 0:
|
|
1133
|
+
msg = result.get("Message", "Unknown API error")
|
|
1134
|
+
raise AgentEngineAPIError(code=code, message=msg)
|
|
1135
|
+
|
|
1136
|
+
if result.get("Error"):
|
|
1137
|
+
raise Exception(result["Error"].get("Message", "Unknown error"))
|
|
1138
|
+
|
|
1139
|
+
# 提取 Data 并统一转换为 snake_case
|
|
1140
|
+
data = result.get("Data") if result.get("Data") is not None else result
|
|
1141
|
+
data = self._to_snake_case(data)
|
|
1142
|
+
|
|
1143
|
+
# 预发环境 endpoint 协议归一化: https -> http
|
|
1144
|
+
if self.logical_region and self.logical_region.strip().lower() == "pre-online":
|
|
1145
|
+
data = self._fix_endpoints_protocol(data)
|
|
1146
|
+
|
|
1147
|
+
return data
|
|
1148
|
+
|
|
1149
|
+
def download_attachment_content(self, file_uri: str) -> AttachmentContent:
|
|
1150
|
+
"""Download hosted attachment bytes through the signed KOP action API."""
|
|
1151
|
+
normalized_uri = str(file_uri or "").strip()
|
|
1152
|
+
if not normalized_uri:
|
|
1153
|
+
raise AgentEngineAPIError(400, "FileUri is required")
|
|
1154
|
+
response = self._action_raw_request(
|
|
1155
|
+
"GET",
|
|
1156
|
+
"AttachmentContent",
|
|
1157
|
+
params={"FileUri": normalized_uri},
|
|
1158
|
+
accept="application/octet-stream",
|
|
1159
|
+
)
|
|
1160
|
+
return AttachmentContent(
|
|
1161
|
+
data=response.content,
|
|
1162
|
+
content_type=str(response.headers.get("content-type") or "application/octet-stream"),
|
|
1163
|
+
display_name=self._display_name_from_content_disposition(
|
|
1164
|
+
str(response.headers.get("content-disposition") or "")
|
|
1165
|
+
)
|
|
1166
|
+
or Path(normalized_uri.removeprefix("ae-upload://")).name
|
|
1167
|
+
or "uploaded_file",
|
|
1168
|
+
)
|
|
1169
|
+
|
|
1170
|
+
# ===== Agent Actions =====
|
|
1171
|
+
|
|
1172
|
+
@staticmethod
|
|
1173
|
+
def _normalize_network_payload(network: Optional[Dict[str, Any]]) -> Optional[Dict[str, Any]]:
|
|
1174
|
+
if not isinstance(network, dict):
|
|
1175
|
+
return None
|
|
1176
|
+
|
|
1177
|
+
def _pick(*keys: str, default: Any = None) -> Any:
|
|
1178
|
+
for key in keys:
|
|
1179
|
+
if key in network and network[key] is not None:
|
|
1180
|
+
return network[key]
|
|
1181
|
+
return default
|
|
1182
|
+
|
|
1183
|
+
payload: Dict[str, Any] = {
|
|
1184
|
+
"EnablePublicAccess": bool(
|
|
1185
|
+
_pick("enable_public_access", "enablePublicAccess", "EnablePublicAccess", default=False)
|
|
1186
|
+
),
|
|
1187
|
+
"EnableVpcAccess": bool(
|
|
1188
|
+
_pick("enable_vpc_access", "enableVpcAccess", "EnableVpcAccess", default=False)
|
|
1189
|
+
),
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
field_map = {
|
|
1193
|
+
"VpcId": ("vpc_id", "vpcId", "VpcId"),
|
|
1194
|
+
"SubnetId": ("subnet_id", "subnetId", "SubnetId"),
|
|
1195
|
+
"SecurityGroupId": ("security_group_id", "securityGroupId", "SecurityGroupId"),
|
|
1196
|
+
"AvailabilityZone": ("availability_zone", "availabilityZone", "AvailabilityZone"),
|
|
1197
|
+
}
|
|
1198
|
+
for target_key, source_keys in field_map.items():
|
|
1199
|
+
value = str(_pick(*source_keys, default="") or "").strip()
|
|
1200
|
+
if value:
|
|
1201
|
+
payload[target_key] = value
|
|
1202
|
+
|
|
1203
|
+
return payload
|
|
1204
|
+
|
|
1205
|
+
@staticmethod
|
|
1206
|
+
def _normalize_ui_config_payload(ui_config: Optional[Dict[str, Any]]) -> Optional[Dict[str, Any]]:
|
|
1207
|
+
if not isinstance(ui_config, dict):
|
|
1208
|
+
return None
|
|
1209
|
+
|
|
1210
|
+
def _extract(*keys: str) -> tuple[bool, Any]:
|
|
1211
|
+
for key in keys:
|
|
1212
|
+
if key in ui_config:
|
|
1213
|
+
return True, ui_config.get(key)
|
|
1214
|
+
return False, None
|
|
1215
|
+
|
|
1216
|
+
profile_present, profile = _extract("profile", "Profile")
|
|
1217
|
+
path_present, path = _extract("path", "Path")
|
|
1218
|
+
url_present, url = _extract("url", "Url")
|
|
1219
|
+
if not (profile_present or path_present or url_present):
|
|
1220
|
+
return None
|
|
1221
|
+
|
|
1222
|
+
payload: Dict[str, Any] = {}
|
|
1223
|
+
if profile_present:
|
|
1224
|
+
payload["Profile"] = str(profile).strip() if profile is not None else None
|
|
1225
|
+
if path_present:
|
|
1226
|
+
payload["Path"] = str(path).strip() if path is not None else None
|
|
1227
|
+
if url_present:
|
|
1228
|
+
payload["Url"] = str(url).strip() if url is not None else None
|
|
1229
|
+
return payload
|
|
1230
|
+
|
|
1231
|
+
@staticmethod
|
|
1232
|
+
def _normalize_storage_payload(storage: Optional[Dict[str, Any]]) -> Optional[Dict[str, Any]]:
|
|
1233
|
+
if not isinstance(storage, dict):
|
|
1234
|
+
return None
|
|
1235
|
+
|
|
1236
|
+
mount_path = str(
|
|
1237
|
+
storage.get("mount_path")
|
|
1238
|
+
or storage.get("mountPath")
|
|
1239
|
+
or storage.get("MountPath")
|
|
1240
|
+
or ""
|
|
1241
|
+
).strip()
|
|
1242
|
+
size_gi = storage.get("size_gi", storage.get("sizeGi", storage.get("SizeGi")))
|
|
1243
|
+
if not mount_path and size_gi is None:
|
|
1244
|
+
return None
|
|
1245
|
+
|
|
1246
|
+
payload: Dict[str, Any] = {}
|
|
1247
|
+
if mount_path:
|
|
1248
|
+
payload["MountPath"] = mount_path
|
|
1249
|
+
if size_gi is not None:
|
|
1250
|
+
payload["SizeGi"] = int(size_gi)
|
|
1251
|
+
return payload
|
|
1252
|
+
|
|
1253
|
+
@staticmethod
|
|
1254
|
+
def _normalize_memory_config_payload(memory_config: Optional[Dict[str, Any]]) -> Optional[Dict[str, Any]]:
|
|
1255
|
+
if not isinstance(memory_config, dict):
|
|
1256
|
+
return None
|
|
1257
|
+
|
|
1258
|
+
memory_system = str(
|
|
1259
|
+
memory_config.get("memory_system")
|
|
1260
|
+
or memory_config.get("MemorySystem")
|
|
1261
|
+
or ""
|
|
1262
|
+
).strip()
|
|
1263
|
+
if not memory_system:
|
|
1264
|
+
return None
|
|
1265
|
+
|
|
1266
|
+
payload: Dict[str, Any] = {
|
|
1267
|
+
"MemorySystem": memory_system,
|
|
1268
|
+
}
|
|
1269
|
+
field_mapping = {
|
|
1270
|
+
"Mem0InstanceId": (
|
|
1271
|
+
memory_config.get("mem0_instance_id")
|
|
1272
|
+
or memory_config.get("Mem0InstanceId")
|
|
1273
|
+
),
|
|
1274
|
+
"Mem0InstanceName": (
|
|
1275
|
+
memory_config.get("mem0_instance_name")
|
|
1276
|
+
or memory_config.get("Mem0InstanceName")
|
|
1277
|
+
),
|
|
1278
|
+
"Mem0Region": (
|
|
1279
|
+
memory_config.get("mem0_region")
|
|
1280
|
+
or memory_config.get("Mem0Region")
|
|
1281
|
+
),
|
|
1282
|
+
}
|
|
1283
|
+
for key, value in field_mapping.items():
|
|
1284
|
+
text = str(value or "").strip()
|
|
1285
|
+
if text:
|
|
1286
|
+
payload[key] = text
|
|
1287
|
+
return payload
|
|
1288
|
+
|
|
1289
|
+
async def create_agent(self, data: Dict[str, Any]) -> Dict[str, Any]:
|
|
1290
|
+
"""创建 Agent (通过 CreateAgentProduct 走订单流程)"""
|
|
1291
|
+
framework = self._normalize_framework_name(data.get("framework"))
|
|
1292
|
+
params = {
|
|
1293
|
+
"Name": data.get("name"),
|
|
1294
|
+
"Description": data.get("description"),
|
|
1295
|
+
"Framework": framework,
|
|
1296
|
+
"DeploymentType": data.get("artifact_type", "Code"),
|
|
1297
|
+
"Region": self._normalize_payload_region(data.get("region", "cn-beijing-6")),
|
|
1298
|
+
"Resource": {
|
|
1299
|
+
"Cpu": int(float(data.get("resources", {}).get("cpu", 2))),
|
|
1300
|
+
"Memory": int(str(data.get("resources", {}).get("memory", "4Gi")).replace("Gi", ""))
|
|
1301
|
+
},
|
|
1302
|
+
"Scaling": {
|
|
1303
|
+
"MinReplicas": int(data.get("scaling", {}).get("min_replicas", 1)),
|
|
1304
|
+
"MaxReplicas": int(data.get("scaling", {}).get("max_replicas", 10)),
|
|
1305
|
+
"QpsPerInstance": int(data.get("scaling", {}).get("concurrency", 20)),
|
|
1306
|
+
},
|
|
1307
|
+
"AutoPay": True,
|
|
1308
|
+
}
|
|
1309
|
+
|
|
1310
|
+
network_payload = self._normalize_network_payload(data.get("network"))
|
|
1311
|
+
if network_payload:
|
|
1312
|
+
params["Network"] = network_payload
|
|
1313
|
+
|
|
1314
|
+
ui_config_payload = self._normalize_ui_config_payload(data.get("ui_config"))
|
|
1315
|
+
if ui_config_payload is not None:
|
|
1316
|
+
params["UiConfig"] = ui_config_payload
|
|
1317
|
+
|
|
1318
|
+
storage_payload = self._normalize_storage_payload(data.get("storage"))
|
|
1319
|
+
if storage_payload is not None:
|
|
1320
|
+
params["Storage"] = storage_payload
|
|
1321
|
+
|
|
1322
|
+
memory_config_payload = self._normalize_memory_config_payload(data.get("memory_config"))
|
|
1323
|
+
if memory_config_payload is not None:
|
|
1324
|
+
params["MemoryConfig"] = memory_config_payload
|
|
1325
|
+
|
|
1326
|
+
# 访问控制 (默认 ApiKey;OpenClaw 可显式传入 None 关闭平台层鉴权)
|
|
1327
|
+
auth_type = data.get("auth_type")
|
|
1328
|
+
if auth_type:
|
|
1329
|
+
params["Access"] = {
|
|
1330
|
+
"AuthType": auth_type,
|
|
1331
|
+
"IamRole": data.get("iam_role", "KsyunAgentEngineDefaultRole"),
|
|
1332
|
+
}
|
|
1333
|
+
|
|
1334
|
+
if params["DeploymentType"] == "Code":
|
|
1335
|
+
ks3 = data.get("ks3", {})
|
|
1336
|
+
params["CodeConfig"] = {
|
|
1337
|
+
"Path": data.get("artifact_path", ""),
|
|
1338
|
+
"AccessKey": ks3.get("access_key"),
|
|
1339
|
+
"SecretKey": ks3.get("secret_key"),
|
|
1340
|
+
"Region": self._normalize_payload_region(ks3.get("region", "cn-beijing-6")),
|
|
1341
|
+
"Bucket": ks3.get("bucket"),
|
|
1342
|
+
}
|
|
1343
|
+
else:
|
|
1344
|
+
ic = data.get("image_credential", {}) or {}
|
|
1345
|
+
artifact = (data.get("artifact_path", "") or "").strip()
|
|
1346
|
+
params["ContainerConfig"] = self._build_container_config_payload(
|
|
1347
|
+
artifact=artifact,
|
|
1348
|
+
image_credential=ic,
|
|
1349
|
+
)
|
|
1350
|
+
|
|
1351
|
+
env_vars = []
|
|
1352
|
+
envs = data.get("env_vars") or data.get("environment_variables")
|
|
1353
|
+
if envs:
|
|
1354
|
+
if isinstance(envs, dict):
|
|
1355
|
+
for k, v in envs.items():
|
|
1356
|
+
env_vars.append({"Key": k, "Value": str(v), "IsSensitive": False})
|
|
1357
|
+
elif isinstance(envs, list):
|
|
1358
|
+
env_vars = envs
|
|
1359
|
+
|
|
1360
|
+
advanced = {
|
|
1361
|
+
"EnableObservability": True,
|
|
1362
|
+
"EnvironmentVariables": env_vars,
|
|
1363
|
+
}
|
|
1364
|
+
inbound_identity_auth = data.get("inbound_identity_auth")
|
|
1365
|
+
if inbound_identity_auth is not None:
|
|
1366
|
+
advanced["InboundIdentityAuth"] = inbound_identity_auth
|
|
1367
|
+
project_id = data.get("project_id")
|
|
1368
|
+
if project_id:
|
|
1369
|
+
advanced["ProjectId"] = project_id
|
|
1370
|
+
params["Advanced"] = advanced
|
|
1371
|
+
|
|
1372
|
+
return self._action("CreateAgentProduct", params)
|
|
1373
|
+
|
|
1374
|
+
async def get_agent(self, agent_id: str = None, name: str = None, include_api_key: bool = False) -> Dict[str, Any]:
|
|
1375
|
+
"""获取 Agent 详情(支持 AgentId 或 Name 查询)"""
|
|
1376
|
+
if agent_id:
|
|
1377
|
+
params: Dict[str, Any] = {"AgentId": agent_id}
|
|
1378
|
+
if include_api_key:
|
|
1379
|
+
params["IncludeApiKey"] = True
|
|
1380
|
+
try:
|
|
1381
|
+
return self._action("GetAgent", params)
|
|
1382
|
+
except Exception as e:
|
|
1383
|
+
# 兼容旧控制面:极少数版本仍使用 Id 字段
|
|
1384
|
+
if self._should_fallback_get_agent_with_legacy_id(e):
|
|
1385
|
+
fallback = {"Id": agent_id}
|
|
1386
|
+
if include_api_key:
|
|
1387
|
+
fallback["IncludeApiKey"] = True
|
|
1388
|
+
return self._action("GetAgent", fallback)
|
|
1389
|
+
raise
|
|
1390
|
+
|
|
1391
|
+
if name:
|
|
1392
|
+
params = {"Name": name}
|
|
1393
|
+
if include_api_key:
|
|
1394
|
+
params["IncludeApiKey"] = True
|
|
1395
|
+
return self._action("GetAgent", params)
|
|
1396
|
+
|
|
1397
|
+
return self._action("GetAgent", {})
|
|
1398
|
+
|
|
1399
|
+
async def get_agent_ui_bootstrap(
|
|
1400
|
+
self,
|
|
1401
|
+
*,
|
|
1402
|
+
agent_id: str | None = None,
|
|
1403
|
+
name: str | None = None,
|
|
1404
|
+
session_id: str | None = None,
|
|
1405
|
+
) -> Dict[str, Any]:
|
|
1406
|
+
"""获取 Agent UI bootstrap 元数据。"""
|
|
1407
|
+
params: Dict[str, Any] = {}
|
|
1408
|
+
if agent_id:
|
|
1409
|
+
params["AgentId"] = agent_id
|
|
1410
|
+
if name:
|
|
1411
|
+
params["Name"] = name
|
|
1412
|
+
if session_id:
|
|
1413
|
+
params["SessionId"] = session_id
|
|
1414
|
+
return self._action("GetAgentUiBootstrap", params)
|
|
1415
|
+
|
|
1416
|
+
async def create_dashboard_access_link(
|
|
1417
|
+
self,
|
|
1418
|
+
*,
|
|
1419
|
+
agent_id: Optional[str] = None,
|
|
1420
|
+
name: Optional[str] = None,
|
|
1421
|
+
link_type: str = "private",
|
|
1422
|
+
path: Optional[str] = None,
|
|
1423
|
+
expires_seconds: Optional[int] = None,
|
|
1424
|
+
force_new: bool = False,
|
|
1425
|
+
) -> Dict[str, Any]:
|
|
1426
|
+
"""创建 Dashboard 短链接。"""
|
|
1427
|
+
params: Dict[str, Any] = {
|
|
1428
|
+
"LinkType": link_type,
|
|
1429
|
+
"ForceNew": bool(force_new),
|
|
1430
|
+
}
|
|
1431
|
+
if path is not None:
|
|
1432
|
+
params["Path"] = path
|
|
1433
|
+
if expires_seconds is not None:
|
|
1434
|
+
params["ExpiresSeconds"] = int(expires_seconds)
|
|
1435
|
+
if agent_id:
|
|
1436
|
+
params["AgentId"] = agent_id
|
|
1437
|
+
if name:
|
|
1438
|
+
params["Name"] = name
|
|
1439
|
+
return self._action("CreateDashboardAccessLink", params)
|
|
1440
|
+
|
|
1441
|
+
async def get_client_bootstrap_config(
|
|
1442
|
+
self,
|
|
1443
|
+
*,
|
|
1444
|
+
product: Optional[str] = None,
|
|
1445
|
+
framework: Optional[str] = None,
|
|
1446
|
+
region: Optional[str] = None,
|
|
1447
|
+
client_type: str = "cli",
|
|
1448
|
+
client_version: Optional[str] = None,
|
|
1449
|
+
locale: Optional[str] = None,
|
|
1450
|
+
ignore_dry_run: bool = False,
|
|
1451
|
+
) -> Dict[str, Any]:
|
|
1452
|
+
"""获取客户端启动配置(动态默认值/升级提示/公告)。"""
|
|
1453
|
+
params: Dict[str, Any] = {
|
|
1454
|
+
"ClientType": client_type or "cli",
|
|
1455
|
+
}
|
|
1456
|
+
if product:
|
|
1457
|
+
params["Product"] = product
|
|
1458
|
+
if framework:
|
|
1459
|
+
params["Framework"] = framework
|
|
1460
|
+
if region:
|
|
1461
|
+
params["Region"] = region
|
|
1462
|
+
if client_version:
|
|
1463
|
+
params["ClientVersion"] = client_version
|
|
1464
|
+
if locale:
|
|
1465
|
+
params["Locale"] = locale
|
|
1466
|
+
return self._action("GetClientBootstrapConfig", params, ignore_dry_run=ignore_dry_run)
|
|
1467
|
+
|
|
1468
|
+
async def list_dashboard_access_links(
|
|
1469
|
+
self,
|
|
1470
|
+
*,
|
|
1471
|
+
agent_id: Optional[str] = None,
|
|
1472
|
+
name: Optional[str] = None,
|
|
1473
|
+
link_type: Optional[str] = None,
|
|
1474
|
+
status: Optional[str] = None,
|
|
1475
|
+
page: int = 1,
|
|
1476
|
+
size: int = 20,
|
|
1477
|
+
) -> Dict[str, Any]:
|
|
1478
|
+
"""列出 Dashboard 短链接。"""
|
|
1479
|
+
params: Dict[str, Any] = {
|
|
1480
|
+
"Page": int(page),
|
|
1481
|
+
"Size": int(size),
|
|
1482
|
+
}
|
|
1483
|
+
if agent_id:
|
|
1484
|
+
params["AgentId"] = agent_id
|
|
1485
|
+
if name:
|
|
1486
|
+
params["Name"] = name
|
|
1487
|
+
if link_type:
|
|
1488
|
+
params["LinkType"] = link_type
|
|
1489
|
+
if status:
|
|
1490
|
+
params["Status"] = status
|
|
1491
|
+
return self._action("ListDashboardAccessLinks", params)
|
|
1492
|
+
|
|
1493
|
+
async def delete_dashboard_access_link(self, *, link_id: str) -> Dict[str, Any]:
|
|
1494
|
+
"""删除 Dashboard 短链接。"""
|
|
1495
|
+
return self._action("DeleteDashboardAccessLink", {"LinkId": link_id})
|
|
1496
|
+
|
|
1497
|
+
async def list_agents(
|
|
1498
|
+
self,
|
|
1499
|
+
region: Optional[str] = None,
|
|
1500
|
+
framework: Optional[str | Sequence[str]] = None,
|
|
1501
|
+
status: Optional[str] = None,
|
|
1502
|
+
name: Optional[str] = None,
|
|
1503
|
+
agent_id: Optional[str] = None,
|
|
1504
|
+
page: int = 1,
|
|
1505
|
+
page_size: int = 20,
|
|
1506
|
+
) -> Dict[str, Any]:
|
|
1507
|
+
"""列出 Agents"""
|
|
1508
|
+
params = {
|
|
1509
|
+
"Page": int(page),
|
|
1510
|
+
"PageSize": int(page_size),
|
|
1511
|
+
"Region": self._normalize_payload_region(region),
|
|
1512
|
+
}
|
|
1513
|
+
normalized_framework = self._normalize_framework_filters(framework)
|
|
1514
|
+
if normalized_framework:
|
|
1515
|
+
params["Framework"] = normalized_framework
|
|
1516
|
+
if status:
|
|
1517
|
+
params["Status"] = status
|
|
1518
|
+
if name:
|
|
1519
|
+
params["Name"] = name
|
|
1520
|
+
if agent_id:
|
|
1521
|
+
params["Id"] = agent_id
|
|
1522
|
+
return self._action("ListAgents", params)
|
|
1523
|
+
|
|
1524
|
+
async def get_agent_logs(
|
|
1525
|
+
self,
|
|
1526
|
+
*,
|
|
1527
|
+
agent_id: str,
|
|
1528
|
+
instance: Optional[str] = None,
|
|
1529
|
+
log_type: str = "Stdout",
|
|
1530
|
+
start_time: Optional[int] = None,
|
|
1531
|
+
end_time: Optional[int] = None,
|
|
1532
|
+
keyword: Optional[str] = None,
|
|
1533
|
+
page: int = 1,
|
|
1534
|
+
page_size: int = 100,
|
|
1535
|
+
) -> Dict[str, Any]:
|
|
1536
|
+
"""获取 Agent 日志。"""
|
|
1537
|
+
normalized_log_type = (log_type or "Stdout").strip()
|
|
1538
|
+
if normalized_log_type.lower() == "stdout":
|
|
1539
|
+
normalized_log_type = "Stdout"
|
|
1540
|
+
elif normalized_log_type.lower() == "log":
|
|
1541
|
+
normalized_log_type = "Log"
|
|
1542
|
+
|
|
1543
|
+
params: Dict[str, Any] = {
|
|
1544
|
+
"AgentId": agent_id,
|
|
1545
|
+
"LogType": normalized_log_type,
|
|
1546
|
+
"Page": int(page),
|
|
1547
|
+
"PageSize": int(page_size),
|
|
1548
|
+
}
|
|
1549
|
+
if instance:
|
|
1550
|
+
params["Instance"] = instance
|
|
1551
|
+
if start_time is not None:
|
|
1552
|
+
params["StartTime"] = int(start_time)
|
|
1553
|
+
if end_time is not None:
|
|
1554
|
+
params["EndTime"] = int(end_time)
|
|
1555
|
+
if keyword:
|
|
1556
|
+
params["Keyword"] = keyword
|
|
1557
|
+
return self._action("GetAgentLogs", params)
|
|
1558
|
+
|
|
1559
|
+
async def run_openclaw_repair(
|
|
1560
|
+
self,
|
|
1561
|
+
agent_id: str,
|
|
1562
|
+
*,
|
|
1563
|
+
repair_action: str = "doctor-fix",
|
|
1564
|
+
) -> Dict[str, Any]:
|
|
1565
|
+
"""在控制面触发 OpenClaw runtime 修复动作。"""
|
|
1566
|
+
return self._action(
|
|
1567
|
+
"RunOpenClawRepair",
|
|
1568
|
+
{
|
|
1569
|
+
"AgentId": agent_id,
|
|
1570
|
+
"RepairAction": repair_action,
|
|
1571
|
+
},
|
|
1572
|
+
)
|
|
1573
|
+
|
|
1574
|
+
async def delete_agent(self, agent_id: str) -> bool:
|
|
1575
|
+
"""删除 Agent"""
|
|
1576
|
+
self._action("DeleteAgent", {"AgentId": agent_id})
|
|
1577
|
+
return True
|
|
1578
|
+
|
|
1579
|
+
|
|
1580
|
+
|
|
1581
|
+
async def update_agent(self, agent_id: str, data: Dict[str, Any]) -> Dict[str, Any]:
|
|
1582
|
+
"""更新 Agent (热更新)"""
|
|
1583
|
+
params = {"AgentId": agent_id}
|
|
1584
|
+
if data.get("description"):
|
|
1585
|
+
params["Description"] = data["description"]
|
|
1586
|
+
|
|
1587
|
+
if data.get("artifact_path"):
|
|
1588
|
+
artifact = (data.get("artifact_path", "") or "").strip()
|
|
1589
|
+
if (data.get("artifact_type") or "").lower() == "container":
|
|
1590
|
+
ic = data.get("image_credential", {}) or {}
|
|
1591
|
+
params["ContainerConfig"] = self._build_container_config_payload(
|
|
1592
|
+
artifact=artifact,
|
|
1593
|
+
image_credential=ic,
|
|
1594
|
+
)
|
|
1595
|
+
else:
|
|
1596
|
+
ks3 = data.get("ks3", {})
|
|
1597
|
+
params["CodeConfig"] = {
|
|
1598
|
+
"Path": artifact,
|
|
1599
|
+
"AccessKey": ks3.get("access_key"),
|
|
1600
|
+
"SecretKey": ks3.get("secret_key"),
|
|
1601
|
+
"Region": self._normalize_payload_region(ks3.get("region", "cn-beijing-6")),
|
|
1602
|
+
"Bucket": ks3.get("bucket"),
|
|
1603
|
+
}
|
|
1604
|
+
|
|
1605
|
+
if data.get("resources"):
|
|
1606
|
+
params["Resource"] = {
|
|
1607
|
+
"Cpu": int(float(data["resources"].get("cpu", 2))),
|
|
1608
|
+
"Memory": int(str(data["resources"].get("memory", "4Gi")).replace("Gi", ""))
|
|
1609
|
+
}
|
|
1610
|
+
|
|
1611
|
+
if data.get("scaling"):
|
|
1612
|
+
params["Scaling"] = {
|
|
1613
|
+
"MinReplicas": int(data["scaling"].get("min_replicas", 1)),
|
|
1614
|
+
"MaxReplicas": int(data["scaling"].get("max_replicas", 10)),
|
|
1615
|
+
"Concurrency": int(data["scaling"].get("concurrency", 20)),
|
|
1616
|
+
}
|
|
1617
|
+
|
|
1618
|
+
envs = data.get("env_vars") or data.get("environment_variables")
|
|
1619
|
+
if envs:
|
|
1620
|
+
env_vars = []
|
|
1621
|
+
if isinstance(envs, dict):
|
|
1622
|
+
for k, v in envs.items():
|
|
1623
|
+
env_vars.append({"Key": k, "Value": str(v), "IsSensitive": False})
|
|
1624
|
+
elif isinstance(envs, list):
|
|
1625
|
+
env_vars = envs
|
|
1626
|
+
params["EnvironmentVariables"] = env_vars
|
|
1627
|
+
|
|
1628
|
+
network_payload = self._normalize_network_payload(data.get("network"))
|
|
1629
|
+
if network_payload:
|
|
1630
|
+
params["Network"] = network_payload
|
|
1631
|
+
|
|
1632
|
+
ui_config_payload = self._normalize_ui_config_payload(data.get("ui_config"))
|
|
1633
|
+
if ui_config_payload is not None:
|
|
1634
|
+
params["UiConfig"] = ui_config_payload
|
|
1635
|
+
|
|
1636
|
+
storage_payload = self._normalize_storage_payload(data.get("storage"))
|
|
1637
|
+
if storage_payload is not None:
|
|
1638
|
+
params["Storage"] = storage_payload
|
|
1639
|
+
|
|
1640
|
+
memory_config_payload = self._normalize_memory_config_payload(data.get("memory_config"))
|
|
1641
|
+
if memory_config_payload is not None:
|
|
1642
|
+
params["MemoryConfig"] = memory_config_payload
|
|
1643
|
+
|
|
1644
|
+
# 访问控制 (可选)
|
|
1645
|
+
auth_type = data.get("auth_type")
|
|
1646
|
+
if auth_type:
|
|
1647
|
+
params["Access"] = {
|
|
1648
|
+
"AuthType": auth_type,
|
|
1649
|
+
"IamRole": data.get("iam_role", "KsyunAgentEngineDefaultRole"),
|
|
1650
|
+
}
|
|
1651
|
+
|
|
1652
|
+
# 高级配置 (可选)
|
|
1653
|
+
advanced = {}
|
|
1654
|
+
observability = data.get("observability")
|
|
1655
|
+
if isinstance(observability, dict) and "langfuse_enabled" in observability:
|
|
1656
|
+
advanced["EnableObservability"] = bool(observability.get("langfuse_enabled"))
|
|
1657
|
+
elif "enable_observability" in data:
|
|
1658
|
+
advanced["EnableObservability"] = bool(data.get("enable_observability"))
|
|
1659
|
+
inbound_identity_auth = data.get("inbound_identity_auth")
|
|
1660
|
+
if inbound_identity_auth is not None:
|
|
1661
|
+
advanced["InboundIdentityAuth"] = inbound_identity_auth
|
|
1662
|
+
project_id = data.get("project_id")
|
|
1663
|
+
if project_id:
|
|
1664
|
+
advanced["ProjectId"] = project_id
|
|
1665
|
+
if advanced:
|
|
1666
|
+
params["Advanced"] = advanced
|
|
1667
|
+
|
|
1668
|
+
return self._action("UpdateAgent", params)
|
|
1669
|
+
|
|
1670
|
+
# ===== Session Actions =====
|
|
1671
|
+
|
|
1672
|
+
async def create_session(self, agent_id: str, user_id: Optional[str] = None, expires_hours: int = 24) -> Dict[str, Any]:
|
|
1673
|
+
"""创建会话"""
|
|
1674
|
+
return self._action("CreateSession", {
|
|
1675
|
+
"AgentId": agent_id,
|
|
1676
|
+
"UserId": user_id,
|
|
1677
|
+
"ExpiresHours": expires_hours
|
|
1678
|
+
})
|
|
1679
|
+
|
|
1680
|
+
async def get_session(self, session_id: str) -> Dict[str, Any]:
|
|
1681
|
+
"""获取会话详情"""
|
|
1682
|
+
return self._action("GetSession", {"Id": session_id})
|
|
1683
|
+
|
|
1684
|
+
async def list_sessions(self, agent_id: str, page: int = 1, size: int = 20) -> Dict[str, Any]:
|
|
1685
|
+
"""列出会话"""
|
|
1686
|
+
return self._action("ListSessions", {"AgentId": agent_id, "Page": page, "PageSize": size})
|
|
1687
|
+
|
|
1688
|
+
async def delete_session(self, session_id: str) -> bool:
|
|
1689
|
+
"""删除会话"""
|
|
1690
|
+
try:
|
|
1691
|
+
self._action("DeleteSession", {"Id": session_id})
|
|
1692
|
+
return True
|
|
1693
|
+
except Exception:
|
|
1694
|
+
return False
|
|
1695
|
+
|
|
1696
|
+
async def list_workspace_files(
|
|
1697
|
+
self,
|
|
1698
|
+
*,
|
|
1699
|
+
agent_id: str | None = None,
|
|
1700
|
+
name: str | None = None,
|
|
1701
|
+
path: str = ".",
|
|
1702
|
+
recursive: bool = False,
|
|
1703
|
+
endpoint: str | None = None,
|
|
1704
|
+
api_key: str | None = None,
|
|
1705
|
+
) -> Dict[str, Any]:
|
|
1706
|
+
transport = await self._resolve_workspace_transport(
|
|
1707
|
+
agent_id=agent_id,
|
|
1708
|
+
name=name,
|
|
1709
|
+
endpoint=endpoint,
|
|
1710
|
+
api_key=api_key,
|
|
1711
|
+
)
|
|
1712
|
+
if transport["mode"] == "action":
|
|
1713
|
+
return self._annotate_workspace_payload(
|
|
1714
|
+
self._action(
|
|
1715
|
+
"ListWorkspaceFiles",
|
|
1716
|
+
self._compact_params(
|
|
1717
|
+
{
|
|
1718
|
+
"AgentId": transport.get("agent_id"),
|
|
1719
|
+
"Name": transport.get("name"),
|
|
1720
|
+
"Path": path,
|
|
1721
|
+
"Recursive": recursive,
|
|
1722
|
+
}
|
|
1723
|
+
),
|
|
1724
|
+
),
|
|
1725
|
+
transport_mode="action_proxy",
|
|
1726
|
+
)
|
|
1727
|
+
access = transport["access"]
|
|
1728
|
+
response = self._workspace_runtime_request(
|
|
1729
|
+
access=access,
|
|
1730
|
+
method="GET",
|
|
1731
|
+
path="entries",
|
|
1732
|
+
params={"path": path, "recursive": str(bool(recursive)).lower()},
|
|
1733
|
+
)
|
|
1734
|
+
return self._annotate_workspace_payload(
|
|
1735
|
+
self._workspace_runtime_json(response),
|
|
1736
|
+
transport_mode="runtime_direct",
|
|
1737
|
+
)
|
|
1738
|
+
|
|
1739
|
+
async def get_workspace_health(
|
|
1740
|
+
self,
|
|
1741
|
+
*,
|
|
1742
|
+
agent_id: str | None = None,
|
|
1743
|
+
name: str | None = None,
|
|
1744
|
+
endpoint: str | None = None,
|
|
1745
|
+
api_key: str | None = None,
|
|
1746
|
+
) -> Dict[str, Any]:
|
|
1747
|
+
transport = await self._resolve_workspace_transport(
|
|
1748
|
+
agent_id=agent_id,
|
|
1749
|
+
name=name,
|
|
1750
|
+
endpoint=endpoint,
|
|
1751
|
+
api_key=api_key,
|
|
1752
|
+
)
|
|
1753
|
+
if transport["mode"] == "action":
|
|
1754
|
+
return {}
|
|
1755
|
+
access = transport["access"]
|
|
1756
|
+
response = self._workspace_runtime_request(
|
|
1757
|
+
access=access,
|
|
1758
|
+
method="GET",
|
|
1759
|
+
path="healthz",
|
|
1760
|
+
)
|
|
1761
|
+
return self._annotate_workspace_payload(
|
|
1762
|
+
self._workspace_runtime_json(response),
|
|
1763
|
+
transport_mode="runtime_direct",
|
|
1764
|
+
)
|
|
1765
|
+
|
|
1766
|
+
async def upload_workspace_file(
|
|
1767
|
+
self,
|
|
1768
|
+
*,
|
|
1769
|
+
agent_id: str | None = None,
|
|
1770
|
+
name: str | None = None,
|
|
1771
|
+
remote_path: str,
|
|
1772
|
+
local_path: str | Path,
|
|
1773
|
+
endpoint: str | None = None,
|
|
1774
|
+
api_key: str | None = None,
|
|
1775
|
+
) -> Dict[str, Any]:
|
|
1776
|
+
transport = await self._resolve_workspace_transport(
|
|
1777
|
+
agent_id=agent_id,
|
|
1778
|
+
name=name,
|
|
1779
|
+
endpoint=endpoint,
|
|
1780
|
+
api_key=api_key,
|
|
1781
|
+
)
|
|
1782
|
+
file_path = Path(local_path)
|
|
1783
|
+
file_bytes = file_path.read_bytes()
|
|
1784
|
+
guessed_type = mimetypes.guess_type(file_path.name)[0] or "application/octet-stream"
|
|
1785
|
+
if transport["mode"] == "action":
|
|
1786
|
+
response = self._action_raw_request(
|
|
1787
|
+
"POST",
|
|
1788
|
+
"AddWorkspaceFile",
|
|
1789
|
+
data={
|
|
1790
|
+
"AgentId": transport.get("agent_id"),
|
|
1791
|
+
"Name": transport.get("name"),
|
|
1792
|
+
"Path": remote_path,
|
|
1793
|
+
},
|
|
1794
|
+
files={
|
|
1795
|
+
"file": (
|
|
1796
|
+
file_path.name,
|
|
1797
|
+
file_bytes,
|
|
1798
|
+
guessed_type,
|
|
1799
|
+
)
|
|
1800
|
+
},
|
|
1801
|
+
)
|
|
1802
|
+
return self._annotate_workspace_payload(
|
|
1803
|
+
self._workspace_runtime_json(response),
|
|
1804
|
+
transport_mode="action_proxy",
|
|
1805
|
+
)
|
|
1806
|
+
access = transport["access"]
|
|
1807
|
+
response = self._workspace_runtime_request(
|
|
1808
|
+
access=access,
|
|
1809
|
+
method="POST",
|
|
1810
|
+
path=f"files/{self._encode_workspace_runtime_path(remote_path)}",
|
|
1811
|
+
files={
|
|
1812
|
+
"file": (
|
|
1813
|
+
file_path.name,
|
|
1814
|
+
file_bytes,
|
|
1815
|
+
guessed_type,
|
|
1816
|
+
)
|
|
1817
|
+
},
|
|
1818
|
+
)
|
|
1819
|
+
return self._annotate_workspace_payload(
|
|
1820
|
+
self._workspace_runtime_json(response),
|
|
1821
|
+
transport_mode="runtime_direct",
|
|
1822
|
+
)
|
|
1823
|
+
|
|
1824
|
+
async def download_workspace_file(
|
|
1825
|
+
self,
|
|
1826
|
+
*,
|
|
1827
|
+
agent_id: str | None = None,
|
|
1828
|
+
name: str | None = None,
|
|
1829
|
+
remote_path: str,
|
|
1830
|
+
endpoint: str | None = None,
|
|
1831
|
+
api_key: str | None = None,
|
|
1832
|
+
) -> bytes:
|
|
1833
|
+
transport = await self._resolve_workspace_transport(
|
|
1834
|
+
agent_id=agent_id,
|
|
1835
|
+
name=name,
|
|
1836
|
+
endpoint=endpoint,
|
|
1837
|
+
api_key=api_key,
|
|
1838
|
+
)
|
|
1839
|
+
if transport["mode"] == "action":
|
|
1840
|
+
response = self._action_raw_request(
|
|
1841
|
+
"GET",
|
|
1842
|
+
"GetWorkspaceFileContent",
|
|
1843
|
+
params={
|
|
1844
|
+
"AgentId": transport.get("agent_id"),
|
|
1845
|
+
"Name": transport.get("name"),
|
|
1846
|
+
"FilePath": remote_path,
|
|
1847
|
+
},
|
|
1848
|
+
accept="application/octet-stream",
|
|
1849
|
+
)
|
|
1850
|
+
return response.content
|
|
1851
|
+
access = transport["access"]
|
|
1852
|
+
response = self._workspace_runtime_request(
|
|
1853
|
+
access=access,
|
|
1854
|
+
method="GET",
|
|
1855
|
+
path=f"files/{self._encode_workspace_runtime_path(remote_path)}",
|
|
1856
|
+
)
|
|
1857
|
+
return response.content
|
|
1858
|
+
|
|
1859
|
+
async def delete_workspace_file(
|
|
1860
|
+
self,
|
|
1861
|
+
*,
|
|
1862
|
+
agent_id: str | None = None,
|
|
1863
|
+
name: str | None = None,
|
|
1864
|
+
remote_path: str,
|
|
1865
|
+
endpoint: str | None = None,
|
|
1866
|
+
api_key: str | None = None,
|
|
1867
|
+
) -> Dict[str, Any]:
|
|
1868
|
+
transport = await self._resolve_workspace_transport(
|
|
1869
|
+
agent_id=agent_id,
|
|
1870
|
+
name=name,
|
|
1871
|
+
endpoint=endpoint,
|
|
1872
|
+
api_key=api_key,
|
|
1873
|
+
)
|
|
1874
|
+
if transport["mode"] == "action":
|
|
1875
|
+
return self._annotate_workspace_payload(
|
|
1876
|
+
self._action(
|
|
1877
|
+
"DeleteWorkspaceFile",
|
|
1878
|
+
self._compact_params(
|
|
1879
|
+
{
|
|
1880
|
+
"AgentId": transport.get("agent_id"),
|
|
1881
|
+
"Name": transport.get("name"),
|
|
1882
|
+
"Path": remote_path,
|
|
1883
|
+
}
|
|
1884
|
+
),
|
|
1885
|
+
),
|
|
1886
|
+
transport_mode="action_proxy",
|
|
1887
|
+
)
|
|
1888
|
+
access = transport["access"]
|
|
1889
|
+
response = self._workspace_runtime_request(
|
|
1890
|
+
access=access,
|
|
1891
|
+
method="DELETE",
|
|
1892
|
+
path=f"files/{self._encode_workspace_runtime_path(remote_path)}",
|
|
1893
|
+
)
|
|
1894
|
+
return self._annotate_workspace_payload(
|
|
1895
|
+
self._workspace_runtime_json(response),
|
|
1896
|
+
transport_mode="runtime_direct",
|
|
1897
|
+
)
|
|
1898
|
+
|
|
1899
|
+
# ===== MCP Actions =====
|
|
1900
|
+
|
|
1901
|
+
@staticmethod
|
|
1902
|
+
def _looks_like_nested_mcp_payload(data: Dict[str, Any]) -> bool:
|
|
1903
|
+
return any(
|
|
1904
|
+
key in data
|
|
1905
|
+
for key in (
|
|
1906
|
+
"DeploymentType",
|
|
1907
|
+
"CodeConfig",
|
|
1908
|
+
"ContainerConfig",
|
|
1909
|
+
"Resource",
|
|
1910
|
+
"Scaling",
|
|
1911
|
+
"Access",
|
|
1912
|
+
"Advanced",
|
|
1913
|
+
"Network",
|
|
1914
|
+
)
|
|
1915
|
+
)
|
|
1916
|
+
|
|
1917
|
+
@staticmethod
|
|
1918
|
+
def _normalize_mcp_memory(memory: Any, default: int = 2) -> int:
|
|
1919
|
+
raw = str(memory or "").strip()
|
|
1920
|
+
if not raw:
|
|
1921
|
+
return default
|
|
1922
|
+
lowered = raw.lower()
|
|
1923
|
+
if lowered.endswith("gi"):
|
|
1924
|
+
raw = raw[:-2]
|
|
1925
|
+
return int(float(raw))
|
|
1926
|
+
|
|
1927
|
+
@staticmethod
|
|
1928
|
+
def _normalize_mcp_deployment_type(value: Optional[str], default: Optional[str] = None) -> Optional[str]:
|
|
1929
|
+
raw = str(value or default or "").strip()
|
|
1930
|
+
if not raw:
|
|
1931
|
+
return None
|
|
1932
|
+
return "Container" if raw.lower() == "container" else "Code"
|
|
1933
|
+
|
|
1934
|
+
def _infer_mcp_deployment_type(self, data: Dict[str, Any], default: Optional[str] = None) -> Optional[str]:
|
|
1935
|
+
explicit = self._normalize_mcp_deployment_type(
|
|
1936
|
+
data.get("deployment_type") or data.get("artifact_type"),
|
|
1937
|
+
default=default,
|
|
1938
|
+
)
|
|
1939
|
+
if explicit:
|
|
1940
|
+
return explicit
|
|
1941
|
+
if data.get("container_config") or data.get("image_credential"):
|
|
1942
|
+
return "Container"
|
|
1943
|
+
artifact_path = str(data.get("artifact_path") or "").strip()
|
|
1944
|
+
if artifact_path.startswith("ks3://") or data.get("ks3"):
|
|
1945
|
+
return "Code"
|
|
1946
|
+
return default
|
|
1947
|
+
|
|
1948
|
+
def _build_mcp_code_config(self, data: Dict[str, Any]) -> Dict[str, Any]:
|
|
1949
|
+
ks3 = data.get("ks3") or {}
|
|
1950
|
+
code_config: Dict[str, Any] = {
|
|
1951
|
+
"Path": str(data.get("artifact_path") or ""),
|
|
1952
|
+
}
|
|
1953
|
+
if ks3.get("access_key") is not None:
|
|
1954
|
+
code_config["AccessKey"] = ks3.get("access_key")
|
|
1955
|
+
if ks3.get("secret_key") is not None:
|
|
1956
|
+
code_config["SecretKey"] = ks3.get("secret_key")
|
|
1957
|
+
if ks3.get("region") is not None or data.get("region") is not None:
|
|
1958
|
+
code_config["Region"] = self._normalize_payload_region(
|
|
1959
|
+
ks3.get("region") or data.get("region") or self.logical_region
|
|
1960
|
+
)
|
|
1961
|
+
if ks3.get("bucket") is not None:
|
|
1962
|
+
code_config["Bucket"] = ks3.get("bucket")
|
|
1963
|
+
return code_config
|
|
1964
|
+
|
|
1965
|
+
def _build_mcp_container_config(self, data: Dict[str, Any]) -> Dict[str, Any]:
|
|
1966
|
+
container = data.get("container_config") or {}
|
|
1967
|
+
artifact = str(data.get("artifact_path") or container.get("image_addr") or "").strip()
|
|
1968
|
+
ic = data.get("image_credential", {}) or {}
|
|
1969
|
+
return self._build_container_config_payload(
|
|
1970
|
+
artifact=artifact,
|
|
1971
|
+
image_credential=ic,
|
|
1972
|
+
container=container,
|
|
1973
|
+
)
|
|
1974
|
+
|
|
1975
|
+
def _build_mcp_resource(self, data: Dict[str, Any]) -> Dict[str, Any]:
|
|
1976
|
+
resources = data.get("resources", {}) or {}
|
|
1977
|
+
return {
|
|
1978
|
+
"Cpu": int(float(resources.get("cpu", 1))),
|
|
1979
|
+
"Memory": self._normalize_mcp_memory(resources.get("memory", "2Gi"), default=2),
|
|
1980
|
+
}
|
|
1981
|
+
|
|
1982
|
+
@staticmethod
|
|
1983
|
+
def _build_mcp_scaling(data: Dict[str, Any]) -> Dict[str, Any]:
|
|
1984
|
+
scaling = data.get("scaling", {}) or {}
|
|
1985
|
+
return {
|
|
1986
|
+
"MinReplicas": int(scaling.get("min_replicas", 1)),
|
|
1987
|
+
"MaxReplicas": int(scaling.get("max_replicas", 5)),
|
|
1988
|
+
"QpsPerInstance": int(scaling.get("concurrency", 20)),
|
|
1989
|
+
}
|
|
1990
|
+
|
|
1991
|
+
@staticmethod
|
|
1992
|
+
def _build_mcp_advanced(data: Dict[str, Any]) -> Dict[str, Any]:
|
|
1993
|
+
metadata = data.get("metadata", {}) or {}
|
|
1994
|
+
return {
|
|
1995
|
+
"McpVariable": metadata.get("mcp_variable", "mcp"),
|
|
1996
|
+
"Tools": metadata.get("tools", []) or [],
|
|
1997
|
+
}
|
|
1998
|
+
|
|
1999
|
+
async def create_mcp(self, data: Dict[str, Any]) -> Dict[str, Any]:
|
|
2000
|
+
"""创建 MCP"""
|
|
2001
|
+
if self._looks_like_nested_mcp_payload(data):
|
|
2002
|
+
params = dict(data)
|
|
2003
|
+
params["Region"] = self._normalize_payload_region(
|
|
2004
|
+
params.get("Region") or data.get("region") or self.logical_region or "cn-beijing-6"
|
|
2005
|
+
)
|
|
2006
|
+
return self._action("CreateMCP", params)
|
|
2007
|
+
|
|
2008
|
+
deployment_type = self._infer_mcp_deployment_type(data, default="Code") or "Code"
|
|
2009
|
+
params: Dict[str, Any] = {
|
|
2010
|
+
"Name": data.get("name"),
|
|
2011
|
+
"Description": data.get("description"),
|
|
2012
|
+
"Region": self._normalize_payload_region(data.get("region", "cn-beijing-6")),
|
|
2013
|
+
"DeploymentType": deployment_type,
|
|
2014
|
+
"Resource": self._build_mcp_resource(data),
|
|
2015
|
+
"Scaling": self._build_mcp_scaling(data),
|
|
2016
|
+
"Access": {"AuthType": "ApiKey" if data.get("enable_auth") else "None"},
|
|
2017
|
+
"Advanced": self._build_mcp_advanced(data),
|
|
2018
|
+
}
|
|
2019
|
+
if deployment_type == "Container":
|
|
2020
|
+
params["ContainerConfig"] = self._build_mcp_container_config(data)
|
|
2021
|
+
else:
|
|
2022
|
+
params["CodeConfig"] = self._build_mcp_code_config(data)
|
|
2023
|
+
network_payload = self._normalize_network_payload(data.get("network"))
|
|
2024
|
+
if network_payload:
|
|
2025
|
+
params["Network"] = network_payload
|
|
2026
|
+
return self._action("CreateMCP", params)
|
|
2027
|
+
|
|
2028
|
+
async def get_mcp(self, mcp_id: str) -> Dict[str, Any]:
|
|
2029
|
+
"""获取 MCP 详情"""
|
|
2030
|
+
return self._action("GetMCP", {"Id": mcp_id})
|
|
2031
|
+
|
|
2032
|
+
async def list_mcps(
|
|
2033
|
+
self,
|
|
2034
|
+
region: Optional[str] = None,
|
|
2035
|
+
page: int = 1,
|
|
2036
|
+
page_size: int = 20,
|
|
2037
|
+
) -> Dict[str, Any]:
|
|
2038
|
+
"""列出 MCP (注册中心)"""
|
|
2039
|
+
params = {"Page": int(page), "PageSize": int(page_size)}
|
|
2040
|
+
if region:
|
|
2041
|
+
params["Region"] = self._normalize_payload_region(region)
|
|
2042
|
+
result = self._action("ListMCPs", params)
|
|
2043
|
+
# _action 已统一转 snake_case: "MCPs" -> "m_c_ps"...
|
|
2044
|
+
# 实际上 MCPs -> mcps, Total -> total
|
|
2045
|
+
return {"mcps": result.get("mcps", []), "total": result.get("total", 0)}
|
|
2046
|
+
|
|
2047
|
+
async def update_mcp(self, mcp_id: str, data: Dict[str, Any]) -> Dict[str, Any]:
|
|
2048
|
+
"""更新 MCP (热更新)"""
|
|
2049
|
+
if self._looks_like_nested_mcp_payload(data):
|
|
2050
|
+
params = dict(data)
|
|
2051
|
+
params.setdefault("Id", mcp_id)
|
|
2052
|
+
if "Region" in params or data.get("region") or self.logical_region:
|
|
2053
|
+
params["Region"] = self._normalize_payload_region(
|
|
2054
|
+
params.get("Region") or data.get("region") or self.logical_region
|
|
2055
|
+
)
|
|
2056
|
+
return self._action("UpdateMCP", params)
|
|
2057
|
+
|
|
2058
|
+
params: Dict[str, Any] = {"Id": mcp_id}
|
|
2059
|
+
deployment_type = self._infer_mcp_deployment_type(data)
|
|
2060
|
+
|
|
2061
|
+
if data.get("description") is not None:
|
|
2062
|
+
params["Description"] = data["description"]
|
|
2063
|
+
if deployment_type:
|
|
2064
|
+
params["DeploymentType"] = deployment_type
|
|
2065
|
+
|
|
2066
|
+
if data.get("artifact_path"):
|
|
2067
|
+
if deployment_type == "Container":
|
|
2068
|
+
params["ContainerConfig"] = self._build_mcp_container_config(data)
|
|
2069
|
+
elif deployment_type == "Code":
|
|
2070
|
+
params["CodeConfig"] = self._build_mcp_code_config(data)
|
|
2071
|
+
|
|
2072
|
+
resources = data.get("resources", {}) or {}
|
|
2073
|
+
if any(resources.get(key) is not None for key in ("cpu", "memory")):
|
|
2074
|
+
params["Resource"] = {
|
|
2075
|
+
"Cpu": int(float(resources.get("cpu", 1))),
|
|
2076
|
+
"Memory": self._normalize_mcp_memory(resources.get("memory", "2Gi"), default=2),
|
|
2077
|
+
}
|
|
2078
|
+
|
|
2079
|
+
scaling = data.get("scaling", {}) or {}
|
|
2080
|
+
if any(scaling.get(key) is not None for key in ("min_replicas", "max_replicas", "concurrency")):
|
|
2081
|
+
params["Scaling"] = {
|
|
2082
|
+
"MinReplicas": int(scaling.get("min_replicas", 1)),
|
|
2083
|
+
"MaxReplicas": int(scaling.get("max_replicas", 5)),
|
|
2084
|
+
"QpsPerInstance": int(scaling.get("concurrency", 20)),
|
|
2085
|
+
}
|
|
2086
|
+
|
|
2087
|
+
if data.get("enable_auth") is not None:
|
|
2088
|
+
params["Access"] = {"AuthType": "ApiKey" if data.get("enable_auth") else "None"}
|
|
2089
|
+
|
|
2090
|
+
metadata = data.get("metadata", {}) or {}
|
|
2091
|
+
if metadata.get("mcp_variable") is not None or metadata.get("tools") is not None:
|
|
2092
|
+
params["Advanced"] = {
|
|
2093
|
+
"McpVariable": metadata.get("mcp_variable"),
|
|
2094
|
+
"Tools": metadata.get("tools"),
|
|
2095
|
+
}
|
|
2096
|
+
|
|
2097
|
+
network_payload = self._normalize_network_payload(data.get("network"))
|
|
2098
|
+
if network_payload:
|
|
2099
|
+
params["Network"] = network_payload
|
|
2100
|
+
if data.get("region") is not None:
|
|
2101
|
+
params["Region"] = self._normalize_payload_region(data.get("region"))
|
|
2102
|
+
return self._action("UpdateMCP", params)
|
|
2103
|
+
|
|
2104
|
+
async def delete_mcp(self, mcp_id: str) -> bool:
|
|
2105
|
+
"""删除 MCP"""
|
|
2106
|
+
try:
|
|
2107
|
+
self._action("DeleteMCP", {"Id": mcp_id})
|
|
2108
|
+
return True
|
|
2109
|
+
except Exception:
|
|
2110
|
+
return False
|
|
2111
|
+
|
|
2112
|
+
async def get_mcp_by_name(self, name: str, region: Optional[str] = None) -> Optional[Dict[str, Any]]:
|
|
2113
|
+
"""按名称查询 MCP"""
|
|
2114
|
+
# 使用 ListMCPs 然后过滤 (Action API 暂不支持 by-name)
|
|
2115
|
+
try:
|
|
2116
|
+
page = 1
|
|
2117
|
+
page_size = 100
|
|
2118
|
+
while True:
|
|
2119
|
+
result = await self.list_mcps(region=region, page=page, page_size=page_size)
|
|
2120
|
+
mcps = result.get("mcps", [])
|
|
2121
|
+
for mcp in mcps:
|
|
2122
|
+
if mcp.get("name") == name:
|
|
2123
|
+
return mcp
|
|
2124
|
+
if len(mcps) < page_size:
|
|
2125
|
+
break
|
|
2126
|
+
page += 1
|
|
2127
|
+
return None
|
|
2128
|
+
except Exception:
|
|
2129
|
+
return None
|
|
2130
|
+
|
|
2131
|
+
# ===== Upload Actions =====
|
|
2132
|
+
|
|
2133
|
+
async def get_presigned_url(self, filename: str) -> Dict[str, Any]:
|
|
2134
|
+
"""获取 KS3 预签名上传 URL"""
|
|
2135
|
+
return self._action("GetPresignedUrl", {"Filename": filename})
|
|
2136
|
+
|
|
2137
|
+
# ===== Chat Actions =====
|
|
2138
|
+
|
|
2139
|
+
async def chat(self, agent_id: str, message: str, session_id: Optional[str] = None) -> Dict[str, Any]:
|
|
2140
|
+
"""调用 Agent"""
|
|
2141
|
+
params = {
|
|
2142
|
+
"AgentId": agent_id,
|
|
2143
|
+
"Messages": [{"role": "user", "content": message}],
|
|
2144
|
+
"Stream": False
|
|
2145
|
+
}
|
|
2146
|
+
if session_id:
|
|
2147
|
+
params["SessionId"] = session_id
|
|
2148
|
+
return self._action("RunAgent", params)
|
|
2149
|
+
|
|
2150
|
+
# ===== Version Actions =====
|
|
2151
|
+
|
|
2152
|
+
async def release_version(
|
|
2153
|
+
self,
|
|
2154
|
+
agent_id: str,
|
|
2155
|
+
tag: Optional[str] = None,
|
|
2156
|
+
description: Optional[str] = None
|
|
2157
|
+
) -> Dict[str, Any]:
|
|
2158
|
+
"""发布新版本
|
|
2159
|
+
|
|
2160
|
+
Args:
|
|
2161
|
+
agent_id: Agent ID
|
|
2162
|
+
tag: 版本标签,不填则自动生成
|
|
2163
|
+
description: 版本描述
|
|
2164
|
+
|
|
2165
|
+
Returns:
|
|
2166
|
+
版本信息
|
|
2167
|
+
"""
|
|
2168
|
+
params = {"AgentId": agent_id}
|
|
2169
|
+
if tag:
|
|
2170
|
+
params["Tag"] = tag
|
|
2171
|
+
if description:
|
|
2172
|
+
params["Description"] = description
|
|
2173
|
+
return self._action("CreateVersion", params)
|
|
2174
|
+
|
|
2175
|
+
async def list_versions(
|
|
2176
|
+
self,
|
|
2177
|
+
agent_id: str,
|
|
2178
|
+
page: int = 1,
|
|
2179
|
+
size: int = 10
|
|
2180
|
+
) -> Dict[str, Any]:
|
|
2181
|
+
"""列出版本历史
|
|
2182
|
+
|
|
2183
|
+
Args:
|
|
2184
|
+
agent_id: Agent ID
|
|
2185
|
+
page: 页码
|
|
2186
|
+
size: 每页数量
|
|
2187
|
+
|
|
2188
|
+
Returns:
|
|
2189
|
+
版本列表和分页信息
|
|
2190
|
+
"""
|
|
2191
|
+
return self._action("ListVersions", {
|
|
2192
|
+
"AgentId": agent_id,
|
|
2193
|
+
"Page": page,
|
|
2194
|
+
"PageSize": size
|
|
2195
|
+
})
|
|
2196
|
+
|
|
2197
|
+
async def rollback_version(
|
|
2198
|
+
self,
|
|
2199
|
+
agent_id: str,
|
|
2200
|
+
target_version_id: Optional[str] = None,
|
|
2201
|
+
target_tag: Optional[str] = None,
|
|
2202
|
+
ks3_access_key: Optional[str] = None,
|
|
2203
|
+
ks3_secret_key: Optional[str] = None,
|
|
2204
|
+
) -> Dict[str, Any]:
|
|
2205
|
+
"""回滚到指定版本
|
|
2206
|
+
|
|
2207
|
+
Args:
|
|
2208
|
+
agent_id: Agent ID
|
|
2209
|
+
target_version_id: 目标版本 ID(与 target_tag 二选一)
|
|
2210
|
+
target_tag: 目标版本标签(与 target_version_id 二选一)
|
|
2211
|
+
ks3_access_key: KS3 Access Key (可选)
|
|
2212
|
+
ks3_secret_key: KS3 Secret Key (可选)
|
|
2213
|
+
|
|
2214
|
+
Returns:
|
|
2215
|
+
回滚结果
|
|
2216
|
+
"""
|
|
2217
|
+
params = {"AgentId": agent_id}
|
|
2218
|
+
if target_version_id:
|
|
2219
|
+
params["TargetVersionId"] = target_version_id
|
|
2220
|
+
if target_tag:
|
|
2221
|
+
params["TargetTag"] = target_tag
|
|
2222
|
+
if ks3_access_key:
|
|
2223
|
+
params["KS3AccessKey"] = ks3_access_key
|
|
2224
|
+
if ks3_secret_key:
|
|
2225
|
+
params["KS3SecretKey"] = ks3_secret_key
|
|
2226
|
+
|
|
2227
|
+
return self._action("RollbackVersion", params)
|