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.
Files changed (490) hide show
  1. agentengine_sdk_python-0.6.9.dist-info/METADATA +179 -0
  2. agentengine_sdk_python-0.6.9.dist-info/RECORD +487 -0
  3. {agentengine_sdk_python-0.6.2.dist-info → agentengine_sdk_python-0.6.9.dist-info}/WHEEL +1 -1
  4. agentengine_sdk_python-0.6.9.dist-info/entry_points.txt +3 -0
  5. agentengine_sdk_python-0.6.9.dist-info/licenses/LICENSE +175 -0
  6. agentengine_sdk_python-0.6.9.dist-info/top_level.txt +2 -0
  7. ksadk/__init__.py +13 -0
  8. ksadk/__main__.py +4 -0
  9. ksadk/a2a/__init__.py +15 -0
  10. ksadk/a2a/card_builder.py +64 -0
  11. ksadk/a2a/client.py +309 -0
  12. ksadk/a2a/executor.py +201 -0
  13. ksadk/a2a/server.py +79 -0
  14. ksadk/agents/__init__.py +20 -0
  15. ksadk/agents/base.py +370 -0
  16. ksadk/agents/context.py +38 -0
  17. ksadk/agents/event.py +35 -0
  18. ksadk/agents/loop_agent.py +71 -0
  19. ksadk/agents/parallel_agent.py +109 -0
  20. ksadk/agents/runner_adapter.py +24 -0
  21. ksadk/agents/sequential_agent.py +34 -0
  22. ksadk/api/__init__.py +3 -0
  23. ksadk/api/client.py +2227 -0
  24. ksadk/builders/__init__.py +20 -0
  25. ksadk/builders/base.py +66 -0
  26. ksadk/builders/code_builder.py +1990 -0
  27. ksadk/builders/container_builder.py +786 -0
  28. ksadk/builders/framework_requirements.py +59 -0
  29. ksadk/builders/ks3_uploader.py +389 -0
  30. ksadk/builders/mcp_builder.py +476 -0
  31. ksadk/builders/requirements_utils.py +75 -0
  32. ksadk/cli/__init__.py +531 -0
  33. ksadk/cli/__main__.py +4 -0
  34. ksadk/cli/agent_ref.py +204 -0
  35. ksadk/cli/cmd_a2a.py +167 -0
  36. ksadk/cli/cmd_agent.py +201 -0
  37. ksadk/cli/cmd_build.py +270 -0
  38. ksadk/cli/cmd_completion.py +304 -0
  39. ksadk/cli/cmd_config.py +758 -0
  40. ksadk/cli/cmd_create.py +2277 -0
  41. ksadk/cli/cmd_dashboard.py +900 -0
  42. ksadk/cli/cmd_deploy.py +704 -0
  43. ksadk/cli/cmd_destroy.py +345 -0
  44. ksadk/cli/cmd_files.py +1227 -0
  45. ksadk/cli/cmd_hermes.py +1259 -0
  46. ksadk/cli/cmd_invoke.py +1336 -0
  47. ksadk/cli/cmd_launch.py +564 -0
  48. ksadk/cli/cmd_mcp.py +1174 -0
  49. ksadk/cli/cmd_model.py +302 -0
  50. ksadk/cli/cmd_openclaw.py +3918 -0
  51. ksadk/cli/cmd_run.py +232 -0
  52. ksadk/cli/cmd_status.py +588 -0
  53. ksadk/cli/cmd_version.py +522 -0
  54. ksadk/cli/cmd_web.py +288 -0
  55. ksadk/cli/deploy_utils.py +87 -0
  56. ksadk/cli/dry_run.py +214 -0
  57. ksadk/cli/env_options.py +82 -0
  58. ksadk/cli/error_utils.py +525 -0
  59. ksadk/cli/global_options.py +31 -0
  60. ksadk/cli/local_runtime.py +97 -0
  61. ksadk/cli/model_catalog.py +136 -0
  62. ksadk/cli/network_options.py +410 -0
  63. ksadk/cli/resource_common.py +621 -0
  64. ksadk/cli/storage.py +84 -0
  65. ksadk/cli/ui.py +431 -0
  66. ksadk/cli/workflow_common.py +552 -0
  67. ksadk/common/__init__.py +14 -0
  68. ksadk/common/aicp_env.py +128 -0
  69. ksadk/common/auth.py +190 -0
  70. ksadk/common/constants.py +84 -0
  71. ksadk/common/llm_utils.py +61 -0
  72. ksadk/configs/__init__.py +61 -0
  73. ksadk/configs/env_registry.py +342 -0
  74. ksadk/configs/global_config.py +200 -0
  75. ksadk/configs/settings.py +706 -0
  76. ksadk/conversations/__init__.py +95 -0
  77. ksadk/conversations/attachment_storage.py +379 -0
  78. ksadk/conversations/attachments.py +774 -0
  79. ksadk/conversations/compaction_pipeline.py +387 -0
  80. ksadk/conversations/compaction_prompt.py +128 -0
  81. ksadk/conversations/context.py +303 -0
  82. ksadk/conversations/model_context.py +360 -0
  83. ksadk/conversations/model_options.py +136 -0
  84. ksadk/conversations/normalize.py +446 -0
  85. ksadk/conversations/reasoning_markup.py +91 -0
  86. ksadk/conversations/run_kinds.py +61 -0
  87. ksadk/conversations/run_status.py +53 -0
  88. ksadk/conversations/runtime.py +5208 -0
  89. ksadk/conversations/semantic_summary.py +359 -0
  90. ksadk/conversations/session_title.py +254 -0
  91. ksadk/deployment/__init__.py +83 -0
  92. ksadk/deployment/agent_access.py +226 -0
  93. ksadk/deployment/base.py +229 -0
  94. ksadk/deployment/manager.py +332 -0
  95. ksadk/deployment/providers/__init__.py +10 -0
  96. ksadk/deployment/providers/serverless.py +1105 -0
  97. ksadk/deployment/registry.py +120 -0
  98. ksadk/deployment/state.py +78 -0
  99. ksadk/deployment/ui_config.py +21 -0
  100. ksadk/detection/__init__.py +7 -0
  101. ksadk/detection/detector.py +431 -0
  102. ksadk/detection/mcp_detector.py +250 -0
  103. ksadk/hermes_terminal.py +467 -0
  104. ksadk/identity/__init__.py +17 -0
  105. ksadk/identity/resolver.py +385 -0
  106. ksadk/knowledge_base/__init__.py +62 -0
  107. ksadk/knowledge_base/adk_tool.py +47 -0
  108. ksadk/knowledge_base/client.py +297 -0
  109. ksadk/knowledge_base/langchain_tool.py +55 -0
  110. ksadk/knowledge_base/service.py +66 -0
  111. ksadk/knowledge_base/tool.py +50 -0
  112. ksadk/markdown.py +169 -0
  113. ksadk/mcp_runtime/__init__.py +231 -0
  114. ksadk/memory/__init__.py +57 -0
  115. ksadk/memory/adk/__init__.py +51 -0
  116. ksadk/memory/adk/backends/__init__.py +5 -0
  117. ksadk/memory/adk/backends/base_ltm_backend.py +52 -0
  118. ksadk/memory/adk/backends/http_ltm_backend.py +183 -0
  119. ksadk/memory/adk/backends/inmemory_ltm_backend.py +96 -0
  120. ksadk/memory/adk/backends/sdk_ltm_backend.py +456 -0
  121. ksadk/memory/adk/long_term_memory.py +273 -0
  122. ksadk/memory/adk/short_term_memory.py +289 -0
  123. ksadk/memory/adk_tool.py +31 -0
  124. ksadk/memory/backends/__init__.py +6 -0
  125. ksadk/memory/backends/base.py +103 -0
  126. ksadk/memory/backends/memory.py +112 -0
  127. ksadk/memory/backends/redis.py +146 -0
  128. ksadk/memory/langchain_tool.py +39 -0
  129. ksadk/memory/ltm_backend_factory.py +24 -0
  130. ksadk/memory/manager.py +215 -0
  131. ksadk/memory/service.py +172 -0
  132. ksadk/memory/tool.py +96 -0
  133. ksadk/model_policy.py +223 -0
  134. ksadk/openclaw_gateway.py +378 -0
  135. ksadk/runners/__init__.py +8 -0
  136. ksadk/runners/adk_runner.py +1135 -0
  137. ksadk/runners/base_runner.py +305 -0
  138. ksadk/runners/deepagents_runner.py +13 -0
  139. ksadk/runners/factory.py +73 -0
  140. ksadk/runners/langchain_runner.py +539 -0
  141. ksadk/runners/langgraph_runner.py +852 -0
  142. ksadk/runners/patch_langchain.py +187 -0
  143. ksadk/runners/remote_runner.py +667 -0
  144. ksadk/runners/usage_accumulator.py +38 -0
  145. ksadk/runners/utils/__init__.py +19 -0
  146. ksadk/runners/utils/langfuse.py +230 -0
  147. ksadk/runners/utils/loader.py +62 -0
  148. ksadk/runtime_context.py +166 -0
  149. ksadk/runtime_state.py +25 -0
  150. ksadk/sandbox/__init__.py +28 -0
  151. ksadk/sandbox/backends/__init__.py +9 -0
  152. ksadk/sandbox/backends/e2b.py +154 -0
  153. ksadk/sandbox/backends/local_process.py +131 -0
  154. ksadk/sandbox/base.py +101 -0
  155. ksadk/sandbox/factory.py +53 -0
  156. ksadk/sandbox/registry.py +187 -0
  157. ksadk/server/__init__.py +7 -0
  158. ksadk/server/api_models.py +73 -0
  159. ksadk/server/app.py +3789 -0
  160. ksadk/server/static/assets/ArtifactsPanel-C0meuOdi.js +1 -0
  161. ksadk/server/static/assets/CodeBlock-BbI4HPtJ.js +9 -0
  162. ksadk/server/static/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
  163. ksadk/server/static/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
  164. ksadk/server/static/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
  165. ksadk/server/static/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
  166. ksadk/server/static/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
  167. ksadk/server/static/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
  168. ksadk/server/static/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
  169. ksadk/server/static/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
  170. ksadk/server/static/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
  171. ksadk/server/static/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
  172. ksadk/server/static/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
  173. ksadk/server/static/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
  174. ksadk/server/static/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
  175. ksadk/server/static/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
  176. ksadk/server/static/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
  177. ksadk/server/static/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
  178. ksadk/server/static/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
  179. ksadk/server/static/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
  180. ksadk/server/static/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
  181. ksadk/server/static/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
  182. ksadk/server/static/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
  183. ksadk/server/static/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
  184. ksadk/server/static/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
  185. ksadk/server/static/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
  186. ksadk/server/static/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
  187. ksadk/server/static/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
  188. ksadk/server/static/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
  189. ksadk/server/static/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
  190. ksadk/server/static/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
  191. ksadk/server/static/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
  192. ksadk/server/static/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
  193. ksadk/server/static/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
  194. ksadk/server/static/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
  195. ksadk/server/static/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
  196. ksadk/server/static/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
  197. ksadk/server/static/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
  198. ksadk/server/static/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
  199. ksadk/server/static/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
  200. ksadk/server/static/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
  201. ksadk/server/static/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
  202. ksadk/server/static/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
  203. ksadk/server/static/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
  204. ksadk/server/static/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
  205. ksadk/server/static/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
  206. ksadk/server/static/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
  207. ksadk/server/static/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
  208. ksadk/server/static/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
  209. ksadk/server/static/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
  210. ksadk/server/static/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
  211. ksadk/server/static/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
  212. ksadk/server/static/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
  213. ksadk/server/static/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
  214. ksadk/server/static/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
  215. ksadk/server/static/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
  216. ksadk/server/static/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
  217. ksadk/server/static/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
  218. ksadk/server/static/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
  219. ksadk/server/static/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
  220. ksadk/server/static/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
  221. ksadk/server/static/assets/MathMessageMarkdown-DMLQYoLp.js +8 -0
  222. ksadk/server/static/assets/MathMessageMarkdown-DeYh8QPr.css +1 -0
  223. ksadk/server/static/assets/MermaidBlock-DCR8WZ17.js +303 -0
  224. ksadk/server/static/assets/NativeTerminalPanel-CHOd7Rch.css +1 -0
  225. ksadk/server/static/assets/NativeTerminalPanel-CKUvWhUz.js +1 -0
  226. ksadk/server/static/assets/addon-fit-DthTIhi3.js +1 -0
  227. ksadk/server/static/assets/apl-CnwPGSsG.js +1 -0
  228. ksadk/server/static/assets/arc-bj2rZnP5.js +1 -0
  229. ksadk/server/static/assets/architecture-7EHR7CIX-Cr6Xz1_c.js +1 -0
  230. ksadk/server/static/assets/architectureDiagram-3BPJPVTR-2u1ltDIj.js +36 -0
  231. ksadk/server/static/assets/asciiarmor-qTkVPQu6.js +1 -0
  232. ksadk/server/static/assets/asn1-Dr8qZg38.js +1 -0
  233. ksadk/server/static/assets/asterisk-Bja0s6e1.js +1 -0
  234. ksadk/server/static/assets/blockDiagram-GPEHLZMM-D6fvdK20.js +132 -0
  235. ksadk/server/static/assets/brainfuck-D5EjA2JK.js +1 -0
  236. ksadk/server/static/assets/c4Diagram-AAUBKEIU-BK82L5n5.js +10 -0
  237. ksadk/server/static/assets/channel-BSSOwFIb.js +1 -0
  238. ksadk/server/static/assets/chunk-2J33WTMH-DLVdkeyQ.js +1 -0
  239. ksadk/server/static/assets/chunk-4BX2VUAB-BbwsYcbW.js +1 -0
  240. ksadk/server/static/assets/chunk-55IACEB6-DOairxzw.js +1 -0
  241. ksadk/server/static/assets/chunk-727SXJPM-hCO487gs.js +206 -0
  242. ksadk/server/static/assets/chunk-AQP2D5EJ-XU8vDOuG.js +231 -0
  243. ksadk/server/static/assets/chunk-FMBD7UC4-PrapyfMu.js +15 -0
  244. ksadk/server/static/assets/chunk-ND2GUHAM-CboKhTZ3.js +1 -0
  245. ksadk/server/static/assets/chunk-QZHKN3VN-XXpdiwUL.js +1 -0
  246. ksadk/server/static/assets/classDiagram-4FO5ZUOK-C5HvGiyK.js +1 -0
  247. ksadk/server/static/assets/classDiagram-v2-Q7XG4LA2-C5HvGiyK.js +1 -0
  248. ksadk/server/static/assets/clike-DHH8Ad3s.js +1 -0
  249. ksadk/server/static/assets/clojure-BflJGmDX.js +1 -0
  250. ksadk/server/static/assets/cmake-CyVbuzPu.js +1 -0
  251. ksadk/server/static/assets/cobol-B_OZ4V-R.js +1 -0
  252. ksadk/server/static/assets/coffeescript-B2pKbFk8.js +1 -0
  253. ksadk/server/static/assets/commonlisp-CcllspGY.js +1 -0
  254. ksadk/server/static/assets/cose-bilkent-S5V4N54A-_N8bJ-EU.js +1 -0
  255. ksadk/server/static/assets/crystal-M5-qzICs.js +1 -0
  256. ksadk/server/static/assets/css-CYpP4FRV.js +1 -0
  257. ksadk/server/static/assets/cypher-p9eYesGn.js +1 -0
  258. ksadk/server/static/assets/cytoscape.esm-DelgaX4f.js +321 -0
  259. ksadk/server/static/assets/d-BhMjBjQP.js +1 -0
  260. ksadk/server/static/assets/dagre-BM42HDAG-FoDlo5J2.js +4 -0
  261. ksadk/server/static/assets/dagre-Bx709z4p.js +1 -0
  262. ksadk/server/static/assets/defaultLocale-C8Fc0cco.js +1 -0
  263. ksadk/server/static/assets/diagram-2AECGRRQ-BBp6ebBw.js +43 -0
  264. ksadk/server/static/assets/diagram-5GNKFQAL-CqOvG1PR.js +10 -0
  265. ksadk/server/static/assets/diagram-KO2AKTUF-O9T3t6ba.js +3 -0
  266. ksadk/server/static/assets/diagram-LMA3HP47-CGG8ekBO.js +24 -0
  267. ksadk/server/static/assets/diagram-OG6HWLK6-4BmH1EEg.js +24 -0
  268. ksadk/server/static/assets/diff-ChtP43wD.js +1 -0
  269. ksadk/server/static/assets/dist-6kSZvCZh.js +1 -0
  270. ksadk/server/static/assets/dist-B67uF2uK.js +1 -0
  271. ksadk/server/static/assets/dist-BMKM66B4.js +1 -0
  272. ksadk/server/static/assets/dist-BVzcWa6D.js +1 -0
  273. ksadk/server/static/assets/dist-Bk1l05Ni.js +1 -0
  274. ksadk/server/static/assets/dist-BkJjNih3.js +1 -0
  275. ksadk/server/static/assets/dist-BmuTWcm7.js +9 -0
  276. ksadk/server/static/assets/dist-C2DKRFVK.js +1 -0
  277. ksadk/server/static/assets/dist-CWiTDN_V.js +6 -0
  278. ksadk/server/static/assets/dist-CZ6C3x8B.js +23 -0
  279. ksadk/server/static/assets/dist-CiZHB6ZW.js +1 -0
  280. ksadk/server/static/assets/dist-Cp1_jKwf.js +1 -0
  281. ksadk/server/static/assets/dist-Cwhk4lwI.js +2 -0
  282. ksadk/server/static/assets/dist-D5JGcxBx.js +11 -0
  283. ksadk/server/static/assets/dist-D99wf6_X.js +1 -0
  284. ksadk/server/static/assets/dist-DCpZrjaK.js +1 -0
  285. ksadk/server/static/assets/dist-DI-oVAXw.js +1 -0
  286. ksadk/server/static/assets/dist-DLMjL01k.js +1 -0
  287. ksadk/server/static/assets/dist-DLeM1OCK.js +1 -0
  288. ksadk/server/static/assets/dist-DM0rEQPk.js +1 -0
  289. ksadk/server/static/assets/dist-DbaH3p5h.js +1 -0
  290. ksadk/server/static/assets/dist-S4klkK5W.js +1 -0
  291. ksadk/server/static/assets/dockerfile-BLkNEvjs.js +1 -0
  292. ksadk/server/static/assets/dtd-DTF72YNO.js +1 -0
  293. ksadk/server/static/assets/dylan-C6jEEEk-.js +1 -0
  294. ksadk/server/static/assets/ebnf-D4c0_ac3.js +1 -0
  295. ksadk/server/static/assets/ecl-BSZaIHnp.js +1 -0
  296. ksadk/server/static/assets/eiffel-BEjRio4Q.js +1 -0
  297. ksadk/server/static/assets/elm-Cshzl8qu.js +1 -0
  298. ksadk/server/static/assets/erDiagram-TEJ5UH35-oJhaci7S.js +85 -0
  299. ksadk/server/static/assets/erlang-DaB2Rkuy.js +1 -0
  300. ksadk/server/static/assets/eventmodeling-FCH6USID-CIgwHJLL.js +1 -0
  301. ksadk/server/static/assets/factor-D7LVTn2l.js +1 -0
  302. ksadk/server/static/assets/fcl-ClOhbFR7.js +1 -0
  303. ksadk/server/static/assets/flowDiagram-I6XJVG4X-DtTmUan-.js +162 -0
  304. ksadk/server/static/assets/forth-Cezjo90N.js +1 -0
  305. ksadk/server/static/assets/fortran-Bt6PBEDR.js +1 -0
  306. ksadk/server/static/assets/ganttDiagram-6RSMTGT7-BSsb_eij.js +292 -0
  307. ksadk/server/static/assets/gas-BHEdbvp9.js +1 -0
  308. ksadk/server/static/assets/gherkin-oBAE_ms0.js +1 -0
  309. ksadk/server/static/assets/gitGraph-WXDBUCRP-BVAJ7uo0.js +1 -0
  310. ksadk/server/static/assets/gitGraphDiagram-PVQCEYII-BR5s6Pws.js +106 -0
  311. ksadk/server/static/assets/graphlib-B8gBHxth.js +1 -0
  312. ksadk/server/static/assets/groovy-BC3IOsC8.js +1 -0
  313. ksadk/server/static/assets/haskell-BRsoo5mP.js +1 -0
  314. ksadk/server/static/assets/haxe-DIz0ZZqd.js +1 -0
  315. ksadk/server/static/assets/http-CLVgA2GD.js +1 -0
  316. ksadk/server/static/assets/idl-BUZw3wgd.js +1 -0
  317. ksadk/server/static/assets/index-BH7-pC0B.js +165 -0
  318. ksadk/server/static/assets/index-DAURbCsP.css +1 -0
  319. ksadk/server/static/assets/info-J43DQDTF-MT80V4ge.js +1 -0
  320. ksadk/server/static/assets/infoDiagram-5YYISTIA-DYPc5cvh.js +2 -0
  321. ksadk/server/static/assets/init-D6jRqBbL.js +1 -0
  322. ksadk/server/static/assets/ishikawaDiagram-YF4QCWOH-MoBdALDg.js +70 -0
  323. ksadk/server/static/assets/javascript-9Tg8ixDm.js +1 -0
  324. ksadk/server/static/assets/journeyDiagram-JHISSGLW-DgLiQT8l.js +139 -0
  325. ksadk/server/static/assets/julia-CQfgDRfP.js +1 -0
  326. ksadk/server/static/assets/kanban-definition-UN3LZRKU-CsFx112F.js +89 -0
  327. ksadk/server/static/assets/katex-DRsiwpM2.js +265 -0
  328. ksadk/server/static/assets/linear-TNRQ5Orv.js +1 -0
  329. ksadk/server/static/assets/livescript-BXxG0Yva.js +1 -0
  330. ksadk/server/static/assets/lua-CiA1ziua.js +1 -0
  331. ksadk/server/static/assets/mathematica-u8YMmbU0.js +1 -0
  332. ksadk/server/static/assets/mbox-BEMVcqjs.js +1 -0
  333. ksadk/server/static/assets/mermaid-parser.core-D2eMVOgo.js +161 -0
  334. ksadk/server/static/assets/mindmap-definition-RKZ34NQL-BtByYFhL.js +96 -0
  335. ksadk/server/static/assets/mirc-C9zpzAZU.js +1 -0
  336. ksadk/server/static/assets/mllike-B45xgp2S.js +1 -0
  337. ksadk/server/static/assets/modelica-CqHI_q-D.js +1 -0
  338. ksadk/server/static/assets/mscgen-qgSLujhx.js +1 -0
  339. ksadk/server/static/assets/mumps-CW_TBmxz.js +1 -0
  340. ksadk/server/static/assets/nginx-BMaDbqW3.js +1 -0
  341. ksadk/server/static/assets/nsis-AQ_alPln.js +1 -0
  342. ksadk/server/static/assets/ntriples-d28e9m0E.js +1 -0
  343. ksadk/server/static/assets/octave-D2Q8cUp1.js +1 -0
  344. ksadk/server/static/assets/ordinal-hYBb2elL.js +1 -0
  345. ksadk/server/static/assets/oz-BwTct_5T.js +1 -0
  346. ksadk/server/static/assets/packet-YPE3B663-BLKhbXZB.js +1 -0
  347. ksadk/server/static/assets/pascal-Dp_KdFgX.js +1 -0
  348. ksadk/server/static/assets/perl-CdEHsPfU.js +1 -0
  349. ksadk/server/static/assets/pie-LRSECV5Y-DFOikjSY.js +1 -0
  350. ksadk/server/static/assets/pieDiagram-4H26LBE5-DPnuCB8c.js +30 -0
  351. ksadk/server/static/assets/pig-AF4Hwhju.js +1 -0
  352. ksadk/server/static/assets/powershell-CzG71I4L.js +1 -0
  353. ksadk/server/static/assets/properties-C357ku5U.js +1 -0
  354. ksadk/server/static/assets/protobuf-D1ij_kNL.js +1 -0
  355. ksadk/server/static/assets/pug-Bugr-47h.js +1 -0
  356. ksadk/server/static/assets/puppet-B4hC2X4m.js +1 -0
  357. ksadk/server/static/assets/python-BqYw9LYJ.js +1 -0
  358. ksadk/server/static/assets/q-B9NhS7L_.js +1 -0
  359. ksadk/server/static/assets/quadrantDiagram-W4KKPZXB-miFpCPv4.js +7 -0
  360. ksadk/server/static/assets/r-DADKenSm.js +1 -0
  361. ksadk/server/static/assets/radar-GUYGQ44K-095b1Ikw.js +1 -0
  362. ksadk/server/static/assets/requirementDiagram-4Y6WPE33-mjB_yYkW.js +84 -0
  363. ksadk/server/static/assets/rpm-gpDK5sbt.js +1 -0
  364. ksadk/server/static/assets/ruby-DsYpTuWg.js +1 -0
  365. ksadk/server/static/assets/sankeyDiagram-5OEKKPKP-D7hSlZZZ.js +40 -0
  366. ksadk/server/static/assets/sas-C2OyD2Y6.js +1 -0
  367. ksadk/server/static/assets/scheme-CyDdOh7e.js +1 -0
  368. ksadk/server/static/assets/sequenceDiagram-3UESZ5HK-D9Cx6hC3.js +162 -0
  369. ksadk/server/static/assets/shell-Du5Qg9im.js +1 -0
  370. ksadk/server/static/assets/sieve-BAxa3IjF.js +1 -0
  371. ksadk/server/static/assets/simple-mode-DRpGK0lJ.js +1 -0
  372. ksadk/server/static/assets/smalltalk-BLp5-jwC.js +1 -0
  373. ksadk/server/static/assets/solr-BBopId9w.js +1 -0
  374. ksadk/server/static/assets/sparql-Dz-mlCAC.js +1 -0
  375. ksadk/server/static/assets/spreadsheet-CTIncYxj.js +1 -0
  376. ksadk/server/static/assets/sql-DcCJ9jop.js +1 -0
  377. ksadk/server/static/assets/stateDiagram-AJRCARHV-D_zrg47o.js +1 -0
  378. ksadk/server/static/assets/stateDiagram-v2-BHNVJYJU-J9kCb0nC.js +1 -0
  379. ksadk/server/static/assets/stex-sDrdk2BW.js +1 -0
  380. ksadk/server/static/assets/stylus-DLLZ8dgm.js +1 -0
  381. ksadk/server/static/assets/swift-DKB6_1j6.js +1 -0
  382. ksadk/server/static/assets/tcl-BGfruO1u.js +1 -0
  383. ksadk/server/static/assets/textile-DWBqthVk.js +1 -0
  384. ksadk/server/static/assets/tiddlywiki-VlUiK86J.js +1 -0
  385. ksadk/server/static/assets/tiki-CxVlf5KZ.js +1 -0
  386. ksadk/server/static/assets/timeline-definition-PNZ67QCA-CwSec0Wa.js +120 -0
  387. ksadk/server/static/assets/toml-LvoKBwCs.js +1 -0
  388. ksadk/server/static/assets/treeView-BLDUP644-yinfJEY6.js +1 -0
  389. ksadk/server/static/assets/treemap-LRROVOQU-DosUqN1Q.js +1 -0
  390. ksadk/server/static/assets/troff-C2dAgh7P.js +1 -0
  391. ksadk/server/static/assets/ttcn-BePYh-hG.js +1 -0
  392. ksadk/server/static/assets/ttcn-cfg-CY0aot7Q.js +1 -0
  393. ksadk/server/static/assets/turtle-BDLWE2cD.js +1 -0
  394. ksadk/server/static/assets/vb-BthdM_4N.js +1 -0
  395. ksadk/server/static/assets/vbscript-D9f6rSsU.js +1 -0
  396. ksadk/server/static/assets/velocity-CqftRsjg.js +1 -0
  397. ksadk/server/static/assets/vennDiagram-CIIHVFJN-agCVobiF.js +34 -0
  398. ksadk/server/static/assets/verilog-D-x6ne0Z.js +1 -0
  399. ksadk/server/static/assets/vhdl-CMrOq2q1.js +1 -0
  400. ksadk/server/static/assets/wardley-L42UT6IY-Bvp7-5uH.js +1 -0
  401. ksadk/server/static/assets/wardleyDiagram-YWT4CUSO-TmrlhWNt.js +78 -0
  402. ksadk/server/static/assets/webidl-D5jiJvOU.js +1 -0
  403. ksadk/server/static/assets/xquery-fj_R-kMw.js +1 -0
  404. ksadk/server/static/assets/xterm-DooSxjI5.js +36 -0
  405. ksadk/server/static/assets/xychartDiagram-2RQKCTM6-DV71V-ah.js +7 -0
  406. ksadk/server/static/assets/yacas-CXXgtw0p.js +1 -0
  407. ksadk/server/static/assets/z80-CL1naaMV.js +1 -0
  408. ksadk/server/static/favicon.svg +1 -0
  409. ksadk/server/static/icons.svg +24 -0
  410. ksadk/server/static/index.html +14 -0
  411. ksadk/server/terminal_sessions.py +551 -0
  412. ksadk/sessions/__init__.py +283 -0
  413. ksadk/sessions/base.py +354 -0
  414. ksadk/sessions/continuity.py +313 -0
  415. ksadk/sessions/errors.py +7 -0
  416. ksadk/sessions/in_memory.py +273 -0
  417. ksadk/sessions/local_service.py +949 -0
  418. ksadk/sessions/postgres_service.py +838 -0
  419. ksadk/sessions/sqlite_service.py +13 -0
  420. ksadk/skills/__init__.py +9 -0
  421. ksadk/skills/loader.py +46 -0
  422. ksadk/skills/models.py +120 -0
  423. ksadk/skills/package_store.py +104 -0
  424. ksadk/skills/runtime/__init__.py +17 -0
  425. ksadk/skills/runtime/agent.py +183 -0
  426. ksadk/skills/runtime/artifacts.py +39 -0
  427. ksadk/skills/runtime/backends/__init__.py +1 -0
  428. ksadk/skills/runtime/backends/disabled.py +8 -0
  429. ksadk/skills/runtime/backends/e2b.py +168 -0
  430. ksadk/skills/runtime/backends/local.py +95 -0
  431. ksadk/skills/runtime/base.py +103 -0
  432. ksadk/skills/runtime/executor.py +219 -0
  433. ksadk/skills/runtime/factory.py +37 -0
  434. ksadk/skills/runtime/loader.py +131 -0
  435. ksadk/skills/runtime/registry.py +150 -0
  436. ksadk/skills/runtime/request.py +67 -0
  437. ksadk/skills/service_client.py +239 -0
  438. ksadk/skills/service_env.py +58 -0
  439. ksadk/skills/tool_defs.py +221 -0
  440. ksadk/terminal_client.py +69 -0
  441. ksadk/terminal_exec_policy.py +229 -0
  442. ksadk/tools/__init__.py +17 -0
  443. ksadk/tools/gateway.py +230 -0
  444. ksadk/tools/result_budget.py +154 -0
  445. ksadk/toolsets/__init__.py +852 -0
  446. ksadk/toolsets/_langchain.py +11 -0
  447. ksadk/toolsets/platform.py +93 -0
  448. ksadk/toolsets/sandbox.py +310 -0
  449. ksadk/toolsets/skills.py +217 -0
  450. ksadk/toolsets/web.py +266 -0
  451. ksadk/toolsets/workspace.py +878 -0
  452. ksadk/toolsets/workspace_state.py +44 -0
  453. ksadk/tracing/__init__.py +37 -0
  454. ksadk/tracing/exporters/__init__.py +1 -0
  455. ksadk/tracing/exporters/inmemory_exporter.py +116 -0
  456. ksadk/tracing/exporters/langfuse_exporter.py +445 -0
  457. ksadk/tracing/setup.py +829 -0
  458. ksadk/tracing/span_utils.py +114 -0
  459. ksadk/tui/__init__.py +12 -0
  460. ksadk/tui/app.py +400 -0
  461. ksadk/tui/clipboard.py +100 -0
  462. ksadk/tui/widgets/__init__.py +22 -0
  463. ksadk/tui/widgets/assistant.py +89 -0
  464. ksadk/tui/widgets/base.py +54 -0
  465. ksadk/tui/widgets/chat_input.py +246 -0
  466. ksadk/tui/widgets/history.py +156 -0
  467. ksadk/tui/widgets/system.py +87 -0
  468. ksadk/tui/widgets/thinking.py +110 -0
  469. ksadk/tui/widgets/tool_call.py +161 -0
  470. ksadk/tui/widgets/user.py +37 -0
  471. ksadk/ui_config.py +168 -0
  472. ksadk/version.py +4 -0
  473. ksadk_runtime_common/__init__.py +20 -0
  474. ksadk_runtime_common/memory_backend/__init__.py +29 -0
  475. ksadk_runtime_common/memory_backend/manifest.py +79 -0
  476. ksadk_runtime_common/memory_backend/providers/__init__.py +6 -0
  477. ksadk_runtime_common/memory_backend/providers/lancedb.py +151 -0
  478. ksadk_runtime_common/memory_backend/providers/mem0.py +74 -0
  479. ksadk_runtime_common/memory_backend/registry.py +56 -0
  480. ksadk_runtime_common/memory_backend/render.py +71 -0
  481. ksadk_runtime_common/schemas/memory_backend_manifest.schema.json +148 -0
  482. ksadk_runtime_common/workspace_files/__init__.py +29 -0
  483. ksadk_runtime_common/workspace_files/bootstrap.py +56 -0
  484. ksadk_runtime_common/workspace_files/constants.py +9 -0
  485. ksadk_runtime_common/workspace_files/path_utils.py +71 -0
  486. ksadk_runtime_common/workspace_files/preview.py +96 -0
  487. ksadk_runtime_common/workspace_files/router.py +254 -0
  488. agentengine_sdk_python-0.6.2.dist-info/METADATA +0 -46
  489. agentengine_sdk_python-0.6.2.dist-info/RECORD +0 -4
  490. 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)