238 lines
15 KiB
Markdown
238 lines
15 KiB
Markdown
# 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 项)**:
|
|||
|
|
|
|||
|
|
1. **多用户使用**:多个编导/账号运营各自登录使用;
|
|||
|
|
2. **数据持久化**:会话、配置、凭据、工作区不随容器重建丢失;
|
|||
|
|
3. **跨用户文件安全**:用户之间互相看不到对方的数据与工作区(严格隔离);
|
|||
|
|
4. **便于使用/复制/恢复**:一个用户 = 一个主目录,备份恢复按目录粒度一键完成;
|
|||
|
|
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 配置与插件天然分层,以下机制是方案成立的前提(无需自研):
|
|||
|
|
|
|||
|
|
1. **配置/插件叠加顺序**(后层覆盖前层):
|
|||
|
|
```
|
|||
|
|
插件 bundle 默认(官方/系统)
|
|||
|
|
→ profile 层 $DSH_HOME/profiles/web/cordis.patch.yml
|
|||
|
|
→ 用户层 $DSH_HOME/cordis.patch.yml
|
|||
|
|
→ 命令行 --patch(最高)
|
|||
|
|
```
|
|||
|
|
2. **SKILL 发现目录**(rank 越小优先级越高,同名高优先覆盖低优先):
|
|||
|
|
|
|||
|
|
| rank | 目录 | 性质 |
|
|||
|
|
|---|---|---|
|
|||
|
|
| 100 | 工作区根 `/.dsh/skills/` | 项目级(随项目分发) |
|
|||
|
|
| 200 | 工作区根 `/.agents/skills/` | 项目级备选 |
|
|||
|
|
| 300 | `~/.dsh/skills/` | 用户个人 |
|
|||
|
|
| 400 | `~/.agents/skills/` | 用户级全局(**系统标准库落点,只读兜底**) |
|
|||
|
|
|
|||
|
|
3. **插件安装**:`dsh plugin --profile <name> add <源>`(npm/GitHub/git/tarball/link),写入该 profile 的 bundles 层与 node_modules,**只影响该用户实例**。
|
|||
|
|
4. **模型 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 份。
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
|
|||
|
|
## 九、安全边界与已知限制
|
|||
|
|
|
|||
|
|
1. **dsh 禁 `--host 0.0.0.0`**(RCE 防护),容器内只监听 127.0.0.1 → 必须 `--network host` + nginx 反代;
|
|||
|
|
2. **特权 API 强制 loopback-only**(settings/credentials.describe 等),即使 `--trusted-host` 也拒非本机访问 → nginx 必须把 `Host`/`Origin`/`Sec-Fetch-*` 头伪装为 `127.0.0.1:8900`(现有反代模板已含);
|
|||
|
|
3. **Settings/Models 页前端 loopback 检测**:非本机浏览器地址访问时页面禁用(`settings are unavailable in this browser`)→ 外网用户**无法在 UI 自配模型**;普通用户模型一律管理员统一配置;要开放自配需对 web 前端打 `--patch` 放行(待验证项);
|
|||
|
|
4. **插件 = 任意代码执行**:普通用户不开 `dsh plugin add` 权限;公共插件管理员审核后入镜像;
|
|||
|
|
5. **token 认证**:每实例首次启动生成 token,nginx 后建议加一层 Basic Auth 或尽快接 dshs;
|
|||
|
|
6. `.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 单元)
|
|||
|
|
|
|||
|
|
### ⚠️ 未完成 / 待办
|
|||
|
|
|
|||
|
|
- [x] **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 扩展(预留)
|
|||
|
|
|
|||
|
|
### 踩坑记录
|
|||
|
|
|
|||
|
|
1. **db 路径不一致**:bootstrap-admin 写入 `dshs.db`,但服务默认用 `dshs.db`(空)→ systemd ExecStart 必须显式 `--db`
|
|||
|
|
2. **用户名超长**:`dsh-`+uuid=41 字符超 Linux 32 限制 → provision 脚本截断为 `dsh-<uuid去横线前20>`
|
|||
|
|
3. **初始属主不一致**:bootstrap 建目录属主 100001,hash uid 为 114801 → 以 `uid-for-user` 输出为准
|
|||
|
|
4. **中间路径权限**:`/var/lib/dshs` 与 `/users` 需 755(root 700 会挡住降权账号穿透),用户目录本身 700 保持隔离
|
|||
|
|
5. **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 |
|