两个前端目录是什么关系,技术栈各是什么

2026-08-10

clone 下 DeepTutor 之后,第一眼会被目录名骗一次:仓库根下同时有 deeptutor_web/web/,前者名字更”正式”,看着像主目录。实际上在源码检出里,deeptutor_web/ 是空的——ls -a deeptutor_web 只列得出一个 __init__.py

这篇就把这两个目录的关系、web/ 到底是什么技术栈、以及前端怎么找到后端这三件事按行号读一遍。

下面所有行号与数字都对应我们采集的仓库快照:HEAD 456f9c2,采集日 2026-08-10,deeptutor/__version__.py 里的版本号是 1.5.11。你自己 clone 之后行号可能已经漂了,文件名和常量名是稳的。

一、deeptutor_web/ 是个壳,web/ 才是本体

deeptutor_web/__init__.py 的 docstring 已经把这件事写清楚了(deeptutor_web/__init__.py:1-6):这个包是用来装「打包好的 DeepTutor Web standalone 资产」的,release 构建会用 next build 的 standalone 产物——server.js.next/staticpublic 以及最小 Node 运行时文件——把它填满;源码检出通常留空,launcher 回退到 web/

所以两个目录不是”新旧两版前端”,而是同一份前端的两种存在形态:web/ 是源码,deeptutor_web/ 是发行包里的构建产物落点。

反直觉的地方在 launcher 的判定条件上。deeptutor/runtime/launcher.py:486-492_packaged_web_dir()import deeptutor_web导入成功也不算数——它接着检查该目录下有没有 server.js,没有就返回 None。也就是说,判据不是”包在不在”,而是”包里有没有被填过”。源码检出下这个包永远导得进来、永远返回 None,于是走到 _source_web_dir(home)launcher.py:565-570):依次看 home/webPACKAGE_ROOT/web,取第一个含 package.json 的。

这个设计的直接后果是:如果你把 deeptutor_web/ 手工塞了几个文件进去但没有 server.js,它仍然会被跳过;反过来,web/ 里如果 package.json 丢了,两条路就都断了。排查”前端起不来”时,这两处是判定走哪条支路的直接依据。

二、打包前端为什么还要复制一份

打包路径上还有一步容易被忽略。Next.js 的 NEXT_PUBLIC_* 变量是构建期内联进 bundle 的,发行包里的前端已经把值烧死了。launcher 的处理是:把整包复制到可写的运行缓存 data/user/runtime/web,再在副本里替换两个占位符 __NEXT_PUBLIC_API_BASE_PLACEHOLDER____NEXT_PUBLIC_AUTH_ENABLED_PLACEHOLDER__launcher.py:31502-553537-543)。

另外一个常量值得单记:源码下的生产构建输出目录是 SOURCE_PRODUCTION_DIST_DIR = ".next-deeptutor",与开发用的 .next 分开(launcher.py:32web/next.config.js:94-98)。注释给的理由是否则两条命令会互相作废对方的缓存。

三、源码检出下,它会在你机器上跑 npm

这一点必须说明白:launcher.py:571-590_ensure_web_dependencies() 会在 web/node_modules 不存在时自动执行 npm——有 lockfile 用 npm ci,否则 npm install,注释里引用了 issue #709,具体缘由见该函数的 docstring。

换句话说,你敲的是一条 Python 命令,实际发生的是本机的一次外部程序执行与一轮网络依赖安装。我们采集时本地就不存在 web/node_modulesls -d web/node_modules 返回不存在),也就是说这条分支在一个干净检出上必然会触发。是否接受这个行为、在什么网络环境下跑,请自行评估。

同一条线上还有一处相关的:web/app/(utility)/settings 下的 agents 分组包含 claude-codecodexgeminikimimimoopencode 六个子页。这类对接的具体机制不在本文核对范围(我们另有一篇专门讲它与本机 agent CLI 的关系),但你至少要知道这条线是存在的。

四、web/ 的技术栈:从 package.json

包名是 opentutor-webversion 1.0.0private: trueweb/package.json:2-4)。目录叫 web、Python 侧的包叫 deeptutor_web、npm 包名叫 opentutor-web——三个名字各不相同,找东西时按文件路径找,别按名字猜。

分类依赖与版本
框架next ^16.2.3react ^19.0.0react-dom ^19.0.0、TypeScript ^5
样式tailwindcss ^3.4.17postcss ^8autoprefixertailwind-merge ^3.4.0clsx ^2.1.1
内容渲染react-markdown ^10.1.0remark-gfm ^4.0.0remark-math ^6.0.0rehype-katex ^7.0.1rehype-raw ^7.0.0react-syntax-highlighter ^16.1.1
图表图形chart.js ^4.5.1 + react-chartjs-2 ^5.3.1mermaid ^11.14.0cytoscape ^3.33.1framer-motion ^12.24.0lucide-react ^0.562.0
文档导出docx-preview ^0.3.7exceljs ^4.4.0jspdf ^4.2.0html2canvas ^1.4.1
i18n / 测试i18next ^25.8.0 + react-i18next ^16.5.3@playwright/test ^1.53.2

