DeepSeek Harness 服务化改造实践
DeepSeek Harness 服务化改造实践
摘要:DeepSeek Harness 原本更接近面向个人的开发工具。为了让团队成员只通过浏览器就能使用,同时保证账号、模型配置、会话和文件彼此隔离,我们在它前面增加了一层零依赖 Node.js 网关,并用 Docker 为每个用户运行独立实例。本文记录整个改造的架构、关键实现、踩坑过程与安全取舍。
一、为什么要做这次改造
直接把一个本地 AI Agent 工具暴露到服务器上并不难,真正困难的是把它变成一个可供多人长期使用的服务。
我们希望最终的使用方式足够简单:用户打开浏览器,登录自己的账号,进入独立工作区,选择平台统一提供的模型或添加自己的模型配置,完成任务后直接下载生成的文件。用户电脑上不需要安装 Node.js、Python、Docker,也不需要了解服务器目录结构。
这意味着改造不能只解决“页面能打开”,还必须同时处理下面几个问题:
- 多个用户如何认证,并被准确路由到各自的 DSH 实例?
- 用户会话、API Key、模型设置和生成文件如何避免串数据?
- DSH 的部分特权接口只信任回环地址,经过公网反向代理后如何继续工作?
- 平台统一模型与用户自定义模型如何共存?
- Agent 生成的文件怎样方便地交付给浏览器用户?
- 在受限或离线环境中,怎样让容器开箱即用?
我们的核心原则是:共享入口,不共享运行时;统一运维,不牺牲用户数据边界。
二、目标架构
最终架构由四层组成:TLS 反向代理、认证网关、用户实例和持久化数据卷。
公网只暴露 HTTPS 入口。网关负责登录、会话、CSRF、限流、反向代理、用户管理和文件交付;每个用户拥有独立的 DSH 进程、端口和数据目录。Docker 是推荐部署方式,同时保留 systemd 方案,方便已有环境平滑迁移。
三、第一步:在 DSH 前增加认证与路由网关
网关使用 Node.js 标准库实现,不额外引入 Web 框架。这样做不是为了追求代码最少,而是为了让镜像依赖和部署链路保持可控。
登录链路包含以下防护:
- 密码使用 scrypt 保存,并兼容旧版 APR1 记录;
- 会话由 HMAC 签名,Cookie 设置
HttpOnly、Secure和SameSite=Lax; - 登录表单采用双提交 Cookie 进行 CSRF 校验;
- 失败登录同时按 IP 和账号限流;
- 未登录的普通页面跳转登录页,API 请求直接返回 401。
用户登录成功后,会话中只保存经过签名的用户标识和过期时间。网关根据用户记录找到对应端口,再把 HTTP 和 WebSocket 流量转发到该用户实例。
这里有一个容易忽略的兼容问题:DSH 的 settings、credentials、agent preset 等接口对来源和 Host 有严格判断。如果把浏览器传来的公网 Host 原样转发,页面能打开,但某些设置操作会失败。
解决方式是在可信网关内部重写上游请求:
- 上游 Host 固定为 DSH 信任的回环地址;
- 去掉不应从浏览器透传的信任标记;
- 对外仍然只开放网关,不直接暴露用户实例端口。
本质上,网关承担了“公网身份边界”和“内部可信调用方”两种角色。
四、第二步:从共享实例改成每用户独立容器
仅在应用层给请求附加用户名,并不足以隔离一个具备文件读写和命令执行能力的 Agent。我们的方案是为每个用户创建独立 DSH 容器:
dsh-gateway
dsh-web-alice
dsh-web-bob
dsh-web-charlie
网关提供带 Bearer Token 的 Admin API,用于创建、查看、启动、停止、重启和删除用户;dsh-users.sh 再把 HTTP API 包装成运维命令。用户记录保存在 users.json,包含端口、家目录、密码哈希和 Key 配置状态。
4.1 为什么采用“一镜像、两角色”
同一个镜像通过 ROLE 环境变量承担两种职责:
ROLE=gateway:启动认证网关;ROLE=user:启动指定用户的 DSH Web 实例。
这样可以避免维护两套基础环境。升级 DSH、Python 依赖或文件助手后,只需构建一个镜像,再按需重建用户容器。
4.2 DSH 必须绑定回环地址带来的网络问题
DSH 为避免误暴露高权限能力,会拒绝直接绑定 0.0.0.0。如果网关和用户实例位于普通 bridge 网络的不同容器中,网关又无法访问用户容器内部的 127.0.0.1。
我们最终让用户容器加入网关容器的网络命名空间:
docker run \
--network container:dsh-gateway \
-e ROLE=user \
-e DSH_PORT=3101 \
...
每个用户实例仍然监听 127.0.0.1,但网关可以通过共享的回环网络访问它。不同用户使用 3101、3102 等不同端口避免冲突。
这个设计也带来明确的运维约束:重建网关后,旧用户容器仍引用原来的网络命名空间,不能只执行 docker restart。正确做法是删除并重新创建用户容器。批量脚本只重建当前运行中的用户,原本停止的账号保持停止状态。
五、第三步:把“有持久化”升级成“有隔离的持久化”
早期方案让网关和所有用户容器挂载同一个完整数据卷。数据确实不会随容器删除而丢失,但任意用户容器都能遍历其他用户目录,这只是持久化,不是隔离。
改造后,网关仍挂载完整命名卷,负责用户生命周期管理;用户容器只挂载自己的子目录:
dsh-gateway-data
└── users
├── alice ← 只挂载给 alice
├── bob ← 只挂载给 bob
└── charlie ← 只挂载给 charlie
容器创建时使用 volume subpath,把 users/alice 单独挂载到 Alice 的 DSH_HOME。因此即使 Agent 在容器内遍历挂载点,也无法看到兄弟目录。
同时,入口脚本必须显式导出:
export DSH_HOME=/data/users/alice
export HOME=/data/users/alice
我们曾经遇到一个很典型的问题:数据卷挂载正确,但 DSH 仍按容器默认 HOME 把文件写到 /root/workspace,而文件交付页面读取的是 /data/users/alice/workspace。结果是 Agent 明明生成了文件,用户却看不到。把 DSH_HOME 和 HOME 同时钉到用户目录后,运行时与交付视图才真正一致。
对于 legacy/systemd 模式,则由独立 OS 账号、0700 家目录和 sudoers 白名单助手提供类似边界。两种模式沿用相同的用户数据结构,降低迁移成本。
六、第四步:重新设计模型 Provider 管理
模型配置是这次改造中反复调整最多的部分。
最初的思路是设置 Provider 白名单:普通用户只能看到平台配置的两个 Provider,其他 Provider 在网关响应中被过滤;用户提交非白名单 Provider 时,网关直接返回 403。这种做法控制力强,但有两个明显问题:
- 它把“界面精简”变成了“能力封锁”;
- 用户明明拥有独立容器和独立配置文件,却无法维护自己的 Provider。
最终我们把模型配置拆成两个层次:
- 系统托管 Provider:由运维人员通过环境变量统一配置,在每次用户容器启动时同步;
- 用户自定义 Provider:保存在用户自己的
settings.yaml中,网关不阻止读写,也不会与其他用户共享。
系统配置采用模板渲染和 YAML 结构化合并,而不是简单追加文本:
for (const id of Object.keys(templateProviders)) {
current['llm-pi-ai'].providers[id] = templateProviders[id];
}
这段逻辑有三个重要性质:
- 幂等:重复启动不会生成重复配置;
- 有界覆盖:只覆盖模板中由系统托管的同名 Provider;
- 保留用户配置:模板之外的 Provider 原样保留。
采用结构化合并还解决了一个实际故障:直接向 YAML 末尾追加配置可能产生重复的 llm-pi-ai 键,js-yaml 会报 duplicate-key,最终导致整个 settings 文件加载失败。
对于 DSH 自带但平台暂时不想展示的 Provider,我们只在列表和设置查询响应中做“展示隐藏”,不拦截用户写入。也就是说,DSH_HIDDEN_PROVIDERS 是 UI 降噪开关,不是安全策略。真正的数据隔离依赖的是独立实例和独立目录,而不是对 JSON 响应做字符串过滤。
默认模型也采用温和迁移策略:仅当用户尚未设置默认模型,或仍使用旧版默认值时,才切换到新的系统默认模型,避免覆盖用户已经做出的选择。
七、第五步:让 Agent 生成的文件真正可交付
AI Agent 的价值经常落在最终产物上,例如 Excel、Word、PDF、图片或代码压缩包。如果用户还要登录服务器、寻找工作目录并用 SCP 下载,Web 化只完成了一半。
因此我们在 DSH 页面中注入了“交付文件”和“上传文件”入口,并通过同源抽屉提供:
- 目录浏览;
- 中文文件名下载;
- 多文件上传;
- 默认 100 MB 的单次上传限制;
- 自动定位当前会话工作目录。
自动定位并不是靠猜。网关会观察 session.history 与 session.list 的代理响应,关联当前会话及其 cwd,并把最后一次有效目录持久化。用户打开文件抽屉时,优先展示当前对话的工作目录;目录失效时再回退到 workspace。
文件接口还必须防止路径穿越。实现中同时检查用户 home 与目标路径的 realpath,拒绝越界路径,并禁止下载隐藏文件,尤其是 .credentials.yaml。Docker 模式下,助手运行在网关容器中;legacy 模式则通过固定路径的 sudoers 白名单执行,网关进程本身不直接获得用户目录的任意访问权。
八、第六步:把常用工具链做进镜像
一个能聊天的 Agent 和一个能完成工作的 Agent,差别往往在运行环境。
我们在镜像中预装了几类常用能力:
- Python Web 与脚本工具:FastAPI、HTTP 客户端、Pydantic;
- 数据处理:NumPy、Pandas、SQLAlchemy;
- Office 与文档:openpyxl、python-docx、python-pptx、PyMuPDF、pypdf;
- 图片处理:Pillow、OpenCV;
- 对象存储和向量数据库客户端:MinIO、PyMilvus;
- Node 包管理:固定版本的 pnpm。
依赖在镜像构建阶段一次性安装,而不是等每个用户容器启动后再联网下载。这样既缩短了用户实例启动时间,也让不同用户获得一致的工具版本。
对于离线部署,我们额外保留镜像 tar 包和独立 compose 配置。交付环境只需加载镜像、填写脱敏后的环境变量并启动服务,不依赖现场访问 npm 或 PyPI。
九、运维流程也要随架构一起改
多用户容器化之后,“改一个环境变量然后重启服务”已经不够了。以系统模型配置变更为例,完整流程是:
cd docker
docker compose build
docker compose up -d --force-recreate dsh-gateway
bash debug/restart_all.sh
为什么要重新构建?因为 Provider 模板、入口脚本和 Python/Node 依赖都属于镜像内容。
为什么要重建用户容器?因为环境变量只在容器创建时注入,普通 restart 不会刷新它们;同时用户容器共享网关网络命名空间,网关被替换后必须重新关联。
为什么只处理运行中的用户?因为“停止”本身可能是管理员有意设置的状态,批量升级不应该擅自改变业务状态。
用户数据位于命名卷中,重建容器不会删除会话、工作区或用户设置。但在生产环境执行升级前,仍应备份:
/data/gateway/users.json;/data/users/<name>/;- 网关会话密钥及部署侧环境配置。
十、测试与验证
这类改造不能只验证首页能否打开。当前项目的网关冒烟测试覆盖了:
- 健康检查;
- 登录页渲染;
- 错误密码拒绝;
- 正确登录与跳转;
- 登录后反向代理;
- 用户自定义 Provider 可见;
- 指定内置 Provider 被隐藏;
- 用户 Provider 写入能到达自己的 DSH;
- 未认证 API 返回 401。
本文整理时,Node.js 语法检查全部通过,网关冒烟测试结果为 9 passed, 0 failed。
生产上线前还应增加三类验证:
- 跨用户隔离测试:在 Alice 容器内尝试访问 Bob 的目录、配置和端口;
- 生命周期测试:创建、停止、重建、删除用户,并验证数据保留或清理符合预期;
- 升级回滚测试:备份命名卷,更新镜像和系统 Provider,再验证旧会话、用户自定义配置与生成文件不丢失。
十一、几个值得记录的设计取舍
11.1 网关挂载 Docker Socket 是高权限能力
即使以只读方式挂载 /var/run/docker.sock,能够调用 Docker API 的进程仍然接近宿主机 root 权限。真正的控制点是:Admin API 不对公网直出、使用高强度 Token、Token 不传入用户容器,并限制网关代码可执行的 Docker 子命令。
如果未来对安全边界要求更高,可以把容器管理拆成独立的最小权限控制服务,甚至迁移到 Kubernetes Operator,而不是继续让业务网关直接接触 Docker Socket。
11.2 共享网络命名空间解决了兼容性,也降低了网络隔离
该方案很好地兼容了 DSH 必须监听回环地址的限制,但用户进程理论上能够访问同一网络命名空间中的其他回环服务。因此不能把 Admin Token、数据库口令等管理凭据注入用户容器,也应继续收紧内部管理接口的认证。
11.3 “隐藏”与“禁止”必须明确区分
Provider 展示隐藏只是产品体验策略,不能被描述成安全隔离。安全边界应建立在进程、目录、凭据与权限上。把 UI 过滤误当成安全控制,是本次迭代中最重要的反思之一。
十二、改造后的收益
改造完成后,DeepSeek Harness 从一个需要用户自行安装和维护的工具,变成了可由团队统一运营的浏览器工作台:
- 用户侧只需要浏览器;
- 平台统一提供模型和工具链;
- 用户仍可维护自己的模型配置;
- 会话、凭据和文件按用户隔离;
- 生成文件可以直接上传、浏览和下载;
- 用户实例可独立启停、重建和排障;
- Docker、legacy 和离线交付路径都得到保留。
更重要的是,这次改造没有把“多用户”简单理解为增加一张用户表。对于具备代码执行和文件访问能力的 AI Agent,多用户化的本质是重新划分身份、进程、网络、存储、模型配置与运维权限的边界。
结语
把开发者工具搬到服务器上很容易,把它做成安全、可维护、真正能交付成果的多人服务则完全是另一件事。
这套方案目前仍有继续演进的空间,例如引入更严格的资源配额、审计日志、容器级 CPU/内存限制、自动化备份恢复,以及把 Docker 管理权限从网关中进一步拆分。但通过“认证网关 + 每用户实例 + 子目录挂载 + 分层 Provider + 文件交付”的组合,我们已经建立了一条清晰且可持续演进的服务化路径。
本文方案参考了 https://github.com/AnkoCD/dsh-server-deployment
使用openai Gpt-5.6sol进行改造
个人博客地址:april note

浙公网安备 33010602011771号