@agentunion/kite 1.4.0 → 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.
- package/CHANGELOG.md +102 -0
- package/cli.js +44 -5
- package/core/dependency_checker.py +250 -0
- package/core/env_checker.py +490 -0
- package/dependencies_lock.json +128 -0
- package/extensions/agents/assistant/server.py +33 -17
- package/extensions/channels/acp_channel/server.py +33 -17
- package/extensions/services/backup/entry.py +23 -16
- package/extensions/services/evol/auth_manager.py +443 -0
- package/extensions/services/evol/config.yaml +149 -0
- package/extensions/services/evol/config_loader.py +117 -0
- package/extensions/services/evol/entry.py +406 -0
- package/extensions/services/evol/evol_api.py +173 -0
- package/extensions/services/evol/evol_config.json5 +29 -0
- package/extensions/services/evol/migrate_tokens.py +122 -0
- package/extensions/services/evol/module.md +32 -0
- package/extensions/services/evol/pairing.py +250 -0
- package/extensions/services/evol/pairing_codes.jsonl +1 -0
- package/extensions/services/evol/relay.py +682 -0
- package/extensions/services/evol/relay_config.json5 +67 -0
- package/extensions/services/evol/routes/__init__.py +1 -0
- package/extensions/services/evol/routes/routes_management_ws.py +127 -0
- package/extensions/services/evol/routes/routes_rpc.py +89 -0
- package/extensions/services/evol/routes/routes_test.py +61 -0
- package/extensions/services/evol/server.py +875 -0
- package/extensions/services/evol/static/css/style.css +1200 -0
- package/extensions/services/evol/static/index.html +781 -0
- package/extensions/services/evol/static/index_evol.html +14 -0
- package/extensions/services/evol/static/js/app.js +6304 -0
- package/extensions/services/evol/static/js/auth.js +326 -0
- package/extensions/services/evol/static/js/dialog.js +285 -0
- package/extensions/services/evol/static/js/evol-app-fixed.js +50 -0
- package/extensions/services/evol/static/js/evol-app.js +1949 -0
- package/extensions/services/evol/static/js/evol-app.js.bak +1800 -0
- package/extensions/services/evol/static/js/kernel-client-example.js +228 -0
- package/extensions/services/evol/static/js/kernel-client.js +396 -0
- package/extensions/services/evol/static/js/main.js +141 -0
- package/extensions/services/evol/static/js/registry-tests.js +585 -0
- package/extensions/services/evol/static/js/stats.js +217 -0
- package/extensions/services/evol/static/js/token-manager.js +175 -0
- package/extensions/services/evol/static/pairing.html +248 -0
- package/extensions/services/evol/static/test_registry.html +262 -0
- package/extensions/services/evol/static/test_relay.html +462 -0
- package/extensions/services/evol/stats_manager.py +240 -0
- package/extensions/services/model_service/entry.py +23 -1
- package/extensions/services/proxy/.claude/settings.local.json +13 -0
- package/extensions/services/proxy/CHANGELOG_20260308.md +258 -0
- package/extensions/services/proxy/_fix_prints.py +133 -0
- package/extensions/services/proxy/_fix_prints2.py +87 -0
- package/extensions/services/proxy/agentcp/LICENCE +178 -0
- package/extensions/services/proxy/agentcp/README copy.md +85 -0
- package/extensions/services/proxy/agentcp/README.md +260 -0
- package/extensions/services/proxy/agentcp/__init__.py +16 -0
- package/extensions/services/proxy/agentcp/agent.py +4 -0
- package/extensions/services/proxy/agentcp/agentcp.py +2494 -0
- package/extensions/services/proxy/agentcp/agentprofile.json +89 -0
- package/extensions/services/proxy/agentcp/ap/__init__.py +16 -0
- package/extensions/services/proxy/agentcp/ap/ap_client.py +316 -0
- package/extensions/services/proxy/agentcp/assets/images/wechat_qr.png +0 -0
- package/extensions/services/proxy/agentcp/backup/metrics.json +31 -0
- package/extensions/services/proxy/agentcp/base/__init__.py +20 -0
- package/extensions/services/proxy/agentcp/base/auth_client.py +257 -0
- package/extensions/services/proxy/agentcp/base/client.py +112 -0
- package/extensions/services/proxy/agentcp/base/env.py +34 -0
- package/extensions/services/proxy/agentcp/base/html_util.py +336 -0
- package/extensions/services/proxy/agentcp/base/log.py +98 -0
- package/extensions/services/proxy/agentcp/ca/__init__.py +17 -0
- package/extensions/services/proxy/agentcp/ca/ca_client.py +414 -0
- package/extensions/services/proxy/agentcp/ca/ca_root.py +74 -0
- package/extensions/services/proxy/agentcp/context/__init__.py +20 -0
- package/extensions/services/proxy/agentcp/context/context.py +73 -0
- package/extensions/services/proxy/agentcp/context/exceptions.py +114 -0
- package/extensions/services/proxy/agentcp/create_profile.py +125 -0
- package/extensions/services/proxy/agentcp/create_profile_weather.py +125 -0
- package/extensions/services/proxy/agentcp/db/__init__.py +15 -0
- package/extensions/services/proxy/agentcp/db/db_mananger.py +550 -0
- package/extensions/services/proxy/agentcp/docs/UDP_HEARTBEAT_FIX_REPORT.md +265 -0
- package/extensions/services/proxy/agentcp/docs/heartbeat_issue_analysis.md +291 -0
- package/extensions/services/proxy/agentcp/file/__init__.py +16 -0
- package/extensions/services/proxy/agentcp/file/file_client.py +141 -0
- package/extensions/services/proxy/agentcp/file/wss_binary_message.py +137 -0
- package/extensions/services/proxy/agentcp/hcp.py +299 -0
- package/extensions/services/proxy/agentcp/heartbeat/__init__.py +16 -0
- package/extensions/services/proxy/agentcp/heartbeat/heartbeat_client.py +360 -0
- package/extensions/services/proxy/agentcp/improved_scheduler.py +498 -0
- package/extensions/services/proxy/agentcp/llm_agent_utils.py +249 -0
- package/extensions/services/proxy/agentcp/llm_server.py +172 -0
- package/extensions/services/proxy/agentcp/mermaid.py +210 -0
- package/extensions/services/proxy/agentcp/message.py +149 -0
- package/extensions/services/proxy/agentcp/metrics.py +256 -0
- package/extensions/services/proxy/agentcp/monitoring/__init__.py +20 -0
- package/extensions/services/proxy/agentcp/monitoring/global_monitor.py +27 -0
- package/extensions/services/proxy/agentcp/monitoring/metrics_store.py +325 -0
- package/extensions/services/proxy/agentcp/monitoring/monitoring_service.py +269 -0
- package/extensions/services/proxy/agentcp/monitoring/sliding_window.py +222 -0
- package/extensions/services/proxy/agentcp/monitoring/standalone_reader.py +224 -0
- package/extensions/services/proxy/agentcp/msg/__init__.py +21 -0
- package/extensions/services/proxy/agentcp/msg/connection_manager.py +456 -0
- package/extensions/services/proxy/agentcp/msg/message_client.py +2058 -0
- package/extensions/services/proxy/agentcp/msg/message_serialize.py +263 -0
- package/extensions/services/proxy/agentcp/msg/open_ai_message.py +88 -0
- package/extensions/services/proxy/agentcp/msg/session_manager.py +1062 -0
- package/extensions/services/proxy/agentcp/msg/stream_client.py +267 -0
- package/extensions/services/proxy/agentcp/msg/websocket_file_receiver.py +89 -0
- package/extensions/services/proxy/agentcp/msg/ws_logger.py +685 -0
- package/extensions/services/proxy/agentcp/msg/wss_binary_message.py +137 -0
- package/extensions/services/proxy/agentcp/requirements.txt +7 -0
- package/extensions/services/proxy/agentcp/samples/agent_graph/README.md +37 -0
- package/extensions/services/proxy/agentcp/samples/agent_graph/agentprofile.json +89 -0
- package/extensions/services/proxy/agentcp/samples/agent_graph/create_profile.py +138 -0
- package/extensions/services/proxy/agentcp/samples/agent_graph/main.py +164 -0
- package/extensions/services/proxy/agentcp/samples/agent_use/create_profile.py +123 -0
- package/extensions/services/proxy/agentcp/samples/agent_use/llm/create_profile.py +129 -0
- package/extensions/services/proxy/agentcp/samples/agent_use/llm/env.json +5 -0
- package/extensions/services/proxy/agentcp/samples/agent_use/llm/main.py +146 -0
- package/extensions/services/proxy/agentcp/samples/agent_use/main.py +123 -0
- package/extensions/services/proxy/agentcp/samples/agent_use/readme.md +379 -0
- package/extensions/services/proxy/agentcp/samples/agent_use/search/create_profile.py +129 -0
- package/extensions/services/proxy/agentcp/samples/agent_use/search/main.py +28 -0
- package/extensions/services/proxy/agentcp/samples/agent_use/tool/create_profile.py +129 -0
- package/extensions/services/proxy/agentcp/samples/agent_use/tool/main.py +20 -0
- package/extensions/services/proxy/agentcp/samples/ali_amap/README.md +97 -0
- package/extensions/services/proxy/agentcp/samples/ali_amap/amap_agent.py +88 -0
- package/extensions/services/proxy/agentcp/samples/ali_amap/create_profile.py +125 -0
- package/extensions/services/proxy/agentcp/samples/compute_agent/agent/powershell.py +228 -0
- package/extensions/services/proxy/agentcp/samples/compute_agent/agent/software.py +63 -0
- package/extensions/services/proxy/agentcp/samples/compute_agent/agent/tools.py +36 -0
- package/extensions/services/proxy/agentcp/samples/compute_agent/browser_user.py +41 -0
- package/extensions/services/proxy/agentcp/samples/deepseek/README.md +79 -0
- package/extensions/services/proxy/agentcp/samples/deepseek/create_profile.py +126 -0
- package/extensions/services/proxy/agentcp/samples/deepseek/deepseek.py +42 -0
- package/extensions/services/proxy/agentcp/samples/dify_chat/README.md +78 -0
- package/extensions/services/proxy/agentcp/samples/dify_chat/create_profile.py +126 -0
- package/extensions/services/proxy/agentcp/samples/dify_chat/dify_chat.py +47 -0
- package/extensions/services/proxy/agentcp/samples/dify_workflow/README.md +78 -0
- package/extensions/services/proxy/agentcp/samples/dify_workflow/create_profile.py +126 -0
- package/extensions/services/proxy/agentcp/samples/dify_workflow/dify_workflow.py +46 -0
- package/extensions/services/proxy/agentcp/samples/executor/README.md +44 -0
- package/extensions/services/proxy/agentcp/samples/executor/agentprofile.json +89 -0
- package/extensions/services/proxy/agentcp/samples/executor/create_profile.py +139 -0
- package/extensions/services/proxy/agentcp/samples/executor/main.py +160 -0
- package/extensions/services/proxy/agentcp/samples/filereader/README.md +45 -0
- package/extensions/services/proxy/agentcp/samples/filereader/agentprofile.json +90 -0
- package/extensions/services/proxy/agentcp/samples/filereader/create_profile.py +137 -0
- package/extensions/services/proxy/agentcp/samples/filereader/main.py +253 -0
- package/extensions/services/proxy/agentcp/samples/filewriter/README.md +38 -0
- package/extensions/services/proxy/agentcp/samples/filewriter/agentprofile.json +91 -0
- package/extensions/services/proxy/agentcp/samples/filewriter/create_profile.py +138 -0
- package/extensions/services/proxy/agentcp/samples/filewriter/main.py +289 -0
- package/extensions/services/proxy/agentcp/samples/hcp/README.md +85 -0
- package/extensions/services/proxy/agentcp/samples/hcp/acp_weather_agent.zip +0 -0
- package/extensions/services/proxy/agentcp/samples/hcp/create_profile.py +125 -0
- package/extensions/services/proxy/agentcp/samples/hcp/hcp.py +237 -0
- package/extensions/services/proxy/agentcp/samples/helloworld/README.md +68 -0
- package/extensions/services/proxy/agentcp/samples/helloworld/hello_world.py +40 -0
- package/extensions/services/proxy/agentcp/samples/llm_agent/MEADME.md +117 -0
- package/extensions/services/proxy/agentcp/samples/llm_agent/create_profile.py +125 -0
- package/extensions/services/proxy/agentcp/samples/llm_agent/qwen_agent.py +136 -0
- package/extensions/services/proxy/agentcp/samples/local_llm_agent/README.md +90 -0
- package/extensions/services/proxy/agentcp/samples/local_llm_agent/create_profile.py +125 -0
- package/extensions/services/proxy/agentcp/samples/local_llm_agent/main.py +49 -0
- package/extensions/services/proxy/agentcp/samples/query_llm_from_agent/README.md +55 -0
- package/extensions/services/proxy/agentcp/samples/query_llm_from_agent/create_profile.py +125 -0
- package/extensions/services/proxy/agentcp/samples/query_llm_from_agent/main.py +23 -0
- package/extensions/services/proxy/agentcp/samples/query_weather_api_agent/README.md +103 -0
- package/extensions/services/proxy/agentcp/samples/query_weather_api_agent/create_profile.py +125 -0
- package/extensions/services/proxy/agentcp/samples/query_weather_api_agent/main.py +69 -0
- package/extensions/services/proxy/agentcp/samples/query_weather_from_agent/README.md +58 -0
- package/extensions/services/proxy/agentcp/samples/query_weather_from_agent/create_profile.py +125 -0
- package/extensions/services/proxy/agentcp/samples/query_weather_from_agent/main.py +25 -0
- package/extensions/services/proxy/agentcp/samples/qwen3/README.md +71 -0
- package/extensions/services/proxy/agentcp/samples/qwen3/create_profile.py +126 -0
- package/extensions/services/proxy/agentcp/samples/qwen3/qwen3.py +37 -0
- package/extensions/services/proxy/agentcp/samples/qwen3_tools/README.md +133 -0
- package/extensions/services/proxy/agentcp/samples/qwen3_tools/create_profile.py +126 -0
- package/extensions/services/proxy/agentcp/samples/qwen3_tools/qwen3_tools.py +98 -0
- package/extensions/services/proxy/agentcp/samples/search/create_profile_qwen.py +125 -0
- package/extensions/services/proxy/agentcp/samples/search/create_profile_search.py +125 -0
- package/extensions/services/proxy/agentcp/samples/search/qwen_agent.py +136 -0
- package/extensions/services/proxy/agentcp/samples/search/search_agent.py +170 -0
- package/extensions/services/proxy/agentcp/samples/wrapper_agently_to_agent/README.md +89 -0
- package/extensions/services/proxy/agentcp/samples/wrapper_agently_to_agent/create_profile.py +125 -0
- package/extensions/services/proxy/agentcp/samples/wrapper_agently_to_agent/main.py +44 -0
- package/extensions/services/proxy/agentcp/utils/__init__.py +15 -0
- package/extensions/services/proxy/agentcp/utils/file_util.py +117 -0
- package/extensions/services/proxy/agentcp/utils/proxy_bypass.py +99 -0
- package/extensions/services/proxy/agentcp/workflow.py +203 -0
- package/extensions/services/proxy/console_auth.py +109 -0
- package/extensions/services/proxy/evol/__init__.py +1 -0
- package/extensions/services/proxy/evol/config.py +37 -0
- package/extensions/services/proxy/evol/http/__init__.py +1 -0
- package/extensions/services/proxy/evol/http/async_http.py +551 -0
- package/extensions/services/proxy/evol/log.py +28 -0
- package/extensions/services/proxy/evol/presenter/__init__.py +2 -0
- package/extensions/services/proxy/evol/presenter/agentIdPresenter.py +1031 -0
- package/extensions/services/proxy/evol/presenter/apikeyPresenter.py +106 -0
- package/extensions/services/proxy/evol/presenter/configPresenter.py +1281 -0
- package/extensions/services/proxy/evol/presenter/userPresenter.py +477 -0
- package/extensions/services/proxy/evol/server/__init__.py +1 -0
- package/extensions/services/proxy/evol/server/claude_proxy_async.py +3430 -0
- package/extensions/services/proxy/evol/server/openclaw_proxy.py +1861 -0
- package/extensions/services/proxy/evol/server/proxy_config.py +15 -0
- package/extensions/services/proxy/evol/server/proxy_engine.py +501 -0
- package/extensions/services/proxy/evol/version.py +24 -0
- package/extensions/services/proxy/logs/websocket.log +260 -0
- package/extensions/services/proxy/main.py +240 -0
- package/extensions/services/proxy/requirements.txt +13 -0
- package/extensions/services/proxy/server.py +271 -0
- package/extensions/services/watchdog/entry.py +42 -16
- package/extensions/services/watchdog/module.md +1 -0
- package/extensions/services/watchdog/monitor.py +34 -4
- package/extensions/services/web/module.md +1 -1
- package/extensions/services/web/server.py +30 -18
- package/extensions/services/web/static/js/token-manager.js +10 -10
- package/kernel/entry.py +1 -1
- package/kernel/module.md +25 -1
- package/kernel/registry_store.py +2 -26
- package/kernel/rpc_router.py +36 -10
- package/kernel/server.py +106 -17
- package/kite_cli/commands/deps_install.py +67 -0
- package/kite_cli/commands/env_check.py +45 -0
- package/kite_cli/commands/prepare.py +49 -0
- package/kite_cli/commands/venv_setup.py +56 -0
- package/kite_cli/main.py +29 -1
- package/launcher/entry.py +306 -21
- package/launcher/module.md +9 -0
- package/launcher/module_scanner.py +11 -1
- package/main.py +4 -1
- package/package.json +8 -1
- package/python_version.json +4 -0
- package/requirements.txt +38 -0
- package/scripts/env-manager.js +328 -0
- package/scripts/python-env.js +79 -0
- package/scripts/scan_dependencies.py +461 -0
- package/scripts/setup-python-env.js +191 -0
|
@@ -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()
|