@agentunion/kite 1.3.2 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (293) hide show
  1. package/CHANGELOG.md +302 -0
  2. package/cli.js +119 -4
  3. package/core/dependency_checker.py +250 -0
  4. package/core/env_checker.py +490 -0
  5. package/dependencies_lock.json +128 -0
  6. package/extensions/agents/assistant/entry.py +111 -1
  7. package/extensions/agents/assistant/server.py +279 -215
  8. package/extensions/channels/acp_channel/entry.py +111 -1
  9. package/extensions/channels/acp_channel/module.md +23 -22
  10. package/extensions/channels/acp_channel/server.py +279 -215
  11. package/extensions/event_hub_bench/entry.py +107 -1
  12. package/extensions/services/backup/entry.py +306 -21
  13. package/extensions/services/backup/module.md +24 -22
  14. package/extensions/services/evol/auth_manager.py +443 -0
  15. package/extensions/services/evol/config.yaml +149 -0
  16. package/extensions/services/evol/config_loader.py +117 -0
  17. package/extensions/services/evol/entry.py +406 -0
  18. package/extensions/services/evol/evol_api.py +173 -0
  19. package/extensions/services/evol/evol_config.json5 +29 -0
  20. package/extensions/services/evol/migrate_tokens.py +122 -0
  21. package/extensions/services/evol/module.md +32 -0
  22. package/extensions/services/evol/pairing.py +250 -0
  23. package/extensions/services/evol/pairing_codes.jsonl +1 -0
  24. package/extensions/services/evol/relay.py +682 -0
  25. package/extensions/services/evol/relay_config.json5 +67 -0
  26. package/extensions/services/evol/routes/__init__.py +1 -0
  27. package/extensions/services/evol/routes/routes_management_ws.py +127 -0
  28. package/extensions/services/evol/routes/routes_rpc.py +89 -0
  29. package/extensions/services/evol/routes/routes_test.py +61 -0
  30. package/extensions/services/evol/server.py +875 -0
  31. package/extensions/services/evol/static/css/style.css +1200 -0
  32. package/extensions/services/evol/static/index.html +781 -0
  33. package/extensions/services/evol/static/index_evol.html +14 -0
  34. package/extensions/services/evol/static/js/app.js +6304 -0
  35. package/extensions/services/evol/static/js/auth.js +326 -0
  36. package/extensions/services/evol/static/js/dialog.js +285 -0
  37. package/extensions/services/evol/static/js/evol-app-fixed.js +50 -0
  38. package/extensions/services/evol/static/js/evol-app.js +1949 -0
  39. package/extensions/services/evol/static/js/evol-app.js.bak +1800 -0
  40. package/extensions/services/evol/static/js/kernel-client-example.js +228 -0
  41. package/extensions/services/evol/static/js/kernel-client.js +396 -0
  42. package/extensions/services/evol/static/js/main.js +141 -0
  43. package/extensions/services/evol/static/js/registry-tests.js +585 -0
  44. package/extensions/services/evol/static/js/stats.js +217 -0
  45. package/extensions/services/evol/static/js/token-manager.js +175 -0
  46. package/extensions/services/evol/static/pairing.html +248 -0
  47. package/extensions/services/evol/static/test_registry.html +262 -0
  48. package/extensions/services/evol/static/test_relay.html +462 -0
  49. package/extensions/services/evol/stats_manager.py +240 -0
  50. package/extensions/services/model_service/entry.py +167 -19
  51. package/extensions/services/model_service/module.md +21 -22
  52. package/extensions/services/proxy/.claude/settings.local.json +13 -0
  53. package/extensions/services/proxy/CHANGELOG_20260308.md +258 -0
  54. package/extensions/services/proxy/_fix_prints.py +133 -0
  55. package/extensions/services/proxy/_fix_prints2.py +87 -0
  56. package/extensions/services/proxy/agentcp/LICENCE +178 -0
  57. package/extensions/services/proxy/agentcp/README copy.md +85 -0
  58. package/extensions/services/proxy/agentcp/README.md +260 -0
  59. package/extensions/services/proxy/agentcp/__init__.py +16 -0
  60. package/extensions/services/proxy/agentcp/agent.py +4 -0
  61. package/extensions/services/proxy/agentcp/agentcp.py +2494 -0
  62. package/extensions/services/proxy/agentcp/agentprofile.json +89 -0
  63. package/extensions/services/proxy/agentcp/ap/__init__.py +16 -0
  64. package/extensions/services/proxy/agentcp/ap/ap_client.py +316 -0
  65. package/extensions/services/proxy/agentcp/assets/images/wechat_qr.png +0 -0
  66. package/extensions/services/proxy/agentcp/backup/metrics.json +31 -0
  67. package/extensions/services/proxy/agentcp/base/__init__.py +20 -0
  68. package/extensions/services/proxy/agentcp/base/auth_client.py +257 -0
  69. package/extensions/services/proxy/agentcp/base/client.py +112 -0
  70. package/extensions/services/proxy/agentcp/base/env.py +34 -0
  71. package/extensions/services/proxy/agentcp/base/html_util.py +336 -0
  72. package/extensions/services/proxy/agentcp/base/log.py +98 -0
  73. package/extensions/services/proxy/agentcp/ca/__init__.py +17 -0
  74. package/extensions/services/proxy/agentcp/ca/ca_client.py +414 -0
  75. package/extensions/services/proxy/agentcp/ca/ca_root.py +74 -0
  76. package/extensions/services/proxy/agentcp/context/__init__.py +20 -0
  77. package/extensions/services/proxy/agentcp/context/context.py +73 -0
  78. package/extensions/services/proxy/agentcp/context/exceptions.py +114 -0
  79. package/extensions/services/proxy/agentcp/create_profile.py +125 -0
  80. package/extensions/services/proxy/agentcp/create_profile_weather.py +125 -0
  81. package/extensions/services/proxy/agentcp/db/__init__.py +15 -0
  82. package/extensions/services/proxy/agentcp/db/db_mananger.py +550 -0
  83. package/extensions/services/proxy/agentcp/docs/UDP_HEARTBEAT_FIX_REPORT.md +265 -0
  84. package/extensions/services/proxy/agentcp/docs/heartbeat_issue_analysis.md +291 -0
  85. package/extensions/services/proxy/agentcp/file/__init__.py +16 -0
  86. package/extensions/services/proxy/agentcp/file/file_client.py +141 -0
  87. package/extensions/services/proxy/agentcp/file/wss_binary_message.py +137 -0
  88. package/extensions/services/proxy/agentcp/hcp.py +299 -0
  89. package/extensions/services/proxy/agentcp/heartbeat/__init__.py +16 -0
  90. package/extensions/services/proxy/agentcp/heartbeat/heartbeat_client.py +360 -0
  91. package/extensions/services/proxy/agentcp/improved_scheduler.py +498 -0
  92. package/extensions/services/proxy/agentcp/llm_agent_utils.py +249 -0
  93. package/extensions/services/proxy/agentcp/llm_server.py +172 -0
  94. package/extensions/services/proxy/agentcp/mermaid.py +210 -0
  95. package/extensions/services/proxy/agentcp/message.py +149 -0
  96. package/extensions/services/proxy/agentcp/metrics.py +256 -0
  97. package/extensions/services/proxy/agentcp/monitoring/__init__.py +20 -0
  98. package/extensions/services/proxy/agentcp/monitoring/global_monitor.py +27 -0
  99. package/extensions/services/proxy/agentcp/monitoring/metrics_store.py +325 -0
  100. package/extensions/services/proxy/agentcp/monitoring/monitoring_service.py +269 -0
  101. package/extensions/services/proxy/agentcp/monitoring/sliding_window.py +222 -0
  102. package/extensions/services/proxy/agentcp/monitoring/standalone_reader.py +224 -0
  103. package/extensions/services/proxy/agentcp/msg/__init__.py +21 -0
  104. package/extensions/services/proxy/agentcp/msg/connection_manager.py +456 -0
  105. package/extensions/services/proxy/agentcp/msg/message_client.py +2058 -0
  106. package/extensions/services/proxy/agentcp/msg/message_serialize.py +263 -0
  107. package/extensions/services/proxy/agentcp/msg/open_ai_message.py +88 -0
  108. package/extensions/services/proxy/agentcp/msg/session_manager.py +1062 -0
  109. package/extensions/services/proxy/agentcp/msg/stream_client.py +267 -0
  110. package/extensions/services/proxy/agentcp/msg/websocket_file_receiver.py +89 -0
  111. package/extensions/services/proxy/agentcp/msg/ws_logger.py +685 -0
  112. package/extensions/services/proxy/agentcp/msg/wss_binary_message.py +137 -0
  113. package/extensions/services/proxy/agentcp/requirements.txt +7 -0
  114. package/extensions/services/proxy/agentcp/samples/agent_graph/README.md +37 -0
  115. package/extensions/services/proxy/agentcp/samples/agent_graph/agentprofile.json +89 -0
  116. package/extensions/services/proxy/agentcp/samples/agent_graph/create_profile.py +138 -0
  117. package/extensions/services/proxy/agentcp/samples/agent_graph/main.py +164 -0
  118. package/extensions/services/proxy/agentcp/samples/agent_use/create_profile.py +123 -0
  119. package/extensions/services/proxy/agentcp/samples/agent_use/llm/create_profile.py +129 -0
  120. package/extensions/services/proxy/agentcp/samples/agent_use/llm/env.json +5 -0
  121. package/extensions/services/proxy/agentcp/samples/agent_use/llm/main.py +146 -0
  122. package/extensions/services/proxy/agentcp/samples/agent_use/main.py +123 -0
  123. package/extensions/services/proxy/agentcp/samples/agent_use/readme.md +379 -0
  124. package/extensions/services/proxy/agentcp/samples/agent_use/search/create_profile.py +129 -0
  125. package/extensions/services/proxy/agentcp/samples/agent_use/search/main.py +28 -0
  126. package/extensions/services/proxy/agentcp/samples/agent_use/tool/create_profile.py +129 -0
  127. package/extensions/services/proxy/agentcp/samples/agent_use/tool/main.py +20 -0
  128. package/extensions/services/proxy/agentcp/samples/ali_amap/README.md +97 -0
  129. package/extensions/services/proxy/agentcp/samples/ali_amap/amap_agent.py +88 -0
  130. package/extensions/services/proxy/agentcp/samples/ali_amap/create_profile.py +125 -0
  131. package/extensions/services/proxy/agentcp/samples/compute_agent/agent/powershell.py +228 -0
  132. package/extensions/services/proxy/agentcp/samples/compute_agent/agent/software.py +63 -0
  133. package/extensions/services/proxy/agentcp/samples/compute_agent/agent/tools.py +36 -0
  134. package/extensions/services/proxy/agentcp/samples/compute_agent/browser_user.py +41 -0
  135. package/extensions/services/proxy/agentcp/samples/deepseek/README.md +79 -0
  136. package/extensions/services/proxy/agentcp/samples/deepseek/create_profile.py +126 -0
  137. package/extensions/services/proxy/agentcp/samples/deepseek/deepseek.py +42 -0
  138. package/extensions/services/proxy/agentcp/samples/dify_chat/README.md +78 -0
  139. package/extensions/services/proxy/agentcp/samples/dify_chat/create_profile.py +126 -0
  140. package/extensions/services/proxy/agentcp/samples/dify_chat/dify_chat.py +47 -0
  141. package/extensions/services/proxy/agentcp/samples/dify_workflow/README.md +78 -0
  142. package/extensions/services/proxy/agentcp/samples/dify_workflow/create_profile.py +126 -0
  143. package/extensions/services/proxy/agentcp/samples/dify_workflow/dify_workflow.py +46 -0
  144. package/extensions/services/proxy/agentcp/samples/executor/README.md +44 -0
  145. package/extensions/services/proxy/agentcp/samples/executor/agentprofile.json +89 -0
  146. package/extensions/services/proxy/agentcp/samples/executor/create_profile.py +139 -0
  147. package/extensions/services/proxy/agentcp/samples/executor/main.py +160 -0
  148. package/extensions/services/proxy/agentcp/samples/filereader/README.md +45 -0
  149. package/extensions/services/proxy/agentcp/samples/filereader/agentprofile.json +90 -0
  150. package/extensions/services/proxy/agentcp/samples/filereader/create_profile.py +137 -0
  151. package/extensions/services/proxy/agentcp/samples/filereader/main.py +253 -0
  152. package/extensions/services/proxy/agentcp/samples/filewriter/README.md +38 -0
  153. package/extensions/services/proxy/agentcp/samples/filewriter/agentprofile.json +91 -0
  154. package/extensions/services/proxy/agentcp/samples/filewriter/create_profile.py +138 -0
  155. package/extensions/services/proxy/agentcp/samples/filewriter/main.py +289 -0
  156. package/extensions/services/proxy/agentcp/samples/hcp/README.md +85 -0
  157. package/extensions/services/proxy/agentcp/samples/hcp/acp_weather_agent.zip +0 -0
  158. package/extensions/services/proxy/agentcp/samples/hcp/create_profile.py +125 -0
  159. package/extensions/services/proxy/agentcp/samples/hcp/hcp.py +237 -0
  160. package/extensions/services/proxy/agentcp/samples/helloworld/README.md +68 -0
  161. package/extensions/services/proxy/agentcp/samples/helloworld/hello_world.py +40 -0
  162. package/extensions/services/proxy/agentcp/samples/llm_agent/MEADME.md +117 -0
  163. package/extensions/services/proxy/agentcp/samples/llm_agent/create_profile.py +125 -0
  164. package/extensions/services/proxy/agentcp/samples/llm_agent/qwen_agent.py +136 -0
  165. package/extensions/services/proxy/agentcp/samples/local_llm_agent/README.md +90 -0
  166. package/extensions/services/proxy/agentcp/samples/local_llm_agent/create_profile.py +125 -0
  167. package/extensions/services/proxy/agentcp/samples/local_llm_agent/main.py +49 -0
  168. package/extensions/services/proxy/agentcp/samples/query_llm_from_agent/README.md +55 -0
  169. package/extensions/services/proxy/agentcp/samples/query_llm_from_agent/create_profile.py +125 -0
  170. package/extensions/services/proxy/agentcp/samples/query_llm_from_agent/main.py +23 -0
  171. package/extensions/services/proxy/agentcp/samples/query_weather_api_agent/README.md +103 -0
  172. package/extensions/services/proxy/agentcp/samples/query_weather_api_agent/create_profile.py +125 -0
  173. package/extensions/services/proxy/agentcp/samples/query_weather_api_agent/main.py +69 -0
  174. package/extensions/services/proxy/agentcp/samples/query_weather_from_agent/README.md +58 -0
  175. package/extensions/services/proxy/agentcp/samples/query_weather_from_agent/create_profile.py +125 -0
  176. package/extensions/services/proxy/agentcp/samples/query_weather_from_agent/main.py +25 -0
  177. package/extensions/services/proxy/agentcp/samples/qwen3/README.md +71 -0
  178. package/extensions/services/proxy/agentcp/samples/qwen3/create_profile.py +126 -0
  179. package/extensions/services/proxy/agentcp/samples/qwen3/qwen3.py +37 -0
  180. package/extensions/services/proxy/agentcp/samples/qwen3_tools/README.md +133 -0
  181. package/extensions/services/proxy/agentcp/samples/qwen3_tools/create_profile.py +126 -0
  182. package/extensions/services/proxy/agentcp/samples/qwen3_tools/qwen3_tools.py +98 -0
  183. package/extensions/services/proxy/agentcp/samples/search/create_profile_qwen.py +125 -0
  184. package/extensions/services/proxy/agentcp/samples/search/create_profile_search.py +125 -0
  185. package/extensions/services/proxy/agentcp/samples/search/qwen_agent.py +136 -0
  186. package/extensions/services/proxy/agentcp/samples/search/search_agent.py +170 -0
  187. package/extensions/services/proxy/agentcp/samples/wrapper_agently_to_agent/README.md +89 -0
  188. package/extensions/services/proxy/agentcp/samples/wrapper_agently_to_agent/create_profile.py +125 -0
  189. package/extensions/services/proxy/agentcp/samples/wrapper_agently_to_agent/main.py +44 -0
  190. package/extensions/services/proxy/agentcp/utils/__init__.py +15 -0
  191. package/extensions/services/proxy/agentcp/utils/file_util.py +117 -0
  192. package/extensions/services/proxy/agentcp/utils/proxy_bypass.py +99 -0
  193. package/extensions/services/proxy/agentcp/workflow.py +203 -0
  194. package/extensions/services/proxy/console_auth.py +109 -0
  195. package/extensions/services/proxy/evol/__init__.py +1 -0
  196. package/extensions/services/proxy/evol/config.py +37 -0
  197. package/extensions/services/proxy/evol/http/__init__.py +1 -0
  198. package/extensions/services/proxy/evol/http/async_http.py +551 -0
  199. package/extensions/services/proxy/evol/log.py +28 -0
  200. package/extensions/services/proxy/evol/presenter/__init__.py +2 -0
  201. package/extensions/services/proxy/evol/presenter/agentIdPresenter.py +1031 -0
  202. package/extensions/services/proxy/evol/presenter/apikeyPresenter.py +106 -0
  203. package/extensions/services/proxy/evol/presenter/configPresenter.py +1281 -0
  204. package/extensions/services/proxy/evol/presenter/userPresenter.py +477 -0
  205. package/extensions/services/proxy/evol/server/__init__.py +1 -0
  206. package/extensions/services/proxy/evol/server/claude_proxy_async.py +3430 -0
  207. package/extensions/services/proxy/evol/server/openclaw_proxy.py +1861 -0
  208. package/extensions/services/proxy/evol/server/proxy_config.py +15 -0
  209. package/extensions/services/proxy/evol/server/proxy_engine.py +501 -0
  210. package/extensions/services/proxy/evol/version.py +24 -0
  211. package/extensions/services/proxy/logs/websocket.log +260 -0
  212. package/extensions/services/proxy/main.py +240 -0
  213. package/extensions/services/proxy/requirements.txt +13 -0
  214. package/extensions/services/proxy/server.py +271 -0
  215. package/extensions/services/watchdog/entry.py +215 -26
  216. package/extensions/services/watchdog/module.md +1 -0
  217. package/extensions/services/watchdog/monitor.py +178 -38
  218. package/extensions/services/web/WEBSOCKET_STATUS.md +143 -0
  219. package/extensions/services/web/config_example.py +35 -0
  220. package/extensions/services/web/config_loader.py +110 -0
  221. package/extensions/services/web/entry.py +114 -26
  222. package/extensions/services/web/module.md +35 -24
  223. package/extensions/services/web/pairing.py +250 -0
  224. package/extensions/services/web/pairing_codes.jsonl +16 -0
  225. package/extensions/services/web/relay.py +643 -0
  226. package/extensions/services/web/relay_config.json5 +67 -0
  227. package/extensions/services/web/routes/routes_management_ws.py +127 -0
  228. package/extensions/services/web/routes/routes_rpc.py +89 -0
  229. package/extensions/services/web/routes/routes_test.py +61 -0
  230. package/extensions/services/web/routes/schemas.py +0 -22
  231. package/extensions/services/web/server.py +434 -99
  232. package/extensions/services/web/static/css/style.css +67 -28
  233. package/extensions/services/web/static/index.html +234 -44
  234. package/extensions/services/web/static/js/app.js +1335 -48
  235. package/extensions/services/web/static/js/kernel-client-example.js +161 -0
  236. package/extensions/services/web/static/js/kernel-client.js +383 -0
  237. package/extensions/services/web/static/js/registry-tests.js +558 -0
  238. package/extensions/services/web/static/js/token-manager.js +175 -0
  239. package/extensions/services/web/static/pairing.html +248 -0
  240. package/extensions/services/web/static/test_registry.html +262 -0
  241. package/extensions/services/web/web_config.json5 +29 -0
  242. package/kernel/entry.py +120 -32
  243. package/kernel/event_hub.py +141 -16
  244. package/kernel/module.md +60 -33
  245. package/kernel/registry_store.py +45 -36
  246. package/kernel/rpc_router.py +152 -59
  247. package/kernel/server.py +322 -26
  248. package/kite_cli/__init__.py +3 -0
  249. package/kite_cli/__main__.py +5 -0
  250. package/kite_cli/commands/__init__.py +1 -0
  251. package/kite_cli/commands/clean.py +101 -0
  252. package/kite_cli/commands/deps_install.py +67 -0
  253. package/kite_cli/commands/doctor.py +35 -0
  254. package/kite_cli/commands/env_check.py +45 -0
  255. package/kite_cli/commands/history.py +111 -0
  256. package/kite_cli/commands/info.py +96 -0
  257. package/kite_cli/commands/install.py +313 -0
  258. package/kite_cli/commands/list.py +143 -0
  259. package/kite_cli/commands/log.py +81 -0
  260. package/kite_cli/commands/prepare.py +49 -0
  261. package/kite_cli/commands/rollback.py +88 -0
  262. package/kite_cli/commands/search.py +73 -0
  263. package/kite_cli/commands/uninstall.py +85 -0
  264. package/kite_cli/commands/update.py +118 -0
  265. package/kite_cli/commands/venv_setup.py +56 -0
  266. package/kite_cli/core/__init__.py +1 -0
  267. package/kite_cli/core/checker.py +142 -0
  268. package/kite_cli/core/dependency.py +229 -0
  269. package/kite_cli/core/downloader.py +209 -0
  270. package/kite_cli/core/install_info.py +40 -0
  271. package/kite_cli/core/tool_installer.py +397 -0
  272. package/kite_cli/core/validator.py +78 -0
  273. package/kite_cli/main.py +317 -0
  274. package/kite_cli/utils/__init__.py +1 -0
  275. package/kite_cli/utils/i18n.py +252 -0
  276. package/kite_cli/utils/interactive.py +63 -0
  277. package/kite_cli/utils/operation_log.py +77 -0
  278. package/kite_cli/utils/paths.py +34 -0
  279. package/kite_cli/utils/version.py +308 -0
  280. package/launcher/entry.py +1124 -178
  281. package/launcher/logging_setup.py +104 -0
  282. package/launcher/module.md +46 -37
  283. package/launcher/module_scanner.py +11 -1
  284. package/main.py +4 -1
  285. package/package.json +9 -1
  286. package/python_version.json +4 -0
  287. package/requirements.txt +38 -0
  288. package/scripts/env-manager.js +328 -0
  289. package/scripts/plan_manager.py +315 -0
  290. package/scripts/python-env.js +79 -0
  291. package/scripts/scan_dependencies.py +461 -0
  292. package/scripts/setup-python-env.js +191 -0
  293. package/extensions/services/web/routes/routes_modules.py +0 -249
