首页
/ GPT Academic 基于 FastAPI 的二级路径(子路径)部署实践与源码解析

GPT Academic 基于 FastAPI 的二级路径(子路径)部署实践与源码解析

2026-09-05 22:28:58作者:袁立春Spencer

本文围绕 GPT Academic 仓库中的 FastAPI 部署文档 展开,讲解如何用 FastAPI + uvicorn 将项目部署到「二级路径」(子路径,例如 http://ip:port/gpt_academic/)之下,以解决反向代理场景中的 sub-path deploy 问题。读完本篇,你可以:完整复现文档给出的三步配置流程,理解 CUSTOM_PATH 的合法取值与校验逻辑,并深入阅读 shared_utils/fastapi_server.pytoolbox.py 中负责子路径挂载、敏感文件拦截、鉴权文件端点替换与线程化 uvicorn 服务的真实实现。

为什么需要基于 FastAPI 的二级路径部署

项目原生的 Gradio 启动方式(demo.launch(...))只能把 Web 服务挂在服务器根路径 http://ip:port/ 下。在以下场景会出现问题:

  • 通过 Nginx / Apache 等反向代理把多个服务挂到同一域名的不同路径段下;
  • 与 Apache 等 web server 共存时,根路径已被占用;
  • 部署平台要求应用必须运行在某个子路径下。

文档开篇即点明动机:「We currently support fastapi in order to solve sub-path deploy issue.」(目前支持 FastAPI,用于解决子路径部署问题)。原理上,Gradio 3.x 的 Blocks 对象本身是一个 Starlette/FastAPI 应用,可以用 gr.mount_gradio_app 把它挂载到一个外层 FastAPI 实例的任意路径上,再由 uvicorn 统一提供 HTTP/WebSocket 服务。

文档原版三步走:修改 CUSTOM_PATH、编辑 main.py、启动

以下完整继承 docs/WithFastapi.md 的操作步骤,并结合当前仓库源码补充了实际取值与验证要点。

第 1 步:修改 config.py 中的 CUSTOM_PATH

nano config.py

config.py 中,该配置项位于第 206-208 行:

# 如果需要在二级路径下运行(常规情况下,不要修改!!)
# (举例 CUSTOM_PATH = "/gpt_academic",可以让软件运行在 http://ip:port/gpt_academic/ 下。)
CUSTOM_PATH = "/"

取值规则(可结合下文 is_path_legal 校验函数理解):

取值 行为
"/" 常规部署,运行在根路径 http://ip:port/不需要任何额外修改
"/gpt_academic"(必须以单个 / 开头、后不跟第二个 / 二级路径部署,运行在 http://ip:port/gpt_academic/
空字符串或不以 / 开头 视为非法路径,回退到根路径部署

第 2 步:按文档 diff 编辑 main.py(历史版本流程)

文档给出的原始 diff 是:把 demo.queue(...).launch(...) 一行改为只保留 demo.queue(concurrency_count=CONCURRENT_COUNT),并启用被注释掉的分支逻辑:

    auto_opentab_delay()
    - demo.queue(concurrency_count=CONCURRENT_COUNT).launch(server_name="0.0.0.0", server_port=PORT, auth=AUTHENTICATION, favicon_path="docs/logo.png")
    + demo.queue(concurrency_count=CONCURRENT_COUNT)

    - # 如果需要在二级路径下运行
    - # CUSTOM_PATH = get_conf('CUSTOM_PATH')
    - # if CUSTOM_PATH != "/":
    - #     from toolbox import run_gradio_in_subpath
    - #     run_gradio_in_subpath(demo, auth=AUTHENTICATION, port=PORT, custom_path=CUSTOM_PATH)
    - # else:
    - #     demo.launch(server_name="0.0.0.0", server_port=PORT, auth=AUTHENTICATION, favicon_path="docs/logo.png")

    + 如果需要在二级路径下运行
    + CUSTOM_PATH = get_conf('CUSTOM_PATH')
    + if CUSTOM_PATH != "/":
    +     from toolbox import run_gradio_in_subpath
    +     run_gradio_in_subpath(demo, auth=AUTHENTICATION, port=PORT, custom_path=CUSTOM_PATH)
    + else:
    +     demo.launch(server_name="0.0.0.0", server_port=PORT, auth=AUTHENTICATION, favicon_path="docs/logo.png")

if __name__ == "__main__":
    main()

即:当 CUSTOM_PATH != "/" 时,不再调用 Gradio 原生的 launch,而是走 run_gradio_in_subpath(见下文源码分析);否则维持原生 launch

重要更新说明:阅读当前仓库代码可以发现,这条历史流程已经「内化」为主流程,无需再手动编辑 main.py。当前 main.py 末尾(L353-355)的启动逻辑是:

# 最后,正式开始服务
from shared_utils.fastapi_server import start_app
start_app(app_block, CONCURRENT_COUNT, AUTHENTICATION, PORT, SSL_KEYFILE, SSL_CERTFILE)

也就是说,当前版本统一通过 shared_utils/fastapi_server.py 中的 start_app 以 FastAPI + uvicorn 方式提供服务CUSTOM_PATHAUTHENTICATIONSSL 全部由这一个入口处理。文档中的 diff 可视为早期版本的操作记录,用于理解「为什么要从 launch 切换到 FastAPI 挂载」的演进背景。

第 3 步:启动

python main.py

启动前请注意两个前置条件(均可在源码中确认):

  • 项目对 Gradio 版本有强约束:main.py 第 37-38 行要求 gr.__version__ 必须为 3.32.15,否则直接抛出 ModuleNotFoundError,提示运行 pip install -r requirements.txt
  • 端口取自配置项 WEB_PORTconfig.py 第 146 行,默认 -1 表示随机选取空闲端口),PORT = find_free_port() if WEB_PORT <= 0 else WEB_PORTmain.py 第 58 行)。

