@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,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()
@@ -504,7 +504,7 @@ async def _ws_connect(token: str, kernel_port: int, _t0: float):
504
504
  ws_url = f"ws://127.0.0.1:{kernel_port}/ws?token={token}&id=watchdog"
505
505
  print(f"[watchdog] Connecting to Kernel: {ws_url}")
506
506
 
507
- async with websockets.connect(ws_url, open_timeout=5, ping_interval=20, ping_timeout=20, close_timeout=10) as ws:
507
+ async with websockets.connect(ws_url, open_timeout=5, ping_interval=None, close_timeout=10) as ws:
508
508
  _ws_global = ws
509
509
  print(f"[watchdog] Connected to Kernel ({_fmt_elapsed(_t0)})")
510
510
 
@@ -564,12 +564,14 @@ async def _ws_connect(token: str, kernel_port: int, _t0: float):
564
564
 
565
565
  # Publish module.ready (every reconnect)
566
566
  if not _shutting_down:
567
+ startup_time = time.monotonic() - _t0
567
568
  await _rpc_call(ws, "event.publish", {
568
569
  "event_id": str(uuid.uuid4()),
569
570
  "event": "module.ready",
570
571
  "data": {
571
572
  "module_id": "watchdog",
572
573
  "graceful_shutdown": True,
574
+ "startup_time": startup_time,
573
575
  },
574
576
  })
575
577
  print(f"[watchdog] module.ready published ({_fmt_elapsed(_t0)})")
@@ -578,9 +580,6 @@ async def _ws_connect(token: str, kernel_port: int, _t0: float):
578
580
  if _monitor_task is None or _monitor_task.done():
579
581
  _monitor_task = asyncio.create_task(_monitor.run())
580
582
 
581
- # Start heartbeat loop
582
- heartbeat_task = asyncio.create_task(_heartbeat_loop(ws))
583
-
584
583
  # Message loop: handle incoming RPC + events
585
584
  # CRITICAL: RPC 死锁防范
586
585
  # - 入站 RPC 请求必须用 create_task() 异步执行,不可 await
@@ -622,19 +621,16 @@ async def _rpc_call(ws, method: str, params: dict = None):
622
621
  await ws.send(json.dumps(msg))
623
622
 
624
623
 
625
- async def _heartbeat_loop(ws):
626
- """Send registry.heartbeat every 30 seconds to prevent TTL expiration."""
627
- while True:
628
- try:
629
- await asyncio.sleep(30)
630
- if not _shutting_down:
631
- await _rpc_call(ws, "registry.heartbeat", {"module_id": "watchdog"})
632
- except Exception as e:
633
- print(f"[watchdog] Heartbeat error: {e}")
634
- break
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
+ })
635
631
 
636
632
 
637
- async def _rpc_call_with_response(ws, method: str, params: dict = None, timeout: float = 5) -> dict:
633
+ async def _rpc_call_with_response(ws, method: str, params: dict = None, timeout: float = 5):
638
634
  """Send a JSON-RPC 2.0 request and await the response."""
639
635
  rpc_id = str(uuid.uuid4())
640
636
  msg = {"jsonrpc": "2.0", "id": rpc_id, "method": method}
@@ -665,12 +661,32 @@ async def _publish_event(ws, event: dict):
665
661
  })
666
662
 
667
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
+
668
679
  async def _handle_event_notification(msg: dict, monitor: HealthMonitor):
669
680
  """Handle an event notification (JSON-RPC 2.0 Notification with method='event')."""
670
681
  params = msg.get("params", {})
671
682
  event_type = params.get("event", "")
672
683
  data = params.get("data", {})
673
684
 
685
+ # Handle system.ping event
686
+ if event_type == "system.ping":
687
+ await _handle_ping_event(data)
688
+ return
689
+
674
690
  # Debug: log all shutdown events
675
691
  if event_type == "module.shutdown":
676
692
  target = data.get("module_id", "")
@@ -713,11 +729,21 @@ async def _handle_rpc_request(ws, msg: dict, monitor: HealthMonitor):
713
729
 
714
730
  async def _rpc_health(monitor: HealthMonitor) -> dict:
715
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
+
716
739
  return {
717
740
  "status": "healthy",
741
+ "uptime_seconds": round(time.time() - _start_ts),
718
742
  "details": {
719
743
  "monitored_modules": len(monitor.modules),
720
- "uptime_seconds": round(time.time() - _start_ts),
744
+ "unhealthy_modules": unhealthy_count,
745
+ "critical_resources": critical_resources,
746
+ "total_restarts": total_restarts,
721
747
  },
722
748
  }