@@ -0,0 +1,271 @@
1
+ """
2
+ server.py - 精简版 FastAPI 服务器
3
+
4
+ 仅提供4个代理路由 + 健康检查:
5
+ - /claude-proxy/{path} - Claude代理(仅 v1/messages 路径且 model 包含 "claude")
6
+ - /codex-proxy/{path} - Codex代理(不做 model 过滤)
7
+ - /gemini-proxy/{path} - Gemini代理(不做 model 过滤)
8
+ - /openclaw-proxy/{path} - OpenClaw代理(OpenAI兼容接口)
9
+ - /health - 健康检查
10
+ """
11
+
12
+ import asyncio
13
+ import json
14
+ import logging
15
+ import threading
16
+ import time
17
+ import uuid
18
+
19
+ import uvicorn
20
+ from fastapi import FastAPI, Request, HTTPException
21
+ from fastapi.middleware.cors import CORSMiddleware
22
+ from fastapi.responses import JSONResponse, Response
23
+
24
+
25
+ # ==================== 并发管理器 ====================
26
+
27
+ class ConcurrencyLimitManager:
28
+ """管理代理接口的总并发数,带自动清理超时请求"""
29
+
30
+ _instance = None
31
+ _lock = threading.Lock()
32
+
33
+ def __new__(cls):
34
+ if cls._instance is None:
35
+ with cls._lock:
36
+ if cls._instance is None:
37
+ cls._instance = super().__new__(cls)
38
+ cls._instance._initialized = False
39
+ return cls._instance
40
+
41
+ def __init__(self):
42
+ if self._initialized:
43
+ return
44
+ self._initialized = True
45
+ self._active_requests = {}
46
+ self._count_lock = threading.Lock()
47
+ self._timeout_seconds = 300
48
+ self._limit = 5
49
+ print("[ConcurrencyLimitManager] initialized")
50
+
51
+ def acquire(self) -> tuple:
52
+ with self._count_lock:
53
+ self._cleanup_timeout_requests()
54
+ request_id = f"req_{int(time.time() * 1000)}_{id(threading.current_thread())}"
55
+ self._active_requests[request_id] = time.time()
56
+ return True, "", request_id
57
+
58
+ def release(self, request_id: str = None):
59
+ with self._count_lock:
60
+ if request_id and request_id in self._active_requests:
61
+ del self._active_requests[request_id]
62
+ elif not request_id and self._active_requests:
63
+ oldest_id = min(self._active_requests, key=self._active_requests.get)
64
+ del self._active_requests[oldest_id]
65
+
66
+ def _cleanup_timeout_requests(self):
67
+ current_time = time.time()
68
+ timeout_ids = [
69
+ rid for rid, t in self._active_requests.items()
70
+ if current_time - t > self._timeout_seconds
71
+ ]
72
+ for rid in timeout_ids:
73
+ del self._active_requests[rid]
74
+
75
+ def get_current_count(self) -> int:
76
+ with self._count_lock:
77
+ return len(self._active_requests)
78
+
79
+
80
+ concurrency_manager = ConcurrencyLimitManager()
81
+
82
+
83
+ # ==================== FastAPI 应用 ====================
84
+
85
+ app = FastAPI(title="Evol Sample Backend", version="1.0.0")
86
+
87
+ app.add_middleware(
88
+ CORSMiddleware,
89
+ allow_origins=["*"],
90
+ allow_credentials=True,
91
+ allow_methods=["*"],
92
+ allow_headers=["*"],
93
+ )
94
+
95
+
96
+ # ==================== 健康检查 ====================
97
+
98
+ @app.get("/health")
99
+ async def health_check():
100
+ from evol.server.claude_proxy_async import get_current_agent_id
101
+ agent_id = get_current_agent_id()
102
+ return {
103
+ "status": "ok",
104
+ "agent_id": agent_id.id if agent_id else None,
105
+ "is_online": agent_id.is_online_success if agent_id else False,
106
+ "active_requests": concurrency_manager.get_current_count()
107
+ }
108
+
109
+
110
+ # ==================== Claude Proxy 路由 ====================
111
+
112
+ @app.api_route("/claude-proxy/{full_path:path}",
113
+ methods=["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"])
114
+ async def claude_proxy_route(request: Request, full_path: str):
115
+ """Claude代理路由 - 仅放行 v1/messages 路径且 model 包含 'claude'"""
116
+ acquired, error_msg, request_id = concurrency_manager.acquire()
117
+ if not acquired:
118
+ return Response(content=error_msg.encode("utf-8"), status_code=429)
119
+
120
+ if "v1/messages" not in full_path:
121
+ concurrency_manager.release(request_id)
122
+ raise HTTPException(status_code=200, detail="OK")
123
+
124
+ try:
125
+ request_body = await request.body()
126
+ bodyjson = await asyncio.to_thread(json.loads, request_body.decode('utf-8'))
127
+ model = bodyjson.get('model', '')
128
+ if "claude" not in model:
129
+ raise HTTPException(status_code=400, detail="Unsupported model")
130
+
131
+ from evol.server.claude_proxy_async import proxy_claude_request
132
+ response = await proxy_claude_request(request)
133
+ return response
134
+
135
+ except asyncio.CancelledError:
136
+ logging.getLogger("evol_server").warning("claude-proxy request cancelled (client disconnected?)")
137
+ return Response(content=b"Client Closed Request", status_code=499)
138
+ except Exception as e:
139
+ if isinstance(e, HTTPException):
140
+ return Response(content=str(e.detail).encode("utf-8"), status_code=e.status_code)
141
+ return Response(content=str(e).encode("utf-8"), status_code=503)
142
+ finally:
143
+ concurrency_manager.release(request_id)
144
+
145
+
146
+ # ==================== Codex Proxy 路由 ====================
147
+
148
+ @app.api_route("/codex-proxy/{full_path:path}",
149
+ methods=["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"])
150
+ async def codex_proxy_route(request: Request, full_path: str):
151
+ """Codex代理路由 - 不做 model 过滤"""
152
+ acquired, error_msg, request_id = concurrency_manager.acquire()
153
+ if not acquired:
154
+ return Response(content=error_msg.encode("utf-8"), status_code=429)
155
+
156
+ try:
157
+ from evol.server.claude_proxy_async import proxy_claude_request
158
+ response = await proxy_claude_request(request)
159
+ return response
160
+
161
+ except asyncio.CancelledError:
162
+ logging.getLogger("evol_server").warning("codex-proxy request cancelled (client disconnected?)")
163
+ return Response(content=b"Client Closed Request", status_code=499)
164
+ except Exception as e:
165
+ if isinstance(e, HTTPException):
166
+ return Response(content=str(e.detail).encode("utf-8"), status_code=e.status_code)
167
+ return Response(content=str(e).encode("utf-8"), status_code=503)
168
+ finally:
169
+ concurrency_manager.release(request_id)
170
+
171
+
172
+ # ==================== Gemini Proxy 路由 ====================
173
+
174
+ @app.api_route("/gemini-proxy/{full_path:path}",
175
+ methods=["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"])
176
+ async def gemini_proxy_route(request: Request, full_path: str):
177
+ """Gemini代理路由 - 不做 model 过滤"""
178
+ acquired, error_msg, request_id = concurrency_manager.acquire()
179
+ if not acquired:
180
+ return Response(content=error_msg.encode("utf-8"), status_code=429)
181
+
182
+ try:
183
+ from evol.server.claude_proxy_async import proxy_claude_request
184
+ response = await proxy_claude_request(request)
185
+ return response
186
+
187
+ except asyncio.CancelledError:
188
+ logging.getLogger("evol_server").warning("gemini-proxy request cancelled (client disconnected?)")
189
+ return Response(content=b"Client Closed Request", status_code=499)
190
+ except Exception as e:
191
+ if isinstance(e, HTTPException):
192
+ return Response(content=str(e.detail).encode("utf-8"), status_code=e.status_code)
193
+ return Response(content=str(e).encode("utf-8"), status_code=503)
194
+ finally:
195
+ concurrency_manager.release(request_id)
196
+
197
+
198
+ # ==================== OpenClaw Proxy 路由 ====================
199
+
200
+ @app.api_route("/openclaw-proxy/{full_path:path}",
201
+ methods=["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"])
202
+ async def openclaw_proxy_route(request: Request, full_path: str):
203
+ """OpenClaw代理路由 - OpenAI兼容接口"""
204
+ acquired, error_msg, request_id = concurrency_manager.acquire()
205
+ if not acquired:
206
+ from evol.server.openclaw_proxy import openai_error_response
207
+ return openai_error_response(error_msg, error_type="rate_limit_exceeded", status_code=429)
208
+
209
+ try:
210
+ if full_path == "v1/models":
211
+ from evol.server.openclaw_proxy import get_models_list
212
+ return JSONResponse(get_models_list())
213
+ elif full_path == "v1/chat/completions":
214
+ from evol.server.openclaw_proxy import proxy_openclaw_request
215
+ response = await proxy_openclaw_request(request)
216
+ return response
217
+ else:
218
+ from evol.server.openclaw_proxy import openai_error_response
219
+ return openai_error_response(
220
+ f"Path not found: /{full_path}. Supported: /v1/models, /v1/chat/completions.",
221
+ error_type="invalid_request_error",
222
+ status_code=404
223
+ )
224
+
225
+ except asyncio.CancelledError:
226
+ logging.getLogger("evol_server").warning("openclaw-proxy request cancelled (client disconnected?)")
227
+ return Response(content=b"Client Closed Request", status_code=499)
228
+ except Exception as e:
229
+ from evol.server.openclaw_proxy import openai_error_response
230
+ return openai_error_response(f"Internal server error: {str(e)}", error_type="api_error", status_code=500)
231
+ finally:
232
+ concurrency_manager.release(request_id)
233
+
234
+
235
+ # ==================== 启动服务器 ====================
236
+
237
+ # 全局变量存储实际端口
238
+ _actual_port = None
239
+
240
+ def get_actual_port():
241
+ """获取服务器实际使用的端口"""
242
+ return _actual_port
243
+
244
+ async def run_server(port: int = 0):
245
+ """启动 FastAPI 服务器
246
+
247
+ Args:
248
+ port: 端口号,0 表示系统自动分配
249
+ """
250
+ global _actual_port
251
+
252
+ # 如果端口为 0,先获取一个可用端口
253
+ if port == 0:
254
+ import socket
255
+ sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
256
+ sock.bind(('', 0))
257
+ port = sock.getsockname()[1]
258
+ sock.close()
259
+
260
+ _actual_port = port
261
+
262
+ config = uvicorn.Config(
263
+ app,
264
+ host="0.0.0.0",
265
+ port=port,
266
+ log_level="info",
267
+ access_log=True,
268
+ )
269
+ server = uvicorn.Server(config)
270
+ server.install_signal_handlers = False # 禁止 uvicorn 安装信号处理器,防止外部信号导致服务器关闭
271
+ await server.serve()
@@ -20,7 +20,113 @@ import websockets
20
20
 
