GPT Academic 基于 FastAPI 的二级路径(子路径)部署实践与源码解析
本文围绕 GPT Academic 仓库中的 FastAPI 部署文档 展开,讲解如何用 FastAPI + uvicorn 将项目部署到「二级路径」(子路径,例如 http://ip:port/gpt_academic/)之下,以解决反向代理场景中的 sub-path deploy 问题。读完本篇,你可以:完整复现文档给出的三步配置流程,理解 CUSTOM_PATH 的合法取值与校验逻辑,并深入阅读 shared_utils/fastapi_server.py 与 toolbox.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_PATH、AUTHENTICATION、SSL 全部由这一个入口处理。文档中的 diff 可视为早期版本的操作记录,用于理解「为什么要从 launch 切换到 FastAPI 挂载」的演进背景。
第 3 步:启动
python main.py
启动前请注意两个前置条件(均可在源码中确认):
- 项目对 Gradio 版本有强约束:main.py 第 37-38 行要求
gr.__version__必须为3.32.15,否则直接抛出ModuleNotFoundError,提示运行pip install -r requirements.txt; - 端口取自配置项
WEB_PORT(config.py 第 146 行,默认-1表示随机选取空闲端口),PORT = find_free_port() if WEB_PORT <= 0 else WEB_PORT(main.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)
几个值得注意的实现细节:
is_path_legal的路径校验(toolbox.py L680-L705):只接受"/"或「单个斜杠开头」的路径,"/a//b"这类含双斜杠的写法会被判为非法并抛出RuntimeError("Illegal custom path")。配置CUSTOM_PATH时务必写成"/gpt_academic"而不是"gpt_academic"或"/gpt_academic/"(尾斜杠虽不影响挂载,但文档注释的规范写法是单前导斜杠)。- 根路径提示接口:当使用子路径部署时,
http://ip:port/会返回{"message": "Gradio is running at: /gpt_academic"},方便访问者发现真实入口; gr.mount_gradio_app(app, demo, path=custom_path)是核心一行:Gradio Blocks 被作为子应用挂载到外层 FastAPI 路由custom_path下,Gradio 内部的静态资源、WebSocket(queue 通信)均相对该前缀工作;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.py、config_private.py、Dockerfile等敏感文件经/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反查登录用户,只允许访问「本用户目录、autogen、arxiv_cache、默认用户」四类目录,其余一律返回越权提示。上传目录(PATH_PRIVATE_UPLOAD)与日志目录(PATH_LOGGING)按用户划分,天然形成租户隔离;- 对外链(
http(s)://)与../路径穿越一律拒绝; - 开启
AUTHENTICATION(config.py 第 203 行,格式为[("username", "password"), ...])时,额外注册/academic_logout注销路由(L170-L175),清除access-tokencookie 后 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.py 的
SSL_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" 时):
http://ip:port/cc/gptac/页面正常加载,favicon 正确;http://ip:port/根路径(简化入口)或 FastAPI 中间件下/docs、/openapi.json返回 404/提示;- 文件上传、下载(
/file=端点)在子路径前缀下正常; - 对话提交(queue WebSocket)正常,停止/流式输出正常;
- 直接请求
__pycache__、config.py等敏感路径被拦截; - 开启
AUTHENTICATION后,用 A 用户登录不能访问 B 用户的上传/日志目录(返回「越权访问!」),注销后 cookie 被清除并跳回CUSTOM_PATH。
小结
docs/WithFastapi.md 给出的三步流程(改 CUSTOM_PATH → 让 main.py 走 run_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.py 的 run_gradio_in_subpath 有助于把握「FastAPI 外层应用 + mount 挂载 Gradio」这一核心机制,而 shared_utils/fastapi_server.py 则是该机制在生产形态下的完整工程化实现,两者对照阅读能完整覆盖本文主题。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00