723
749
 
@@ -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
@@ -55,6 +55,8 @@ class ModuleStatus:
55
55
  self.memory_samples: list[float] = [] # last 5 memory_rss samples
56
56
  self.recovery_since: float = 0 # when recovery observation started
57
57
  self.last_metrics: dict = {}
58
+ # Startup metrics
59
+ self.startup_time: float = 0 # module startup time in seconds (from module.ready event)
58
60
 
59
61
 
60
62
  class HealthMonitor:
@@ -409,6 +411,10 @@ class HealthMonitor:
409
411
  return
410
412
 
411
413
  if not module_id or module_id == "watchdog":
414
+ # Handle registry.updated (no module_id)
415
+ if event_type == "registry.updated":
416
+ # Registry changed, re-discover modules
417
+ asyncio.create_task(self.discover_modules())
412
418
  return
413
419
 
414
420
  if event_type == "module.started":
@@ -421,6 +427,8 @@ class HealthMonitor:
421
427
  elif event_type == "module.stopped":
422
428
  print(f"[watchdog] Received module.stopped: {module_id}")
423
429
  self.modules.pop(module_id, None)
430
+ # Re-discover to update module list
431
+ asyncio.create_task(self.discover_modules())
424
432
  await self._handle_module_stopped(module_id, data)
425
433
 
426
434
  elif event_type == "module.exiting":
@@ -441,8 +449,12 @@ class HealthMonitor:
441
449
 
442
450
  elif event_type == "module.ready":
443
451
  graceful = bool(data.get("graceful_shutdown"))
444
- print(f"[watchdog] Received module.ready: {module_id}, graceful_shutdown={graceful}")
452
+ startup_time = data.get("startup_time", 0)
453
+ print(f"[watchdog] Received module.ready: {module_id}, graceful_shutdown={graceful}, startup_time={startup_time:.3f}s")
445
454
  self._graceful_modules[module_id] = graceful
455
+ # Save startup time to module status
456
+ if module_id in self.modules:
457
+ self.modules[module_id].startup_time = startup_time
446
458
  # Reset launcher loss tracking when launcher reconnects
447
459
  if module_id == "launcher":
448
460
  self._launcher_offline = False
@@ -694,10 +706,15 @@ class HealthMonitor:
694
706
  return
695
707
  print("[watchdog] system.ready received, starting health checks")
696
708
 
697
- while self._running:
698
- # Re-discover every cycle to pick up newly started/stopped modules
699
- await self.discover_modules()
709
+ # Initial discovery (first time)
710
+ await self.discover_modules()
711
+
712
+ # Track discovery count and last discovery time
713
+ discovery_count = 1
714
+ last_discovery = asyncio.get_event_loop().time()
715
+ discovery_interval = 300.0 # 5 minutes after first 2 discoveries
700
716
 
717
+ while self._running:
701
718
  if self.modules:
702
719
  tasks = []
703
720
  for s in self.modules.values():
@@ -709,6 +726,18 @@ class HealthMonitor:
709
726
  interval = self._min_interval()
710
727
  await asyncio.sleep(interval)
711
728
 
729
+ # Periodic re-discovery
730
+ now = asyncio.get_event_loop().time()
731
+ if discovery_count < 2:
732
+ # First 2 times: discover every cycle
733
+ await self.discover_modules()
734
+ discovery_count += 1
735
+ last_discovery = now
736
+ elif now - last_discovery >= discovery_interval:
737
+ # After that: discover every 5 minutes
738
+ await self.discover_modules()
739
+ last_discovery = now
740
+
712
741
  def _min_interval(self) -> float:
713
742
  """Return the shortest check interval needed across all modules."""
714
743
  if not self.modules:
@@ -732,6 +761,7 @@ class HealthMonitor:
732
761
  "last_error": s.last_error,
733
762
  "resource_state": s.resource_state,
734
763
  "metrics": s.last_metrics,
764
+ "startup_time": s.startup_time,
735
765
  }
736
766
  for mid, s in self.modules.items()
737
767
  }
@@ -3,7 +3,7 @@ name: web
3
3
  display_name: Web Management
4
4
  version: '1.0'
