@agentunion/kite 1.3.2 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (293) hide show
  1. package/CHANGELOG.md +302 -0
  2. package/cli.js +119 -4
  3. package/core/dependency_checker.py +250 -0
  4. package/core/env_checker.py +490 -0
  5. package/dependencies_lock.json +128 -0
  6. package/extensions/agents/assistant/entry.py +111 -1
  7. package/extensions/agents/assistant/server.py +279 -215
  8. package/extensions/channels/acp_channel/entry.py +111 -1
  9. package/extensions/channels/acp_channel/module.md +23 -22
  10. package/extensions/channels/acp_channel/server.py +279 -215
  11. package/extensions/event_hub_bench/entry.py +107 -1
  12. package/extensions/services/backup/entry.py +306 -21
  13. package/extensions/services/backup/module.md +24 -22
  14. package/extensions/services/evol/auth_manager.py +443 -0
  15. package/extensions/services/evol/config.yaml +149 -0
  16. package/extensions/services/evol/config_loader.py +117 -0
  17. package/extensions/services/evol/entry.py +406 -0
  18. package/extensions/services/evol/evol_api.py +173 -0
  19. package/extensions/services/evol/evol_config.json5 +29 -0
  20. package/extensions/services/evol/migrate_tokens.py +122 -0
  21. package/extensions/services/evol/module.md +32 -0
  22. package/extensions/services/evol/pairing.py +250 -0
  23. package/extensions/services/evol/pairing_codes.jsonl +1 -0
  24. package/extensions/services/evol/relay.py +682 -0
  25. package/extensions/services/evol/relay_config.json5 +67 -0
  26. package/extensions/services/evol/routes/__init__.py +1 -0
  27. package/extensions/services/evol/routes/routes_management_ws.py +127 -0
  28. package/extensions/services/evol/routes/routes_rpc.py +89 -0
  29. package/extensions/services/evol/routes/routes_test.py +61 -0
  30. package/extensions/services/evol/server.py +875 -0
  31. package/extensions/services/evol/static/css/style.css +1200 -0
  32. package/extensions/services/evol/static/index.html +781 -0
  33. package/extensions/services/evol/static/index_evol.html +14 -0
  34. package/extensions/services/evol/static/js/app.js +6304 -0
  35. package/extensions/services/evol/static/js/auth.js +326 -0
  36. package/extensions/services/evol/static/js/dialog.js +285 -0
  37. package/extensions/services/evol/static/js/evol-app-fixed.js +50 -0
  38. package/extensions/services/evol/static/js/evol-app.js +1949 -0
  39. package/extensions/services/evol/static/js/evol-app.js.bak +1800 -0
  40. package/extensions/services/evol/static/js/kernel-client-example.js +228 -0
  41. package/extensions/services/evol/static/js/kernel-client.js +396 -0
  42. package/extensions/services/evol/static/js/main.js +141 -0
  43. package/extensions/services/evol/static/js/registry-tests.js +585 -0
  44. package/extensions/services/evol/static/js/stats.js +217 -0
  45. package/extensions/services/evol/static/js/token-manager.js +175 -0
  46. package/extensions/services/evol/static/pairing.html +248 -0
  47. package/extensions/services/evol/static/test_registry.html +262 -0
  48. package/extensions/services/evol/static/test_relay.html +462 -0
  49. package/extensions/services/evol/stats_manager.py +240 -0
  50. package/extensions/services/model_service/entry.py +167 -19
  51. package/extensions/services/model_service/module.md +21 -22
  52. package/extensions/services/proxy/.claude/settings.local.json +13 -0
  53. package/extensions/services/proxy/CHANGELOG_20260308.md +258 -0
  54. package/extensions/services/proxy/_fix_prints.py +133 -0
  55. package/extensions/services/proxy/_fix_prints2.py +87 -0
  56. package/extensions/services/proxy/agentcp/LICENCE +178 -0
  57. package/extensions/services/proxy/agentcp/README copy.md +85 -0
  58. package/extensions/services/proxy/agentcp/README.md +260 -0
  59. package/extensions/services/proxy/agentcp/__init__.py +16 -0
  60. package/extensions/services/proxy/agentcp/agent.py +4 -0
  61. package/extensions/services/proxy/agentcp/agentcp.py +2494 -0
  62. package/extensions/services/proxy/agentcp/agentprofile.json +89 -0
  63. package/extensions/services/proxy/agentcp/ap/__init__.py +16 -0
  64. package/extensions/services/proxy/agentcp/ap/ap_client.py +316 -0
  65. package/extensions/services/proxy/agentcp/assets/images/wechat_qr.png +0 -0
  66. package/extensions/services/proxy/agentcp/backup/metrics.json +31 -0
  67. package/extensions/services/proxy/agentcp/base/__init__.py +20 -0
  68. package/extensions/services/proxy/agentcp/base/auth_client.py +257 -0
  69. package/extensions/services/proxy/agentcp/base/client.py +112 -0
  70. package/extensions/services/proxy/agentcp/base/env.py +34 -0
  71. package/extensions/services/proxy/agentcp/base/html_util.py +336 -0
  72. package/extensions/services/proxy/agentcp/base/log.py +98 -0
  73. package/extensions/services/proxy/agentcp/ca/__init__.py +17 -0
  74. package/extensions/services/proxy/agentcp/ca/ca_client.py +414 -0
  75. package/extensions/services/proxy/agentcp/ca/ca_root.py +74 -0
  76. package/extensions/services/proxy/agentcp/context/__init__.py +20 -0
  77. package/extensions/services/proxy/agentcp/context/context.py +73 -0
  78. package/extensions/services/proxy/agentcp/context/exceptions.py +114 -0
  79. package/extensions/services/proxy/agentcp/create_profile.py +125 -0
  80. package/extensions/services/proxy/agentcp/create_profile_weather.py +125 -0
  81. package/extensions/services/proxy/agentcp/db/__init__.py +15 -0
  82. package/extensions/services/proxy/agentcp/db/db_mananger.py +550 -0
  83. package/extensions/services/proxy/agentcp/docs/UDP_HEARTBEAT_FIX_REPORT.md +265 -0
  84. package/extensions/services/proxy/agentcp/docs/heartbeat_issue_analysis.md +291 -0
  85. package/extensions/services/proxy/agentcp/file/__init__.py +16 -0
  86. package/extensions/services/proxy/agentcp/file/file_client.py +141 -0
  87. package/extensions/services/proxy/agentcp/file/wss_binary_message.py +137 -0
  88. package/extensions/services/proxy/agentcp/hcp.py +299 -0
  89. package/extensions/services/proxy/agentcp/heartbeat/__init__.py +16 -0
  90. package/extensions/services/proxy/agentcp/heartbeat/heartbeat_client.py +360 -0
  91. package/extensions/services/proxy/agentcp/improved_scheduler.py +498 -0
  92. package/extensions/services/proxy/agentcp/llm_agent_utils.py +249 -0
  93. package/extensions/services/proxy/agentcp/llm_server.py +172 -0
  94. package/extensions/services/proxy/agentcp/mermaid.py +210 -0
  95. package/extensions/services/proxy/agentcp/message.py +149 -0
  96. package/extensions/services/proxy/agentcp/metrics.py +256 -0
  97. package/extensions/services/proxy/agentcp/monitoring/__init__.py +20 -0
  98. package/extensions/services/proxy/agentcp/monitoring/global_monitor.py +27 -0
  99. package/extensions/services/proxy/agentcp/monitoring/metrics_store.py +325 -0
  100. package/extensions/services/proxy/agentcp/monitoring/monitoring_service.py +269 -0
  101. package/extensions/services/proxy/agentcp/monitoring/sliding_window.py +222 -0
  102. package/extensions/services/proxy/agentcp/monitoring/standalone_reader.py +224 -0
  103. package/extensions/services/proxy/agentcp/msg/__init__.py +21 -0
  104. package/extensions/services/proxy/agentcp/msg/connection_manager.py +456 -0
  105. package/extensions/services/proxy/agentcp/msg/message_client.py +2058 -0
  106. package/extensions/services/proxy/agentcp/msg/message_serialize.py +263 -0
  107. package/extensions/services/proxy/agentcp/msg/open_ai_message.py +88 -0
  108. package/extensions/services/proxy/agentcp/msg/session_manager.py +1062 -0
  109. package/extensions/services/proxy/agentcp/msg/stream_client.py +267 -0
  110. package/extensions/services/proxy/agentcp/msg/websocket_file_receiver.py +89 -0
  111. package/extensions/services/proxy/agentcp/msg/ws_logger.py +685 -0
  112. package/extensions/services/proxy/agentcp/msg/wss_binary_message.py +137 -0
  113. package/extensions/services/proxy/agentcp/requirements.txt +7 -0
  114. package/extensions/services/proxy/agentcp/samples/agent_graph/README.md +37 -0
  115. package/extensions/services/proxy/agentcp/samples/agent_graph/agentprofile.json +89 -0
  116. package/extensions/services/proxy/agentcp/samples/agent_graph/create_profile.py +138 -0
  117. package/extensions/services/proxy/agentcp/samples/agent_graph/main.py +164 -0
  118. package/extensions/services/proxy/agentcp/samples/agent_use/create_profile.py +123 -0
  119. package/extensions/services/proxy/agentcp/samples/agent_use/llm/create_profile.py +129 -0
  120. package/extensions/services/proxy/agentcp/samples/agent_use/llm/env.json +5 -0
  121. package/extensions/services/proxy/agentcp/samples/agent_use/llm/main.py +146 -0
  122. package/extensions/services/proxy/agentcp/samples/agent_use/main.py +123 -0
  123. package/extensions/services/proxy/agentcp/samples/agent_use/readme.md +379 -0
  124. package/extensions/services/proxy/agentcp/samples/agent_use/search/create_profile.py +129 -0
  125. package/extensions/services/proxy/agentcp/samples/agent_use/search/main.py +28 -0
  126. package/extensions/services/proxy/agentcp/samples/agent_use/tool/create_profile.py +129 -0
  127. package/extensions/services/proxy/agentcp/samples/agent_use/tool/main.py +20 -0
  128. package/extensions/services/proxy/agentcp/samples/ali_amap/README.md +97 -0
  129. package/extensions/services/proxy/agentcp/samples/ali_amap/amap_agent.py +88 -0
  130. package/extensions/services/proxy/agentcp/samples/ali_amap/create_profile.py +125 -0
  131. package/extensions/services/proxy/agentcp/samples/compute_agent/agent/powershell.py +228 -0
  132. package/extensions/services/proxy/agentcp/samples/compute_agent/agent/software.py +63 -0
  133. package/extensions/services/proxy/agentcp/samples/compute_agent/agent/tools.py +36 -0
  134. package/extensions/services/proxy/agentcp/samples/compute_agent/browser_user.py +41 -0
  135. package/extensions/services/proxy/agentcp/samples/deepseek/README.md +79 -0
  136. package/extensions/services/proxy/agentcp/samples/deepseek/create_profile.py +126 -0
  137. package/extensions/services/proxy/agentcp/samples/deepseek/deepseek.py +42 -0
  138. package/extensions/services/proxy/agentcp/samples/dify_chat/README.md +78 -0
  139. package/extensions/services/proxy/agentcp/samples/dify_chat/create_profile.py +126 -0
  140. package/extensions/services/proxy/agentcp/samples/dify_chat/dify_chat.py +47 -0
  141. package/extensions/services/proxy/agentcp/samples/dify_workflow/README.md +78 -0
  142. package/extensions/services/proxy/agentcp/samples/dify_workflow/create_profile.py +126 -0
  143. package/extensions/services/proxy/agentcp/samples/dify_workflow/dify_workflow.py +46 -0
  144. package/extensions/services/proxy/agentcp/samples/executor/README.md +44 -0
  145. package/extensions/services/proxy/agentcp/samples/executor/agentprofile.json +89 -0
  146. package/extensions/services/proxy/agentcp/samples/executor/create_profile.py +139 -0
  147. package/extensions/services/proxy/agentcp/samples/executor/main.py +160 -0
  148. package/extensions/services/proxy/agentcp/samples/filereader/README.md +45 -0
  149. package/extensions/services/proxy/agentcp/samples/filereader/agentprofile.json +90 -0
  150. package/extensions/services/proxy/agentcp/samples/filereader/create_profile.py +137 -0
  151. package/extensions/services/proxy/agentcp/samples/filereader/main.py +253 -0
  152. package/extensions/services/proxy/agentcp/samples/filewriter/README.md +38 -0
  153. package/extensions/services/proxy/agentcp/samples/filewriter/agentprofile.json +91 -0
  154. package/extensions/services/proxy/agentcp/samples/filewriter/create_profile.py +138 -0
  155. package/extensions/services/proxy/agentcp/samples/filewriter/main.py +289 -0
  156. package/extensions/services/proxy/agentcp/samples/hcp/README.md +85 -0
  157. package/extensions/services/proxy/agentcp/samples/hcp/acp_weather_agent.zip +0 -0
  158. package/extensions/services/proxy/agentcp/samples/hcp/create_profile.py +125 -0
  159. package/extensions/services/proxy/agentcp/samples/hcp/hcp.py +237 -0
  160. package/extensions/services/proxy/agentcp/samples/helloworld/README.md +68 -0
  161. package/extensions/services/proxy/agentcp/samples/helloworld/hello_world.py +40 -0
  162. package/extensions/services/proxy/agentcp/samples/llm_agent/MEADME.md +117 -0
  163. package/extensions/services/proxy/agentcp/samples/llm_agent/create_profile.py +125 -0
  164. package/extensions/services/proxy/agentcp/samples/llm_agent/qwen_agent.py +136 -0
  165. package/extensions/services/proxy/agentcp/samples/local_llm_agent/README.md +90 -0
  166. package/extensions/services/proxy/agentcp/samples/local_llm_agent/create_profile.py +125 -0
  167. package/extensions/services/proxy/agentcp/samples/local_llm_agent/main.py +49 -0
  168. package/extensions/services/proxy/agentcp/samples/query_llm_from_agent/README.md +55 -0
  169. package/extensions/services/proxy/agentcp/samples/query_llm_from_agent/create_profile.py +125 -0
  170. package/extensions/services/proxy/agentcp/samples/query_llm_from_agent/main.py +23 -0
  171. package/extensions/services/proxy/agentcp/samples/query_weather_api_agent/README.md +103 -0
  172. package/extensions/services/proxy/agentcp/samples/query_weather_api_agent/create_profile.py +125 -0
  173. package/extensions/services/proxy/agentcp/samples/query_weather_api_agent/main.py +69 -0
  174. package/extensions/services/proxy/agentcp/samples/query_weather_from_agent/README.md +58 -0
  175. package/extensions/services/proxy/agentcp/samples/query_weather_from_agent/create_profile.py +125 -0
  176. package/extensions/services/proxy/agentcp/samples/query_weather_from_agent/main.py +25 -0
  177. package/extensions/services/proxy/agentcp/samples/qwen3/README.md +71 -0
  178. package/extensions/services/proxy/agentcp/samples/qwen3/create_profile.py +126 -0
  179. package/extensions/services/proxy/agentcp/samples/qwen3/qwen3.py +37 -0
  180. package/extensions/services/proxy/agentcp/samples/qwen3_tools/README.md +133 -0
  181. package/extensions/services/proxy/agentcp/samples/qwen3_tools/create_profile.py +126 -0
  182. package/extensions/services/proxy/agentcp/samples/qwen3_tools/qwen3_tools.py +98 -0
  183. package/extensions/services/proxy/agentcp/samples/search/create_profile_qwen.py +125 -0
  184. package/extensions/services/proxy/agentcp/samples/search/create_profile_search.py +125 -0
  185. package/extensions/services/proxy/agentcp/samples/search/qwen_agent.py +136 -0
  186. package/extensions/services/proxy/agentcp/samples/search/search_agent.py +170 -0
  187. package/extensions/services/proxy/agentcp/samples/wrapper_agently_to_agent/README.md +89 -0
  188. package/extensions/services/proxy/agentcp/samples/wrapper_agently_to_agent/create_profile.py +125 -0
  189. package/extensions/services/proxy/agentcp/samples/wrapper_agently_to_agent/main.py +44 -0
  190. package/extensions/services/proxy/agentcp/utils/__init__.py +15 -0
  191. package/extensions/services/proxy/agentcp/utils/file_util.py +117 -0
  192. package/extensions/services/proxy/agentcp/utils/proxy_bypass.py +99 -0
  193. package/extensions/services/proxy/agentcp/workflow.py +203 -0
  194. package/extensions/services/proxy/console_auth.py +109 -0
  195. package/extensions/services/proxy/evol/__init__.py +1 -0
  196. package/extensions/services/proxy/evol/config.py +37 -0
  197. package/extensions/services/proxy/evol/http/__init__.py +1 -0
  198. package/extensions/services/proxy/evol/http/async_http.py +551 -0
  199. package/extensions/services/proxy/evol/log.py +28 -0
  200. package/extensions/services/proxy/evol/presenter/__init__.py +2 -0
  201. package/extensions/services/proxy/evol/presenter/agentIdPresenter.py +1031 -0
  202. package/extensions/services/proxy/evol/presenter/apikeyPresenter.py +106 -0
  203. package/extensions/services/proxy/evol/presenter/configPresenter.py +1281 -0
  204. package/extensions/services/proxy/evol/presenter/userPresenter.py +477 -0
  205. package/extensions/services/proxy/evol/server/__init__.py +1 -0
  206. package/extensions/services/proxy/evol/server/claude_proxy_async.py +3430 -0
  207. package/extensions/services/proxy/evol/server/openclaw_proxy.py +1861 -0
  208. package/extensions/services/proxy/evol/server/proxy_config.py +15 -0
  209. package/extensions/services/proxy/evol/server/proxy_engine.py +501 -0
  210. package/extensions/services/proxy/evol/version.py +24 -0
  211. package/extensions/services/proxy/logs/websocket.log +260 -0
  212. package/extensions/services/proxy/main.py +240 -0
  213. package/extensions/services/proxy/requirements.txt +13 -0
  214. package/extensions/services/proxy/server.py +271 -0
  215. package/extensions/services/watchdog/entry.py +215 -26
  216. package/extensions/services/watchdog/module.md +1 -0
  217. package/extensions/services/watchdog/monitor.py +178 -38
  218. package/extensions/services/web/WEBSOCKET_STATUS.md +143 -0
  219. package/extensions/services/web/config_example.py +35 -0
  220. package/extensions/services/web/config_loader.py +110 -0
  221. package/extensions/services/web/entry.py +114 -26
  222. package/extensions/services/web/module.md +35 -24
  223. package/extensions/services/web/pairing.py +250 -0
  224. package/extensions/services/web/pairing_codes.jsonl +16 -0
  225. package/extensions/services/web/relay.py +643 -0
  226. package/extensions/services/web/relay_config.json5 +67 -0
  227. package/extensions/services/web/routes/routes_management_ws.py +127 -0
  228. package/extensions/services/web/routes/routes_rpc.py +89 -0
  229. package/extensions/services/web/routes/routes_test.py +61 -0
  230. package/extensions/services/web/routes/schemas.py +0 -22
  231. package/extensions/services/web/server.py +434 -99
  232. package/extensions/services/web/static/css/style.css +67 -28
  233. package/extensions/services/web/static/index.html +234 -44
  234. package/extensions/services/web/static/js/app.js +1335 -48
  235. package/extensions/services/web/static/js/kernel-client-example.js +161 -0
  236. package/extensions/services/web/static/js/kernel-client.js +383 -0
  237. package/extensions/services/web/static/js/registry-tests.js +558 -0
  238. package/extensions/services/web/static/js/token-manager.js +175 -0
  239. package/extensions/services/web/static/pairing.html +248 -0
  240. package/extensions/services/web/static/test_registry.html +262 -0
  241. package/extensions/services/web/web_config.json5 +29 -0
  242. package/kernel/entry.py +120 -32
  243. package/kernel/event_hub.py +141 -16
  244. package/kernel/module.md +60 -33
  245. package/kernel/registry_store.py +45 -36
  246. package/kernel/rpc_router.py +152 -59
  247. package/kernel/server.py +322 -26
  248. package/kite_cli/__init__.py +3 -0
  249. package/kite_cli/__main__.py +5 -0
  250. package/kite_cli/commands/__init__.py +1 -0
  251. package/kite_cli/commands/clean.py +101 -0
  252. package/kite_cli/commands/deps_install.py +67 -0
  253. package/kite_cli/commands/doctor.py +35 -0
  254. package/kite_cli/commands/env_check.py +45 -0
  255. package/kite_cli/commands/history.py +111 -0
  256. package/kite_cli/commands/info.py +96 -0
  257. package/kite_cli/commands/install.py +313 -0
  258. package/kite_cli/commands/list.py +143 -0
  259. package/kite_cli/commands/log.py +81 -0
  260. package/kite_cli/commands/prepare.py +49 -0
  261. package/kite_cli/commands/rollback.py +88 -0
  262. package/kite_cli/commands/search.py +73 -0
  263. package/kite_cli/commands/uninstall.py +85 -0
  264. package/kite_cli/commands/update.py +118 -0
  265. package/kite_cli/commands/venv_setup.py +56 -0
  266. package/kite_cli/core/__init__.py +1 -0
  267. package/kite_cli/core/checker.py +142 -0
  268. package/kite_cli/core/dependency.py +229 -0
  269. package/kite_cli/core/downloader.py +209 -0
  270. package/kite_cli/core/install_info.py +40 -0
  271. package/kite_cli/core/tool_installer.py +397 -0
  272. package/kite_cli/core/validator.py +78 -0
  273. package/kite_cli/main.py +317 -0
  274. package/kite_cli/utils/__init__.py +1 -0
  275. package/kite_cli/utils/i18n.py +252 -0
  276. package/kite_cli/utils/interactive.py +63 -0
  277. package/kite_cli/utils/operation_log.py +77 -0
  278. package/kite_cli/utils/paths.py +34 -0
  279. package/kite_cli/utils/version.py +308 -0
  280. package/launcher/entry.py +1124 -178
  281. package/launcher/logging_setup.py +104 -0
  282. package/launcher/module.md +46 -37
  283. package/launcher/module_scanner.py +11 -1
  284. package/main.py +4 -1
  285. package/package.json +9 -1
  286. package/python_version.json +4 -0
  287. package/requirements.txt +38 -0
  288. package/scripts/env-manager.js +328 -0
  289. package/scripts/plan_manager.py +315 -0
  290. package/scripts/python-env.js +79 -0
  291. package/scripts/scan_dependencies.py +461 -0
  292. package/scripts/setup-python-env.js +191 -0
  293. package/extensions/services/web/routes/routes_modules.py +0 -249
