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

15 KiB
Raw Blame History

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 单元)

⚠️ 未完成 / 待办

  • 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