这张表怎么读:remark-math + rehype-katex 说明公式渲染在前端做;docx-preview / exceljs / jspdf / html2canvas 四件一起出现,说明导出这件事也压在浏览器端。测试侧除 Playwright(npm run audit--project=ui-audit)外还有一套自研 node 跑法 test:nodescripts/run-node-tests.mjs

next.config.js 里有四处值得单看:

  1. output: "standalone"transpilePackages: ["mermaid"],以及 experimental.proxyClientMaxBodySize = 210*1024*1024web/next.config.js:108-120)。注释解释这个 210MB 是对着后端最大 200MB 上传(对应 DocumentValidator.MAX_FILE_SIZE)留的余量,因为 proxy 默认的 10MB 会静默截断。碰到大文件上传失败又没有报错,这是第一个该看的数字。
  2. 版本号是用正则读 Python 文件拿到的:解析 deeptutor/__version__.py 后注入 NEXT_PUBLIC_APP_VERSIONweb/next.config.js:78-106),注释说明这样是为了避免 JS 构建过程去执行 Python。
  3. allowedDevOrigins 会动态探测本机非回环 IPv4(web/next.config.js:130-142)。注释写的是:Next 16 不放行的话,跨 host 访问会出现「SSR HTML 渲染出来但 React 事件与 effect 永不挂载」这种半死不活的状态。
  4. webpack 与 turbopack 两边都把 cytoscape 别名指向 CJS 构建(web/next.config.js:144-163),注释说是为了修 mermaid 那条 cytoscape 依赖。

规模上有两组口径不同的数字,别混着看。一组是 web/ 下排除构建产物与 node_modules 后的 .ts/.tsx 文件数,共 465 个;另一组是各子目录的文件总数(不限 .ts/.tsx):components 185、lib 96、app 95、tests 61、hooks 15、scripts 8、context 5、features 3、types 1。后一组不是前一组的分解,两者用的 find 条件不一样,相加对不上是正常的。页面侧 find web/app -name 'page.tsx' 是 60 个,layout.tsx 9 个。60 个页面里 settings 一组就占 27 页(/settings 加 26 个子页)、space 占 9 页、memory 占 8 页——这三块加起来 44 页,是页面数的大头。

五、前端怎么找到后端:bundle 是 URL 无关的

这是本文第二处反直觉的地方。前端 bundle 里没有后端地址:浏览器一律请求前端自身 origin 下的 /api/.../api/.../wsweb/lib/api.ts:1-38 里的 apiUrl()wsUrl() 都是原样返回路径的 pass-through,真正的改写发生在 web/proxy.ts 这个 Next.js middleware 里。

  • 后端基址取自 DEEPTUTOR_API_BASE_URL,兜底 http://127.0.0.1:8001web/proxy.ts:12-21)。注释特意说明必须写 IPv4 字面量而不是 localhost:双栈主机会先解析到 ::1,而 uvicorn 绑的是 0.0.0.0,只覆盖 IPv4。
  • 改写判定极简:pathname.startsWith("/api/") || pathname.startsWith("/ws/")web/lib/proxy-policy.ts:22-24)。
  • Codex OAuth 回调是单独一条改写规则:前端 /auth/callback → 后端 /api/v1/auth/openai-codex/callbackweb/lib/proxy-policy.ts:11-16web/proxy.ts:44-49)。
  • middleware 的 matcher 排除 _next/static_next/imagefavicon.icoweb/proxy.ts:74-80)。

鉴权这块有两个细节要分清楚,免得排查时找错层:

其一,middleware 那道 gate 是不验签的。 它对 cookie 的校验只是前线快速判断,真正的验证仍由后端在每次 API 调用时做(web/lib/proxy-policy.ts:48-74)。这道 gate 的四态划分与豁免清单我们另有一篇专讲鉴权的文章展开,本文只取这条结论:在 middleware 这层被放行,不等于后端认了你。

其二,“auth 是否开启”不走构建期 env。 DEEPTUTOR_AUTH_ENABLED 没有 NEXT_PUBLIC_ 前缀,不会被内联,浏览器 bundle 看不到它;实际由 fetchAuthStatus() 在运行时向后端问,拿到后调 setRuntimeAuthEnabled(),默认值是 falseweb/lib/api.ts:52-66)。注释给的理由是避免默认无鉴权的部署被偶发 401 弹去登录页。这跟本节开头那件事是同一个取向:后端地址与鉴权开关都不进 bundle,前者交给 middleware 改写,后者交给运行时询问。

