技术蓝图

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-login bin(node lib/cli.js)直接跑 Fastify + SQLite + 进程编排。
  • 市场识别:根 package.jsondsh 字段(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):

  1. 诊断:读退出码 + stderr 尾部 + 会话日志尾部,判定崩溃点。
  2. 修复会话日志interruptedTurnCloserspackages/core/session/src/repair.ts)+ session-persistence.load/commitRepair 把中断 turn 合成 tool/result/step/end/turn/end{interrupted},产出可恢复的合法转录。
  3. 修复根因:守护 DSH 以 agent 身份(对共享 workspace 有工具权限)修文件/配置、摘坏插件、杀卡死子进程。
  4. 接手会话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 使用:userssessionsworkspacesfolder_pluginsdsh_instancesaudit_log
  • 预留:domainscredential_vault(references-not-secrets,密文引用不落明文)。

字段与约束见 src/db/schema.ts

6. API 面

路由脚手架状态
AuthPOST /api/auth/register|login|logoutGET /api/auth/me已实现(P1)
AdminGET /api/admin/usersPOST /api/admin/users/:id/approve|disable|enable已实现(P1;disable 会同时删会话 + 停 DSH)
Desktop/FSGET /api/desktop/treePOST /api/fs/mkdir|upload|create已实现(P2;路径经词法 + 符号链接双重围栏)
凭据库GET/POST /api/me/keysPOST /api/me/keys/:id/selectDELETE /api/me/keys/:id已实现(每用户命名密钥,AES-256-GCM 加密落库)
DSHPOST /api/dsh/launch|stop|restartGET /api/dsh/statusGET /u/:slug/dsh/*已实现(P3/P5,HTTP 代理;main+watchdog 编排层)
PluginGET /api/pluginsPOST /api/plugins/select已实现(P4)
Domain/nginxGET/PUT /api/domainPOST /api/nginx/regenGET /api/admin/domainsPOST /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