二级路径入口:run_gradio_in_subpath 的完整实现

文档 diff 中提到的 run_gradio_in_subpath 定义在 toolbox.py 第 675-721 行,其完整流程如下:

def run_gradio_in_subpath(demo, auth, port, custom_path):
    """把gradio的运行地址更改到指定的二次路径上"""

    def is_path_legal(path: str) -> bool:
        # path == "/" 直接放行
        # 空路径 -> 记录日志并回退根路径
        # 以单个 "/" 开头 -> 合法子路径
        # 以 "//" 开头或不以 "/" 开头 -> 非法,回退根路径

    if not is_path_legal(custom_path):
        raise RuntimeError("Illegal custom path")
    import uvicorn
    import gradio as gr
    from fastapi import FastAPI

    app = FastAPI()
    if custom_path != "/":
        @app.get("/")
        def read_main():
            return {"message": f"Gradio is running at: {custom_path}"}

    app = gr.mount_gradio_app(app, demo, path=custom_path)
    uvicorn.run(app, host="0.0.0.0", port=port)

几个值得注意的实现细节:

  1. is_path_legal 的路径校验toolbox.py L680-L705):只接受 "/" 或「单个斜杠开头」的路径,"/a//b" 这类含双斜杠的写法会被判为非法并抛出 RuntimeError("Illegal custom path")。配置 CUSTOM_PATH 时务必写成 "/gpt_academic" 而不是 "gpt_academic""/gpt_academic/"(尾斜杠虽不影响挂载,但文档注释的规范写法是单前导斜杠)。
  2. 根路径提示接口:当使用子路径部署时,http://ip:port/ 会返回 {"message": "Gradio is running at: /gpt_academic"},方便访问者发现真实入口;
  3. gr.mount_gradio_app(app, demo, path=custom_path) 是核心一行:Gradio Blocks 被作为子应用挂载到外层 FastAPI 路由 custom_path 下,Gradio 内部的静态资源、WebSocket(queue 通信)均相对该前缀工作;
  4. uvicorn.run(app, host="0.0.0.0", port=port) 接管了原本由 demo.launch 内部完成的 ASGI 服务职责。注意源码中 auth=auth 参数已被注释掉——该简化入口并不处理账号密码鉴权,需要鉴权时应使用下文 start_app 提供的完整能力。

当前主流程的核心:shared_utils/fastapi_server.py 的 start_app

当前仓库真正的部署入口是 shared_utils/fastapi_server.py 中的 start_app(L109 起)。它把文档中「手动改 main.py」的所有工作收敛为一个函数调用,功能上远超文档 diff 的覆盖范围。

1. Gradio Blocks 的服务化配置