21
21
 
22
22
  # ── Module configuration ──
23
- MODULE_NAME = "watchdog"
23
+
24
+ def _load_module_config() -> dict:
25
+ """Load module configuration from module.md frontmatter.
26
+
27
+ Returns:
28
+ Dict with keys: name, preferred_port, advertise_ip
29
+
30
+ Raises:
31
+ SystemExit: If module.md is invalid or name is non-compliant
32
+ """
33
+ _this_dir = os.path.dirname(os.path.abspath(__file__))
34
+ module_md = os.path.join(_this_dir, "module.md")
35
+
36
+ # Calculate relative path for error messages
37
+ project_root = os.environ.get("KITE_PROJECT", "")
38
+ if project_root and _this_dir.startswith(project_root):
39
+ rel_path = os.path.relpath(_this_dir, project_root)
40
+ else:
41
+ rel_path = _this_dir
42
+
43
+ # Default values (will be overridden if valid config exists)
44
+ result = {
45
+ "name": "",
46
+ "preferred_port": 0,
47
+ "advertise_ip": "0.0.0.0"
48
+ }
49
+
50
+ # Check if module.md exists
51
+ if not os.path.exists(module_md):
52
+ print(f"[{rel_path}] ERROR: Invalid module configuration in module.md")
53
+ print(f" Path: {rel_path}/module.md")
54
+ print(f" Reason: File not found")
55
+ sys.exit(1)
56
+
57
+ try:
58
+ with open(module_md, encoding="utf-8") as f:
59
+ text = f.read()
60
+
61
+ # Extract YAML frontmatter (between --- markers)
62
+ import re
63
+ m = re.match(r'^---\s*\n(.*?)\n---', text, re.DOTALL)
64
+ if not m:
65
+ print(f"[{rel_path}] ERROR: Invalid module configuration in module.md")
66
+ print(f" Path: {rel_path}/module.md")
67
+ print(f" Reason: Missing YAML frontmatter")
68
+ sys.exit(1)
69
+
70
+ # Parse YAML frontmatter
71
+ try:
72
+ import yaml
73
+ fm = yaml.safe_load(m.group(1)) or {}
74
+ except ImportError:
75
+ print(f"[{rel_path}] ERROR: PyYAML not installed, cannot parse module.md")
76
+ sys.exit(1)
77
+ except Exception as e:
78
+ print(f"[{rel_path}] ERROR: Invalid module configuration in module.md")
79
+ print(f" Path: {rel_path}/module.md")
80
+ print(f" Reason: YAML parse error: {e}")
81
+ sys.exit(1)
82
+
83
+ # Validate 'name' field (required)
84
+ if "name" not in fm:
85
+ print(f"[{rel_path}] ERROR: Invalid module configuration in module.md")
86
+ print(f" Path: {rel_path}/module.md")
87
+ print(f" Reason: Missing 'name' field")
88
+ sys.exit(1)
89
+
90
+ raw_name = str(fm["name"]).strip()
91
+
92
+ if not raw_name:
93
+ print(f"[{rel_path}] ERROR: Invalid module configuration in module.md")
94
+ print(f" Path: {rel_path}/module.md")
95
+ print(f" Reason: Empty module name")
96
+ sys.exit(1)
97
+
98
+ # Validate name characters
99
+ sanitized = re.sub(r'[^a-zA-Z0-9_\-]', '', raw_name)
100
+
101
+ if sanitized != raw_name:
102
+ invalid_chars = ''.join(sorted(set(c for c in raw_name if c not in sanitized)))
103
+ print(f"[{rel_path}] ERROR: Invalid module configuration in module.md")
104
+ print(f" Path: {rel_path}/module.md")
105
+ print(f" Reason: Invalid characters in name '{raw_name}': {repr(invalid_chars)}")
106
+ sys.exit(1)
107
+
108
+ result["name"] = sanitized
109
+
110
+ # Extract optional fields
111
+ if "preferred_port" in fm:
112
+ try:
113
+ result["preferred_port"] = int(fm["preferred_port"])
114
+ except (ValueError, TypeError):
115
+ pass
116
+
117
+ if "advertise_ip" in fm:
118
+ result["advertise_ip"] = str(fm["advertise_ip"])
119
+
120
+ except SystemExit:
121
+ raise # Re-raise exit to prevent catching by outer except
122
+ except Exception as e:
123
+ print(f"[{rel_path}] ERROR: Failed to read module.md: {e}")
124
+ sys.exit(1)
125
+
126
+ return result
127
+
128
+ _module_config = _load_module_config()
129
+ MODULE_NAME = _module_config["name"]
24
130
 
