agentengine-sdk-python 0.6.2__py3-none-any.whl → 0.6.9__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- agentengine_sdk_python-0.6.9.dist-info/METADATA +179 -0
- agentengine_sdk_python-0.6.9.dist-info/RECORD +487 -0
- {agentengine_sdk_python-0.6.2.dist-info → agentengine_sdk_python-0.6.9.dist-info}/WHEEL +1 -1
- agentengine_sdk_python-0.6.9.dist-info/entry_points.txt +3 -0
- agentengine_sdk_python-0.6.9.dist-info/licenses/LICENSE +175 -0
- agentengine_sdk_python-0.6.9.dist-info/top_level.txt +2 -0
- ksadk/__init__.py +13 -0
- ksadk/__main__.py +4 -0
- ksadk/a2a/__init__.py +15 -0
- ksadk/a2a/card_builder.py +64 -0
- ksadk/a2a/client.py +309 -0
- ksadk/a2a/executor.py +201 -0
- ksadk/a2a/server.py +79 -0
- ksadk/agents/__init__.py +20 -0
- ksadk/agents/base.py +370 -0
- ksadk/agents/context.py +38 -0
- ksadk/agents/event.py +35 -0
- ksadk/agents/loop_agent.py +71 -0
- ksadk/agents/parallel_agent.py +109 -0
- ksadk/agents/runner_adapter.py +24 -0
- ksadk/agents/sequential_agent.py +34 -0
- ksadk/api/__init__.py +3 -0
- ksadk/api/client.py +2227 -0
- ksadk/builders/__init__.py +20 -0
- ksadk/builders/base.py +66 -0
- ksadk/builders/code_builder.py +1990 -0
- ksadk/builders/container_builder.py +786 -0
- ksadk/builders/framework_requirements.py +59 -0
- ksadk/builders/ks3_uploader.py +389 -0
- ksadk/builders/mcp_builder.py +476 -0
- ksadk/builders/requirements_utils.py +75 -0
- ksadk/cli/__init__.py +531 -0
- ksadk/cli/__main__.py +4 -0
- ksadk/cli/agent_ref.py +204 -0
- ksadk/cli/cmd_a2a.py +167 -0
- ksadk/cli/cmd_agent.py +201 -0
- ksadk/cli/cmd_build.py +270 -0
- ksadk/cli/cmd_completion.py +304 -0
- ksadk/cli/cmd_config.py +758 -0
- ksadk/cli/cmd_create.py +2277 -0
- ksadk/cli/cmd_dashboard.py +900 -0
- ksadk/cli/cmd_deploy.py +704 -0
- ksadk/cli/cmd_destroy.py +345 -0
- ksadk/cli/cmd_files.py +1227 -0
- ksadk/cli/cmd_hermes.py +1259 -0
- ksadk/cli/cmd_invoke.py +1336 -0
- ksadk/cli/cmd_launch.py +564 -0
- ksadk/cli/cmd_mcp.py +1174 -0
- ksadk/cli/cmd_model.py +302 -0
- ksadk/cli/cmd_openclaw.py +3918 -0
- ksadk/cli/cmd_run.py +232 -0
- ksadk/cli/cmd_status.py +588 -0
- ksadk/cli/cmd_version.py +522 -0
- ksadk/cli/cmd_web.py +288 -0
- ksadk/cli/deploy_utils.py +87 -0
- ksadk/cli/dry_run.py +214 -0
- ksadk/cli/env_options.py +82 -0
- ksadk/cli/error_utils.py +525 -0
- ksadk/cli/global_options.py +31 -0
- ksadk/cli/local_runtime.py +97 -0
- ksadk/cli/model_catalog.py +136 -0
- ksadk/cli/network_options.py +410 -0
- ksadk/cli/resource_common.py +621 -0
- ksadk/cli/storage.py +84 -0
- ksadk/cli/ui.py +431 -0
- ksadk/cli/workflow_common.py +552 -0
- ksadk/common/__init__.py +14 -0
- ksadk/common/aicp_env.py +128 -0
- ksadk/common/auth.py +190 -0
- ksadk/common/constants.py +84 -0
- ksadk/common/llm_utils.py +61 -0
- ksadk/configs/__init__.py +61 -0
- ksadk/configs/env_registry.py +342 -0
- ksadk/configs/global_config.py +200 -0
- ksadk/configs/settings.py +706 -0
- ksadk/conversations/__init__.py +95 -0
- ksadk/conversations/attachment_storage.py +379 -0
- ksadk/conversations/attachments.py +774 -0
- ksadk/conversations/compaction_pipeline.py +387 -0
- ksadk/conversations/compaction_prompt.py +128 -0
- ksadk/conversations/context.py +303 -0
- ksadk/conversations/model_context.py +360 -0
- ksadk/conversations/model_options.py +136 -0
- ksadk/conversations/normalize.py +446 -0
- ksadk/conversations/reasoning_markup.py +91 -0
- ksadk/conversations/run_kinds.py +61 -0
- ksadk/conversations/run_status.py +53 -0
- ksadk/conversations/runtime.py +5208 -0
- ksadk/conversations/semantic_summary.py +359 -0
- ksadk/conversations/session_title.py +254 -0
- ksadk/deployment/__init__.py +83 -0
- ksadk/deployment/agent_access.py +226 -0
- ksadk/deployment/base.py +229 -0
- ksadk/deployment/manager.py +332 -0
- ksadk/deployment/providers/__init__.py +10 -0
- ksadk/deployment/providers/serverless.py +1105 -0
- ksadk/deployment/registry.py +120 -0
- ksadk/deployment/state.py +78 -0
- ksadk/deployment/ui_config.py +21 -0
- ksadk/detection/__init__.py +7 -0
- ksadk/detection/detector.py +431 -0
- ksadk/detection/mcp_detector.py +250 -0
- ksadk/hermes_terminal.py +467 -0
- ksadk/identity/__init__.py +17 -0
- ksadk/identity/resolver.py +385 -0
- ksadk/knowledge_base/__init__.py +62 -0
- ksadk/knowledge_base/adk_tool.py +47 -0
- ksadk/knowledge_base/client.py +297 -0
- ksadk/knowledge_base/langchain_tool.py +55 -0
- ksadk/knowledge_base/service.py +66 -0
- ksadk/knowledge_base/tool.py +50 -0
- ksadk/markdown.py +169 -0
- ksadk/mcp_runtime/__init__.py +231 -0
- ksadk/memory/__init__.py +57 -0
- ksadk/memory/adk/__init__.py +51 -0
- ksadk/memory/adk/backends/__init__.py +5 -0
- ksadk/memory/adk/backends/base_ltm_backend.py +52 -0
- ksadk/memory/adk/backends/http_ltm_backend.py +183 -0
- ksadk/memory/adk/backends/inmemory_ltm_backend.py +96 -0
- ksadk/memory/adk/backends/sdk_ltm_backend.py +456 -0
- ksadk/memory/adk/long_term_memory.py +273 -0
- ksadk/memory/adk/short_term_memory.py +289 -0
- ksadk/memory/adk_tool.py +31 -0
- ksadk/memory/backends/__init__.py +6 -0
- ksadk/memory/backends/base.py +103 -0
- ksadk/memory/backends/memory.py +112 -0
- ksadk/memory/backends/redis.py +146 -0
- ksadk/memory/langchain_tool.py +39 -0
- ksadk/memory/ltm_backend_factory.py +24 -0
- ksadk/memory/manager.py +215 -0
- ksadk/memory/service.py +172 -0
- ksadk/memory/tool.py +96 -0
- ksadk/model_policy.py +223 -0
- ksadk/openclaw_gateway.py +378 -0
- ksadk/runners/__init__.py +8 -0
- ksadk/runners/adk_runner.py +1135 -0
- ksadk/runners/base_runner.py +305 -0
- ksadk/runners/deepagents_runner.py +13 -0
- ksadk/runners/factory.py +73 -0
- ksadk/runners/langchain_runner.py +539 -0
- ksadk/runners/langgraph_runner.py +852 -0
- ksadk/runners/patch_langchain.py +187 -0
- ksadk/runners/remote_runner.py +667 -0
- ksadk/runners/usage_accumulator.py +38 -0
- ksadk/runners/utils/__init__.py +19 -0
- ksadk/runners/utils/langfuse.py +230 -0
- ksadk/runners/utils/loader.py +62 -0
- ksadk/runtime_context.py +166 -0
- ksadk/runtime_state.py +25 -0
- ksadk/sandbox/__init__.py +28 -0
- ksadk/sandbox/backends/__init__.py +9 -0
- ksadk/sandbox/backends/e2b.py +154 -0
- ksadk/sandbox/backends/local_process.py +131 -0
- ksadk/sandbox/base.py +101 -0
- ksadk/sandbox/factory.py +53 -0
- ksadk/sandbox/registry.py +187 -0
- ksadk/server/__init__.py +7 -0
- ksadk/server/api_models.py +73 -0
- ksadk/server/app.py +3789 -0
- ksadk/server/static/assets/ArtifactsPanel-C0meuOdi.js +1 -0
- ksadk/server/static/assets/CodeBlock-BbI4HPtJ.js +9 -0
- ksadk/server/static/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
- ksadk/server/static/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
- ksadk/server/static/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
- ksadk/server/static/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
- ksadk/server/static/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
- ksadk/server/static/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
- ksadk/server/static/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
- ksadk/server/static/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
- ksadk/server/static/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
- ksadk/server/static/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
- ksadk/server/static/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
- ksadk/server/static/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
- ksadk/server/static/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
- ksadk/server/static/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
- ksadk/server/static/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
- ksadk/server/static/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
- ksadk/server/static/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
- ksadk/server/static/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
- ksadk/server/static/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
- ksadk/server/static/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
- ksadk/server/static/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
- ksadk/server/static/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
- ksadk/server/static/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
- ksadk/server/static/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
- ksadk/server/static/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
- ksadk/server/static/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
- ksadk/server/static/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
- ksadk/server/static/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
- ksadk/server/static/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
- ksadk/server/static/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
- ksadk/server/static/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
- ksadk/server/static/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
- ksadk/server/static/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
- ksadk/server/static/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
- ksadk/server/static/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
- ksadk/server/static/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
- ksadk/server/static/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
- ksadk/server/static/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
- ksadk/server/static/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
- ksadk/server/static/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
- ksadk/server/static/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
- ksadk/server/static/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
- ksadk/server/static/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
- ksadk/server/static/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
- ksadk/server/static/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
- ksadk/server/static/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
- ksadk/server/static/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
- ksadk/server/static/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
- ksadk/server/static/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
- ksadk/server/static/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
- ksadk/server/static/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
- ksadk/server/static/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
- ksadk/server/static/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
- ksadk/server/static/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
- ksadk/server/static/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
- ksadk/server/static/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
- ksadk/server/static/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
- ksadk/server/static/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
- ksadk/server/static/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
- ksadk/server/static/assets/MathMessageMarkdown-DMLQYoLp.js +8 -0
- ksadk/server/static/assets/MathMessageMarkdown-DeYh8QPr.css +1 -0
- ksadk/server/static/assets/MermaidBlock-DCR8WZ17.js +303 -0
- ksadk/server/static/assets/NativeTerminalPanel-CHOd7Rch.css +1 -0
- ksadk/server/static/assets/NativeTerminalPanel-CKUvWhUz.js +1 -0
- ksadk/server/static/assets/addon-fit-DthTIhi3.js +1 -0
- ksadk/server/static/assets/apl-CnwPGSsG.js +1 -0
- ksadk/server/static/assets/arc-bj2rZnP5.js +1 -0
- ksadk/server/static/assets/architecture-7EHR7CIX-Cr6Xz1_c.js +1 -0
- ksadk/server/static/assets/architectureDiagram-3BPJPVTR-2u1ltDIj.js +36 -0
- ksadk/server/static/assets/asciiarmor-qTkVPQu6.js +1 -0
- ksadk/server/static/assets/asn1-Dr8qZg38.js +1 -0
- ksadk/server/static/assets/asterisk-Bja0s6e1.js +1 -0
- ksadk/server/static/assets/blockDiagram-GPEHLZMM-D6fvdK20.js +132 -0
- ksadk/server/static/assets/brainfuck-D5EjA2JK.js +1 -0
- ksadk/server/static/assets/c4Diagram-AAUBKEIU-BK82L5n5.js +10 -0
- ksadk/server/static/assets/channel-BSSOwFIb.js +1 -0
- ksadk/server/static/assets/chunk-2J33WTMH-DLVdkeyQ.js +1 -0
- ksadk/server/static/assets/chunk-4BX2VUAB-BbwsYcbW.js +1 -0
- ksadk/server/static/assets/chunk-55IACEB6-DOairxzw.js +1 -0
- ksadk/server/static/assets/chunk-727SXJPM-hCO487gs.js +206 -0
- ksadk/server/static/assets/chunk-AQP2D5EJ-XU8vDOuG.js +231 -0
- ksadk/server/static/assets/chunk-FMBD7UC4-PrapyfMu.js +15 -0
- ksadk/server/static/assets/chunk-ND2GUHAM-CboKhTZ3.js +1 -0
- ksadk/server/static/assets/chunk-QZHKN3VN-XXpdiwUL.js +1 -0
- ksadk/server/static/assets/classDiagram-4FO5ZUOK-C5HvGiyK.js +1 -0
- ksadk/server/static/assets/classDiagram-v2-Q7XG4LA2-C5HvGiyK.js +1 -0
- ksadk/server/static/assets/clike-DHH8Ad3s.js +1 -0
- ksadk/server/static/assets/clojure-BflJGmDX.js +1 -0
- ksadk/server/static/assets/cmake-CyVbuzPu.js +1 -0
- ksadk/server/static/assets/cobol-B_OZ4V-R.js +1 -0
- ksadk/server/static/assets/coffeescript-B2pKbFk8.js +1 -0
- ksadk/server/static/assets/commonlisp-CcllspGY.js +1 -0
- ksadk/server/static/assets/cose-bilkent-S5V4N54A-_N8bJ-EU.js +1 -0
- ksadk/server/static/assets/crystal-M5-qzICs.js +1 -0
- ksadk/server/static/assets/css-CYpP4FRV.js +1 -0
- ksadk/server/static/assets/cypher-p9eYesGn.js +1 -0
- ksadk/server/static/assets/cytoscape.esm-DelgaX4f.js +321 -0
- ksadk/server/static/assets/d-BhMjBjQP.js +1 -0
- ksadk/server/static/assets/dagre-BM42HDAG-FoDlo5J2.js +4 -0
- ksadk/server/static/assets/dagre-Bx709z4p.js +1 -0
- ksadk/server/static/assets/defaultLocale-C8Fc0cco.js +1 -0
- ksadk/server/static/assets/diagram-2AECGRRQ-BBp6ebBw.js +43 -0
- ksadk/server/static/assets/diagram-5GNKFQAL-CqOvG1PR.js +10 -0
- ksadk/server/static/assets/diagram-KO2AKTUF-O9T3t6ba.js +3 -0
- ksadk/server/static/assets/diagram-LMA3HP47-CGG8ekBO.js +24 -0
- ksadk/server/static/assets/diagram-OG6HWLK6-4BmH1EEg.js +24 -0
- ksadk/server/static/assets/diff-ChtP43wD.js +1 -0
- ksadk/server/static/assets/dist-6kSZvCZh.js +1 -0
- ksadk/server/static/assets/dist-B67uF2uK.js +1 -0
- ksadk/server/static/assets/dist-BMKM66B4.js +1 -0
- ksadk/server/static/assets/dist-BVzcWa6D.js +1 -0
- ksadk/server/static/assets/dist-Bk1l05Ni.js +1 -0
- ksadk/server/static/assets/dist-BkJjNih3.js +1 -0
- ksadk/server/static/assets/dist-BmuTWcm7.js +9 -0
- ksadk/server/static/assets/dist-C2DKRFVK.js +1 -0
- ksadk/server/static/assets/dist-CWiTDN_V.js +6 -0
- ksadk/server/static/assets/dist-CZ6C3x8B.js +23 -0
- ksadk/server/static/assets/dist-CiZHB6ZW.js +1 -0
- ksadk/server/static/assets/dist-Cp1_jKwf.js +1 -0
- ksadk/server/static/assets/dist-Cwhk4lwI.js +2 -0
- ksadk/server/static/assets/dist-D5JGcxBx.js +11 -0
- ksadk/server/static/assets/dist-D99wf6_X.js +1 -0
- ksadk/server/static/assets/dist-DCpZrjaK.js +1 -0
- ksadk/server/static/assets/dist-DI-oVAXw.js +1 -0
- ksadk/server/static/assets/dist-DLMjL01k.js +1 -0
- ksadk/server/static/assets/dist-DLeM1OCK.js +1 -0
- ksadk/server/static/assets/dist-DM0rEQPk.js +1 -0
- ksadk/server/static/assets/dist-DbaH3p5h.js +1 -0
- ksadk/server/static/assets/dist-S4klkK5W.js +1 -0
- ksadk/server/static/assets/dockerfile-BLkNEvjs.js +1 -0
- ksadk/server/static/assets/dtd-DTF72YNO.js +1 -0
- ksadk/server/static/assets/dylan-C6jEEEk-.js +1 -0
- ksadk/server/static/assets/ebnf-D4c0_ac3.js +1 -0
- ksadk/server/static/assets/ecl-BSZaIHnp.js +1 -0
- ksadk/server/static/assets/eiffel-BEjRio4Q.js +1 -0
- ksadk/server/static/assets/elm-Cshzl8qu.js +1 -0
- ksadk/server/static/assets/erDiagram-TEJ5UH35-oJhaci7S.js +85 -0
- ksadk/server/static/assets/erlang-DaB2Rkuy.js +1 -0
- ksadk/server/static/assets/eventmodeling-FCH6USID-CIgwHJLL.js +1 -0
- ksadk/server/static/assets/factor-D7LVTn2l.js +1 -0
- ksadk/server/static/assets/fcl-ClOhbFR7.js +1 -0
- ksadk/server/static/assets/flowDiagram-I6XJVG4X-DtTmUan-.js +162 -0
- ksadk/server/static/assets/forth-Cezjo90N.js +1 -0
- ksadk/server/static/assets/fortran-Bt6PBEDR.js +1 -0
- ksadk/server/static/assets/ganttDiagram-6RSMTGT7-BSsb_eij.js +292 -0
- ksadk/server/static/assets/gas-BHEdbvp9.js +1 -0
- ksadk/server/static/assets/gherkin-oBAE_ms0.js +1 -0
- ksadk/server/static/assets/gitGraph-WXDBUCRP-BVAJ7uo0.js +1 -0
- ksadk/server/static/assets/gitGraphDiagram-PVQCEYII-BR5s6Pws.js +106 -0
- ksadk/server/static/assets/graphlib-B8gBHxth.js +1 -0
- ksadk/server/static/assets/groovy-BC3IOsC8.js +1 -0
- ksadk/server/static/assets/haskell-BRsoo5mP.js +1 -0
- ksadk/server/static/assets/haxe-DIz0ZZqd.js +1 -0
- ksadk/server/static/assets/http-CLVgA2GD.js +1 -0
- ksadk/server/static/assets/idl-BUZw3wgd.js +1 -0
- ksadk/server/static/assets/index-BH7-pC0B.js +165 -0
- ksadk/server/static/assets/index-DAURbCsP.css +1 -0
- ksadk/server/static/assets/info-J43DQDTF-MT80V4ge.js +1 -0
- ksadk/server/static/assets/infoDiagram-5YYISTIA-DYPc5cvh.js +2 -0
- ksadk/server/static/assets/init-D6jRqBbL.js +1 -0
- ksadk/server/static/assets/ishikawaDiagram-YF4QCWOH-MoBdALDg.js +70 -0
- ksadk/server/static/assets/javascript-9Tg8ixDm.js +1 -0
- ksadk/server/static/assets/journeyDiagram-JHISSGLW-DgLiQT8l.js +139 -0
- ksadk/server/static/assets/julia-CQfgDRfP.js +1 -0
- ksadk/server/static/assets/kanban-definition-UN3LZRKU-CsFx112F.js +89 -0
- ksadk/server/static/assets/katex-DRsiwpM2.js +265 -0
- ksadk/server/static/assets/linear-TNRQ5Orv.js +1 -0
- ksadk/server/static/assets/livescript-BXxG0Yva.js +1 -0
- ksadk/server/static/assets/lua-CiA1ziua.js +1 -0
- ksadk/server/static/assets/mathematica-u8YMmbU0.js +1 -0
- ksadk/server/static/assets/mbox-BEMVcqjs.js +1 -0
- ksadk/server/static/assets/mermaid-parser.core-D2eMVOgo.js +161 -0
- ksadk/server/static/assets/mindmap-definition-RKZ34NQL-BtByYFhL.js +96 -0
- ksadk/server/static/assets/mirc-C9zpzAZU.js +1 -0
- ksadk/server/static/assets/mllike-B45xgp2S.js +1 -0
- ksadk/server/static/assets/modelica-CqHI_q-D.js +1 -0
- ksadk/server/static/assets/mscgen-qgSLujhx.js +1 -0
- ksadk/server/static/assets/mumps-CW_TBmxz.js +1 -0
- ksadk/server/static/assets/nginx-BMaDbqW3.js +1 -0
- ksadk/server/static/assets/nsis-AQ_alPln.js +1 -0
- ksadk/server/static/assets/ntriples-d28e9m0E.js +1 -0
- ksadk/server/static/assets/octave-D2Q8cUp1.js +1 -0
- ksadk/server/static/assets/ordinal-hYBb2elL.js +1 -0
- ksadk/server/static/assets/oz-BwTct_5T.js +1 -0
- ksadk/server/static/assets/packet-YPE3B663-BLKhbXZB.js +1 -0
- ksadk/server/static/assets/pascal-Dp_KdFgX.js +1 -0
- ksadk/server/static/assets/perl-CdEHsPfU.js +1 -0
- ksadk/server/static/assets/pie-LRSECV5Y-DFOikjSY.js +1 -0
- ksadk/server/static/assets/pieDiagram-4H26LBE5-DPnuCB8c.js +30 -0
- ksadk/server/static/assets/pig-AF4Hwhju.js +1 -0
- ksadk/server/static/assets/powershell-CzG71I4L.js +1 -0
- ksadk/server/static/assets/properties-C357ku5U.js +1 -0
- ksadk/server/static/assets/protobuf-D1ij_kNL.js +1 -0
- ksadk/server/static/assets/pug-Bugr-47h.js +1 -0
- ksadk/server/static/assets/puppet-B4hC2X4m.js +1 -0
- ksadk/server/static/assets/python-BqYw9LYJ.js +1 -0
- ksadk/server/static/assets/q-B9NhS7L_.js +1 -0
- ksadk/server/static/assets/quadrantDiagram-W4KKPZXB-miFpCPv4.js +7 -0
- ksadk/server/static/assets/r-DADKenSm.js +1 -0
- ksadk/server/static/assets/radar-GUYGQ44K-095b1Ikw.js +1 -0
- ksadk/server/static/assets/requirementDiagram-4Y6WPE33-mjB_yYkW.js +84 -0
- ksadk/server/static/assets/rpm-gpDK5sbt.js +1 -0
- ksadk/server/static/assets/ruby-DsYpTuWg.js +1 -0
- ksadk/server/static/assets/sankeyDiagram-5OEKKPKP-D7hSlZZZ.js +40 -0
- ksadk/server/static/assets/sas-C2OyD2Y6.js +1 -0
- ksadk/server/static/assets/scheme-CyDdOh7e.js +1 -0
- ksadk/server/static/assets/sequenceDiagram-3UESZ5HK-D9Cx6hC3.js +162 -0
- ksadk/server/static/assets/shell-Du5Qg9im.js +1 -0
- ksadk/server/static/assets/sieve-BAxa3IjF.js +1 -0
- ksadk/server/static/assets/simple-mode-DRpGK0lJ.js +1 -0
- ksadk/server/static/assets/smalltalk-BLp5-jwC.js +1 -0
- ksadk/server/static/assets/solr-BBopId9w.js +1 -0
- ksadk/server/static/assets/sparql-Dz-mlCAC.js +1 -0
- ksadk/server/static/assets/spreadsheet-CTIncYxj.js +1 -0
- ksadk/server/static/assets/sql-DcCJ9jop.js +1 -0
- ksadk/server/static/assets/stateDiagram-AJRCARHV-D_zrg47o.js +1 -0
- ksadk/server/static/assets/stateDiagram-v2-BHNVJYJU-J9kCb0nC.js +1 -0
- ksadk/server/static/assets/stex-sDrdk2BW.js +1 -0
- ksadk/server/static/assets/stylus-DLLZ8dgm.js +1 -0
- ksadk/server/static/assets/swift-DKB6_1j6.js +1 -0
- ksadk/server/static/assets/tcl-BGfruO1u.js +1 -0
- ksadk/server/static/assets/textile-DWBqthVk.js +1 -0
- ksadk/server/static/assets/tiddlywiki-VlUiK86J.js +1 -0
- ksadk/server/static/assets/tiki-CxVlf5KZ.js +1 -0
- ksadk/server/static/assets/timeline-definition-PNZ67QCA-CwSec0Wa.js +120 -0
- ksadk/server/static/assets/toml-LvoKBwCs.js +1 -0
- ksadk/server/static/assets/treeView-BLDUP644-yinfJEY6.js +1 -0
- ksadk/server/static/assets/treemap-LRROVOQU-DosUqN1Q.js +1 -0
- ksadk/server/static/assets/troff-C2dAgh7P.js +1 -0
- ksadk/server/static/assets/ttcn-BePYh-hG.js +1 -0
- ksadk/server/static/assets/ttcn-cfg-CY0aot7Q.js +1 -0
- ksadk/server/static/assets/turtle-BDLWE2cD.js +1 -0
- ksadk/server/static/assets/vb-BthdM_4N.js +1 -0
- ksadk/server/static/assets/vbscript-D9f6rSsU.js +1 -0
- ksadk/server/static/assets/velocity-CqftRsjg.js +1 -0
- ksadk/server/static/assets/vennDiagram-CIIHVFJN-agCVobiF.js +34 -0
- ksadk/server/static/assets/verilog-D-x6ne0Z.js +1 -0
- ksadk/server/static/assets/vhdl-CMrOq2q1.js +1 -0
- ksadk/server/static/assets/wardley-L42UT6IY-Bvp7-5uH.js +1 -0
- ksadk/server/static/assets/wardleyDiagram-YWT4CUSO-TmrlhWNt.js +78 -0
- ksadk/server/static/assets/webidl-D5jiJvOU.js +1 -0
- ksadk/server/static/assets/xquery-fj_R-kMw.js +1 -0
- ksadk/server/static/assets/xterm-DooSxjI5.js +36 -0
- ksadk/server/static/assets/xychartDiagram-2RQKCTM6-DV71V-ah.js +7 -0
- ksadk/server/static/assets/yacas-CXXgtw0p.js +1 -0
- ksadk/server/static/assets/z80-CL1naaMV.js +1 -0
- ksadk/server/static/favicon.svg +1 -0
- ksadk/server/static/icons.svg +24 -0
- ksadk/server/static/index.html +14 -0
- ksadk/server/terminal_sessions.py +551 -0
- ksadk/sessions/__init__.py +283 -0
- ksadk/sessions/base.py +354 -0
- ksadk/sessions/continuity.py +313 -0
- ksadk/sessions/errors.py +7 -0
- ksadk/sessions/in_memory.py +273 -0
- ksadk/sessions/local_service.py +949 -0
- ksadk/sessions/postgres_service.py +838 -0
- ksadk/sessions/sqlite_service.py +13 -0
- ksadk/skills/__init__.py +9 -0
- ksadk/skills/loader.py +46 -0
- ksadk/skills/models.py +120 -0
- ksadk/skills/package_store.py +104 -0
- ksadk/skills/runtime/__init__.py +17 -0
- ksadk/skills/runtime/agent.py +183 -0
- ksadk/skills/runtime/artifacts.py +39 -0
- ksadk/skills/runtime/backends/__init__.py +1 -0
- ksadk/skills/runtime/backends/disabled.py +8 -0
- ksadk/skills/runtime/backends/e2b.py +168 -0
- ksadk/skills/runtime/backends/local.py +95 -0
- ksadk/skills/runtime/base.py +103 -0
- ksadk/skills/runtime/executor.py +219 -0
- ksadk/skills/runtime/factory.py +37 -0
- ksadk/skills/runtime/loader.py +131 -0
- ksadk/skills/runtime/registry.py +150 -0
- ksadk/skills/runtime/request.py +67 -0
- ksadk/skills/service_client.py +239 -0
- ksadk/skills/service_env.py +58 -0
- ksadk/skills/tool_defs.py +221 -0
- ksadk/terminal_client.py +69 -0
- ksadk/terminal_exec_policy.py +229 -0
- ksadk/tools/__init__.py +17 -0
- ksadk/tools/gateway.py +230 -0
- ksadk/tools/result_budget.py +154 -0
- ksadk/toolsets/__init__.py +852 -0
- ksadk/toolsets/_langchain.py +11 -0
- ksadk/toolsets/platform.py +93 -0
- ksadk/toolsets/sandbox.py +310 -0
- ksadk/toolsets/skills.py +217 -0
- ksadk/toolsets/web.py +266 -0
- ksadk/toolsets/workspace.py +878 -0
- ksadk/toolsets/workspace_state.py +44 -0
- ksadk/tracing/__init__.py +37 -0
- ksadk/tracing/exporters/__init__.py +1 -0
- ksadk/tracing/exporters/inmemory_exporter.py +116 -0
- ksadk/tracing/exporters/langfuse_exporter.py +445 -0
- ksadk/tracing/setup.py +829 -0
- ksadk/tracing/span_utils.py +114 -0
- ksadk/tui/__init__.py +12 -0
- ksadk/tui/app.py +400 -0
- ksadk/tui/clipboard.py +100 -0
- ksadk/tui/widgets/__init__.py +22 -0
- ksadk/tui/widgets/assistant.py +89 -0
- ksadk/tui/widgets/base.py +54 -0
- ksadk/tui/widgets/chat_input.py +246 -0
- ksadk/tui/widgets/history.py +156 -0
- ksadk/tui/widgets/system.py +87 -0
- ksadk/tui/widgets/thinking.py +110 -0
- ksadk/tui/widgets/tool_call.py +161 -0
- ksadk/tui/widgets/user.py +37 -0
- ksadk/ui_config.py +168 -0
- ksadk/version.py +4 -0
- ksadk_runtime_common/__init__.py +20 -0
- ksadk_runtime_common/memory_backend/__init__.py +29 -0
- ksadk_runtime_common/memory_backend/manifest.py +79 -0
- ksadk_runtime_common/memory_backend/providers/__init__.py +6 -0
- ksadk_runtime_common/memory_backend/providers/lancedb.py +151 -0
- ksadk_runtime_common/memory_backend/providers/mem0.py +74 -0
- ksadk_runtime_common/memory_backend/registry.py +56 -0
- ksadk_runtime_common/memory_backend/render.py +71 -0
- ksadk_runtime_common/schemas/memory_backend_manifest.schema.json +148 -0
- ksadk_runtime_common/workspace_files/__init__.py +29 -0
- ksadk_runtime_common/workspace_files/bootstrap.py +56 -0
- ksadk_runtime_common/workspace_files/constants.py +9 -0
- ksadk_runtime_common/workspace_files/path_utils.py +71 -0
- ksadk_runtime_common/workspace_files/preview.py +96 -0
- ksadk_runtime_common/workspace_files/router.py +254 -0
- agentengine_sdk_python-0.6.2.dist-info/METADATA +0 -46
- agentengine_sdk_python-0.6.2.dist-info/RECORD +0 -4
- agentengine_sdk_python-0.6.2.dist-info/top_level.txt +0 -1
|
@@ -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
|
+
)
|