@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,360 @@
1
+ # -*- coding: utf-8 -*-
2
+ # Copyright 2025 AgentUnion Inc.
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+ import requests
16
+ import datetime
17
+ import socket
18
+ import threading
19
+ import time
20
+ from typing import Optional
21
+ from agentcp.base.log import log_debug, log_error, log_exception, log_info, log_warning
22
+ from agentcp.base.auth_client import AuthClient
23
+
24
+ from agentcp.msg.message_serialize import *
25
+ from ..context import ErrorContext, exceptions
26
+
27
+
28
+ class HeartbeatClient:
29
+ # 重连相关常量
30
+ MAX_SEND_FAILURES = 3 # 发送失败触发重连的阈值
31
+ MAX_RECV_FAILURES = 3 # 接收失败触发重连的阈值
32
+ MAX_MISSED_HEARTBEATS = 3 # 心跳响应超时阈值(错过次数)
33
+ RECONNECT_BACKOFF_MAX = 30 # 重连退避上限(秒)
34
+ SOCKET_TIMEOUT = 1.0 # socket 超时时间(秒)
35
+
36
+ def __init__(self, agent_id: str, server_url: str, aid_path: str, seed_password: str):
37
+ self.agent_id = agent_id
38
+ self.server_url = server_url
39
+ self.seed_password = seed_password
40
+ self.port = 0 # server_port
41
+ self.sign_cookie = 0
42
+ self.udp_socket = None
43
+ self.local_ip = "0.0.0.0"
44
+ self.local_port = 0
45
+ self.server_ip = "127.0.0.1"
46
+ self.heartbeat_interval = 5000
47
+ self.is_running = False
48
+ self.is_sending_heartbeat = False
49
+ self.send_thread: Optional[threading.Thread] = None
50
+ self.receive_thread: Optional[threading.Thread] = None
51
+ self.msg_seq = 0
52
+ self.last_hb = 0
53
+ self.message_listener = None
54
+ self.auth_client = AuthClient(agent_id, server_url, aid_path, seed_password)
55
+ self.on_recv_invite = None
56
+
57
+ # 新增:用于自动恢复的状态
58
+ self._socket_lock = threading.Lock() # 保护 socket 操作
59
+ self._reconnect_lock = threading.Lock() # 防止并发重连
60
+ self._last_reconnect_ts = 0 # 上次重连时间戳
61
+ self._last_hb_recv = 0 # 上次收到心跳响应的时间戳
62
+ self._send_failures = 0 # 连续发送失败次数
63
+ self._recv_failures = 0 # 连续接收失败次数
64
+
65
+ def initialize(self):
66
+ self.sign_in()
67
+
68
+ def sign_in(self) -> bool:
69
+ data = self.auth_client.sign_in()
70
+ if data is None:
71
+ log_error("sign_in failed: data is None")
72
+ return False
73
+ self.server_ip = data.get("server_ip")
74
+ self.port = int(data.get("port", 0))
75
+ self.sign_cookie = data.get("sign_cookie")
76
+ log_info(f'signin {self.server_ip} {self.port} {self.sign_cookie}')
77
+
78
+ return self.server_ip is not None and self.port != 0 and self.sign_cookie is not None
79
+
80
+ def sign_out(self):
81
+ self.auth_client.sign_out()
82
+
83
+ def set_on_recv_invite(self, listener):
84
+ """设置消息监听器"""
85
+ self.on_recv_invite = listener
86
+
87
+ # ========== 新增:Socket 生命周期管理 ==========
88
+
89
+ def _create_socket(self):
90
+ """创建并绑定 UDP socket,设置超时"""
91
+ with self._socket_lock:
92
+ self._close_socket_internal()
93
+ self.udp_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
94
+ self.udp_socket.settimeout(self.SOCKET_TIMEOUT)
95
+ self.udp_socket.bind((self.local_ip, 0)) # 使用新端口
96
+ self.local_ip, self.local_port = self.udp_socket.getsockname()
97
+ log_info(f"UDP socket created and bound to {self.local_ip}:{self.local_port}")
98
+
99
+ def _close_socket_internal(self):
100
+ """内部方法:关闭 socket(不加锁,由调用方保证线程安全)"""
101
+ if self.udp_socket is not None:
102
+ try:
103
+ self.udp_socket.close()
104
+ except Exception as e:
105
+ log_warning(f"Close socket error: {e}")
106
+ self.udp_socket = None
107
+
108
+ def _close_socket(self):
109
+ """安全关闭 socket"""
110
+ with self._socket_lock:
111
+ self._close_socket_internal()
112
+
113
+ def _reconnect(self, reason: str):
114
+ """重连:限流/退避后执行 sign_in() + _create_socket()"""
115
+ if not self.is_running:
116
+ log_debug(f"Reconnect skipped (client offline): {reason}")
117
+ return False
118
+ if not self._reconnect_lock.acquire(blocking=False):
119
+ log_debug(f"Reconnect already in progress, skip: {reason}")
120
+ return False
121
+
122
+ try:
123
+ now = time.time()
124
+ # 限流:距离上次重连至少间隔 5 秒
125
+ elapsed = now - self._last_reconnect_ts
126
+ if elapsed < 5:
127
+ backoff = min(5 - elapsed, self.RECONNECT_BACKOFF_MAX)
128
+ log_info(f"Reconnect backoff: waiting {backoff:.1f}s")
129
+ time.sleep(backoff)
130
+
131
+ log_info(f"Reconnecting due to: {reason}")
132
+ self._last_reconnect_ts = time.time()
133
+
134
+ # 重新登录
135
+ if not self.sign_in():
136
+ log_error("Reconnect failed: sign_in returned False")
137
+ return False
138
+
139
+ # 重建 socket
140
+ self._create_socket()
141
+
142
+ # 重置失败计数
143
+ self._send_failures = 0
144
+ self._recv_failures = 0
145
+ self._last_hb_recv = int(time.time() * 1000)
146
+
147
+ log_info("Reconnect successful")
148
+ return True
149
+ except Exception as e:
150
+ log_error(f"Reconnect exception: {e}")
151
+ return False
152
+ finally:
153
+ self._reconnect_lock.release()
154
+
155
+ # ========== 发送心跳(带异常恢复和超时检测) ==========
156
+
157
+ def __send_heartbeat(self):
158
+ backoff = 1 # 初始退避时间(秒)
159
+
160
+ while self.is_sending_heartbeat and self.is_running:
161
+ try:
162
+ current_time_ms = int(datetime.datetime.now().timestamp() * 1000)
163
+
164
+ # 检查心跳响应超时
165
+ if self._last_hb_recv > 0:
166
+ timeout_threshold = self.MAX_MISSED_HEARTBEATS * self.heartbeat_interval
167
+ if current_time_ms - self._last_hb_recv > timeout_threshold:
168
+ log_warning(f"Heartbeat response timeout: {current_time_ms - self._last_hb_recv}ms > {timeout_threshold}ms")
169
+ self._reconnect("heartbeat_response_timeout")
170
+ backoff = 1
171
+ continue
172
+
173
+ # 发送心跳
174
+ if current_time_ms > (self.last_hb + self.heartbeat_interval):
175
+ log_debug(f'send heartbeat message to {self.server_ip}:{self.port}')
176
+ self.last_hb = current_time_ms
177
+ self.msg_seq = self.msg_seq + 1
178
+ req = HeartbeatMessageReq()
179
+ req.header.MessageMask = 0
180
+ req.header.MessageSeq = self.msg_seq
181
+ req.header.MessageType = 513
182
+ req.header.PayloadSize = 100
183
+ req.AgentId = self.agent_id
184
+ req.SignCookie = self.sign_cookie
185
+ buf = io.BytesIO()
186
+ req.serialize(buf)
187
+ data = buf.getvalue()
188
+
189
+ with self._socket_lock:
190
+ if self.udp_socket is not None:
191
+ self.udp_socket.sendto(data, (self.server_ip, self.port))
192
+ else:
193
+ raise Exception("UDP socket is None")
194
+
195
+ # 发送成功,重置失败计数和退避
196
+ self._send_failures = 0
197
+ backoff = 1
198
+
199
+ time.sleep(1)
200
+
201
+ except Exception as e:
202
+ self._send_failures += 1
203
+ log_error(f"Heartbeat send error (failures={self._send_failures}): {e}")
204
+ ErrorContext.publish(exceptions.SDKError(f"Heartbeat send error: {e}"))
205
+
206
+ # 达到阈值,触发重连
207
+ if self._send_failures >= self.MAX_SEND_FAILURES:
208
+ log_warning(f"Send failures reached threshold ({self.MAX_SEND_FAILURES}), triggering reconnect")
209
+ self._reconnect("send_failures_threshold")
210
+ backoff = 1
211
+ else:
212
+ # 指数退避
213
+ time.sleep(backoff)
214
+ backoff = min(backoff * 2, self.RECONNECT_BACKOFF_MAX)
215
+
216
+ # ========== 接收消息(可中断、可恢复) ==========
217
+
218
+ def _receive_messages(self):
219
+ while self.is_running:
220
+ try:
221
+ # 使用 socket 超时,确保能定期检查 is_running
222
+ with self._socket_lock:
223
+ if self.udp_socket is None:
224
+ time.sleep(0.5)
225
+ continue
226
+ sock = self.udp_socket
227
+
228
+ try:
229
+ data, addr = sock.recvfrom(1536)
230
+ except socket.timeout:
231
+ # 超时是正常的,继续循环检查 is_running
232
+ continue
233
+
234
+ # 接收成功,重置失败计数
235
+ self._recv_failures = 0
236
+
237
+ udp_header, offset = UdpMessageHeader.deserialize(data, 0)
238
+
239
+ if udp_header.MessageType == 258:
240
+ hb_resp, offset = HeartbeatMessageResp.deserialize(data, 0)
241
+ self.heartbeat_interval = hb_resp.NextBeat
242
+
243
+ # 更新最后收到心跳响应的时间
244
+ self._last_hb_recv = int(datetime.datetime.now().timestamp() * 1000)
245
+
246
+ # 服务器端身份验证失败(比如服务器发生了异常重启),需要重新登录
247
+ if hb_resp.NextBeat == 401:
248
+ log_warning(f"Heartbeat failed: {hb_resp.NextBeat}, triggering reconnect")
249
+ ErrorContext.publish(exceptions.SDKError(f"401,心跳", code=0))
250
+ self._reconnect("401_auth_failed")
251
+ continue
252
+
253
+ if self.heartbeat_interval <= 5000:
254
+ self.heartbeat_interval = 5000
255
+
256
+ elif udp_header.MessageType == 259:
257
+ invite_req, offset = InviteMessageReq.deserialize(data, 0)
258
+ if self.on_recv_invite is not None:
259
+ ErrorContext.publish(exceptions.SDKError(f"收到邀请,加入session: {invite_req}", code=0))
260
+ self.on_recv_invite(invite_req)
261
+
262
+ resp = InviteMessageResp()
263
+ self.msg_seq = self.msg_seq + 1
264
+ resp.header.MessageMask = 0
265
+ resp.header.MessageSeq = self.msg_seq
266
+ resp.header.MessageType = 516
267
+ resp.AgentId = self.agent_id
268
+ resp.InviterAgentId = invite_req.InviterAgentId
269
+ resp.SignCookie = self.sign_cookie
270
+ buf = io.BytesIO()
271
+ resp.serialize(buf)
272
+ resp_data = buf.getvalue()
273
+
274
+ with self._socket_lock:
275
+ if self.udp_socket is not None:
276
+ self.udp_socket.sendto(resp_data, (self.server_ip, self.port))
277
+
278
+ except socket.timeout:
279
+ # 超时是正常的,继续循环
280
+ continue
281
+ except Exception as e:
282
+ if not self.is_running:
283
+ break
284
+
285
+ self._recv_failures += 1
286
+ log_error(f"Receive message exception (failures={self._recv_failures}): {e}")
287
+ ErrorContext.publish(exceptions.SDKError(f"Receive message exception: {e}"))
288
+
289
+ # 达到阈值,触发重连
290
+ if self._recv_failures >= self.MAX_RECV_FAILURES:
291
+ log_warning(f"Recv failures reached threshold ({self.MAX_RECV_FAILURES}), triggering reconnect")
292
+ self._reconnect("recv_failures_threshold")
293
+ else:
294
+ time.sleep(1.5)
295
+
296
+ def online(self):
297
+ """开始心跳"""
298
+ if self.is_running:
299
+ return
300
+
301
+ # 使用统一的 socket 创建方法
302
+ self._create_socket()
303
+
304
+ # 初始化心跳响应时间
305
+ self._last_hb_recv = int(time.time() * 1000)
306
+
307
+ self.is_running = True
308
+ self.is_sending_heartbeat = True
309
+
310
+ self.send_thread = threading.Thread(target=self.__send_heartbeat, daemon=True)
311
+ self.receive_thread = threading.Thread(target=self._receive_messages, daemon=True)
312
+
313
+ self.send_thread.start()
314
+ self.receive_thread.start()
315
+ log_info("Successfully went online")
316
+
317
+ def offline(self):
318
+ """停止心跳"""
319
+ log_info("Going offline...")
320
+
321
+ # 1. 先设置标志位,通知线程退出
322
+ self.is_running = False
323
+ self.is_sending_heartbeat = False
324
+
325
+ # 2. 关闭 socket(会使阻塞的 recvfrom 抛出异常)
326
+ self._close_socket()
327
+
328
+ # 3. 等待线程退出
329
+ if self.send_thread is not None and self.send_thread.is_alive():
330
+ self.send_thread.join(timeout=3)
331
+ if self.send_thread.is_alive():
332
+ log_warning("Send thread did not exit in time")
333
+
334
+ if self.receive_thread is not None and self.receive_thread.is_alive():
335
+ self.receive_thread.join(timeout=3)
336
+ if self.receive_thread.is_alive():
337
+ log_warning("Receive thread did not exit in time")
338
+
339
+ self.send_thread = None
340
+ self.receive_thread = None
341
+ log_info("Successfully went offline")
342
+
343
+ def get_online_status(self, aids):
344
+ try:
345
+ ep_url = self.server_url + "/query_online_state"
346
+ data = {
347
+ "agent_id": f"{self.agent_id}",
348
+ "signature": self.auth_client.signature,
349
+ "agents": aids
350
+ }
351
+ response = requests.post(ep_url, json=data, verify=False, proxies={}, timeout=(3, 10))
352
+ if response.status_code == 200:
353
+ log_info(f"get_online_status ok:{response.json()}")
354
+ return response.json()["data"]
355
+ else:
356
+ log_error(f"get_online_status failed:{response.json()}")
357
+ return []
358
+ except Exception as e:
359
+ log_exception(f"get_online_status exception: {e}")
360
+ return []