Files
chenxin 73ad3c02b0 启动补丁:乾坤装配参数名、注册接口双前缀、/chat 残留链接清理
- 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
2026-08-28 10:31:36 +08:00

219 lines
9.0 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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 接管
)