1) dsh-server-docs/ 从工作区(原 E:\...\aliyun-dsh-server\dsh-server-docs)**整体并入本仓**,
保留目录名 ⇒ 仓库内 dsh-server-docs/... 的相对引用天然继续有效;旧目录(含其 .git)已归档到
工作区 _中间产物_待清理/,未随本提交带入。
2) .gitattributes:新增 `dsh-server-docs/** -text` —— 原文档库是 `* -text` + autocrlf=false,
必须保持纯 LF,否则会被本仓的 CRLF 规则翻掉。
3) 活引用里的绝对路径已全部改到新位置(docs 的 INDEX / README / scripts / skills + 用户级 skills
+ ~/.workbuddy/settings.json 的 hooks);历史档案(04-调整方案/、archive/)按「只增不改」未动。
⚠️ hooks 路径改动需「完全重启会话」才生效(配置是会话启动快照)。
4) 交接单/T08:新增 §16「生产整体切换执行记录」(形态 / 落地动作 / **4 个只有真上线才暴露的真 bug** /
验收证据 / 回滚命令 / 残留项);台账 T08 行 → 已完成并归档;03-路线图 §二 登记 T08 收尾项。
5) 统一称谓:**「本机」只指跑 WorkBuddy 的开发机**,47 / 106 一律写「远程服务器」。
15 KiB
dsh(DeepSeek Harness)多用户部署与配置改造方案
⚠️ 历史文档(2026-09-08 成稿):本文记录当时的规划与架构。当前部署以
DEPLOY-本部署.md为准;文中「Docker 容器 /127.0.0.1:8900/ 旧域名dsh.alotbuy.com」等已被 2026-09-08~11 的架构切换取代(见档案 05 / 22)。SSH 端口为 22(非文中笔误的 32022)。
| 项 | 值 |
|---|---|
| 版本 | v1.0(2026-09-08) |
| 适用对象 | 服务器 47.77.182.89(root SSH 端口 22) |
| 软件 | @deepseek-ai/dsh 0.1.2-rc.1(镜像 dsh-web:0.1.2) |
| 状态 | P0 迁移已执行(2026-09-08 17:30);域名接入完成(2026-09-08 18:00);架构切换为 dshs 多租户 + account 硬隔离(2026-09-08 18:30) |
| 文档位置 | 本机 D:\AI技能\aliyun-work-space\dsh-server-docs\;服务器 /opt/dsh/docs/ |
一、背景与目标
dsh Web UI 已单实例部署并跑通(外网 http://47.77.182.89/,模型对话正常)。需要从「单人单实例」升级为「公司多人使用」形态。
目标(5 项):
- 多用户使用:多个编导/账号运营各自登录使用;
- 数据持久化:会话、配置、凭据、工作区不随容器重建丢失;
- 跨用户文件安全:用户之间互相看不到对方的数据与工作区(严格隔离);
- 便于使用/复制/恢复:一个用户 = 一个主目录,备份恢复按目录粒度一键完成;
- 登录方式可扩展:前期用账号密码(注册审核制),后期可按需接入飞书 OAuth。
二、现状盘点(2026-09-08 服务器实测)
/opt/dsh/
├── backups/20260908_initial/ # 旧备份(已归档)
├── docs/dsh-improvement-plan.md # 本文档
├── users/main/.dsh/ # ★ 迁移后:容器 /root/.dsh(原 data/)
├── users/main/workspaces/ # ★ 迁移后:容器 /root/workspaces(工作区卷,新约定)
├── .env # 系统模型 key(--env-file)
├── backup.sh / restore.sh # 备份恢复脚本
└── dsh-web-0.1.2.tar # 镜像 tar(140MB)
容器 dsh-web:Up,--network host,--restart always
挂载:/opt/dsh/users/main/.dsh → /root/.dsh
/opt/dsh/users/main/workspaces → /root/workspaces
环境:DEEPSEEK_API_KEY(--env-file /opt/dsh/.env)
端口:容器内 127.0.0.1:8900 → nginx 反代 80(Host/Origin 伪装 127.0.0.1)
访问:token 认证(docker logs 取)→ dsh-auth cookie(30 天)
已知问题:容器内工作区 /root/maiyamcn 已丢失(重建容器后被清掉,注册表残留失效路径)——工作区必须卷化持久化。迁移后此问题已解决,但 storages/workspace.json 中旧路径 /root/maiyamcn 记录仍在,需在 Web UI 删除该失效工作区入口。
三、总体架构(目标态)
核心原则:
- 一个用户 = 一个主目录 + 一个实例:
/opt/dsh/users/<uid>/,备份/复制/恢复只对这一个目录操作; - 严格隔离:每用户独立实例(容器),独立
~/.dsh与工作区,互相不可见; - 系统标准层(公共模型/插件/技能)只读共享,用户增量放在个人层,同名覆盖系统层(dsh 原生分层机制,无需自研);
- 配置分层:dsh 插件 bundle → profile 层 → home 级用户覆盖层 →
--patch,天然支持「公司统一 + 个人覆盖」。
目录布局(目标态)
/opt/dsh/
├── .env # 系统级模型 key(DEEPSEEK_API_KEY),容器 --env-file 注入
├── std/ # ★ 系统标准层(只读,管理员维护)
│ ├── agents/skills/ # → 容器 /root/.agents/skills(rank400 兜底标准技能库)
│ │ └── mcn-video-script/ # 公司标准技能(SKILL.md + references,只读)
│ └── profile-skel/ # 新用户 profile 种子(含公共插件 bundles 声明)
├── users/ # ★ 每用户 = 一个主目录(实例隔离边界)
│ ├── main/ # ─ 管理员实例(由现有 data/ 迁移而来)─
│ │ ├── .dsh/ # → 容器 /root/.dsh(DSH_HOME)
│ │ │ ├── profiles/web/
│ │ │ │ ├── dsh.profile # bundles 有序清单(官方 + 公共插件)
│ │ │ │ ├── cordis.patch.yml # profile 层配置
│ │ │ │ └── node_modules/ # 个人插件落点(dsh plugin add,只影响本用户)
│ │ │ ├── cordis.patch.yml # ★ home 级用户覆盖层(优先级高于 profile 层)
│ │ │ ├── .credentials.yaml # ★ 个人模型 key(用户在 Models 页填写)
│ │ │ ├── settings.yaml # 用户全局设置
│ │ │ ├── skills/ # ★ 个人技能 rank300(可覆盖系统标准同名技能)
│ │ │ ├── sessions/ # 会话数据
│ │ │ └── storages/ # 存储数据
│ │ └── workspaces/ # → 容器 /root/workspaces
│ │ ├── maiyamcn/ # 工作区项目(=容器内 workspace 根)
│ │ │ ├── .dsh/skills/ # 项目级技能 rank100(最高优先,随项目分发)
│ │ │ ├── .agents/skills/ # 项目级技能备选 rank200
│ │ │ ├── AGENTS.md # 项目指令
│ │ │ └── ... # 项目文件/素材/脚本
│ │ └── <其他项目>/
│ ├── zhangsan/ # ─ 编导 A(与 main 完全同构)─
│ │ ├── .dsh/
│ │ └── workspaces/
│ └── lisi/ # ─ 编导 B ─
├── backups/ # 备份产物(backup.sh 打包 users/ → 时间戳 tar.gz)
│ └── 20260908_initial/ # 旧备份归档
├── backup.sh # 一键备份
└── restore.sh # 一键恢复
运行拓扑(多用户)
镜像 dsh-web:0.1.2(官方 bundle + 公共插件 + std 标准技能内置/挂载)
├── dsh-web-main --network host → 8900 # 管理员
├── dsh-web-zhangsan --network host → 8901 # 编导 A
└── dsh-web-lisi --network host → 8902 # 编导 B
nginx 外网按子域名/路径分流到 127.0.0.1:890x(沿用现有 Host/Origin 伪装模板)
四、dsh 分层机制要点(方案依据)
dsh 配置与插件天然分层,以下机制是方案成立的前提(无需自研):
-
配置/插件叠加顺序(后层覆盖前层):
插件 bundle 默认(官方/系统) → profile 层 $DSH_HOME/profiles/web/cordis.patch.yml → 用户层 $DSH_HOME/cordis.patch.yml → 命令行 --patch(最高) -
SKILL 发现目录(rank 越小优先级越高,同名高优先覆盖低优先):
rank 目录 性质 100 工作区根 /.dsh/skills/项目级(随项目分发) 200 工作区根 /.agents/skills/项目级备选 300 ~/.dsh/skills/用户个人 400 ~/.agents/skills/用户级全局(系统标准库落点,只读兜底) -
插件安装:
dsh plugin --profile <name> add <源>(npm/GitHub/git/tarball/link),写入该 profile 的 bundles 层与 node_modules,只影响该用户实例。 -
模型 provider:即插件;key 走 credentials service(UI Models 页 →
.credentials.yaml)或环境变量(DEEPSEEK_API_KEY)。
五、模型 / 插件 / SKILL 归属矩阵
| 能力 | 公共(系统层,管理员维护) | 个人(用户可改) | 项目(随工作区) |
|---|---|---|---|
| 模型 | .env 统一 key + profile 层默认 provider;用户不自配模型(见安全限制) |
.credentials.yaml 个人 key(若开放) |
— |
| 插件 | 镜像预置公共插件(官方 bundle 同级,人人可用,不可改) | profiles/web/node_modules + bundles 声明(dsh plugin add,需网络) |
— |
| SKILL | std/agents/skills(rank400 只读兜底,同名可被用户覆盖) |
~/.dsh/skills(rank300) |
工作区 .dsh/skills(100) / .agents/skills(200) |
一句话:公共层只读放低优先,个人层可写放高优先——公司统一给兜底,个人自配天然覆盖,无需把标准配置复制 N 份。
九、安全边界与已知限制
- dsh 禁
--host 0.0.0.0(RCE 防护),容器内只监听 127.0.0.1 → 必须--network host+ nginx 反代; - 特权 API 强制 loopback-only(settings/credentials.describe 等),即使
--trusted-host也拒非本机访问 → nginx 必须把Host/Origin/Sec-Fetch-*头伪装为127.0.0.1:8900(现有反代模板已含); - Settings/Models 页前端 loopback 检测:非本机浏览器地址访问时页面禁用(
settings are unavailable in this browser)→ 外网用户无法在 UI 自配模型;普通用户模型一律管理员统一配置;要开放自配需对 web 前端打--patch放行(待验证项); - 插件 = 任意代码执行:普通用户不开
dsh plugin add权限;公共插件管理员审核后入镜像; - token 认证:每实例首次启动生成 token,nginx 后建议加一层 Basic Auth 或尽快接 dshs;
.env含密钥,权限 600,不进备份包(整机迁移需单独携带或重设)。
十三、架构切换:容器 → dshs 多租户 + account 硬隔离(2026-09-08 18:30)
决策:去掉 Docker 单容器方案,全量迁移到 dshs 进程隔离模式(便于 dsh/插件升级迭代 = 一条 npm 命令);用户确认"立即全量迁移 main" + "account 账号级硬隔离"。
最终架构
宿主机:Node v22.23.2 + 全局 dsh 0.1.2-rc.1(/usr/local/bin)
dshs(systemd,127.0.0.1:3080,account 模式)
├── /var/lib/dshs/
│ ├── dshs.db # 用户/审核数据
│ ├── secret.key # 每用户 key 库主密钥
│ └── users/<userId>/
│ ├── home/ # DSH_HOME(profiles/sessions/credentials)— 由原 .dsh 迁入
│ ├── ws/ # 工作区(HOME 指向)— 由原 workspaces 迁入
│ └── patches/
nginx:dsh.alotbuy.com + *.dsh.alotbuy.com → 127.0.0.1:3080(端口动态分配由编排器管理)
关键文件清单
| 文件 | 作用 |
|---|---|
/etc/dshs.env |
BASE_DOMAIN=dsh.alotbuy.com / COOKIE_DOMAIN=.dsh.alotbuy.com / ISOLATION_MODE=account / BASE_UID=100000 / DSH_BIN=/usr/local/bin/dsh |
/etc/systemd/system/dshs.service |
编排服务(ExecStart 带 --db 指向 dshs.db) |
/etc/systemd/system/dsh-provision.path + .service |
监控新用户目录自动建 OS 账号 |
/usr/local/bin/provision-new-users.sh |
建号脚本(用户名截断 20 字符,规避 Linux 32 字符限制;每次 chown 幂等) |
已验证
- admin 登录 API ✅(role=admin)
- account 降权:dsh 可跑(0.1.2-rc.1)、读 /root Permission denied、读自己 home 正常 ✅
- nginx 主域/子域 → 3080 路由 ✅(登录门户 HTML 返回)
- 备份/恢复脚本已切换新数据根(backup.sh 打包 /var/lib/dshs + 配置 + systemd 单元)
⚠️ 未完成 / 待办
- HTTPS 通配证书(2026-09-08 18:40 完成):certbot dns-cloudflare 签
*.dsh.alotbuy.com,nginx 443 + HTTP 跳转,全链路登录验证通过 - 每用户 API Key:dshs 为每用户独立 key 库(登录后桌面"管理密钥"填),原 .env 统一 key 的模型已不适用(迁移后 admin 需自填 key)
- 注册普通用户 → 审核 → 桌面/DSH 启动全链路验证(子域 404 需注册后确认)
- main 旧容器数据 /opt/dsh/users/main 已迁入 admin home,容器已停,可清理
- 飞书 OAuth 扩展(预留)
踩坑记录
- db 路径不一致:bootstrap-admin 写入
dshs.db,但服务默认用dshs.db(空)→ systemd ExecStart 必须显式--db - 用户名超长:
dsh-+uuid=41 字符超 Linux 32 限制 → provision 脚本截断为dsh-<uuid去横线前20> - 初始属主不一致:bootstrap 建目录属主 100001,hash uid 为 114801 → 以
uid-for-user输出为准 - 中间路径权限:
/var/lib/dshs与/users需 755(root 700 会挡住降权账号穿透),用户目录本身 700 保持隔离 - pkill 自杀坑:
pkill -f 'lib/cli.js'会匹配到远程 shell 自身命令行 → 用 systemctl 管理
附一、机制索引(2026-09-11 补 · 档案 19 §D1)
本文件正文停在 2026-09-08 的架构切换;此后新增/变更的机制以「档案」为准,此表为快速索引。
| 机制 | 生效位置 | 档案 |
|---|---|---|
访问入口:门户 https://alotbuy.com、用户 <用户名>.alotbuy.com(旧域 301) |
nginx vhost + BASE_DOMAIN/COOKIE_DOMAIN |
22 |
多租户隔离 = account + bwrap(--ro-bind /usr //lib64 /etc、--dev/--proc、私有 /tmp、--unshare-pid、setpriv 改 uid) |
src/supervisor/orchestrator.ts#spawnAsUser + systemd-run scope(512M/150%/TasksMax 128) |
16 |
出网护栏:封云元数据端点 100.100.100.200 + 外联观测 |
/etc/nftables-dsh-egress.nft + dsh-egress.service |
14 |
| 单活跃会话(last-wins)+ 常驻上限(idle-reap / LRU) | orchestrator.ts(instanceIdleTtlSeconds、maxIdleInstances) |
08 |
崩溃自愈:指数退避 + 窗口熔断 + 观测(restarts/lastCrashedAt) |
crash-policy.ts + orchestrator.scheduleCrashRestart |
20 |
| 登录直达会话(enter 等 launch token;401 守卫;冷启动 404 修复) | routes/dsh.ts、web/*.html |
06 / 13 / 15 |
| 技能分层:共享只读层(bundledSkillDir)+ 个人层 API/页面 | 编排器 env 注入 + routes/skills.ts |
10 / 11 |
| 插件三层归属:自带插件(仅 admin)/ 功能插件投放(门户,仅 admin)/ 实例设置启停 | routes/business-plugins.ts + plugins.html + @dsh-local/business-plugins |
16 |
| 目录选择器收敛:仅见自有目录(含 admin) | @dsh-local/workspace-scoped-picker + profile patch(insert + disabled) |
18 |
| 角色化 profile patch(非 admin 隐藏模型分区等) | ensure-role-profile-patch.cjs → <profile>/cordis.patch.yml |
09 / 15 |
nginx 侧关键配置:proxy_buffering off(流式)、gzip_proxied any(~930KB bundle 压缩)、/assets/ 长缓存、CF 真实 IP 还原 |
vhost/nginx/{alotbuy.com.conf,dsh.alotbuy.com.conf} |
21 / 22 |
证书:alotbuy.com + *.alotbuy.com(DNS-01 via /etc/cloudflare.ini,传播 60s) |
certbot + /etc/letsencrypt/live/alotbuy.com/ |
22 |