技术蓝图
August 26, 2026 · View on GitHub
🧭 ← 返回 README · 部署(模式 A):deployment · 部署(模式 B):k8s-deployment
多租户托管平台,让用户通过域名安全访问自己的 DeepSeek Harness(DSH)实例。本文是权威技术设计;实现与本文冲突时以本文为准,并同步回改。
1. 运行拓扑
用户浏览器
├─(HTTPS)─> dsh.<域名> → 编排服务 Fastify(认证/管理/桌面/域名,/api/*)
└─(HTTPS)─> <用户名>.dsh.<域名> → 编排服务按 Host 头路由到该用户 DSH(HTTP + WebSocket)
- 主域
dsh.<域名>:编排服务自己的登录 / 管理台 / 桌面 / API。 - 每用户子域
<用户名>.dsh.<域名>:编排服务按 Host 头解析用户名 → 校验会话 cookie → 反向代理到该用户 DSH 的127.0.0.1:<动态端口>(HTTP + WebSocket 隧道)。 - 每用户 DSH 只绑回环端口、不直接暴露公网;端口表随 spawn/respawn 即时更新,nginx 用通配
*.dsh.<域名>透传即可,无需每次 reload。 - 编排服务是独立 Node 进程,
child_process.spawn('dsh --profile web --host 127.0.0.1 --port <随机>', ...)拉起每用户 DSH(主 + 按需守护)。
2. 打包与启动
- 主入口:独立
dsh-server-loginbin(node lib/cli.js)直接跑 Fastify + SQLite + 进程编排。 - 市场识别:根
package.json的dsh字段(plugin/kind/bundle.patch)+cordis.patch.yml;不 import 任何@deepseek-ai/*宿主包,peerDependencies 为空,规避宿主包遮蔽。 - cordis 入口
apply()是带守卫空操作:默认无副作用,装进任意 profile 都不起服务器。 - 产物型分发:提交
lib/(构建产物),prepare=npm run build供 git 安装自构建。
3. spawn 每用户 DSH
spawn(dshBinPath, ['--profile', 'web', '--patch', mainPatchPath, '--host', '127.0.0.1', '--port', String(port)], {
cwd: workspacePath, // 用户在桌面选的文件夹
env: { ...scrubEnv(process.env), HOME: workspacePath, DSH_HOME: homeDir, DEEPSEEK_API_KEY: userApiKey },
stdio: ['ignore', 'pipe', 'pipe'],
detached: false,
})
- env 擦除镜像 harness 的
scrubbedParentEnv/SENSITIVE_ENV_PATTERN思路:只向子进程显式注入已解析 key。 - 端口是 CLI flag(
--port),不是 env、不是 patch(踩坑记录见 troubleshooting.md「端口冲突」)。 - 进程树 teardown 自行实现:SIGTERM → grace → SIGKILL。
4. 双 DSH「共享对话 + 崩溃接管」
共享状态 = 每用户 $DSH_HOME 里的持久会话日志(append-only;session-persistence 落盘)。
- 主 DSH 独占实时会话并持续 append;绑定回环端口对外服务。
- 守护 DSH 是按需拉起的一次性 headless DSH(不常驻):主 DSH 崩溃时、或需执行 post-restart 命令时,编排服务才 spawn 一次;它与主 DSH 同 home/同 workspace,通过
loadStoredFrom(id, fromSeq)读同一日志,修复/resume 后退出。
崩溃接管闭环(主 DSH 崩溃时,编排服务拉起一次守护 DSH 并自动重启主 DSH):
- 诊断:读退出码 + stderr 尾部 + 会话日志尾部,判定崩溃点。
- 修复会话日志:
interruptedTurnClosers(packages/core/session/src/repair.ts)+session-persistence.load/commitRepair把中断 turn 合成tool/result/step/end/turn/end{interrupted},产出可恢复的合法转录。 - 修复根因:守护 DSH 以 agent 身份(对共享 workspace 有工具权限)修文件/配置、摘坏插件、杀卡死子进程。
- 接手会话:
ctx.sessionPersistence.prepare/load(或ctx.sessions.create({seed}))恢复修复后的日志,接续对话成为新主 DSH;随后可选重拉 fresh 主 DSH 并退回守护位。
计划内重启(装插件):主 DSH 退出前把「post-restart 自动命令」写成 JSON 落到 $DSH_HOME,守护 DSH 执行重启后命令。
关键澄清:「接手」= 顺序 failover(恢复同一持久日志续接对话),非两个活体同时驱动同一 turn——这是 harness 的 resume 语义,无需自建双向活体通道。
5. 数据模型(SQLite,migration v1)
- v1 使用:
users、sessions、workspaces、folder_plugins、dsh_instances、audit_log。 - 预留:
domains、credential_vault(references-not-secrets,密文引用不落明文)。
字段与约束见 src/db/schema.ts。
6. API 面
| 组 | 路由 | 脚手架状态 |
|---|---|---|
| Auth | POST /api/auth/register|login|logout、GET /api/auth/me | 已实现(P1) |
| Admin | GET /api/admin/users、POST /api/admin/users/:id/approve|disable|enable | 已实现(P1;disable 会同时删会话 + 停 DSH) |
| Desktop/FS | GET /api/desktop/tree、POST /api/fs/mkdir|upload|create | 已实现(P2;路径经词法 + 符号链接双重围栏) |
| 凭据库 | GET/POST /api/me/keys、POST /api/me/keys/:id/select、DELETE /api/me/keys/:id | 已实现(每用户命名密钥,AES-256-GCM 加密落库) |
| DSH | POST /api/dsh/launch|stop|restart、GET /api/dsh/status、GET /u/:slug/dsh/* | 已实现(P3/P5,HTTP 代理;main+watchdog 编排层) |
| Plugin | GET /api/plugins、POST /api/plugins/select | 已实现(P4) |
| Domain/nginx | GET/PUT /api/domain、POST /api/nginx/regen、GET /api/admin/domains、POST /api/admin/domains/:id/verify | 已实现(P6,管理员手动验证;ACME 待接入) |
| 静态 | GET /*(占位 SPA) | 已接 |
7. 安全模型
默认软隔离(每用户 $DSH_HOME + session cwd + 沙箱写隔离);Linux 生产建议开启账号级硬隔离(每用户 OS 账号),见 hard-isolation.md。两个兜底:
- 端口守卫(portGuard):iptables OUTPUT owner-match 规则,阻止同机其他账号直连各用户 DSH 的回环端口、绕过编排服务认证;不支持的环境下开启即拒绝启动。
- 路径围栏:所有文件面先做词法包含校验(
resolveWithinRoot),再逐段lstat拒绝符号链接分量,防止经工作区内的链接越出用户根。
8. 分阶段路线
P1–P7 已完成:登录审核、桌面/FS、单 DSH 启动、每文件夹插件、守护/双 DSH、域名/nginx、硬隔离。模式 B(K8s + Postgres + leader election + reconcile,每用户 Pod)已落地:清单见 deploy/,部署教程见 k8s-deployment.md。