# ⚙️ 后端逻辑/核心服务端.py (Hugging Face Spaces app.py) from runtime_log_privacy import install_uvicorn_privacy_filters, route_label, RequestTraceMiddleware install_uvicorn_privacy_filters() from fastapi import FastAPI, File, UploadFile, Form, Depends, HTTPException, Request from rate_limit import shared_limit from response_privacy import PrivateResponseCacheControl from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import Response, JSONResponse, FileResponse from pydantic import BaseModel from huggingface_hub import hf_hub_download, HfApi import uuid import urllib.parse import urllib.request import urllib.error import os import logging import time import threading import 数据库连接 as db import asyncio from 安全认证 import require_auth from profile_io import start_profile_io, close_profile_io from verification_io import start_verification_io, stop_verification_io, close_verification_io from auth_io import start_auth_io, stop_auth_io, close_auth_io from auth_io import start_identity_io, stop_identity_io, close_identity_io from rate_limit import start_shared_work, stop_shared_work, close_shared_work from script_execution import start_admin_scripts, stop_admin_scripts, close_admin_scripts from comment_store import start_comment_reads, stop_comment_reads, close_comment_reads from periodic_tasks import start_periodic_task, request_periodic_stop, wait_periodic_tasks from owned_thread import run_owned_thread from resource_work import start_resource_work, stop_resource_work, close_resource_work from shutdown_lifecycle import (prepare_application_shutdown, run_application_shutdown, shutdown_cancel_requested, ShutdownFailures) from startup_warmup import (prepare_startup_warmup, run_startup_warmup, stop_startup_warmup, wait_startup_warmup, startup_warmup_stopped, startup_shutdown_requested) # ========================================== # 📝 P2优化:日志配置 # ========================================== logging.basicConfig( level=logging.INFO, format='%(asctime)s | %(levelname)s | %(name)s | %(message)s', datefmt='%Y-%m-%d %H:%M:%S' ) logger = logging.getLogger("ComfyUI-Ranking") _UPLOAD_CONCURRENCY = max(1, min(int(os.environ.get("MAX_CONCURRENT_UPLOADS", "4")), 16)) _UPLOAD_SEMAPHORE = threading.BoundedSemaphore(_UPLOAD_CONCURRENCY) from 云端_定时版本检测引擎 import daily_version_check_task, reset_daily_views_task # ========================================== # 👥 用户模块 (拆分为3个子模块) # ========================================== # router_users_auth.py - 🔐 登录/注册/密码重置/验证码 # router_users_profile.py - 👤 获取/更新用户资料 # router_users_social.py - 🤝 关注/隐私设置 from router_users_auth import router as users_auth_router from router_users_profile import router as users_profile_router from router_users_social import router as users_social_router # ========================================== # 其他业务模块 # ========================================== from router_items import (router as items_router, start_resource_reads, stop_resource_reads, close_resource_reads) # 📦 内容管理(工具/Skill/应用/推荐) from router_comments import (router as comments_router, start_comment_writes, stop_comment_writes, close_comment_writes) # 💬 评论系统 from router_messages import (router as messages_router, start_message_io, stop_message_io, close_message_io) # ✉️ 私信系统 from router_wallet import (router as wallet_router, start_wallet_sql, stop_wallet_sql, close_wallet_sql) # 💰 钱包/提现 from proxy_access import start_proxy_access, stop_proxy_access, close_proxy_access from proxy_download_work import start_proxy_downloads, stop_proxy_downloads, close_proxy_downloads from media_proxy_service import start_public_media, stop_public_media, close_public_media, public_media_endpoint from router_proxy import router as proxy_router # 🔗 代理下载 from router_posts import (router as posts_router, start_posts_io, stop_posts_io, close_posts_io) # 💬 讨论区 from router_prompts import (router as prompts_router, start_prompts_io, stop_prompts_io, close_prompts_io) # ✨ 提示词市场 from router_tasks import (router as tasks_router, start_tasks_io, stop_tasks_io, close_tasks_io) # 📝 任务榜 from router_open_api import start_open_sql, stop_open_sql, close_open_sql from router_open_api import router as open_api_router # 🌐 通用开放API(第三方插件) from router_admin_plugins import router as admin_plugins_router # 🔒 管理后台 - 第三方插件管理API from database_sql import init_sql_db from router_system_config import (router as system_config_router, start_config_sql, stop_config_sql, close_config_sql) from fastapi.exceptions import RequestValidationError from starlette.exceptions import HTTPException as StarletteHTTPException app = FastAPI( title="ComfyUI Ranking API", description="ComfyUI 社区排名系统 API", version="2.0.0", docs_url="/api/docs", redoc_url="/api/redoc" ) # ========================================== # 🛡️ 稳定性优化:全局异常处理器 # ========================================== # 作用:捕获所有未处理异常,防止单个请求崩溃整个服务 # 🚀 P3优化:统一错误响应格式 def create_error_response(status_code: int, detail: str, error_type: str = None, error_id: str = None, extra: dict = None): """🚀 P3优化:统一错误响应构造器""" response = { "success": False, "status": "error", "code": status_code, "detail": detail, } if error_type: response["type"] = error_type if error_id: response["error_id"] = error_id if extra: response.update(extra) return response @app.exception_handler(StarletteHTTPException) async def http_exception_handler(request, exc): """🚀 P3优化:HTTP 异常处理,返回统一格式""" logger.warning(f"HTTP {exc.status_code} | {route_label(request)} | detail_type={type(exc.detail).__name__}") # 根据状态码确定错误类型 error_type_map = { 400: "bad_request", 401: "unauthorized", 403: "forbidden", 404: "not_found", 409: "conflict", 429: "rate_limited", 502: "upstream_error", } error_type = error_type_map.get(exc.status_code, "http_error") return JSONResponse( status_code=exc.status_code, content=create_error_response(exc.status_code, str(exc.detail), error_type), headers={"X-Ranking-Error-Scope": "endpoint"} if exc.status_code == 503 else None, ) from request_body_guard import RequestBodyGuard app.add_middleware(RequestBodyGuard, error_response=create_error_response) from upload_body_guard import UploadBodyGuard app.add_middleware(UploadBodyGuard, capacity=_UPLOAD_CONCURRENCY, error_response=create_error_response) from public_list_etag import PublicListETag app.add_middleware(PublicListETag) app.add_middleware(PrivateResponseCacheControl) @app.exception_handler(RequestValidationError) async def validation_exception_handler(request, exc): """🚀 P3优化:请求参数校验失败处理""" logger.warning("Validation Error | %s | count=%d", route_label(request), len(exc.errors())) # 提取简洁的错误信息 errors = [] for err in exc.errors(): field = ".".join(str(loc) for loc in err["loc"][1:]) # 跳过 'body' errors.append({"field": field, "message": "该字段格式不正确", "type": err["type"]}) return JSONResponse( status_code=422, content=create_error_response( 422, "请求参数格式错误", "validation_error", extra={"errors": errors} ) ) @app.exception_handler(Exception) async def global_exception_handler(request, exc): """🚀 P3优化:全局异常处理,捕获所有未预期异常""" error_id = getattr(request.state, 'ranking_request_id', None) or uuid.uuid4().hex logger.error(f"Unhandled Exception [{error_id}] | {route_label(request)} | error_type={type(exc).__name__}") return JSONResponse( status_code=500, content=create_error_response( 500, "服务器内部错误,请稍后重试", "internal_error", error_id=error_id ), headers={"X-Ranking-Request-Id": error_id} ) # ========================================== # ❤️ 健康检查接口 (增强版) # ========================================== @app.get("/") def health_check(): return {"status": "ok", "message": "ComfyUI Ranking API is running perfectly!"} @app.get("/health") def detailed_health_check(): """详细健康检查,含依赖状态""" health_status = { "status": "ok", "components": {} } # 检查 SQL 数据库 try: from database_sql import check_db_connection if check_db_connection(): health_status["components"]["sql_database"] = "ok" else: health_status["components"]["sql_database"] = "error: connection failed" health_status["status"] = "degraded" except Exception as e: logger.warning(f"健康检查 SQL 数据库异常: error_type={type(e).__name__}") health_status["components"]["sql_database"] = "error: connection failed" health_status["status"] = "degraded" # 检查 JSON 文件访问 try: import 数据库连接 as json_db json_db.load_data("users.json", default_data={}) health_status["components"]["json_storage"] = "ok" except Exception as e: logger.warning(f"健康检查 JSON 存储异常: error_type={type(e).__name__}") health_status["components"]["json_storage"] = "error: storage unavailable" health_status["status"] = "degraded" return health_status def _warmup_dependencies(): if startup_warmup_stopped(app.state): return # ========== 预热检查:SQL 数据库 ========== try: init_sql_db() logger.info("✅ SQL 数据库初始化完成") except Exception as e: logger.error(f"❌ SQL 数据库初始化失败: error_type={type(e).__name__}") # 不抛出异常,允许降级运行 if startup_warmup_stopped(app.state): return # ========== 加载第三方插件注册表(依赖 SQL 数据库) ========== try: from router_open_api import load_plugins_from_db load_plugins_from_db() logger.info("✅ 第三方插件注册表已加载") except Exception as e: logger.warning(f"⚠️ 第三方插件注册表加载失败: error_type={type(e).__name__}") if startup_warmup_stopped(app.state): return # ========== 预热检查:JSON 数据库 ========== try: db.load_data("users.json", default_data={}) if startup_warmup_stopped(app.state): return db.load_data("items.json", default_data=[]) logger.info("✅ JSON 数据库访问正常") except Exception as e: logger.warning(f"⚠️ JSON 数据库访问异常: error_type={type(e).__name__}") # ========================================== # 🚀 应用启动事件 # ========================================== # 作用:初始化数据库 + 启动定时版本检测后台任务 + 预热检查 @app.on_event("startup") async def on_startup(): logger.info("🚀 ComfyUI-Ranking API 启动中...") if getattr(app.state, "_ranking_starting", False) or getattr(app.state, "_ranking_closers", 0): raise RuntimeError("application lifecycle is already in progress") if getattr(app.state, "_ranking_started", False): return app.state._ranking_starting = True try: prepare_startup_warmup(app.state) prepare_application_shutdown(app.state) start_profile_io() try: start_verification_io() start_auth_io() start_identity_io() start_shared_work() start_open_sql() start_config_sql() start_wallet_sql() start_resource_work() start_resource_reads() start_comment_writes() start_message_io() start_posts_io() start_prompts_io() start_tasks_io() start_proxy_access() start_proxy_downloads() start_public_media() start_comment_reads() start_admin_scripts() await run_startup_warmup(app.state, _warmup_dependencies) # SQL资金已冻结但JSON发布失败的任务,重启后继续投影;持有句柄供停机收尾。 from task_publication import publication_worker from database_sql import engine from db_utils import sort_cache start_periodic_task(app.state, "task_publication", lambda stop: publication_worker(engine, db, sort_cache.invalidate, stop)) from resource_publication import publication_worker as resource_publication_worker start_periodic_task(app.state, "resource_publication", lambda stop: resource_publication_worker(engine, db, sort_cache.invalidate, stop)) from resource_deletion import resource_deletion_worker start_periodic_task(app.state, "resource_deletion", lambda stop: resource_deletion_worker(db, stop, sort_cache.invalidate)) # ========== 启动后台任务 ========== start_periodic_task(app.state, "daily_version", daily_version_check_task) logger.info("✅ 定时版本检测任务已挂载") start_periodic_task(app.state, "daily_views", reset_daily_views_task) logger.info("✅ daily_views 每日重置任务已挂载") # ========== 启动 HF 批量同步定时器 ========== db.start_batch_sync() logger.info("✅ HF 同步调度初始化已完成") app.state._ranking_started = True logger.info("🎉 ComfyUI-Ranking API 启动完成!") except BaseException: # 启动可能只登记了部分句柄;正常关闭按已登记对象收尾。 if not startup_shutdown_requested(app.state): try: await on_shutdown() except BaseException as error: logger.error("启动收尾失败: error_type=%s", type(error).__name__) raise finally: app.state._ranking_starting = False @app.on_event("shutdown") async def on_shutdown(): return await run_application_shutdown(app.state, _shutdown_resources) async def _shutdown_resources(): """逐项收尾,错误在其它组件得到关闭机会后传播。""" app.state._ranking_started = False app.state._ranking_shutdown_incomplete = True failures = ShutdownFailures(logger) failures.call('log_start', logger.info, "🛑 ComfyUI-Ranking API 正在关闭...") failures.call('stop_warmup', stop_startup_warmup, app.state) for stage, stop in ( ('stop_wallet_sql', stop_wallet_sql), ('stop_proxy_downloads', stop_proxy_downloads), ('stop_public_media', stop_public_media), ('stop_resource_reads', stop_resource_reads), ('stop_comment_writes', stop_comment_writes), ('stop_posts', stop_posts_io), ('stop_prompts', stop_prompts_io), ('stop_tasks', stop_tasks_io), ('stop_proxy_access', stop_proxy_access), ('stop_messages', stop_message_io), ('stop_resources', stop_resource_work), ('stop_open_sql', stop_open_sql), ('stop_config_sql', stop_config_sql), ('stop_shared', stop_shared_work), ('stop_identity', stop_identity_io), ('stop_auth', stop_auth_io), ('stop_verification', stop_verification_io), ('stop_scripts', stop_admin_scripts), ('stop_comments', stop_comment_reads), ): failures.call(stage, stop) workers = [] for name in ('daily_version', 'daily_views', 'task_publication', 'resource_publication', 'resource_deletion'): worker = failures.call('capture_' + name, getattr, app.state, name + '_worker', None) if worker is not None: workers.append(worker) failures.call('stop_' + name, request_periodic_stop, app.state, (name,)) for stage, close, args in ( ('wait_warmup', wait_startup_warmup, (app.state,)), ('close_proxy_downloads', close_proxy_downloads, ()), ('close_public_media', close_public_media, ()), ('close_wallet_sql', close_wallet_sql, ()), ('close_resource_reads', close_resource_reads, ()), ('close_comment_writes', close_comment_writes, ()), ('close_posts', close_posts_io, ()), ('close_prompts', close_prompts_io, ()), ('close_tasks', close_tasks_io, ()), ('close_proxy_access', close_proxy_access, ()), ('close_messages', close_message_io, ()), ('close_resources', close_resource_work, ()), ('close_open_sql', close_open_sql, ()), ('close_config_sql', close_config_sql, ()), ('close_shared', close_shared_work, ()), ('close_identity', close_identity_io, ()), ('close_auth', close_auth_io, ()), ('close_verification', close_verification_io, ()), ('close_profile', close_profile_io, ()), ('close_scripts', close_admin_scripts, ()), ('close_comments', close_comment_reads, ()), ('wait_periodic', wait_periodic_tasks, (workers,)), ): await failures.wait(stage, close, *args) app.state._ranking_shutdown_incomplete = failures.incomplete failures.raise_if_failed(cancel_requested=shutdown_cancel_requested(app.state)) await run_owned_thread(db.flush_sync) logger.info("✅ HF 关闭前同步尝试已结束") logger.info("✅ 关闭完成") # 🛡️ CORS 配置:默认通配符(插件前端运行于用户本地 ComfyUI 页面,origin 为 # 127.0.0.1/localhost/局域网 IP/自定义端口等无法穷举;本 API 通过 Authorization 头 # 传递 Token 且 allow_credentials=False 不依赖 Cookie,通配风险可控)。 # 如需收紧可在环境变量 ALLOWED_ORIGINS 中逗号分隔指定域名。 ALLOWED_ORIGINS = os.environ.get("ALLOWED_ORIGINS", "*").split(",") logger.info( f"CORS 配置已加载: origin_count={len(ALLOWED_ORIGINS)} | " f"wildcard={len(ALLOWED_ORIGINS) == 1 and ALLOWED_ORIGINS[0] == '*'}" ) app.add_middleware( CORSMiddleware, allow_origins=ALLOWED_ORIGINS, allow_credentials=False, allow_methods=["*"], allow_headers=["*"], expose_headers=["ETag", "X-Ranking-Error-Scope", "X-Ranking-Request-Id"], ) app.add_middleware(RequestTraceMiddleware) # ========================================== # 路由挂载 # ========================================== # 用户模块 (3个子模块) app.include_router(users_auth_router) # 🔐 登录/注册/密码重置 app.include_router(users_profile_router) # 👤 用户资料 app.include_router(users_social_router) # 🤝 关注/隐私 # 其他业务模块 app.include_router(items_router) # 📦 内容管理 app.include_router(comments_router) # 💬 评论系统 app.include_router(messages_router) # ✉️ 私信系统 app.include_router(wallet_router) # 💰 钱包/提现 app.include_router(proxy_router) # 🔗 代理下载 app.include_router(posts_router) # 💬 讨论区 app.include_router(prompts_router) # ✨ 提示词市场 app.include_router(tasks_router) # 📝 任务榜 app.include_router(open_api_router) # 🌐 通用开放API(供第三方插件调用) app.include_router(admin_plugins_router) # 🔒 管理后台 - 第三方插件管理API # ========================================== # 🟢 私有图床代理中心 (Image Proxy) # 解决 Private 仓库下,本地客户端报 401 Unauthorized 的终极方案 # ========================================== from media_proxy_service import read_public_media from media_file_response import OwnedMediaFileResponse @app.get("/api/image_proxy") @public_media_endpoint async def proxy_hf_image(url: str = None, path: str = None): return await run_owned_thread(read_public_media, url, path, store=db, download=hf_hub_download, file_response=OwnedMediaFileResponse, error_response=create_error_response) # ========================================== # 上传接口 (将返回的 URL 替换为 Proxy 代理链接) # 🔒 P0安全优化:集成图片内容审核 # ========================================== from image_moderation import moderate_image_sync, ModerationResult # ========================================== # 🎵 音频安全净化:重编码压缩,剥离元数据与隐藏附加信息 # ========================================== from upload_service import process_upload @app.post("/api/upload") @shared_limit(30) def upload_file(request: Request, file: UploadFile = File(...), file_type: str = Form(...), current_user: str = Depends(require_auth)): return process_upload(file, file_type, current_user, store=db, storage_factory=HfApi, moderate=moderate_image_sync, limiter=_UPLOAD_SEMAPHORE, error_response=create_error_response) from github_source import select_github_token class ValidateResourceRequest(BaseModel): url: str item_id: str account: str from resource_probe_service import probe_resource from github_probe_transport import open_github_probe from skill_preflight import verify_skill_repository from router_proxy import _send_with_validated_redirects from safe_http_transport import ValidatedAsyncHTTPTransport import httpx def _probe_skill_repository(item, token): return verify_skill_repository(item, token, send=_send_with_validated_redirects, client_factory=httpx.AsyncClient, transport_factory=ValidatedAsyncHTTPTransport) @app.post("/api/validate_resource") @shared_limit(30) def validate_resource(req_data: ValidateResourceRequest, request: Request, current_user: str = Depends(require_auth)): return probe_resource(req_data.item_id, store=db, download=hf_hub_download, open_url=open_github_probe, token_selector=select_github_token, error_response=create_error_response, skill_probe=_probe_skill_repository) # ========================================== # 🔒 管理员:系统配置 API # ========================================== app.include_router(system_config_router) # ========================================== # 🖥️ 管理界面静态文件挂载 # ⚠️ 必须放在所有 API 路由注册之后,避免拦截 API # 访问 /admin/ 会自动返回 管理界面/index.html # ========================================== from fastapi.staticfiles import StaticFiles app.mount("/admin", StaticFiles(directory="管理界面", html=True), name="admin") # ========================================== # 📱 移动端网页静态挂载(复用前端的 Web 版) # 部署约定:同步 HF Space 时,除本目录内容外, # 须将仓库根目录的 前端页面/ 整个目录一并复制到 Space 仓库根目录 # /m/assets 提供共享前端模块;/m 返回 移动端网页/index.html # ⚠️ 必须放在所有 API 路由之后;先挂载更具体的 /m/assets,再挂载 /m # 容错:目录缺失时仅跳过挂载并警告,不阻断 API 服务启动(移动端页面将 404) # ========================================== if os.path.isdir("前端页面"): app.mount("/m/assets", StaticFiles(directory="前端页面"), name="web_assets") app.mount("/m", StaticFiles(directory="移动端网页", html=True), name="mobile_web") else: logger.warning("⚠️ 未找到 前端页面/ 目录,移动端网页 /m/ 未挂载(部署时须将仓库根目录的 前端页面/ 复制到 Space 仓库根目录),API 服务不受影响")