25
131
 
26
132
  def _fmt_elapsed(t0: float) -> str:
@@ -266,6 +372,7 @@ def _read_stdin_kite_message(expected_type: str, timeout: float = 10) -> dict |
266
372
  # Global WS reference for publish_event callback
267
373
  _ws_global = None
268
374
  _shutting_down = False
375
+ _exit_code = 0 # Exit code for main() to use
269
376
  _monitor = None
270
377
  _monitor_task = None
271
378
 
@@ -347,7 +454,7 @@ async def main():
347
454
 
348
455
  async def _ws_loop(token: str, kernel_port: int, _t0: float):
349
456
  """Connect to Kernel with exponential backoff reconnection."""
350
- global _shutting_down
457
+ global _shutting_down, _exit_code
351
458
  retry_delay = 0.3
352
459
  max_delay = 5.0
353
460
  max_retries = 10
@@ -363,12 +470,21 @@ async def _ws_loop(token: str, kernel_port: int, _t0: float):
363
470
  attempt += 1
364
471
  if _is_auth_failure(e):
365
472
  print(f"[watchdog] Kernel 认证失败,退出")
366
- sys.exit(1)
473
+ _exit_code = 1
474
+ _shutting_down = True
475
+ return
367
476
  if attempt >= max_retries:
368
477
  print(f"[watchdog] 重连失败 {max_retries} 次,退出")
