# Copyright 2026 zhaoxi826 # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import os from typing import Dict from fastapi import FastAPI, WebSocket, Request from fastapi.responses import FileResponse, JSONResponse from fastapi.staticfiles import StaticFiles from ray import serve from .agent import agent_router from .auth import auth_router from .cluster import cluster_router from .platform.frontend import client_router from .provider import provider_router from .resource import resource_router from .workflow import workflow_router from pretor.utils.error import ( DemandError, ModelNotExistError, UserError, UserNotExistError, UserPasswordError, ProviderError, ProviderNotExistError, WorkflowError, WorkflowExit ) app = FastAPI() app.include_router(client_router) # 客户端路径 app.include_router(auth_router) # 用户路径 app.include_router(provider_router) # 供应商路径 app.include_router(resource_router) # 资源路径 app.include_router(cluster_router) # 集群信息路径 app.include_router(agent_router) # agent路径 app.include_router(workflow_router) # workflow路径 @app.exception_handler(UserNotExistError) async def user_not_exist_handler(request: Request, exc: UserNotExistError): return JSONResponse(status_code=404, content={"message": "用户不存在"}) @app.exception_handler(UserPasswordError) async def user_password_handler(request: Request, exc: UserPasswordError): return JSONResponse(status_code=401, content={"message": "密码错误"}) @app.exception_handler(UserError) async def user_error_handler(request: Request, exc: UserError): return JSONResponse(status_code=400, content={"message": "用户相关错误"}) @app.exception_handler(ProviderNotExistError) async def provider_not_exist_handler(request: Request, exc: ProviderNotExistError): return JSONResponse(status_code=404, content={"message": "服务提供商不存在"}) @app.exception_handler(ProviderError) async def provider_error_handler(request: Request, exc: ProviderError): return JSONResponse(status_code=400, content={"message": "服务提供商错误"}) @app.exception_handler(ModelNotExistError) async def model_not_exist_handler(request: Request, exc: ModelNotExistError): return JSONResponse(status_code=404, content={"message": "模型不存在"}) @app.exception_handler(DemandError) async def demand_error_handler(request: Request, exc: DemandError): return JSONResponse(status_code=400, content={"message": "需求格式错误或不满足"}) @app.exception_handler(WorkflowExit) async def workflow_exit_handler(request: Request, exc: WorkflowExit): return JSONResponse(status_code=400, content={"message": "工作流已退出"}) @app.exception_handler(WorkflowError) async def workflow_error_handler(request: Request, exc: WorkflowError): return JSONResponse(status_code=500, content={"message": "工作流执行错误"}) base_dir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))) frontend_dir = os.path.join(base_dir, "frontend", "dist") if os.path.exists(frontend_dir): app.mount("/assets", StaticFiles(directory=os.path.join(frontend_dir, "assets")), name="assets") @app.get("/favicon.svg", include_in_schema=False) async def serve_favicon(): return FileResponse(os.path.join(frontend_dir, "favicon.svg")) @app.get("/icons.svg", include_in_schema=False) async def serve_icons(): return FileResponse(os.path.join(frontend_dir, "icons.svg")) @app.get("/{full_path:path}", include_in_schema=False) async def serve_frontend(full_path: str): # 【重要安全修复】避免拦截不存在的 API 路由。如果是调用了不存在的 /api/ 接口,直接返回 404,不返回前端页面 if full_path.startswith("api/"): return JSONResponse(status_code=404, content={"detail": "API endpoint not found"}) index_path = os.path.join(frontend_dir, "index.html") if os.path.exists(index_path): return FileResponse(index_path) return JSONResponse(status_code=404, content={"detail": "Frontend build not found"}) else: import logging logging.getLogger("pretor").warning(f"Frontend dist folder not found at {frontend_dir}. Skipping frontend mount.") @serve.deployment @serve.ingress(app) class PretorGateway: gateway: Dict[str, WebSocket] def __init__(self): self.gateway = {}