chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1 @@
|
||||
# utils包初始化文件
|
||||
@@ -0,0 +1,67 @@
|
||||
"""访问日志中间件 - 记录请求处理时间"""
|
||||
|
||||
import time
|
||||
import logging
|
||||
from collections.abc import Callable
|
||||
|
||||
from fastapi import Request, Response
|
||||
from starlette.middleware.base import BaseHTTPMiddleware
|
||||
|
||||
# 创建专用的访问日志记录器
|
||||
access_logger = logging.getLogger("access_logger")
|
||||
|
||||
# 设置访问日志记录器
|
||||
if not access_logger.handlers:
|
||||
handler = logging.StreamHandler()
|
||||
formatter = logging.Formatter(fmt="%(asctime)s %(levelname)s: %(message)s", datefmt="%m-%d %H:%M:%S")
|
||||
handler.setFormatter(formatter)
|
||||
access_logger.addHandler(handler)
|
||||
access_logger.setLevel(logging.INFO)
|
||||
# 避免传播到根日志记录器,防止重复日志
|
||||
access_logger.propagate = False
|
||||
|
||||
|
||||
def _extract_client_ip(request: Request) -> str:
|
||||
"""提取客户端IP地址"""
|
||||
forwarded_for = request.headers.get("x-forwarded-for")
|
||||
if forwarded_for:
|
||||
return forwarded_for.split(",")[0].strip()
|
||||
if request.client:
|
||||
return request.client.host
|
||||
return "unknown"
|
||||
|
||||
|
||||
class AccessLogMiddleware(BaseHTTPMiddleware):
|
||||
"""访问日志中间件 - 记录请求处理时间"""
|
||||
|
||||
def __init__(self, app, logger: logging.Logger = None):
|
||||
super().__init__(app)
|
||||
self.logger = logger or access_logger
|
||||
|
||||
async def dispatch(self, request: Request, call_next: Callable) -> Response:
|
||||
"""处理请求并记录访问日志"""
|
||||
# 记录请求开始时间
|
||||
start_time = time.perf_counter()
|
||||
|
||||
# 获取客户端IP
|
||||
client_ip = _extract_client_ip(request)
|
||||
|
||||
# 处理请求
|
||||
response = await call_next(request)
|
||||
|
||||
# 计算处理时间
|
||||
process_time = time.perf_counter() - start_time
|
||||
process_time_ms = int(process_time * 1000) # 转换为毫秒
|
||||
|
||||
# 格式化日志消息,添加处理时间
|
||||
log_message = (
|
||||
f"{client_ip}:{request.client.port if request.client else 'unknown'} - "
|
||||
f'"{request.method} {request.url.path}{"?" + request.url.query if request.url.query else ""} '
|
||||
f'HTTP/{request.scope["http_version"]}" '
|
||||
f"{response.status_code} - {process_time_ms}ms"
|
||||
)
|
||||
|
||||
# 记录日志
|
||||
self.logger.info(log_message)
|
||||
|
||||
return response
|
||||
@@ -0,0 +1,140 @@
|
||||
import hashlib
|
||||
|
||||
from fastapi import Depends, Header, HTTPException, status
|
||||
from fastapi.security import OAuth2PasswordBearer
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from yuxi.storage.postgres.manager import pg_manager
|
||||
from yuxi.storage.postgres.models_business import APIKey, User
|
||||
from yuxi.utils.datetime_utils import utc_now_naive
|
||||
|
||||
from yuxi.utils.auth_utils import AuthUtils
|
||||
|
||||
# 定义OAuth2密码承载器,指定token URL
|
||||
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/auth/token", auto_error=False)
|
||||
|
||||
|
||||
# 获取数据库会话(异步版本)
|
||||
async def get_db():
|
||||
async with pg_manager.get_async_session_context() as db:
|
||||
yield db
|
||||
|
||||
|
||||
async def _verify_api_key(key: str, db: AsyncSession) -> tuple[User | None, APIKey | None]:
|
||||
"""验证 API Key 并返回关联用户和 APIKey 对象"""
|
||||
key_hash = hashlib.sha256(key.encode()).hexdigest()
|
||||
|
||||
result = await db.execute(select(APIKey).filter(APIKey.key_hash == key_hash))
|
||||
api_key = result.scalar_one_or_none()
|
||||
|
||||
if api_key is None:
|
||||
return None, None
|
||||
|
||||
if not api_key.is_enabled:
|
||||
return None, None
|
||||
|
||||
if api_key.expires_at and utc_now_naive() > api_key.expires_at:
|
||||
return None, None
|
||||
|
||||
if not api_key.user_id:
|
||||
return None, None
|
||||
|
||||
result = await db.execute(select(User).filter(User.id == api_key.user_id))
|
||||
user = result.scalar_one_or_none()
|
||||
if user and not user.is_deleted:
|
||||
return user, api_key
|
||||
|
||||
return None, None
|
||||
|
||||
|
||||
# 获取当前用户(异步版本)
|
||||
async def get_current_user(
|
||||
authorization: str | None = Header(None),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
credentials_exception = HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="无效的凭证",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
|
||||
if authorization is None:
|
||||
return None
|
||||
|
||||
if not authorization.startswith("Bearer "):
|
||||
return None
|
||||
|
||||
token = authorization.split("Bearer ")[1]
|
||||
if not token:
|
||||
return None
|
||||
|
||||
# 根据 token 前缀判断认证方式
|
||||
if token.startswith("yxkey_"):
|
||||
# API Key 认证
|
||||
user, api_key_obj = await _verify_api_key(token, db)
|
||||
if user is not None and api_key_obj is not None:
|
||||
api_key_obj.last_used_at = utc_now_naive()
|
||||
await db.commit()
|
||||
return user
|
||||
|
||||
# JWT Token 认证
|
||||
try:
|
||||
payload = AuthUtils.verify_access_token(token)
|
||||
user_id = payload.get("sub")
|
||||
if user_id is None:
|
||||
raise credentials_exception
|
||||
except ValueError as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail=str(e),
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
|
||||
result = await db.execute(select(User).filter(User.id == int(user_id), User.is_deleted == 0))
|
||||
user = result.scalar_one_or_none()
|
||||
if user is None:
|
||||
raise credentials_exception
|
||||
if user.is_login_locked():
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_423_LOCKED,
|
||||
detail="登录被锁定,请稍后重试",
|
||||
headers={"X-Lock-Remaining": str(user.get_remaining_lock_time())},
|
||||
)
|
||||
|
||||
return user
|
||||
|
||||
|
||||
# 获取已登录用户(抛出401如果未登录)
|
||||
async def get_required_user(user: User | None = Depends(get_current_user)):
|
||||
if user is None:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="请登录后再访问",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
if not user.department_id:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="当前用户未绑定部门",
|
||||
)
|
||||
return user
|
||||
|
||||
|
||||
# 获取管理员用户
|
||||
async def get_admin_user(current_user: User = Depends(get_required_user)):
|
||||
if current_user.role not in ["admin", "superadmin"]:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="需要管理员权限",
|
||||
)
|
||||
return current_user
|
||||
|
||||
|
||||
# 获取超级管理员用户
|
||||
async def get_superadmin_user(current_user: User = Depends(get_required_user)):
|
||||
if current_user.role != "superadmin":
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="需要超级管理员权限",
|
||||
)
|
||||
return current_user
|
||||
@@ -0,0 +1,17 @@
|
||||
import logging
|
||||
|
||||
|
||||
def setup_logging() -> None:
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format="%(asctime)s %(levelname)s: %(message)s",
|
||||
datefmt="%Y-%m-%d %H:%M:%S",
|
||||
force=True,
|
||||
)
|
||||
|
||||
uvicorn_logger = logging.getLogger("uvicorn")
|
||||
logging.getLogger("uvicorn.access").handlers.clear()
|
||||
|
||||
formatter = logging.Formatter(fmt="%(asctime)s %(levelname)s: %(message)s", datefmt="%m-%d %H:%M:%S")
|
||||
for handler in uvicorn_logger.handlers:
|
||||
handler.setFormatter(formatter)
|
||||
@@ -0,0 +1,123 @@
|
||||
import os
|
||||
from contextlib import asynccontextmanager
|
||||
|
||||
from fastapi import FastAPI
|
||||
from langgraph.checkpoint.postgres.aio import AsyncPostgresSaver
|
||||
|
||||
from yuxi.services.task_service import tasker
|
||||
from yuxi.agents.mcp.service import ensure_builtin_mcp_servers_in_db
|
||||
from yuxi.models.providers.service import ensure_builtin_model_providers_in_db
|
||||
from yuxi.services.run_queue_service import close_queue_clients, get_redis_client
|
||||
from yuxi.storage.postgres.manager import pg_manager
|
||||
from yuxi.knowledge import knowledge_base
|
||||
from yuxi.utils import logger
|
||||
from yuxi.agents.backends.sandbox import init_sandbox_provider, shutdown_sandbox_provider
|
||||
from yuxi import get_version
|
||||
from yuxi.config import config
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(app: FastAPI):
|
||||
"""FastAPI lifespan事件管理器"""
|
||||
# 初始化数据库连接
|
||||
try:
|
||||
pg_manager.initialize()
|
||||
await pg_manager.create_tables()
|
||||
await pg_manager.ensure_business_schema()
|
||||
await pg_manager.ensure_knowledge_schema()
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to initialize database during startup: {e}")
|
||||
|
||||
# 确保内置 MCP 服务器定义存在于数据库
|
||||
try:
|
||||
await ensure_builtin_mcp_servers_in_db()
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to ensure builtin MCP servers during startup: {e}")
|
||||
|
||||
try:
|
||||
from yuxi.agents.skills.service import init_builtin_skills
|
||||
|
||||
async with pg_manager.get_async_session_context() as session:
|
||||
await init_builtin_skills(session)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to initialize builtin skills during startup: {e}")
|
||||
|
||||
try:
|
||||
from yuxi.repositories.agent_repository import AgentRepository
|
||||
|
||||
async with pg_manager.get_async_session_context() as session:
|
||||
repository = AgentRepository(session)
|
||||
await repository.ensure_default_agent()
|
||||
await repository.ensure_general_purpose_subagent()
|
||||
await repository.ensure_web_search_subagent()
|
||||
await repository.ensure_deep_research_agents()
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to ensure default agent during startup: {e}")
|
||||
|
||||
# 初始化内置模型供应商配置
|
||||
try:
|
||||
async with pg_manager.get_async_session_context() as session:
|
||||
await ensure_builtin_model_providers_in_db(session)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to ensure builtin model providers during startup: {e}")
|
||||
|
||||
# 初始化模型缓存(v2 模型选择使用)
|
||||
try:
|
||||
from yuxi.models.providers.cache import model_cache
|
||||
from yuxi.models.providers.service import get_all_model_providers
|
||||
|
||||
async with pg_manager.get_async_session_context() as session:
|
||||
providers = await get_all_model_providers(session)
|
||||
model_cache.rebuild(providers)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to initialize model cache during startup: {e}")
|
||||
|
||||
# 初始化知识库管理器
|
||||
if os.environ.get("LITE_MODE", "").lower() in ("true", "1"):
|
||||
logger.info("LITE_MODE enabled, skipping knowledge base initialization")
|
||||
else:
|
||||
try:
|
||||
await knowledge_base.initialize()
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to initialize knowledge base manager: {e}")
|
||||
|
||||
# 预热 Redis(run 队列)
|
||||
try:
|
||||
redis = await get_redis_client()
|
||||
await redis.ping()
|
||||
except Exception as e:
|
||||
logger.warning(f"Run queue redis unavailable on startup: {e}")
|
||||
|
||||
# 启动运行时配置同步线程(周期性从 Redis 拉取管理员保存的配置快照)
|
||||
config.start_runtime_sync()
|
||||
|
||||
try:
|
||||
init_sandbox_provider()
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to initialize sandbox provider during startup: {e}")
|
||||
|
||||
# =========================================================
|
||||
# 2. 核心修复:在这里执行一次 setup(),建完表就拉倒
|
||||
# =========================================================
|
||||
checkpointer = AsyncPostgresSaver(pg_manager.langgraph_pool)
|
||||
await checkpointer.setup()
|
||||
print("LangGraph Checkpoint tables verified/created!")
|
||||
|
||||
await tasker.start()
|
||||
logger.info(f"""
|
||||
|
||||
░██ ░██ ░██
|
||||
░██ ░██
|
||||
░██ ░██ ░██ ░██ ░██ ░██ ░██
|
||||
░████ ░██ ░██ ░██ ░██ ░██
|
||||
░██ ░██ ░██ ░█████ ░██
|
||||
░██ ░██ ░███ ░██ ░██ ░██
|
||||
░██ ░█████░██ ░██ ░██ ░██ v{get_version()}
|
||||
|
||||
""")
|
||||
logger.info("Yuxi backend startup complete")
|
||||
yield
|
||||
await tasker.shutdown()
|
||||
shutdown_sandbox_provider()
|
||||
await close_queue_clients()
|
||||
await pg_manager.close()
|
||||
Reference in New Issue
Block a user