@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.
Files changed (235) hide show
  1. package/CHANGELOG.md +102 -0
  2. package/cli.js +44 -5
  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/server.py +33 -17
  7. package/extensions/channels/acp_channel/server.py +33 -17
  8. package/extensions/services/backup/entry.py +23 -16
  9. package/extensions/services/evol/auth_manager.py +443 -0
  10. package/extensions/services/evol/config.yaml +149 -0
  11. package/extensions/services/evol/config_loader.py +117 -0
  12. package/extensions/services/evol/entry.py +406 -0
  13. package/extensions/services/evol/evol_api.py +173 -0
  14. package/extensions/services/evol/evol_config.json5 +29 -0
  15. package/extensions/services/evol/migrate_tokens.py +122 -0
  16. package/extensions/services/evol/module.md +32 -0
  17. package/extensions/services/evol/pairing.py +250 -0
  18. package/extensions/services/evol/pairing_codes.jsonl +1 -0
  19. package/extensions/services/evol/relay.py +682 -0
  20. package/extensions/services/evol/relay_config.json5 +67 -0
  21. package/extensions/services/evol/routes/__init__.py +1 -0
  22. package/extensions/services/evol/routes/routes_management_ws.py +127 -0
  23. package/extensions/services/evol/routes/routes_rpc.py +89 -0
  24. package/extensions/services/evol/routes/routes_test.py +61 -0
  25. package/extensions/services/evol/server.py +875 -0
  26. package/extensions/services/evol/static/css/style.css +1200 -0
  27. package/extensions/services/evol/static/index.html +781 -0
  28. package/extensions/services/evol/static/index_evol.html +14 -0
  29. package/extensions/services/evol/static/js/app.js +6304 -0
  30. package/extensions/services/evol/static/js/auth.js +326 -0
  31. package/extensions/services/evol/static/js/dialog.js +285 -0
  32. package/extensions/services/evol/static/js/evol-app-fixed.js +50 -0
  33. package/extensions/services/evol/static/js/evol-app.js +1949 -0
  34. package/extensions/services/evol/static/js/evol-app.js.bak +1800 -0
  35. package/extensions/services/evol/static/js/kernel-client-example.js +228 -0
  36. package/extensions/services/evol/static/js/kernel-client.js +396 -0
  37. package/extensions/services/evol/static/js/main.js +141 -0
  38. package/extensions/services/evol/static/js/registry-tests.js +585 -0
  39. package/extensions/services/evol/static/js/stats.js +217 -0
  40. package/extensions/services/evol/static/js/token-manager.js +175 -0
  41. package/extensions/services/evol/static/pairing.html +248 -0
  42. package/extensions/services/evol/static/test_registry.html +262 -0
  43. package/extensions/services/evol/static/test_relay.html +462 -0
  44. package/extensions/services/evol/stats_manager.py +240 -0
  45. package/extensions/services/model_service/entry.py +23 -1
  46. package/extensions/services/proxy/.claude/settings.local.json +13 -0
  47. package/extensions/services/proxy/CHANGELOG_20260308.md +258 -0
  48. package/extensions/services/proxy/_fix_prints.py +133 -0
  49. package/extensions/services/proxy/_fix_prints2.py +87 -0
  50. package/extensions/services/proxy/agentcp/LICENCE +178 -0
  51. package/extensions/services/proxy/agentcp/README copy.md +85 -0
  52. package/extensions/services/proxy/agentcp/README.md +260 -0
  53. package/extensions/services/proxy/agentcp/__init__.py +16 -0
  54. package/extensions/services/proxy/agentcp/agent.py +4 -0
  55. package/extensions/services/proxy/agentcp/agentcp.py +2494 -0
  56. package/extensions/services/proxy/agentcp/agentprofile.json +89 -0
  57. package/extensions/services/proxy/agentcp/ap/__init__.py +16 -0
  58. package/extensions/services/proxy/agentcp/ap/ap_client.py +316 -0
  59. package/extensions/services/proxy/agentcp/assets/images/wechat_qr.png +0 -0
  60. package/extensions/services/proxy/agentcp/backup/metrics.json +31 -0
  61. package/extensions/services/proxy/agentcp/base/__init__.py +20 -0
  62. package/extensions/services/proxy/agentcp/base/auth_client.py +257 -0
  63. package/extensions/services/proxy/agentcp/base/client.py +112 -0
  64. package/extensions/services/proxy/agentcp/base/env.py +34 -0
  65. package/extensions/services/proxy/agentcp/base/html_util.py +336 -0
  66. package/extensions/services/proxy/agentcp/base/log.py +98 -0
  67. package/extensions/services/proxy/agentcp/ca/__init__.py +17 -0
  68. package/extensions/services/proxy/agentcp/ca/ca_client.py +414 -0
  69. package/extensions/services/proxy/agentcp/ca/ca_root.py +74 -0
  70. package/extensions/services/proxy/agentcp/context/__init__.py +20 -0
  71. package/extensions/services/proxy/agentcp/context/context.py +73 -0
  72. package/extensions/services/proxy/agentcp/context/exceptions.py +114 -0
  73. package/extensions/services/proxy/agentcp/create_profile.py +125 -0
  74. package/extensions/services/proxy/agentcp/create_profile_weather.py +125 -0
  75. package/extensions/services/proxy/agentcp/db/__init__.py +15 -0
  76. package/extensions/services/proxy/agentcp/db/db_mananger.py +550 -0
  77. package/extensions/services/proxy/agentcp/docs/UDP_HEARTBEAT_FIX_REPORT.md +265 -0
  78. package/extensions/services/proxy/agentcp/docs/heartbeat_issue_analysis.md +291 -0
  79. package/extensions/services/proxy/agentcp/file/__init__.py +16 -0
  80. package/extensions/services/proxy/agentcp/file/file_client.py +141 -0
  81. package/extensions/services/proxy/agentcp/file/wss_binary_message.py +137 -0
  82. package/extensions/services/proxy/agentcp/hcp.py +299 -0
  83. package/extensions/services/proxy/agentcp/heartbeat/__init__.py +16 -0
  84. package/extensions/services/proxy/agentcp/heartbeat/heartbeat_client.py +360 -0
  85. package/extensions/services/proxy/agentcp/improved_scheduler.py +498 -0
  86. package/extensions/services/proxy/agentcp/llm_agent_utils.py +249 -0
  87. package/extensions/services/proxy/agentcp/llm_server.py +172 -0
  88. package/extensions/services/proxy/agentcp/mermaid.py +210 -0
  89. package/extensions/services/proxy/agentcp/message.py +149 -0
  90. package/extensions/services/proxy/agentcp/metrics.py +256 -0
  91. package/extensions/services/proxy/agentcp/monitoring/__init__.py +20 -0
  92. package/extensions/services/proxy/agentcp/monitoring/global_monitor.py +27 -0
  93. package/extensions/services/proxy/agentcp/monitoring/metrics_store.py +325 -0
  94. package/extensions/services/proxy/agentcp/monitoring/monitoring_service.py +269 -0
  95. package/extensions/services/proxy/agentcp/monitoring/sliding_window.py +222 -0
  96. package/extensions/services/proxy/agentcp/monitoring/standalone_reader.py +224 -0
  97. package/extensions/services/proxy/agentcp/msg/__init__.py +21 -0
  98. package/extensions/services/proxy/agentcp/msg/connection_manager.py +456 -0
  99. package/extensions/services/proxy/agentcp/msg/message_client.py +2058 -0
  100. package/extensions/services/proxy/agentcp/msg/message_serialize.py +263 -0
  101. package/extensions/services/proxy/agentcp/msg/open_ai_message.py +88 -0
  102. package/extensions/services/proxy/agentcp/msg/session_manager.py +1062 -0
  103. package/extensions/services/proxy/agentcp/msg/stream_client.py +267 -0
  104. package/extensions/services/proxy/agentcp/msg/websocket_file_receiver.py +89 -0
  105. package/extensions/services/proxy/agentcp/msg/ws_logger.py +685 -0
  106. package/extensions/services/proxy/agentcp/msg/wss_binary_message.py +137 -0
  107. package/extensions/services/proxy/agentcp/requirements.txt +7 -0
  108. package/extensions/services/proxy/agentcp/samples/agent_graph/README.md +37 -0
  109. package/extensions/services/proxy/agentcp/samples/agent_graph/agentprofile.json +89 -0
  110. package/extensions/services/proxy/agentcp/samples/agent_graph/create_profile.py +138 -0
  111. package/extensions/services/proxy/agentcp/samples/agent_graph/main.py +164 -0
  112. package/extensions/services/proxy/agentcp/samples/agent_use/create_profile.py +123 -0
  113. package/extensions/services/proxy/agentcp/samples/agent_use/llm/create_profile.py +129 -0
  114. package/extensions/services/proxy/agentcp/samples/agent_use/llm/env.json +5 -0
  115. package/extensions/services/proxy/agentcp/samples/agent_use/llm/main.py +146 -0
  116. package/extensions/services/proxy/agentcp/samples/agent_use/main.py +123 -0
  117. package/extensions/services/proxy/agentcp/samples/agent_use/readme.md +379 -0
  118. package/extensions/services/proxy/agentcp/samples/agent_use/search/create_profile.py +129 -0
  119. package/extensions/services/proxy/agentcp/samples/agent_use/search/main.py +28 -0
  120. package/extensions/services/proxy/agentcp/samples/agent_use/tool/create_profile.py +129 -0
  121. package/extensions/services/proxy/agentcp/samples/agent_use/tool/main.py +20 -0
  122. package/extensions/services/proxy/agentcp/samples/ali_amap/README.md +97 -0
  123. package/extensions/services/proxy/agentcp/samples/ali_amap/amap_agent.py +88 -0
  124. package/extensions/services/proxy/agentcp/samples/ali_amap/create_profile.py +125 -0
  125. package/extensions/services/proxy/agentcp/samples/compute_agent/agent/powershell.py +228 -0
  126. package/extensions/services/proxy/agentcp/samples/compute_agent/agent/software.py +63 -0
  127. package/extensions/services/proxy/agentcp/samples/compute_agent/agent/tools.py +36 -0
  128. package/extensions/services/proxy/agentcp/samples/compute_agent/browser_user.py +41 -0
  129. package/extensions/services/proxy/agentcp/samples/deepseek/README.md +79 -0
  130. package/extensions/services/proxy/agentcp/samples/deepseek/create_profile.py +126 -0
  131. package/extensions/services/proxy/agentcp/samples/deepseek/deepseek.py +42 -0
  132. package/extensions/services/proxy/agentcp/samples/dify_chat/README.md +78 -0
  133. package/extensions/services/proxy/agentcp/samples/dify_chat/create_profile.py +126 -0
  134. package/extensions/services/proxy/agentcp/samples/dify_chat/dify_chat.py +47 -0
  135. package/extensions/services/proxy/agentcp/samples/dify_workflow/README.md +78 -0
  136. package/extensions/services/proxy/agentcp/samples/dify_workflow/create_profile.py +126 -0
  137. package/extensions/services/proxy/agentcp/samples/dify_workflow/dify_workflow.py +46 -0
  138. package/extensions/services/proxy/agentcp/samples/executor/README.md +44 -0
  139. package/extensions/services/proxy/agentcp/samples/executor/agentprofile.json +89 -0
  140. package/extensions/services/proxy/agentcp/samples/executor/create_profile.py +139 -0
  141. package/extensions/services/proxy/agentcp/samples/executor/main.py +160 -0
  142. package/extensions/services/proxy/agentcp/samples/filereader/README.md +45 -0
  143. package/extensions/services/proxy/agentcp/samples/filereader/agentprofile.json +90 -0
  144. package/extensions/services/proxy/agentcp/samples/filereader/create_profile.py +137 -0
  145. package/extensions/services/proxy/agentcp/samples/filereader/main.py +253 -0
  146. package/extensions/services/proxy/agentcp/samples/filewriter/README.md +38 -0
  147. package/extensions/services/proxy/agentcp/samples/filewriter/agentprofile.json +91 -0
  148. package/extensions/services/proxy/agentcp/samples/filewriter/create_profile.py +138 -0
  149. package/extensions/services/proxy/agentcp/samples/filewriter/main.py +289 -0
  150. package/extensions/services/proxy/agentcp/samples/hcp/README.md +85 -0
  151. package/extensions/services/proxy/agentcp/samples/hcp/acp_weather_agent.zip +0 -0
  152. package/extensions/services/proxy/agentcp/samples/hcp/create_profile.py +125 -0
  153. package/extensions/services/proxy/agentcp/samples/hcp/hcp.py +237 -0
  154. package/extensions/services/proxy/agentcp/samples/helloworld/README.md +68 -0
  155. package/extensions/services/proxy/agentcp/samples/helloworld/hello_world.py +40 -0
  156. package/extensions/services/proxy/agentcp/samples/llm_agent/MEADME.md +117 -0
  157. package/extensions/services/proxy/agentcp/samples/llm_agent/create_profile.py +125 -0
  158. package/extensions/services/proxy/agentcp/samples/llm_agent/qwen_agent.py +136 -0
  159. package/extensions/services/proxy/agentcp/samples/local_llm_agent/README.md +90 -0
  160. package/extensions/services/proxy/agentcp/samples/local_llm_agent/create_profile.py +125 -0
  161. package/extensions/services/proxy/agentcp/samples/local_llm_agent/main.py +49 -0
  162. package/extensions/services/proxy/agentcp/samples/query_llm_from_agent/README.md +55 -0
  163. package/extensions/services/proxy/agentcp/samples/query_llm_from_agent/create_profile.py +125 -0
  164. package/extensions/services/proxy/agentcp/samples/query_llm_from_agent/main.py +23 -0
  165. package/extensions/services/proxy/agentcp/samples/query_weather_api_agent/README.md +103 -0
  166. package/extensions/services/proxy/agentcp/samples/query_weather_api_agent/create_profile.py +125 -0
  167. package/extensions/services/proxy/agentcp/samples/query_weather_api_agent/main.py +69 -0
  168. package/extensions/services/proxy/agentcp/samples/query_weather_from_agent/README.md +58 -0
  169. package/extensions/services/proxy/agentcp/samples/query_weather_from_agent/create_profile.py +125 -0
  170. package/extensions/services/proxy/agentcp/samples/query_weather_from_agent/main.py +25 -0
  171. package/extensions/services/proxy/agentcp/samples/qwen3/README.md +71 -0
  172. package/extensions/services/proxy/agentcp/samples/qwen3/create_profile.py +126 -0
  173. package/extensions/services/proxy/agentcp/samples/qwen3/qwen3.py +37 -0
  174. package/extensions/services/proxy/agentcp/samples/qwen3_tools/README.md +133 -0
  175. package/extensions/services/proxy/agentcp/samples/qwen3_tools/create_profile.py +126 -0
  176. package/extensions/services/proxy/agentcp/samples/qwen3_tools/qwen3_tools.py +98 -0
  177. package/extensions/services/proxy/agentcp/samples/search/create_profile_qwen.py +125 -0
  178. package/extensions/services/proxy/agentcp/samples/search/create_profile_search.py +125 -0
  179. package/extensions/services/proxy/agentcp/samples/search/qwen_agent.py +136 -0
  180. package/extensions/services/proxy/agentcp/samples/search/search_agent.py +170 -0
  181. package/extensions/services/proxy/agentcp/samples/wrapper_agently_to_agent/README.md +89 -0
  182. package/extensions/services/proxy/agentcp/samples/wrapper_agently_to_agent/create_profile.py +125 -0
  183. package/extensions/services/proxy/agentcp/samples/wrapper_agently_to_agent/main.py +44 -0
  184. package/extensions/services/proxy/agentcp/utils/__init__.py +15 -0
  185. package/extensions/services/proxy/agentcp/utils/file_util.py +117 -0
  186. package/extensions/services/proxy/agentcp/utils/proxy_bypass.py +99 -0
  187. package/extensions/services/proxy/agentcp/workflow.py +203 -0
  188. package/extensions/services/proxy/console_auth.py +109 -0
  189. package/extensions/services/proxy/evol/__init__.py +1 -0
  190. package/extensions/services/proxy/evol/config.py +37 -0
  191. package/extensions/services/proxy/evol/http/__init__.py +1 -0
  192. package/extensions/services/proxy/evol/http/async_http.py +551 -0
  193. package/extensions/services/proxy/evol/log.py +28 -0
  194. package/extensions/services/proxy/evol/presenter/__init__.py +2 -0
  195. package/extensions/services/proxy/evol/presenter/agentIdPresenter.py +1031 -0
  196. package/extensions/services/proxy/evol/presenter/apikeyPresenter.py +106 -0
  197. package/extensions/services/proxy/evol/presenter/configPresenter.py +1281 -0
  198. package/extensions/services/proxy/evol/presenter/userPresenter.py +477 -0
  199. package/extensions/services/proxy/evol/server/__init__.py +1 -0
  200. package/extensions/services/proxy/evol/server/claude_proxy_async.py +3430 -0
  201. package/extensions/services/proxy/evol/server/openclaw_proxy.py +1861 -0
  202. package/extensions/services/proxy/evol/server/proxy_config.py +15 -0
  203. package/extensions/services/proxy/evol/server/proxy_engine.py +501 -0
  204. package/extensions/services/proxy/evol/version.py +24 -0
  205. package/extensions/services/proxy/logs/websocket.log +260 -0
  206. package/extensions/services/proxy/main.py +240 -0
  207. package/extensions/services/proxy/requirements.txt +13 -0
  208. package/extensions/services/proxy/server.py +271 -0
  209. package/extensions/services/watchdog/entry.py +42 -16
  210. package/extensions/services/watchdog/module.md +1 -0
  211. package/extensions/services/watchdog/monitor.py +34 -4
  212. package/extensions/services/web/module.md +1 -1
  213. package/extensions/services/web/server.py +30 -18
  214. package/extensions/services/web/static/js/token-manager.js +10 -10
  215. package/kernel/entry.py +1 -1
  216. package/kernel/module.md +25 -1
  217. package/kernel/registry_store.py +2 -26
  218. package/kernel/rpc_router.py +36 -10
  219. package/kernel/server.py +106 -17
  220. package/kite_cli/commands/deps_install.py +67 -0
  221. package/kite_cli/commands/env_check.py +45 -0
  222. package/kite_cli/commands/prepare.py +49 -0
  223. package/kite_cli/commands/venv_setup.py +56 -0
  224. package/kite_cli/main.py +29 -1
  225. package/launcher/entry.py +306 -21
  226. package/launcher/module.md +9 -0
  227. package/launcher/module_scanner.py +11 -1
  228. package/main.py +4 -1
  229. package/package.json +8 -1
  230. package/python_version.json +4 -0
  231. package/requirements.txt +38 -0
  232. package/scripts/env-manager.js +328 -0
  233. package/scripts/python-env.js +79 -0
  234. package/scripts/scan_dependencies.py +461 -0
  235. package/scripts/setup-python-env.js +191 -0
