agentengine-sdk-python 0.6.2__py3-none-any.whl → 0.6.9__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (490) hide show
  1. agentengine_sdk_python-0.6.9.dist-info/METADATA +179 -0
  2. agentengine_sdk_python-0.6.9.dist-info/RECORD +487 -0
  3. {agentengine_sdk_python-0.6.2.dist-info → agentengine_sdk_python-0.6.9.dist-info}/WHEEL +1 -1
  4. agentengine_sdk_python-0.6.9.dist-info/entry_points.txt +3 -0
  5. agentengine_sdk_python-0.6.9.dist-info/licenses/LICENSE +175 -0
  6. agentengine_sdk_python-0.6.9.dist-info/top_level.txt +2 -0
  7. ksadk/__init__.py +13 -0
  8. ksadk/__main__.py +4 -0
  9. ksadk/a2a/__init__.py +15 -0
  10. ksadk/a2a/card_builder.py +64 -0
  11. ksadk/a2a/client.py +309 -0
  12. ksadk/a2a/executor.py +201 -0
  13. ksadk/a2a/server.py +79 -0
  14. ksadk/agents/__init__.py +20 -0
  15. ksadk/agents/base.py +370 -0
  16. ksadk/agents/context.py +38 -0
  17. ksadk/agents/event.py +35 -0
  18. ksadk/agents/loop_agent.py +71 -0
  19. ksadk/agents/parallel_agent.py +109 -0
  20. ksadk/agents/runner_adapter.py +24 -0
  21. ksadk/agents/sequential_agent.py +34 -0
  22. ksadk/api/__init__.py +3 -0
  23. ksadk/api/client.py +2227 -0
  24. ksadk/builders/__init__.py +20 -0
  25. ksadk/builders/base.py +66 -0
  26. ksadk/builders/code_builder.py +1990 -0
  27. ksadk/builders/container_builder.py +786 -0
  28. ksadk/builders/framework_requirements.py +59 -0
  29. ksadk/builders/ks3_uploader.py +389 -0
  30. ksadk/builders/mcp_builder.py +476 -0
  31. ksadk/builders/requirements_utils.py +75 -0
  32. ksadk/cli/__init__.py +531 -0
  33. ksadk/cli/__main__.py +4 -0
  34. ksadk/cli/agent_ref.py +204 -0
  35. ksadk/cli/cmd_a2a.py +167 -0
  36. ksadk/cli/cmd_agent.py +201 -0
  37. ksadk/cli/cmd_build.py +270 -0
  38. ksadk/cli/cmd_completion.py +304 -0
  39. ksadk/cli/cmd_config.py +758 -0
  40. ksadk/cli/cmd_create.py +2277 -0
  41. ksadk/cli/cmd_dashboard.py +900 -0
  42. ksadk/cli/cmd_deploy.py +704 -0
  43. ksadk/cli/cmd_destroy.py +345 -0
  44. ksadk/cli/cmd_files.py +1227 -0
  45. ksadk/cli/cmd_hermes.py +1259 -0
  46. ksadk/cli/cmd_invoke.py +1336 -0
  47. ksadk/cli/cmd_launch.py +564 -0
  48. ksadk/cli/cmd_mcp.py +1174 -0
  49. ksadk/cli/cmd_model.py +302 -0
  50. ksadk/cli/cmd_openclaw.py +3918 -0
  51. ksadk/cli/cmd_run.py +232 -0
  52. ksadk/cli/cmd_status.py +588 -0
  53. ksadk/cli/cmd_version.py +522 -0
  54. ksadk/cli/cmd_web.py +288 -0
  55. ksadk/cli/deploy_utils.py +87 -0
  56. ksadk/cli/dry_run.py +214 -0
  57. ksadk/cli/env_options.py +82 -0
  58. ksadk/cli/error_utils.py +525 -0
  59. ksadk/cli/global_options.py +31 -0
  60. ksadk/cli/local_runtime.py +97 -0
  61. ksadk/cli/model_catalog.py +136 -0
  62. ksadk/cli/network_options.py +410 -0
  63. ksadk/cli/resource_common.py +621 -0
  64. ksadk/cli/storage.py +84 -0
  65. ksadk/cli/ui.py +431 -0
  66. ksadk/cli/workflow_common.py +552 -0
  67. ksadk/common/__init__.py +14 -0
  68. ksadk/common/aicp_env.py +128 -0
  69. ksadk/common/auth.py +190 -0
  70. ksadk/common/constants.py +84 -0
  71. ksadk/common/llm_utils.py +61 -0
  72. ksadk/configs/__init__.py +61 -0
  73. ksadk/configs/env_registry.py +342 -0
  74. ksadk/configs/global_config.py +200 -0
  75. ksadk/configs/settings.py +706 -0
  76. ksadk/conversations/__init__.py +95 -0
  77. ksadk/conversations/attachment_storage.py +379 -0
  78. ksadk/conversations/attachments.py +774 -0
  79. ksadk/conversations/compaction_pipeline.py +387 -0
  80. ksadk/conversations/compaction_prompt.py +128 -0
  81. ksadk/conversations/context.py +303 -0
  82. ksadk/conversations/model_context.py +360 -0
  83. ksadk/conversations/model_options.py +136 -0
  84. ksadk/conversations/normalize.py +446 -0
  85. ksadk/conversations/reasoning_markup.py +91 -0
  86. ksadk/conversations/run_kinds.py +61 -0
  87. ksadk/conversations/run_status.py +53 -0
  88. ksadk/conversations/runtime.py +5208 -0
  89. ksadk/conversations/semantic_summary.py +359 -0
  90. ksadk/conversations/session_title.py +254 -0
  91. ksadk/deployment/__init__.py +83 -0
  92. ksadk/deployment/agent_access.py +226 -0
  93. ksadk/deployment/base.py +229 -0
  94. ksadk/deployment/manager.py +332 -0
  95. ksadk/deployment/providers/__init__.py +10 -0
  96. ksadk/deployment/providers/serverless.py +1105 -0
  97. ksadk/deployment/registry.py +120 -0
  98. ksadk/deployment/state.py +78 -0
  99. ksadk/deployment/ui_config.py +21 -0
  100. ksadk/detection/__init__.py +7 -0
  101. ksadk/detection/detector.py +431 -0
  102. ksadk/detection/mcp_detector.py +250 -0
  103. ksadk/hermes_terminal.py +467 -0
  104. ksadk/identity/__init__.py +17 -0
  105. ksadk/identity/resolver.py +385 -0
  106. ksadk/knowledge_base/__init__.py +62 -0
  107. ksadk/knowledge_base/adk_tool.py +47 -0
  108. ksadk/knowledge_base/client.py +297 -0
  109. ksadk/knowledge_base/langchain_tool.py +55 -0
  110. ksadk/knowledge_base/service.py +66 -0
  111. ksadk/knowledge_base/tool.py +50 -0
  112. ksadk/markdown.py +169 -0
  113. ksadk/mcp_runtime/__init__.py +231 -0
  114. ksadk/memory/__init__.py +57 -0
  115. ksadk/memory/adk/__init__.py +51 -0
  116. ksadk/memory/adk/backends/__init__.py +5 -0
  117. ksadk/memory/adk/backends/base_ltm_backend.py +52 -0
  118. ksadk/memory/adk/backends/http_ltm_backend.py +183 -0
  119. ksadk/memory/adk/backends/inmemory_ltm_backend.py +96 -0
  120. ksadk/memory/adk/backends/sdk_ltm_backend.py +456 -0
  121. ksadk/memory/adk/long_term_memory.py +273 -0
  122. ksadk/memory/adk/short_term_memory.py +289 -0
  123. ksadk/memory/adk_tool.py +31 -0
  124. ksadk/memory/backends/__init__.py +6 -0
  125. ksadk/memory/backends/base.py +103 -0
  126. ksadk/memory/backends/memory.py +112 -0
  127. ksadk/memory/backends/redis.py +146 -0
  128. ksadk/memory/langchain_tool.py +39 -0
  129. ksadk/memory/ltm_backend_factory.py +24 -0
  130. ksadk/memory/manager.py +215 -0
  131. ksadk/memory/service.py +172 -0
  132. ksadk/memory/tool.py +96 -0
  133. ksadk/model_policy.py +223 -0
  134. ksadk/openclaw_gateway.py +378 -0
  135. ksadk/runners/__init__.py +8 -0
  136. ksadk/runners/adk_runner.py +1135 -0
  137. ksadk/runners/base_runner.py +305 -0
  138. ksadk/runners/deepagents_runner.py +13 -0
  139. ksadk/runners/factory.py +73 -0
  140. ksadk/runners/langchain_runner.py +539 -0
  141. ksadk/runners/langgraph_runner.py +852 -0
  142. ksadk/runners/patch_langchain.py +187 -0
  143. ksadk/runners/remote_runner.py +667 -0
  144. ksadk/runners/usage_accumulator.py +38 -0
  145. ksadk/runners/utils/__init__.py +19 -0
  146. ksadk/runners/utils/langfuse.py +230 -0
  147. ksadk/runners/utils/loader.py +62 -0
  148. ksadk/runtime_context.py +166 -0
  149. ksadk/runtime_state.py +25 -0
  150. ksadk/sandbox/__init__.py +28 -0
  151. ksadk/sandbox/backends/__init__.py +9 -0
  152. ksadk/sandbox/backends/e2b.py +154 -0
  153. ksadk/sandbox/backends/local_process.py +131 -0
  154. ksadk/sandbox/base.py +101 -0
  155. ksadk/sandbox/factory.py +53 -0
  156. ksadk/sandbox/registry.py +187 -0
  157. ksadk/server/__init__.py +7 -0
  158. ksadk/server/api_models.py +73 -0
  159. ksadk/server/app.py +3789 -0
  160. ksadk/server/static/assets/ArtifactsPanel-C0meuOdi.js +1 -0
  161. ksadk/server/static/assets/CodeBlock-BbI4HPtJ.js +9 -0
  162. ksadk/server/static/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
  163. ksadk/server/static/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
  164. ksadk/server/static/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
  165. ksadk/server/static/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
  166. ksadk/server/static/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
  167. ksadk/server/static/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
  168. ksadk/server/static/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
  169. ksadk/server/static/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
  170. ksadk/server/static/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
  171. ksadk/server/static/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
  172. ksadk/server/static/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
  173. ksadk/server/static/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
  174. ksadk/server/static/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
  175. ksadk/server/static/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
  176. ksadk/server/static/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
  177. ksadk/server/static/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
  178. ksadk/server/static/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
  179. ksadk/server/static/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
  180. ksadk/server/static/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
  181. ksadk/server/static/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
  182. ksadk/server/static/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
  183. ksadk/server/static/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
  184. ksadk/server/static/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
  185. ksadk/server/static/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
  186. ksadk/server/static/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
  187. ksadk/server/static/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
  188. ksadk/server/static/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
  189. ksadk/server/static/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
  190. ksadk/server/static/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
  191. ksadk/server/static/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
  192. ksadk/server/static/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
  193. ksadk/server/static/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
  194. ksadk/server/static/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
  195. ksadk/server/static/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
  196. ksadk/server/static/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
  197. ksadk/server/static/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
  198. ksadk/server/static/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
  199. ksadk/server/static/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
  200. ksadk/server/static/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
  201. ksadk/server/static/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
  202. ksadk/server/static/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
  203. ksadk/server/static/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
  204. ksadk/server/static/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
  205. ksadk/server/static/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
  206. ksadk/server/static/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
  207. ksadk/server/static/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
  208. ksadk/server/static/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
  209. ksadk/server/static/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
  210. ksadk/server/static/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
  211. ksadk/server/static/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
  212. ksadk/server/static/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
  213. ksadk/server/static/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
  214. ksadk/server/static/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
  215. ksadk/server/static/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
  216. ksadk/server/static/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
  217. ksadk/server/static/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
  218. ksadk/server/static/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
  219. ksadk/server/static/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
  220. ksadk/server/static/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
  221. ksadk/server/static/assets/MathMessageMarkdown-DMLQYoLp.js +8 -0
  222. ksadk/server/static/assets/MathMessageMarkdown-DeYh8QPr.css +1 -0
  223. ksadk/server/static/assets/MermaidBlock-DCR8WZ17.js +303 -0
  224. ksadk/server/static/assets/NativeTerminalPanel-CHOd7Rch.css +1 -0
  225. ksadk/server/static/assets/NativeTerminalPanel-CKUvWhUz.js +1 -0
  226. ksadk/server/static/assets/addon-fit-DthTIhi3.js +1 -0
  227. ksadk/server/static/assets/apl-CnwPGSsG.js +1 -0
  228. ksadk/server/static/assets/arc-bj2rZnP5.js +1 -0
  229. ksadk/server/static/assets/architecture-7EHR7CIX-Cr6Xz1_c.js +1 -0
  230. ksadk/server/static/assets/architectureDiagram-3BPJPVTR-2u1ltDIj.js +36 -0
  231. ksadk/server/static/assets/asciiarmor-qTkVPQu6.js +1 -0
  232. ksadk/server/static/assets/asn1-Dr8qZg38.js +1 -0
  233. ksadk/server/static/assets/asterisk-Bja0s6e1.js +1 -0
  234. ksadk/server/static/assets/blockDiagram-GPEHLZMM-D6fvdK20.js +132 -0
  235. ksadk/server/static/assets/brainfuck-D5EjA2JK.js +1 -0
  236. ksadk/server/static/assets/c4Diagram-AAUBKEIU-BK82L5n5.js +10 -0
  237. ksadk/server/static/assets/channel-BSSOwFIb.js +1 -0
  238. ksadk/server/static/assets/chunk-2J33WTMH-DLVdkeyQ.js +1 -0
  239. ksadk/server/static/assets/chunk-4BX2VUAB-BbwsYcbW.js +1 -0
  240. ksadk/server/static/assets/chunk-55IACEB6-DOairxzw.js +1 -0
  241. ksadk/server/static/assets/chunk-727SXJPM-hCO487gs.js +206 -0
  242. ksadk/server/static/assets/chunk-AQP2D5EJ-XU8vDOuG.js +231 -0
  243. ksadk/server/static/assets/chunk-FMBD7UC4-PrapyfMu.js +15 -0
  244. ksadk/server/static/assets/chunk-ND2GUHAM-CboKhTZ3.js +1 -0
  245. ksadk/server/static/assets/chunk-QZHKN3VN-XXpdiwUL.js +1 -0
  246. ksadk/server/static/assets/classDiagram-4FO5ZUOK-C5HvGiyK.js +1 -0
  247. ksadk/server/static/assets/classDiagram-v2-Q7XG4LA2-C5HvGiyK.js +1 -0
  248. ksadk/server/static/assets/clike-DHH8Ad3s.js +1 -0
  249. ksadk/server/static/assets/clojure-BflJGmDX.js +1 -0
  250. ksadk/server/static/assets/cmake-CyVbuzPu.js +1 -0
  251. ksadk/server/static/assets/cobol-B_OZ4V-R.js +1 -0
  252. ksadk/server/static/assets/coffeescript-B2pKbFk8.js +1 -0
  253. ksadk/server/static/assets/commonlisp-CcllspGY.js +1 -0
  254. ksadk/server/static/assets/cose-bilkent-S5V4N54A-_N8bJ-EU.js +1 -0
  255. ksadk/server/static/assets/crystal-M5-qzICs.js +1 -0
  256. ksadk/server/static/assets/css-CYpP4FRV.js +1 -0
  257. ksadk/server/static/assets/cypher-p9eYesGn.js +1 -0
  258. ksadk/server/static/assets/cytoscape.esm-DelgaX4f.js +321 -0
  259. ksadk/server/static/assets/d-BhMjBjQP.js +1 -0
  260. ksadk/server/static/assets/dagre-BM42HDAG-FoDlo5J2.js +4 -0
  261. ksadk/server/static/assets/dagre-Bx709z4p.js +1 -0
  262. ksadk/server/static/assets/defaultLocale-C8Fc0cco.js +1 -0
  263. ksadk/server/static/assets/diagram-2AECGRRQ-BBp6ebBw.js +43 -0
  264. ksadk/server/static/assets/diagram-5GNKFQAL-CqOvG1PR.js +10 -0
  265. ksadk/server/static/assets/diagram-KO2AKTUF-O9T3t6ba.js +3 -0
  266. ksadk/server/static/assets/diagram-LMA3HP47-CGG8ekBO.js +24 -0
  267. ksadk/server/static/assets/diagram-OG6HWLK6-4BmH1EEg.js +24 -0
  268. ksadk/server/static/assets/diff-ChtP43wD.js +1 -0
  269. ksadk/server/static/assets/dist-6kSZvCZh.js +1 -0
  270. ksadk/server/static/assets/dist-B67uF2uK.js +1 -0
  271. ksadk/server/static/assets/dist-BMKM66B4.js +1 -0
  272. ksadk/server/static/assets/dist-BVzcWa6D.js +1 -0
  273. ksadk/server/static/assets/dist-Bk1l05Ni.js +1 -0
  274. ksadk/server/static/assets/dist-BkJjNih3.js +1 -0
  275. ksadk/server/static/assets/dist-BmuTWcm7.js +9 -0
  276. ksadk/server/static/assets/dist-C2DKRFVK.js +1 -0
  277. ksadk/server/static/assets/dist-CWiTDN_V.js +6 -0
  278. ksadk/server/static/assets/dist-CZ6C3x8B.js +23 -0
  279. ksadk/server/static/assets/dist-CiZHB6ZW.js +1 -0
  280. ksadk/server/static/assets/dist-Cp1_jKwf.js +1 -0
  281. ksadk/server/static/assets/dist-Cwhk4lwI.js +2 -0
  282. ksadk/server/static/assets/dist-D5JGcxBx.js +11 -0
  283. ksadk/server/static/assets/dist-D99wf6_X.js +1 -0
  284. ksadk/server/static/assets/dist-DCpZrjaK.js +1 -0
  285. ksadk/server/static/assets/dist-DI-oVAXw.js +1 -0
  286. ksadk/server/static/assets/dist-DLMjL01k.js +1 -0
  287. ksadk/server/static/assets/dist-DLeM1OCK.js +1 -0
  288. ksadk/server/static/assets/dist-DM0rEQPk.js +1 -0
  289. ksadk/server/static/assets/dist-DbaH3p5h.js +1 -0
  290. ksadk/server/static/assets/dist-S4klkK5W.js +1 -0
  291. ksadk/server/static/assets/dockerfile-BLkNEvjs.js +1 -0
  292. ksadk/server/static/assets/dtd-DTF72YNO.js +1 -0
  293. ksadk/server/static/assets/dylan-C6jEEEk-.js +1 -0
  294. ksadk/server/static/assets/ebnf-D4c0_ac3.js +1 -0
  295. ksadk/server/static/assets/ecl-BSZaIHnp.js +1 -0
  296. ksadk/server/static/assets/eiffel-BEjRio4Q.js +1 -0
  297. ksadk/server/static/assets/elm-Cshzl8qu.js +1 -0
  298. ksadk/server/static/assets/erDiagram-TEJ5UH35-oJhaci7S.js +85 -0
  299. ksadk/server/static/assets/erlang-DaB2Rkuy.js +1 -0
  300. ksadk/server/static/assets/eventmodeling-FCH6USID-CIgwHJLL.js +1 -0
  301. ksadk/server/static/assets/factor-D7LVTn2l.js +1 -0
  302. ksadk/server/static/assets/fcl-ClOhbFR7.js +1 -0
  303. ksadk/server/static/assets/flowDiagram-I6XJVG4X-DtTmUan-.js +162 -0
  304. ksadk/server/static/assets/forth-Cezjo90N.js +1 -0
  305. ksadk/server/static/assets/fortran-Bt6PBEDR.js +1 -0
  306. ksadk/server/static/assets/ganttDiagram-6RSMTGT7-BSsb_eij.js +292 -0
  307. ksadk/server/static/assets/gas-BHEdbvp9.js +1 -0
  308. ksadk/server/static/assets/gherkin-oBAE_ms0.js +1 -0
  309. ksadk/server/static/assets/gitGraph-WXDBUCRP-BVAJ7uo0.js +1 -0
  310. ksadk/server/static/assets/gitGraphDiagram-PVQCEYII-BR5s6Pws.js +106 -0
  311. ksadk/server/static/assets/graphlib-B8gBHxth.js +1 -0
  312. ksadk/server/static/assets/groovy-BC3IOsC8.js +1 -0
  313. ksadk/server/static/assets/haskell-BRsoo5mP.js +1 -0
  314. ksadk/server/static/assets/haxe-DIz0ZZqd.js +1 -0
  315. ksadk/server/static/assets/http-CLVgA2GD.js +1 -0
  316. ksadk/server/static/assets/idl-BUZw3wgd.js +1 -0
  317. ksadk/server/static/assets/index-BH7-pC0B.js +165 -0
  318. ksadk/server/static/assets/index-DAURbCsP.css +1 -0
  319. ksadk/server/static/assets/info-J43DQDTF-MT80V4ge.js +1 -0
  320. ksadk/server/static/assets/infoDiagram-5YYISTIA-DYPc5cvh.js +2 -0
  321. ksadk/server/static/assets/init-D6jRqBbL.js +1 -0
  322. ksadk/server/static/assets/ishikawaDiagram-YF4QCWOH-MoBdALDg.js +70 -0
  323. ksadk/server/static/assets/javascript-9Tg8ixDm.js +1 -0
  324. ksadk/server/static/assets/journeyDiagram-JHISSGLW-DgLiQT8l.js +139 -0
  325. ksadk/server/static/assets/julia-CQfgDRfP.js +1 -0
  326. ksadk/server/static/assets/kanban-definition-UN3LZRKU-CsFx112F.js +89 -0
  327. ksadk/server/static/assets/katex-DRsiwpM2.js +265 -0
  328. ksadk/server/static/assets/linear-TNRQ5Orv.js +1 -0
  329. ksadk/server/static/assets/livescript-BXxG0Yva.js +1 -0
  330. ksadk/server/static/assets/lua-CiA1ziua.js +1 -0
  331. ksadk/server/static/assets/mathematica-u8YMmbU0.js +1 -0
  332. ksadk/server/static/assets/mbox-BEMVcqjs.js +1 -0
  333. ksadk/server/static/assets/mermaid-parser.core-D2eMVOgo.js +161 -0
  334. ksadk/server/static/assets/mindmap-definition-RKZ34NQL-BtByYFhL.js +96 -0
  335. ksadk/server/static/assets/mirc-C9zpzAZU.js +1 -0
  336. ksadk/server/static/assets/mllike-B45xgp2S.js +1 -0
  337. ksadk/server/static/assets/modelica-CqHI_q-D.js +1 -0
  338. ksadk/server/static/assets/mscgen-qgSLujhx.js +1 -0
  339. ksadk/server/static/assets/mumps-CW_TBmxz.js +1 -0
  340. ksadk/server/static/assets/nginx-BMaDbqW3.js +1 -0
  341. ksadk/server/static/assets/nsis-AQ_alPln.js +1 -0
  342. ksadk/server/static/assets/ntriples-d28e9m0E.js +1 -0
  343. ksadk/server/static/assets/octave-D2Q8cUp1.js +1 -0
  344. ksadk/server/static/assets/ordinal-hYBb2elL.js +1 -0
  345. ksadk/server/static/assets/oz-BwTct_5T.js +1 -0
  346. ksadk/server/static/assets/packet-YPE3B663-BLKhbXZB.js +1 -0
  347. ksadk/server/static/assets/pascal-Dp_KdFgX.js +1 -0
  348. ksadk/server/static/assets/perl-CdEHsPfU.js +1 -0
  349. ksadk/server/static/assets/pie-LRSECV5Y-DFOikjSY.js +1 -0
  350. ksadk/server/static/assets/pieDiagram-4H26LBE5-DPnuCB8c.js +30 -0
  351. ksadk/server/static/assets/pig-AF4Hwhju.js +1 -0
  352. ksadk/server/static/assets/powershell-CzG71I4L.js +1 -0
  353. ksadk/server/static/assets/properties-C357ku5U.js +1 -0
  354. ksadk/server/static/assets/protobuf-D1ij_kNL.js +1 -0
  355. ksadk/server/static/assets/pug-Bugr-47h.js +1 -0
  356. ksadk/server/static/assets/puppet-B4hC2X4m.js +1 -0
  357. ksadk/server/static/assets/python-BqYw9LYJ.js +1 -0
  358. ksadk/server/static/assets/q-B9NhS7L_.js +1 -0
  359. ksadk/server/static/assets/quadrantDiagram-W4KKPZXB-miFpCPv4.js +7 -0
  360. ksadk/server/static/assets/r-DADKenSm.js +1 -0
  361. ksadk/server/static/assets/radar-GUYGQ44K-095b1Ikw.js +1 -0
  362. ksadk/server/static/assets/requirementDiagram-4Y6WPE33-mjB_yYkW.js +84 -0
  363. ksadk/server/static/assets/rpm-gpDK5sbt.js +1 -0
  364. ksadk/server/static/assets/ruby-DsYpTuWg.js +1 -0
  365. ksadk/server/static/assets/sankeyDiagram-5OEKKPKP-D7hSlZZZ.js +40 -0
  366. ksadk/server/static/assets/sas-C2OyD2Y6.js +1 -0
  367. ksadk/server/static/assets/scheme-CyDdOh7e.js +1 -0
  368. ksadk/server/static/assets/sequenceDiagram-3UESZ5HK-D9Cx6hC3.js +162 -0
  369. ksadk/server/static/assets/shell-Du5Qg9im.js +1 -0
  370. ksadk/server/static/assets/sieve-BAxa3IjF.js +1 -0
  371. ksadk/server/static/assets/simple-mode-DRpGK0lJ.js +1 -0
  372. ksadk/server/static/assets/smalltalk-BLp5-jwC.js +1 -0
  373. ksadk/server/static/assets/solr-BBopId9w.js +1 -0
  374. ksadk/server/static/assets/sparql-Dz-mlCAC.js +1 -0
  375. ksadk/server/static/assets/spreadsheet-CTIncYxj.js +1 -0
  376. ksadk/server/static/assets/sql-DcCJ9jop.js +1 -0
  377. ksadk/server/static/assets/stateDiagram-AJRCARHV-D_zrg47o.js +1 -0
  378. ksadk/server/static/assets/stateDiagram-v2-BHNVJYJU-J9kCb0nC.js +1 -0
  379. ksadk/server/static/assets/stex-sDrdk2BW.js +1 -0
  380. ksadk/server/static/assets/stylus-DLLZ8dgm.js +1 -0
  381. ksadk/server/static/assets/swift-DKB6_1j6.js +1 -0
  382. ksadk/server/static/assets/tcl-BGfruO1u.js +1 -0
  383. ksadk/server/static/assets/textile-DWBqthVk.js +1 -0
  384. ksadk/server/static/assets/tiddlywiki-VlUiK86J.js +1 -0
  385. ksadk/server/static/assets/tiki-CxVlf5KZ.js +1 -0
  386. ksadk/server/static/assets/timeline-definition-PNZ67QCA-CwSec0Wa.js +120 -0
  387. ksadk/server/static/assets/toml-LvoKBwCs.js +1 -0
  388. ksadk/server/static/assets/treeView-BLDUP644-yinfJEY6.js +1 -0
  389. ksadk/server/static/assets/treemap-LRROVOQU-DosUqN1Q.js +1 -0
  390. ksadk/server/static/assets/troff-C2dAgh7P.js +1 -0
  391. ksadk/server/static/assets/ttcn-BePYh-hG.js +1 -0
  392. ksadk/server/static/assets/ttcn-cfg-CY0aot7Q.js +1 -0
  393. ksadk/server/static/assets/turtle-BDLWE2cD.js +1 -0
  394. ksadk/server/static/assets/vb-BthdM_4N.js +1 -0
  395. ksadk/server/static/assets/vbscript-D9f6rSsU.js +1 -0
  396. ksadk/server/static/assets/velocity-CqftRsjg.js +1 -0
  397. ksadk/server/static/assets/vennDiagram-CIIHVFJN-agCVobiF.js +34 -0
  398. ksadk/server/static/assets/verilog-D-x6ne0Z.js +1 -0
  399. ksadk/server/static/assets/vhdl-CMrOq2q1.js +1 -0
  400. ksadk/server/static/assets/wardley-L42UT6IY-Bvp7-5uH.js +1 -0
  401. ksadk/server/static/assets/wardleyDiagram-YWT4CUSO-TmrlhWNt.js +78 -0
  402. ksadk/server/static/assets/webidl-D5jiJvOU.js +1 -0
  403. ksadk/server/static/assets/xquery-fj_R-kMw.js +1 -0
  404. ksadk/server/static/assets/xterm-DooSxjI5.js +36 -0
  405. ksadk/server/static/assets/xychartDiagram-2RQKCTM6-DV71V-ah.js +7 -0
  406. ksadk/server/static/assets/yacas-CXXgtw0p.js +1 -0
  407. ksadk/server/static/assets/z80-CL1naaMV.js +1 -0
  408. ksadk/server/static/favicon.svg +1 -0
  409. ksadk/server/static/icons.svg +24 -0
  410. ksadk/server/static/index.html +14 -0
  411. ksadk/server/terminal_sessions.py +551 -0
  412. ksadk/sessions/__init__.py +283 -0
  413. ksadk/sessions/base.py +354 -0
  414. ksadk/sessions/continuity.py +313 -0
  415. ksadk/sessions/errors.py +7 -0
  416. ksadk/sessions/in_memory.py +273 -0
  417. ksadk/sessions/local_service.py +949 -0
  418. ksadk/sessions/postgres_service.py +838 -0
  419. ksadk/sessions/sqlite_service.py +13 -0
  420. ksadk/skills/__init__.py +9 -0
  421. ksadk/skills/loader.py +46 -0
  422. ksadk/skills/models.py +120 -0
  423. ksadk/skills/package_store.py +104 -0
  424. ksadk/skills/runtime/__init__.py +17 -0
  425. ksadk/skills/runtime/agent.py +183 -0
  426. ksadk/skills/runtime/artifacts.py +39 -0
  427. ksadk/skills/runtime/backends/__init__.py +1 -0
  428. ksadk/skills/runtime/backends/disabled.py +8 -0
  429. ksadk/skills/runtime/backends/e2b.py +168 -0
  430. ksadk/skills/runtime/backends/local.py +95 -0
  431. ksadk/skills/runtime/base.py +103 -0
  432. ksadk/skills/runtime/executor.py +219 -0
  433. ksadk/skills/runtime/factory.py +37 -0
  434. ksadk/skills/runtime/loader.py +131 -0
  435. ksadk/skills/runtime/registry.py +150 -0
  436. ksadk/skills/runtime/request.py +67 -0
  437. ksadk/skills/service_client.py +239 -0
  438. ksadk/skills/service_env.py +58 -0
  439. ksadk/skills/tool_defs.py +221 -0
  440. ksadk/terminal_client.py +69 -0
  441. ksadk/terminal_exec_policy.py +229 -0
  442. ksadk/tools/__init__.py +17 -0
  443. ksadk/tools/gateway.py +230 -0
  444. ksadk/tools/result_budget.py +154 -0
  445. ksadk/toolsets/__init__.py +852 -0
  446. ksadk/toolsets/_langchain.py +11 -0
  447. ksadk/toolsets/platform.py +93 -0
  448. ksadk/toolsets/sandbox.py +310 -0
  449. ksadk/toolsets/skills.py +217 -0
  450. ksadk/toolsets/web.py +266 -0
  451. ksadk/toolsets/workspace.py +878 -0
  452. ksadk/toolsets/workspace_state.py +44 -0
  453. ksadk/tracing/__init__.py +37 -0
  454. ksadk/tracing/exporters/__init__.py +1 -0
  455. ksadk/tracing/exporters/inmemory_exporter.py +116 -0
  456. ksadk/tracing/exporters/langfuse_exporter.py +445 -0
  457. ksadk/tracing/setup.py +829 -0
  458. ksadk/tracing/span_utils.py +114 -0
  459. ksadk/tui/__init__.py +12 -0
  460. ksadk/tui/app.py +400 -0
  461. ksadk/tui/clipboard.py +100 -0
  462. ksadk/tui/widgets/__init__.py +22 -0
  463. ksadk/tui/widgets/assistant.py +89 -0
  464. ksadk/tui/widgets/base.py +54 -0
  465. ksadk/tui/widgets/chat_input.py +246 -0
  466. ksadk/tui/widgets/history.py +156 -0
  467. ksadk/tui/widgets/system.py +87 -0
  468. ksadk/tui/widgets/thinking.py +110 -0
  469. ksadk/tui/widgets/tool_call.py +161 -0
  470. ksadk/tui/widgets/user.py +37 -0
  471. ksadk/ui_config.py +168 -0
  472. ksadk/version.py +4 -0
  473. ksadk_runtime_common/__init__.py +20 -0
  474. ksadk_runtime_common/memory_backend/__init__.py +29 -0
  475. ksadk_runtime_common/memory_backend/manifest.py +79 -0
  476. ksadk_runtime_common/memory_backend/providers/__init__.py +6 -0
  477. ksadk_runtime_common/memory_backend/providers/lancedb.py +151 -0
  478. ksadk_runtime_common/memory_backend/providers/mem0.py +74 -0
  479. ksadk_runtime_common/memory_backend/registry.py +56 -0
  480. ksadk_runtime_common/memory_backend/render.py +71 -0
  481. ksadk_runtime_common/schemas/memory_backend_manifest.schema.json +148 -0
  482. ksadk_runtime_common/workspace_files/__init__.py +29 -0
  483. ksadk_runtime_common/workspace_files/bootstrap.py +56 -0
  484. ksadk_runtime_common/workspace_files/constants.py +9 -0
  485. ksadk_runtime_common/workspace_files/path_utils.py +71 -0
  486. ksadk_runtime_common/workspace_files/preview.py +96 -0
  487. ksadk_runtime_common/workspace_files/router.py +254 -0
  488. agentengine_sdk_python-0.6.2.dist-info/METADATA +0 -46
  489. agentengine_sdk_python-0.6.2.dist-info/RECORD +0 -4
  490. agentengine_sdk_python-0.6.2.dist-info/top_level.txt +0 -1
