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,1503 @@
1
+ """
2
+ agentengine invoke - 与已部署的 Agent 进行交互
3
+
4
+ 支持 OpenAI 兼容格式调用,支持流式输出
5
+ """
6
+
7
+ import click
8
+ import asyncio
9
+ import io
10
+ import json
11
+ import os
12
+ from datetime import datetime
13
+ from pathlib import Path
14
+ from typing import Any, Callable, Dict, Optional
15
+ import time
16
+ import uuid
17
+ from ksadk.api import AgentEngineAPIError, AgentEngineClient
18
+ from ksadk.cli.agent_ref import merge_agent_inputs, resolve_agent_ref, resolve_openclaw_ref
19
+ from ksadk.cli.cmd_files import (
20
+ _build_sync_payload,
21
+ _collect_local_files_report,
22
+ _emit_sync_payload,
23
+ _format_size,
24
+ _normalize_workspace_dir,
25
+ _push_workspace_files,
26
+ )
27
+ from ksadk.cli.resource_common import CONTEXT_SETTINGS, CompatibilityAliasCommand, print_compatibility_hint
28
+ from ksadk.hermes_terminal import run_hermes_terminal_session
29
+ from ksadk.terminal_client import run_terminal_session
30
+ from ksadk_runtime_common.workspace_files.constants import DEFAULT_WORKSPACE_MAX_UPLOAD_BYTES
31
+
32
+ try:
33
+ from rich.console import Console
34
+ from rich.markdown import Markdown
35
+ from rich.live import Live
36
+ console = Console()
37
+ except ImportError:
38
+ console = None
39
+ Markdown = None
40
+ Live = None
41
+
42
+
43
+ @click.command(
44
+ context_settings=CONTEXT_SETTINGS,
45
+ hidden=True,
46
+ cls=CompatibilityAliasCommand,
47
+ canonical_command="agentengine agent invoke",
48
+ )
49
+ @click.argument("agent_ref", required=False)
50
+ @click.option("--agent", "--agent-id", "agent_option", "-a", help="Agent 名称或 ID")
51
+ @click.option("--endpoint", "-e", help="Agent Endpoint URL (覆盖自动获取)")
52
+ @click.option("--api-key", help="AgentEngine API Key (覆盖本地配置)")
53
+ @click.option(
54
+ "--gateway-token",
55
+ "openclaw_gateway_token",
56
+ envvar="OPENCLAW_GATEWAY_TOKEN",
57
+ help="OpenClaw Gateway token/password(用于 OpenClaw token/password 模式的 /v1/responses)",
58
+ )
59
+ @click.option("--message", "-m", help="发送的消息 (单次调用模式)")
60
+ @click.option("--session", "-s", help="Session ID (可选)")
61
+ @click.option("--region", "-r", default="cn-beijing-6", envvar="KSYUN_REGION", help="区域")
62
+ @click.option("--local", "-l", is_flag=True, help="连接本地服务 (http://localhost:8080)")
63
+ @click.option("--insecure", "-k", is_flag=True, help="跳过 SSL 证书验证 (类似 curl -k)")
64
+ @click.option(
65
+ "--transport",
66
+ type=click.Choice(["auto", "chat", "native"], case_sensitive=False),
67
+ default="auto",
68
+ show_default=True,
69
+ help="交互传输层: auto(自动), chat(HTTP OpenAI API), native(框架原生远端终端)",
70
+ )
71
+ @click.option(
72
+ "--local-workspace",
73
+ type=click.Path(exists=True, file_okay=False, dir_okay=True, path_type=Path),
74
+ help="仅 Hermes 远程 native 模式: 先将本地目录同步到远端 workspace",
75
+ )
76
+ @click.option(
77
+ "--remote-workspace-path",
78
+ help="远端 workspace 子目录 (默认使用本地目录名)",
79
+ )
80
+ @click.option(
81
+ "--verbose-workspace-sync",
82
+ is_flag=True,
83
+ help="显示逐文件 workspace 同步日志(默认使用单行进度刷新)",
84
+ )
85
+ @click.option("--model", help="指定模型名称")
86
+ @click.option("--show-thinking", is_flag=True, help="显示模型思考过程")
87
+ @click.option(
88
+ "--api-format",
89
+ "api_format",
90
+ type=click.Choice(["auto", "responses", "chat_completions"], case_sensitive=False),
91
+ default="auto",
92
+ show_default=True,
93
+ help="HTTP 协议: auto(优先 responses,不通回退 chat), responses, chat_completions",
94
+ )
95
+ @click.option(
96
+ "--no-alt-screen",
97
+ "no_alt_screen",
98
+ is_flag=True,
99
+ help="兼容参数;TUI 已默认使用 inline viewport 并保留终端 scrollback",
100
+ )
101
+ def invoke(
102
+ agent_ref: str,
103
+ agent_option: str,
104
+ endpoint: str,
105
+ api_key: str,
106
+ openclaw_gateway_token: str,
107
+ message: str,
108
+ session: str,
109
+ region: str,
110
+ local: bool,
111
+ insecure: bool,
112
+ transport: str,
113
+ local_workspace: Path | None,
114
+ remote_workspace_path: str | None,
115
+ verbose_workspace_sync: bool,
116
+ model: str,
117
+ show_thinking: bool,
118
+ api_format: str,
119
+ no_alt_screen: bool,
120
+ ):
121
+ """与 Agent 进行交互 (本地或远程)。"""
122
+ run_invoke_command(
123
+ agent_ref=agent_ref,
124
+ agent_option=agent_option,
125
+ endpoint=endpoint,
126
+ api_key=api_key,
127
+ openclaw_gateway_token=openclaw_gateway_token,
128
+ message=message,
129
+ session=session,
130
+ region=region,
131
+ local=local,
132
+ insecure=insecure,
133
+ transport=transport,
134
+ local_workspace=local_workspace,
135
+ remote_workspace_path=remote_workspace_path,
136
+ verbose_workspace_sync=verbose_workspace_sync,
137
+ model=model,
138
+ show_thinking=show_thinking,
139
+ compatibility_alias=True,
140
+ api_format=None if (api_format or "auto").lower() == "auto" else api_format,
141
+ no_alt_screen=no_alt_screen,
142
+ )
143
+
144
+
145
+ def _resolve_remote_workspace_seed_path(
146
+ local_workspace: Path,
147
+ remote_workspace_path: str | None,
148
+ ) -> str:
149
+ if remote_workspace_path:
150
+ return _normalize_workspace_dir(remote_workspace_path)
151
+ default_name = local_workspace.resolve().name or local_workspace.name or "workspace"
152
+ return _normalize_workspace_dir(default_name)
153
+
154
+
155
+ def _summarize_local_workspace_dir(
156
+ local_dir: Path,
157
+ *,
158
+ ignore_git_artifacts: bool,
159
+ ) -> dict[str, Any]:
160
+ if not local_dir.exists():
161
+ raise click.ClickException(f"本地目录不存在: {local_dir}")
162
+ if not local_dir.is_dir():
163
+ raise click.ClickException(f"本地路径不是目录: {local_dir}")
164
+
165
+ report = _collect_local_files_report(
166
+ local_dir,
167
+ ignore_dev_artifacts=True,
168
+ ignore_git_artifacts=ignore_git_artifacts,
169
+ )
170
+ files = [
171
+ {
172
+ "path": file_path,
173
+ "relative_path": relative_path,
174
+ "size_bytes": int(file_path.stat().st_size),
175
+ }
176
+ for relative_path, file_path in report["files"]
177
+ ]
178
+ return {
179
+ "local_dir": local_dir,
180
+ "files": files,
181
+ "total_files": len(files),
182
+ "total_bytes": int(report["total_bytes"]),
183
+ "ignored_artifacts": list(report["ignored_artifacts"]),
184
+ }
185
+
186
+
187
+ def _describe_workspace_sync_phase(event: dict[str, Any] | None) -> str:
188
+ phase = str((event or {}).get("phase") or "").strip()
189
+ remote_path = str((event or {}).get("remote_path") or "").strip()
190
+ if phase == "list_start":
191
+ return f"列出远端目录 {remote_path or '.'}"
192
+ if phase == "upload_start":
193
+ return f"上传 {remote_path or '文件'}"
194
+ if phase == "upload_done":
195
+ return f"确认上传 {remote_path or '文件'}"
196
+ if phase == "limit_done":
197
+ return "读取 workspace 上传限制"
198
+ if phase == "scan_done":
199
+ return "扫描本地目录"
200
+ return "同步 workspace"
201
+
202
+
203
+ def _render_workspace_sync_progress_bar(current: object | None, total: object | None, *, width: int = 20) -> str:
204
+ try:
205
+ current_value = int(current or 0)
206
+ total_value = max(int(total or 0), 1)
207
+ except (TypeError, ValueError):
208
+ current_value = 0
209
+ total_value = 1
210
+ current_value = max(0, min(current_value, total_value))
211
+ filled = int(width * current_value / total_value)
212
+ return f"[{'#' * filled}{'-' * (width - filled)}]"
213
+
214
+
215
+ def _format_workspace_sync_percent(current: object | None, total: object | None) -> str:
216
+ try:
217
+ current_value = float(current or 0)
218
+ total_value = max(float(total or 0), 1.0)
219
+ except (TypeError, ValueError):
220
+ current_value = 0.0
221
+ total_value = 1.0
222
+ percent = max(0.0, min(current_value * 100.0 / total_value, 100.0))
223
+ return f"{percent:.1f}".rstrip("0").rstrip(".") + "%"
224
+
225
+
226
+ def _build_workspace_sync_progress_emitter(*, verbose: bool) -> Callable[[dict[str, Any]], None]:
227
+ active_inline = False
228
+ last_inline_width = 0
229
+
230
+ def _finish_inline() -> None:
231
+ nonlocal active_inline, last_inline_width
232
+ if active_inline:
233
+ click.echo(f"\r{' ' * last_inline_width}\r", nl=False)
234
+ click.echo("")
235
+ active_inline = False
236
+ last_inline_width = 0
237
+
238
+ def _emit(event: dict[str, Any]) -> None:
239
+ nonlocal active_inline, last_inline_width
240
+
241
+ phase = str(event.get("phase") or "").strip()
242
+ if phase == "scan_done":
243
+ _finish_inline()
244
+ click.secho(
245
+ "📂 准备同步 workspace: "
246
+ f"{event.get('total_files', 0)} 个文件,"
247
+ f"{_format_size(event.get('total_bytes', 0))} -> workspace:/{event.get('remote_path', '.')}",
248
+ fg="blue",
249
+ )
250
+ ignored_artifacts = list(event.get("ignored_artifacts") or [])
251
+ if ignored_artifacts:
252
+ click.echo(f" 默认忽略: {', '.join(ignored_artifacts)}")
253
+ return
254
+ if phase == "limit_done":
255
+ _finish_inline()
256
+ click.echo(f" 上传上限: {_format_size(event.get('max_upload_bytes'))}")
257
+ return
258
+ if phase == "list_start":
259
+ _finish_inline()
260
+ click.echo(f" 检查远端目录: workspace:/{event.get('remote_path', '.')}")
261
+ return
262
+ if phase == "list_done":
263
+ _finish_inline()
264
+ click.echo(f" 远端已有条目: {event.get('remote_entry_count', 0)}")
265
+ return
266
+ if phase == "upload_start":
267
+ total = max(int(event.get("total") or 0), 1)
268
+ current = max(0, min(int(event.get("current") or 0), total))
269
+ message = (
270
+ " "
271
+ f"{_render_workspace_sync_progress_bar(current, total)} "
272
+ f"{_format_workspace_sync_percent(current, total)} ({current}/{total}) "
273
+ f"上传 {event.get('remote_path', '')} "
274
+ f"({_format_size(event.get('size_bytes'))})"
275
+ )
276
+ if verbose:
277
+ click.echo(message)
278
+ else:
279
+ padded_message = message.ljust(last_inline_width)
280
+ click.echo(f"\r{padded_message}", nl=False)
281
+ active_inline = True
282
+ last_inline_width = max(last_inline_width, len(message))
283
+ return
284
+ if phase == "upload_skipped":
285
+ total = max(int(event.get("total") or 0), 1)
286
+ current = max(0, min(int(event.get("current") or 0), total))
287
+ message = (
288
+ " "
289
+ f"{_render_workspace_sync_progress_bar(current, total)} "
290
+ f"{_format_workspace_sync_percent(current, total)} ({current}/{total}) "
291
+ f"跳过已存在 {event.get('remote_path', '')}"
292
+ )
293
+ if verbose:
294
+ click.echo(message)
295
+ else:
296
+ padded_message = message.ljust(last_inline_width)
297
+ click.echo(f"\r{padded_message}", nl=False)
298
+ active_inline = True
299
+ last_inline_width = max(last_inline_width, len(message))
300
+ return
301
+ if phase == "upload_done":
302
+ total = max(int(event.get("total") or 0), 1)
303
+ current = max(0, min(int(event.get("current") or 0), total))
304
+ if not verbose and current >= total:
305
+ _finish_inline()
306
+ return
307
+
308
+ return _emit
309
+
310
+
311
+ def _emit_workspace_sync_progress(event: dict[str, Any]) -> None:
312
+ if not hasattr(_emit_workspace_sync_progress, "_emitter"):
313
+ _emit_workspace_sync_progress._emitter = _build_workspace_sync_progress_emitter( # type: ignore[attr-defined]
314
+ verbose=True
315
+ )
316
+ _emit_workspace_sync_progress._emitter(event) # type: ignore[attr-defined]
317
+
318
+
319
+ def _extract_workspace_upload_limit(bootstrap: dict[str, Any] | None) -> int | None:
320
+ if not isinstance(bootstrap, dict):
321
+ return None
322
+ workspace = bootstrap.get("workspace_files") or bootstrap.get("WorkspaceFiles") or {}
323
+ if not isinstance(workspace, dict):
324
+ return None
325
+ raw_limit = workspace.get("max_upload_bytes") or workspace.get("MaxUploadBytes")
326
+ try:
327
+ limit = int(raw_limit)
328
+ except (TypeError, ValueError):
329
+ return None
330
+ return limit if limit > 0 else None
331
+
332
+
333
+ async def _lookup_workspace_upload_limit(
334
+ *,
335
+ agent_ref: str | None,
336
+ region: str,
337
+ ) -> int:
338
+ if not agent_ref:
339
+ return DEFAULT_WORKSPACE_MAX_UPLOAD_BYTES
340
+
341
+ try:
342
+ async with AgentEngineClient(region=region) as client:
343
+ if str(agent_ref).startswith("ar-"):
344
+ payload = await client.get_agent_ui_bootstrap(agent_id=agent_ref)
345
+ else:
346
+ payload = await client.get_agent_ui_bootstrap(name=agent_ref)
347
+ except Exception:
348
+ return DEFAULT_WORKSPACE_MAX_UPLOAD_BYTES
349
+
350
+ return _extract_workspace_upload_limit(payload) or DEFAULT_WORKSPACE_MAX_UPLOAD_BYTES
351
+
352
+
353
+ async def _sync_local_workspace_for_hermes_invoke(
354
+ *,
355
+ agent_ref: str | None,
356
+ local_workspace: str | Path,
357
+ remote_path: str,
358
+ region: str,
359
+ endpoint: str | None,
360
+ api_key: str | None,
361
+ progress_callback: Callable[[dict], None] | None = None,
362
+ ) -> dict[str, Any]:
363
+ local_dir = Path(local_workspace).expanduser().resolve()
364
+ max_upload_bytes = await _lookup_workspace_upload_limit(agent_ref=agent_ref, region=region)
365
+ if progress_callback:
366
+ progress_callback(
367
+ {
368
+ "phase": "limit_done",
369
+ "remote_path": remote_path,
370
+ "max_upload_bytes": max_upload_bytes,
371
+ }
372
+ )
373
+ summary = _summarize_local_workspace_dir(local_dir, ignore_git_artifacts=False)
374
+ if summary["total_bytes"] > max_upload_bytes:
375
+ summary_without_git = _summarize_local_workspace_dir(local_dir, ignore_git_artifacts=True)
376
+ if summary_without_git["total_bytes"] <= max_upload_bytes:
377
+ summary = summary_without_git
378
+ if progress_callback:
379
+ progress_callback(
380
+ {
381
+ "phase": "scan_done",
382
+ "local_dir": str(local_dir),
383
+ "remote_path": remote_path,
384
+ "total_files": summary["total_files"],
385
+ "total_bytes": summary["total_bytes"],
386
+ "ignored_artifacts": summary.get("ignored_artifacts", []),
387
+ }
388
+ )
389
+
390
+ if summary["total_files"] == 0:
391
+ raise click.ClickException(f"本地目录为空,当前版本暂不支持同步纯空目录: {local_dir}")
392
+
393
+ for item in summary["files"]:
394
+ if item["size_bytes"] > max_upload_bytes:
395
+ raise click.ClickException(
396
+ "本地目录中存在超过远端 workspace 上传上限的文件:"
397
+ f"{item['path']}({_format_size(item['size_bytes'])} > {_format_size(max_upload_bytes)})"
398
+ )
399
+
400
+ if summary["total_bytes"] > max_upload_bytes:
401
+ raise click.ClickException(
402
+ "本地目录总大小超过远端 workspace 单次同步上限:"
403
+ f"{_format_size(summary['total_bytes'])} > {_format_size(max_upload_bytes)}"
404
+ )
405
+
406
+ last_progress: dict[str, Any] | None = None
407
+
408
+ def _record_progress(event: dict[str, Any]) -> None:
409
+ nonlocal last_progress
410
+ last_progress = dict(event)
411
+ if progress_callback:
412
+ progress_callback(event)
413
+
414
+ try:
415
+ return await _push_workspace_files(
416
+ agent_ref=agent_ref,
417
+ local_dir=local_dir,
418
+ remote_path=remote_path,
419
+ force=True,
420
+ region=region,
421
+ endpoint=endpoint,
422
+ api_key=api_key,
423
+ progress_callback=_record_progress,
424
+ ignore_dev_artifacts=True,
425
+ ignore_git_artifacts=".git" in list(summary.get("ignored_artifacts") or []),
426
+ )
427
+ except AgentEngineAPIError as exc:
428
+ phase = _describe_workspace_sync_phase(last_progress)
429
+ raise click.ClickException(
430
+ f"同步远端 workspace 失败({phase}):{exc.message}"
431
+ ) from exc
432
+
433
+
434
+ def run_invoke_command(
435
+ *,
436
+ agent_ref: str | None,
437
+ agent_option: str | None,
438
+ endpoint: str | None,
439
+ api_key: str | None,
440
+ message: str | None,
441
+ session: str | None,
442
+ region: str,
443
+ local: bool,
444
+ insecure: bool,
445
+ transport: str,
446
+ local_workspace: str | Path | None = None,
447
+ remote_workspace_path: str | None = None,
448
+ verbose_workspace_sync: bool = False,
449
+ model: str | None,
450
+ show_thinking: bool,
451
+ openclaw_gateway_token: str | None = None,
452
+ compatibility_alias: bool = False,
453
+ api_format: str | None = None,
454
+ no_alt_screen: bool = False,
455
+ ):
456
+ """与 Agent 进行交互 (本地或远程)。"""
457
+ if compatibility_alias:
458
+ print_compatibility_hint(
459
+ legacy="agentengine invoke",
460
+ canonical="agentengine agent invoke",
461
+ )
462
+ try:
463
+ agent_input = merge_agent_inputs(
464
+ agent_option=agent_option,
465
+ positional_agent=agent_ref,
466
+ )
467
+ except ValueError as e:
468
+ click.secho(f"❌ {e}", fg="red")
469
+ raise SystemExit(1)
470
+
471
+ if remote_workspace_path and not local_workspace:
472
+ click.secho("❌ --remote-workspace-path 需要与 --local-workspace 一起使用。", fg="red")
473
+ raise SystemExit(1)
474
+ if local_workspace and message:
475
+ click.secho("❌ --local-workspace 仅支持远程 Hermes native 交互模式。", fg="red")
476
+ raise SystemExit(1)
477
+
478
+ # 加载本地状态
479
+ state = _load_state()
480
+ latest_access: dict[str, Any] = {}
481
+ target_agent: str | None = None
482
+
483
+ normalized_transport = (transport or "auto").strip().lower() or "auto"
484
+
485
+ # 确定 Endpoint
486
+ if local:
487
+ endpoint = "http://localhost:8080"
488
+ elif not endpoint:
489
+ resolved = resolve_agent_ref(
490
+ agent_input,
491
+ cwd=Path("."),
492
+ include_state=True,
493
+ include_project_config=True,
494
+ )
495
+ if not resolved and not agent_input:
496
+ resolved = resolve_openclaw_ref(
497
+ None,
498
+ cwd=Path("."),
499
+ include_state=True,
500
+ )
501
+ if not resolved:
502
+ click.secho("❌ 请指定 Agent(--agent 或位置参数)、--local 或 --endpoint", fg="red")
503
+ click.echo(" 自动解析顺序: .agentengine.state -> agentengine.yaml/ksadk.yaml")
504
+ raise SystemExit(1)
505
+ target_agent = resolved.value
506
+ if resolved.source != "cli":
507
+ click.echo(f"ℹ 未显式指定 Agent,使用 {resolved.source_text}: {target_agent}")
508
+
509
+ latest_access = _refresh_remote_access(
510
+ target_agent=target_agent,
511
+ region=region,
512
+ state=state,
513
+ persist=_state_matches_target(state, target_agent),
514
+ )
515
+
516
+ # 优先使用 state 里的 endpoint (如果是对应的 agent)
517
+ if latest_access.get("endpoint"):
518
+ endpoint = latest_access["endpoint"]
519
+ elif not agent_input or _state_matches_target(state, target_agent):
520
+ endpoint = state.get("endpoint")
521
+
522
+ if not endpoint:
523
+ # 自动获取
524
+ endpoint = _get_endpoint(target_agent, region)
525
+
526
+ # API Key
527
+ api_key = api_key or latest_access.get("api_key") or state.get("api_key")
528
+ # 单次 --message 调用默认开新 session,避免复用 .agentengine.state 里长上下文
529
+ # 默认每次调用开新 session(单次 -m 与交互 TUI 都是),不复用 .agentengine.state
530
+ # 里的上次 session,避免长上下文累积导致首包变慢。TUI 内多轮续聊由
531
+ # InteractionLoop.session_id 维持;跨次想续聊上次会话用显式 --session 指定。
532
+ session_id = session or str(uuid.uuid4())[:8]
533
+
534
+ next_state = _load_state() or dict(state)
535
+ for key in ("agent_id", "name", "endpoint", "api_key", "framework"):
536
+ if latest_access.get(key):
537
+ next_state[key] = latest_access[key]
538
+ if latest_access.get("framework") == "hermes":
539
+ next_state["type"] = "hermes"
540
+ if latest_access.get("framework") == "openclaw":
541
+ next_state["type"] = "openclaw"
542
+ next_state["session_id"] = session_id
543
+ _save_state(next_state)
544
+ runtime_api_key = _select_runtime_api_key(
545
+ api_key=api_key,
546
+ openclaw_gateway_token=openclaw_gateway_token,
547
+ state=next_state,
548
+ latest_access=latest_access,
549
+ )
550
+
551
+ click.secho(f"🤖 连接到 Agent", fg="blue", bold=True)
552
+ click.echo(f" Endpoint: {endpoint}")
553
+ if _is_openclaw_target(next_state, latest_access):
554
+ if runtime_api_key:
555
+ click.echo(f" Runtime Auth: OpenClaw Gateway token {_mask_secret(runtime_api_key)}")
556
+ else:
557
+ click.echo(" Runtime Auth: OpenClaw Gateway anonymous/trusted-proxy")
558
+ if api_key:
559
+ click.echo(f" AgentEngine API: Bearer {_mask_secret(api_key)}")
560
+ elif api_key:
561
+ click.echo(f" Auth: Bearer {_mask_secret(api_key)}")
562
+ else:
563
+ click.secho(" ⚠️ 未发现 API Key,尝试匿名调用", fg="yellow")
564
+
565
+ if insecure:
566
+ click.secho(" ⚠️ SSL 证书验证已禁用", fg="yellow")
567
+
568
+ if message:
569
+ # 单次调用模式
570
+ api_format_resolved = asyncio.run(
571
+ _resolve_remote_api_format(
572
+ endpoint=endpoint,
573
+ api_key=api_key,
574
+ runtime_api_key=runtime_api_key,
575
+ insecure=insecure,
576
+ state=next_state,
577
+ latest_access=latest_access,
578
+ api_format=api_format,
579
+ )
580
+ )
581
+ asyncio.run(
582
+ _invoke_once(
583
+ endpoint,
584
+ message,
585
+ runtime_api_key,
586
+ session_id,
587
+ True,
588
+ insecure,
589
+ model,
590
+ api_format_resolved,
591
+ )
592
+ )
593
+ else:
594
+ is_hermes_target = _is_hermes_target(next_state, latest_access)
595
+ is_openclaw_target = _is_openclaw_target(next_state, latest_access)
596
+ if normalized_transport == "chat" and is_hermes_target:
597
+ click.secho("❌ Hermes 不再支持 ksadk 通用 chat TUI。", fg="red")
598
+ click.echo(" 浏览器聊天页请改用: agentengine hermes open --chat")
599
+ raise SystemExit(1)
600
+ if local_workspace:
601
+ if local or normalized_transport == "chat":
602
+ click.secho("❌ --local-workspace 仅支持远程 Hermes native 交互模式。", fg="red")
603
+ raise SystemExit(1)
604
+ if not is_hermes_target:
605
+ click.secho("❌ --local-workspace 仅支持 Hermes 远程 native 模式。", fg="red")
606
+ raise SystemExit(1)
607
+
608
+ local_workspace_dir = Path(local_workspace).expanduser().resolve()
609
+ normalized_remote_workspace_path = _resolve_remote_workspace_seed_path(
610
+ local_workspace_dir,
611
+ remote_workspace_path,
612
+ )
613
+ sync_agent_ref = (
614
+ latest_access.get("agent_id")
615
+ or next_state.get("agent_id")
616
+ or target_agent
617
+ or latest_access.get("name")
618
+ or next_state.get("name")
619
+ )
620
+ sync_payload = asyncio.run(
621
+ _sync_local_workspace_for_hermes_invoke(
622
+ agent_ref=sync_agent_ref,
623
+ local_workspace=local_workspace_dir,
624
+ remote_path=normalized_remote_workspace_path,
625
+ region=region,
626
+ endpoint=endpoint,
627
+ api_key=api_key,
628
+ progress_callback=_build_workspace_sync_progress_emitter(
629
+ verbose=verbose_workspace_sync
630
+ ),
631
+ )
632
+ )
633
+ _emit_sync_payload(_build_sync_payload(sync_payload), None)
634
+ else:
635
+ normalized_remote_workspace_path = None
636
+ # 默认 TUI 模式
637
+ if _should_use_hermes_native_tui(
638
+ transport=normalized_transport,
639
+ local=local,
640
+ state=next_state,
641
+ latest_access=latest_access,
642
+ ):
643
+ native_kwargs: dict[str, Any] = {
644
+ "endpoint": endpoint,
645
+ "api_key": api_key,
646
+ "session_id": session_id,
647
+ "insecure": insecure,
648
+ }
649
+ if normalized_remote_workspace_path is not None:
650
+ native_kwargs["cwd"] = normalized_remote_workspace_path
651
+ _invoke_hermes_terminal_tui(
652
+ **native_kwargs,
653
+ )
654
+ elif _should_use_openclaw_native_tui(
655
+ transport=normalized_transport,
656
+ local=local,
657
+ state=next_state,
658
+ latest_access=latest_access,
659
+ ):
660
+ _invoke_openclaw_terminal_tui(
661
+ endpoint=endpoint,
662
+ api_key=runtime_api_key,
663
+ session_id=session_id,
664
+ insecure=insecure,
665
+ )
666
+ else:
667
+ api_format_resolved = asyncio.run(
668
+ _resolve_remote_api_format(
669
+ endpoint=endpoint,
670
+ api_key=api_key,
671
+ runtime_api_key=runtime_api_key,
672
+ insecure=insecure,
673
+ state=next_state,
674
+ latest_access=latest_access,
675
+ api_format=api_format,
676
+ )
677
+ )
678
+ tui_agent_id = (
679
+ latest_access.get("agent_id")
680
+ or next_state.get("agent_id")
681
+ or target_agent
682
+ or latest_access.get("name")
683
+ or next_state.get("name")
684
+ )
685
+ _invoke_tui(
686
+ endpoint,
687
+ runtime_api_key,
688
+ session_id,
689
+ insecure,
690
+ model,
691
+ show_thinking,
692
+ api_format=api_format_resolved,
693
+ responses_session_header=(
694
+ "x-openclaw-session-key" if _is_openclaw_target(next_state, latest_access) else None
695
+ ),
696
+ no_alt_screen=no_alt_screen,
697
+ region=region,
698
+ agent_id=tui_agent_id,
699
+ )
700
+
701
+
702
+
703
+
704
+ def _invoke_tui(
705
+ endpoint: str,
706
+ api_key: str = None,
707
+ session_id: str = None,
708
+ insecure: bool = False,
709
+ model: str = None,
710
+ show_thinking: bool = False,
711
+ api_format: str = "chat_completions",
712
+ responses_session_header: str | None = None,
713
+ no_alt_screen: bool = False,
714
+ region: str = "cn-beijing-6",
715
+ agent_id: str | None = None,
716
+ ):
717
+ """使用 TUI 模式调用"""
718
+ from ksadk.runners.remote_runner import RemoteRunner
719
+ from ksadk.tui.loop import run_tui
720
+
721
+ runner = RemoteRunner(
722
+ endpoint=endpoint,
723
+ api_key=api_key,
724
+ session_id=session_id,
725
+ insecure=insecure,
726
+ model=model,
727
+ api_format=api_format,
728
+ responses_session_header=responses_session_header,
729
+ )
730
+ # 优先用 ListAgentModels(控制面 action,server 侧封装 runtime catalog,
731
+ # 避开 CLI 直连 runtime /v1/models 对 openclaw/hermes 的鉴权坑)拿模型列表 +
732
+ # 当前模型 metadata。失败 fallback 到 _fetch_tui_model_metadata(直连 /v1/models)。
733
+ model_list = asyncio.run(_fetch_tui_model_list(region=region, agent_id=agent_id))
734
+ if model_list:
735
+ runner.available_models = model_list.get("models") or []
736
+ current_id = model_list.get("current")
737
+ matched = None
738
+ if current_id:
739
+ matched = next((m for m in runner.available_models if m.get("id") == current_id), None)
740
+ if matched is None and runner.available_models:
741
+ matched = runner.available_models[0]
742
+ if matched:
743
+ runner.model_metadata = matched
744
+ # 用户未指定 --model 时,用 ListAgentModels 的 current 作为 runner.model,
745
+ # 让 TUI 启动即显示真实模型名(而非 unknown)。
746
+ if not runner.model and matched.get("id"):
747
+ runner.model = str(matched["id"])
748
+ else:
749
+ # fallback:直连 runtime /v1/models(旧路径,托管 runtime 常失败但不阻断)
750
+ model_metadata = asyncio.run(
751
+ _fetch_tui_model_metadata(endpoint=endpoint, api_key=api_key, model=model)
752
+ )
753
+ if model_metadata:
754
+ runner.model_metadata = model_metadata
755
+
756
+ run_tui(runner, show_thinking=show_thinking, project_dir=".", no_alt_screen=no_alt_screen)
757
+
758
+
759
+ async def _fetch_tui_model_list(
760
+ *,
761
+ region: str,
762
+ agent_id: str | None,
763
+ ) -> dict[str, Any] | None:
764
+ """通过控制面 ListAgentModels action 拿 Agent 可选模型列表(对标 hosted UI)。
765
+
766
+ 需 KSYUN access/secret key(env),与 _fetch_remote_access 同套鉴权。
767
+ 失败返回 None(不阻断 TUI,由 _fetch_tui_model_metadata fallback)。
768
+ """
769
+ if not agent_id:
770
+ return None
771
+ try:
772
+ from ksadk.api.client import AgentEngineClient
773
+
774
+ async with AgentEngineClient(region=region) as client:
775
+ return await client.list_agent_models(agent_id=agent_id)
776
+ except Exception:
777
+ return None
778
+
779
+
780
+ async def _fetch_tui_model_metadata(
781
+ *,
782
+ endpoint: str,
783
+ api_key: str | None,
784
+ model: str | None,
785
+ ) -> dict[str, Any] | None:
786
+ try:
787
+ from ksadk.cli.model_catalog import (
788
+ fetch_provider_model_catalog,
789
+ find_model_in_catalog,
790
+ )
791
+ from ksadk.conversations.model_context import normalize_model_metadata
792
+
793
+ catalog = await fetch_provider_model_catalog(
794
+ api_base=endpoint,
795
+ api_key=api_key,
796
+ timeout=3.0,
797
+ )
798
+ except Exception:
799
+ return None
800
+
801
+ if not catalog:
802
+ return None
803
+ raw_models = [item.get("_provider_raw_model") or item for item in catalog]
804
+ matched = find_model_in_catalog(raw_models, model)
805
+ if matched is not None:
806
+ return normalize_model_metadata(matched)
807
+ if model:
808
+ return None
809
+ if len(catalog) == 1:
810
+ item = dict(catalog[0])
811
+ item.pop("_provider_raw_model", None)
812
+ return item
813
+ return None
814
+
815
+
816
+ def _load_state() -> dict:
817
+ """从 .agentengine.state 加载状态"""
818
+ import yaml
819
+ state_file = Path(".") / ".agentengine.state"
820
+ if state_file.exists():
821
+ try:
822
+ with open(state_file, 'r', encoding='utf-8') as f:
823
+ return yaml.safe_load(f) or {}
824
+ except Exception:
825
+ pass
826
+ return {}
827
+
828
+
829
+ def _save_state(state: dict) -> None:
830
+ """保存 .agentengine.state。"""
831
+ import yaml
832
+
833
+ state_file = Path(".") / ".agentengine.state"
834
+ payload = dict(state)
835
+ payload["updated_at"] = datetime.now().isoformat()
836
+ with open(state_file, "w", encoding="utf-8") as f:
837
+ yaml.safe_dump(payload, f, allow_unicode=True)
838
+
839
+
840
+ def _state_matches_target(state: dict, target_agent: str | None) -> bool:
841
+ if not state or not target_agent:
842
+ return False
843
+ return target_agent == state.get("agent_id") or target_agent == state.get("name")
844
+
845
+
846
+ def _extract_remote_access(detail: Dict[str, Any]) -> Dict[str, Any]:
847
+ if not isinstance(detail, dict):
848
+ return {}
849
+
850
+ basic = detail.get("basic") if isinstance(detail.get("basic"), dict) else {}
851
+ quick = detail.get("quick_access") if isinstance(detail.get("quick_access"), dict) else {}
852
+
853
+ deployment = detail.get("deployment") if isinstance(detail.get("deployment"), dict) else {}
854
+ framework = (
855
+ deployment.get("framework")
856
+ or basic.get("framework")
857
+ or detail.get("framework")
858
+ )
859
+
860
+ return {
861
+ "agent_id": basic.get("agent_id") or detail.get("agent_id"),
862
+ "name": basic.get("name") or detail.get("name"),
863
+ "endpoint": quick.get("public_endpoint")
864
+ or quick.get("private_endpoint")
865
+ or detail.get("endpoint"),
866
+ "api_key": quick.get("api_key") or detail.get("api_key"),
867
+ "framework": str(framework or "").strip().lower() or None,
868
+ }
869
+
870
+
871
+ async def _fetch_remote_access(target_agent: str, region: str) -> Dict[str, Any]:
872
+ from ksadk.api import AgentEngineClient
873
+
874
+ async with AgentEngineClient(region=region) as client:
875
+ try:
876
+ return _extract_remote_access(await client.get_agent(agent_id=target_agent, include_api_key=True))
877
+ except Exception:
878
+ return _extract_remote_access(await client.get_agent(name=target_agent, include_api_key=True))
879
+
880
+
881
+ def _refresh_remote_access(
882
+ *,
883
+ target_agent: str,
884
+ region: str,
885
+ state: dict,
886
+ persist: bool,
887
+ ) -> Dict[str, Any]:
888
+ try:
889
+ latest = asyncio.run(_fetch_remote_access(target_agent, region))
890
+ except Exception:
891
+ return {}
892
+
893
+ if persist and latest:
894
+ merged = dict(state)
895
+ for key in ("agent_id", "name", "endpoint", "api_key", "framework"):
896
+ if latest.get(key):
897
+ merged[key] = latest[key]
898
+ if latest.get("framework") == "hermes":
899
+ merged["type"] = "hermes"
900
+ if latest.get("framework") == "openclaw":
901
+ merged["type"] = "openclaw"
902
+ _save_state(merged)
903
+ return latest
904
+
905
+
906
+ def _is_hermes_target(state: dict, latest_access: dict) -> bool:
907
+ framework = str(
908
+ latest_access.get("framework")
909
+ or state.get("framework")
910
+ or state.get("type")
911
+ or ""
912
+ ).strip().lower()
913
+ return framework == "hermes"
914
+
915
+
916
+ def _is_openclaw_target(state: dict, latest_access: dict) -> bool:
917
+ framework = str(
918
+ latest_access.get("framework")
919
+ or state.get("framework")
920
+ or state.get("type")
921
+ or ""
922
+ ).strip().lower()
923
+ return framework == "openclaw"
924
+
925
+
926
+ def _mask_secret(secret: str | None) -> str:
927
+ if not secret:
928
+ return ""
929
+ text = str(secret)
930
+ if len(text) <= 4:
931
+ return "****"
932
+ return f"{text[:4]}****"
933
+
934
+
935
+ def _openclaw_auth_mode(state: dict, latest_access: dict) -> str:
936
+ return str(
937
+ latest_access.get("openclaw_auth_mode")
938
+ or latest_access.get("gateway_auth_mode")
939
+ or state.get("openclaw_auth_mode")
940
+ or state.get("gateway_auth_mode")
941
+ or ""
942
+ ).strip().lower()
943
+
944
+
945
+ def _select_runtime_api_key(
946
+ *,
947
+ api_key: str | None,
948
+ openclaw_gateway_token: str | None,
949
+ state: dict,
950
+ latest_access: dict,
951
+ ) -> str | None:
952
+ if not _is_openclaw_target(state, latest_access):
953
+ return api_key
954
+
955
+ gateway_token = (
956
+ openclaw_gateway_token
957
+ or os.environ.get("OPENCLAW_GATEWAY_TOKEN")
958
+ or os.environ.get("OPENCLAW_GATEWAY_PASSWORD")
959
+ or latest_access.get("openclaw_gateway_token")
960
+ or latest_access.get("openclaw_gateway_password")
961
+ or state.get("openclaw_gateway_token")
962
+ or state.get("openclaw_gateway_password")
963
+ )
964
+ if gateway_token:
965
+ return str(gateway_token).strip() or None
966
+
967
+ auth_mode = _openclaw_auth_mode(state, latest_access)
968
+ if auth_mode in {"token", "password"}:
969
+ raise click.ClickException(
970
+ "当前 OpenClaw Gateway 为 token/password 模式,agentengine invoke 需要 OpenClaw Gateway token。\n"
971
+ "请使用: agentengine invoke --gateway-token <token>\n"
972
+ "或设置: OPENCLAW_GATEWAY_TOKEN=<token> agentengine invoke\n"
973
+ "如果部署时传过 OPENCLAW_GATEWAY_TOKEN,请重新运行部署让本地 .agentengine.state 记录该 token。\n"
974
+ "注意:这里不是 AgentEngine API Key(ak-*),而是 OPENCLAW_GATEWAY_TOKEN/OPENCLAW_GATEWAY_PASSWORD。"
975
+ )
976
+
977
+ return api_key
978
+
979
+
980
+ def _select_remote_api_format(state: dict, latest_access: dict) -> str:
981
+ """auto 模式下所有框架首选 responses(probe 不通再回退 chat_completions)。"""
982
+ return "responses"
983
+
984
+
985
+ # probe 结果进程内短缓存,key=endpoint,value=(过期时间戳, 是否暴露 responses)。
986
+ # 避免每次 invoke 都多打一跳 GET /v1/responses。
987
+ _RESPONSES_ROUTE_CACHE: dict[tuple, tuple[float, bool]] = {}
988
+ _RESPONSES_ROUTE_CACHE_TTL = 30.0
989
+
990
+
991
+ async def _resolve_remote_api_format(
992
+ *,
993
+ endpoint: str,
994
+ api_key: str | None,
995
+ runtime_api_key: str | None = None,
996
+ insecure: bool,
997
+ state: dict,
998
+ latest_access: dict,
999
+ api_format: str | None = None,
1000
+ ) -> str:
1001
+ # 显式指定 api_format(非 auto)时直接用,不 probe。
1002
+ explicit = str(api_format or "").strip().lower()
1003
+ if explicit in {"responses", "chat_completions"}:
1004
+ return explicit
1005
+
1006
+ # auto:按框架决定首选格式,probe /v1/responses 不通回退 chat_completions。
1007
+ preferred = _select_remote_api_format(state, latest_access)
1008
+ if preferred != "responses":
1009
+ return preferred
1010
+ probe_api_key = runtime_api_key if runtime_api_key is not None else api_key
1011
+ # openclaw/hermes 的 responses 用独立 gateway 鉴权头,GET 401 不代表 POST 401,
1012
+ # 保留 401=可用的既有行为;普通框架 401=鉴权不通,回退 chat。
1013
+ treat_401 = _is_openclaw_target(state, latest_access) or _is_hermes_target(state, latest_access)
1014
+ if await _probe_responses_route_cached(
1015
+ endpoint=endpoint,
1016
+ api_key=probe_api_key,
1017
+ insecure=insecure,
1018
+ treat_401_as_available=treat_401,
1019
+ ):
1020
+ return "responses"
1021
+ return "chat_completions"
1022
+
1023
+
1024
+ # probe 结果进程内短缓存。cache key 含 treat_401:同一 endpoint 在 openclaw/hermes
1025
+ # 与普通框架下对 401 的解读不同,分开缓存避免串味。
1026
+ async def _probe_responses_route_cached(
1027
+ *,
1028
+ endpoint: str,
1029
+ api_key: str | None,
1030
+ insecure: bool,
1031
+ treat_401_as_available: bool = False,
1032
+ ) -> bool:
1033
+ import time as _time
1034
+
1035
+ cache_key = (endpoint.rstrip("/"), treat_401_as_available)
1036
+ now = _time.time()
1037
+ cached = _RESPONSES_ROUTE_CACHE.get(cache_key)
1038
+ if cached and cached[0] > now:
1039
+ return cached[1]
1040
+
1041
+ available = await _probe_responses_route(
1042
+ endpoint=endpoint,
1043
+ api_key=api_key,
1044
+ insecure=insecure,
1045
+ treat_401_as_available=treat_401_as_available,
1046
+ )
1047
+ _RESPONSES_ROUTE_CACHE[cache_key] = (now + _RESPONSES_ROUTE_CACHE_TTL, available)
1048
+ return available
1049
+
1050
+
1051
+ async def _probe_responses_route(
1052
+ *,
1053
+ endpoint: str,
1054
+ api_key: str | None,
1055
+ insecure: bool,
1056
+ treat_401_as_available: bool = False,
1057
+ ) -> bool:
1058
+ try:
1059
+ import httpx
1060
+ except ImportError:
1061
+ return False
1062
+
1063
+ url = f"{endpoint.rstrip('/')}/v1/responses"
1064
+ headers = {}
1065
+ if api_key:
1066
+ headers["Authorization"] = f"Bearer {api_key}"
1067
+ client_kwargs: dict[str, Any] = {"timeout": 5, "trust_env": True}
1068
+ if insecure:
1069
+ client_kwargs["verify"] = False
1070
+
1071
+ try:
1072
+ async with httpx.AsyncClient(**client_kwargs) as client:
1073
+ response = await client.get(url, headers=headers)
1074
+ except Exception:
1075
+ return False
1076
+
1077
+ # POST-only API routes usually answer GET with 405/422/400 = 路由存在且鉴权通过。
1078
+ # 401 = 未鉴权:带 key 仍 401 说明该 key 不被 responses 路由接受,走 responses 也会 401,
1079
+ # 应回退 chat。openclaw/hermes 的 responses 用独立 gateway 鉴权头,GET 401 不代表 POST 401,
1080
+ # 由 treat_401_as_available 保留其既有行为。404/HTML 200 = 路由不存在。
1081
+ available = response.status_code in {400, 405, 422}
1082
+ if treat_401_as_available and response.status_code == 401:
1083
+ available = True
1084
+ return available
1085
+
1086
+
1087
+ def _should_use_hermes_native_tui(*, transport: str, local: bool, state: dict, latest_access: dict) -> bool:
1088
+ if transport == "chat":
1089
+ return False
1090
+ if transport == "native":
1091
+ return True
1092
+ if local:
1093
+ return False
1094
+ return _is_hermes_target(state, latest_access)
1095
+
1096
+
1097
+ def _should_use_openclaw_native_tui(*, transport: str, local: bool, state: dict, latest_access: dict) -> bool:
1098
+ if transport == "chat":
1099
+ return False
1100
+ if transport == "native":
1101
+ return True
1102
+ if local:
1103
+ return False
1104
+ return _is_openclaw_target(state, latest_access)
1105
+
1106
+
1107
+ def _invoke_hermes_terminal_tui(
1108
+ endpoint: str,
1109
+ api_key: str = None,
1110
+ session_id: str = None,
1111
+ insecure: bool = False,
1112
+ cwd: str | None = None,
1113
+ ):
1114
+ click.secho("🖥️ Hermes Native Remote TUI", fg="blue", bold=True)
1115
+ click.echo(" 退出: Ctrl-D 或 Ctrl-C")
1116
+ click.echo(" 新 Pod 首次启动会加载 Hermes tools/skills,可能需要几十秒到 1 分钟;后续进入会更快。")
1117
+ try:
1118
+ _warmup_hermes_terminal(
1119
+ endpoint=endpoint,
1120
+ api_key=api_key,
1121
+ session_id=session_id,
1122
+ insecure=insecure,
1123
+ )
1124
+ exit_code = asyncio.run(
1125
+ run_hermes_terminal_session(
1126
+ endpoint=endpoint,
1127
+ api_key=api_key,
1128
+ session_id=session_id,
1129
+ insecure=insecure,
1130
+ mode="tui",
1131
+ argv=[],
1132
+ cwd=cwd,
1133
+ )
1134
+ )
1135
+ except (KeyboardInterrupt, asyncio.CancelledError):
1136
+ raise SystemExit(130)
1137
+ except Exception as e:
1138
+ click.secho(f"\n❌ Hermes 终端连接失败: {e}", fg="red")
1139
+ click.echo(" 浏览器聊天页请改用: agentengine hermes open --chat")
1140
+ raise SystemExit(1)
1141
+ if exit_code:
1142
+ raise SystemExit(exit_code)
1143
+
1144
+
1145
+ def _warmup_hermes_terminal(
1146
+ *,
1147
+ endpoint: str,
1148
+ api_key: str | None,
1149
+ session_id: str | None,
1150
+ insecure: bool,
1151
+ ) -> None:
1152
+ try:
1153
+ asyncio.run(
1154
+ asyncio.wait_for(
1155
+ run_hermes_terminal_session(
1156
+ endpoint=endpoint,
1157
+ api_key=api_key,
1158
+ session_id=session_id,
1159
+ insecure=insecure,
1160
+ mode="exec",
1161
+ argv=["status"],
1162
+ stdin=io.BytesIO(b""),
1163
+ stdout=io.BytesIO(),
1164
+ ),
1165
+ timeout=30,
1166
+ )
1167
+ )
1168
+ except Exception as exc:
1169
+ if os.getenv("AGENTENGINE_HERMES_TUI_DEBUG"):
1170
+ click.echo(f" Hermes terminal warmup skipped: {exc}", err=True)
1171
+
1172
+
1173
+ def _invoke_openclaw_terminal_tui(
1174
+ endpoint: str,
1175
+ api_key: str = None,
1176
+ session_id: str = None,
1177
+ insecure: bool = False,
1178
+ ):
1179
+ click.secho("🖥️ OpenClaw Native Remote TUI", fg="blue", bold=True)
1180
+ click.echo(" 退出: Ctrl-D 或 Ctrl-C")
1181
+ try:
1182
+ exit_code = asyncio.run(
1183
+ run_terminal_session(
1184
+ endpoint=endpoint,
1185
+ api_key=api_key,
1186
+ session_id=session_id,
1187
+ insecure=insecure,
1188
+ mode="tui",
1189
+ argv=[],
1190
+ )
1191
+ )
1192
+ except (KeyboardInterrupt, asyncio.CancelledError):
1193
+ raise SystemExit(130)
1194
+ except Exception as e:
1195
+ click.secho(f"\n❌ OpenClaw 终端连接失败: {e}", fg="red")
1196
+ click.echo(" 浏览器聊天页请改用: agentengine dashboard open --path /chat")
1197
+ raise SystemExit(1)
1198
+ if exit_code:
1199
+ raise SystemExit(exit_code)
1200
+
1201
+
1202
+ def _get_api_key() -> Optional[str]:
1203
+ """兼容旧代码"""
1204
+ return _load_state().get("api_key")
1205
+
1206
+
1207
+ def _get_endpoint(agent_ref: str, region: str) -> str:
1208
+ """获取 Agent Endpoint(先按 ID,再按名称)"""
1209
+ from ksadk.api import AgentEngineClient
1210
+ import asyncio
1211
+
1212
+ async def _get():
1213
+ async with AgentEngineClient(region=region) as client:
1214
+ # 1) 优先按 ID 查询
1215
+ try:
1216
+ res = await client.get_agent(agent_id=agent_ref)
1217
+ endpoint = _extract_remote_access(res).get("endpoint", "")
1218
+ if endpoint:
1219
+ return endpoint
1220
+ except Exception:
1221
+ pass
1222
+
1223
+ # 2) 回退按名称查询
1224
+ res = await client.get_agent(name=agent_ref)
1225
+ endpoint = _extract_remote_access(res).get("endpoint", "")
1226
+ if endpoint:
1227
+ return endpoint
1228
+
1229
+ # endpoint 为空时,尽量提取真实 ID 供默认域名拼接
1230
+ basic = res.get("basic", {})
1231
+ return basic.get("agent_id") or res.get("agent_id") or ""
1232
+
1233
+ try:
1234
+ resolved = asyncio.run(_get())
1235
+ if resolved and resolved.startswith("http"):
1236
+ return resolved
1237
+ if resolved:
1238
+ click.secho(f"⚠️ Agent '{agent_ref}' 未返回 Endpoint,尝试默认域名", fg="yellow")
1239
+ return f"https://{resolved}.agent.kspmas.ksyun.com"
1240
+ click.secho(f"⚠️ Agent '{agent_ref}' 未返回 Endpoint,尝试使用默认格式", fg="yellow")
1241
+ return f"https://{agent_ref}.agent.kspmas.ksyun.com"
1242
+ except Exception as e:
1243
+ # 如果是本地开发环境或者连接失败,降级处理
1244
+ click.secho(f"⚠️ 获取 Endpoint 失败: {e},尝试使用默认格式", fg="yellow")
1245
+ return f"https://{agent_ref}.agent.kspmas.ksyun.com"
1246
+
1247
+
1248
+ async def _invoke_once(
1249
+ endpoint: str,
1250
+ message: str,
1251
+ api_key: str = None,
1252
+ session_id: str = None,
1253
+ stream: bool = True,
1254
+ insecure: bool = False,
1255
+ model: str = None,
1256
+ api_format: str = "chat_completions",
1257
+ ):
1258
+ """单次调用"""
1259
+ click.echo(f"\n👤 你: {message}")
1260
+ click.echo(f"🤖 Agent: ", nl=False)
1261
+
1262
+ try:
1263
+ if stream:
1264
+ full_response = ""
1265
+ if Live and Markdown:
1266
+ # 降低刷新率减少闪烁,vertical_overflow="visible"防止回滚丢失
1267
+ # 手动控制刷新以减少闪烁
1268
+ with Live(Markdown("", justify="left"), console=console, auto_refresh=False, vertical_overflow="visible") as live:
1269
+ last_refresh_time = 0
1270
+ full_reasoning = ""
1271
+ async for chunk in _stream_chat(endpoint, message, api_key, session_id, True, insecure, model, api_format):
1272
+ content, reasoning = _extract_content(chunk)
1273
+
1274
+ updated = False
1275
+ if reasoning:
1276
+ full_reasoning += reasoning
1277
+ updated = True
1278
+ if content:
1279
+ full_response += content
1280
+ updated = True
1281
+
1282
+ if updated:
1283
+ # 构造显示文本
1284
+ display_text = ""
1285
+ if full_reasoning:
1286
+ formatted_reasoning = full_reasoning.replace('\n', '\n> ')
1287
+ display_text += f"> 🧠 **Thinking:**\n> {formatted_reasoning}\n\n"
1288
+ display_text += full_response
1289
+
1290
+ live.update(Markdown(display_text, justify="left"))
1291
+
1292
+ # 基于时间限流刷新 (每0.2秒一次 = 5 FPS)
1293
+ now = time.time()
1294
+ if now - last_refresh_time > 0.2:
1295
+ live.refresh()
1296
+ last_refresh_time = now
1297
+ live.refresh() # 确保最后一次刷新
1298
+ else:
1299
+ async for chunk in _stream_chat(endpoint, message, api_key, session_id, True, insecure, model, api_format):
1300
+ content, reasoning = _extract_content(chunk)
1301
+ if reasoning:
1302
+ click.secho(reasoning, fg="bright_black", nl=False)
1303
+ if content:
1304
+ print(content, end="", flush=True)
1305
+ click.echo() # 换行
1306
+ else:
1307
+ response = await _chat(endpoint, message, api_key, session_id, insecure, model, api_format)
1308
+ content = _extract_response_content(response)
1309
+ if console and Markdown:
1310
+ console.print(Markdown(content))
1311
+ else:
1312
+ click.echo(content)
1313
+ except Exception as e:
1314
+ click.secho(f"\n❌ 调用失败: {e}", fg="red")
1315
+
1316
+
1317
+
1318
+ async def _chat(
1319
+ endpoint: str,
1320
+ message: str,
1321
+ api_key: str = None,
1322
+ session_id: str = None,
1323
+ insecure: bool = False,
1324
+ model: str = None,
1325
+ api_format: str = "chat_completions",
1326
+ ) -> dict:
1327
+ """非流式调用 (OpenAI 兼容格式)"""
1328
+ try:
1329
+ import httpx
1330
+ except ImportError:
1331
+ click.secho("❌ 请安装 httpx: pip install httpx", fg="red")
1332
+ raise SystemExit(1)
1333
+
1334
+ normalized_api_format = str(api_format or "chat_completions").strip().lower()
1335
+ if normalized_api_format == "responses":
1336
+ url = f"{endpoint.rstrip('/')}/v1/responses"
1337
+ payload = {"input": [{"role": "user", "content": message}], "stream": False}
1338
+ else:
1339
+ url = f"{endpoint.rstrip('/')}/v1/chat/completions"
1340
+ payload = {"messages": [{"role": "user", "content": message}], "stream": False}
1341
+
1342
+ if session_id:
1343
+ payload["session_id"] = session_id
1344
+
1345
+ if model:
1346
+ payload["model"] = model
1347
+
1348
+ # 本地请求禁用系统代理 (ClashX 等会导致本地请求 502 错误)
1349
+ # trust_env=False 会禁用: 代理设置、SSL 证书环境变量、.netrc 文件
1350
+ # 对本地请求通常无影响,因为不需要这些配置
1351
+ is_local = "localhost" in url or "127.0.0.1" in url or "0.0.0.0" in url
1352
+
1353
+ # 构造 httpx client 配置
1354
+ client_kwargs = {"timeout": 60, "trust_env": not is_local}
1355
+
1356
+ # 如果指定了 --insecure 参数,跳过 SSL 证书验证(类似 curl -k)
1357
+ if insecure:
1358
+ client_kwargs["verify"] = False
1359
+
1360
+ # 构造 Headers
1361
+ headers = {}
1362
+ if api_key:
1363
+ headers["Authorization"] = f"Bearer {api_key}"
1364
+
1365
+ async with httpx.AsyncClient(**client_kwargs) as client:
1366
+ response = await client.post(url, json=payload, headers=headers)
1367
+ response.raise_for_status()
1368
+ return response.json()
1369
+
1370
+
1371
+ async def _stream_chat(
1372
+ endpoint: str,
1373
+ message: str,
1374
+ api_key: str = None,
1375
+ session_id: str = None,
1376
+ is_once: bool = False,
1377
+ insecure: bool = False,
1378
+ model: str = None,
1379
+ api_format: str = "chat_completions",
1380
+ ):
1381
+ """流式调用 (SSE)"""
1382
+ try:
1383
+ import httpx
1384
+ except ImportError:
1385
+ click.secho("❌ 请安装 httpx: pip install httpx", fg="red")
1386
+ raise SystemExit(1)
1387
+
1388
+ normalized_api_format = str(api_format or "chat_completions").strip().lower()
1389
+ if normalized_api_format == "responses":
1390
+ url = f"{endpoint.rstrip('/')}/v1/responses"
1391
+ payload = {"input": [{"role": "user", "content": message}], "stream": True}
1392
+ else:
1393
+ url = f"{endpoint.rstrip('/')}/v1/chat/completions"
1394
+ payload = {"messages": [{"role": "user", "content": message}], "stream": True}
1395
+
1396
+ if session_id:
1397
+ payload["session_id"] = session_id
1398
+
1399
+ if model:
1400
+ payload["model"] = model
1401
+
1402
+ # 本地请求禁用系统代理 (ClashX 等会导致本地请求 502 错误)
1403
+ # trust_env=False 会禁用: 代理设置、SSL 证书环境变量、.netrc 文件
1404
+ # 对本地请求通常无影响,因为不需要这些配置
1405
+ is_local = "localhost" in url or "127.0.0.1" in url or "0.0.0.0" in url
1406
+
1407
+ # 构造 httpx client 配置
1408
+ client_kwargs = {"timeout": 60, "trust_env": not is_local}
1409
+
1410
+ # 如果指定了 --insecure 参数,跳过 SSL 证书验证(类似 curl -k)
1411
+ if insecure:
1412
+ client_kwargs["verify"] = False
1413
+
1414
+ # 构造 Headers
1415
+ headers = {}
1416
+ if api_key:
1417
+ headers["Authorization"] = f"Bearer {api_key}"
1418
+
1419
+ async with httpx.AsyncClient(**client_kwargs) as client:
1420
+ async with client.stream("POST", url, json=payload, headers=headers) as response:
1421
+ response.raise_for_status()
1422
+ try:
1423
+ current_event = ""
1424
+ # Use aiter_lines() for robust UTF-8 decoding and line splitting
1425
+ async for line in response.aiter_lines():
1426
+ if not line:
1427
+ continue
1428
+
1429
+ if line.startswith("event: "):
1430
+ current_event = line[7:].strip()
1431
+ continue
1432
+
1433
+ if line.startswith("data: "):
1434
+ data_str = line[6:].strip()
1435
+ if data_str == "[DONE]":
1436
+ break
1437
+
1438
+ try:
1439
+ data = json.loads(data_str)
1440
+ if current_event and isinstance(data, dict):
1441
+ data = {**data, "_event": current_event}
1442
+ # 直接 yield 解析后的 JSON 数据,让 _extract_content 处理
1443
+ yield data
1444
+
1445
+ # Handle events/errors
1446
+ error = data.get("error")
1447
+ if error:
1448
+ click.secho(f"\nError: {error}", fg="red")
1449
+
1450
+ except json.JSONDecodeError:
1451
+ pass
1452
+ except Exception as e:
1453
+ click.secho(f"\nStream error: {e}", fg="red")
1454
+
1455
+
1456
+ def _extract_content(chunk: dict) -> tuple[str, str]:
1457
+ """从 OpenAI 流式响应中提取内容 (包含 reasoning_content)"""
1458
+ event_name = str(chunk.get("_event") or "")
1459
+ if event_name == "response.output_text.delta":
1460
+ return str(chunk.get("delta") or ""), ""
1461
+ if event_name == "response.reasoning.delta":
1462
+ return "", str(chunk.get("delta") or "")
1463
+ if event_name == "response.completed":
1464
+ return "", ""
1465
+
1466
+ # OpenAI 格式: {"choices": [{"delta": {"content": "xxx", "reasoning_content": "thought"}}]}
1467
+ try:
1468
+ choices = chunk.get("choices", [])
1469
+ if choices:
1470
+ delta = choices[0].get("delta", {})
1471
+ return delta.get("content", "") or "", delta.get("reasoning_content", "") or ""
1472
+ except (KeyError, IndexError):
1473
+ pass
1474
+
1475
+ if isinstance(chunk.get("output_text"), str):
1476
+ return chunk["output_text"], ""
1477
+ if isinstance(chunk.get("delta"), str):
1478
+ return chunk["delta"], ""
1479
+ return "", ""
1480
+
1481
+
1482
+ def _extract_response_content(response: dict) -> str:
1483
+ """从 OpenAI 非流式响应中提取内容"""
1484
+ output_text = response.get("output_text")
1485
+ if output_text:
1486
+ return str(output_text)
1487
+ output = response.get("output") or []
1488
+ for item in output:
1489
+ if not isinstance(item, dict):
1490
+ continue
1491
+ for content in item.get("content") or []:
1492
+ if isinstance(content, dict) and content.get("text"):
1493
+ return str(content["text"])
1494
+
1495
+ # OpenAI 格式: {"choices": [{"message": {"content": "xxx"}}]}
1496
+ try:
1497
+ choices = response.get("choices", [])
1498
+ if choices:
1499
+ message = choices[0].get("message", {})
1500
+ return message.get("content", "")
1501
+ except (KeyError, IndexError):
1502
+ pass
1503
+ return str(response)