refactor: introduce transport-neutral connection runtime

This commit is contained in:
caixypromise
2026-07-27 02:05:26 +08:00
parent f5ed1aaec8
commit 0c582ed3b6
56 changed files with 9931 additions and 237 deletions
+124 -37
View File
@@ -3,13 +3,14 @@ import uuid
import signal
import asyncio
from aioconsole import ainput
from config.settings import load_config
from config.config_loader import load_config
from config.logger import setup_logging
from core.utils.util import get_local_ip, validate_mcp_endpoint
from core.http_server import SimpleHttpServer
from core.websocket_server import WebSocketServer
from core.xiaozhi_server_facade import XiaozhiServerFacade
from core.utils.util import check_ffmpeg_installed
from core.utils.gc_manager import get_gc_manager
from config.manage_api_client import manage_api_http_close
TAG = __name__
logger = setup_logging()
@@ -51,14 +52,14 @@ async def main():
# auth_key用于jwt认证,比如视觉分析接口的jwt认证、ota接口的token生成与websocket认证
# 获取配置文件中的auth_key
auth_key = config["server"].get("auth_key", "")
# 验证auth_key,无效则尝试使用manager-api.secret
if not auth_key or len(auth_key) == 0 or "" in auth_key:
auth_key = config.get("manager-api", {}).get("secret", "")
# 验证secret,无效则生成随机密钥
if not auth_key or len(auth_key) == 0 or "" in auth_key:
auth_key = str(uuid.uuid4().hex)
config["server"]["auth_key"] = auth_key
# 添加 stdin 监控任务
@@ -68,12 +69,47 @@ async def main():
gc_manager = get_gc_manager(interval_seconds=300)
await gc_manager.start()
# 启动 WebSocket 服务器
ws_server = WebSocketServer(config)
ws_task = asyncio.create_task(ws_server.start())
# 启动 Simple http 服务器
ota_server = SimpleHttpServer(config)
ota_task = asyncio.create_task(ota_server.start())
# 启动小智服务器门面
xiaozhi_server = XiaozhiServerFacade(config)
ota_server = None
ota_task = None
try:
# Facade.start() returns after every listener is ready. Await it so bind
# or configuration failures stop startup instead of leaving a partial process.
await xiaozhi_server.start()
ota_server = SimpleHttpServer(config)
ota_task = asyncio.create_task(
ota_server.start(), name="xiaozhi-http-server"
)
await ota_server.wait_started(
ota_task,
timeout=float(config.get("server_startup_timeout", 10)),
)
except Exception:
if ota_server:
try:
await ota_server.stop()
except Exception as cleanup_error:
logger.bind(tag=TAG).error(f"停止HTTP服务器失败: {cleanup_error}")
if ota_task:
if not ota_task.done():
ota_task.cancel()
await asyncio.gather(ota_task, return_exceptions=True)
try:
await xiaozhi_server.stop()
except Exception as cleanup_error:
logger.bind(tag=TAG).error(f"停止协议服务器失败: {cleanup_error}")
try:
await gc_manager.stop()
except Exception as cleanup_error:
logger.bind(tag=TAG).error(f"停止GC管理器失败: {cleanup_error}")
try:
await manage_api_http_close()
except Exception as cleanup_error:
logger.bind(tag=TAG).error(f"关闭管理API客户端失败: {cleanup_error}")
stdin_task.cancel()
await asyncio.gather(stdin_task, return_exceptions=True)
raise
read_config_from_api = config.get("read_config_from_api", False)
port = int(config["server"].get("http_port", 8003))
@@ -100,24 +136,31 @@ async def main():
logger.bind(tag=TAG).error("mcp接入点不符合规范")
config["mcp_endpoint"] = "你的接入点 websocket地址"
# 获取WebSocket配置,使用安全的默认值
websocket_port = 8000
server_config = config.get("server", {})
if isinstance(server_config, dict):
websocket_port = int(server_config.get("port", 8000))
# 显示协议连接信息
connection_info = xiaozhi_server.get_connection_info()
logger.bind(tag=TAG).info(
"Websocket地址是\tws://{}:{}/xiaozhi/v1/",
get_local_ip(),
websocket_port,
)
# WebSocket信息
websocket_info = connection_info.get("websocket", {})
if websocket_info.get("enabled", False):
websocket_port = websocket_info.get("port", 8000)
logger.bind(tag=TAG).info(
"WebSocket地址是\tws://{}:{}/xiaozhi/v1/",
get_local_ip(),
websocket_port,
)
# 显示启用的协议
enabled_protocols = xiaozhi_server.config.get("enabled_protocols", [])
logger.bind(tag=TAG).info(f"启用的协议: {', '.join(enabled_protocols)}")
if "websocket" in enabled_protocols:
logger.bind(tag=TAG).info(
"=======上面的WebSocket地址请勿用浏览器访问======="
)
logger.bind(tag=TAG).info(
"如想测试WebSocket请启动digital-human模块,打开浏览器交互测试"
)
logger.bind(tag=TAG).info(
"=======上面的地址是websocket协议地址,请勿用浏览器访问======="
)
logger.bind(tag=TAG).info(
"如想测试websocket请启动digital-human模块,打开浏览器交互测试"
)
logger.bind(tag=TAG).info(
"=============================================================\n"
)
@@ -127,22 +170,66 @@ async def main():
except asyncio.CancelledError:
print("任务被取消,清理资源中...")
finally:
shutdown_errors = []
if ota_server:
try:
await ota_server.stop()
except Exception as e:
shutdown_errors.append(("HTTP服务器", e))
logger.bind(tag=TAG).error(f"停止HTTP服务器失败: {e}")
# 停止小智服务器
try:
await xiaozhi_server.stop()
except Exception as e:
shutdown_errors.append(("协议服务器", e))
logger.bind(tag=TAG).error(f"停止小智服务器失败: {e}")
# 停止全局GC管理器
await gc_manager.stop()
try:
await gc_manager.stop()
except Exception as e:
shutdown_errors.append(("GC管理器", e))
logger.bind(tag=TAG).error(f"停止GC管理器失败: {e}")
# 取消所有任务(关键修复点)
try:
await manage_api_http_close()
except Exception as e:
shutdown_errors.append(("管理API客户端", e))
logger.bind(tag=TAG).error(f"关闭管理API客户端失败: {e}")
# stdin 可立即取消;HTTP task 先获得窗口完成 runner.cleanup()。
stdin_task.cancel()
ws_task.cancel()
if ota_task:
ota_task.cancel()
await asyncio.gather(stdin_task, return_exceptions=True)
# 等待任务终止(必须加超时)
await asyncio.wait(
[stdin_task, ws_task, ota_task] if ota_task else [stdin_task, ws_task],
timeout=3.0,
return_when=asyncio.ALL_COMPLETED,
)
if ota_task:
done, pending = await asyncio.wait({ota_task}, timeout=3.0)
if pending:
ota_task.cancel()
await asyncio.gather(ota_task, return_exceptions=True)
elif done and not ota_task.cancelled():
try:
ota_task.result()
except Exception as e:
shutdown_errors.append((ota_task.get_name(), e))
logger.bind(tag=TAG).error(
f"后台任务退出失败: {ota_task.get_name()}: {e}"
)
# If task cancellation or its first cleanup attempt retained the
# runner, retry after the task has fully relinquished ownership.
if ota_server:
try:
await ota_server.stop()
except Exception as e:
shutdown_errors.append(("HTTP服务器重试清理", e))
logger.bind(tag=TAG).error(f"重试停止HTTP服务器失败: {e}")
print("服务器已关闭,程序退出。")
if shutdown_errors:
details = ", ".join(
f"{owner}: {error}" for owner, error in shutdown_errors
)
raise RuntimeError(f"服务器清理失败: {details}")
if __name__ == "__main__":