@@ -0,0 +1,685 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ WebSocket 专用日志模块
4
+
5
+ 独立记录 WebSocket 连接的断开、重连、错误等事件。
6
+ 日志文件: logs/websocket.log
7
+
8
+ 使用直接文件写入方式,避免与其他日志库冲突。
9
+ """
10
+
11
+ import os
12
+ import json
13
+ import threading
14
+ import sys
15
+ import time
16
+ from datetime import datetime
17
+ from typing import Optional, Dict, Any
18
+
19
+ # 跨平台文件锁支持
20
+ if sys.platform == 'win32':
21
+ import msvcrt
22
+ # Windows: 使用 msvcrt.locking,锁定文件开头的一段区域
23
+ _LOCK_BYTES = 1024 * 1024 # 锁定 1MB 区域(足够覆盖日志写入)
24
+
25
+ def lock_file(f):
26
+ """获取文件锁(Windows)"""
27
+ try:
28
+ # 移动到文件开头进行锁定
29
+ f.seek(0)
30
+ msvcrt.locking(f.fileno(), msvcrt.LK_NBLCK, _LOCK_BYTES)
31
+ except (IOError, OSError):
32
+ # 锁定失败(可能被其他进程占用),忽略继续写入
33
+ pass
34
+
35
+ def unlock_file(f):
36
+ """释放文件锁(Windows)"""
37
+ try:
38
+ f.seek(0)
39
+ msvcrt.locking(f.fileno(), msvcrt.LK_UNLCK, _LOCK_BYTES)
40
+ except (IOError, OSError, ValueError):
41
+ # 解锁失败,忽略
42
+ pass
43
+ else:
44
+ import fcntl
45
+
46
+ def lock_file(f):
47
+ """获取文件锁(Unix/Linux/Mac)"""
48
+ try:
49
+ fcntl.flock(f.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB)
50
+ except (IOError, OSError):
51
+ # 锁定失败,忽略继续写入
52
+ pass
53
+
54
+ def unlock_file(f):
55
+ """释放文件锁(Unix/Linux/Mac)"""
56
+ try:
57
+ fcntl.flock(f.fileno(), fcntl.LOCK_UN)
58
+ except (IOError, OSError):
59
+ pass
60
+
61
+
62
+ class WebSocketLogger:
63
+ """WebSocket 专用日志记录器 - 直接文件写入版本"""
64
+
65
+ _instance = None
66
+ _lock = threading.Lock()
67
+
68
+ def __new__(cls):
69
+ if cls._instance is None:
70
+ with cls._lock:
71
+ if cls._instance is None:
72
+ cls._instance = super().__new__(cls)
73
+ cls._instance._initialized = False
74
+ return cls._instance
75
+
76
+ def __init__(self):
77
+ if self._initialized:
78
+ return
79
+ self._initialized = True
80
+
81
+ # 文件写入锁
82
+ self._file_lock = threading.Lock()
83
+
84
+ try:
85
+ # 创建日志目录
86
+ self.log_dir = os.path.join(os.getcwd(), "logs")
87
+ os.makedirs(self.log_dir, exist_ok=True)
88
+
89
+ # 日志文件路径
90
+ self.log_file = os.path.join(self.log_dir, "websocket.log")
91
+
92
+ # 最大文件大小 (10MB)
93
+ self.max_file_size = 10 * 1024 * 1024
94
+ # 保留备份数量
95
+ self.backup_count = 5
96
+
97
+ self._logger_ready = True
98
+
99
+ except Exception as e:
100
+ print(f"[WARNING] WebSocket 日志初始化失败: {e}")
101
+ self._logger_ready = False
102
+ self.log_file = None
103
+
104
+ # 统计信息
105
+ self._stats = {
106
+ "disconnect_count": 0,
107
+ "reconnect_count": 0,
108
+ "reconnect_success_count": 0,
109
+ "reconnect_fail_count": 0,
110
+ "last_disconnect_time": None,
111
+ "last_reconnect_time": None,
112
+ "last_error": None
113
+ }
114
+ self._stats_lock = threading.Lock()
115
+
116
+ def _rotate_if_needed(self):
117
+ """检查并执行日志轮转"""
118
+ if not self.log_file or not os.path.exists(self.log_file):
119
+ return
120
+
121
+ try:
122
+ file_size = os.path.getsize(self.log_file)
123
+ if file_size >= self.max_file_size:
124
+ # 执行轮转
125
+ for i in range(self.backup_count - 1, 0, -1):
126
+ old_file = f"{self.log_file}.{i}"
127
+ new_file = f"{self.log_file}.{i + 1}"
128
+ if os.path.exists(old_file):
129
+ if os.path.exists(new_file):
130
+ os.remove(new_file)
131
+ os.rename(old_file, new_file)
132
+
133
+ # 将当前日志重命名为 .1
134
+ backup_file = f"{self.log_file}.1"
135
+ if os.path.exists(backup_file):
136
+ os.remove(backup_file)
137
+ os.rename(self.log_file, backup_file)
138
+
139
+ except Exception as e:
140
+ # 轮转失败不影响日志写入
141
+ print(f"[WARNING] 日志轮转失败: {e}")
142
+
143
+ def _write_log(self, level: str, message: str):
144
+ """直接写入日志文件(线程安全)"""
145
+ if not self._logger_ready or not self.log_file:
146
+ # 如果日志系统不可用,输出到标准输出
147
+ print(f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] [{level}] {message}")
148
+ return
149
+
150
+ timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
151
+ log_line = f"[{timestamp}] [{level}] {message}\n"
152
+
153
+ with self._file_lock:
154
+ try:
155
+ # 检查是否需要轮转
156
+ self._rotate_if_needed()
157
+
158
+ # 直接写入文件
159
+ with open(self.log_file, 'a', encoding='utf-8') as f:
160
+ # 尝试使用文件锁(跨进程安全)
161
+ locked = False
162
+ try:
163
+ lock_file(f)
164
+ locked = True
165
+ # 写入时移动到文件末尾(append模式已自动处理,但显式调用更安全)
166
+ f.seek(0, 2) # SEEK_END
167
+ f.write(log_line)
168
+ f.flush()
169
+ os.fsync(f.fileno()) # 确保写入磁盘
170
+ except (IOError, OSError) as write_err:
171
+ # 写入失败,尝试不带 fsync 写入
172
+ try:
173
+ f.write(log_line)
174
+ f.flush()
175
+ except Exception:
176
+ raise write_err
177
+ finally:
178
+ # 确保始终尝试解锁
179
+ if locked:
180
+ unlock_file(f)
181
+
182
+ except Exception as e:
183
+ # 写入失败时输出到标准输出
184
+ print(f"[WARNING] 写入日志文件失败: {e}")
185
+ print(log_line.strip())
186
+
187
+ def _format_data(self, data: Any, max_length: int = 500) -> str:
188
+ """格式化数据用于日志记录,限制长度"""
189
+ if data is None:
190
+ return "None"
191
+ try:
192
+ if isinstance(data, bytes):
193
+ try:
194
+ data_str = data.decode('utf-8')
195
+ except UnicodeDecodeError:
196
+ data_str = f"<binary data, length={len(data)}>"
197
+ elif isinstance(data, dict):
198
+ data_str = json.dumps(data, ensure_ascii=False, indent=2)
199
+ else:
200
+ data_str = str(data)
201
+
202
+ if len(data_str) > max_length:
203
+ return data_str[:max_length] + f"... (truncated, total {len(data_str)} chars)"
204
+ return data_str
205
+ except Exception as e:
206
+ return f"<format error: {e}>"
207
+
208
+ def log_disconnect(
209
+ self,
210
+ conn_id: int,
211
+ reason: str,
212
+ code: Optional[int] = None,
213
+ received_data: Any = None,
214
+ pending_requests: int = 0,
215
+ extra_info: Optional[Dict] = None
216
+ ):
217
+ """记录连接断开事件"""
218
+ with self._stats_lock:
219
+ self._stats["disconnect_count"] += 1
220
+ self._stats["last_disconnect_time"] = datetime.now().isoformat()
221
+ self._stats["last_error"] = reason
222
+
223
+ log_lines = [
224
+ "=" * 80,
225
+ "CONNECTION DISCONNECTED",
226
+ "=" * 80,
227
+ f" Connection ID : {conn_id}",
228
+ f" Disconnect Time : {datetime.now().strftime('%Y-%m-%d %H:%M:%S.%f')}",
229
+ f" Close Code : {code if code else 'N/A'}",
230
+ f" Reason : {reason}",
231
+ f" Pending Requests : {pending_requests}",
232
+ ]
233
+
234
+ if received_data:
235
+ log_lines.append(f" Received Data : {self._format_data(received_data)}")
236
+
237
+ if extra_info:
238
+ try:
239
+ log_lines.append(f" Extra Info : {json.dumps(extra_info, ensure_ascii=False)}")
240
+ except (TypeError, ValueError):
241
+ log_lines.append(f" Extra Info : {str(extra_info)}")
242
+
243
+ log_lines.append("=" * 80)
244
+
245
+ self._write_log("WARNING", "\n".join(log_lines))
246
+
247
+ def log_reconnect_start(self, conn_id: int, attempt: int, interval: float):
248
+ """记录开始重连"""
249
+ self._write_log(
250
+ "INFO",
251
+ f"[RECONNECT START] conn_id={conn_id}, attempt={attempt}, interval={interval:.1f}s"
252
+ )
253
+
254
+ def log_reconnect_success(
255
+ self,
256
+ conn_id: int,
257
+ attempt: int,
258
+ duration: float,
259
+ pending_recovered: int = 0
260
+ ):
261
+ """记录重连成功"""
262
+ with self._stats_lock:
263
+ self._stats["reconnect_count"] += 1
264
+ self._stats["reconnect_success_count"] += 1
265
+ self._stats["last_reconnect_time"] = datetime.now().isoformat()
266
+
267
+ log_lines = [
268
+ "-" * 60,
269
+ "RECONNECTION SUCCESSFUL",
270
+ "-" * 60,
271
+ f" New Connection ID : {conn_id}",
272
+ f" Reconnect Time : {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}",
273
+ f" Attempts : {attempt}",
274
+ f" Duration : {duration:.2f}s",
275
+ f" Pending Recovered : {pending_recovered}",
276
+ "-" * 60
277
+ ]
278
+
279
+ self._write_log("INFO", "\n".join(log_lines))
280
+
281
+ def log_reconnect_fail(self, conn_id: int, attempt: int, reason: str):
282
+ """记录重连失败"""
283
+ with self._stats_lock:
284
+ self._stats["reconnect_count"] += 1
285
+ self._stats["reconnect_fail_count"] += 1
286
+
287
+ self._write_log(
288
+ "ERROR",
289
+ f"[RECONNECT FAILED] conn_id={conn_id}, attempt={attempt}, reason={reason}"
290
+ )
291
+
292
+ def log_connection_closed(
293
+ self,
294
+ conn_id: int,
295
+ code: int,
296
+ reason: str,
297
+ connection_duration: float = 0,
298
+ messages_received: int = 0,
299
+ last_pong_time: float = 0,
300
+ extra_info: Optional[Dict] = None
301
+ ):
302
+ """记录连接关闭事件(增强版,包含诊断信息)"""
303
+ from datetime import datetime as dt
304
+
305
+ # 计算最后一次 pong 距离现在的时间
306
+ time_since_last_pong = time.time() - last_pong_time if last_pong_time > 0 else -1
307
+
308
+ log_lines = [
309
+ "X" * 80,
310
+ "CONNECTION CLOSED (DETAILED)",
311
+ "X" * 80,
312
+ f" Connection ID : {conn_id}",
313
+ f" Close Time : {dt.now().strftime('%Y-%m-%d %H:%M:%S.%f')}",
314
+ f" Close Code : {code}",
315
+ f" Close Reason : {reason}",
316
+ f" Connection Duration: {connection_duration:.2f}s",
317
+ f" Messages Received : {messages_received}",
318
+ f" Time Since Pong : {time_since_last_pong:.2f}s" if time_since_last_pong >= 0 else f" Time Since Pong : N/A",
319
+ ]
320
+
321
+ if extra_info:
322
+ log_lines.append(" --- Extra Info ---")
323
+ for key, value in extra_info.items():
324
+ # 特殊处理消息类型列表,使其更易读
325
+ if key == "recent_msg_types" and isinstance(value, list):
326
+ if value:
327
+ log_lines.append(f" {key:18}: {', '.join(value)}")
328
+ else:
329
+ log_lines.append(f" {key:18}: (none)")
330
+ else:
331
+ log_lines.append(f" {key:18}: {value}")
332
+
333
+ # 添加诊断提示
334
+ if code == 1006:
335
+ log_lines.append(" --- Diagnosis ---")
336
+ log_lines.append(" Code 1006 表示异常关闭,可能的原因:")
337
+ log_lines.append(" 1. 网络中断或不稳定")
338
+ log_lines.append(" 2. 服务器主动断开但未发送关闭帧")
339
+ log_lines.append(" 3. 心跳超时(检查 ping_interval 和 ping_timeout 配置)")
340
+ log_lines.append(" 4. 防火墙/代理/负载均衡器超时断开")
341
+ if connection_duration < 60:
342
+ log_lines.append(f" 5. 连接仅存活 {connection_duration:.1f}s,可能是认证失败或服务器拒绝")
343
+ if time_since_last_pong > 30:
344
+ log_lines.append(f" 6. 距离上次心跳响应已 {time_since_last_pong:.1f}s,可能是心跳超时")
345
+
346
+ log_lines.append("X" * 80)
347
+
348
+ self._write_log("ERROR", "\n".join(log_lines))
349
+
350
+ def log_full_reset(
351
+ self,
352
+ conn_id: int,
353
+ queue_cleared: int,
354
+ streams_cleared: int
355
+ ):
356
+ """记录完全重置事件"""
357
+ log_lines = [
358
+ "🔄" * 40,
359
+ "FULL RESET EXECUTED",
360
+ "🔄" * 40,
361
+ f" Connection ID : {conn_id}",
362
+ f" Reset Time : {datetime.now().strftime('%Y-%m-%d %H:%M:%S.%f')}",
363
+ f" Queue Cleared : {queue_cleared} messages discarded",
364
+ f" Streams Cleared : {streams_cleared} pending requests cleared",
365
+ f" Connection ID Reset: Yes (will start from 1)",
366
+ "🔄" * 40
367
+ ]
368
+
369
+ self._write_log("WARNING", "\n".join(log_lines))
370
+
371
+ def log_abnormal_data(
372
+ self,
373
+ conn_id: int,
374
+ data: Any,
375
+ error: str,
376
+ data_type: str = "unknown"
377
+ ):
378
+ """记录异常数据"""
379
+ log_lines = [
380
+ "!" * 60,
381
+ "ABNORMAL DATA RECEIVED",
382
+ "!" * 60,
383
+ f" Connection ID : {conn_id}",
384
+ f" Time : {datetime.now().strftime('%Y-%m-%d %H:%M:%S.%f')}",
385
+ f" Data Type : {data_type}",
386
+ f" Error : {error}",
387
+ f" Data Content : {self._format_data(data, max_length=1000)}",
388
+ "!" * 60
389
+ ]
390
+
391
+ self._write_log("ERROR", "\n".join(log_lines))
392
+
393
+ def log_connection_established(
394
+ self,
395
+ conn_id: int,
396
+ ws_url: str,
397
+ extra_info: Optional[Dict] = None
398
+ ):
399
+ """记录连接建立成功"""
400
+ log_lines = [
401
+ "=" * 60,
402
+ "CONNECTION ESTABLISHED",
403
+ "=" * 60,
404
+ f" Connection ID : {conn_id}",
405
+ f" Time : {datetime.now().strftime('%Y-%m-%d %H:%M:%S.%f')}",
406
+ f" URL : {ws_url[:100] if ws_url else 'N/A'}...",
407
+ ]
408
+
409
+ if extra_info:
410
+ for key, value in extra_info.items():
411
+ log_lines.append(f" {key:14}: {value}")
412
+
413
+ log_lines.append("=" * 60)
414
+ self._write_log("INFO", "\n".join(log_lines))
415
+
416
+ def log_message_received(
417
+ self,
418
+ conn_id: int,
419
+ message_type: str,
420
+ message_size: int,
421
+ cmd: str = None,
422
+ extra_info: Optional[Dict] = None
423
+ ):
424
+ """记录收到消息"""
425
+ info_parts = [
426
+ f"conn_id={conn_id}",
427
+ f"type={message_type}",
428
+ f"size={message_size}",
429
+ ]
430
+ if cmd:
431
+ info_parts.append(f"cmd={cmd}")
432
+ if extra_info:
433
+ for key, value in extra_info.items():
434
+ info_parts.append(f"{key}={value}")
435
+
436
+ self._write_log("DEBUG", f"[MSG RECV] {', '.join(info_parts)}")
437
+
438
+ def log_message_loop_exit(
439
+ self,
440
+ conn_id: int,
441
+ reason: str,
442
+ messages_received: int = 0,
443
+ duration: float = 0
444
+ ):
445
+ """记录消息循环退出"""
446
+ log_lines = [
447
+ "~" * 60,
448
+ "MESSAGE LOOP EXITED",
449
+ "~" * 60,
450
+ f" Connection ID : {conn_id}",
451
+ f" Exit Time : {datetime.now().strftime('%Y-%m-%d %H:%M:%S.%f')}",
452
+ f" Reason : {reason}",
453
+ f" Messages Received : {messages_received}",
454
+ f" Loop Duration : {duration:.2f}s",
455
+ "~" * 60
456
+ ]
457
+ self._write_log("WARNING", "\n".join(log_lines))
458
+
459
+ def log_on_open_callback(
460
+ self,
461
+ conn_id: int,
462
+ success: bool,
463
+ error: str = None,
464
+ handler_type: str = None
465
+ ):
466
+ """记录 on_open 回调状态"""
467
+ if success:
468
+ self._write_log(
469
+ "INFO",
470
+ f"[ON_OPEN] conn_id={conn_id}, status=SUCCESS, handler={handler_type or 'unknown'}"
471
+ )
472
+ else:
473
+ self._write_log(
474
+ "ERROR",
475
+ f"[ON_OPEN] conn_id={conn_id}, status=FAILED, handler={handler_type or 'unknown'}, error={error}"
476
+ )
477
+
478
+ def log_health_check(
479
+ self,
480
+ conn_id: int,
481
+ ws_open: bool,
482
+ connection_state: str,
483
+ action: str = None
484
+ ):
485
+ """记录健康检查结果"""
486
+ self._write_log(
487
+ "DEBUG",
488
+ f"[HEALTH CHECK] conn_id={conn_id}, ws_open={ws_open}, state={connection_state}, action={action or 'none'}"
489
+ )
490
+
491
+ def log_system_recovery(
492
+ self,
493
+ conn_id: int,
494
+ recovery_status: Dict[str, Any]
495
+ ):
496
+ """记录系统恢复状态"""
497
+ log_lines = [
498
+ "+" * 60,
499
+ "SYSTEM RECOVERY STATUS",
500
+ "+" * 60,
501
+ f" Connection ID : {conn_id}",
502
+ f" Recovery Time : {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}",
503
+ ]
504
+
505
+ for key, value in recovery_status.items():
506
+ log_lines.append(f" {key:20}: {value}")
507
+
508
+ log_lines.append("+" * 60)
509
+
510
+ self._write_log("INFO", "\n".join(log_lines))
511
+
512
+ def log_message_error(
513
+ self,
514
+ conn_id: int,
515
+ message: Any,
516
+ error: str
517
+ ):
518
+ """记录消息处理错误"""
519
+ self._write_log(
520
+ "ERROR",
521
+ f"[MESSAGE ERROR] conn_id={conn_id}, error={error}, "
522
+ f"message={self._format_data(message, max_length=200)}"
523
+ )
524
+
525
+ def log_connection_superseded(
526
+ self,
527
+ old_conn_id: int,
528
+ new_conn_id: int,
529
+ location: str
530
+ ):
531
+ """记录连接被取代"""
532
+ self._write_log(
533
+ "WARNING",
534
+ f"[CONN SUPERSEDED] old_conn={old_conn_id} superseded by new_conn={new_conn_id}, location={location}"
535
+ )
536
+
537
+ def log_connection_attempt(
538
+ self,
539
+ conn_id: int,
540
+ ws_url: str,
541
+ reason: str
542
+ ):
543
+ """记录连接尝试"""
544
+ self._write_log(
545
+ "INFO",
546
+ f"[CONN ATTEMPT] conn_id={conn_id}, reason={reason}, url={ws_url[:80]}..."
547
+ )
548
+
549
+ def log_state_change(
550
+ self,
551
+ conn_id: int,
552
+ old_state: str,
553
+ new_state: str,
554
+ reason: str = ""
555
+ ):
556
+ """记录连接状态变化"""
557
+ self._write_log(
558
+ "DEBUG",
559
+ f"[STATE CHANGE] conn_id={conn_id}, {old_state} -> {new_state}, reason={reason}"
560
+ )
561
+
562
+ def log_helper_thread(
563
+ self,
564
+ conn_id: int,
565
+ thread_name: str,
566
+ action: str,
567
+ success: bool = True,
568
+ error: str = None
569
+ ):
570
+ """记录辅助线程操作"""
571
+ if success:
572
+ self._write_log(
573
+ "DEBUG",
574
+ f"[THREAD] conn_id={conn_id}, thread={thread_name}, action={action}"
575
+ )
576
+ else:
577
+ self._write_log(
578
+ "ERROR",
579
+ f"[THREAD ERROR] conn_id={conn_id}, thread={thread_name}, action={action}, error={error}"
580
+ )
581
+
582
+ def log_stream_request(
583
+ self,
584
+ conn_id: int,
585
+ request_id: str,
586
+ action: str,
587
+ receiver: str = "",
588
+ extra_info: Optional[Dict] = None
589
+ ):
590
+ """记录流请求操作"""
591
+ info_parts = [
592
+ f"conn_id={conn_id}",
593
+ f"request_id={request_id[:8]}...",
594
+ f"action={action}",
595
+ ]
596
+ if receiver:
597
+ info_parts.append(f"receiver={receiver}")
598
+ if extra_info:
599
+ for key, value in extra_info.items():
600
+ info_parts.append(f"{key}={value}")
601
+
602
+ self._write_log("DEBUG", f"[STREAM REQ] {', '.join(info_parts)}")
603
+
604
+ def log_full_reset_detail(
605
+ self,
606
+ conn_id: int,
607
+ step: str,
608
+ detail: str
609
+ ):
610
+ """记录完全重置的详细步骤"""
611
+ self._write_log(
612
+ "INFO",
613
+ f"[FULL RESET] conn_id={conn_id}, step={step}, detail={detail}"
614
+ )
615
+
616
+ def log_send_message(
617
+ self,
618
+ conn_id: int,
619
+ msg_size: int,
620
+ success: bool,
621
+ error: str = None
622
+ ):
623
+ """记录消息发送"""
624
+ if success:
625
+ self._write_log(
626
+ "DEBUG",
627
+ f"[SEND] conn_id={conn_id}, size={msg_size}, status=OK"
628
+ )
629
+ else:
630
+ self._write_log(
631
+ "WARNING",
632
+ f"[SEND FAILED] conn_id={conn_id}, size={msg_size}, error={error}"
633
+ )
634
+
635
+ def log_queue_operation(
636
+ self,
637
+ conn_id: int,
638
+ operation: str,
639
+ queue_size: int,
640
+ detail: str = ""
641
+ ):
642
+ """记录队列操作"""
643
+ self._write_log(
644
+ "DEBUG",
645
+ f"[QUEUE] conn_id={conn_id}, op={operation}, size={queue_size}, detail={detail}"
646
+ )
647
+
648
+ def get_stats(self) -> Dict[str, Any]:
649
+ """获取统计信息"""
650
+ with self._stats_lock:
651
+ return self._stats.copy()
652
+
653
+ def log_stats(self):
654
+ """记录当前统计信息"""
655
+ stats = self.get_stats()
656
+ log_lines = [
657
+ "#" * 60,
658
+ "WEBSOCKET STATISTICS",
659
+ "#" * 60,
660
+ f" Total Disconnects : {stats['disconnect_count']}",
661
+ f" Total Reconnect Tries : {stats['reconnect_count']}",
662
+ f" Reconnect Successes : {stats['reconnect_success_count']}",
663
+ f" Reconnect Failures : {stats['reconnect_fail_count']}",
664
+ f" Last Disconnect Time : {stats['last_disconnect_time'] or 'N/A'}",
665
+ f" Last Reconnect Time : {stats['last_reconnect_time'] or 'N/A'}",
666
+ f" Last Error : {stats['last_error'] or 'N/A'}",
667
+ "#" * 60
668
+ ]
669
+
670
+ self._write_log("INFO", "\n".join(log_lines))
671
+
672
+
673
+ # 全局单例
674
+ _ws_logger: Optional[WebSocketLogger] = None
675
+ _ws_logger_lock = threading.Lock()
676
+
677
+
678
+ def get_ws_logger() -> WebSocketLogger:
679
+ """获取 WebSocket 日志记录器单例(线程安全)"""
680
+ global _ws_logger
681
+ if _ws_logger is None:
682
+ with _ws_logger_lock:
683
+ if _ws_logger is None:
684
+ _ws_logger = WebSocketLogger()
685
+ return _ws_logger