369
- sys.exit(1)
478
+ _exit_code = 1
479
+ _shutting_down = True
480
+ return
370
481
  _write_crash(type(e), e, e.__traceback__, severity="error", handled=True)
371
482
  print(f"[watchdog] 连接错误: {e}, {retry_delay:.1f}s 后重试 ({attempt}/{max_retries})")
483
+ if attempt == 5:
484
+ print(f"\033[33m[watchdog] 提示: 已连续 {attempt} 次无法连接 Kernel (端口 {kernel_port})")
485
+ if kernel_port < 1024:
486
+ print(f"[watchdog] ⚠ 端口 {kernel_port} 异常偏低,可能是 Kernel 端口绑定失败或配置错误")
487
+ print(f"[watchdog] 请检查: 1) Kernel 进程是否存活 2) kernel/module.md 中 preferred_port 配置是否正确\033[0m")
372
488
  _ws_global_clear()
373
489
  if _shutting_down:
374
490
  return
@@ -388,7 +504,7 @@ async def _ws_connect(token: str, kernel_port: int, _t0: float):
388
504
  ws_url = f"ws://127.0.0.1:{kernel_port}/ws?token={token}&id=watchdog"
389
505
  print(f"[watchdog] Connecting to Kernel: {ws_url}")
390
506
 