5
5
  type: service
6
- state: enabled
6
+ state: manual
7
7
  runtime: python
8
8
  entry: entry.py
9
9
  preferred_port: 18766
@@ -305,7 +305,7 @@ class WebServer:
305
305
  url = f"ws://127.0.0.1:{self.kernel_port}/ws?token={self.token}&id=web"
306
306
  print(f"[web] WS connecting to Kernel")
307
307
  try:
308
- async with websockets.connect(url, open_timeout=5, ping_interval=20, ping_timeout=20, close_timeout=10) as ws:
308
+ async with websockets.connect(url, open_timeout=5, ping_interval=None, close_timeout=10) as ws:
309
309
  self._ws = ws
310
310
  elapsed = time.monotonic() - self.boot_t0 if self.boot_t0 else 0
311
311
  elapsed_str = f" ({elapsed:.1f}s)" if elapsed else ""
@@ -364,17 +364,18 @@ class WebServer:
364
364
 
365
365
  # Send module.ready (every reconnect, not just first time)
366
366
  if not self._shutting_down:
367
+ startup_time = time.monotonic() - self.boot_t0 if self.boot_t0 else 0
367
368
  await self._rpc_call(ws, "event.publish", {
368
369
  "event_id": str(uuid.uuid4()),
369
370
  "event": "module.ready",
370
371
  "data": {
371
372
  "module_id": "web",
372
373
  "graceful_shutdown": True,
374
+ "startup_time": startup_time,
373
375
  },
374
376
  })
375
- elapsed = time.monotonic() - self.boot_t0 if self.boot_t0 else 0
376
- elapsed_str = f" ({elapsed:.1f}s)" if elapsed else ""
377
- print(f"[web] module.ready sent{elapsed_str}")
377
+ elapsed_str = self._fmt_elapsed(self.boot_t0)
378
+ print(f"[web] module.ready published ({elapsed_str})")
378
379
 
379
380
  # Publish web.started event with access URL
380
381
  display_host = "localhost" if self.host == "0.0.0.0" else self.host
@@ -397,9 +398,6 @@ class WebServer:
397
398
  # - 事件通知和 RPC 响应可以同步处理(它们不会反向调用 rpc_call)
398
399
  print(f"[web] Entering receive loop")
399
400
 
400
- # Start heartbeat loop
401
- heartbeat_task = asyncio.create_task(self._heartbeat_loop(ws))
402
-
403
401
  try:
404
402
  async for raw in ws:
405
403
  try:
@@ -443,16 +441,20 @@ class WebServer:
443
441
  msg["params"] = params
444
442
  await ws.send(json.dumps(msg))
445
443
 
446
- async def _heartbeat_loop(self, ws):
447
- """Send registry.heartbeat every 30 seconds to prevent TTL expiration."""
448
- while True:
449
- try:
450
- await asyncio.sleep(30)
451
- if not self._shutting_down:
452
- await self._rpc_call(ws, "registry.heartbeat", {"module_id": "web"})
453
- except Exception as e:
454
- print(f"[web] Heartbeat error: {e}")
455
- break
444
+ async def _handle_ping_event(self, data: dict):
445
+ """Handle system.ping event and reply with system.pong."""
446
+ import time
447
+ t1 = data.get("ping_time")
448
+ t2 = time.time()
449
+
450
+ await self._publish_event({
451
+ "event": "system.pong",
452
+ "data": {
453
+ "module_id": "web",
454
+ "ping_time": t1,
455
+ "pong_time": t2,
456
+ },
457
+ })
456
458
 
457
459
  async def _handle_event_notification(self, msg: dict):
458
460
  """Handle an event notification (JSON-RPC 2.0 Notification with method='event')."""
@@ -460,6 +462,11 @@ class WebServer:
460
462
  event_type = params.get("event", "")
461
463
  data = params.get("data", {})
462
464
 
465
+ # Handle system.ping event
466
+ if event_type == "system.ping":
467
+ await self._handle_ping_event(data)
468
+ return
469
+
463
470
  # Log all events for debugging
464
471
  print(f"[web] Event received: {event_type}, data: {data}")
465
472
 
@@ -532,10 +539,15 @@ class WebServer:
532
539
 
533
540
  async def _rpc_health(self) -> dict:
534
541
  """RPC handler for web.health."""
