Files
dsh_shenxian/dsh-server-docs/01-规划与架构.md
T
admin 5ad755116e chore(docs): 文档库并入代码仓(R4 选 a)+ 索引/台账跟进
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 一律写「远程服务器」。
2026-09-15 18:47:13 +08:00

239 lines
15 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 |