app_block.auth = AUTHENTICATION if len(AUTHENTICATION) != 0 else None
app_block.blocked_paths = ["config.py", "__pycache__", "config_private.py",
                           "docker-compose.yml", "Dockerfile", f"{PATH_LOGGING}/admin"]
app_block.dev_mode = False
app_block.enable_queue = True
app_block.queue(concurrency_count=CONCURRENT_COUNT)
  • blocked_paths 直接封禁了 config.pyconfig_private.pyDockerfile 等敏感文件经 /file= 端点被下载的可能;
  • CONCURRENT_COUNT 来自 config.py 第 190 行(默认 100),控制 Gradio queue 的并发任务数;
  • favicon 被显式指向仓库内的 docs/logo.png(L122),即使挂在子路径下图标也能正确显示。

2. 用 FastAPI 重新组装应用并挂载到 CUSTOM_PATH

gradio_app = App.create_app(app_block)          # 把 Blocks 转成 Starlette 应用
...
fastapi_app = FastAPI(lifespan=app_lifespan)
fastapi_app.mount(CUSTOM_PATH, gradio_app)      # 核心:挂载到二级路径

fastapi_app.mount(CUSTOM_PATH, gradio_app)run_gradio_in_subpath 中的 gr.mount_gradio_app 等价,是把整个 UI(页面、资源、queue WebSocket)挪到 CUSTOM_PATH 前缀下的关键步骤。

3. 文件端点替换:鉴权模式下的越权与路径穿越防护

start_app 移除了 Gradio 原生的 /file/{path:path}/file={path_or_url:path} 路由,重新实现了带校验的替代端点(L145-L195):

async def file(path_or_url: str, request: fastapi.Request):
    if not _authorize_user(path_or_url, request, gradio_app):
        return "越权访问!"
    stripped = path_or_url.lstrip().lower()
    if stripped.startswith("https://") or stripped.startswith("http://"):
        return "账户密码授权模式下, 禁止链接!"
    if '../' in stripped:
        return "非法路径!"
    return await endpoint(path_or_url, request)
  • _authorize_user(L72-L90)通过请求 cookie 中的 access-token 反查登录用户,只允许访问「本用户目录、autogenarxiv_cache、默认用户」四类目录,其余一律返回越权提示。上传目录(PATH_PRIVATE_UPLOAD)与日志目录(PATH_LOGGING)按用户划分,天然形成租户隔离;
  • 对外链(http(s)://)与 ../ 路径穿越一律拒绝;
  • 开启 AUTHENTICATIONconfig.py 第 203 行,格式为 [("username", "password"), ...])时,额外注册 /academic_logout 注销路由(L170-L175),清除 access-token cookie 后 302 回 CUSTOM_PATH——子路径部署时注销落地页也会正确回到应用前缀下。

文件头部的大段 docstring(L1-L45)就是作者为这套实现维护的测试清单,覆盖了「有无 custom_path × 有无账号鉴权」四组组合下的文件上传、下载、WebSocket 以及 __pycache__ 拦截验证,可作为你自测子路径部署时的核对表。

4. TTS 端点随应用一起挂载

TTS_TYPE != "DISABLE"config.py 第 245 行,可选 EDGE_TTS / LOCAL_SOVITS_API / DISABLE,默认 EDGE_TTS),start_app 会额外注册 POST /vits 路由(L238):

  • EDGE_TTS 模式下,服务端用 edge_tts 合成 MP3,再经 pydub 转成 WAV 返回(需要本机安装 ffmpeg,否则抛出带安装提示的 RuntimeError);
  • LOCAL_SOVITS_API 模式下,把请求体原样转发到 GPT_SOVITS_URL 指向的本地服务。

由于这些路由注册在 gradio_app 上,子路径部署时它们自动获得 CUSTOM_PATH 前缀,前端 JS(themes/tts.js)通过 local_url 拼出的请求地址无需感知前缀差异。

5. 线程化 uvicorn、SSL 与地址回写

class Server(uvicorn.Server):
    def install_signal_handlers(self): pass
    def run_in_thread(self):
        self.thread = threading.Thread(target=self.run, daemon=True)
        ...

Server 继承 uvicorn.Server 并在独立守护线程中运行(L93-L106),信号处理被禁用以避免子线程里 SIGINT 处理冲突。随后:

