- main.py:Executor(sandbox, http_client=…) 参数名与构造签名不符(http=…),致乾坤沙箱装配降级 503——修正 - 注册页手写 fetch 多拼一层 v1(/api/proxy/v1/auth/register → 后端 /api/v1/v1/* 404);BACKEND_ROOT 已含 /api/v1,proxy 调用不应再带——去掉 - /chat 页 M2b 已随线性会话退役,对话主入口为 /trees(镜树);六处残留链接(首页/登录/注册跳转/错误页/404 页/管理壳)全部改指 /trees - 回归:前端 type-check + 203 passed;后端百工+API 套件 45 passed
219 lines
9.0 KiB
Python
219 lines
9.0 KiB
Python
"""FastAPI 应用入口。
|
||
|
||
既导出 ASGI 应用实例 ``app``(供 ``uvicorn app.main:app`` 使用),又在作为脚本执行
|
||
(``python -m app.main``)时启动 uvicorn。日志系统在模块级初始化,确保无论从哪种
|
||
入口启动,uvicorn 横幅、第三方库与应用日志都统一走 ``app.core.logging`` 出口。
|
||
"""
|
||
|
||
from collections.abc import AsyncGenerator, Awaitable, Callable
|
||
from contextlib import asynccontextmanager
|
||
from time import perf_counter
|
||
|
||
import httpx
|
||
from fastapi import FastAPI, Request, Response
|
||
from fastapi.middleware.cors import CORSMiddleware
|
||
from sqlalchemy.exc import OperationalError
|
||
|
||
from app.api.router import api_router
|
||
from app.core.config import settings
|
||
from app.core.errors import register_exception_handlers, trace_id_var
|
||
from app.core.ids import new_trace_id
|
||
from app.core.logging import (
|
||
clear_log_context,
|
||
log_access,
|
||
log_with_context,
|
||
set_request_id,
|
||
setup_logging,
|
||
)
|
||
from app.db.init_data import seed_default_data
|
||
from app.db.session import AsyncSessionLocal, close_engine
|
||
from app.domains.baigong.executor import Executor
|
||
from app.integrations.sandbox.client import SandboxClient
|
||
from app.schemas.common import ErrorResponse
|
||
from app.services.dispatcher import Dispatcher
|
||
from app.services.event_store import DbEventStore
|
||
|
||
# 模块级初始化日志系统:import 本模块即完成 loguru 配置并接管标准 logging,
|
||
# 覆盖 ``python -m app.main`` 与 ``uvicorn app.main:app`` 两种启动入口。
|
||
setup_logging()
|
||
log_with_context(
|
||
"应用日志已初始化",
|
||
log_dir=str(settings.resolved_log_dir),
|
||
log_level=settings.log_level,
|
||
app_env=settings.app_env,
|
||
)
|
||
|
||
|
||
@asynccontextmanager
|
||
async def lifespan(_app: FastAPI) -> AsyncGenerator[None, None]:
|
||
try:
|
||
async with AsyncSessionLocal() as session:
|
||
await seed_default_data(session)
|
||
except OperationalError:
|
||
log_with_context("数据库尚未完成迁移,跳过默认数据初始化。", level="WARNING")
|
||
|
||
# SQLite WAL 模式:用 sqlite3 直接在文件层面设置(必须跑在事务外才能生效),
|
||
# 后续所有 aiosqlite 连接自动继承 WAL 模式;允许读写并发,
|
||
# 避免叩心枢后台循环与请求处理同时写时锁冲突
|
||
if settings.resolved_database_url.startswith("sqlite"):
|
||
try:
|
||
import sqlite3
|
||
|
||
_db_path = settings.resolved_database_url.replace("sqlite+aiosqlite:///", "")
|
||
_raw_conn = sqlite3.connect(_db_path)
|
||
_raw_conn.execute("PRAGMA journal_mode=WAL")
|
||
_raw_conn.execute("PRAGMA busy_timeout=5000")
|
||
_raw_conn.close()
|
||
log_with_context("SQLite WAL 模式已启用,busy_timeout=5000ms")
|
||
except Exception: # noqa: BLE001
|
||
log_with_context("SQLite WAL 模式设置失败,已跳过", level="WARNING")
|
||
|
||
# 运行时注入:事件存储始终就绪(DbEventStore 持久化到 events 表,重启不丢);
|
||
# 工具运行时依赖沙箱配置,未配置(缺 SANDBOX_AUTH_SECRET 等)则降级 None(工具 API 503)。
|
||
# 事件流与历史查询不依赖沙箱,始终可用。
|
||
_app.state.event_store = DbEventStore(AsyncSessionLocal)
|
||
|
||
# Kernel(rewrite/1.0 M0):极小硬核 + 事件日志 + 前台席位;神识客户端按配置装配,
|
||
# 未配置神识地址时为进程内回声桩(骨架形态,见 app.shenshi_client)。
|
||
from app.kernel import Kernel, KernelEventLog
|
||
from app.shenshi_client import build_shenshi_client
|
||
|
||
_app.state.kernel = Kernel(
|
||
shenshi=build_shenshi_client(settings.shenshi_base_url, settings.shenshi_service_token),
|
||
session_factory=AsyncSessionLocal,
|
||
event_log=KernelEventLog(_app.state.event_store),
|
||
timeout_seconds=float(settings.run_timeout_seconds),
|
||
)
|
||
|
||
# 镜生域事件写入绑定一炁(灵胚/启镜/命台/死亡事件落 events 表)
|
||
from app.domains.jingsheng import bind_event_store
|
||
|
||
bind_event_store(_app.state.event_store)
|
||
|
||
# ── MCP 进程管理器(stdio 子进程生命周期管理) ──────────────
|
||
from app.integrations.mcp.process_manager import (
|
||
ProcessManager,
|
||
set_process_manager,
|
||
)
|
||
|
||
process_manager = ProcessManager()
|
||
_app.state.mcp_process_manager = process_manager
|
||
set_process_manager(process_manager)
|
||
log_with_context("MCP 进程管理器就绪")
|
||
|
||
# 一炁进程内订阅分发器:订阅 EventStore 全量事件,按 glob pattern 分发给注册的 handler。
|
||
# 各枢核心就绪后在 start() 前 register;lifespan 管理 start/stop。
|
||
_app.state.dispatcher = Dispatcher(_app.state.event_store)
|
||
await _app.state.dispatcher.start()
|
||
|
||
# 百工执行器:乾坤沙箱客户端(HMAC 授契)+ 长生命周期 HTTP;未配置时乾坤类能力 503
|
||
sandbox_http: httpx.AsyncClient | None = None
|
||
try:
|
||
if settings.sandbox_auth_enabled and not settings.sandbox_auth_secret:
|
||
raise ValueError("SANDBOX_AUTH_ENABLED=true 但 SANDBOX_AUTH_SECRET 未配置")
|
||
sandbox_http = httpx.AsyncClient(trust_env=False)
|
||
sandbox = SandboxClient(sandbox_http, settings)
|
||
_app.state.baigong_sandbox = sandbox
|
||
_app.state.baigong_executor = Executor(sandbox, http=sandbox_http)
|
||
log_with_context("百工执行器就绪(乾坤沙箱已接入)")
|
||
except Exception as exc: # noqa: BLE001 — 配置缺失降级,不阻断启动
|
||
_app.state.baigong_sandbox = None
|
||
_app.state.baigong_executor = Executor(None)
|
||
log_with_context(f"乾坤沙箱未就绪,qiankun 类能力将返回 503:{exc}", level="WARNING")
|
||
|
||
# 启动登记 MCP 传输实例(stdio 不自动启动;派生能力已在登记/刷新时落库)
|
||
try:
|
||
from app.services.mcp import load_registered_mcp_servers
|
||
|
||
await load_registered_mcp_servers(
|
||
process_manager=_app.state.mcp_process_manager,
|
||
)
|
||
except Exception: # noqa: BLE001
|
||
log_with_context("MCP 启动加载失败,已跳过", level="WARNING")
|
||
|
||
yield
|
||
|
||
# Dispatcher 先停(取消 pump/consumer task + unsubscribe all_queue),再关 http/engine
|
||
if getattr(_app.state, "dispatcher", None) is not None:
|
||
await _app.state.dispatcher.stop()
|
||
if sandbox_http is not None:
|
||
await sandbox_http.aclose()
|
||
# MCP 进程管理器关闭所有 stdio 子进程
|
||
if getattr(_app.state, "mcp_process_manager", None) is not None:
|
||
await _app.state.mcp_process_manager.shutdown_all()
|
||
log_with_context("MCP stdio 子进程已全部关闭")
|
||
await close_engine()
|
||
|
||
|
||
def create_app() -> FastAPI:
|
||
# OpenAPI servers 仅作展示:backend_host 为通配地址(0.0.0.0 / ::)时回退 127.0.0.1,
|
||
# 避免在 Swagger 文档里暴露不可访问的地址或写死的占位域名。
|
||
server_host = (
|
||
settings.backend_host if settings.backend_host not in ("0.0.0.0", "::") else "127.0.0.1"
|
||
)
|
||
app = FastAPI(
|
||
title=settings.app_name,
|
||
debug=settings.debug,
|
||
lifespan=lifespan,
|
||
servers=[
|
||
{"url": f"http://{server_host}:{settings.backend_port}", "description": "本地开发"},
|
||
],
|
||
responses={
|
||
400: {"model": ErrorResponse, "description": "请求无效或业务前置条件不满足。"},
|
||
404: {"model": ErrorResponse, "description": "资源不存在或不属于当前人物。"},
|
||
422: {"model": ErrorResponse, "description": "请求字段校验失败。"},
|
||
502: {"model": ErrorResponse, "description": "模型或外部工具调用失败。"},
|
||
},
|
||
)
|
||
app.add_middleware(
|
||
CORSMiddleware,
|
||
allow_origins=settings.cors_origins,
|
||
allow_credentials=True,
|
||
allow_methods=["*"],
|
||
allow_headers=["*"],
|
||
)
|
||
|
||
@app.middleware("http")
|
||
async def trace_middleware(
|
||
request: Request, call_next: Callable[[Request], Awaitable[Response]]
|
||
) -> Response:
|
||
trace_id = request.headers.get("X-Trace-Id") or new_trace_id()
|
||
token = trace_id_var.set(trace_id)
|
||
set_request_id(trace_id)
|
||
started_at = perf_counter()
|
||
try:
|
||
response = await call_next(request)
|
||
response.headers["X-Trace-Id"] = trace_id
|
||
duration_ms = round((perf_counter() - started_at) * 1000, 2)
|
||
log_access(
|
||
"HTTP 请求完成",
|
||
method=request.method,
|
||
path=request.url.path,
|
||
status=response.status_code,
|
||
duration_ms=duration_ms,
|
||
)
|
||
return response
|
||
finally:
|
||
trace_id_var.reset(token)
|
||
clear_log_context()
|
||
|
||
register_exception_handlers(app)
|
||
app.include_router(api_router, prefix=settings.api_prefix)
|
||
return app
|
||
|
||
|
||
app = create_app()
|
||
|
||
|
||
if __name__ == "__main__":
|
||
import uvicorn
|
||
|
||
uvicorn.run(
|
||
"app.main:app",
|
||
host=settings.backend_host,
|
||
port=settings.backend_port,
|
||
reload=settings.is_development,
|
||
workers=1 if settings.is_development else 4,
|
||
log_config=None, # 禁用 uvicorn 默认 dictConfig,统一由 InterceptHandler 接管
|
||
)
|