@@ -0,0 +1,551 @@
1
+ from typing import Any, Awaitable, Callable, Dict, Optional
2
+ import asyncio
3
+ import ssl
4
+ import uuid
5
+ import os
6
+ import threading
7
+ from urllib.parse import urlparse
8
+
9
+ import aiohttp
10
+ import certifi
11
+
12
+ from ..presenter.configPresenter import configPresenter
13
+ from ..version import __version__, __cmp_version__
14
+
15
+
16
+ def _ensure_no_proxy_for_local_env():
17
+ local_addrs = 'localhost,127.0.0.1,::1'
18
+ for key in ('NO_PROXY', 'no_proxy'):
19
+ existing = (os.environ.get(key) or '').strip()
20
+ if not existing:
21
+ os.environ[key] = local_addrs
22
+ continue
23
+
24
+ existing_parts = [p.strip() for p in existing.replace(';', ',').split(',') if p.strip()]
25
+ existing_lower = {p.lower() for p in existing_parts}
26
+ for addr in local_addrs.split(','):
27
+ if addr.lower() not in existing_lower:
28
+ existing_parts.append(addr)
29
+ os.environ[key] = ','.join(existing_parts)
30
+
31
+
32
+ def _is_local_url(url: str) -> bool:
33
+ try:
34
+ parsed = urlparse(url)
35
+ host = parsed.hostname
36
+ if not host:
37
+ return False
38
+ host = host.strip('[]').lower()
39
+ return host in ('localhost', '127.0.0.1', '::1', '0.0.0.0')
40
+ except Exception:
41
+ return False
42
+
43
+
44
+ _ensure_no_proxy_for_local_env()
45
+
46
+
47
+ class AsyncHttpClient:
48
+ # SSL验证配置:False=禁用验证(开发环境), True=启用验证(生产环境)
49
+ VERIFY_SSL = False
50
+
51
+ # 连接池配置
52
+ _connector: Optional[aiohttp.TCPConnector] = None
53
+ _connector_no_proxy: Optional[aiohttp.TCPConnector] = None
54
+ _connector_loop: Optional[asyncio.AbstractEventLoop] = None
55
+ _connector_no_proxy_loop: Optional[asyncio.AbstractEventLoop] = None
56
+ _lock = threading.Lock()
57
+
58
+ # 超时配置(秒)
59
+ CONNECT_TIMEOUT = 10
60
+ TOTAL_TIMEOUT = 60
61
+
62
+ @classmethod
63
+ def _is_connector_valid(cls, connector: Optional[aiohttp.TCPConnector], connector_loop: Optional[asyncio.AbstractEventLoop]) -> bool:
64
+ """
65
+ 检查 connector 是否有效
66
+ - connector 不为 None
67
+ - connector 未关闭
68
+ - connector 绑定的 loop 与当前 loop 一致
69
+ - 当前 loop 未关闭
70
+ """
71
+ if connector is None or connector.closed:
72
+ return False
73
+
74
+ try:
75
+ current_loop = asyncio.get_running_loop()
76
+ except RuntimeError:
77
+ # 没有运行中的 event loop
78
+ return False
79
+
80
+ if current_loop is None or current_loop.is_closed():
81
+ return False
82
+
83
+ if connector_loop is not current_loop:
84
+ return False
85
+
86
+ return True
87
+
88
+ @classmethod
89
+ def _safe_close_connector(cls, connector: Optional[aiohttp.TCPConnector]) -> None:
90
+ """安全关闭 connector(同步方式,用于 event loop 变更时的清理)"""
91
+ if connector is None:
92
+ return
93
+ try:
94
+ if not connector.closed:
95
+ # 使用内部方法同步关闭,避免在错误的 loop 中 await
96
+ connector._close()
97
+ except Exception:
98
+ pass
99
+
100
+ @classmethod
101
+ def _get_connector(cls, use_proxy: bool = True) -> aiohttp.TCPConnector:
102
+ """
103
+ 获取共享的 TCPConnector(连接池)
104
+ - limit: 总连接数限制
105
+ - limit_per_host: 每个主机的连接数限制
106
+ 线程安全:使用锁保护 connector 的创建
107
+
108
+ 修复 Event loop is closed 问题:
109
+ - 检查 connector 绑定的 event loop 是否与当前 loop 一致
110
+ - 如果 loop 变更,自动重建 connector
111
+ """
112
+ with cls._lock:
113
+ ssl_context = cls._get_ssl_context()
114
+
115
+ # 获取当前 event loop
116
+ try:
117
+ current_loop = asyncio.get_running_loop()
118
+ except RuntimeError:
119
+ current_loop = None
120
+
121
+ if use_proxy:
122
+ # 检查 connector 是否有效(包括 event loop 检查)
123
+ if not cls._is_connector_valid(cls._connector, cls._connector_loop):
124
+ # 清理旧的 connector
125
+ cls._safe_close_connector(cls._connector)
126
+ # 创建新的 connector
127
+ cls._connector = aiohttp.TCPConnector(
128
+ ssl=ssl_context,
129
+ limit=100, # 总连接数
130
+ limit_per_host=30, # 每个主机最大连接数
131
+ enable_cleanup_closed=True,
132
+ )
133
+ cls._connector_loop = current_loop
134
+ return cls._connector
135
+ else:
136
+ # 检查 no_proxy connector 是否有效
137
+ if not cls._is_connector_valid(cls._connector_no_proxy, cls._connector_no_proxy_loop):
138
+ # 清理旧的 connector
139
+ cls._safe_close_connector(cls._connector_no_proxy)
140
+ # 创建新的 connector
141
+ cls._connector_no_proxy = aiohttp.TCPConnector(
142
+ ssl=ssl_context,
143
+ limit=100,
144
+ limit_per_host=30,
145
+ enable_cleanup_closed=True,
146
+ )
147
+ cls._connector_no_proxy_loop = current_loop
148
+ return cls._connector_no_proxy
149
+
150
+ @classmethod
151
+ async def close(cls):
152
+ """
153
+ 关闭所有连接池,应在应用退出时调用
154
+ """
155
+ with cls._lock:
156
+ if cls._connector is not None and not cls._connector.closed:
157
+ await cls._connector.close()
158
+ cls._connector = None
159
+ cls._connector_loop = None
160
+ if cls._connector_no_proxy is not None and not cls._connector_no_proxy.closed:
161
+ await cls._connector_no_proxy.close()
162
+ cls._connector_no_proxy = None
163
+ cls._connector_no_proxy_loop = None
164
+
165
+ @classmethod
166
+ def reset_connectors(cls):
167
+ """
168
+ 重置所有 connector(同步方法,用于 event loop 变更后的恢复)
169
+ """
170
+ with cls._lock:
171
+ cls._safe_close_connector(cls._connector)
172
+ cls._connector = None
173
+ cls._connector_loop = None
174
+ cls._safe_close_connector(cls._connector_no_proxy)
175
+ cls._connector_no_proxy = None
176
+ cls._connector_no_proxy_loop = None
177
+
178
+ @classmethod
179
+ def _is_connector_closed_error(cls, error: Exception) -> bool:
180
+ message = str(error).lower()
181
+ return "connector is closed" in message or "event loop is closed" in message
182
+
183
+ @classmethod
184
+ async def _request_with_retry(cls, request_fn: Callable[[], Awaitable[Any]]) -> Any:
185
+ try:
186
+ return await request_fn()
187
+ except Exception as e:
188
+ if cls._is_connector_closed_error(e):
189
+ cls.reset_connectors()
190
+ return await request_fn()
191
+ raise
192
+
193
+ @classmethod
194
+ def _get_timeout(cls, total_timeout: Optional[int] = None) -> aiohttp.ClientTimeout:
195
+ """获取超时配置"""
196
+ effective_total_timeout = total_timeout if total_timeout is not None else cls.TOTAL_TIMEOUT
197
+ return aiohttp.ClientTimeout(
198
+ total=effective_total_timeout,
199
+ connect=cls.CONNECT_TIMEOUT,
200
+ )
201
+
202
+ @classmethod
203
+ def _get_ssl_context(cls):
204
+ """
205
+ 创建 SSL 上下文
206
+ 根据 VERIFY_SSL 配置决定是否验证SSL证书
207
+
208
+ - VERIFY_SSL=False: 禁用SSL验证,适用于开发环境或自签名证书
209
+ - VERIFY_SSL=True: 使用certifi证书验证,适用于生产环境
210
+ """
211
+ if not cls.VERIFY_SSL:
212
+ # 开发环境:禁用SSL验证
213
+ return False
214
+ else:
215
+ # 生产环境:使用certifi证书包
216
+ try:
217
+ ssl_context = ssl.create_default_context(cafile=certifi.where())
218
+ return ssl_context
219
+ except Exception:
220
+ # certifi加载失败,降级为禁用验证
221
+ return False
222
+
223
+ @classmethod
224
+ def _get_trust_env(cls, url: str) -> bool:
225
+ """获取是否信任环境变量(包括代理环境变量);localhost 永远直连。"""
226
+ return configPresenter.get_use_system_proxy() and (not _is_local_url(url))
227
+
228
+ @classmethod
229
+ def _get_device_id(cls) -> str:
230
+ """
231
+ 获取设备ID:优先从配置读取,否则根据系统生成
232
+ 参考 userPresenter._get_device_id() 实现
233
+ """
234
+ import sys
235
+
236
+ is_mac = sys.platform == "darwin"
237
+
238
+ # 1. 尝试从配置获取
239
+ device_id = configPresenter.getSetting("device_id")
240
+ if device_id:
241
+ # Mac 系统如果是 mac_ 开头,转换为 uuid_ 格式
242
+ if is_mac and device_id.startswith("mac_"):
243
+ device_id = f"uuid_{uuid.uuid4().hex}"
244
+ configPresenter.setSetting("device_id", device_id)
245
+ return device_id
246
+
247
+ # 根据系统选择生成策略
248
+ if is_mac:
249
+ # Mac: 优先 UUID,备选 MAC 地址
250
+ try:
251
+ device_id = f"uuid_{uuid.uuid4().hex}"
252
+ except:
253
+ try:
254
+ mac = uuid.getnode()
255
+ device_id = f"mac_{mac:012x}"
256
+ except:
257
+ device_id = f"uuid_{uuid.uuid4().hex}"
258
+ else:
259
+ # Windows/Linux: 优先 MAC 地址,备选 UUID
260
+ try:
261
+ mac = uuid.getnode()
262
+ device_id = f"mac_{mac:012x}"
263
+ except:
264
+ device_id = f"uuid_{uuid.uuid4().hex}"
265
+
266
+ # 保存到配置
267
+ configPresenter.setSetting("device_id", device_id)
268
+ return device_id
269
+
270
+ @classmethod
271
+ def _get_default_headers(cls, include_token: bool = True) -> Dict[str, str]:
272
+ headers = {
273
+ "User-Agent": f"Evol/{__version__}",
274
+ "Accept": "application/json",
275
+ "version": __version__,
276
+ "cmp_version": __cmp_version__,
277
+ "source": "evol_client",
278
+ "deviceId": cls._get_device_id(),
279
+ }
280
+
281
+ if include_token:
282
+ # 每次都从配置文件读取最新的token,避免使用缓存的旧token
283
+ token = configPresenter.get_token()
284
+ if token:
285
+ headers["Authorization"] = f"Bearer {token}"
286
+ return headers
287
+
288
+ @classmethod
289
+ async def get(cls, url: str, headers: Optional[Dict[str, str]] = None, include_token: bool = True, **kwargs) -> Any:
290
+ merged_headers = cls._merge_headers(headers, include_token)
291
+ total_timeout = kwargs.pop("total_timeout", None)
292
+
293
+ async def _do_request() -> Any:
294
+ trust_env = cls._get_trust_env(url)
295
+ connector = cls._get_connector(use_proxy=trust_env)
296
+ timeout = cls._get_timeout(total_timeout=total_timeout)
297
+ async with aiohttp.ClientSession(connector=connector, trust_env=trust_env, connector_owner=False) as session:
298
+ async with session.get(url, headers=merged_headers, timeout=timeout, **kwargs) as response:
299
+ try:
300
+ json_data = await response.json()
301
+ except Exception:
302
+ response.raise_for_status()
303
+ raise
304
+
305
+ if response.status >= 400:
306
+ if isinstance(json_data, dict) and 'code' in json_data:
307
+ return json_data
308
+ response.raise_for_status()
309
+
310
+ return json_data
311
+
312
+ return await cls._request_with_retry(_do_request)
313
+
314
+ @classmethod
315
+ async def post(
316
+ cls, url: str, data: Optional[Any] = None, headers: Optional[Dict[str, str]] = None, include_token: bool = True, **kwargs
317
+ ) -> Any:
318
+ merged_headers = cls._merge_headers(headers, include_token)
319
+ total_timeout = kwargs.pop("total_timeout", None)
320
+
321
+ async def _do_request() -> Any:
322
+ trust_env = cls._get_trust_env(url)
323
+ connector = cls._get_connector(use_proxy=trust_env)
324
+ timeout = cls._get_timeout(total_timeout=total_timeout)
325
+ async with aiohttp.ClientSession(connector=connector, trust_env=trust_env, connector_owner=False) as session:
326
+ async with session.post(url, json=data, headers=merged_headers, timeout=timeout, **kwargs) as response:
327
+ try:
328
+ json_data = await response.json()
329
+ except Exception:
330
+ response.raise_for_status()
331
+ raise
332
+
333
+ if response.status >= 400:
334
+ if isinstance(json_data, dict) and 'code' in json_data:
335
+ return json_data
336
+ response.raise_for_status()
337
+
338
+ return json_data
339
+
340
+ return await cls._request_with_retry(_do_request)
341
+
342
+ @classmethod
343
+ async def post_form(
344
+ cls, url: str, data: Optional[Dict[str, Any]] = None, headers: Optional[Dict[str, str]] = None, include_token: bool = False, **kwargs
345
+ ) -> Any:
346
+ """
347
+ 发送 application/x-www-form-urlencoded 格式的 POST 请求
348
+ """
349
+ merged_headers = cls._merge_headers(headers, include_token)
350
+ # 移除 Content-Type,让 aiohttp 自动设置为 application/x-www-form-urlencoded
351
+ merged_headers.pop("Content-Type", None)
352
+
353
+ async def _do_request() -> Any:
354
+ trust_env = cls._get_trust_env(url)
355
+ connector = cls._get_connector(use_proxy=trust_env)
356
+ timeout = cls._get_timeout()
357
+ async with aiohttp.ClientSession(connector=connector, trust_env=trust_env, connector_owner=False) as session:
358
+ async with session.post(url, data=data, headers=merged_headers, timeout=timeout, **kwargs) as response:
359
+ try:
360
+ json_data = await response.json()
361
+ except Exception:
362
+ response.raise_for_status()
363
+ raise
364
+
365
+ if response.status >= 400:
366
+ if isinstance(json_data, dict) and 'code' in json_data:
367
+ return json_data
368
+ response.raise_for_status()
369
+
370
+ return json_data
371
+
372
+ return await cls._request_with_retry(_do_request)
373
+
374
+ @classmethod
375
+ async def put(
376
+ cls, url: str, data: Optional[Any] = None, headers: Optional[Dict[str, str]] = None, include_token: bool = True, **kwargs
377
+ ) -> Any:
378
+ merged_headers = cls._merge_headers(headers, include_token)
379
+ total_timeout = kwargs.pop("total_timeout", None)
380
+
381
+ async def _do_request() -> Any:
382
+ trust_env = cls._get_trust_env(url)
383
+ connector = cls._get_connector(use_proxy=trust_env)
384
+ timeout = cls._get_timeout(total_timeout=total_timeout)
385
+ async with aiohttp.ClientSession(connector=connector, trust_env=trust_env, connector_owner=False) as session:
386
+ async with session.put(url, json=data, headers=merged_headers, timeout=timeout, **kwargs) as response:
387
+ try:
388
+ json_data = await response.json()
389
+ except Exception:
390
+ response.raise_for_status()
391
+ raise
392
+
393
+ if response.status >= 400:
394
+ if isinstance(json_data, dict) and 'code' in json_data:
395
+ return json_data
396
+ response.raise_for_status()
397
+
398
+ return json_data
399
+
400
+ return await cls._request_with_retry(_do_request)
401
+
402
+ @classmethod
403
+ async def delete(
404
+ cls, url: str, headers: Optional[Dict[str, str]] = None, include_token: bool = True, **kwargs
405
+ ) -> Any:
406
+ merged_headers = cls._merge_headers(headers, include_token)
407
+ total_timeout = kwargs.pop("total_timeout", None)
408
+
409
+ async def _do_request() -> Any:
410
+ trust_env = cls._get_trust_env(url)
411
+ connector = cls._get_connector(use_proxy=trust_env)
412
+ timeout = cls._get_timeout(total_timeout=total_timeout)
413
+ async with aiohttp.ClientSession(connector=connector, trust_env=trust_env, connector_owner=False) as session:
414
+ async with session.delete(url, headers=merged_headers, timeout=timeout, **kwargs) as response:
415
+ try:
416
+ json_data = await response.json()
417
+ except Exception:
418
+ response.raise_for_status()
419
+ raise
420
+
421
+ if response.status >= 400:
422
+ if isinstance(json_data, dict) and 'code' in json_data:
423
+ return json_data
424
+ response.raise_for_status()
425
+
426
+ return json_data
427
+
428
+ return await cls._request_with_retry(_do_request)
429
+
430
+ @classmethod
431
+ async def upload_file(
432
+ cls, url: str, form_data: Dict[str, Any], headers: Optional[Dict[str, str]] = None, include_token: bool = True, **kwargs
433
+ ) -> Any:
434
+ default_headers = cls._get_default_headers(include_token)
435
+ default_headers.pop("Accept", None)
436
+
437
+ if headers:
438
+ default_headers.update(headers)
439
+
440
+ data = aiohttp.FormData()
441
+ for key, value in form_data.items():
442
+ if isinstance(value, tuple):
443
+ filename, content, content_type = value
444
+ data.add_field(key, content, filename=filename, content_type=content_type)
445
+ else:
446
+ data.add_field(key, str(value))
447
+
448
+ async def _do_request() -> Any:
449
+ trust_env = cls._get_trust_env(url)
450
+ connector = cls._get_connector(use_proxy=trust_env)
451
+ timeout = cls._get_timeout()
452
+ async with aiohttp.ClientSession(connector=connector, trust_env=trust_env, connector_owner=False) as session:
453
+ async with session.post(url, data=data, headers=default_headers, timeout=timeout, **kwargs) as response:
454
+ try:
455
+ json_data = await response.json()
456
+ except Exception:
457
+ response.raise_for_status()
458
+ raise
459
+
460
+ if response.status >= 400:
461
+ if isinstance(json_data, dict) and 'code' in json_data:
462
+ return json_data
463
+ response.raise_for_status()
464
+
465
+ return json_data
466
+
467
+ return await cls._request_with_retry(_do_request)
468
+
469
+ @classmethod
470
+ async def download_file(
471
+ cls, url: str, save_path: str, headers: Optional[Dict[str, str]] = None, include_token: bool = True, **kwargs
472
+ ) -> Dict[str, Any]:
473
+ """
474
+ 下载文件到本地
475
+
476
+ Args:
477
+ url: 下载接口地址
478
+ save_path: 保存文件的路径
479
+ headers: 额外的请求头
480
+ include_token: 是否包含认证token
481
+ **kwargs: 其他请求参数
482
+
483
+ Returns:
484
+ Dict: {"success": True, "file_path": save_path, "filename": filename} 或 {"success": False, "error": error_msg}
485
+ """
486
+ merged_headers = cls._merge_headers(headers, include_token)
487
+ # 下载文件不需要 Accept: application/json
488
+ merged_headers.pop("Accept", None)
489
+
490
+ trust_env = cls._get_trust_env(url)
491
+ connector = cls._get_connector(use_proxy=trust_env)
492
+ timeout = cls._get_timeout()
493
+
494
+ try:
495
+ async with aiohttp.ClientSession(connector=connector, trust_env=trust_env, connector_owner=False) as session:
496
+ async with session.get(url, headers=merged_headers, timeout=timeout, **kwargs) as response:
497
+ if response.status >= 400:
498
+ # 尝试获取错误信息
499
+ try:
500
+ json_data = await response.json()
501
+ if isinstance(json_data, dict) and 'msg' in json_data:
502
+ return {"success": False, "error": json_data.get('msg', '下载失败')}
503
+ except:
504
+ pass
505
+ return {"success": False, "error": f"下载失败,状态码: {response.status}"}
506
+
507
+ # 从 Content-Disposition 获取文件名
508
+ content_disposition = response.headers.get('Content-Disposition', '')
509
+ filename = None
510
+ if content_disposition:
511
+ import re
512
+ # 尝试匹配 filename*=UTF-8''xxx 格式
513
+ match = re.search(r"filename\*=UTF-8''(.+)", content_disposition)
514
+ if match:
515
+ from urllib.parse import unquote
516
+ filename = unquote(match.group(1))
517
+ else:
518
+ # 尝试匹配 filename="xxx" 或 filename=xxx 格式
519
+ match = re.search(r'filename[^;=\n]*=(["\']?)([^"\';\n]+)\1', content_disposition)
520
+ if match:
521
+ filename = match.group(2)
522
+
523
+ # 如果没有获取到文件名,使用默认名称
524
+ if not filename:
525
+ filename = os.path.basename(save_path) or 'downloaded_file'
526
+
527
+ # 确保保存目录存在
528
+ save_dir = os.path.dirname(save_path)
529
+ if save_dir and not os.path.exists(save_dir):
530
+ os.makedirs(save_dir, exist_ok=True)
531
+
532
+ # 如果 save_path 是目录,则使用获取到的文件名
533
+ if os.path.isdir(save_path):
534
+ save_path = os.path.join(save_path, filename)
535
+
536
+ # 写入文件
537
+ with open(save_path, 'wb') as f:
538
+ async for chunk in response.content.iter_chunked(8192):
539
+ f.write(chunk)
540
+
541
+ return {"success": True, "file_path": save_path, "filename": filename}
542
+ except Exception as e:
543
+ return {"success": False, "error": str(e)}
544
+
545
+ @classmethod
546
+ def _merge_headers(cls, headers: Optional[Dict[str, str]], include_token: bool = True) -> Dict[str, str]:
547
+ if headers:
548
+ merged = cls._get_default_headers(include_token)
549
+ merged.update(headers)
550
+ return merged
551
+ return cls._get_default_headers(include_token)
@@ -0,0 +1,28 @@
1
+ """
2
+ logger工具 - 精简版
3
+ """
4
+
5
+ import logging
6
+ import os
7
+
8
+
9
+ def setup_logger():
10
+ log_level = os.getenv("LOG_LEVEL", "INFO")
11
+ logger = logging.getLogger("modelgate")
12
+ logger.setLevel(getattr(logging, log_level, logging.INFO))
13
+ logger.propagate = False
14
+
15
+ if not logger.handlers:
16
+ console_handler = logging.StreamHandler()
17
+ formatter = logging.Formatter(
18
+ "%(asctime)s - %(levelname)s - %(filename)s:%(lineno)d: %(message)s",
19
+ datefmt="%Y-%m-%d %H:%M:%S",
20
+ )
21
+ console_handler.setFormatter(formatter)
22
+ console_handler.setLevel(getattr(logging, log_level, logging.INFO))
23
+ logger.addHandler(console_handler)
24
+
25
+ return logger
26
+
27
+
28
+ logger = setup_logger()
@@ -0,0 +1,2 @@
1
+ # -*- coding: utf-8 -*-
2
+ from .configPresenter import configPresenter