chore: import upstream snapshot with attribution
Ruff Format Check / Ruff Format & Lint (push) Failing after 7m39s
Deploy VitePress site to Pages / build (push) Failing after 9m11s
Deploy VitePress site to Pages / Deploy (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 12:32:26 +08:00
commit 1443d3fdf9
732 changed files with 196602 additions and 0 deletions
+1
View File
@@ -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
+140
View File
@@ -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
+17
View File
@@ -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)
+123
View File
@@ -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}")
# 预热 Redisrun 队列)
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()