From cfc6601c76847a75cbf1e63fa5867b6cc751a37e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9C=B1=E6=BD=AE?= Date: Wed, 20 May 2026 16:40:16 +0800 Subject: [PATCH 1/3] update dep --- fastapi_app.py | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/fastapi_app.py b/fastapi_app.py index 3d88d15..6af4548 100644 --- a/fastapi_app.py +++ b/fastapi_app.py @@ -80,13 +80,8 @@ from utils.log_util.logger import init_with_fastapi # Initialize logger logger = logging.getLogger('app') -# Import route modules -<<<<<<< Updated upstream from routes import chat, files, projects, system, skill_manager, database, memory -======= -from routes import chat, files, projects, system, skill_manager, database, memory, bot_manager, knowledge_base, payment, voice from routes.mcp_resources import router as mcp_resources_router ->>>>>>> Stashed changes from routes.webdav import wsgidav_app @@ -215,23 +210,14 @@ app.include_router(skill_manager.router) app.include_router(database.router) app.include_router(memory.router) -<<<<<<< Updated upstream -======= -# 注册语音对话路由 -app.include_router(voice.router) - # 注册文件管理API路由 app.include_router(file_manager_router) -# 注册知识库API路由 -app.include_router(knowledge_base.router, prefix="/api/v1/knowledge-base", tags=["knowledge-base"]) - # MCP App resources endpoint app.include_router(mcp_resources_router) # 挂载 WsgiDAV(WSGI 应用通过 WSGIMiddleware 集成到 ASGI) ->>>>>>> Stashed changes # Register the file management API routes app.include_router(file_manager_router) From 209a8c344dfddeae9a4fce091dda87d1ef767884 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9C=B1=E6=BD=AE?= Date: Wed, 20 May 2026 16:41:20 +0800 Subject: [PATCH 2/3] update dep --- fastapi_app.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/fastapi_app.py b/fastapi_app.py index 6af4548..5e5ca35 100644 --- a/fastapi_app.py +++ b/fastapi_app.py @@ -210,9 +210,6 @@ app.include_router(skill_manager.router) app.include_router(database.router) app.include_router(memory.router) -# 注册文件管理API路由 -app.include_router(file_manager_router) - # MCP App resources endpoint app.include_router(mcp_resources_router) From b4f8bb2935fd0d33d618dcea413ca9d6eb1c7187 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9C=B1=E6=BD=AE?= Date: Wed, 20 May 2026 18:58:58 +0800 Subject: [PATCH 3/3] refactor: replace mcp_resources API with direct /robots/ static file access - Remove routes/mcp_resources.py (ui:// URI resolver endpoint) - Frontend now directly accesses /robots/{bot_id}/skills/{server}/apps/{resource}.html - Add Daytona fallback middleware to fetch files from sandbox on 404 - Add utils/daytona_file_fetcher.py for on-demand single file download Co-Authored-By: Claude Opus 4.6 --- fastapi_app.py | 33 +++++++++++++-- routes/mcp_resources.py | 77 ----------------------------------- utils/daytona_file_fetcher.py | 51 +++++++++++++++++++++++ 3 files changed, 81 insertions(+), 80 deletions(-) delete mode 100644 routes/mcp_resources.py create mode 100644 utils/daytona_file_fetcher.py diff --git a/fastapi_app.py b/fastapi_app.py index 5e5ca35..998a9ef 100644 --- a/fastapi_app.py +++ b/fastapi_app.py @@ -81,7 +81,6 @@ from utils.log_util.logger import init_with_fastapi logger = logging.getLogger('app') from routes import chat, files, projects, system, skill_manager, database, memory -from routes.mcp_resources import router as mcp_resources_router from routes.webdav import wsgidav_app @@ -189,6 +188,36 @@ app.mount("/public", StaticFiles(directory="public"), name="static") # Mount robot projects directory as static files (supports HTML/CSS/JS/images) app.mount("/robots", StaticFiles(directory="projects/robot", html=True), name="robots") + +# Daytona fallback middleware: fetch file from sandbox when local file is missing +from utils.settings import DAYTONA_ENABLED + +if DAYTONA_ENABLED: + import asyncio + from pathlib import Path + from starlette.responses import FileResponse + + PROJECT_ROOT = Path(__file__).parent + + @app.middleware("http") + async def daytona_robots_fallback(request, call_next): + response = await call_next(request) + if response.status_code == 404 and request.url.path.startswith("/robots/"): + path_after = request.url.path.removeprefix("/robots/") + parts = path_after.split("/", 1) + if len(parts) == 2: + bot_id, rel_path = parts + local_path = PROJECT_ROOT / "projects" / "robot" / bot_id / rel_path + if not local_path.is_file(): + from utils.daytona_file_fetcher import fetch_file_from_daytona + fetched = await asyncio.to_thread( + fetch_file_from_daytona, bot_id, rel_path, local_path + ) + if fetched and local_path.is_file(): + return FileResponse(str(local_path)) + return response + + # Add CORS middleware for frontend pages app.add_middleware( CORSMiddleware, @@ -210,8 +239,6 @@ app.include_router(skill_manager.router) app.include_router(database.router) app.include_router(memory.router) -# MCP App resources endpoint -app.include_router(mcp_resources_router) # 挂载 WsgiDAV(WSGI 应用通过 WSGIMiddleware 集成到 ASGI) diff --git a/routes/mcp_resources.py b/routes/mcp_resources.py deleted file mode 100644 index 1377ddc..0000000 --- a/routes/mcp_resources.py +++ /dev/null @@ -1,77 +0,0 @@ -""" -REST endpoint for serving MCP App resources. - -Maps ui:// URIs to static HTML App files under the bot's skill directory. - -GET /api/v1/mcp/resources?uri=ui://data-dashboard/chart&bot_id=xxx - -> projects/robot/{bot_id}/skills/data-dashboard/apps/chart.html -""" - -import logging -from pathlib import Path - -from fastapi import APIRouter, Query, HTTPException -from fastapi.responses import HTMLResponse - -logger = logging.getLogger(__name__) - -router = APIRouter(prefix="/api/v1/mcp", tags=["mcp-resources"]) - -PROJECT_ROOT = Path(__file__).parent.parent - -# In-memory cache: (bot_id, uri) -> html string -_resource_cache: dict[tuple[str, str], str] = {} - - -def _resolve_uri_to_path(uri: str, bot_id: str) -> Path: - """Resolve a ui:// URI to a local file path. - - URI format: ui://{server-name}/{resource-name} - File path: projects/robot/{bot_id}/skills/{server-name}/apps/{resource-name}.html - """ - if not uri.startswith("ui://"): - raise ValueError(f"Invalid URI scheme: {uri}") - - rest = uri[5:] # Remove "ui://" - parts = rest.split("/", 1) - if len(parts) != 2 or not parts[0] or not parts[1]: - raise ValueError(f"Invalid URI format: {uri}") - - server_name, resource_name = parts - - # Prevent path traversal - if ".." in server_name or "/" in server_name: - raise ValueError(f"Invalid server name: {server_name}") - safe_name = Path(resource_name).name - if safe_name != resource_name: - raise ValueError(f"Invalid resource name: {resource_name}") - - return PROJECT_ROOT / "projects" / "robot" / bot_id / "skills" / server_name / "apps" / f"{resource_name}.html" - - -@router.get("/resources") -async def get_resource( - uri: str = Query(..., description="Resource URI (e.g. ui://data-dashboard/chart)"), - bot_id: str = Query(..., description="Bot ID"), -): - """Fetch an MCP App HTML resource by URI and bot_id. - - Returns the static HTML App that should be loaded into an iframe. - The host sends tool result data to the iframe via postMessage. - """ - cache_key = (bot_id, uri) - if cache_key in _resource_cache: - return HTMLResponse(content=_resource_cache[cache_key], media_type="text/html") - - try: - file_path = _resolve_uri_to_path(uri, bot_id) - except ValueError as e: - raise HTTPException(status_code=400, detail=str(e)) - - if not file_path.is_file(): - raise HTTPException(status_code=404, detail=f"Resource not found: {uri} (bot_id={bot_id})") - - html = file_path.read_text(encoding="utf-8") - _resource_cache[cache_key] = html - - return HTMLResponse(content=html, media_type="text/html") diff --git a/utils/daytona_file_fetcher.py b/utils/daytona_file_fetcher.py new file mode 100644 index 0000000..8697541 --- /dev/null +++ b/utils/daytona_file_fetcher.py @@ -0,0 +1,51 @@ +"""On-demand single file fetcher from Daytona sandbox.""" + +import logging +from pathlib import Path + +from utils.settings import DAYTONA_ENABLED, DAYTONA_API_KEY, DAYTONA_SERVER_URL + +logger = logging.getLogger('app') + +REMOTE_WORKSPACE_ROOT = "/workspace" + + +def fetch_file_from_daytona(bot_id: str, relative_path: str, local_path: Path) -> bool: + """Fetch a single file from the Daytona sandbox and save it locally. + + Args: + bot_id: The bot ID (sandbox is named "bot-{bot_id}") + relative_path: Path relative to the bot's workspace root + local_path: Local filesystem path where the file should be saved + + Returns: + True if the file was successfully fetched and saved, False otherwise + """ + if not (DAYTONA_ENABLED and DAYTONA_API_KEY and DAYTONA_SERVER_URL): + return False + + try: + from daytona import Daytona, DaytonaConfig + + config = DaytonaConfig(api_key=DAYTONA_API_KEY, api_url=DAYTONA_SERVER_URL) + client = Daytona(config) + + sandbox_name = f"bot-{bot_id}" + sandbox = client.get(sandbox_name) + + if sandbox.state not in ("Started", "Creating"): + logger.warning(f"Sandbox {sandbox_name} not running (state={sandbox.state})") + return False + + remote_path = f"{REMOTE_WORKSPACE_ROOT}/{relative_path}" + content = sandbox.fs.download_file(remote_path) + + local_path.parent.mkdir(parents=True, exist_ok=True) + local_path.write_bytes(content) + + logger.info(f"Fetched file from Daytona: {remote_path} -> {local_path}") + return True + + except Exception as e: + logger.warning(f"Failed to fetch file from Daytona for bot {bot_id}: {e}") + return False