agentengine-sdk-python 0.6.4__py3-none-any.whl → 0.7.0__py3-none-any.whl

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