import errno import logging import socket from contextlib import asynccontextmanager import uvicorn from fastapi import FastAPI try: import psutil except Exception: # pragma: no cover - optional runtime dependency for richer diagnostics psutil = None from config import Config from workflows.workflow_manager import WorkflowManager from services.integrations.nacos_service import load_nacos_config, load_service_config, NacosManager from services.tools.tool_router import ToolRouter from services.core.prompt_manager import get_prompt_manager from api import endpoints logger = logging.getLogger(__name__) def _find_listening_process_on_port(port: int) -> dict | None: if psutil is None: return None try: for conn in psutil.net_connections(kind="tcp"): local_port = getattr(getattr(conn, "laddr", None), "port", None) if local_port != port or conn.status != psutil.CONN_LISTEN: continue pid = conn.pid if pid is None: return {"pid": None, "process_name": None, "cmdline": None} try: proc = psutil.Process(pid) return { "pid": pid, "process_name": proc.name(), "cmdline": " ".join(proc.cmdline()), } except Exception: return {"pid": pid, "process_name": None, "cmdline": None} except Exception: return None return None def _ensure_port_available(host: str, port: int) -> None: probe = socket.socket(socket.AF_INET, socket.SOCK_STREAM) try: probe.bind((host, port)) except OSError as exc: if exc.errno == errno.EADDRINUSE or getattr(exc, "winerror", None) == 10048: process_info = _find_listening_process_on_port(port) or {} owner_bits = [] if process_info.get("pid") is not None: owner_bits.append(f"pid={process_info['pid']}") if process_info.get("process_name"): owner_bits.append(f"name={process_info['process_name']}") if process_info.get("cmdline"): owner_bits.append(f"cmdline={process_info['cmdline']}") logger.error( "端口占用,服务无法启动: host=%s port=%s config=%s%s", host, port, Config._get_config_path(), f" owner=({' | '.join(owner_bits)})" if owner_bits else "", ) logger.error("请先停止占用该端口的进程,或修改 `config/config.ini` 中 `[app].port`。") raise SystemExit(1) from exc raise finally: probe.close() def create_app() -> FastAPI: """创建 FastAPI 应用""" service_config = load_service_config() nacos_config = load_nacos_config() default_model_section = Config._config.get("app", "model_section", fallback=Config.DEFAULT_MODEL_SECTION) workflow_manager = WorkflowManager(default_model_section=default_model_section) nacos_manager = NacosManager(nacos_config=nacos_config, service_config=service_config) tool_router = ToolRouter() prompt_manager = get_prompt_manager() @asynccontextmanager async def lifespan(app: FastAPI): """应用生命周期管理""" logger.info("🚀 服务启动中...") app.state.workflow_manager = workflow_manager app.state.nacos_manager = nacos_manager app.state.service_config = service_config app.state.tool_router = tool_router app.state.prompt_manager = prompt_manager await nacos_manager.start() logger.info("✅ 服务准备就绪: %s on %s:%s", service_config.service_name, service_config.ip, service_config.port) yield logger.info("🛑 服务关闭中...") await nacos_manager.stop() logger.info("✅ 服务已关闭") app = FastAPI( title="More Dots API", description="More Dots 工作流服务", version=service_config.metadata.get("version", "1.0.0"), lifespan=lifespan, ) app.include_router(endpoints.router) return app app = create_app() if __name__ == "__main__": service_config = load_service_config() _ensure_port_available(service_config.host, service_config.port) uvicorn.run( "server:app", host=service_config.host, port=service_config.port, reload=False, log_level="info", )