news 2026/9/9 20:12:24

DeepTutor v1.4.10 解读:个人头像与资料页、单端口 rootless 容器化与多用户 MCP 工具默认拒绝

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
DeepTutor v1.4.10 解读:个人头像与资料页、单端口 rootless 容器化与多用户 MCP 工具默认拒绝

DeepTutor v1.4.10 解读:个人头像与资料页、单端口 rootless 容器化与多用户 MCP 工具默认拒绝

【免费下载链接】DeepTutorDeepTutor: Lifelong Personalized Tutoring. https://deeptutor.info/.项目地址: https://gitcode.com/GitHub_Trending/dee/DeepTutor

导读

DeepTutor v1.4.10 是面向部署形态与账号体系的一次「收尾型」版本:为每个已登录用户提供自助 Profile 页面与头像、重构容器发布链路使整个应用可以以 rootless + 只读根文件系统的方式在单端口(:3782)背后运行、并在多用户模式下把 MCP 工具访问收紧为「默认拒绝」。本文以 v1.4.10 发布说明 为核心,结合 CONTAINERIZATION.md、compose.yaml、web/proxy.ts、deeptutor/api/main.py 与 deeptutor/multi_user/tool_access.py 等仓库实现,讲解每个改动的原理、配置方式与升级影响,帮助你理解并复现这一版本的部署与权限模型。

版本定位与影响范围

官方发布说明将该版本定位为 v1.4.9 的部署与账号跟进版本,核心包含四块能力:

  1. 带头像的自助Profile 个人资料页
  2. 一套可直接 rootless 运行的容器化方案(单端口、无构建期 API 地址、无启动期sed);
  3. 多用户模式下收紧的 MCP 工具访问(非管理员默认拒绝,管理员放行后再授予);
  4. 更安静的访问日志(例行200不再刷屏)。

发布说明特别强调:该版本对绝大多数用户是drop-in(可直接替换),唯一的行为变更局限在多用户模式下针对非管理员用户的 MCP 工具策略上。升级不涉及数据迁移,也不需要修改既有配置。

自助 Profile 页与头像体系

功能概览

从 v1.4.10 起,每个已登录用户都会获得一个自助Profile页面,支持:

  • 从内置图标列表中选择一个头像(可搭配颜色),或上传自定义图片作为头像;
  • 查看自己的账号信息与角色;
  • 直接登出账号。

头像会随后出现在侧边栏中;管理员账号会被一个「admin 环」标记出来,便于一眼识别身份。

仓库中的实现证据

头像在前端的落点可以在以下文件中追踪:

  • web/components/UserAvatar.tsx:头像组件,负责把头像标记(avatar marker)渲染为图标或图片,并在空标记时回退到确定性 fallback;
  • web/lib/avatar.ts:头像标记解析与确定性回退的纯逻辑(发布说明中提到的测试 web/tests/avatar.test.ts 与之一一对应);
  • web/lib/profile-api.ts:Profile 与头像上传相关的客户端 API。

后端与权限侧可在 tests/multi_user/test_profile_avatar.py 与 tests/multi_user/test_profile_router.py 看到围绕头像与 Profile 路由的契约测试。发布说明还包含两个细节修复:未知颜色导致的头像渲染异常会安全回退(不再出现"隐身头像"),以及auth-status 拉取被去重(避免同一会话重复请求)。

单端口容器化:从「构建期 URL」到「请求期代理」

为什么这是一个架构级变化

v1.4.10 之前,前端通常需要在构建时写入后端 API 地址(形如NEXT_PUBLIC_API_BASE占位符 + 启动时sed -i替换打包产物),浏览器因此要直连后端端口,容器需要暴露两个端口、并依赖宿主机上的多次改写。

