diff --git a/backend/package/yuxi/agents/context.py b/backend/package/yuxi/agents/context.py index d263ebc9..fec0f6e6 100644 --- a/backend/package/yuxi/agents/context.py +++ b/backend/package/yuxi/agents/context.py @@ -320,12 +320,26 @@ async def resolve_agent_resource_options( if "tools" in fields_to_load: from yuxi.agents.toolkits.service import get_tool_metadata + from yuxi.external_systems.infrastructure.container import ( + create_use_cases_from_db, + ) + from yuxi.external_systems.use_cases.dto.tool import ( + ListToolOptionsInput, + ) options["tools"] = [ _resource_option(tool["slug"], tool.get("name"), tool.get("description")) for tool in get_tool_metadata(category="buildin") if tool.get("slug") ] + try: + use_cases = create_use_cases_from_db(db) + output = await use_cases.tool_service.list_tool_options( + ListToolOptionsInput(), + ) + options["tools"].extend(output.items) + except Exception as e: + logger.warning(f"Failed to load external tool options: {e}") if "knowledges" in fields_to_load: from yuxi.knowledge import knowledge_base diff --git a/backend/package/yuxi/agents/toolkits/service.py b/backend/package/yuxi/agents/toolkits/service.py index 73b606a1..39af703c 100644 --- a/backend/package/yuxi/agents/toolkits/service.py +++ b/backend/package/yuxi/agents/toolkits/service.py @@ -96,6 +96,13 @@ def get_tool_instances_by_category(category: str) -> list[Any]: async def resolve_configured_runtime_tools(context) -> list[Any]: from yuxi.agents.mcp.service import get_enabled_mcp_tools + from yuxi.external_systems.infrastructure.container import ( + create_use_cases_from_db, + ) + from yuxi.external_systems.use_cases.dto.tool import ( + BuildRuntimeToolsInput, + ) + from yuxi.storage.postgres.manager import pg_manager selected_tools = [] selected_tool_names: set[str] = set() @@ -130,4 +137,24 @@ async def resolve_configured_runtime_tools(context) -> list[Any]: selected_tools.append(tool) selected_tool_names.add(tool.name) + # 追加加载外部系统工具(按 slug 匹配 context.tools 中的选择) + external_tool_names = [ + name + for name in getattr(context, "tools", None) or [] + if isinstance(name, str) and name not in selected_tool_names + ] + if external_tool_names: + async with pg_manager.get_async_session_context() as db: + use_cases = create_use_cases_from_db(db) + output = await use_cases.tool_service.build_runtime_tools( + BuildRuntimeToolsInput(slugs=external_tool_names), + ) + selected_tools.extend(output.items) + selected_tool_names.update(tool.name for tool in output.items) + # 构建失败的工具 slug 必须显式记录,避免用户配置的工具被静默丢失 + if output.failed_slugs: + logger.warning( + f"Failed to build external runtime tools, skipped: {output.failed_slugs}" + ) + return selected_tools diff --git a/backend/server/main.py b/backend/server/main.py index d3b9e7dd..5502e089 100644 --- a/backend/server/main.py +++ b/backend/server/main.py @@ -20,6 +20,7 @@ from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import JSONResponse from starlette.middleware.base import BaseHTTPMiddleware +from yuxi.external_systems.exceptions import ExternalSystemError from server.routers import router from server.utils.lifespan import lifespan from server.utils.auth_middleware import is_public_path @@ -41,6 +42,27 @@ app = FastAPI(lifespan=lifespan) # 所有业务接口统一挂载到 /api,具体分组在 server.routers 中集中注册。 app.include_router(router, prefix="/api") + +@app.exception_handler(ExternalSystemError) +async def external_system_error_handler(_request: Request, exc: ExternalSystemError) -> JSONResponse: + """外部系统限界上下文异常统一映射为 HTTP 响应。 + + - ``status_code`` 来自异常类。 + - ``details`` 透传到响应体,供前端结构化处理。 + - 429/503 且 ``details`` 含 ``retry_after`` 时,自动设置 ``Retry-After`` 响应头。 + """ + headers: dict[str, str] = {} + if exc.status_code in (429, 503): + retry_after = exc.details.get("retry_after") + if retry_after is not None: + headers["Retry-After"] = str(retry_after) + return JSONResponse( + status_code=exc.status_code, + content={"detail": exc.message, "details": exc.details}, + headers=headers, + ) + + # CORS 设置 app.add_middleware( CORSMiddleware, diff --git a/backend/server/utils/lifespan.py b/backend/server/utils/lifespan.py index 2f4cef64..17d2044d 100644 --- a/backend/server/utils/lifespan.py +++ b/backend/server/utils/lifespan.py @@ -9,6 +9,7 @@ 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.external import register_adapters from yuxi.knowledge import knowledge_base from yuxi.utils import logger from yuxi.agents.backends.sandbox import init_sandbox_provider, shutdown_sandbox_provider @@ -17,6 +18,14 @@ from yuxi import get_version @asynccontextmanager async def lifespan(app: FastAPI): + + # 注册外部系统适配器(避免模块导入副作用) + try: + register_adapters() + except Exception as e: + logger.error(f"Failed to register external system adapters during startup: {e}") + + """FastAPI lifespan事件管理器""" # 初始化数据库连接 try: @@ -24,6 +33,7 @@ async def lifespan(app: FastAPI): await pg_manager.create_tables() await pg_manager.ensure_business_schema() await pg_manager.ensure_knowledge_schema() + await pg_manager.ensure_external_schema() except Exception as e: logger.error(f"Failed to initialize database during startup: {e}")