542
+ # 获取 WebSocket 连接数
543
+ from routes.routes_management_ws import _management_clients
544
+ active_connections = len(_management_clients)
545
+
535
546
  return {
536
547
  "status": "healthy",
548
+ "uptime_seconds": round(time.time() - self._start_time),
537
549
  "details": {
538
- "uptime_seconds": round(time.time() - self._start_time),
550
+ "active_ws_connections": active_connections,
539
551
  },
540
552
  }
541
553
 
@@ -5,7 +5,7 @@
5
5
 
6
6
  // Load tokens on page load
7
7
  async function loadTokens() {
8
- console.log('[token-manager] loadTokens() called');
8
+ console.log('[web] loadTokens() called');
9
9
 
10
10
  // 立即显示加载中
11
11
  const tbody = document.getElementById('tokens-tbody');
@@ -18,16 +18,16 @@ async function loadTokens() {
18
18
  try {
19
19
  // Use kernel client to call RPC
20
20
  if (!kernelClient || !kernelClient.connected) {
21
- console.error('[token-manager] kernelClient not ready:', { kernelClient, connected: kernelClient?.connected });
21
+ console.error('[web] kernelClient not ready:', { kernelClient, connected: kernelClient?.connected });
22
22
  throw new Error('WebSocket 未连接');
23
23
  }
24
24
 
25
- console.log('[token-manager] Calling web.list_tokens...');
25
+ console.log('[web] Calling web.list_tokens...');
26
26
  const result = await kernelClient.call('web.list_tokens', {});
27
- console.log('[token-manager] Got result:', result);
27
+ console.log('[web] Got result:', result);
28
28
  renderTokens(result.tokens || []);
29
29
  } catch (err) {
30
- console.error('[token-manager] Failed to load tokens:', err);
30
+ console.error('[web] Failed to load tokens:', err);
31
31
 
32
32
  // 根据错误类型显示不同的提示
33
33
  let errorMsg = err.message;
@@ -126,7 +126,7 @@ async function revokeToken(token) {
126
126
  await kernelClient.call('web.revoke_token', { token });
127
127
 
128
128
  } catch (err) {
129
- console.error('[token-manager] Failed to revoke token:', err);
129
+ console.error('[web] Failed to revoke token:', err);
130
130
 
131
131
  // 失败后恢复该行
132
132
  const row = event.target.closest('tr');
@@ -140,7 +140,7 @@ async function revokeToken(token) {
140
140
  ? '权限不足:无法吊销 Token'
141
141
  : `吊销失败: ${err.message}`;
142
142
 
143
- console.error('[token-manager]', errorMsg);
143
+ console.error('[web]', errorMsg);
144
144
  }
145
145
  }
146
146
 
@@ -157,7 +157,7 @@ document.addEventListener('DOMContentLoaded', () => {
157
157
  modulesNav.addEventListener('click', () => {
158
158
  // Wait a bit for page switch, then load tokens
159
159
  setTimeout(() => {
160
- console.log('[token-manager] Modules page clicked, loading tokens...');
160
+ console.log('[web] Modules page clicked, loading tokens...');
161
161
  loadTokens();
162
162
  }, 100);
163
163
  });
@@ -165,10 +165,10 @@ document.addEventListener('DOMContentLoaded', () => {
165
165
 
166
166
  // Wait for kernelClient to be ready before loading tokens
167
167
  window.addEventListener('kernelClientReady', () => {
168
- console.log('[token-manager] kernelClient ready');
168
+ console.log('[web] kernelClient ready');
169
169
  // Load tokens if modules page is active on load
170
170
  if (document.getElementById('page-modules').classList.contains('active')) {
171
- console.log('[token-manager] Modules page is active, loading tokens...');
171
+ console.log('[web] Modules page is active, loading tokens...');
172
172
  loadTokens();
173
173
  }
174
174
  });
package/kernel/entry.py CHANGED
@@ -460,7 +460,7 @@ def main():
460
460
  launcher_token = secrets.token_urlsafe(32)
461
461
 
462
462
  # Step 3: Create KernelServer with launcher_token
463
- server = KernelServer(launcher_token=launcher_token, advertise_ip=advertise_ip, module_id=MODULE_NAME)
463
+ server = KernelServer(launcher_token=launcher_token, advertise_ip=advertise_ip, module_id=MODULE_NAME, boot_t0=_t0)
464
464
 
465
465
  # Step 4: Bind port
466
466
  bind_host = advertise_ip