调用面的规模:apiUrl("/api... 这种形式的调用点在 web/libweb/componentsweb/appweb/features 四处共 184 个;用到 wsUrl( 的文件只有 6 个——web/lib/api.tsweb/lib/unified-ws.tsweb/lib/book-api.tsweb/lib/quiz-judge.tsweb/hooks/useKnowledgeProgress.tsweb/components/partners/PartnerChat.tsx。要摸清 WebSocket 这条线,读这 6 个文件就够,不用在 web/components 的 185 个文件里翻。

六、前端只有两种语言

web/i18n/init.ts:10-1728-42export type AppLanguage = "en" | "zh"normalizeLanguagezh/cn/chinese 归一到 zh,其余一律 en,fallback 是 en。这与后端 deeptutor/i18n/metadata_i18n.py 里只有 enzh 两个语言键是对上的。

locale 文件只有 4 个:web/locales/{en,zh}/{app,common}.json。我们用 Python 递归数叶子节点,app.json 两边各 2930 个键、common.json 两边各 6 个键,en 与 zh 数量完全一致。中文包是懒加载的——ensureLanguage("zh") 时才 import("@/locales/zh/app.json")addResourceBundleweb/i18n/init.ts:48-54)。仓库自带 i18n 校验脚本 i18n:parity / i18n:audit / i18n:audit:strict / i18n:checkweb/package.json 的 scripts 段)。

要加第三种语言的话,光丢一份 JSON 不够:AppLanguage 这个联合类型与 normalizeLanguage 的归一逻辑都得改,后端那侧的语言键也是另一套。这里只陈述改动会牵动哪几处,具体怎么改超出我们核对范围。

七、一处可核实的不一致

.gitignore:79 写着忽略 web/.next-deeptutor/;而 git ls-files web/.next-deeptutor | wc -l 返回 4322,git ls-files -s web/.next-deeptutor/BUILD_ID 也能返回一条索引项。两者不一致——.gitignore 只对未跟踪文件生效,已进索引的文件不受它约束,所以这个构建产物目录目前是被跟踪的状态。

我们本地是 depth=1 的 shallow clone,只有一个提交,无法通过历史判断该目录是何时、以何种方式进入索引的。按纪律,说完差异就停:不推断原因,也不据此评价什么。

顺带记一处文档侧的覆盖缺口:AGENTS.md 的 Key Files 表(AGENTS.md:101-118)里没有 web/proxy.ts。这个文件是前后端之间唯一的改写层,看那张表入门会漏掉它。

八、你可以照着核的六步

  1. ls -a deeptutor_web,确认源码检出下只有 __init__.py;再读它的 docstring 第 1-6 行。
  2. 打开 deeptutor/runtime/launcher.py:486-492,确认打包前端的判据是 server.js 存在而非包能导入;再看 565-570 的回退顺序。
  3. 打开 launcher.py:571-590,确认自动执行 npm ci / npm install 的那段,以及它的触发条件是 node_modules 不存在。
  4. 打开 web/next.config.js:108-120,把 210MB、后端 200MB、proxy 默认 10MB 这三个数字放在一起看。
  5. 打开 web/lib/proxy-policy.ts:22-24web/proxy.ts:12-21,确认改写规则与 127.0.0.1:8001 这个兜底基址。
  6. 打开 web/lib/api.ts:52-66,确认 auth 开关是运行时向后端问来的、默认值是 false,而不是构建期内联进 bundle 的。

需要说明的边界:本文只读了 web/lib/api.tsweb/lib/proxy-policy.tsweb/i18n/init.tsweb/next.config.jsweb/package.jsonlauncher.py 中与 web 目录解析、npm 安装相关的约一百二十行;web/components 的 185 个文件、web/lib 96 个文件里除 api.tsproxy-policy.ts 之外的部分、web/tests 的 61 个文件我们都没有读,因此”某个页面具体渲染什么""测试覆盖了什么”本文一律不下结论。web/.next-deeptutor/ 下的 4322 个文件是构建产物,只报存在与数量,内容未读。文中出现的所有默认值都是源码中的默认配置,不是运行结果的保证。


本文依据 DeepTutor 官方仓库(github.com/HKUDS/DeepTutor)的 README、AGENTS.mdpyproject.tomldeeptutor/ 下的源码整理,核对日 2026-08-10,对应仓库快照 456f9c2(版本 1.5.11)。 本文内容为仓库源码与文档口径,我们没有安装、部署或运行过该项目,也没有调用过其中任何一个模型 API, 因此不涉及生成质量、响应速度与教学效果的任何描述。 参数与默认值随版本变动,请以仓库最新代码与 --help 的实际输出为准。 安全相关做法请结合自身环境评估,本文不构成安全方案建议。

想系统学会用 AI?报名体系课或加入会员,照着学、照着用。