ssl_keyfile = None if SSL_KEYFILE == "" else SSL_KEYFILE
ssl_certfile = None if SSL_CERTFILE == "" else SSL_CERTFILE
config = uvicorn.Config(fastapi_app, host="0.0.0.0", port=PORT,
                        reload=False, log_level="warning",
                        ssl_keyfile=ssl_keyfile, ssl_certfile=ssl_certfile)
server = Server(config)
server.run_in_thread()
  • SSL 参数来自 config.pySSL_KEYFILE / SSL_CERTFILE(默认均为空字符串即 HTTP);提供 key 而缺 cert 会抛出 ValueError
  • 服务 URL 的拼装在 L289-L299:http(s)://host:PORT/ 之后,若 CUSTOM_PATH != '/',追加去掉首尾斜杠的路径段并补尾斜杠,得到形如 http://localhost:7860/gpt_academic/local_url
  • local_url 被回写到 app_block.local_url 并同步给 queue(app_block._queue.set_url(...),L314)。这是子路径部署能否正常工作的一环:Gradio 前端的 queue WebSocket 地址必须带上前缀,否则页面能打开但所有请求会打到根路径而失败;
  • 最后 requests.get(f"{app_block.local_url}startup-events", ...) 触发启动事件并以 app_block.block_thread() 阻塞主线程,保持进程存活。

6. 屏蔽 FastAPI 自带的 API 文档路由

@fastapi_app.middleware("http")
async def middleware(request: Request, call_next):
    if request.scope['path'] in ["/docs", "/redoc", "/openapi.json"]:
        return JSONResponse(status_code=404, content={"message": "Not Found"})
    response = await call_next(request)
    return response

子路径部署时会注册该中间件与自定义 favicon.ico 路由(L261-L272),把 FastAPI 自动生成的 /docs/redoc/openapi.json 全部 404 掉,避免把内部接口结构暴露给访问者。

与文档流程相关的其他配置项速查

配置项 位置 默认值 说明
CUSTOM_PATH config.py "/" 二级路径部署开关,非 "/" 时应用挂载到该前缀
WEB_PORT config.py -1 -1 表示随机选取空闲端口
CONCURRENT_COUNT config.py 100 Gradio queue 并发任务数,传入 start_app
AUTHENTICATION config.py [] 非空时启用账号密码登录 + 文件越权防护 + /academic_logout
SSL_KEYFILE / SSL_CERTFILE config.py "" 非空时以 HTTPS 启动(两者必须同时提供)
TTS_TYPE config.py "EDGE_TTS" 决定是否注册 POST /vits 音频合成端点

部署验证清单

结合 shared_utils/fastapi_server.py 头部维护的测试矩阵,完成子路径部署后建议逐项验证(custom_path = "/cc/gptac" 时):

  1. http://ip:port/cc/gptac/ 页面正常加载,favicon 正确;
  2. http://ip:port/ 根路径(简化入口)或 FastAPI 中间件下 /docs/openapi.json 返回 404/提示;
  3. 文件上传、下载(/file= 端点)在子路径前缀下正常;
  4. 对话提交(queue WebSocket)正常,停止/流式输出正常;
  5. 直接请求 __pycache__config.py 等敏感路径被拦截;
  6. 开启 AUTHENTICATION 后,用 A 用户登录不能访问 B 用户的上传/日志目录(返回「越权访问!」),注销后 cookie 被清除并跳回 CUSTOM_PATH

小结

docs/WithFastapi.md 给出的三步流程(改 CUSTOM_PATH → 让 main.pyrun_gradio_in_subpath 分支 → python main.py)回答了「怎么把 GPT Academic 挂到二级路径」这个问题;而当前仓库的实现已经把这一能力升级为统一的 start_app 入口:main.py 只需一次 start_app(app_block, CONCURRENT_COUNT, AUTHENTICATION, PORT, SSL_KEYFILE, SSL_CERTFILE) 调用,即可获得子路径挂载、敏感文件拦截、基于 cookie 的文件越权防护、TTS 端点、HTTPS 支持与线程化 uvicorn 服务。理解 toolbox.pyrun_gradio_in_subpath 有助于把握「FastAPI 外层应用 + mount 挂载 Gradio」这一核心机制,而 shared_utils/fastapi_server.py 则是该机制在生产形态下的完整工程化实现,两者对照阅读能完整覆盖本文主题。

登录后查看全文
热门项目推荐
相关项目推荐