391
- async with websockets.connect(ws_url, open_timeout=5, ping_interval=None, ping_timeout=None, close_timeout=10) as ws:
507
+ async with websockets.connect(ws_url, open_timeout=5, ping_interval=None, close_timeout=10) as ws:
392
508
  _ws_global = ws
393
509
  print(f"[watchdog] Connected to Kernel ({_fmt_elapsed(_t0)})")
394
510
 
@@ -410,10 +526,25 @@ async def _ws_connect(token: str, kernel_port: int, _t0: float):
410
526
  await _rpc_call(ws, "registry.register", {
411
527
  "module_id": "watchdog",
412
528
  "module_type": "service",
529
+ "tools": {
530
+ "rpc": {
531
+ "module": {
532
+ "health": {"method": "health", "description": "健康检查"},
533
+ "status": {"method": "status", "description": "状态查询"}
534
+ }
535
+ }
536
+ },
413
537
  "events_publish": {
414
- "watchdog.module.unhealthy": {},
415
- "watchdog.module.recovered": {},
416
- "watchdog.alert": {},
538
+ "watchdog": {
539
+ "module": {
540
+ "unhealthy": {"description": "模块不健康"},
541
+ "recovered": {"description": "模块恢复"},
542
+ "resource_critical": {"description": "资源严重不足"},
543
+ "resource_warning": {"description": "资源警告"},
544
+ "resource_recovered": {"description": "资源恢复正常"}
545
+ },
546
+ "alert": {"description": "监控告警"}
547
+ }
417
548
  },
418
549
  "events_subscribe": [
419
550
  "system.ready",
@@ -433,12 +564,14 @@ async def _ws_connect(token: str, kernel_port: int, _t0: float):
433
564
 
434
565
  # Publish module.ready (every reconnect)
435
566
  if not _shutting_down:
567
+ startup_time = time.monotonic() - _t0
436
568
  await _rpc_call(ws, "event.publish", {
437
569
  "event_id": str(uuid.uuid4()),
438
570
  "event": "module.ready",
439
571
  "data": {
440
572
  "module_id": "watchdog",
441
573
  "graceful_shutdown": True,
574
+ "startup_time": startup_time,
442
575
  },
443
576
  })
444
577
  print(f"[watchdog] module.ready published ({_fmt_elapsed(_t0)})")
@@ -448,6 +581,11 @@ async def _ws_connect(token: str, kernel_port: int, _t0: float):
448
581
  _monitor_task = asyncio.create_task(_monitor.run())
449
582
 
450
583
  # Message loop: handle incoming RPC + events
584
+ # CRITICAL: RPC 死锁防范
585
+ # - 入站 RPC 请求必须用 create_task() 异步执行,不可 await
586
+ # - 原因:如果 handler 内部调用 rpc_call_with_response() 发出站请求,出站响应需要本接收循环来分发
587
+ # - 如果接收循环被 await handler 阻塞,出站响应永远收不到 → 超时死锁
588
+ # - 事件通知和 RPC 响应可以同步处理(它们不会反向调用 rpc_call)
451
589
  async for raw in ws:
452
590
  try:
453
591
  msg = json.loads(raw)
@@ -462,8 +600,8 @@ async def _ws_connect(token: str, kernel_port: int, _t0: float):
462
600
  # Event Notification
463
601
  await _handle_event_notification(msg, _monitor)
464
602
  elif has_method and has_id:
465
- # Incoming RPC request
466
- await _handle_rpc_request(ws, msg, _monitor)
603
+ # Incoming RPC request — run in background to prevent deadlock
604
+ asyncio.create_task(_handle_rpc_request(ws, msg, _monitor))
467
605
  elif has_id and not has_method:
468
606
  # RPC response — route to waiter
469
607
  msg_id = msg["id"]
@@ -483,7 +621,16 @@ async def _rpc_call(ws, method: str, params: dict = None):
483
621
  await ws.send(json.dumps(msg))
484
622
 
485
623
 
486
- async def _rpc_call_with_response(ws, method: str, params: dict = None, timeout: float = 5) -> dict:
624
+ async def _publish_event(ws, event: dict):
625
+ """Publish an event via RPC event.publish."""
626
+ await _rpc_call(ws, "event.publish", {
627
+ "event_id": str(uuid.uuid4()),
628
+ "event": event.get("event", ""),
629
+ "data": event.get("data", {}),
630
+ })
631
+
632
+
633
+ async def _rpc_call_with_response(ws, method: str, params: dict = None, timeout: float = 5):
487
634
  """Send a JSON-RPC 2.0 request and await the response."""
488
635
  rpc_id = str(uuid.uuid4())
489
636
  msg = {"jsonrpc": "2.0", "id": rpc_id, "method": method}
@@ -514,17 +661,41 @@ async def _publish_event(ws, event: dict):
514
661
  })
515
662
 
516
663
 
664
+ async def _handle_ping_event(data: dict):
665
+ """Handle system.ping event and reply with system.pong."""
666
+ t1 = data.get("ping_time")
667
+ t2 = time.time()
668
+
669
+ await _publish_event(_ws_global, {
670
+ "event": "system.pong",
671
+ "data": {
672
+ "module_id": MODULE_NAME,
673
+ "ping_time": t1,
674
+ "pong_time": t2,
675
+ },
676
+ })
677
+
678
+
517
679
  async def _handle_event_notification(msg: dict, monitor: HealthMonitor):
518
680
  """Handle an event notification (JSON-RPC 2.0 Notification with method='event')."""
519
681
  params = msg.get("params", {})
520
682
  event_type = params.get("event", "")
521
683
  data = params.get("data", {})
522
684
 
523
- # Special handling for module.shutdown targeting watchdog
524
- if event_type == "module.shutdown" and data.get("module_id") == "watchdog":
525
- await _handle_shutdown(monitor)
685
+ # Handle system.ping event
686
+ if event_type == "system.ping":
687
+ await _handle_ping_event(data)
526
688
  return
527
689
 
690
+ # Debug: log all shutdown events
691
+ if event_type == "module.shutdown":
692
+ target = data.get("module_id", "")
693
+ reason = data.get("reason", "")
694
+ # Handle both targeted shutdown (module_id == "watchdog") and broadcast shutdown (no module_id or launcher_lost)
695
+ if target == "watchdog" or not target or reason == "launcher_lost":
696
+ await _handle_shutdown(monitor)
697
+ return
698
+
528
699
  # Forward to monitor (extract params from JSON-RPC notification)
529
700
  await monitor.handle_event(params)
530
701
 
@@ -558,11 +729,21 @@ async def _handle_rpc_request(ws, msg: dict, monitor: HealthMonitor):
558
729
 
559
730
  async def _rpc_health(monitor: HealthMonitor) -> dict:
560
731
  """RPC handler for watchdog.health."""
732
+ # 统计不健康的模块数量
733
+ unhealthy_count = sum(1 for s in monitor.modules.values() if s.state == "unhealthy")
734
+ # 统计资源严重不足的模块数量
735
+ critical_resources = sum(1 for s in monitor.modules.values() if s.resource_state == "critical")
736
+ # 统计总重启次数
737
+ total_restarts = sum(s.restarted_count for s in monitor.modules.values())
738
+
561
739
  return {
562
740
  "status": "healthy",
741
+ "uptime_seconds": round(time.time() - _start_ts),
563
742
  "details": {
564
743
  "monitored_modules": len(monitor.modules),
565
- "uptime_seconds": round(time.time() - _start_ts),
744
+ "unhealthy_modules": unhealthy_count,
745
+ "critical_resources": critical_resources,
746
+ "total_restarts": total_restarts,
566
747
  },
567
748
  }
568
749
 
@@ -573,30 +754,38 @@ async def _rpc_status(monitor: HealthMonitor) -> dict:
573
754
 
574
755
 
575
756
  async def _handle_shutdown(monitor: HealthMonitor):
576
- """Handle module.shutdown event — exitingack → cleanup → ready → exit."""
757
+ """Handle module.shutdown event — ackexiting → cleanup → ready → exit."""
577
758
  global _shutting_down
578
759
  print("[watchdog] Received shutdown request")
579
760
  _shutting_down = True
580
- # Step 0: Send module.exiting
761
+ # Step 1: Send ack (立即确认收到)
581
762
  await _publish_event(_ws_global, {
582
- "event": "module.exiting",
583
- "data": {"module_id": "watchdog", "action": "none"},
763
+ "event": "module.shutdown.ack",
764
+ "data": {"module_id": "watchdog"},
584
765
  })
585
- # Step 1: Send ack
766
+ # Step 2: Send module.exiting (开始清理)
586
767
  await _publish_event(_ws_global, {
587
- "event": "module.shutdown.ack",
588
- "data": {"module_id": "watchdog", "estimated_cleanup": 2},
768
+ "event": "module.exiting",
769
+ "data": {
770
+ "module_id": "watchdog",
771
+ "type": "passive",
772
+ "reason": "shutdown_requested",
773
+ "restart": "auto",
774
+ "action": "none",
775
+ "timeout": 2.0,
776
+ "restart_delay": 0.0,
777
+ },
589
778
  })
590
- # Step 2: Cleanup
779
+ # Step 3: Cleanup
591
780
  monitor.stop()
592
- # Step 3: Send ready
781
+ # Step 4: Send ready (清理完成)
593
782
  await _publish_event(_ws_global, {
594
783
  "event": "module.shutdown.ready",
595
784
  "data": {"module_id": "watchdog"},
596
785
  })
597
786
  print("[watchdog] Shutdown ready, exiting")
598
- # Step 4: Exit
599
- sys.exit(0)
787
+ # Step 5: Exit
788
+ sys.exit(_exit_code)
600
789
 
601
790
 
602
791
  if __name__ == "__main__":
@@ -6,6 +6,7 @@ type: service
6
6
  state: enabled
7
7
  runtime: python
8
8
  entry: entry.py
9
+ display_order: 99
9
10
  events:
10
11
  - watchdog.module.unhealthy
11
12
  - watchdog.module.recovered