DeepTutor 的 api 层:一个 FastAPI 实例、装配顺序与全部路由挂载点
读一个后端项目的路由层,最容易掉进去的坑是从最大的那个 router 开始读。DeepTutor 里最大的 router 是 deeptutor/api/routers/knowledge.py,2962 行——从这儿进去,读三个小时也拼不出整体形状。
更省时间的路径是先读装配点。DeepTutor 的 deeptutor/api 包整体是 40 个 .py 文件、15796 行(截至我们采集时),但所有 router 的挂载都收在 deeptutor/api/main.py 一个文件里,app.include_router 的 prefix 与顺序一眼可见。
以下行号与数字都对应我们采集的仓库快照:HEAD 456f9c2,采集日 2026-08-10,deeptutor/__version__.py 里的版本号是 1.5.11。你自己 clone 之后行号可能已经漂了,文件名与参数名是稳的。
一、app 那十行里,有一个被主动关掉的默认行为
应用实例的构造在 deeptutor/api/main.py:234-243:
app = FastAPI(
title="DeepTutor API",
version="1.0.0",
lifespan=lifespan,
redirect_slashes=False,
)
前三个参数没什么可说的。值得停一下的是第四个。
redirect_slashes 是 FastAPI/Starlette 的一个默认开启的贴心行为:请求 /api/v1/foo 而路由注册的是 /api/v1/foo/(或反过来)时,框架发一个 307 重定向把你导到正确的那个。绝大多数项目不会去动它。
DeepTutor 把它关了,理由直接写在 main.py:238-242 的注释里:部署在 HTTPS 反向代理后面时,这个 307 重定向可能把 HTTPS 换成 HTTP,也就是协议降级。注释同时给了 issue 编号 #112。
这处反直觉在于:出问题的不是你写的代码,是框架替你做的一个”修正”。反代把 HTTPS 卸载掉之后,应用自己看到的是明文 HTTP,它据此生成的重定向 Location 也就成了 http://,浏览器跟过去就掉出了加密链路。关掉之后代价是尾斜杠不再被自动纠正——路径写错就是 404,不再有兜底。这个取舍是项目的选择,我们没有部署过,不评价它在别的部署形态下合不合适。
二、启动时先做一次”配置漂移”检查
main.py:36-68 定义了一个校验:capability manifest 里引用、但没有在 ToolRegistry 里注册的工具,会直接抛 RuntimeError;调用点在 main.py:112,属于 lifespan 启动路径的一部分。
也就是说这类不一致是启动期 fail-fast,而不是等到某轮对话真的去调那个工具时才炸。排查方向因此很明确:如果 DeepTutor 起不来且报的是这个 RuntimeError,去比对 capability manifest 与 ToolRegistry 两侧的工具名,不用去翻对话链路。
三、lifespan 的启停序列,是对称着写的
启动序列(main.py:114-170):初始化 LLM client → 启动 EventBus → auto_start_partners() → 启动 cron → ping PocketBase → 迁移 v1 memory,并把 legacy 的 tutorbot memory surface 改名为 partner。
关闭序列(main.py:177-231):停 cron → 停 partners(带 preserve_auto_start=True)→ 关 MCP 连接 → 关 LLM provider 池 → 关 agentic client 池 → 停 EventBus。
两段对照着看会更省事。关闭时 preserve_auto_start=True 这个参数的意思是这次停机不改写”下次是否自动启动”的状态——也就是说进程重启后 partners 仍会被 auto_start_partners() 拉起来。partners 是 IM 渠道层,我们另有一篇专门讲它。
值得注意的是启动序列里没有”等 PocketBase 就绪”这种阻塞语义,只是 ping;而 memory 迁移排在最后。这些顺序都能在上面两个行段里逐行对上。
四、CORS:鉴权关掉的时候,反而是放开的
CORS 配置在 main.py:79-99,应用在 main.py:290-297。默认放行四个来源:http://localhost:{frontend_port}、http://127.0.0.1:{frontend_port},以及 3000 端口的这两个变体。
关键在下一行:auth 关闭时 allow_origin_regex 被设为 r"https?://.*",模式标记为 permissive;auth 开启时改为 explicit,跨源来源需要显式配置 CORS_ORIGIN(S)。
这个方向和直觉是反的——通常我们预期”鉴权关掉”意味着更小心,而这里是鉴权关掉→ CORS 放开到任意来源。这个取舍写在代码里,我们只照抄取值,不去推断它的动机,也不推断这个默认值适不适合你的网络环境;只提醒一句:这是默认配置,不是”这样部署就安全”的结论。鉴权机制本身(JWT、cookie、WebSocket 侧怎么校验)我们另有一篇专门讲。
五、日志与进程:两处容易找不到的开关
- 访问日志只记非 200。自定义中间件在
main.py:245-256,uvicorn 自身的 per-request access log 在所有启动路径上都被关掉(deeptutor/api/run_server.py:77里access_log=False)。所以你在日志里翻不到成功请求不是日志坏了,是设计如此。 - 绑定与热重载:uvicorn 绑
host="0.0.0.0",端口来自配置里的get_backend_port(),reload默认关闭、由环境变量DEEPTUTOR_DEV_RELOAD打开(run_server.py:64-79)。 - Windows 侧单独一条:
run_server.py:14-18在 Windows 下强制WindowsProactorEventLoopPolicy,注释给的理由是子进程 API(Math Animator 渲染等)需要。这一行同时说明了一件该讲清楚的事——这个后端会在你本机拉起子进程。往后看还有更直接的:路由表里的/api/v1/space/cli-apps对应的cli_apps是”管理员安装、chat agent 调用的命令行工具”(模块 docstring 在deeptutor/services/cli_apps/__init__.py:1),而subagent相关能力会把你本机已经装好的 agent CLI 当作子代理来驱动(deeptutor/services/subagent/__init__.py:1-7)。要不要开、开给谁,是需要你自己按环境判断的事,本文不给方案。
六、路由挂载点全表
下面这张表是本篇的主数据:每一行是 app.include_router 的一个 prefix,以及对应 router 文件里路由装饰器的条数(grep -c 数出来的)。看表的方法是按数量找重心,而不是逐条记端点。
| 挂载 prefix | 路由装饰器数 |
|---|---|
/api/v1/auth(公开,无 _auth 依赖) | 15 |
/api/outputs | 1(@router.api_route) |
/api/v1/multi-user | 5 |
/api/v1(chat) | 4 |
/api/v1/question | 2(均为 WebSocket) |
/api/v1/knowledge | 49 |
/api/v1/imports / /api/v1/dashboard | 2 / 2 |
/api/v1/learning(mastery_path) | 8 |
/api/v1/co_writer / /api/v1/notebook | 12 / 11 |
/api/v1/book / /api/v1/memory | 23 / 27 |
/api/v1/capabilities / /api/v1/sessions | 2 / 7 |
/api/v1/question-notebook | 12 |
/api/v1/settings | 46 |
/api/v1/settings/mcp / /api/v1/space/mcp | 5 / 8 |
/api/v1/space/cli-apps | 5 |
/api/v1/skills / /api/v1/subagents | 12 / 10 |
/api/v1/personas / /api/v1/tools | 5 / 1 |
/api/v1/system | 6 |
/api/v1/voice | 2(POST /tts、POST /stt) |
/api/v1/plugins / /api/v1/agent-config | 4 / 2 |
/api/attachments | 1 |
/api/v1/partners | 32 |
/api/v1(unified_ws) | 1(WS /ws) |
/api/v1(quiz_judge) | 1(WS /question/judge) |
重心一眼可见:knowledge(49)、settings(46)、partners(32)、memory(27)四块占了将近一半。想快速摸清这个项目在做什么,从这四个 prefix 进去比从首页进去有效。
一句必要的限定:/api/v1/learning 这 8 条端点背后的掌握度计算、复习间隔一类参数,是该项目的实现选择,不是经过验证的教学结论;本篇只讲它挂在哪个 prefix 下,不评价它的教学效果。
七、表里有四个例外,比表本身更值得记
/api/outputs用的不是常规装饰器。它是一条@router.api_route("/{output_path:path}", methods=["GET","HEAD"])(deeptutor/api/routers/outputs.py:40),所以按@router.get|post|...去 grep 时数不到它。/api/v1/partners是唯一整体挂require_admin的业务 router(main.py:464-466)。别的 router 是逐端点判定,这个是整块拦死。/api/v1/settings里有一条是公开的:GET /ui挂在public_router上、无需登录(main.py:410-421、deeptutor/api/routers/settings.py:1153)。理由写在代码里:登录页得先拿到界面语言。- 两个 WebSocket router 的鉴权在 handler 内部做(
main.py:474-480):unified_ws的WS /ws与quiz_judge的WS /question/judge,原因是 WebSocket 用不了常规的 FastAPI 依赖。所以你在include_router那一行看不到它们的鉴权,得进 handler 里找。
八、可复现的核查动作
这几步你自己 clone 之后就能跑,不需要装依赖、不需要启动服务:
# 1. 数路由装饰器总数(不含 outputs 的 api_route)
grep -rnE '@router\.(get|post|put|delete|patch|websocket)|@public_router\.' \
deeptutor/api/routers/ deeptutor/multi_user/router.py | wc -l
# 2. 看所有挂载点与顺序
grep -n 'include_router' deeptutor/api/main.py
# 3. 直接跳到那处被关掉的默认行为
sed -n '234,243p' deeptutor/api/main.py
第一条命令在我们采集时得到 322。把上面表格里除 /api/outputs 之外的各项相加,同样是 322——这两个数能对上,说明表没有漏项。
但322 不等于 322 个可访问 URL:同一路径的不同方法会各占一个装饰器(比如 GET|DELETE /chat/sessions/{session_id} 就是两条),我们也没有逐条拼出完整 URL 全表来核实是否存在别的重复计数情形。要精确到 URL,得自己展开。
另一个值得做的比对:把第 2 步的输出和 deeptutor/api/routers/ 的 ls 结果对一遍,看有没有存在文件但未被挂载的 router。
九、一处文档与代码对不上的地方
仓库根目录的 AGENTS.md 有一张 Key Files 表(AGENTS.md:101-118)。这张表在 api 层只列了一个文件:deeptutor/api/routers/unified_ws.py(AGENTS.md:117)。本文引用到的 deeptutor/api/main.py、上面那几个高密度 router,以及 deeptutor/multi_user/、deeptutor/logging/、deeptutor/i18n/ 等包,都不在这张表里。
两处口径不一致,以我们实读的仓库状态为准。至于为什么不一致,我们没有依据,不推断。
实际影响只有一条:如果你把 AGENTS.md 的 Key Files 当作 api 层的入口索引,会漏掉装配点本身。读这一层,还是直接从 main.py 进。
本文依据 DeepTutor 官方仓库(github.com/HKUDS/DeepTutor)的 README、AGENTS.md、
pyproject.toml 与 deeptutor/ 下的源码整理,核对日 2026-08-10,对应仓库快照 456f9c2(版本 1.5.11)。
本文内容为仓库源码与文档口径,我们没有安装、部署或运行过该项目,也没有调用过其中任何一个模型 API,
因此不涉及生成质量、响应速度与教学效果的任何描述。
参数与默认值随版本变动,请以仓库最新代码与 --help 的实际输出为准。
该项目会在本机启动子进程并驱动已安装的命令行工具,安全相关做法请结合自身环境评估,本文不构成安全方案建议。