v1.4.10 将「URL 知识」从前端产物中整体移除,改为请求期转发

  • 前端包不再包含任何NEXT_PUBLIC_API_BASE占位符,也没有对构建产物的sed改写;
  • web/lib/api.ts 中apiUrlwsUrl退化为一行透传——浏览器始终以前端 origin 下的相对路径发起请求(如:3782/api/.../ws/...),既有调用点无需修改即可继续工作;
  • web/proxy.ts 作为 Next.js 中间件在每次请求时拦截/api/*/ws/*,读取DEEPTUTOR_API_BASE_URL并将其 rewrite 到后端。

其效果正如发布说明所述:浏览器只与一个端口(:3782)通信,没有烧进 JS 包的 API URL,也没有启动期的sed,映射单个端口即可完成部署。

请求期 URL 解析链路

从源码看,URL 的解析链路是:

  1. 容器入口(entrypoint)每次启动时读取数据目录中的data/user/settings/system.json,导出环境变量DEEPTUTOR_API_BASE_URL
  2. web/proxy.ts 读取它作为 rewrite 目标,缺省回退到http://localhost:8001
    const API_BASE_URL = process.env.DEEPTUTOR_API_BASE_URL ?? "http://localhost:8001";
  3. 解析优先级为next_public_api_basenext_public_api_base_externalhttp://localhost:8001
  4. proxy.tsisBackendPath(pathname)命中的路径执行NextResponse.rewrite(new URL(pathname + search, API_BASE_URL))

因此只有前后端分离部署(后端跑在独立容器中)才需要显式设置 API 基址。在镜像内置后端的单容器场景下,浏览器只需对 UI 所在 origin 发起相对请求,由容器内中间件把请求转到localhost:8001

{ "next_public_api_base": "http://backend:8001" }

public_api_base作为兼容别名被接受,保存时会归一化为next_public_api_base_external。注意:因为代理发生在服务端DEEPTUTOR_API_BASE_URL是前端服务器访问后端所用的内网地址,浏览器永远看不到它。CORS 走的是前端origin而非 API URL,启用 auth 时在system.json中写入精确 origin 即可:

{ "cors_origins": ["https://deeptutor.example.com"] }

镜像与进程模型

按 CONTAINERIZATION.md 的说明,发布镜像ghcr.io/hkuds/deeptutor:latest在同一容器内通过supervisord同时托管 FastAPI 后端(:8001)与 Next.js 前端(:3782),底层是python:3.11-slim。全部状态(settings、工作区、memory、知识库、日志)收敛在一棵数据树/app/data中,bind-mount 这棵数据树到宿主机即可持久化

关键安全设计:

  • supervisord作为 PID 1 以 root 启动,但通过逐程序user=指令把后端、前端子进程降权到非 root 的deeptutor用户(UID 1000);
  • 在 rootless +userns_mode: keep-id下,该 UID 映射到宿主用户 UID,容器逃逸落地后也是宿主用户权限而非 root;
  • [supervisord]不带user=指令(早期设计固定user=root,在 rootless keep-id 下 PID 1 是非 root 宿主用户、缺少CAP_SETUID,supervisord 会以Can't drop privilege as nonroot user启动失败);
  • supervisord pidfile 写到/tmp/supervisord.pidmode=1777,任意配置下均可写),避免早期版本/var/run/supervisord.pid在 rootless 下因属主/权限导致的 cosmetic 错误。

三种容器部署形态

形态命令/文件特征
docker run单容器命令rootful、可写 rootfs、单个 bind mount/app/data
docker composedocker-compose.yml同镜像 + PocketBase 与 sandbox-runner 旁车
podman composecompose.yaml加固路径:rootless + 只读 rootfs + tmpfs 系统目录

最简单的docker run部署:

docker run --rm --name deeptutor \ -p 127.0.0.1:3782:3782 \ -v deeptutor-data:/app/data \ ghcr.io/hkuds/deeptutor:latest

只需发布3782;需要直连 API(curl/调试)时可额外-p 127.0.0.1:8001:8001。宿主机跑本地模型服务(Ollama/LM Studio/llama.cpp/vLLM/Lemonade)时,用--add-host=host.docker.internal:host-gateway并把 Base URL 指到http://host.docker.internal:<port>;Linux 下也可--network=host直接共享宿主网络。

rootless 加固路径:compose.yaml

v1.4.10 发布说明 所指的加固容器故事集中体现在 compose.yaml:

cp .env.example .env # 按需编辑 podman compose -f compose.yaml up -d podman compose -f compose.yaml ps podman compose -f compose.yaml logs -f deeptutor

可用podman info | grep -i rootless验证 rootless 是否生效。compose.yaml的关键取舍(文件头部注释有完整论证):

  • 每个服务read_only: true:rootfs 只读,唯一可写面是各服务声明的tmpfs:挂载与 bind mount 的./data
  • userns_mode: keep-id+ 所有卷挂载的:U后缀:容器 UID 1000 映射宿主 UID 1000,由 podman 自动 chown bind mount 目标;
  • tmpfs:覆盖系统运行目录/tmp(Python/Node 临时区 + supervisord pidfile)、/run/var/run(pidfile)、/var/log/root/home,容量给得较宽裕,可按需收紧;
  • 不使用命名卷:keep-id 下 podman 以 userns 映射的 root(UID 100000)自动创建命名卷,755 权限 + 属主错配会在首次 JSON 写入时报PermissionError;宿主自有的 bind mount 目录则干净可用;
  • 端口仅绑定回环127.0.0.1:,去掉前缀即暴露到所有网卡;
  • 不含 sandbox-runner 旁车docker-compose.yml形态额外提供在最小权限容器中运行模型生成代码的加固旁车;podman 形态下主应用回退到bwrap(镜像内含且为 Linux)或由system.jsonsandbox_allow_subprocess控制的受限子进程后端。

不依赖compose.yaml时,也可以用等价的podman run直驱(含:U--read-only与各 tmpfs 参数)。启动后podman exec deeptutor ps -o user,pid,comm会看到uvicorn/node子进程都以deeptutor(UID 1000)运行。

PocketBase 可选旁车与挂载修正

PocketBase 是可选 auth + storage 旁车:在data/user/settings/integrations.json中设置integrations.pocketbase_url = http://pocketbase:8090并启动pocketbase服务后,账号与会话即存入 PocketBase,否则回退到 SQLite 单用户布局。compose.yaml与修正后的 docker-compose.yml 把./data下三个子目录——/pb_data/pb_public/pb_hooks——bind mount 进容器。发布说明特别指出 v1.4.10 修复了此前docker-compose.yml中 PocketBase 挂载路径错误(旧示例挂在/pb/pb_data,首启会因只读 rootfs 报mkdir /pb_data: read-only file system)。对多用户部署而言,除非已接外部用户存储,建议保持pocketbase_url为空。

运行时配置入口

按 CONTAINERIZATION.md 的运行期配置章节,几乎所有可调项都位于数据树data/user/settings/下;容器入口每次启动都会 unset相关环境变量(BACKEND_PORTFRONTEND_PORTNEXT_PUBLIC_API_BASEAUTH_ENABLEDPOCKETBASE_URL等)再从 JSON 重新导出,因此正确做法是「改 JSON → 重启」,而不是用 compose 环境变量驱动:

文件用途
system.json前后端端口、public API base、CORS、SSL 校验、附件目录
auth.json可选 auth 开关、用户名、口令哈希、token/cookie 设置
integrations.json可选 PocketBase 与 sidecar 集成设置
model_catalog.jsonLLM / embedding / 搜索 provider 配置、API key、激活模型
interface.jsonUI 语言 / 主题 / 侧边栏偏好
main.yaml运行时行为默认值与路径注入
agents.yamlcapability/tool 的温度与 token 设置

新装最关心的两个键是system.json中的next_public_api_base(内网)/next_public_api_base_external(外部覆盖),以及backend_port/frontend_port(改动后需同步所有-p映射右侧或HOST_PORT_*环境变量)。项目根目录的.env文件被有意忽略为应用配置源,推荐用 WebSettings页面编辑这些 JSON/YAML。

多用户模式下 MCP 工具「默认拒绝」

变更语义

这是 v1.4.10唯一的行为变更,且被刻意限定在多用户模式下的非管理员范围内:

  • 非管理员用户无法再发现(discover)或加载(load)部署级 MCP 主机工具,直到管理员在对应账号的 grant 中显式授予具体工具名;
  • 管理员不受限
  • 单用户 / 无 auth 部署不受影响——它们的会话按管理员运行。

源码级原理

deeptutor/multi_user/tool_access.py 的模块 docstring 说明了设计动因:MCP 工具可以通过配置的 MCP 服务器代理宿主机能力,因此对非管理员真实用户而言,grant 中缺失 MCP 条目即默认拒绝

def allowed_mcp_tools() -> set[str] | None: """Whitelist of MCP (deferred) tool names. ``None`` means unrestricted and is reserved for administrators. Real non-admin users fail closed when the grant omits ``mcp_tools`` so a chat turn cannot discover or load deployment-wide MCP host tools until an admin explicitly grants the tool names. """ grant = _current_grant() if grant is None: return None value = grant.get("mcp_tools") if value is None: return set() return {str(name) for name in value}

对应的执行点(见 tool_access.py docstring 与 deeptutor/multi_user/grants.py):

  • allowed_optional_tools:turn 运行时对每轮的tools载荷做过滤(全 capability 的唯一收口点),tools router 对/settings/tools列表做同样过滤,保证 UI 与后端一致;
  • allowed_mcp_tools:chat 管线在构建 deferred-tool loader 之前先与调用方作用域的mcp_tools_filter求交集,被拒绝的 MCP 工具既不能列出也不能加载;
  • allowed_cli_apps:对已安装 CLI 应用采取同样的默认拒绝姿态(已安装应用会在沙箱内运行第三方代码)。

empty_grant(grants.py)中mcp_tools默认为None_normalize_tool_list会规范化enabled_tools/mcp_tools/cli_apps三个名单。因此多用户部署的运维动作是:在用户 grant 中显式写入具体 MCP 工具名,例如:

{ "mcp_tools": ["web_search", "file_read", "rag_query"] }

(实际工具名以你部署的 MCP 服务器暴露的 deferred tool 名为准。)管理员界面中的 grant 编辑器会与内置工具白名单并列呈现 MCP 工具授权,便于逐用户核对与配置可达范围。

更安静的日志:只留非 200

例行前端轮询(/settings/tools/knowledge/list等)会制造大量无意义的200日志。v1.4.10 在所有启动路径deeptutor start、launcher、Docker 入口)上关闭了 uvicorn 的逐请求 access log,并由单个中间件只暴露非200响应。

源码证据位于 deeptutor/api/main.py:deeptutor.accesslogger 配置了独立的 INFO stdout handler(避免被全局 WARNING 级 root handler 吞掉),中间件selective_access_logresponse.status_code != 200时才写一条 5 元组格式的访问日志。配合 deeptutor/api/run_server.py 的access_log=False与 deeptutor/runtime/launcher.py 的--no-access-log,所有启动路径行为一致;tests/api/test_selective_access_log.py 对该中间件的输出格式做了契约级验证。结果是:日志行数大幅下降,留下的都是真正值得关注的错误状态

升级指引与注意点

官方声明从 v1.4.9 升级为 drop-in:

pip install -U deeptutor

Docker 用户拉取ghcr.io/hkuds/deeptutor:latest即可。无迁移、无配置变更。唯一需要操作的情形:多用户部署下若非管理员用户依赖 MCP 工具,请逐用户授予具体工具名——在授权之前它们会保持「默认拒绝」。

v1.4.10 附带修复回顾:

  • 客户端 auth 状态改为经新代理在请求期解析,代理同时为上传统计请求体大小(头像图片、附件);
  • 未知颜色的头像渲染安全回退;auth-status 请求去重;
  • docker-compose.yml 中 PocketBase 挂载路径修正。

如果你想深入了解镜像的详细运行形态与故障排查(例如"页面能开但 Settings 报 Backend unreachable"、"命名卷下首次 JSON 写入 Permission denied"、"podman 宿主上 Cannot connect to the Docker daemon"等场景的成因与解法),可以直接阅读仓库根目录的 CONTAINERIZATION.md 及其 Troubleshooting 与 Security notes 章节。

【免费下载链接】DeepTutorDeepTutor: Lifelong Personalized Tutoring. https://deeptutor.info/.项目地址: https://gitcode.com/GitHub_Trending/dee/DeepTutor

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/9 20:12:08

WorkBuddy企业版落地指南:从个人工作台到团队协作自动化

1. 从“一个人一堆工具”到“一个团队一套系统” 先聊一个很现实的场景&#xff1a;作为一个开发者或者业务负责人&#xff0c;你本地可能装了十几个工具——待办清单、笔记软件、表格、IM、项目管理、文档库&#xff0c;再加上公司内部的各种后台系统。工具越多&#xff0c;信…

作者头像 李华
网站建设 2026/9/9 20:09:29

用COMSOL模拟锂枝晶生长:四种建模路径与工程实践指南

写锂枝晶仿真这几年&#xff0c;听到最多的需求就是&#xff1a;“能不能用COMSOL把枝晶长出来看看&#xff1f;”这个问题看着简单&#xff0c;真做起来却一点都不省心。锂枝晶涉及电化学沉积、离子传质、界面运动、力学损伤多个物理过程&#xff0c;不同阶段起主导作用的机制…

作者头像 李华
网站建设 2026/9/9 19:59:50

扩增子分析全流程解析:从16S/ITS到二三代测序与可视化

引言这年头做微生物组研究&#xff0c;离了扩增子测序几乎是寸步难行。16S和ITS这两个经典的标记基因&#xff0c;在过去十几年里撑起了肠道、土壤、水体、植物根际等无数微生态研究方向的基本盘。但恰恰是这个“基本盘”&#xff0c;这几年正在经历一轮非常明显的技术换挡&…

作者头像 李华