@@ -0,0 +1,1990 @@
1
+ """
2
+ Code Builder - zip 打包模式构建
3
+
4
+ 构建流程:
5
+ 1. 准备依赖清单 (requirements.txt)
6
+ 2. 使用 pip 安装依赖 (自动替换 macOS 二进制为 Linux 版本)
7
+ 3. 打包 zip (用户代码 + 依赖 + ksadk 源码 + entrypoint)
8
+ """
9
+
10
+ import os
11
+ import sys
12
+ import shutil
13
+ import subprocess
14
+ import threading
15
+ import time
16
+ import zipfile
17
+ import re
18
+ import json
19
+ import hashlib
20
+ import ast
21
+ from concurrent.futures import ThreadPoolExecutor, as_completed
22
+ from pathlib import Path
23
+ from typing import List, Optional, Set
24
+ from urllib.parse import urlparse
25
+ from urllib.request import Request, urlopen
26
+
27
+ import click
28
+
29
+ from ksadk.builders.base import BaseBuilder, BuildResult
30
+ from ksadk.builders.framework_requirements import (
31
+ FASTAPI_REQUIREMENT,
32
+ requirements_for_framework,
33
+ )
34
+ from ksadk.builders.requirements_utils import (
35
+ exclude_requirement_names,
36
+ merge_requirement_lists,
37
+ parse_requirements_text,
38
+ )
39
+
40
+
41
+ class CodeBuilder(BaseBuilder):
42
+ """Code 模式构建器 - 打包 zip + 依赖"""
43
+
44
+ INPUT_FINGERPRINT_VERSION = 1
45
+ DEPENDENCY_FINGERPRINT_VERSION = 1
46
+ TARGET_INSTALL_PLATFORMS = (
47
+ "manylinux2014_x86_64",
48
+ "manylinux_2_17_x86_64",
49
+ "manylinux_2_28_x86_64",
50
+ "musllinux_1_2_x86_64",
51
+ "linux_x86_64",
52
+ )
53
+ PIP_INDEX_FALLBACKS = (
54
+ "https://pypi.org/simple",
55
+ "https://mirrors.aliyun.com/pypi/simple",
56
+ "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple",
57
+ "https://mirrors.cloud.tencent.com/pypi/simple",
58
+ )
59
+ IGNORED_ROOT_NAMES = {
60
+ "__pycache__",
61
+ "node_modules",
62
+ ".git",
63
+ ".venv",
64
+ "venv",
65
+ "env",
66
+ "site-packages",
67
+ "dist-packages",
68
+ "lib",
69
+ "lib64",
70
+ }
71
+ IGNORED_DIR_NAMES = {
72
+ "__pycache__",
73
+ "node_modules",
74
+ ".git",
75
+ ".venv",
76
+ "venv",
77
+ "env",
78
+ "site-packages",
79
+ "dist-packages",
80
+ "lib",
81
+ "lib64",
82
+ ".pytest_cache",
83
+ ".mypy_cache",
84
+ ".ruff_cache",
85
+ }
86
+ IGNORED_FILE_NAMES = {".DS_Store"}
87
+ KSADK_ALLOWED_SUFFIXES = {
88
+ '.py',
89
+ '.yaml',
90
+ '.yml',
91
+ '.json',
92
+ '.jinja2',
93
+ '.j2',
94
+ '.txt',
95
+ '.md',
96
+ '.html',
97
+ '.js',
98
+ '.css',
99
+ '.svg',
100
+ '.ico',
101
+ '.png',
102
+ '.jpg',
103
+ '.jpeg',
104
+ '.gif',
105
+ '.webp',
106
+ '.woff',
107
+ '.woff2',
108
+ '.ttf',
109
+ '.map',
110
+ }
111
+ BUNDLED_KSADK_CORE_RUNTIME_REQUIREMENTS = (
112
+ "a2a-sdk>=0.3.22",
113
+ "httpx-sse>=0.4.0",
114
+ "sse-starlette>=2.1.0",
115
+ "python-multipart>=0.0.9,<1.0.0",
116
+ "requests>=2.28.0",
117
+ "requests-aws4auth>=1.2.0",
118
+ "kingsoftcloud-sdk-python>=1.5.8.94",
119
+ "cryptography>=44.0.0",
120
+ "websockets>=12.0,<16.0",
121
+ "qrcode>=7.4.0",
122
+ )
123
+ BUNDLED_KSADK_MCP_RUNTIME_REQUIREMENTS = (
124
+ "mcp>=1.1.0",
125
+ "langchain-mcp-adapters>=0.0.1",
126
+ )
127
+ BUNDLED_KSADK_POSTGRES_SESSION_REQUIREMENTS = (
128
+ "asyncpg>=0.30.0,<1.0.0",
129
+ )
130
+ BUNDLED_KSADK_ATTACHMENT_RUNTIME_REQUIREMENTS = (
131
+ "pypdf>=6.0.0",
132
+ "beautifulsoup4>=4.12.0",
133
+ )
134
+ BUNDLED_KSADK_ATTACHMENT_OCR_RUNTIME_REQUIREMENTS = (
135
+ "rapidocr-onnxruntime>=1.2.0",
136
+ )
137
+ BUNDLED_KSADK_RUNTIME_REQUIREMENTS = (
138
+ *BUNDLED_KSADK_CORE_RUNTIME_REQUIREMENTS,
139
+ *BUNDLED_KSADK_ATTACHMENT_RUNTIME_REQUIREMENTS,
140
+ )
141
+ INSTALL_PROGRESS_BAR_WIDTH = 24
142
+ INSTALL_PROGRESS_EVENT_MILESTONES = frozenset({1, 5, 10})
143
+ INSTALL_DOWNLOAD_PROGRESS_MAX = 84
144
+ PACKAGE_PROGRESS_BAR_WIDTH = 24
145
+ PACKAGE_PROGRESS_LOG_MILESTONES = (25, 50, 75, 100)
146
+ PIP_INDEX_CACHE_VERSION = 1
147
+ PIP_INDEX_CACHE_TTL_SECONDS = 6 * 60 * 60
148
+ PIP_INDEX_PROBE_TIMEOUT_SECONDS = 1.5
149
+ PIP_INSTALL_TIMEOUT_SECONDS = 45 * 60
150
+ CONTAINER_SUGGESTION_RAW_THRESHOLD_BYTES = 500 * 1024 * 1024
151
+ CONTAINER_SUGGESTION_ZIP_THRESHOLD_BYTES = 300 * 1024 * 1024
152
+
153
+ def __init__(self, project_dir: Path, config: dict = None):
154
+ super().__init__(project_dir, config)
155
+ self.build_dir = self.project_dir / ".agentengine" / "code_build"
156
+ self.deps_dir = self.build_dir / "linux_deps"
157
+ self.pip_cache_dir = self.build_dir / "pip_cache"
158
+ self._install_progress_width = 0
159
+ self._install_progress_percent = 0
160
+ self._install_progress_stage_name = ""
161
+ self._install_progress_summary_text = ""
162
+ self._install_progress_last_line = ""
163
+ self._install_progress_event_counts: dict[str, int] = {}
164
+ self._install_progress_started_at = time.monotonic()
165
+ self._package_progress_width = 0
166
+ self._package_progress_last_line = ""
167
+ self._package_progress_last_percent_by_label: dict[str, int] = {}
168
+ self._package_progress_logged_milestone_by_label: dict[str, int] = {}
169
+ self._pip_index_candidates_cache: Optional[list[Optional[str]]] = None
170
+ self._pip_index_selection_summary = ""
171
+
172
+ def build(self) -> BuildResult:
173
+ """执行 Code 模式构建"""
174
+ from ksadk.detection import FrameworkDetector
175
+
176
+ self._load_dotenv()
177
+ config = self._load_config()
178
+
179
+ # 检测框架
180
+ detector = FrameworkDetector(str(self.project_dir))
181
+ detection_result = detector.detect()
182
+
183
+ if detection_result.type.value == "unknown":
184
+ return BuildResult(
185
+ success=False,
186
+ error_message="未检测到支持的框架"
187
+ )
188
+
189
+ click.echo(f"📦 框架: {click.style(detection_result.type.value, fg='green')}")
190
+ click.echo(f"🤖 Agent: {click.style(detection_result.name, fg='blue')}")
191
+
192
+ agent_name = config.get('name', self.project_dir.name).replace('-', '_').replace('.', '_')
193
+
194
+ # 创建构建目录
195
+ self.build_dir.mkdir(parents=True, exist_ok=True)
196
+ zip_path = self.build_dir / f"{agent_name}.zip"
197
+
198
+ # 检查是否需要重新构建
199
+ no_cache = self.config.get("no_cache", False) if self.config else False
200
+ repackage = self.config.get("repackage", False) if self.config else False
201
+ rebuild_needed, rebuild_reason = self._rebuild_decision(
202
+ zip_path,
203
+ detection_result,
204
+ no_cache=no_cache,
205
+ repackage=repackage,
206
+ )
207
+ if zip_path.exists() and not rebuild_needed:
208
+ incompatibles = self._scan_incompatible_binaries_in_zip(zip_path)
209
+ if incompatibles:
210
+ click.secho("\n⚠️ 检测到缓存构建包含非 Linux 兼容关键二进制,自动重建...", fg='yellow')
211
+ for item in incompatibles[:5]:
212
+ click.echo(f" - {item}")
213
+ rebuild_reason = "缓存 zip 存在 Linux 兼容性问题"
214
+ else:
215
+ self._save_input_fingerprint(zip_path, detection_result)
216
+ zip_size = zip_path.stat().st_size / (1024 * 1024)
217
+ click.secho(f"\n✅ 使用已有构建: {zip_path.name} ({zip_size:.2f} MB)", fg='green')
218
+ click.echo(" (如需只重新打包当前代码/runtime,请使用 --repackage;如需重装依赖,请使用 --no-cache)")
219
+ return BuildResult(
220
+ success=True,
221
+ artifact_path=zip_path,
222
+ artifact_size=zip_path.stat().st_size,
223
+ metadata={
224
+ "agent_name": agent_name,
225
+ "framework": detection_result.type.value
226
+ }
227
+ )
228
+ elif rebuild_reason:
229
+ click.echo(f" 重新打包原因: {rebuild_reason}")
230
+
231
+ # Step 1: 准备依赖
232
+ click.echo("\n📋 Step 1/3: 准备依赖清单...")
233
+ requirements_path = self._prepare_requirements(detection_result)
234
+
235
+ # Step 2: 安装依赖
236
+ click.echo("\n📦 Step 2/3: 安装依赖...")
237
+ reuse_dependencies, reuse_reason = self._can_reuse_dependency_cache(
238
+ requirements_path,
239
+ no_cache=no_cache,
240
+ )
241
+ if reuse_dependencies:
242
+ stats = self._current_dependency_stats()
243
+ click.secho(
244
+ f" ✓ 复用 Linux 依赖缓存: {stats['file_count']} 个文件, {stats['size_mb']:.1f} MB",
245
+ fg="green",
246
+ )
247
+ click.echo(f" {reuse_reason}")
248
+ else:
249
+ if reuse_reason:
250
+ click.echo(f" {reuse_reason}")
251
+ self._clear_dependency_cache()
252
+ self.deps_dir.mkdir(parents=True, exist_ok=True)
253
+
254
+ if not self._install_dependencies(requirements_path):
255
+ return BuildResult(
256
+ success=False,
257
+ error_message="依赖安装失败"
258
+ )
259
+ self._save_dependency_fingerprint(requirements_path)
260
+
261
+ # Step 3: 打包 zip
262
+ click.echo("\n📦 Step 3/3: 打包 zip...")
263
+ package_started_at = time.monotonic()
264
+ self._package_zip(zip_path, detection_result)
265
+ click.echo(f" ✓ 打包耗时: {self._format_elapsed(package_started_at)}")
266
+ self._save_input_fingerprint(zip_path, detection_result)
267
+
268
+ zip_size = zip_path.stat().st_size
269
+ click.echo(f" zip 文件: {zip_path}")
270
+ click.echo(f" 大小: {zip_size / (1024 * 1024):.2f} MB")
271
+
272
+ return BuildResult(
273
+ success=True,
274
+ artifact_path=zip_path,
275
+ artifact_size=zip_size,
276
+ metadata={
277
+ "agent_name": agent_name,
278
+ "framework": detection_result.type.value,
279
+ "deps_dir": str(self.deps_dir)
280
+ }
281
+ )
282
+
283
+ def _rebuild_decision(
284
+ self,
285
+ zip_path: Path,
286
+ detection_result,
287
+ *,
288
+ no_cache: bool,
289
+ repackage: bool,
290
+ ) -> tuple[bool, str]:
291
+ if no_cache:
292
+ return True, "--no-cache 已开启,强制重新打包"
293
+ if repackage:
294
+ return True, "--repackage 已开启,复用依赖并重新打包当前代码/runtime"
295
+ if not zip_path.exists():
296
+ return True, "首次构建"
297
+
298
+ previous = self._load_input_fingerprint(zip_path)
299
+ if not previous:
300
+ return self._need_rebuild_from_mtime(zip_path), "旧缓存缺少输入指纹,按文件时间判断"
301
+
302
+ current = self._build_input_fingerprint(detection_result)
303
+ if previous.get("fingerprint") == current["fingerprint"]:
304
+ return False, ""
305
+ return True, self._classify_rebuild_reason(previous, current)
306
+
307
+ def _classify_rebuild_reason(self, previous: dict, current: dict) -> str:
308
+ previous_files = set(previous.get("files") or [])
309
+ current_files = set(current.get("files") or [])
310
+ changed = previous_files.symmetric_difference(current_files)
311
+ if not changed and previous.get("fingerprint") != current.get("fingerprint"):
312
+ previous_digests = previous.get("file_digests") or {}
313
+ current_digests = current.get("file_digests") or {}
314
+ changed = {
315
+ name
316
+ for name in set(previous_digests) | set(current_digests)
317
+ if previous_digests.get(name) != current_digests.get(name)
318
+ }
319
+ if any(str(name).startswith(("ksadk/", "ksadk_runtime_common/")) for name in changed):
320
+ return "ksadk runtime 变更"
321
+ if changed:
322
+ return "业务代码或项目文件变更"
323
+ return "构建输入指纹变化"
324
+
325
+ def _need_rebuild(self, zip_path: Path, detection_result) -> bool:
326
+ """检查是否需要重新构建。优先使用输入内容指纹,缺失时回退到 mtime。"""
327
+ manifest = self._load_input_fingerprint(zip_path)
328
+ if manifest:
329
+ current_fingerprint = self._build_input_fingerprint(detection_result)
330
+ return manifest.get("fingerprint") != current_fingerprint["fingerprint"]
331
+ return self._need_rebuild_from_mtime(zip_path)
332
+
333
+ def _need_rebuild_from_mtime(self, zip_path: Path) -> bool:
334
+ """兼容旧缓存:当没有指纹文件时,回退到 mtime 判断。"""
335
+ zip_mtime = zip_path.stat().st_mtime
336
+
337
+ for item in self.project_dir.iterdir():
338
+ if item.name.startswith('.') or item.name in ('__pycache__', 'node_modules', '.git', '.venv', 'venv'):
339
+ continue
340
+ if item.is_file() and item.stat().st_mtime > zip_mtime:
341
+ return True
342
+ if item.is_dir():
343
+ for file_path in item.rglob('*.py'):
344
+ if file_path.stat().st_mtime > zip_mtime:
345
+ return True
346
+ return False
347
+
348
+ def _fingerprint_manifest_path(self, zip_path: Path) -> Path:
349
+ return zip_path.with_suffix(".inputs.json")
350
+
351
+ def _dependency_fingerprint_manifest_path(self) -> Path:
352
+ return self.build_dir / "linux_deps.inputs.json"
353
+
354
+ def _load_input_fingerprint(self, zip_path: Path) -> Optional[dict]:
355
+ manifest_path = self._fingerprint_manifest_path(zip_path)
356
+ if not manifest_path.exists():
357
+ return None
358
+ try:
359
+ with open(manifest_path, encoding="utf-8") as f:
360
+ data = json.load(f)
361
+ if data.get("version") != self.INPUT_FINGERPRINT_VERSION:
362
+ return None
363
+ return data
364
+ except Exception:
365
+ return None
366
+
367
+ def _save_input_fingerprint(self, zip_path: Path, detection_result) -> None:
368
+ manifest_path = self._fingerprint_manifest_path(zip_path)
369
+ manifest_path.parent.mkdir(parents=True, exist_ok=True)
370
+ payload = self._build_input_fingerprint(detection_result)
371
+ with open(manifest_path, "w", encoding="utf-8") as f:
372
+ json.dump(payload, f, indent=2, ensure_ascii=False)
373
+
374
+ def _load_dependency_fingerprint(self) -> Optional[dict]:
375
+ manifest_path = self._dependency_fingerprint_manifest_path()
376
+ if not manifest_path.exists():
377
+ return None
378
+ try:
379
+ with open(manifest_path, encoding="utf-8") as f:
380
+ data = json.load(f)
381
+ if data.get("version") != self.DEPENDENCY_FINGERPRINT_VERSION:
382
+ return None
383
+ return data
384
+ except Exception:
385
+ return None
386
+
387
+ def _save_dependency_fingerprint(self, requirements_path: Path) -> None:
388
+ manifest_path = self._dependency_fingerprint_manifest_path()
389
+ manifest_path.parent.mkdir(parents=True, exist_ok=True)
390
+ payload = self._build_dependency_fingerprint(requirements_path)
391
+ payload.update(self._current_dependency_stats())
392
+ with open(manifest_path, "w", encoding="utf-8") as f:
393
+ json.dump(payload, f, indent=2, ensure_ascii=False)
394
+
395
+ def _clear_dependency_cache(self) -> None:
396
+ if self.deps_dir.exists():
397
+ shutil.rmtree(self.deps_dir)
398
+ manifest_path = self._dependency_fingerprint_manifest_path()
399
+ if manifest_path.exists():
400
+ manifest_path.unlink()
401
+
402
+ def _pip_install_timeout_seconds(self) -> int:
403
+ configured = os.getenv("KSADK_BUILD_PIP_INSTALL_TIMEOUT_SECONDS")
404
+ if configured:
405
+ try:
406
+ value = int(configured)
407
+ if value > 0:
408
+ return value
409
+ except ValueError:
410
+ pass
411
+ return self.PIP_INSTALL_TIMEOUT_SECONDS
412
+
413
+ def _attachment_ocr_runtime_enabled(self) -> bool:
414
+ value = os.getenv("KSADK_BUILD_ENABLE_ATTACHMENT_OCR", "")
415
+ return value.strip().lower() in {"1", "true", "yes", "on"}
416
+
417
+ def _bundled_runtime_requirements(self) -> tuple[str, ...]:
418
+ requirements = list(self.BUNDLED_KSADK_RUNTIME_REQUIREMENTS)
419
+ if self._attachment_ocr_runtime_enabled():
420
+ requirements.extend(self.BUNDLED_KSADK_ATTACHMENT_OCR_RUNTIME_REQUIREMENTS)
421
+ if self._mcp_runtime_enabled():
422
+ requirements.extend(self.BUNDLED_KSADK_MCP_RUNTIME_REQUIREMENTS)
423
+ if self._postgres_session_runtime_enabled():
424
+ requirements.extend(self.BUNDLED_KSADK_POSTGRES_SESSION_REQUIREMENTS)
425
+ return tuple(requirements)
426
+
427
+ def _mcp_runtime_enabled(self) -> bool:
428
+ if self._env_flag_enabled("KSADK_BUILD_ENABLE_MCP"):
429
+ return True
430
+ if self._project_env_has_configured_value("KSADK_MCP_SERVERS"):
431
+ return True
432
+ if self._project_env_value("KSADK_ENABLE_MCP_TOOLS").strip().lower() in {"1", "true", "yes", "on"}:
433
+ return True
434
+ return self._project_imports_any({"mcp", "langchain_mcp_adapters"})
435
+
436
+ def _postgres_session_runtime_enabled(self) -> bool:
437
+ if self._env_flag_enabled("KSADK_BUILD_ENABLE_POSTGRES_SESSION"):
438
+ return True
439
+ backend = (
440
+ self._project_env_value("KSADK_SESSION_BACKEND")
441
+ or self._project_env_value("AGENTENGINE_SESSION_BACKEND")
442
+ or self._project_env_value("KSADK_STM_BACKEND")
443
+ )
444
+ if backend.strip().lower() == "postgres":
445
+ return True
446
+ dsn = (
447
+ self._project_env_value("KSADK_SESSION_DSN")
448
+ or self._project_env_value("KSADK_STM_URL")
449
+ or self._project_env_value("KSADK_STM_DB_URL")
450
+ )
451
+ return self._looks_like_postgres_dsn(dsn)
452
+
453
+ def _env_flag_enabled(self, name: str) -> bool:
454
+ return os.getenv(name, "").strip().lower() in {"1", "true", "yes", "on"}
455
+
456
+ def _project_env_has_configured_value(self, name: str) -> bool:
457
+ value = (os.getenv(name) or self._project_env_value(name)).strip()
458
+ if not value:
459
+ return False
460
+ try:
461
+ parsed = json.loads(value)
462
+ except json.JSONDecodeError:
463
+ return value.strip("'\"").lower() not in {"", "[]", "{}", "null", "none"}
464
+ return bool(parsed)
465
+
466
+ def _project_env_value(self, name: str) -> str:
467
+ value = os.getenv(name)
468
+ if value:
469
+ return value
470
+ for env_file in (self.project_dir / ".env", self.project_dir / "agentengine.env"):
471
+ if not env_file.is_file():
472
+ continue
473
+ try:
474
+ for raw_line in env_file.read_text(encoding="utf-8").splitlines():
475
+ stripped = raw_line.strip()
476
+ if not stripped or stripped.startswith("#") or "=" not in stripped:
477
+ continue
478
+ key, raw_value = stripped.split("=", 1)
479
+ if key.strip() == name:
480
+ return raw_value.strip().strip("'\"")
481
+ except OSError:
482
+ continue
483
+ return ""
484
+
485
+ def _looks_like_postgres_dsn(self, value: str) -> bool:
486
+ normalized = (value or "").strip().lower()
487
+ return normalized.startswith(("postgres://", "postgresql://", "postgresql+asyncpg://"))
488
+
489
+ def _project_imports_any(self, module_names: set[str]) -> bool:
490
+ for py_file in self._iter_project_py_files():
491
+ if self._should_skip_project_file(py_file):
492
+ continue
493
+ try:
494
+ tree = ast.parse(py_file.read_text(encoding="utf-8"), filename=str(py_file))
495
+ except (OSError, SyntaxError, UnicodeDecodeError):
496
+ continue
497
+ for node in ast.walk(tree):
498
+ if isinstance(node, ast.Import):
499
+ for alias in node.names:
500
+ root = alias.name.split(".", 1)[0]
501
+ if root in module_names:
502
+ return True
503
+ elif isinstance(node, ast.ImportFrom) and node.module:
504
+ root = node.module.split(".", 1)[0]
505
+ if root in module_names:
506
+ return True
507
+ return False
508
+
509
+ def _iter_project_py_files(self):
510
+ for path in self._iter_project_files():
511
+ if path.suffix == ".py":
512
+ yield path
513
+
514
+ def _build_dependency_fingerprint(self, requirements_path: Path) -> dict:
515
+ digest = hashlib.sha256()
516
+ requirements_text = requirements_path.read_text(encoding="utf-8")
517
+
518
+ digest.update(f"deps-fingerprint-version:{self.DEPENDENCY_FINGERPRINT_VERSION}\n".encode("utf-8"))
519
+ digest.update(f"builder-platform:{sys.platform}\n".encode("utf-8"))
520
+ digest.update(
521
+ f"builder-python:{sys.version_info.major}.{sys.version_info.minor}\n".encode("utf-8")
522
+ )
523
+ digest.update(f"target-python:{self.TARGET_PYTHON_VERSION}\n".encode("utf-8"))
524
+ digest.update(f"target-platforms:{','.join(self.TARGET_INSTALL_PLATFORMS)}\n".encode("utf-8"))
525
+ digest.update(requirements_text.encode("utf-8"))
526
+
527
+ requirements = [line for line in requirements_text.splitlines() if line.strip()]
528
+ return {
529
+ "version": self.DEPENDENCY_FINGERPRINT_VERSION,
530
+ "fingerprint": digest.hexdigest(),
531
+ "requirements": requirements,
532
+ }
533
+
534
+ def _current_dependency_stats(self) -> dict:
535
+ file_count = 0
536
+ size_bytes = 0
537
+ if self.deps_dir.exists():
538
+ for file_path in self.deps_dir.rglob("*"):
539
+ if not file_path.is_file():
540
+ continue
541
+ file_count += 1
542
+ size_bytes += file_path.stat().st_size
543
+ return {
544
+ "file_count": file_count,
545
+ "size_bytes": size_bytes,
546
+ "size_mb": round(size_bytes / (1024 * 1024), 2),
547
+ }
548
+
549
+ def _can_reuse_dependency_cache(
550
+ self,
551
+ requirements_path: Path,
552
+ *,
553
+ no_cache: bool = False,
554
+ ) -> tuple[bool, str]:
555
+ if no_cache:
556
+ return False, "--no-cache 已开启,重新安装 Linux 依赖"
557
+
558
+ if not self.deps_dir.exists():
559
+ return False, "未发现现成 Linux 依赖缓存,重新安装"
560
+
561
+ manifest = self._load_dependency_fingerprint()
562
+ if manifest is None:
563
+ return False, "缺少依赖缓存指纹,重新安装"
564
+
565
+ current_fingerprint = self._build_dependency_fingerprint(requirements_path)
566
+ if manifest.get("fingerprint") != current_fingerprint["fingerprint"]:
567
+ return False, "依赖清单发生变化,重新安装"
568
+
569
+ stats = self._current_dependency_stats()
570
+ if stats["file_count"] == 0:
571
+ return False, "依赖缓存为空,重新安装"
572
+
573
+ incompatibles = self._scan_incompatible_binaries_in_deps()
574
+ if incompatibles:
575
+ preview = ", ".join(incompatibles[:3])
576
+ return False, f"依赖缓存存在兼容性问题 ({preview}),重新安装"
577
+
578
+ return True, "requirements 未变化,跳过依赖重装"
579
+
580
+ def _build_input_fingerprint(self, detection_result) -> dict:
581
+ digest = hashlib.sha256()
582
+ files = []
583
+ file_digests = {}
584
+
585
+ digest.update(f"fingerprint-version:{self.INPUT_FINGERPRINT_VERSION}\n".encode("utf-8"))
586
+ digest.update(f"framework:{detection_result.type.value}\n".encode("utf-8"))
587
+ digest.update(f"entry:{getattr(detection_result, 'entry_point', '')}\n".encode("utf-8"))
588
+ digest.update(f"target-python:{self.TARGET_PYTHON_VERSION}\n".encode("utf-8"))
589
+
590
+ for dep in self._build_requirements_list(detection_result):
591
+ digest.update(dep.encode("utf-8"))
592
+ digest.update(b"\n")
593
+
594
+ for file_path in self._iter_project_files():
595
+ relative = file_path.relative_to(self.project_dir).as_posix()
596
+ files.append(relative)
597
+ digest.update(relative.encode("utf-8"))
598
+ digest.update(b"\0")
599
+ file_digest = hashlib.sha256()
600
+ with open(file_path, "rb") as f:
601
+ while True:
602
+ chunk = f.read(1024 * 1024)
603
+ if not chunk:
604
+ break
605
+ digest.update(chunk)
606
+ file_digest.update(chunk)
607
+ digest.update(b"\0")
608
+ file_digests[relative] = file_digest.hexdigest()
609
+
610
+ for package_name, relative, file_path in self._iter_bundled_source_files():
611
+ fingerprint_name = f"{package_name}/{relative}"
612
+ files.append(fingerprint_name)
613
+ digest.update(fingerprint_name.encode("utf-8"))
614
+ digest.update(b"\0")
615
+ file_digest = hashlib.sha256()
616
+ with open(file_path, "rb") as f:
617
+ while True:
618
+ chunk = f.read(1024 * 1024)
619
+ if not chunk:
620
+ break
621
+ digest.update(chunk)
622
+ file_digest.update(chunk)
623
+ digest.update(b"\0")
624
+ file_digests[fingerprint_name] = file_digest.hexdigest()
625
+
626
+ return {
627
+ "version": self.INPUT_FINGERPRINT_VERSION,
628
+ "fingerprint": digest.hexdigest(),
629
+ "files": files,
630
+ "file_digests": file_digests,
631
+ }
632
+
633
+ def _iter_bundled_source_files(self):
634
+ import ksadk
635
+ import ksadk_runtime_common
636
+
637
+ yield from self._iter_bundled_source_package("ksadk", Path(ksadk.__file__).resolve().parent)
638
+ yield from self._iter_bundled_source_package(
639
+ "ksadk_runtime_common",
640
+ Path(ksadk_runtime_common.__file__).resolve().parent,
641
+ )
642
+
643
+ def _iter_bundled_source_package(self, package_name: str, package_root: Path):
644
+ for file_path in sorted(package_root.rglob('*')):
645
+ if not file_path.is_file():
646
+ continue
647
+ relative_path = file_path.relative_to(package_root)
648
+ if package_name == "ksadk" and self._should_skip_ksadk_relative_path(relative_path):
649
+ continue
650
+ if '__pycache__' in file_path.parts:
651
+ continue
652
+ suffix = file_path.suffix.lower()
653
+ if suffix not in self.KSADK_ALLOWED_SUFFIXES:
654
+ continue
655
+ yield package_name, relative_path.as_posix(), file_path
656
+
657
+ def _should_skip_ksadk_relative_path(self, relative_path: Path) -> bool:
658
+ parts = relative_path.parts
659
+ return len(parts) >= 2 and parts[0] == "server" and parts[1] == "web-ui"
660
+
661
+ def _iter_project_files(self):
662
+ for item in sorted(self.project_dir.iterdir(), key=lambda p: p.name):
663
+ if self._should_skip_root_path(item):
664
+ continue
665
+ if item.is_file():
666
+ yield item
667
+ elif item.is_dir():
668
+ yield from self._walk_project_dir(item)
669
+
670
+ def _walk_project_dir(self, root_dir: Path):
671
+ for current_root, dirnames, filenames in os.walk(root_dir):
672
+ dirnames[:] = sorted(d for d in dirnames if not self._should_skip_dir_name(d))
673
+ for filename in sorted(filenames):
674
+ file_path = Path(current_root) / filename
675
+ if self._should_skip_project_file(file_path):
676
+ continue
677
+ yield file_path
678
+
679
+ def _should_skip_root_path(self, path: Path) -> bool:
680
+ if self._is_real_dotenv_file(path.name):
681
+ return True
682
+ if path.name.startswith(".") and path.name != ".env.example":
683
+ return True
684
+ if path.name in self.IGNORED_ROOT_NAMES:
685
+ return True
686
+ if path.is_dir() and self._should_skip_dir_name(path.name):
687
+ return True
688
+ if path.is_file() and self._should_skip_project_file(path):
689
+ return True
690
+ return False
691
+
692
+ def _should_skip_dir_name(self, dir_name: str) -> bool:
693
+ return dir_name in self.IGNORED_DIR_NAMES
694
+
695
+ def _should_skip_project_file(self, file_path: Path) -> bool:
696
+ if self._is_real_dotenv_file(file_path.name):
697
+ return True
698
+ if file_path.name in self.IGNORED_FILE_NAMES:
699
+ return True
700
+ if file_path.suffix == ".pyc":
701
+ return True
702
+ if "__pycache__" in file_path.parts:
703
+ return True
704
+ return False
705
+
706
+ @staticmethod
707
+ def _is_real_dotenv_file(file_name: str) -> bool:
708
+ return file_name == ".env" or (
709
+ file_name.startswith(".env.")
710
+ and file_name not in {".env.example", ".env.sample", ".env.template"}
711
+ )
712
+
713
+ def _prepare_requirements(self, detection_result) -> Path:
714
+ """准备 requirements.txt"""
715
+ final_deps = self._build_requirements_list(detection_result)
716
+
717
+ if (self.project_dir / "requirements.txt").exists():
718
+ click.echo(f" 发现 requirements.txt,正在合并...")
719
+ else:
720
+ click.echo(f" 自动生成依赖清单")
721
+
722
+ # 写入构建目录
723
+ requirements_path = self.build_dir / "requirements.txt"
724
+ requirements_path.write_text("\n".join(final_deps))
725
+
726
+ click.echo(f" 共 {len(final_deps)} 个依赖包:")
727
+ for dep in final_deps[:5]:
728
+ click.echo(f" • {dep}")
729
+ if len(final_deps) > 5:
730
+ click.echo(f" ... 及其他 {len(final_deps) - 5} 个")
731
+
732
+ return requirements_path
733
+
734
+ def _build_requirements_list(self, detection_result) -> List[str]:
735
+ final_deps = merge_requirement_lists(
736
+ self._get_base_requirements(detection_result),
737
+ self._bundled_runtime_requirements(),
738
+ )
739
+
740
+ user_requirements = self.project_dir / "requirements.txt"
741
+ if user_requirements.exists():
742
+ user_content = user_requirements.read_text(encoding="utf-8")
743
+ user_deps = exclude_requirement_names(
744
+ parse_requirements_text(user_content),
745
+ excluded_names={"ksadk"},
746
+ )
747
+ final_deps = merge_requirement_lists(final_deps, user_deps)
748
+
749
+ return final_deps
750
+
751
+ def _get_base_requirements(self, detection_result) -> List[str]:
752
+ """获取基础依赖列表"""
753
+ deps = [
754
+ # Core
755
+ FASTAPI_REQUIREMENT,
756
+ "uvicorn>=0.23.0",
757
+ "python-dotenv>=1.0.0",
758
+ "pydantic>=2.0.0",
759
+ "pyyaml>=6.0.0",
760
+ "httpx>=0.24.0",
761
+ # Tracing
762
+ "opentelemetry-api>=1.37.0",
763
+ "opentelemetry-sdk>=1.37.0",
764
+ "opentelemetry-exporter-otlp>=1.37.0",
765
+ "openinference-instrumentation-langchain>=0.1.0",
766
+ "langfuse>=2.0.0",
767
+ ]
768
+
769
+ framework = detection_result.type.value
770
+ deps += requirements_for_framework(framework)
771
+
772
+ return deps
773
+
774
+ # 目标 Python 版本 (必须与容器运行时一致)
775
+ TARGET_PYTHON_VERSION = "312" # 容器中为 Python 3.12
776
+
777
+ def _install_progress_stage(self, line: str) -> tuple[int, str] | None:
778
+ normalized = line.strip()
779
+ if not normalized:
780
+ return None
781
+
782
+ patterns = (
783
+ ("Looking in indexes:", (8, "检查依赖源")),
784
+ ("Collecting ", (18, "解析依赖")),
785
+ ("Preparing metadata", (28, "准备元数据")),
786
+ ("Using cached ", (40, "下载依赖")),
787
+ ("Downloading ", (45, "下载依赖")),
788
+ ("Building wheel for ", (76, "构建 wheel")),
789
+ ("Requirement already satisfied:", (82, "复用已安装依赖")),
790
+ ("Installing collected packages:", (86, "安装依赖")),
791
+ ("Successfully installed ", (92, "安装完成")),
792
+ )
793
+ for prefix, value in patterns:
794
+ if normalized.startswith(prefix):
795
+ return value
796
+ return None
797
+
798
+ def _truncate_progress_summary(self, summary: str, max_length: int = 80) -> str:
799
+ normalized = " ".join(summary.strip().split())
800
+ if len(normalized) <= max_length:
801
+ return normalized
802
+ return normalized[: max_length - 3] + "..."
803
+
804
+ def _format_elapsed(self, started_at: float) -> str:
805
+ elapsed = max(0.0, time.monotonic() - started_at)
806
+ if elapsed < 60:
807
+ return f"{elapsed:.1f}s"
808
+ minutes = int(elapsed // 60)
809
+ seconds = int(elapsed % 60)
810
+ return f"{minutes}m{seconds:02d}s"
811
+
812
+ def _render_install_progress(self, percent: int, stage: str, summary: str = "") -> str:
813
+ clamped = max(0, min(percent, 100))
814
+ filled = round((clamped / 100) * self.INSTALL_PROGRESS_BAR_WIDTH)
815
+ if filled <= 0:
816
+ bar = "." * self.INSTALL_PROGRESS_BAR_WIDTH
817
+ elif filled >= self.INSTALL_PROGRESS_BAR_WIDTH:
818
+ bar = "=" * self.INSTALL_PROGRESS_BAR_WIDTH
819
+ else:
820
+ bar = "=" * (filled - 1) + ">" + "." * (self.INSTALL_PROGRESS_BAR_WIDTH - filled)
821
+ message = f" [{bar}] {clamped:>3}% {stage}"
822
+ if summary:
823
+ message += f" | {self._truncate_progress_summary(summary)}"
824
+ return message
825
+
826
+ def _emit_install_progress(self, percent: int, stage: str, summary: str = "") -> None:
827
+ clamped = max(0, min(percent, 100))
828
+ if clamped < self._install_progress_percent:
829
+ return
830
+
831
+ line = self._render_install_progress(clamped, stage, summary)
832
+ if line == self._install_progress_last_line:
833
+ return
834
+
835
+ self._install_progress_percent = clamped
836
+ self._install_progress_stage_name = stage
837
+ self._install_progress_summary_text = summary
838
+ padding = " " * max(0, self._install_progress_width - len(line))
839
+ if sys.stdout.isatty():
840
+ click.echo(f"\r{line}{padding}", nl=False)
841
+ self._install_progress_width = len(line)
842
+ else:
843
+ click.echo(line)
844
+ self._install_progress_width = 0
845
+ self._install_progress_last_line = line
846
+
847
+ def _adjust_install_progress_percent(self, percent: int, stage: str) -> int:
848
+ count = self._install_progress_event_counts.get(stage, 0)
849
+ if count <= 0:
850
+ return percent
851
+
852
+ if stage == "下载依赖":
853
+ base = 45 if percent >= 45 else percent
854
+ return min(
855
+ self.INSTALL_DOWNLOAD_PROGRESS_MAX,
856
+ max(
857
+ percent,
858
+ base + min(self.INSTALL_DOWNLOAD_PROGRESS_MAX - base, int(count * 0.8)),
859
+ ),
860
+ )
861
+
862
+ return percent
863
+
864
+ def _finish_install_progress(self) -> None:
865
+ if self._install_progress_width and sys.stdout.isatty():
866
+ click.echo()
867
+ self._install_progress_width = 0
868
+ self._install_progress_percent = 0
869
+ self._install_progress_stage_name = ""
870
+ self._install_progress_summary_text = ""
871
+ self._install_progress_last_line = ""
872
+ self._install_progress_event_counts = {}
873
+ self._install_progress_started_at = time.monotonic()
874
+
875
+ def _result_error_summary(self, result: subprocess.CompletedProcess[str] | None) -> str:
876
+ if result is None:
877
+ return "unknown"
878
+ combined = "\n".join(
879
+ part for part in (result.stderr or "", result.stdout or "") if part
880
+ )
881
+ for line in reversed(combined.splitlines()):
882
+ normalized = line.strip()
883
+ if normalized:
884
+ return self._truncate_progress_summary(normalized)
885
+ return "unknown"
886
+
887
+ def _pip_missing_from_result(self, result: subprocess.CompletedProcess[str] | None) -> bool:
888
+ if result is None:
889
+ return False
890
+ combined = "\n".join(
891
+ part for part in (result.stderr or "", result.stdout or "") if part
892
+ )
893
+ return "No module named pip" in combined
894
+
895
+ def _bootstrap_pip_if_missing(self, result: subprocess.CompletedProcess[str] | None) -> bool:
896
+ if not self._pip_missing_from_result(result):
897
+ return False
898
+
899
+ self._emit_install_progress(
900
+ 14,
901
+ "准备安装",
902
+ "pip 工具链缺失,正在自动初始化",
903
+ )
904
+ bootstrap_cmd = [
905
+ sys.executable,
906
+ "-m",
907
+ "ensurepip",
908
+ "--upgrade",
909
+ ]
910
+ try:
911
+ bootstrap_result = subprocess.run(
912
+ bootstrap_cmd,
913
+ capture_output=True,
914
+ text=True,
915
+ timeout=120,
916
+ )
917
+ except Exception as exc:
918
+ self._emit_install_progress(
919
+ 14,
920
+ "准备安装",
921
+ f"pip 自动初始化失败: {exc}",
922
+ )
923
+ return False
924
+
925
+ if bootstrap_result.returncode == 0:
926
+ self._emit_install_progress(
927
+ 15,
928
+ "准备安装",
929
+ "pip 工具链已初始化,继续安装依赖",
930
+ )
931
+ return True
932
+
933
+ self._emit_install_progress(
934
+ 14,
935
+ "准备安装",
936
+ f"pip 自动初始化失败: {self._result_error_summary(bootstrap_result)}",
937
+ )
938
+ return False
939
+
940
+ def _extract_pip_artifact_name(self, value: str) -> str:
941
+ token = value.strip().split()[0] if value.strip() else ""
942
+ parsed = urlparse(token)
943
+ if parsed.scheme and parsed.path:
944
+ basename = os.path.basename(parsed.path)
945
+ if basename:
946
+ return basename
947
+ basename = os.path.basename(token)
948
+ return basename or token or value.strip()
949
+
950
+ def _summarize_pip_progress_line(self, percent: int, stage: str, line: str) -> Optional[str]:
951
+ normalized = line.strip()
952
+ if not normalized:
953
+ return None
954
+
955
+ force_emit = percent > self._install_progress_percent or stage != self._install_progress_stage_name
956
+ count = self._install_progress_event_counts.get(stage, 0) + 1
957
+ self._install_progress_event_counts[stage] = count
958
+
959
+ if stage == "检查依赖源":
960
+ return self._truncate_progress_summary(normalized)
961
+
962
+ if stage == "解析依赖":
963
+ detail = normalized[len("Collecting "):].split(" (from ", 1)[0]
964
+ summary = f"已解析 {count} 个依赖,最近: {detail}"
965
+ if force_emit or count in self.INSTALL_PROGRESS_EVENT_MILESTONES or count % 10 == 0:
966
+ return summary
967
+ return None
968
+
969
+ if stage == "准备元数据":
970
+ if force_emit or count in {1, 3} or count % 10 == 0:
971
+ return f"准备元数据: {self._truncate_progress_summary(normalized)}"
972
+ return None
973
+
974
+ if stage == "下载依赖":
975
+ if normalized.startswith("Using cached "):
976
+ payload = normalized[len("Using cached "):]
977
+ elif normalized.startswith("Downloading "):
978
+ payload = normalized[len("Downloading "):]
979
+ else:
980
+ payload = normalized
981
+ target = self._extract_pip_artifact_name(payload)
982
+ summary = f"已处理 {count} 个 wheel,耗时 {self._format_elapsed(self._install_progress_started_at)},最近: {target}"
983
+ if force_emit or count in self.INSTALL_PROGRESS_EVENT_MILESTONES or count % 5 == 0:
984
+ return summary
985
+ return None
986
+
987
+ if stage == "构建 wheel":
988
+ detail = normalized[len("Building wheel for "):].split(" ", 1)[0]
989
+ if force_emit or count == 1 or count % 5 == 0:
990
+ return f"构建 wheel: {detail}"
991
+ return None
992
+
993
+ if stage == "安装依赖":
994
+ detail = normalized[len("Installing collected packages:"):].strip()
995
+ return f"安装包: {self._truncate_progress_summary(detail, max_length=60)}"
996
+
997
+ if stage == "复用已安装依赖":
998
+ detail = normalized[len("Requirement already satisfied:"):].split(" in ", 1)[0].strip()
999
+ if force_emit or count in self.INSTALL_PROGRESS_EVENT_MILESTONES or count % 10 == 0:
1000
+ return f"复用依赖: {detail}"
1001
+ return None
1002
+
1003
+ if stage == "安装完成":
1004
+ detail = normalized[len("Successfully installed "):].strip()
1005
+ return f"最近结果: {self._truncate_progress_summary(detail, max_length=60)}"
1006
+
1007
+ return self._truncate_progress_summary(normalized)
1008
+
1009
+ def _pip_index_cache_path(self) -> Path:
1010
+ return Path.home() / ".agentengine" / "pip-index-cache.json"
1011
+
1012
+ def _normalize_pip_index_url(self, index_url: str) -> str:
1013
+ return index_url.rstrip("/")
1014
+
1015
+ def _short_pip_index_label(self, index_url: str) -> str:
1016
+ parsed = urlparse(index_url)
1017
+ if not parsed.scheme:
1018
+ return index_url
1019
+ label = parsed.netloc
1020
+ path = parsed.path.rstrip("/")
1021
+ if path:
1022
+ label += path
1023
+ return label
1024
+
1025
+ def _load_cached_pip_index_order(self, urls: list[str]) -> Optional[list[str]]:
1026
+ cache_path = self._pip_index_cache_path()
1027
+ if not cache_path.exists():
1028
+ return None
1029
+
1030
+ try:
1031
+ with open(cache_path, encoding="utf-8") as f:
1032
+ payload = json.load(f)
1033
+ except Exception:
1034
+ return None
1035
+
1036
+ if payload.get("version") != self.PIP_INDEX_CACHE_VERSION:
1037
+ return None
1038
+
1039
+ updated_at = payload.get("updated_at")
1040
+ if not isinstance(updated_at, (int, float)):
1041
+ return None
1042
+ if time.time() - float(updated_at) > self.PIP_INDEX_CACHE_TTL_SECONDS:
1043
+ return None
1044
+
1045
+ cached_order = [
1046
+ self._normalize_pip_index_url(item)
1047
+ for item in payload.get("order", [])
1048
+ if isinstance(item, str)
1049
+ ]
1050
+ required = {self._normalize_pip_index_url(url) for url in urls}
1051
+ if not required.issubset(cached_order):
1052
+ return None
1053
+
1054
+ return [item for item in cached_order if item in required]
1055
+
1056
+ def _save_pip_index_order(self, order: list[str]) -> None:
1057
+ cache_path = self._pip_index_cache_path()
1058
+ try:
1059
+ cache_path.parent.mkdir(parents=True, exist_ok=True)
1060
+ with open(cache_path, "w", encoding="utf-8") as f:
1061
+ json.dump(
1062
+ {
1063
+ "version": self.PIP_INDEX_CACHE_VERSION,
1064
+ "updated_at": time.time(),
1065
+ "order": order,
1066
+ },
1067
+ f,
1068
+ indent=2,
1069
+ ensure_ascii=False,
1070
+ )
1071
+ except Exception:
1072
+ return
1073
+
1074
+ def _probe_pip_index_latency(self, index_url: str) -> Optional[float]:
1075
+ probe_url = f"{self._normalize_pip_index_url(index_url)}/pip/"
1076
+ request = Request(
1077
+ probe_url,
1078
+ headers={"User-Agent": "ksadk-build/0.4.0"},
1079
+ )
1080
+ started_at = time.monotonic()
1081
+ try:
1082
+ with urlopen(request, timeout=self.PIP_INDEX_PROBE_TIMEOUT_SECONDS) as response:
1083
+ response.read(1)
1084
+ return time.monotonic() - started_at
1085
+ except Exception:
1086
+ return None
1087
+
1088
+ def _rank_pip_index_urls(self, urls: list[str]) -> tuple[list[str], str]:
1089
+ cached_order = self._load_cached_pip_index_order(urls)
1090
+ if cached_order:
1091
+ best = cached_order[0]
1092
+ return cached_order, f"缓存优先 {self._short_pip_index_label(best)}"
1093
+
1094
+ timings: dict[str, Optional[float]] = {}
1095
+ with ThreadPoolExecutor(max_workers=min(4, len(urls))) as executor:
1096
+ future_to_url = {
1097
+ executor.submit(self._probe_pip_index_latency, url): url
1098
+ for url in urls
1099
+ }
1100
+ for future in as_completed(future_to_url):
1101
+ url = future_to_url[future]
1102
+ try:
1103
+ timings[url] = future.result()
1104
+ except Exception:
1105
+ timings[url] = None
1106
+
1107
+ successful = [url for url in urls if timings.get(url) is not None]
1108
+ if not successful:
1109
+ return urls, "测速失败,按默认顺序尝试源"
1110
+
1111
+ successful.sort(key=lambda item: timings[item] or float("inf"))
1112
+ failed = [url for url in urls if timings.get(url) is None]
1113
+ ordered = [*successful, *failed]
1114
+ self._save_pip_index_order(ordered)
1115
+
1116
+ best = ordered[0]
1117
+ best_ms = round((timings[best] or 0.0) * 1000)
1118
+ return ordered, f"测速优先 {self._short_pip_index_label(best)} ({best_ms} ms)"
1119
+
1120
+ def _install_dependencies(self, requirements_path: Path) -> bool:
1121
+ """安装依赖到 deps_dir"""
1122
+ current_python = f"{sys.version_info.major}.{sys.version_info.minor}"
1123
+ target_python = self._target_python_display()
1124
+ if current_python != target_python:
1125
+ click.echo(
1126
+ f" ⚠ 构建机 Python={current_python},目标运行时 Python={target_python},"
1127
+ "将执行二进制替换与 ABI 校验"
1128
+ )
1129
+
1130
+ try:
1131
+ need_binary_replacement = (
1132
+ sys.platform in ("darwin", "win32")
1133
+ or (sys.platform.startswith("linux") and current_python != target_python)
1134
+ )
1135
+ used_target_runtime_wheels = False
1136
+ result = None
1137
+
1138
+ if need_binary_replacement:
1139
+ self._emit_install_progress(
1140
+ 4,
1141
+ "准备安装",
1142
+ f"目标运行时 Python {target_python}",
1143
+ )
1144
+ result = self._run_pip_install(
1145
+ requirements_path,
1146
+ target_runtime_wheels=True,
1147
+ )
1148
+ if result.returncode == 0:
1149
+ used_target_runtime_wheels = True
1150
+ else:
1151
+ self._emit_install_progress(
1152
+ 34,
1153
+ "回退安装",
1154
+ "目标运行时 wheel 安装失败,改用宿主机构建并替换二进制",
1155
+ )
1156
+ if self.deps_dir.exists():
1157
+ shutil.rmtree(self.deps_dir)
1158
+ self.deps_dir.mkdir(parents=True, exist_ok=True)
1159
+
1160
+ if result is None or result.returncode != 0:
1161
+ self._emit_install_progress(
1162
+ 10,
1163
+ "准备安装",
1164
+ "开始宿主机依赖安装",
1165
+ )
1166
+ result = self._run_pip_install(
1167
+ requirements_path,
1168
+ target_runtime_wheels=False,
1169
+ )
1170
+
1171
+ if result.returncode != 0:
1172
+ self._finish_install_progress()
1173
+ click.secho(" ✗ 安装失败", fg='red')
1174
+ if result.stderr:
1175
+ error_lines = [l for l in result.stderr.split('\n') if 'ERROR' in l.upper()][:3]
1176
+ for line in error_lines:
1177
+ click.echo(f" {line}")
1178
+ return False
1179
+
1180
+ # 替换非 Linux 平台二进制,或 Linux 下非目标 Python ABI 的二进制
1181
+ if need_binary_replacement and not used_target_runtime_wheels:
1182
+ self._emit_install_progress(
1183
+ 88,
1184
+ "替换 Linux wheels",
1185
+ "检测并替换平台相关二进制",
1186
+ )
1187
+ self._replace_platform_binaries()
1188
+
1189
+ # 二进制兼容性校验(避免把不可运行的包部署到 Linux Runtime)
1190
+ self._emit_install_progress(
1191
+ 96,
1192
+ "校验运行时兼容性",
1193
+ "扫描关键原生扩展",
1194
+ )
1195
+ incompatibles = self._scan_incompatible_binaries_in_deps()
1196
+ if incompatibles:
1197
+ self._finish_install_progress()
1198
+ click.secho(" ✗ 检测到与 Linux Runtime 不兼容的关键二进制,构建终止", fg='red')
1199
+ for item in incompatibles[:10]:
1200
+ click.echo(f" - {item}")
1201
+ if any(i.startswith("python-abi-mismatch:") for i in incompatibles):
1202
+ click.echo(
1203
+ f" 提示: 目标运行时为 Python {target_python},"
1204
+ f"请使用 Python {target_python} 构建,或改用 Container 模式部署"
1205
+ )
1206
+ if any("tiktoken" in i for i in incompatibles):
1207
+ click.echo(" 提示: tiktoken 为 langchain-openai 必需;若替换失败可重试或检查网络/镜像")
1208
+ click.echo(" 建议: 删除 .agentengine/*_build 后重新构建,或在 Linux 环境重新打包")
1209
+ return False
1210
+
1211
+ deps_count = sum(1 for _ in self.deps_dir.rglob('*') if _.is_file())
1212
+ deps_size = sum(f.stat().st_size for f in self.deps_dir.rglob('*') if f.is_file()) / (1024 * 1024)
1213
+ self._emit_install_progress(
1214
+ 100,
1215
+ "依赖安装完成",
1216
+ f"{deps_count} 个文件, {deps_size:.1f} MB",
1217
+ )
1218
+ self._finish_install_progress()
1219
+
1220
+ return True
1221
+
1222
+ except subprocess.TimeoutExpired:
1223
+ self._finish_install_progress()
1224
+ timeout_minutes = max(1, round(self._pip_install_timeout_seconds() / 60))
1225
+ click.secho(f" ✗ 安装超时 ({timeout_minutes}分钟)", fg='red')
1226
+ return False
1227
+ except Exception as e:
1228
+ self._finish_install_progress()
1229
+ click.secho(f" ✗ 依赖安装失败: {e}", fg='red')
1230
+ return False
1231
+
1232
+ def _replace_platform_binaries(self) -> None:
1233
+ """替换非目标运行时平台/ABI 的 C 扩展为 Linux 目标版本。"""
1234
+ # 模块名到 pip 包名的映射
1235
+ MODULE_TO_PACKAGE = {
1236
+ '_cffi_backend': 'cffi',
1237
+ 'yaml': 'pyyaml',
1238
+ '_yaml': 'pyyaml',
1239
+ 'rpds': 'rpds-py',
1240
+ 'PIL': 'pillow',
1241
+ 'cv2': 'opencv-python',
1242
+ 'sklearn': 'scikit-learn',
1243
+ '_watchdog_fsevents': 'watchdog',
1244
+ 'google': None, # 跳过命名空间包
1245
+ 'grpc': 'grpcio',
1246
+ '_grpc': 'grpcio',
1247
+ 'uuid_utils': 'uuid-utils',
1248
+ 'pydantic_core': 'pydantic-core',
1249
+ '_pydantic_core': 'pydantic-core',
1250
+ # tiktoken: langchain-openai 核心依赖, Rust 编译的 C 扩展
1251
+ 'tiktoken': 'tiktoken',
1252
+ '_tiktoken': 'tiktoken',
1253
+ # 其他常见原生扩展
1254
+ 'regex': 'regex',
1255
+ '_regex': 'regex',
1256
+ 'multidict': 'multidict',
1257
+ 'yarl': 'yarl',
1258
+ 'aiohttp': 'aiohttp',
1259
+ 'frozenlist': 'frozenlist',
1260
+ 'charset_normalizer': 'charset-normalizer',
1261
+ 'msgpack': 'msgpack',
1262
+ # Windows 特有
1263
+ 'win32': 'pywin32',
1264
+ 'win32com': 'pywin32',
1265
+ }
1266
+
1267
+ # 找到所有二进制文件
1268
+ binary_files = []
1269
+ if sys.platform == 'darwin':
1270
+ binary_files = list(self.deps_dir.rglob('*.so')) + list(self.deps_dir.rglob('*.dylib'))
1271
+ elif sys.platform == 'win32':
1272
+ binary_files = list(self.deps_dir.rglob('*.pyd')) + list(self.deps_dir.rglob('*.dll'))
1273
+ elif sys.platform.startswith('linux'):
1274
+ binary_files = list(self.deps_dir.rglob('*.so'))
1275
+
1276
+ if not binary_files:
1277
+ return
1278
+
1279
+ # 提取需要替换的包名
1280
+ packages_to_replace: Set[str] = set()
1281
+ for bin_file in binary_files:
1282
+ rel_path = bin_file.relative_to(self.deps_dir)
1283
+ parts = rel_path.parts
1284
+
1285
+ # 忽略 bin 目录下的 dll (通常是 runtime)
1286
+ if 'bin' in parts:
1287
+ continue
1288
+
1289
+ if len(parts) > 1:
1290
+ detected_name = parts[0]
1291
+ else:
1292
+ detected_name = bin_file.name.split('.')[0]
1293
+
1294
+ # 跳过特定文件夹
1295
+ if detected_name in ('__pycache__', 'bin', 'include', 'lib', 'Scripts'):
1296
+ continue
1297
+
1298
+ if detected_name in MODULE_TO_PACKAGE:
1299
+ pkg_name = MODULE_TO_PACKAGE[detected_name]
1300
+ if pkg_name:
1301
+ packages_to_replace.add(pkg_name)
1302
+ else:
1303
+ packages_to_replace.add(detected_name)
1304
+
1305
+ if not packages_to_replace:
1306
+ return
1307
+
1308
+ click.echo(f"\r 检测到 {len(binary_files)} 个二进制文件 ({sys.platform}), 替换 {len(packages_to_replace)} 个包为 Linux 版本")
1309
+
1310
+ # 下载 Linux wheels
1311
+ wheels_dir = self.build_dir / "linux_wheels"
1312
+ if wheels_dir.exists():
1313
+ shutil.rmtree(wheels_dir)
1314
+ wheels_dir.mkdir(parents=True)
1315
+
1316
+ replaced_count = 0
1317
+ total_packages = len(packages_to_replace)
1318
+ for index, pkg_name in enumerate(sorted(packages_to_replace), start=1):
1319
+ # 提取确切的版本号以避免不兼容问题 (如 pydantic-core)
1320
+ target_version = ""
1321
+ search_name = pkg_name.replace('-', '_').lower()
1322
+ for info_dir in self.deps_dir.glob(f"{search_name}-*.dist-info"):
1323
+ version_str = info_dir.name[len(search_name)+1:-10] # remove search_name + '-' and '.dist-info'
1324
+ target_version = f"=={version_str}"
1325
+ break
1326
+
1327
+ pkg_with_version = f"{pkg_name}{target_version}"
1328
+ self._emit_install_progress(
1329
+ 88 + min(6, round((index / max(total_packages, 1)) * 6)),
1330
+ "替换 Linux wheels",
1331
+ f"{index}/{total_packages} {pkg_with_version}",
1332
+ )
1333
+
1334
+ try:
1335
+ downloaded = False
1336
+ for index_url in self._pip_index_candidates():
1337
+ download_cmd = [
1338
+ sys.executable, "-m", "pip", "download",
1339
+ pkg_with_version,
1340
+ "-d", str(wheels_dir),
1341
+ "--platform", "manylinux2014_x86_64",
1342
+ "--platform", "manylinux_2_17_x86_64",
1343
+ "--platform", "manylinux_2_28_x86_64",
1344
+ "--platform", "musllinux_1_2_x86_64",
1345
+ "--platform", "linux_x86_64",
1346
+ "--python-version", self.TARGET_PYTHON_VERSION,
1347
+ "--only-binary=:all:",
1348
+ "--implementation", "cp",
1349
+ "--no-deps",
1350
+ "--quiet",
1351
+ "--disable-pip-version-check",
1352
+ "--retries", "2",
1353
+ "--timeout", "30",
1354
+ "--cache-dir", str(self.pip_cache_dir),
1355
+ ]
1356
+ if index_url:
1357
+ download_cmd += ["-i", index_url]
1358
+ result = subprocess.run(download_cmd, capture_output=True, text=True, timeout=90)
1359
+ if result.returncode == 0:
1360
+ downloaded = True
1361
+ break
1362
+ if downloaded:
1363
+ replaced_count += 1
1364
+ else:
1365
+ failed_msg = result.stderr.strip().split('\n')[-1] if result and result.stderr else 'unknown'
1366
+ click.secho(f" ⚠ 替换失败: {pkg_with_version} ({failed_msg})", fg='yellow')
1367
+ except Exception as e:
1368
+ click.secho(f" ⚠ 替换异常: {pkg_name} ({e})", fg='yellow')
1369
+
1370
+ # 解压 wheel 覆盖到 deps_dir
1371
+ for wheel_file in wheels_dir.glob("*.whl"):
1372
+ try:
1373
+ wheel_name = wheel_file.name.split('-')[0].lower().replace('_', '-')
1374
+
1375
+ # 删除旧的包目录
1376
+ for old_dir in self.deps_dir.iterdir():
1377
+ if old_dir.is_dir() and old_dir.name.lower().replace('_', '-') == wheel_name:
1378
+ shutil.rmtree(old_dir)
1379
+ # 不要 break,可能由多个目录 (e.g. pydantic_core, pydantic_core-2.x.dist-info)
1380
+
1381
+ # 删除根目录下的二进制文件
1382
+ for ext in ('*.so', '*.dylib', '*.pyd', '*.dll'):
1383
+ for bin_file in self.deps_dir.glob(f"{wheel_name}*{ext[1:]}"):
1384
+ try:
1385
+ bin_file.unlink()
1386
+ except:
1387
+ pass
1388
+ for bin_file in self.deps_dir.glob(f"{wheel_name.replace('-', '_')}*{ext[1:]}"):
1389
+ try:
1390
+ bin_file.unlink()
1391
+ except:
1392
+ pass
1393
+
1394
+ # 解压新的 wheel
1395
+ with zipfile.ZipFile(wheel_file, 'r') as zf:
1396
+ zf.extractall(self.deps_dir)
1397
+ except Exception:
1398
+ pass
1399
+
1400
+ shutil.rmtree(wheels_dir, ignore_errors=True)
1401
+
1402
+ # 清理所有残留的非 Linux 平台二进制文件
1403
+ # (wheel 解压后可能有旧的 darwin/win .so 文件未被覆盖)
1404
+ cleaned_count = 0
1405
+ for so_file in list(self.deps_dir.rglob('*.so')):
1406
+ name = so_file.name.lower()
1407
+ if 'darwin' in name or 'win' in name:
1408
+ try:
1409
+ so_file.unlink()
1410
+ cleaned_count += 1
1411
+ except Exception:
1412
+ pass
1413
+ for dylib_file in list(self.deps_dir.rglob('*.dylib')):
1414
+ try:
1415
+ dylib_file.unlink()
1416
+ cleaned_count += 1
1417
+ except Exception:
1418
+ pass
1419
+ if cleaned_count > 0:
1420
+ click.echo(f" ✓ 清理 {cleaned_count} 个残留平台二进制文件")
1421
+
1422
+ click.echo(f" ✓ 成功替换 {replaced_count}/{len(packages_to_replace)} 个包")
1423
+
1424
+ def _is_linux_so(self, name: str) -> bool:
1425
+ lower = name.lower()
1426
+ return lower.endswith(".so") and "darwin" not in lower and "win" not in lower
1427
+
1428
+ def _target_python_display(self) -> str:
1429
+ if len(self.TARGET_PYTHON_VERSION) >= 2:
1430
+ major = self.TARGET_PYTHON_VERSION[0]
1431
+ minor = self.TARGET_PYTHON_VERSION[1:]
1432
+ return f"{major}.{minor}"
1433
+ return self.TARGET_PYTHON_VERSION
1434
+
1435
+ def _is_target_python_abi_binary(self, name: str) -> bool:
1436
+ lower = name.lower()
1437
+ if not lower.endswith(".so"):
1438
+ return False
1439
+ if "abi3" in lower:
1440
+ return True
1441
+ if "cpython-" in lower:
1442
+ return f"cpython-{self.TARGET_PYTHON_VERSION}" in lower
1443
+ # 无 ABI tag 的 .so 无法准确识别,按可用处理,避免误伤
1444
+ return True
1445
+
1446
+ def _scan_incompatible_binaries_in_zip(self, zip_path: Path) -> List[str]:
1447
+ """扫描缓存 zip 中关键扩展模块是否缺失 Linux 版本。"""
1448
+ try:
1449
+ with zipfile.ZipFile(zip_path, "r") as zf:
1450
+ names = zf.namelist()
1451
+ return self._detect_critical_binary_issues(names)
1452
+ except Exception:
1453
+ # 缓存损坏时交给后续重建流程处理
1454
+ return ["zip-read-failed"]
1455
+
1456
+ def _scan_incompatible_binaries_in_deps(self) -> List[str]:
1457
+ """扫描 deps 目录中的关键扩展模块是否缺失 Linux 版本。"""
1458
+ names = []
1459
+ for file_path in self.deps_dir.rglob("*"):
1460
+ if file_path.is_file():
1461
+ names.append(file_path.relative_to(self.deps_dir).as_posix())
1462
+ return self._detect_critical_binary_issues(names)
1463
+
1464
+ def _detect_critical_binary_issues(self, names: List[str]) -> List[str]:
1465
+ issues: List[str] = []
1466
+
1467
+ # 定义需要检查的关键原生扩展模块
1468
+ # 格式: (描述, 正则模式)
1469
+ critical_modules = [
1470
+ # pydantic_core: pydantic v2 核心扩展,缺失会直接启动失败
1471
+ ("pydantic_core/_pydantic_core", r"pydantic_core/_pydantic_core.*\.(so|pyd)$"),
1472
+ # _cffi_backend: cryptography / cffi 常见依赖
1473
+ ("_cffi_backend", r"(^|/)_cffi_backend.*\.(so|pyd)$"),
1474
+ # tiktoken/_tiktoken: langchain-openai 核心依赖 (Rust 编译)
1475
+ ("tiktoken/_tiktoken", r"tiktoken/_tiktoken.*\.(so|pyd)$"),
1476
+ ]
1477
+
1478
+ for module_name, pattern in critical_modules:
1479
+ matched_bins = [n for n in names if re.search(pattern, n)]
1480
+ if matched_bins:
1481
+ linux_bins = [n for n in matched_bins if self._is_linux_so(n)]
1482
+ if not linux_bins:
1483
+ issues.append(f"missing-linux:{module_name}")
1484
+ continue
1485
+ if not any(self._is_target_python_abi_binary(n) for n in linux_bins):
1486
+ issues.append(
1487
+ f"python-abi-mismatch:{module_name}:"
1488
+ f"expected-cpython-{self.TARGET_PYTHON_VERSION}-or-abi3"
1489
+ )
1490
+
1491
+ # 通用检查: 所有 .so 文件中不应包含 darwin/win 平台标识
1492
+ all_so_files = [n for n in names if n.endswith('.so')]
1493
+ darwin_so_count = sum(1 for n in all_so_files if 'darwin' in n.lower())
1494
+ if darwin_so_count > 0:
1495
+ issues.append(f"warning:found-{darwin_so_count}-darwin-so-files")
1496
+
1497
+ return issues
1498
+
1499
+ def _package_zip(self, zip_path: Path, detection_result) -> None:
1500
+ """打包 zip 文件"""
1501
+ project_files = list(self._iter_project_files())
1502
+ dependency_files = [file_path for file_path in self.deps_dir.rglob("*") if file_path.is_file()]
1503
+ bundled_source_files = list(self._iter_bundled_source_files())
1504
+ dependency_size = sum(file_path.stat().st_size for file_path in dependency_files)
1505
+ if dependency_files:
1506
+ click.echo(
1507
+ f" 打包输入: {len(project_files)} 个项目文件 + "
1508
+ f"{len(dependency_files)} 个依赖文件 ({dependency_size / (1024 * 1024):.1f} MB) + "
1509
+ f"{len(bundled_source_files)} 个 runtime 文件"
1510
+ )
1511
+
1512
+ with zipfile.ZipFile(zip_path, 'w', zipfile.ZIP_DEFLATED) as zf:
1513
+ # 添加项目文件
1514
+ for file_path in project_files:
1515
+ arcname = file_path.relative_to(self.project_dir).as_posix()
1516
+ zf.write(file_path, arcname)
1517
+
1518
+ # 添加依赖
1519
+ deps_count = 0
1520
+ for file_path in dependency_files:
1521
+ arcname = file_path.relative_to(self.deps_dir).as_posix()
1522
+ zf.write(file_path, arcname)
1523
+ deps_count += 1
1524
+ self._emit_package_progress("打包依赖", deps_count, len(dependency_files))
1525
+ self._finish_package_progress()
1526
+
1527
+ # 添加随运行时下发的 ksadk 源码
1528
+ bundled_source_count = 0
1529
+ for package_name, relative, file_path in bundled_source_files:
1530
+ arcname = f"{package_name}/{relative}"
1531
+ zf.write(file_path, arcname)
1532
+ bundled_source_count += 1
1533
+ self._emit_package_progress(
1534
+ "打包 runtime",
1535
+ bundled_source_count,
1536
+ len(bundled_source_files),
1537
+ )
1538
+ self._finish_package_progress()
1539
+
1540
+ click.echo(f" ✓ 打包运行时源码: {bundled_source_count} 个文件")
1541
+
1542
+ # 添加 entrypoint
1543
+ entrypoint_content = self._generate_entrypoint(detection_result)
1544
+ zf.writestr("entrypoint.py", entrypoint_content)
1545
+
1546
+ click.echo(f" ✓ 打包完成: {len(project_files)} 个项目文件 + {deps_count} 个依赖文件")
1547
+ self._emit_package_size_report(zip_path)
1548
+
1549
+ def _emit_package_size_report(self, zip_path: Path, *, limit: int = 8) -> None:
1550
+ try:
1551
+ with zipfile.ZipFile(zip_path, "r") as zf:
1552
+ infos = zf.infolist()
1553
+ except Exception:
1554
+ return
1555
+
1556
+ if not infos:
1557
+ return
1558
+
1559
+ by_top_level: dict[str, int] = {}
1560
+ for item in infos:
1561
+ name = item.filename.strip("/")
1562
+ if not name:
1563
+ continue
1564
+ top_level = name.split("/", 1)[0]
1565
+ by_top_level[top_level] = by_top_level.get(top_level, 0) + item.file_size
1566
+
1567
+ self._emit_package_size_report_from_entries(
1568
+ raw_total=sum(item.file_size for item in infos),
1569
+ compressed_total=sum(item.compress_size for item in infos),
1570
+ by_top_level=by_top_level,
1571
+ limit=limit,
1572
+ )
1573
+
1574
+ def _emit_package_size_report_from_entries(
1575
+ self,
1576
+ *,
1577
+ raw_total: int,
1578
+ compressed_total: int,
1579
+ by_top_level: dict[str, int],
1580
+ limit: int = 8,
1581
+ ) -> None:
1582
+ click.echo(
1583
+ " 包体积: "
1584
+ f"zip {compressed_total / (1024 * 1024):.1f} MB / "
1585
+ f"解压 {raw_total / (1024 * 1024):.1f} MB"
1586
+ )
1587
+ if by_top_level:
1588
+ top_items = sorted(by_top_level.items(), key=lambda item: item[1], reverse=True)[:limit]
1589
+ summary = ", ".join(
1590
+ f"{name} {size / (1024 * 1024):.1f} MB"
1591
+ for name, size in top_items
1592
+ )
1593
+ click.echo(f" 体积 Top{len(top_items)}: {summary}")
1594
+ if (
1595
+ raw_total > self.CONTAINER_SUGGESTION_RAW_THRESHOLD_BYTES
1596
+ or compressed_total > self.CONTAINER_SUGGESTION_ZIP_THRESHOLD_BYTES
1597
+ ):
1598
+ click.secho(
1599
+ " ⚠ 包体积较大,建议使用 container 模式构建并复用镜像层: "
1600
+ "agentengine build . --mode container --push --registry <registry>",
1601
+ fg="yellow",
1602
+ )
1603
+
1604
+ def _emit_package_progress(self, label: str, current: int, total: int) -> None:
1605
+ if total < 500:
1606
+ return
1607
+
1608
+ percent = max(0, min(100, int((current / total) * 100)))
1609
+ if not self._should_emit_package_progress(label, percent, current, total):
1610
+ return
1611
+
1612
+ line = self._render_package_progress(label, percent, current, total)
1613
+ if line == self._package_progress_last_line:
1614
+ return
1615
+
1616
+ padding = " " * max(0, self._package_progress_width - len(line))
1617
+ if sys.stdout.isatty():
1618
+ click.echo(f"\r{line}{padding}", nl=False)
1619
+ self._package_progress_width = len(line)
1620
+ else:
1621
+ click.echo(line)
1622
+ self._package_progress_width = 0
1623
+ self._package_progress_last_line = line
1624
+
1625
+ def _should_emit_package_progress(
1626
+ self,
1627
+ label: str,
1628
+ percent: int,
1629
+ current: int,
1630
+ total: int,
1631
+ ) -> bool:
1632
+ if sys.stdout.isatty():
1633
+ previous = self._package_progress_last_percent_by_label.get(label, -1)
1634
+ if current == total or percent > previous:
1635
+ self._package_progress_last_percent_by_label[label] = percent
1636
+ return True
1637
+ return False
1638
+
1639
+ previous_milestone = self._package_progress_logged_milestone_by_label.get(label, 0)
1640
+ for milestone in self.PACKAGE_PROGRESS_LOG_MILESTONES:
1641
+ if previous_milestone < milestone <= percent:
1642
+ self._package_progress_logged_milestone_by_label[label] = milestone
1643
+ return True
1644
+ return False
1645
+
1646
+ def _render_package_progress(self, label: str, percent: int, current: int, total: int) -> str:
1647
+ filled = round((percent / 100) * self.PACKAGE_PROGRESS_BAR_WIDTH)
1648
+ if filled <= 0:
1649
+ bar = "." * self.PACKAGE_PROGRESS_BAR_WIDTH
1650
+ elif filled >= self.PACKAGE_PROGRESS_BAR_WIDTH:
1651
+ bar = "=" * self.PACKAGE_PROGRESS_BAR_WIDTH
1652
+ else:
1653
+ bar = "=" * (filled - 1) + ">" + "." * (self.PACKAGE_PROGRESS_BAR_WIDTH - filled)
1654
+ return f" [{bar}] {percent:>3}% {label} | {current}/{total} files"
1655
+
1656
+ def _finish_package_progress(self) -> None:
1657
+ if self._package_progress_width and sys.stdout.isatty():
1658
+ click.echo()
1659
+ self._package_progress_width = 0
1660
+ self._package_progress_last_line = ""
1661
+ self._package_progress_last_percent_by_label = {}
1662
+ self._package_progress_logged_milestone_by_label = {}
1663
+
1664
+ def _generate_entrypoint(self, detection_result) -> str:
1665
+ """生成 entrypoint.py"""
1666
+ package_name = Path(detection_result.package_path).name
1667
+ return f'''"""
1668
+ AgentEngine Code 模式入口
1669
+
1670
+ zip 包结构:
1671
+ - entrypoint.py (本文件)
1672
+ - {package_name}/ (Agent 代码)
1673
+ - ksadk/ (ksadk 源码)
1674
+ - fastapi/, uvicorn/, pydantic/ 等 (Linux 版依赖)
1675
+ """
1676
+
1677
+ import sys
1678
+ import os
1679
+ import logging
1680
+ from pathlib import Path
1681
+
1682
+ # ========== 日志配置 ==========
1683
+ # 通过环境变量 LOG_LEVEL 控制日志级别 (DEBUG, INFO, WARNING, ERROR)
1684
+ LOG_LEVEL = os.environ.get("LOG_LEVEL", "INFO").upper()
1685
+
1686
+ # 配置根日志
1687
+ logging.basicConfig(
1688
+ level=getattr(logging, LOG_LEVEL, logging.INFO),
1689
+ format="%(asctime)s | %(levelname)-8s | %(name)s | %(message)s",
1690
+ datefmt="%Y-%m-%d %H:%M:%S",
1691
+ stream=sys.stdout,
1692
+ force=True, # 覆盖已有配置
1693
+ )
1694
+
1695
+ logger = logging.getLogger("entrypoint")
1696
+ logger.info(f"日志级别: {{LOG_LEVEL}}")
1697
+
1698
+ # 配置第三方库日志级别
1699
+ if LOG_LEVEL == "DEBUG":
1700
+ # DEBUG 模式下显示所有日志
1701
+ logging.getLogger("langchain").setLevel(logging.DEBUG)
1702
+ logging.getLogger("langgraph").setLevel(logging.DEBUG)
1703
+ logging.getLogger("httpx").setLevel(logging.DEBUG)
1704
+ logging.getLogger("opentelemetry").setLevel(logging.DEBUG)
1705
+ else:
1706
+ # 非 DEBUG 模式下减少第三方库噪音
1707
+ logging.getLogger("httpx").setLevel(logging.WARNING)
1708
+ logging.getLogger("httpcore").setLevel(logging.WARNING)
1709
+
1710
+ # LangChain 调试模式 (默认开启, 会打印完整的 prompt 和 LLM 调用信息)
1711
+ # 设置 LANGCHAIN_VERBOSE=false 可关闭
1712
+ if os.environ.get("LANGCHAIN_VERBOSE", "true").lower() not in ("false", "0"):
1713
+ try:
1714
+ from langchain.globals import set_verbose, set_debug
1715
+ set_verbose(True)
1716
+ set_debug(True)
1717
+ logger.info("LangChain 调试模式已启用")
1718
+ except ImportError:
1719
+ pass
1720
+
1721
+ # ========== 路径设置 ==========
1722
+ CODE_ROOT = os.environ.get("CODE_PATH", "/app/code")
1723
+ sys.path.insert(0, CODE_ROOT)
1724
+ CODE_SRC = os.path.join(CODE_ROOT, "src")
1725
+ if os.path.isdir(CODE_SRC):
1726
+ sys.path.insert(0, CODE_SRC)
1727
+ os.chdir(CODE_ROOT)
1728
+
1729
+ # 打印启动信息
1730
+ logger.info("=" * 60)
1731
+ logger.info("AgentEngine 启动")
1732
+ logger.info("=" * 60)
1733
+ logger.info(f"CODE_ROOT: {{CODE_ROOT}}")
1734
+ logger.info(f"Python: {{sys.version}}")
1735
+ logger.info(f"PYTHONPATH: {{os.environ.get('PYTHONPATH', 'N/A')}}")
1736
+
1737
+ # 打印关键环境变量 (隐藏敏感信息)
1738
+ env_keys = [
1739
+ "AGENT_RUNTIME_NAME", "AGENT_RUNTIME_ID", "ACCOUNT_ID", "PORT",
1740
+ "LANGFUSE_BASE_URL", "LANGFUSE_HOST", "LANGFUSE_PUBLIC_KEY",
1741
+ "LANGFUSE_SECRET_KEY", "LANGFUSE_USE_CALLBACK",
1742
+ "OTEL_EXPORTER_OTLP_ENDPOINT", "OTEL_EXPORTER_OTLP_TRACES_ENDPOINT",
1743
+ "OTEL_EXPORTER_OTLP_HEADERS", "OTEL_EXPORTER_OTLP_TRACES_HEADERS",
1744
+ "OTEL_SERVICE_NAME", "CLOUD_MONITOR_APP_KEY",
1745
+ "CLOUD_MONITOR_OTLP_ENABLED", "CLOUD_MONITOR_OTLP_ENDPOINT",
1746
+ "CLOUD_MONITOR_OTLP_TRACES_ENDPOINT", "CLOUD_MONITOR_OTLP_HEADERS",
1747
+ "CLOUD_MONITOR_LANGFUSE_HOST", "CLOUD_MONITOR_LANGFUSE_PUBLIC_KEY",
1748
+ "CLOUD_MONITOR_LANGFUSE_SECRET_KEY", "CLOUD_MONITOR_LANGFUSE_ENABLED",
1749
+ "LANGCHAIN_TRACING_V2", "MODEL_NAME",
1750
+ ]
1751
+ for key in env_keys:
1752
+ value = os.environ.get(key)
1753
+ if value:
1754
+ # 隐藏敏感值
1755
+ if "KEY" in key or "SECRET" in key or "HEADERS" in key:
1756
+ value = value[:8] + "****" if len(value) > 8 else "****"
1757
+ logger.info(f" {{key}}: {{value}}")
1758
+
1759
+ logger.info("=" * 60)
1760
+
1761
+ # ========== 加载 Agent ==========
1762
+ from ksadk.configs import setup_environment
1763
+ setup_environment(Path(CODE_ROOT))
1764
+
1765
+ try:
1766
+ from ksadk.runners.patch_langchain import apply_patch as apply_langchain_patch
1767
+ apply_langchain_patch()
1768
+ except ImportError:
1769
+ pass
1770
+
1771
+ from ksadk.runners import create_runner
1772
+ from ksadk.detection import DetectionResult, FrameworkType
1773
+ from ksadk.server import app, set_runner
1774
+ import uvicorn
1775
+
1776
+ # 检测结果 (构建时固化)
1777
+ detection_result = DetectionResult(
1778
+ type=FrameworkType.{detection_result.type.name},
1779
+ name="{detection_result.name}",
1780
+ entry_point="{detection_result.entry_point}",
1781
+ package_path=os.path.join(CODE_ROOT, "{package_name}"),
1782
+ agent_variable="{detection_result.agent_variable}",
1783
+ runner_class="{getattr(detection_result, 'runner_class', '')}"
1784
+ )
1785
+
1786
+ logger.info(f"框架: {{detection_result.name}}")
1787
+ logger.info(f"入口: {{detection_result.entry_point}}")
1788
+
1789
+ # 初始化 Tracing (如果配置了 Langfuse、标准 OTLP HTTP 或 CloudMonitor OTLP)
1790
+ has_otlp = bool(os.environ.get("OTEL_EXPORTER_OTLP_ENDPOINT") or os.environ.get("OTEL_EXPORTER_OTLP_TRACES_ENDPOINT"))
1791
+ has_cloud_monitor_otlp = bool(
1792
+ os.environ.get("CLOUD_MONITOR_APP_KEY")
1793
+ or os.environ.get("CLOUD_MONITOR_OTLP_ENDPOINT")
1794
+ or os.environ.get("CLOUD_MONITOR_OTLP_TRACES_ENDPOINT")
1795
+ )
1796
+ has_cloud_monitor_langfuse = bool(
1797
+ os.environ.get("CLOUD_MONITOR_LANGFUSE_PUBLIC_KEY")
1798
+ or os.environ.get("CLOUD_MONITOR_LANGFUSE_SECRET_KEY")
1799
+ or os.environ.get("CLOUD_MONITOR_LANGFUSE_HOST")
1800
+ )
1801
+ if os.environ.get("LANGFUSE_PUBLIC_KEY") or has_otlp or has_cloud_monitor_otlp or has_cloud_monitor_langfuse:
1802
+ try:
1803
+ from ksadk.tracing import setup_tracing
1804
+
1805
+ use_callback_only = os.environ.get("LANGFUSE_USE_CALLBACK", "").strip().lower() in ("1", "true", "yes", "on")
1806
+
1807
+ setup_tracing(use_callback_only=use_callback_only)
1808
+ logger.info(
1809
+ f"Tracing 已启用 (OTLP={{has_otlp}}, "
1810
+ f"CloudMonitorOTLP={{has_cloud_monitor_otlp}}, "
1811
+ f"CloudMonitorLangfuse={{has_cloud_monitor_langfuse}}, "
1812
+ f"CallbackOnly={{use_callback_only}})"
1813
+ )
1814
+ except Exception as e:
1815
+ logger.warning(f"Tracing 初始化失败: {{e}}")
1816
+
1817
+ # 创建 Runner 并加载 Agent
1818
+ logger.info("正在加载 Agent...")
1819
+ runner = create_runner(detection_result, CODE_ROOT)
1820
+ runner.load_agent()
1821
+ set_runner(runner)
1822
+ logger.info("Agent 加载成功!")
1823
+
1824
+ if __name__ == "__main__":
1825
+ port = int(os.environ.get("PORT", 8080))
1826
+ logger.info(f"启动 HTTP Server: 0.0.0.0:{{port}}")
1827
+ uvicorn.run(app, host="0.0.0.0", port=port, log_level=LOG_LEVEL.lower())
1828
+ '''
1829
+
1830
+ def _pip_index_candidates(self) -> List[Optional[str]]:
1831
+ """返回 pip index 尝试顺序。显式环境变量优先,其次是官方与内置镜像源。"""
1832
+ if self._pip_index_candidates_cache is not None:
1833
+ return list(self._pip_index_candidates_cache)
1834
+
1835
+ env_index = os.environ.get("PIP_INDEX_URL") or os.environ.get("UV_INDEX_URL")
1836
+ if env_index:
1837
+ candidates: list[Optional[str]] = [None]
1838
+ seen = {self._normalize_pip_index_url(env_index)}
1839
+ for candidate in self.PIP_INDEX_FALLBACKS:
1840
+ normalized = self._normalize_pip_index_url(candidate)
1841
+ if normalized in seen:
1842
+ continue
1843
+ seen.add(normalized)
1844
+ candidates.append(normalized)
1845
+ self._pip_index_selection_summary = (
1846
+ f"使用显式依赖源 {self._short_pip_index_label(env_index)}"
1847
+ )
1848
+ self._pip_index_candidates_cache = candidates
1849
+ return list(candidates)
1850
+
1851
+ seen: Set[str] = set()
1852
+ urls: list[str] = []
1853
+ for candidate in self.PIP_INDEX_FALLBACKS:
1854
+ normalized = self._normalize_pip_index_url(candidate)
1855
+ if normalized in seen:
1856
+ continue
1857
+ seen.add(normalized)
1858
+ urls.append(normalized)
1859
+
1860
+ ordered, summary = self._rank_pip_index_urls(urls)
1861
+ self._pip_index_selection_summary = summary
1862
+ self._pip_index_candidates_cache = list(ordered)
1863
+ return list(self._pip_index_candidates_cache)
1864
+
1865
+ def _pip_index_label(self, index_url: Optional[str]) -> str:
1866
+ if index_url:
1867
+ return self._short_pip_index_label(index_url)
1868
+ env_index = os.environ.get("PIP_INDEX_URL") or os.environ.get("UV_INDEX_URL")
1869
+ if env_index:
1870
+ return f"default pip index ({self._short_pip_index_label(env_index)})"
1871
+ return "default pip index"
1872
+
1873
+ def _run_pip_install(self, requirements_path: Path, *, target_runtime_wheels: bool):
1874
+ index_candidates = self._pip_index_candidates()
1875
+ if self._pip_index_selection_summary:
1876
+ self._emit_install_progress(
1877
+ 6 if target_runtime_wheels else 11,
1878
+ "选择依赖源",
1879
+ self._pip_index_selection_summary,
1880
+ )
1881
+ result = None
1882
+ for index_url in index_candidates:
1883
+ source_label = self._pip_index_label(index_url)
1884
+ install_mode = "目标运行时 wheel" if target_runtime_wheels else "宿主机依赖"
1885
+ self._emit_install_progress(
1886
+ 8 if target_runtime_wheels else 12,
1887
+ "准备安装",
1888
+ f"{install_mode},源 {source_label}",
1889
+ )
1890
+ install_cmd = [
1891
+ sys.executable,
1892
+ "-m",
1893
+ "pip",
1894
+ "install",
1895
+ "-r",
1896
+ str(requirements_path),
1897
+ "-t",
1898
+ str(self.deps_dir),
1899
+ "--disable-pip-version-check",
1900
+ "--no-warn-script-location",
1901
+ "--retries",
1902
+ "3",
1903
+ "--timeout",
1904
+ "60",
1905
+ "--cache-dir",
1906
+ str(self.pip_cache_dir),
1907
+ ]
1908
+ if target_runtime_wheels:
1909
+ for platform in self.TARGET_INSTALL_PLATFORMS:
1910
+ install_cmd += ["--platform", platform]
1911
+ install_cmd += [
1912
+ "--python-version",
1913
+ self.TARGET_PYTHON_VERSION,
1914
+ "--implementation",
1915
+ "cp",
1916
+ "--only-binary=:all:",
1917
+ ]
1918
+ if index_url:
1919
+ install_cmd += ["-i", index_url]
1920
+ result = self._run_streamed_pip_install(
1921
+ install_cmd,
1922
+ timeout=self._pip_install_timeout_seconds(),
1923
+ )
1924
+ if result.returncode != 0 and self._bootstrap_pip_if_missing(result):
1925
+ result = self._run_streamed_pip_install(
1926
+ install_cmd,
1927
+ timeout=self._pip_install_timeout_seconds(),
1928
+ )
1929
+ if result.returncode == 0:
1930
+ break
1931
+ self._emit_install_progress(
1932
+ 22 if target_runtime_wheels else 18,
1933
+ "切换依赖源",
1934
+ f"{source_label} 失败:{self._result_error_summary(result)}",
1935
+ )
1936
+ return result
1937
+
1938
+ def _run_streamed_pip_install(
1939
+ self,
1940
+ install_cmd: list[str],
1941
+ *,
1942
+ timeout: int,
1943
+ ) -> subprocess.CompletedProcess[str]:
1944
+ output_lines: list[str] = []
1945
+ self._install_progress_event_counts = {}
1946
+ self._install_progress_started_at = time.monotonic()
1947
+ process = subprocess.Popen(
1948
+ install_cmd,
1949
+ stdout=subprocess.PIPE,
1950
+ stderr=subprocess.STDOUT,
1951
+ text=True,
1952
+ bufsize=1,
1953
+ )
1954
+
1955
+ def _consume_output() -> None:
1956
+ if process.stdout is None:
1957
+ return
1958
+ try:
1959
+ for raw_line in process.stdout:
1960
+ output_lines.append(raw_line)
1961
+ progress = self._install_progress_stage(raw_line)
1962
+ if progress:
1963
+ percent, stage = progress
1964
+ summary = self._summarize_pip_progress_line(percent, stage, raw_line)
1965
+ if summary:
1966
+ adjusted_percent = self._adjust_install_progress_percent(percent, stage)
1967
+ self._emit_install_progress(adjusted_percent, stage, summary)
1968
+ except ValueError:
1969
+ return
1970
+
1971
+ reader = threading.Thread(target=_consume_output, daemon=True)
1972
+ reader.start()
1973
+ try:
1974
+ returncode = process.wait(timeout=timeout)
1975
+ except subprocess.TimeoutExpired:
1976
+ process.kill()
1977
+ reader.join(timeout=1)
1978
+ raise
1979
+
1980
+ reader.join(timeout=1)
1981
+ if process.stdout is not None:
1982
+ process.stdout.close()
1983
+ stdout = "".join(output_lines)
1984
+ stderr = stdout if returncode != 0 else ""
1985
+ return subprocess.CompletedProcess(
1986
+ install_cmd,
1987
+ returncode,
1988
+ stdout,
1989
+ stderr,
1990
+ )