@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.
- package/CHANGELOG.md +302 -0
- package/cli.js +119 -4
- package/core/dependency_checker.py +250 -0
- package/core/env_checker.py +490 -0
- package/dependencies_lock.json +128 -0
- package/extensions/agents/assistant/entry.py +111 -1
- package/extensions/agents/assistant/server.py +279 -215
- package/extensions/channels/acp_channel/entry.py +111 -1
- package/extensions/channels/acp_channel/module.md +23 -22
- package/extensions/channels/acp_channel/server.py +279 -215
- package/extensions/event_hub_bench/entry.py +107 -1
- package/extensions/services/backup/entry.py +306 -21
- package/extensions/services/backup/module.md +24 -22
- package/extensions/services/evol/auth_manager.py +443 -0
- package/extensions/services/evol/config.yaml +149 -0
- package/extensions/services/evol/config_loader.py +117 -0
- package/extensions/services/evol/entry.py +406 -0
- package/extensions/services/evol/evol_api.py +173 -0
- package/extensions/services/evol/evol_config.json5 +29 -0
- package/extensions/services/evol/migrate_tokens.py +122 -0
- package/extensions/services/evol/module.md +32 -0
- package/extensions/services/evol/pairing.py +250 -0
- package/extensions/services/evol/pairing_codes.jsonl +1 -0
- package/extensions/services/evol/relay.py +682 -0
- package/extensions/services/evol/relay_config.json5 +67 -0
- package/extensions/services/evol/routes/__init__.py +1 -0
- package/extensions/services/evol/routes/routes_management_ws.py +127 -0
- package/extensions/services/evol/routes/routes_rpc.py +89 -0
- package/extensions/services/evol/routes/routes_test.py +61 -0
- package/extensions/services/evol/server.py +875 -0
- package/extensions/services/evol/static/css/style.css +1200 -0
- package/extensions/services/evol/static/index.html +781 -0
- package/extensions/services/evol/static/index_evol.html +14 -0
- package/extensions/services/evol/static/js/app.js +6304 -0
- package/extensions/services/evol/static/js/auth.js +326 -0
- package/extensions/services/evol/static/js/dialog.js +285 -0
- package/extensions/services/evol/static/js/evol-app-fixed.js +50 -0
- package/extensions/services/evol/static/js/evol-app.js +1949 -0
- package/extensions/services/evol/static/js/evol-app.js.bak +1800 -0
- package/extensions/services/evol/static/js/kernel-client-example.js +228 -0
- package/extensions/services/evol/static/js/kernel-client.js +396 -0
- package/extensions/services/evol/static/js/main.js +141 -0
- package/extensions/services/evol/static/js/registry-tests.js +585 -0
- package/extensions/services/evol/static/js/stats.js +217 -0
- package/extensions/services/evol/static/js/token-manager.js +175 -0
- package/extensions/services/evol/static/pairing.html +248 -0
- package/extensions/services/evol/static/test_registry.html +262 -0
- package/extensions/services/evol/static/test_relay.html +462 -0
- package/extensions/services/evol/stats_manager.py +240 -0
- package/extensions/services/model_service/entry.py +167 -19
- package/extensions/services/model_service/module.md +21 -22
- package/extensions/services/proxy/.claude/settings.local.json +13 -0
- package/extensions/services/proxy/CHANGELOG_20260308.md +258 -0
- package/extensions/services/proxy/_fix_prints.py +133 -0
- package/extensions/services/proxy/_fix_prints2.py +87 -0
- package/extensions/services/proxy/agentcp/LICENCE +178 -0
- package/extensions/services/proxy/agentcp/README copy.md +85 -0
- package/extensions/services/proxy/agentcp/README.md +260 -0
- package/extensions/services/proxy/agentcp/__init__.py +16 -0
- package/extensions/services/proxy/agentcp/agent.py +4 -0
- package/extensions/services/proxy/agentcp/agentcp.py +2494 -0
- package/extensions/services/proxy/agentcp/agentprofile.json +89 -0
- package/extensions/services/proxy/agentcp/ap/__init__.py +16 -0
- package/extensions/services/proxy/agentcp/ap/ap_client.py +316 -0
- package/extensions/services/proxy/agentcp/assets/images/wechat_qr.png +0 -0
- package/extensions/services/proxy/agentcp/backup/metrics.json +31 -0
- package/extensions/services/proxy/agentcp/base/__init__.py +20 -0
- package/extensions/services/proxy/agentcp/base/auth_client.py +257 -0
- package/extensions/services/proxy/agentcp/base/client.py +112 -0
- package/extensions/services/proxy/agentcp/base/env.py +34 -0
- package/extensions/services/proxy/agentcp/base/html_util.py +336 -0
- package/extensions/services/proxy/agentcp/base/log.py +98 -0
- package/extensions/services/proxy/agentcp/ca/__init__.py +17 -0
- package/extensions/services/proxy/agentcp/ca/ca_client.py +414 -0
- package/extensions/services/proxy/agentcp/ca/ca_root.py +74 -0
- package/extensions/services/proxy/agentcp/context/__init__.py +20 -0
- package/extensions/services/proxy/agentcp/context/context.py +73 -0
- package/extensions/services/proxy/agentcp/context/exceptions.py +114 -0
- package/extensions/services/proxy/agentcp/create_profile.py +125 -0
- package/extensions/services/proxy/agentcp/create_profile_weather.py +125 -0
- package/extensions/services/proxy/agentcp/db/__init__.py +15 -0
- package/extensions/services/proxy/agentcp/db/db_mananger.py +550 -0
- package/extensions/services/proxy/agentcp/docs/UDP_HEARTBEAT_FIX_REPORT.md +265 -0
- package/extensions/services/proxy/agentcp/docs/heartbeat_issue_analysis.md +291 -0
- package/extensions/services/proxy/agentcp/file/__init__.py +16 -0
- package/extensions/services/proxy/agentcp/file/file_client.py +141 -0
- package/extensions/services/proxy/agentcp/file/wss_binary_message.py +137 -0
- package/extensions/services/proxy/agentcp/hcp.py +299 -0
- package/extensions/services/proxy/agentcp/heartbeat/__init__.py +16 -0
- package/extensions/services/proxy/agentcp/heartbeat/heartbeat_client.py +360 -0
- package/extensions/services/proxy/agentcp/improved_scheduler.py +498 -0
- package/extensions/services/proxy/agentcp/llm_agent_utils.py +249 -0
- package/extensions/services/proxy/agentcp/llm_server.py +172 -0
- package/extensions/services/proxy/agentcp/mermaid.py +210 -0
- package/extensions/services/proxy/agentcp/message.py +149 -0
- package/extensions/services/proxy/agentcp/metrics.py +256 -0
- package/extensions/services/proxy/agentcp/monitoring/__init__.py +20 -0
- package/extensions/services/proxy/agentcp/monitoring/global_monitor.py +27 -0
- package/extensions/services/proxy/agentcp/monitoring/metrics_store.py +325 -0
- package/extensions/services/proxy/agentcp/monitoring/monitoring_service.py +269 -0
- package/extensions/services/proxy/agentcp/monitoring/sliding_window.py +222 -0
- package/extensions/services/proxy/agentcp/monitoring/standalone_reader.py +224 -0
- package/extensions/services/proxy/agentcp/msg/__init__.py +21 -0
- package/extensions/services/proxy/agentcp/msg/connection_manager.py +456 -0
- package/extensions/services/proxy/agentcp/msg/message_client.py +2058 -0
- package/extensions/services/proxy/agentcp/msg/message_serialize.py +263 -0
- package/extensions/services/proxy/agentcp/msg/open_ai_message.py +88 -0
- package/extensions/services/proxy/agentcp/msg/session_manager.py +1062 -0
- package/extensions/services/proxy/agentcp/msg/stream_client.py +267 -0
- package/extensions/services/proxy/agentcp/msg/websocket_file_receiver.py +89 -0
- package/extensions/services/proxy/agentcp/msg/ws_logger.py +685 -0
- package/extensions/services/proxy/agentcp/msg/wss_binary_message.py +137 -0
- package/extensions/services/proxy/agentcp/requirements.txt +7 -0
- package/extensions/services/proxy/agentcp/samples/agent_graph/README.md +37 -0
- package/extensions/services/proxy/agentcp/samples/agent_graph/agentprofile.json +89 -0
- package/extensions/services/proxy/agentcp/samples/agent_graph/create_profile.py +138 -0
- package/extensions/services/proxy/agentcp/samples/agent_graph/main.py +164 -0
- package/extensions/services/proxy/agentcp/samples/agent_use/create_profile.py +123 -0
- package/extensions/services/proxy/agentcp/samples/agent_use/llm/create_profile.py +129 -0
- package/extensions/services/proxy/agentcp/samples/agent_use/llm/env.json +5 -0
- package/extensions/services/proxy/agentcp/samples/agent_use/llm/main.py +146 -0
- package/extensions/services/proxy/agentcp/samples/agent_use/main.py +123 -0
- package/extensions/services/proxy/agentcp/samples/agent_use/readme.md +379 -0
- package/extensions/services/proxy/agentcp/samples/agent_use/search/create_profile.py +129 -0
- package/extensions/services/proxy/agentcp/samples/agent_use/search/main.py +28 -0
- package/extensions/services/proxy/agentcp/samples/agent_use/tool/create_profile.py +129 -0
- package/extensions/services/proxy/agentcp/samples/agent_use/tool/main.py +20 -0
- package/extensions/services/proxy/agentcp/samples/ali_amap/README.md +97 -0
- package/extensions/services/proxy/agentcp/samples/ali_amap/amap_agent.py +88 -0
- package/extensions/services/proxy/agentcp/samples/ali_amap/create_profile.py +125 -0
- package/extensions/services/proxy/agentcp/samples/compute_agent/agent/powershell.py +228 -0
- package/extensions/services/proxy/agentcp/samples/compute_agent/agent/software.py +63 -0
- package/extensions/services/proxy/agentcp/samples/compute_agent/agent/tools.py +36 -0
- package/extensions/services/proxy/agentcp/samples/compute_agent/browser_user.py +41 -0
- package/extensions/services/proxy/agentcp/samples/deepseek/README.md +79 -0
- package/extensions/services/proxy/agentcp/samples/deepseek/create_profile.py +126 -0
- package/extensions/services/proxy/agentcp/samples/deepseek/deepseek.py +42 -0
- package/extensions/services/proxy/agentcp/samples/dify_chat/README.md +78 -0
- package/extensions/services/proxy/agentcp/samples/dify_chat/create_profile.py +126 -0
- package/extensions/services/proxy/agentcp/samples/dify_chat/dify_chat.py +47 -0
- package/extensions/services/proxy/agentcp/samples/dify_workflow/README.md +78 -0
- package/extensions/services/proxy/agentcp/samples/dify_workflow/create_profile.py +126 -0
- package/extensions/services/proxy/agentcp/samples/dify_workflow/dify_workflow.py +46 -0
- package/extensions/services/proxy/agentcp/samples/executor/README.md +44 -0
- package/extensions/services/proxy/agentcp/samples/executor/agentprofile.json +89 -0
- package/extensions/services/proxy/agentcp/samples/executor/create_profile.py +139 -0
- package/extensions/services/proxy/agentcp/samples/executor/main.py +160 -0
- package/extensions/services/proxy/agentcp/samples/filereader/README.md +45 -0
- package/extensions/services/proxy/agentcp/samples/filereader/agentprofile.json +90 -0
- package/extensions/services/proxy/agentcp/samples/filereader/create_profile.py +137 -0
- package/extensions/services/proxy/agentcp/samples/filereader/main.py +253 -0
- package/extensions/services/proxy/agentcp/samples/filewriter/README.md +38 -0
- package/extensions/services/proxy/agentcp/samples/filewriter/agentprofile.json +91 -0
- package/extensions/services/proxy/agentcp/samples/filewriter/create_profile.py +138 -0
- package/extensions/services/proxy/agentcp/samples/filewriter/main.py +289 -0
- package/extensions/services/proxy/agentcp/samples/hcp/README.md +85 -0
- package/extensions/services/proxy/agentcp/samples/hcp/acp_weather_agent.zip +0 -0
- package/extensions/services/proxy/agentcp/samples/hcp/create_profile.py +125 -0
- package/extensions/services/proxy/agentcp/samples/hcp/hcp.py +237 -0
- package/extensions/services/proxy/agentcp/samples/helloworld/README.md +68 -0
- package/extensions/services/proxy/agentcp/samples/helloworld/hello_world.py +40 -0
- package/extensions/services/proxy/agentcp/samples/llm_agent/MEADME.md +117 -0
- package/extensions/services/proxy/agentcp/samples/llm_agent/create_profile.py +125 -0
- package/extensions/services/proxy/agentcp/samples/llm_agent/qwen_agent.py +136 -0
- package/extensions/services/proxy/agentcp/samples/local_llm_agent/README.md +90 -0
- package/extensions/services/proxy/agentcp/samples/local_llm_agent/create_profile.py +125 -0
- package/extensions/services/proxy/agentcp/samples/local_llm_agent/main.py +49 -0
- package/extensions/services/proxy/agentcp/samples/query_llm_from_agent/README.md +55 -0
- package/extensions/services/proxy/agentcp/samples/query_llm_from_agent/create_profile.py +125 -0
- package/extensions/services/proxy/agentcp/samples/query_llm_from_agent/main.py +23 -0
- package/extensions/services/proxy/agentcp/samples/query_weather_api_agent/README.md +103 -0
- package/extensions/services/proxy/agentcp/samples/query_weather_api_agent/create_profile.py +125 -0
- package/extensions/services/proxy/agentcp/samples/query_weather_api_agent/main.py +69 -0
- package/extensions/services/proxy/agentcp/samples/query_weather_from_agent/README.md +58 -0
- package/extensions/services/proxy/agentcp/samples/query_weather_from_agent/create_profile.py +125 -0
- package/extensions/services/proxy/agentcp/samples/query_weather_from_agent/main.py +25 -0
- package/extensions/services/proxy/agentcp/samples/qwen3/README.md +71 -0
- package/extensions/services/proxy/agentcp/samples/qwen3/create_profile.py +126 -0
- package/extensions/services/proxy/agentcp/samples/qwen3/qwen3.py +37 -0
- package/extensions/services/proxy/agentcp/samples/qwen3_tools/README.md +133 -0
- package/extensions/services/proxy/agentcp/samples/qwen3_tools/create_profile.py +126 -0
- package/extensions/services/proxy/agentcp/samples/qwen3_tools/qwen3_tools.py +98 -0
- package/extensions/services/proxy/agentcp/samples/search/create_profile_qwen.py +125 -0
- package/extensions/services/proxy/agentcp/samples/search/create_profile_search.py +125 -0
- package/extensions/services/proxy/agentcp/samples/search/qwen_agent.py +136 -0
- package/extensions/services/proxy/agentcp/samples/search/search_agent.py +170 -0
- package/extensions/services/proxy/agentcp/samples/wrapper_agently_to_agent/README.md +89 -0
- package/extensions/services/proxy/agentcp/samples/wrapper_agently_to_agent/create_profile.py +125 -0
- package/extensions/services/proxy/agentcp/samples/wrapper_agently_to_agent/main.py +44 -0
- package/extensions/services/proxy/agentcp/utils/__init__.py +15 -0
- package/extensions/services/proxy/agentcp/utils/file_util.py +117 -0
- package/extensions/services/proxy/agentcp/utils/proxy_bypass.py +99 -0
- package/extensions/services/proxy/agentcp/workflow.py +203 -0
- package/extensions/services/proxy/console_auth.py +109 -0
- package/extensions/services/proxy/evol/__init__.py +1 -0
- package/extensions/services/proxy/evol/config.py +37 -0
- package/extensions/services/proxy/evol/http/__init__.py +1 -0
- package/extensions/services/proxy/evol/http/async_http.py +551 -0
- package/extensions/services/proxy/evol/log.py +28 -0
- package/extensions/services/proxy/evol/presenter/__init__.py +2 -0
- package/extensions/services/proxy/evol/presenter/agentIdPresenter.py +1031 -0
- package/extensions/services/proxy/evol/presenter/apikeyPresenter.py +106 -0
- package/extensions/services/proxy/evol/presenter/configPresenter.py +1281 -0
- package/extensions/services/proxy/evol/presenter/userPresenter.py +477 -0
- package/extensions/services/proxy/evol/server/__init__.py +1 -0
- package/extensions/services/proxy/evol/server/claude_proxy_async.py +3430 -0
- package/extensions/services/proxy/evol/server/openclaw_proxy.py +1861 -0
- package/extensions/services/proxy/evol/server/proxy_config.py +15 -0
- package/extensions/services/proxy/evol/server/proxy_engine.py +501 -0
- package/extensions/services/proxy/evol/version.py +24 -0
- package/extensions/services/proxy/logs/websocket.log +260 -0
- package/extensions/services/proxy/main.py +240 -0
- package/extensions/services/proxy/requirements.txt +13 -0
- package/extensions/services/proxy/server.py +271 -0
- package/extensions/services/watchdog/entry.py +215 -26
- package/extensions/services/watchdog/module.md +1 -0
- package/extensions/services/watchdog/monitor.py +178 -38
- package/extensions/services/web/WEBSOCKET_STATUS.md +143 -0
- package/extensions/services/web/config_example.py +35 -0
- package/extensions/services/web/config_loader.py +110 -0
- package/extensions/services/web/entry.py +114 -26
- package/extensions/services/web/module.md +35 -24
- package/extensions/services/web/pairing.py +250 -0
- package/extensions/services/web/pairing_codes.jsonl +16 -0
- package/extensions/services/web/relay.py +643 -0
- package/extensions/services/web/relay_config.json5 +67 -0
- package/extensions/services/web/routes/routes_management_ws.py +127 -0
- package/extensions/services/web/routes/routes_rpc.py +89 -0
- package/extensions/services/web/routes/routes_test.py +61 -0
- package/extensions/services/web/routes/schemas.py +0 -22
- package/extensions/services/web/server.py +434 -99
- package/extensions/services/web/static/css/style.css +67 -28
- package/extensions/services/web/static/index.html +234 -44
- package/extensions/services/web/static/js/app.js +1335 -48
- package/extensions/services/web/static/js/kernel-client-example.js +161 -0
- package/extensions/services/web/static/js/kernel-client.js +383 -0
- package/extensions/services/web/static/js/registry-tests.js +558 -0
- package/extensions/services/web/static/js/token-manager.js +175 -0
- package/extensions/services/web/static/pairing.html +248 -0
- package/extensions/services/web/static/test_registry.html +262 -0
- package/extensions/services/web/web_config.json5 +29 -0
- package/kernel/entry.py +120 -32
- package/kernel/event_hub.py +141 -16
- package/kernel/module.md +60 -33
- package/kernel/registry_store.py +45 -36
- package/kernel/rpc_router.py +152 -59
- package/kernel/server.py +322 -26
- package/kite_cli/__init__.py +3 -0
- package/kite_cli/__main__.py +5 -0
- package/kite_cli/commands/__init__.py +1 -0
- package/kite_cli/commands/clean.py +101 -0
- package/kite_cli/commands/deps_install.py +67 -0
- package/kite_cli/commands/doctor.py +35 -0
- package/kite_cli/commands/env_check.py +45 -0
- package/kite_cli/commands/history.py +111 -0
- package/kite_cli/commands/info.py +96 -0
- package/kite_cli/commands/install.py +313 -0
- package/kite_cli/commands/list.py +143 -0
- package/kite_cli/commands/log.py +81 -0
- package/kite_cli/commands/prepare.py +49 -0
- package/kite_cli/commands/rollback.py +88 -0
- package/kite_cli/commands/search.py +73 -0
- package/kite_cli/commands/uninstall.py +85 -0
- package/kite_cli/commands/update.py +118 -0
- package/kite_cli/commands/venv_setup.py +56 -0
- package/kite_cli/core/__init__.py +1 -0
- package/kite_cli/core/checker.py +142 -0
- package/kite_cli/core/dependency.py +229 -0
- package/kite_cli/core/downloader.py +209 -0
- package/kite_cli/core/install_info.py +40 -0
- package/kite_cli/core/tool_installer.py +397 -0
- package/kite_cli/core/validator.py +78 -0
- package/kite_cli/main.py +317 -0
- package/kite_cli/utils/__init__.py +1 -0
- package/kite_cli/utils/i18n.py +252 -0
- package/kite_cli/utils/interactive.py +63 -0
- package/kite_cli/utils/operation_log.py +77 -0
- package/kite_cli/utils/paths.py +34 -0
- package/kite_cli/utils/version.py +308 -0
- package/launcher/entry.py +1124 -178
- package/launcher/logging_setup.py +104 -0
- package/launcher/module.md +46 -37
- package/launcher/module_scanner.py +11 -1
- package/main.py +4 -1
- package/package.json +9 -1
- package/python_version.json +4 -0
- package/requirements.txt +38 -0
- package/scripts/env-manager.js +328 -0
- package/scripts/plan_manager.py +315 -0
- package/scripts/python-env.js +79 -0
- package/scripts/scan_dependencies.py +461 -0
- package/scripts/setup-python-env.js +191 -0
- 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
|
-
|
|
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
|
-
|
|
473
|
+
_exit_code = 1
|
|
474
|
+
_shutting_down = True
|
|
475
|
+
return
|
|
367
476
|
if attempt >= max_retries:
|
|
368
477
|
print(f"[watchdog] 重连失败 {max_retries} 次,退出")
|
|
369
|
-
|
|
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,
|
|
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
|
|
415
|
-
|
|
416
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
#
|
|
524
|
-
if event_type == "
|
|
525
|
-
await
|
|
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
|
-
"
|
|
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 —
|
|
757
|
+
"""Handle module.shutdown event — ack → exiting → cleanup → ready → exit."""
|
|
577
758
|
global _shutting_down
|
|
578
759
|
print("[watchdog] Received shutdown request")
|
|
579
760
|
_shutting_down = True
|
|
580
|
-
# Step
|
|
761
|
+
# Step 1: Send ack (立即确认收到)
|
|
581
762
|
await _publish_event(_ws_global, {
|
|
582
|
-
"event": "module.
|
|
583
|
-
"data": {"module_id": "watchdog"
|
|
763
|
+
"event": "module.shutdown.ack",
|
|
764
|
+
"data": {"module_id": "watchdog"},
|
|
584
765
|
})
|
|
585
|
-
# Step
|
|
766
|
+
# Step 2: Send module.exiting (开始清理)
|
|
586
767
|
await _publish_event(_ws_global, {
|
|
587
|
-
"event": "module.
|
|
588
|
-
"data": {
|
|
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
|
|
779
|
+
# Step 3: Cleanup
|
|
591
780
|
monitor.stop()
|
|
592
|
-
# Step
|
|
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
|
|
599
|
-
sys.exit(
|
|
787
|
+
# Step 5: Exit
|
|
788
|
+
sys.exit(_exit_code)
|
|
600
789
|
|
|
601
790
|
|
|
602
791
|
if __name__ == "__main__":
|