"""v2.6 主入口 - FastAPI + Daemon ticker 共享 asyncio event loop""" from __future__ import annotations import asyncio import logging from contextlib import asynccontextmanager from pathlib import Path from typing import Optional import yaml from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware from fastapi.staticfiles import StaticFiles logger = logging.getLogger("moziplus-v2") # --------------------------------------------------------------------------- # 配置加载 # --------------------------------------------------------------------------- DEFAULT_CONFIG_PATH = Path(__file__).parent.parent / "config" / "default.yaml" def load_config() -> dict: """加载全局默认配置""" if DEFAULT_CONFIG_PATH.exists(): with open(DEFAULT_CONFIG_PATH) as f: return yaml.safe_load(f) or {} return {} config = load_config() # --------------------------------------------------------------------------- # Daemon ticker(占位,F6 实现) # --------------------------------------------------------------------------- _ticker_task: Optional[asyncio.Task] = None async def _run_ticker(): """Daemon ticker 主循环(占位)""" tick_interval = config.get("daemon", {}).get("tick_interval", 30) logger.info("Ticker started (interval=%ss)", tick_interval) while True: try: # F6 会在这里注入真正的 tick 逻辑 await asyncio.sleep(tick_interval) except asyncio.CancelledError: logger.info("Ticker cancelled") return except Exception: logger.exception("Tick error") await asyncio.sleep(tick_interval) # --------------------------------------------------------------------------- # FastAPI 生命周期 # --------------------------------------------------------------------------- @asynccontextmanager async def lifespan(app: FastAPI): """启动 Daemon ticker,关闭时清理""" global _ticker_task logger.info("moziplus-v2 starting...") _ticker_task = asyncio.create_task(_run_ticker()) yield if _ticker_task: _ticker_task.cancel() try: await _ticker_task except asyncio.CancelledError: pass logger.info("moziplus-v2 stopped") # --------------------------------------------------------------------------- # FastAPI app # --------------------------------------------------------------------------- app = FastAPI( title="Sanguo MoziPlus v2", description="AI Native DevOps Platform - Blackboard Architecture", version="2.6.0", lifespan=lifespan, ) app.add_middleware( CORSMiddleware, allow_origins=["*"], allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) # --------------------------------------------------------------------------- # API 路由注册 # --------------------------------------------------------------------------- from src.api.blackboard_routes import router as blackboard_router from src.api.daemon_routes import router as daemon_router from src.api.project_routes import router as project_router from src.api.sse_routes import router as sse_router app.include_router(blackboard_router) app.include_router(daemon_router) app.include_router(project_router) app.include_router(sse_router) # --------------------------------------------------------------------------- # 兼容端点 # --------------------------------------------------------------------------- @app.get("/api/projects") async def list_projects_compat(): """兼容旧端点""" from src.api.project_routes import _registry reg = _registry() return {"projects": {pid: info for pid, info in reg.list_projects().items() if info.get("status") != "archived"}} # --------------------------------------------------------------------------- # 静态文件服务(前端 dist/,F18 实现) # --------------------------------------------------------------------------- DIST_DIR = Path(__file__).parent / "frontend" / "dist" if DIST_DIR.exists(): app.mount("/", StaticFiles(directory=str(DIST_DIR), html=True), name="frontend")