@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
package/kernel/server.py CHANGED
@@ -33,9 +33,13 @@ class KernelServer:
33
33
  - Event notifications (delivered to subscribers)
34
34
  """
35
35
 
36
- def __init__(self, launcher_token: str = None, advertise_ip: str = "127.0.0.1"):
36
+ def __init__(self, launcher_token: str = None, advertise_ip: str = "127.0.0.1", module_id: str = None, boot_t0: float = 0):
37
+ if module_id is None:
38
+ raise ValueError("module_id is required")
39
+ self.module_id = module_id
37
40
  self.advertise_ip = advertise_ip
38
41
  self.port: int = 0 # set by entry.py before uvicorn.run
42
+ self.boot_t0 = boot_t0 # Startup time for ready event
39
43
 
40
44
  # Core components
41
45
  self.registry = RegistryStore(launcher_token) # Can be None
@@ -54,7 +58,6 @@ class KernelServer:
54
58
  )
55
59
 
56
60
  # Background tasks
57
- self._ttl_task: asyncio.Task | None = None
58
61
  self._dedup_task: asyncio.Task | None = None
59
62
  self._uvicorn_server = None # set by entry.py for graceful shutdown
60
63
  self._shutting_down = False
@@ -64,11 +67,25 @@ class KernelServer:
64
67
  self._launcher_subscribed = False
65
68
  self._ready_published = False
66
69
 
70
+ # Subscribe to events that Kernel needs to handle
71
+ # Kernel 通过订阅机制接收事件(与其他模块一致)
72
+ self.event_hub.handle_subscribe(self.module_id, ["module.shutdown", "system.pong"])
73
+
74
+ # Register internal event callback for Kernel
75
+ # Kernel 自身没有 WebSocket 连接,通过回调机制接收订阅的事件
76
+ self.event_hub.register_internal_callback(self.module_id, self._handle_internal_event)
77
+
67
78
  # Debounce timers for disconnected modules (module_id -> asyncio.Task)
68
79
  self._debounce_tasks: dict[str, asyncio.Task] = {}
69
80
  # Launcher loss timer (35s after launcher offline)
70
81
  self._launcher_loss_task: asyncio.Task | None = None
71
82
 
83
+ # Ping/Pong tracking
84
+ self._ping_sent_times: dict[str, float] = {} # module_id -> last ping sent time (t1)
85
+ self._pong_latencies: dict[str, dict] = {} # module_id -> {"outbound": ms, "inbound": ms, "last_update": timestamp}
86
+ self._pong_status: dict[str, str] = {} # module_id -> "ok" | "timeout" | "never"
87
+ self._ping_task: asyncio.Task | None = None # Global ping broadcast task
88
+
72
89
  # Build FastAPI app
73
90
  self.app = self._create_app()
74
91
 
@@ -80,15 +97,16 @@ class KernelServer:
80
97
 
81
98
  @app.on_event("startup")
82
99
  async def _startup():
83
- server._ttl_task = asyncio.create_task(server._ttl_loop())
100
+ server.event_hub.start_internal_senders()
84
101
  server._dedup_task = asyncio.create_task(server._dedup_loop())
102
+ server._ping_task = asyncio.create_task(server._ping_broadcast_loop())
85
103
 
86
104
  @app.on_event("shutdown")
87
105
  async def _shutdown():
88
- if server._ttl_task:
89
- server._ttl_task.cancel()
90
106
  if server._dedup_task:
91
107
  server._dedup_task.cancel()
108
+ if server._ping_task:
109
+ server._ping_task.cancel()
92
110
 
93
111
  # ── WebSocket endpoint ──
94
112
 
@@ -138,9 +156,9 @@ class KernelServer:
138
156
  print(f"[kernel] launcher reconnected, cancelled loss timer")
139
157
  print(f"[kernel] launcher connected")
140
158
 
141
- # Renew heartbeat on connect
142
- if module_id in server.registry.modules:
143
- server.registry.heartbeat(module_id)
159
+ # Initialize ping status for new connection
160
+ if module_id not in server._pong_status:
161
+ server._pong_status[module_id] = "never"
144
162
 
145
163
  try:
146
164
  while True:
@@ -231,17 +249,54 @@ class KernelServer:
231
249
 
232
250
  # ── Background loops ──
233
251
 
234
- async def _ttl_loop(self):
235
- """Check heartbeat TTL every 10s and publish offline events."""
252
+ async def _ping_broadcast_loop(self):
253
+ """Broadcast system.ping event every 20s to all connected modules."""
254
+ import time
255
+ import uuid
236
256
  while True:
237
- await asyncio.sleep(10)
238
257
  try:
239
- expired = self.registry.check_ttl()
240
- for mid in expired:
241
- self.event_hub.publish_internal(
242
- "module.offline", {"module_id": mid})
258
+ await asyncio.sleep(20)
259
+
260
+ # Record ping send time for all connected modules
261
+ t1 = time.time()
262
+ for module_id in list(self.connections.keys()):
263
+ self._ping_sent_times[module_id] = t1
264
+ # Mark as timeout if previous ping didn't get pong
265
+ if module_id in self._pong_status:
266
+ if self._pong_status[module_id] == "ok":
267
+ # Previous was ok, now waiting for new pong
268
+ pass
269
+ elif self._pong_status[module_id] == "never":
270
+ # Still never received pong
271
+ pass
272
+ # If was timeout, keep it as timeout until pong arrives
273
+
274
+ # Broadcast system.ping event
275
+ self.event_hub.publish_internal(
276
+ "system.ping",
277
+ {"ping_time": t1},
278
+ source=self.module_id
279
+ )
280
+
281
+ # Check for timeouts after 20s
282
+ await asyncio.sleep(20)
283
+ t_check = time.time()
284
+ for module_id in list(self.connections.keys()):
285
+ if module_id in self._ping_sent_times:
286
+ # If no pong received within 20s, mark as timeout
287
+ if module_id not in self._pong_latencies or \
288
+ self._pong_latencies[module_id].get("last_update", 0) < self._ping_sent_times[module_id]:
289
+ if self._pong_status.get(module_id) == "never":
290
+ # Keep as "never" if never received
291
+ pass
292
+ else:
293
+ self._pong_status[module_id] = "timeout"
294
+ print(f"[kernel] {module_id} ping timeout (no pong in 20s)")
295
+
296
+ except asyncio.CancelledError:
297
+ break
243
298
  except Exception as e:
244
- print(f"[kernel] TTL loop error: {e}")
299
+ print(f"[kernel] Ping broadcast loop error: {e}")
245
300
 
246
301
  async def _dedup_loop(self):
247
302
  """Clean up dedup table every 30s."""
@@ -265,7 +320,7 @@ class KernelServer:
265
320
  # 5s elapsed, module did not reconnect — mark offline
266
321
  self._debounce_tasks.pop(module_id, None)
267
322
  self.registry.set_offline(module_id)
268
- self.event_hub.publish_internal("module.offline", {"module_id": module_id})
323
+ self.event_hub.publish_internal("module.offline", {"module_id": module_id}, source=self.module_id)
269
324
  print(f"[kernel] {module_id} offline (5s debounce expired)")
270
325
 
271
326
  # If launcher went offline, start 35s launcher loss timer
@@ -289,7 +344,7 @@ class KernelServer:
289
344
  # Publish module.shutdown with reason launcher_lost to all modules
290
345
  self.event_hub.publish_internal("module.shutdown", {
291
346
  "reason": "launcher_lost",
292
- })
347
+ }, source=self.module_id)
293
348
 
294
349
  # Wait for modules to clean up (up to 10s)
295
350
  await asyncio.sleep(10)
@@ -302,7 +357,7 @@ class KernelServer:
302
357
  def self_register(self):
303
358
  """Register Kernel itself in the registry (in-memory, no RPC needed)."""
304
359
  self.registry.register_module({
305
- "module_id": "kernel",
360
+ "module_id": self.module_id,
306
361
  "module_type": "infrastructure",
307
362
  "api_endpoint": f"http://{self.advertise_ip}:{self.port}",
308
363
  "health_endpoint": "/health",
@@ -313,25 +368,266 @@ class KernelServer:
313
368
 
314
369
  def publish_ready(self):
315
370
  """Publish module.ready event for Kernel (internal, no WS needed)."""
371
+ import time
372
+ startup_time = time.monotonic() - self.boot_t0 if self.boot_t0 else 0
316
373
  self.event_hub.publish_internal("module.ready", {
317
- "module_id": "kernel",
374
+ "module_id": self.module_id,
318
375
  "ws_endpoint": f"ws://{self.advertise_ip}:{self.port}/ws",
319
376
  "graceful_shutdown": True,
320
- })
377
+ "startup_time": startup_time,
378
+ }, source=self.module_id)
379
+
380
+ async def _handle_internal_event(self, event_type: str, data: dict):
381
+ """内部事件处理器(通过 EventHub 回调机制调用)
382
+
383
+ Kernel 自身没有 WebSocket 连接,通过此回调接收发送给自己的事件。
384
+
385
+ Args:
386
+ event_type: 事件类型(如 "module.shutdown")
387
+ data: 事件数据
388
+
389
+ Note:
390
+ 此方法由 EventHub 的 _invoke_callback_safe 包装调用,异常会被捕获并记录。
391
+ """
392
+ try:
393
+ # 处理 Kernel 订阅的事件
394
+ if event_type == "module.shutdown":
395
+ await self.handle_shutdown_event(data)
396
+ elif event_type == "system.pong":
397
+ await self._handle_pong_event(data)
398
+ # 忽略系统广播事件(Kernel 没有订阅但会收到广播)
399
+ elif event_type in ("module.ready", "module.registered", "module.started",
400
+ "module.stopped", "module.crashed", "module.exiting",
401
+ "module.offline", "system.ready", "registry.updated"):
402
+ # 这些是系统事件,会广播给所有模块,Kernel 收到是正常的
403
+ pass
404
+ else:
405
+ # 收到未知事件,可能是订阅了但忘记实现处理器
406
+ print(f"[kernel] Warning: Received unhandled event: {event_type}")
407
+ except Exception as e:
408
+ # 双重保险:即使 EventHub 的包装器失败,这里也捕获
409
+ print(f"[kernel] Error handling internal event {event_type}: {e}")
410
+ import traceback
411
+ traceback.print_exc()
412
+ raise # 重新抛出,让 EventHub 的包装器记录
413
+
414
+ async def _handle_pong_event(self, data: dict):
415
+ """处理 system.pong 事件,计算往返延迟
416
+
417
+ Args:
418
+ data: {
419
+ "module_id": str,
420
+ "ping_time": float, # t1 - Kernel 发送 ping 的时间
421
+ "pong_time": float, # t2 - 模块收到 ping 并发送 pong 的时间
422
+ }
423
+ """
424
+ import time
425
+ module_id = data.get("module_id")
426
+ t1 = data.get("ping_time") # Kernel 发送 ping 的时间
427
+ t2 = data.get("pong_time") # 模块收到 ping 并发送 pong 的时间
428
+ t3 = time.time() # Kernel 收到 pong 的时间
429
+
430
+ if not module_id or t1 is None or t2 is None:
431
+ print(f"[kernel] Invalid pong event data: {data}")
432
+ return
433
+
434
+ # 验证 ping_time 是否匹配
435
+ if module_id not in self._ping_sent_times:
436
+ print(f"[kernel] Received pong from {module_id} but no ping record")
437
+ return
438
+
439
+ expected_t1 = self._ping_sent_times[module_id]
440
+ if abs(t1 - expected_t1) > 0.1: # 允许 100ms 误差
441
+ print(f"[kernel] Pong from {module_id} has mismatched ping_time (expected {expected_t1}, got {t1})")
442
+ return
443
+
444
+ # 计算两个延迟
445
+ outbound_ms = (t2 - t1) * 1000 # 去程:Kernel → 模块
446
+ inbound_ms = (t3 - t2) * 1000 # 回程:模块 → Kernel
447
+
448
+ self._pong_latencies[module_id] = {
449
+ "outbound": round(outbound_ms, 2),
450
+ "inbound": round(inbound_ms, 2),
451
+ "last_update": t3,
452
+ }
453
+ self._pong_status[module_id] = "ok"
454
+
455
+ async def handle_shutdown_event(self, event_data):
456
+ """处理 module.shutdown 事件(标准优雅退出流程)
457
+
458
+ Args:
459
+ event_data: 事件数据,应包含 module_id 字段
460
+
461
+ Note:
462
+ 仅响应针对 Kernel 的 shutdown 事件(module_id == "kernel")。
463
+ 防御性检查:验证数据完整性,防止重复处理。
464
+ """
465
+ # 防御性检查:验证数据类型
466
+ if not isinstance(event_data, dict):
467
+ print(f"[kernel] Warning: Invalid shutdown event data type: {type(event_data)}")
468
+ return
469
+
470
+ # 防御性检查:验证 module_id
471
+ target_module = event_data.get("module_id")
472
+ if target_module != self.module_id:
473
+ print(f"[kernel] Warning: Received shutdown for wrong module: {target_module}")
474
+ return
475
+
476
+ # 防御性检查:防止重复处理
477
+ if self._shutting_down:
478
+ print("[kernel] Warning: Shutdown already in progress, ignoring duplicate event")
479
+ return
480
+
481
+ reason = event_data.get("reason", "unknown")
482
+ print(f"[kernel] 收到 shutdown 事件 (reason={reason}),开始优雅退出")
483
+
484
+ try:
485
+ # Step 1: 立即发送 ack
486
+ self.event_hub.publish_internal("module.shutdown.ack", {
487
+ "module_id": self.module_id
488
+ }, source=self.module_id)
489
+ print("[kernel] 已发送 shutdown.ack")
490
+
491
+ # Step 2: 发送 exiting
492
+ self.event_hub.publish_internal("module.exiting", {
493
+ "module_id": self.module_id,
494
+ "type": "passive",
495
+ "reason": f"响应 shutdown ({reason})",
496
+ "restart": "auto",
497
+ "action": "none",
498
+ "timeout": 5.0
499
+ }, source=self.module_id)
500
+ print("[kernel] 已发送 exiting")
501
+
502
+ # Step 3: 执行清理
503
+ await self._do_cleanup()
504
+
505
+ # Step 4: 发送 ready
506
+ self.event_hub.publish_internal("module.shutdown.ready", {
507
+ "module_id": self.module_id
508
+ }, source=self.module_id)
509
+ print("[kernel] 已发送 shutdown.ready")
510
+
511
+ # Step 5: 等待一小段时间确保事件发送完成
512
+ await asyncio.sleep(0.2)
513
+
514
+ # Step 6: 关闭所有 WebSocket 连接(包括 Launcher)
515
+ close_tasks = []
516
+ for mid, ws in list(self.connections.items()):
517
+ try:
518
+ close_tasks.append(ws.close(code=1000, reason="Graceful shutdown complete"))
519
+ except Exception as e:
520
+ print(f"[kernel] 关闭 {mid} 连接失败: {e}")
521
+
522
+ if close_tasks:
523
+ await asyncio.gather(*close_tasks, return_exceptions=True)
524
+ print(f"[kernel] 已关闭 {len(close_tasks)} 个 WebSocket 连接")
525
+
526
+ # Step 7: 触发 uvicorn 关闭
527
+ if self._uvicorn_server:
528
+ print("[kernel] 触发 uvicorn 关闭")
529
+ self._uvicorn_server.should_exit = True
530
+ else:
531
+ print("[kernel] uvicorn 引用未设置,直接退出")
532
+ import sys
533
+ sys.exit(0)
534
+
535
+ except Exception as e:
536
+ # 如果优雅退出失败,强制退出
537
+ print(f"[kernel] 优雅退出过程中发生错误: {e}")
538
+ import traceback
539
+ traceback.print_exc()
540
+ print("[kernel] 强制退出")
541
+ import sys
542
+ sys.exit(1)
543
+
544
+ async def _do_cleanup(self):
545
+ """执行清理工作(从原 shutdown() 方法提取)"""
546
+ if self._shutting_down:
547
+ return
548
+
549
+ self._shutting_down = True
550
+ print("[kernel] 执行清理工作...")
551
+
552
+ # 取消所有 debounce 任务
553
+ if self._debounce_tasks:
554
+ print(f"[kernel] 取消 {len(self._debounce_tasks)} 个 debounce 任务")
555
+ for task in self._debounce_tasks.values():
556
+ if not task.done():
557
+ task.cancel()
558
+ self._debounce_tasks.clear()
559
+
560
+ # 取消 launcher loss 计时器
561
+ if self._launcher_loss_task and not self._launcher_loss_task.done():
562
+ print("[kernel] 取消 launcher loss 计时器")
563
+ self._launcher_loss_task.cancel()
564
+ self._launcher_loss_task = None
565
+
566
+ # 关闭其他模块的 WebSocket 连接(保留 Launcher 连接)
567
+ other_connections = {mid: ws for mid, ws in self.connections.items() if mid != "launcher"}
568
+ if other_connections:
569
+ print(f"[kernel] 关闭 {len(other_connections)} 个其他模块的 WebSocket 连接")
570
+ for module_id, ws in other_connections.items():
571
+ try:
572
+ await ws.close(code=1001, reason="Server shutting down")
573
+ except Exception as e:
574
+ print(f"[kernel] 关闭连接失败 {module_id}: {e}")
575
+
576
+ # 清空 RPC 转发队列
577
+ pending_count = len(self.rpc_router._pending)
578
+ if pending_count > 0:
579
+ print(f"[kernel] 清空 {pending_count} 个待处理的 RPC 转发")
580
+ self.rpc_router._pending.clear()
581
+
582
+ print("[kernel] 清理完成")
321
583
 
322
584
  async def shutdown(self):
323
- """Shutdown Kernel gracefully. Called by Launcher via RPC."""
585
+ """Shutdown Kernel gracefully (legacy method for launcher_lost scenario).
586
+
587
+ This method is kept for the launcher_lost timeout scenario where Kernel
588
+ must shut down autonomously without receiving a shutdown event.
589
+ """
324
590
  if self._shutting_down:
325
591
  return
326
592
 
327
593
  self._shutting_down = True
328
- print("[kernel] Shutting down...")
594
+ print("[kernel] Shutting down (launcher_lost)...")
595
+
596
+ # Cancel all debounce tasks
597
+ print(f"[kernel] Cancelling {len(self._debounce_tasks)} debounce tasks...")
598
+ for task in self._debounce_tasks.values():
599
+ if not task.done():
600
+ task.cancel()
601
+ self._debounce_tasks.clear()
602
+
603
+ # Cancel launcher loss timer
604
+ if self._launcher_loss_task and not self._launcher_loss_task.done():
605
+ print("[kernel] Cancelling launcher loss timer...")
606
+ self._launcher_loss_task.cancel()
607
+ self._launcher_loss_task = None
608
+
609
+ # Close all WebSocket connections
610
+ print(f"[kernel] Closing {len(self.connections)} WebSocket connections...")
611
+ for module_id, ws in list(self.connections.items()):
612
+ try:
613
+ await ws.close(code=1001, reason="Server shutting down")
614
+ except Exception as e:
615
+ print(f"[kernel] Failed to close connection for {module_id}: {e}")
616
+ self.connections.clear()
329
617
 
330
- # Brief delay to ensure RPC response is sent
331
- await asyncio.sleep(0.1)
618
+ # Clear pending RPC forwards
619
+ pending_count = len(self.rpc_router._pending)
620
+ if pending_count > 0:
621
+ print(f"[kernel] Clearing {pending_count} pending RPC forwards...")
622
+ self.rpc_router._pending.clear()
332
623
 
333
624
  # Trigger uvicorn shutdown
334
625
  if self._uvicorn_server:
626
+ print("[kernel] Triggering uvicorn graceful shutdown...")
335
627
  self._uvicorn_server.should_exit = True
336
628
  else:
337
- print("[kernel] Warning: uvicorn server reference not set")
629
+ print("[kernel] Warning: uvicorn server reference not set, forcing exit")
630
+ import sys
631
+ sys.exit(0)
632
+
633
+ print("[kernel] Shutdown complete")
@@ -0,0 +1,3 @@
1
+ """Kite CLI - 模块安装管理工具"""
2
+
3
+ __version__ = "1.0.0"
@@ -0,0 +1,5 @@
1
+ """支持 python -m kite_cli 调用"""
2
+ from kite_cli.main import main
3
+
4
+ if __name__ == "__main__":
5
+ exit(main())
@@ -0,0 +1 @@
1
+ """命令模块"""
@@ -0,0 +1,101 @@
1
+ """clean 命令实现 - 清理临时文件和缓存"""
2
+ import shutil
3
+ import tempfile
4
+ from pathlib import Path
5
+ from kite_cli.utils.interactive import confirm_action
6
+ from kite_cli.utils.operation_log import log_operation
7
+
8
+
9
+ def run_clean(args):
10
+ """执行清理命令"""
11
+ skip_confirm = args.yes if hasattr(args, 'yes') else False
12
+ clean_all = args.all if hasattr(args, 'all') else False
13
+
14
+ cleaned_items = []
15
+
16
+ # 1. 清理系统临时目录中的 kite-install-* 目录
17
+ temp_dir = Path(tempfile.gettempdir())
18
+ kite_temp_dirs = list(temp_dir.glob("kite-install-*"))
19
+
20
+ if kite_temp_dirs:
21
+ print(f"[Info] 发现 {len(kite_temp_dirs)} 个临时安装目录")
22
+ if not skip_confirm:
23
+ if not confirm_action("是否清理临时安装目录?"):
24
+ print("[Cancelled] 已取消")
25
+ return 1
26
+
27
+ for temp_path in kite_temp_dirs:
28
+ try:
29
+ shutil.rmtree(temp_path, ignore_errors=True)
30
+ cleaned_items.append({"type": "temp_dir", "path": str(temp_path)})
31
+ print(f" [Done] 已清理: {temp_path.name}")
32
+ except Exception as e:
33
+ print(f" [Warning] 清理失败: {temp_path.name} - {e}")
34
+
35
+ # 2. 如果指定 --all,清理所有模块的 .venv 和 node_modules
36
+ if clean_all:
37
+ print("\n[Warning] --all 选项会清理所有模块的依赖环境")
38
+ if not skip_confirm:
39
+ if not confirm_action("确认清理所有模块依赖?"):
40
+ print("[Cancelled] 已取消清理依赖")
41
+ else:
42
+ cleaned_items.extend(clean_module_deps())
43
+ else:
44
+ cleaned_items.extend(clean_module_deps())
45
+
46
+ # 记录操作日志
47
+ if cleaned_items:
48
+ log_operation("clean", {
49
+ "cleaned": cleaned_items
50
+ }, success=True)
51
+
52
+ print(f"\n[Done] 清理完成!共清理 {len(cleaned_items)} 项")
53
+ else:
54
+ print("[Info] 没有需要清理的内容")
55
+
56
+ return 0
57
+
58
+
59
+ def clean_module_deps() -> list:
60
+ """清理所有模块的依赖环境"""
61
+ import os
62
+ from kite_cli.utils.paths import get_install_path
63
+
64
+ cleaned = []
65
+
66
+ # 遍历所有安装位置
67
+ for location in ["dev", "local", "global"]:
68
+ try:
69
+ base_path = get_install_path(location, "")
70
+ if not base_path.exists():
71
+ continue
72
+
73
+ # 查找所有模块目录
74
+ for module_dir in base_path.iterdir():
75
+ if not module_dir.is_dir():
76
+ continue
77
+
78
+ # 清理 .venv
79
+ venv_dir = module_dir / ".venv"
80
+ if venv_dir.exists():
81
+ try:
82
+ shutil.rmtree(venv_dir)
83
+ cleaned.append({"type": "venv", "path": str(venv_dir)})
84
+ print(f" [Done] 已清理: {module_dir.name}/.venv")
85
+ except Exception as e:
86
+ print(f" [Warning] 清理失败: {module_dir.name}/.venv - {e}")
87
+
88
+ # 清理 node_modules
89
+ node_modules = module_dir / "node_modules"
90
+ if node_modules.exists():
91
+ try:
92
+ shutil.rmtree(node_modules)
93
+ cleaned.append({"type": "node_modules", "path": str(node_modules)})
94
+ print(f" [Done] 已清理: {module_dir.name}/node_modules")
95
+ except Exception as e:
96
+ print(f" [Warning] 清理失败: {module_dir.name}/node_modules - {e}")
97
+
98
+ except Exception as e:
99
+ print(f" [Warning] 扫描 {location} 失败: {e}")
100
+
101
+ return cleaned
@@ -0,0 +1,67 @@
1
+ """
2
+ kite deps-install 命令
3
+
4
+ 只负责安装依赖库
5
+ """
6
+
7
+ import sys
8
+ from pathlib import Path
9
+
10
+
11
+ def run_deps_install(args):
12
+ """安装依赖库"""
13
+ project_root = Path(__file__).parent.parent.parent
14
+ sys.path.insert(0, str(project_root))
15
+
16
+ from core.env_checker import (
17
+ check_dependencies_installed,
18
+ install_dependencies,
19
+ load_dependencies_lock
20
+ )
21
+
22
+ print("=" * 60)
23
+ print(" Kite 依赖库安装")
24
+ print("=" * 60)
25
+ print()
26
+
27
+ try:
28
+ # 1. 加载依赖锁定
29
+ lock_data = load_dependencies_lock()
30
+ if not lock_data:
31
+ print("✓ 无依赖锁定文件,跳过")
32
+ return 0
33
+
34
+ dependencies = lock_data.get("dependencies", {})
35
+ if not dependencies:
36
+ print("✓ 无依赖要求,跳过")
37
+ return 0
38
+
39
+ print(f"依赖锁定文件: {len(dependencies)} 个依赖")
40
+
41
+ # 2. 检查依赖
42
+ deps_ok, deps_msg, deps_details = check_dependencies_installed()
43
+ if deps_ok:
44
+ print(f"✓ 依赖库已安装且版本一致")
45
+ return 0
46
+
47
+ # 3. 安装依赖
48
+ print(f"✗ {deps_msg}")
49
+ if deps_details:
50
+ if deps_details.get("missing"):
51
+ print(f" 缺失: {len(deps_details['missing'])} 个")
52
+ if deps_details.get("version_mismatch"):
53
+ print(f" 版本不匹配: {len(deps_details['version_mismatch'])} 个")
54
+
55
+ print(f"\n正在安装依赖...")
56
+ if not install_dependencies():
57
+ print("✗ 依赖安装失败")
58
+ return 1
59
+
60
+ print(f"✓ 依赖安装成功")
61
+ return 0
62
+
63
+ except Exception as e:
64
+ print(f"\n✗ 依赖安装失败: {e}")
65
+ import traceback
66
+ traceback.print_exc()
67
+ return 1
@@ -0,0 +1,35 @@
1
+ """doctor 命令实现 - 检查下载工具状态"""
2
+ from kite_cli.core.tool_installer import ToolInstaller
3
+
4
+
5
+ def run_doctor(args):
6
+ """执行环境检查命令"""
7
+ print("[Doctor] 检查下载工具状态...\n")
8
+
9
+ tools = [
10
+ ("Python", "python"),
11
+ ("pip", "pip"),
12
+ ("Node.js", "node"),
13
+ ("npm", "npm"),
14
+ ("Git", "git")
15
+ ]
16
+
17
+ all_ok = True
18
+
19
+ for name, cmd in tools:
20
+ status = ToolInstaller.check_tool(cmd)
21
+ if status:
22
+ print(f" [OK] {name:10} 已安装")
23
+ else:
24
+ print(f" [Missing] {name:10} 未安装")
25
+ all_ok = False
26
+
27
+ print()
28
+
29
+ if all_ok:
30
+ print("[Done] 所有工具已就绪")
31
+ return 0
32
+ else:
33
+ print("[Warning] 部分工具未安装")
34
+ print("[Info] 使用 'kite install' 时会自动尝试安装缺失的工具")
35
+ return 1