555 lines
35 KiB
Markdown
555 lines
35 KiB
Markdown
# dsh(DeepSeek Harness)多用户部署与配置改造方案
|
||||
|
|
|
|||
|
|
| 项 | 值 |
|
|||
|
|
|---|---|
|
|||
|
|
| 版本 | v1.0(2026-09-08) |
|
|||
|
|
| 适用对象 | 服务器 47.77.182.89(root SSH 端口 32022) |
|
|||
|
|
| 软件 | @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 # 用户全局设置
|
|||
|
|
│ │ │ ├── 08-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 份。
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 六、单实例迁移执行步骤(从当前 data/ 形态 → users/main 形态)
|
|||
|
|
|
|||
|
|
> ⚠️ 以下命令在服务器执行(root)。当前容器 `dsh-web` 以 `/opt/dsh/data` 为 `/root/.dsh`。
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
# ===== 1. 停容器 + 重组目录 =====
|
|||
|
|
docker stop dsh-web
|
|||
|
|
mkdir -p /opt/dsh/backups
|
|||
|
|
mv /opt/dsh/backup-20260908 /opt/dsh/backups/20260908_initial # 旧备份归档保留
|
|||
|
|
|
|||
|
|
mkdir -p /opt/dsh/users/main
|
|||
|
|
mv /opt/dsh/data /opt/dsh/users/main/.dsh # data 落入主目录
|
|||
|
|
mkdir -p /opt/dsh/users/main/workspaces
|
|||
|
|
|
|||
|
|
# ===== 2. 固化密钥 + 重建容器(挂载主目录内两点) =====
|
|||
|
|
DK=$(docker inspect dsh-web --format '{{range .Config.Env}}{{println .}}{{end}}' | grep '^DEEPSEEK_API_KEY=' | cut -d= -f2-)
|
|||
|
|
printf 'DEEPSEEK_API_KEY=%s\n' "$DK" > /opt/dsh/.env
|
|||
|
|
chmod 600 /opt/dsh/.env
|
|||
|
|
|
|||
|
|
docker rm dsh-web
|
|||
|
|
docker run -d --name dsh-web --restart always --network host \
|
|||
|
|
-v /opt/dsh/users/main/.dsh:/root/.dsh \
|
|||
|
|
-v /opt/dsh/users/main/workspaces:/root/workspaces \
|
|||
|
|
--env-file /opt/dsh/.env \
|
|||
|
|
dsh-web:0.1.2 dsh web --host 127.0.0.1 --port 8900 --no-open --trusted-host 47.77.182.89
|
|||
|
|
|
|||
|
|
# ===== 3. 生成备份/恢复脚本 =====
|
|||
|
|
cat > /opt/dsh/backup.sh <<'EOF'
|
|||
|
|
#!/bin/bash
|
|||
|
|
# 一键备份:打包 users/ 全部用户主目录 → backups/时间戳.tar.gz
|
|||
|
|
set -euo pipefail
|
|||
|
|
TS=$(date +%Y%m%d_%H%M%S)
|
|||
|
|
OUT=/opt/dsh/backups/dsh-backup-${TS}.tar.gz
|
|||
|
|
cd /opt/dsh
|
|||
|
|
docker stop dsh-web 2>/dev/null || true
|
|||
|
|
tar -czf "$OUT" users
|
|||
|
|
docker start dsh-web 2>/dev/null || true
|
|||
|
|
echo "备份完成: $OUT"
|
|||
|
|
ls -lh "$OUT"
|
|||
|
|
EOF
|
|||
|
|
|
|||
|
|
cat > /opt/dsh/restore.sh <<'EOF'
|
|||
|
|
#!/bin/bash
|
|||
|
|
# 一键恢复:./restore.sh <备份tar.gz>(当前数据先挪走可回滚)
|
|||
|
|
set -euo pipefail
|
|||
|
|
TAR=${1:?用法: ./restore.sh /opt/dsh/backups/dsh-backup-xxx.tar.gz}
|
|||
|
|
test -f "$TAR" || { echo "备份文件不存在: $TAR"; exit 1; }
|
|||
|
|
TS=$(date +%Y%m%d_%H%M%S)
|
|||
|
|
cd /opt/dsh
|
|||
|
|
docker stop dsh-web 2>/dev/null || true
|
|||
|
|
[ -d users ] && mv users users.old-$TS
|
|||
|
|
tar -xzf "$TAR"
|
|||
|
|
docker rm -f dsh-web 2>/dev/null || true
|
|||
|
|
docker run -d --name dsh-web --restart always --network host \
|
|||
|
|
-v /opt/dsh/users/main/.dsh:/root/.dsh \
|
|||
|
|
-v /opt/dsh/users/main/workspaces:/root/workspaces \
|
|||
|
|
--env-file /opt/dsh/.env \
|
|||
|
|
dsh-web:0.1.2 dsh web --host 127.0.0.1 --port 8900 --no-open --trusted-host 47.77.182.89
|
|||
|
|
echo "恢复完成(旧数据在 users.old-$TS,确认无误后可删)"
|
|||
|
|
EOF
|
|||
|
|
|
|||
|
|
chmod +x /opt/dsh/backup.sh /opt/dsh/restore.sh
|
|||
|
|
|
|||
|
|
# ===== 4. 验证 =====
|
|||
|
|
docker ps | grep dsh-web
|
|||
|
|
curl -s -X POST http://127.0.0.1:8900/api/llm/listProviders \
|
|||
|
|
-H 'Content-Type: application/json' \
|
|||
|
|
-d '{"type":"client-request","rpcId":"v1","method":"llm/listProviders","payload":{"args":{}}}'
|
|||
|
|
# 期望 {"ok":true,"value":[{"id":"deepseek-official",...}]}
|
|||
|
|
/opt/dsh/backup.sh
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 七、备份与恢复语义
|
|||
|
|
|
|||
|
|
- **备份 = 打包 `users/`**(全部用户主目录,一个 tar);`.env`(部署配置)与 `std/`(可再生成)不进备份;
|
|||
|
|
- **恢复 = 解压 + 重建容器**(restore.sh 已固化容器参数);
|
|||
|
|
- **复制/迁移用户**:拷 `/opt/dsh/users/<uid>` 到新机同路径即可;
|
|||
|
|
- 多用户同构扩展:新用户增加 `users/<uid>/`,脚本无需改动(tar 整体 users/)。
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 八、多用户扩展(dshs 落地前的过渡方案)
|
|||
|
|
|
|||
|
|
目标:**每用户独立实例 + 严格隔离**。新增一个用户 = 一套同构目录 + 一个容器 + nginx 分流:
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
# 以新增 zhangsan 为例(示例脚本 add-user.sh)
|
|||
|
|
UID_NEW=zhangsan
|
|||
|
|
PORT=8901
|
|||
|
|
mkdir -p /opt/dsh/users/${UID_NEW}/.dsh /opt/dsh/users/${UID_NEW}/workspaces
|
|||
|
|
# 从 main 复制 profile 骨架(不含凭据/会话),或从 std/profile-skel 初始化
|
|||
|
|
docker run -d --name dsh-web-${UID_NEW} --restart always --network host \
|
|||
|
|
-v /opt/dsh/users/${UID_NEW}/.dsh:/root/.dsh \
|
|||
|
|
-v /opt/dsh/users/${UID_NEW}/workspaces:/root/workspaces \
|
|||
|
|
--env-file /opt/dsh/.env \
|
|||
|
|
dsh-web:0.1.2 dsh web --host 127.0.0.1 --port ${PORT} --no-open --trusted-host 47.77.182.89
|
|||
|
|
# nginx:新 server/server_name 分流 → 127.0.0.1:${PORT},沿用 Host/Origin 伪装头模板
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
> 说明:web profile 首次启动会自动初始化模板,新用户空 `.dsh` 直接起容器即可;系统标准技能层由 `std/` 挂载提供。
|
|||
|
|
|
|||
|
|
### 认证路线
|
|||
|
|
|
|||
|
|
| 阶段 | 方案 | 说明 |
|
|||
|
|
|---|---|---|
|
|||
|
|
| 当前 | dsh 自带 token(一次性) | 仅单人过渡 |
|
|||
|
|
| 近期 | **dshs 密码制基座**(每用户独立 DSH 实例 + 注册审核) | 多租户严格隔离的正规托管方案 |
|
|||
|
|
| 后期 | **飞书 OAuth 登录** | 替换/并存 dshs 密码认证,预留扩展点 |
|
|||
|
|
|
|||
|
|
> 备选认知:`@xgone/dsh-remote` / `dsh-passwords` 类认证网关是共享单租户,无法满足严格隔离,不采用。
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 九、安全边界与已知限制
|
|||
|
|
|
|||
|
|
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,不进备份包(整机迁移需单独携带或重设)。
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 十、待验证项(Open Questions)
|
|||
|
|
|
|||
|
|
| # | 问题 | 影响 | 验证方式 |
|
|||
|
|
|---|---|---|---|
|
|||
|
|
| 1 | `.env` 注入的 `DEEPSEEK_API_KEY` 与用户 credentials 中个人 key 的优先级 | 决定统一 key 是否为硬兜底 | 容器内同时存在时调 `llm/listProviders` 观察 |
|
|||
|
|
| 2 | web profile 空 `.dsh` 首启是否能自动初始化模板(供 add-user 流程) | 新用户目录初始化方式 | 用空目录起测试容器观察 |
|
|||
|
|
| 3 | 前端 loopback 检测能否用 `--patch` 放行 | 用户是否可自配模型 | 社区/源码调研 + 试验 |
|
|||
|
|
| 4 | dshs 对 web Settings 限制的处理方式 | 密码制基座下用户可配置面 | 部署后实测 |
|
|||
|
|
| 5 | 容器内 `dsh plugin add` 走 npmmirror 是否可装 | 用户/管理员装公共插件渠道 | 实测 add 一个测试插件 |
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 十一、路线图
|
|||
|
|
|
|||
|
|
- [x] **P0 执行本方案第六节迁移命令**(data → users/main,双卷挂载,脚本就位)— **2026-09-08 17:30 完成**
|
|||
|
|
- [x] **P2 前置调研完成**(2026-09-08):dshs 为多租户首选候选;确认硬前提=域名+通配DNS+通配证书(纯 IP 打不开聊天 UI);服务器 1.8G 内存限 1-3 人规模;**用户决定:解决域名后再做多用户改造**
|
|||
|
|
- [ ] **域名就绪后选型落地**:dshs(模式 A 单机,优先)或自研 Node 网关(备选);新用户按 users/<uid> 同构扩展
|
|||
|
|
- [ ] **P1 镜像 v2**:内置公共插件 + `std/agents/skills` 标准技能库(rank400 层)
|
|||
|
|
- [ ] **P1 系统标准技能库内容确认**(MCN 脚本创作技能等随镜像/`std/` 分发)
|
|||
|
|
- [ ] **P3 飞书 OAuth 登录扩展**(预留接口,按需启用)
|
|||
|
|
- [ ] **P3 编导工作区模板**(新用户自动带账号人设/素材库结构)
|
|||
|
|
|
|||
|
|
### 多用户改造前提条件(用户决策记录 2026-09-08)
|
|||
|
|
|
|||
|
|
1. **域名**:无(仅 IP)。海外服务器解析无需备案;买域名后配 `@` A 记录 + `*` 通配 A 记录 → 47.77.182.89,再签通配证书。域名就绪 = 多用户改造解锁。
|
|||
|
|
2. **用户规模**:1-3 人(现 1.8G 内存可撑,dshs 每用户一个常驻 DSH 子进程)。
|
|||
|
|
3. **候选对比结论**:
|
|||
|
|
- dshs:现成密码注册/审核/隔离/网页桌面/每用户加密 key 库;需宿主机 Node ^22.19 + 全局 dsh(spawn 子进程,非 Docker);数据组织为 `DATA_ROOT/每用户home`,与 users/<uid> 不互通、官方无迁移工具;
|
|||
|
|
- 自研 Node 网关 + Docker 每用户:复用现有 users/<uid> + 镜像,nginx 后加认证/路由层;开发量中等;
|
|||
|
|
- dsh-webui-auth:仅单实例加锁,不做多用户隔离,可作过渡。
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 十二、域名接入记录(2026-09-08)
|
|||
|
|
|
|||
|
|
**域名**:`dsh.alotbuy.com`(Cloudflare 代理,海外服务器免备案)
|
|||
|
|
|
|||
|
|
| 项 | 状态 |
|
|||
|
|
|---|---|
|
|||
|
|
| DNS | `dsh.alotbuy.com` → Cloudflare(172.67.194.206 / 104.21.44.42),CF 回源服务器 80 |
|
|||
|
|
| **通配 `*.dsh.alotbuy.com`** | ⚠️ **未配置**——dshs 每用户子域路由的前提,落地前需在 CF 加 `*` A 记录 → 47.77.182.89 |
|
|||
|
|
| HTTPS | Cloudflare 边缘证书(源站无 443,CF SSL 需为 Flexible 模式,实测已通) |
|
|||
|
|
| 源站 nginx | 新建站点 `/www/server/panel/vhost/nginx/dsh.alotbuy.com.conf`(监听 80,反代 127.0.0.1:8900,Host 伪装 + 剥 Origin/Sec-Fetch,含 WebSocket 头) |
|
|||
|
|
| 验证 | token 登录 303→cookie(host-only 绑 dsh.alotbuy.com)→ 首页 200(24KB)→ API `{"ok":true, deepseek-official}` ✅ |
|
|||
|
|
|
|||
|
|
**访问方式**:`https://dsh.alotbuy.com/?token=<token>`(浏览器打开一次即种 cookie,30 天有效)
|
|||
|
|
|
|||
|
|
**踩坑记录**:
|
|||
|
|
- Windows 原生 curl 写 `/tmp/` 路径失败导致 cookie 不落盘 → 必须用 `C:/...` 路径(历史已知坑,本次复现)
|
|||
|
|
- 对根路径发无 cookie 裸 WS Upgrade 返回 502 为 dsh 正常拒绝(IP 站点同样 502),非配置问题
|
|||
|
|
- nginx 新站点需与 47.77.182.89 站同样伪装头(Host 127.0.0.1:8900 + 空 Origin/Sec-Fetch),否则特权 API 403
|
|||
|
|
|
|||
|
|
## 十三、架构切换:容器 → 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 管理
|
|||
|
|
|
|||
|
|
## 十四、dsh.alotbuy.com 注册为宝塔面板反代站点(2026-09-08 18:46)
|
|||
|
|
|
|||
|
|
**背景**:手写 conf(`/www/server/panel/vhost/nginx/dsh.alotbuy.com.conf`)在面板数据库无站点记录 → 面板重置/批量管理可能误伤。按用户偏好(能走 MCP 尽量走 MCP)用宝塔 MCP 完成注册。
|
|||
|
|
|
|||
|
|
**执行链(MCP 优先,全流程零中断)**:
|
|||
|
|
|
|||
|
|
1. SSH 备份 + 移走手写 conf:`cp` 到 `/opt/dsh/docs/nginx-dsh.alotbuy.com.conf.bak-20260908`,`mv` 出 include 目录(不 reload,服务无感)
|
|||
|
|
2. MCP `ProxyProjectCreate`:domains=`dsh.alotbuy.com`,proxy_pass=`http://127.0.0.1:3080` → 站点 id=2 入面板 DB
|
|||
|
|
3. MCP `ProxyWriteConfig`:把完整精调 conf(HTTP→HTTPS 跳转 ×2 + 443 主域 SSL + 443 通配子域 SSL)覆盖写回同路径
|
|||
|
|
4. SSH `nginx -t` + reload + 本机 curl 验证
|
|||
|
|
|
|||
|
|
**⚠️ 关键坑**:面板反代项目 conf 写入路径与手写 conf 同名同路径(`/www/server/panel/vhost/nginx/<域名>.conf`)——直接 ProxyProjectCreate 会用面板模板覆盖,丢失 SSL/通配子域/301 等精调内容。**必须"先移走手写 conf → 创建 → ProxyWriteConfig 写回精调版"三步走**。
|
|||
|
|
|
|||
|
|
**验证结果**:
|
|||
|
|
|
|||
|
|
| 检查项 | 结果 |
|
|||
|
|
|--------|------|
|
|||
|
|
| SiteList 面板 DB | 2 站点:47.77.182.89(id=1)+ dsh.alotbuy.com(id=2)✅ |
|
|||
|
|
| nginx -t + reload | OK ✅ |
|
|||
|
|
| https://dsh.alotbuy.com(外网直连源站) | 200 ✅ |
|
|||
|
|
| https://*.dsh.alotbuy.com(通配子域,未注册) | 404(dshs 正常拒绝,代理/SSL 层通)✅ |
|
|||
|
|
| http://dsh.alotbuy.com | 301 → https ✅ |
|
|||
|
|
| conf 保留 | 4 段精调 server 块完整(certbot 证书路径原样)✅ |
|
|||
|
|
|
|||
|
|
**遗留说明**:面板站点列表 SSL 状态显示 `enabled:false`——证书为 certbot 手动签发(面板不托管),面板只做站点/conf 管理;如需面板托管 SSL 需换面板 Let's Encrypt 流程(不建议,会与通配证书冲突)。
|
|||
|
|
|
|||
|
|
## 十五、启动 DSH 报"已有运行中"+ 无法对话 排障(2026-09-08 19:00)
|
|||
|
|
|
|||
|
|
**现象**:admin 登录桌面点"启动 DSH",提示"有运行中的 DSH 请先停止",无跳转,无法对话。
|
|||
|
|
|
|||
|
|
**根因一(P0,已修复):DB uid 与系统账号 uid 错配**
|
|||
|
|
- 主 DB(`/var/lib/dshs/dshs.db`,服务 `--db` 显式指向)users.uid=**100001**(bootstrap 写入 `baseUid+rowid`)
|
|||
|
|
- 但 OS 账号按 `hashUid` 建:`dsh-cce6d1cdb376430480f0` uid=**114801**(早期版本 provision 用 hash;后 provision 改为读 DB uid-for-user,但账号没重建)
|
|||
|
|
- 结果:编排器 spawn 用 DB uid=100001 → `setpriv --reuid 100001` → 子进程以**不存在的 uid** 运行 → 打不开 `700 owner=114801` 的用户 home → `EACCES: mkdir .../home/profiles/web` 秒崩
|
|||
|
|
- 编排器 `spawn` 事件即置 status=running,子进程崩后自动重启循环 → 前端一直显示"运行中"→ 再次点击启动返回 409 already_running(即用户看到的提示)
|
|||
|
|
- **修复**:`UPDATE users SET uid=114801 WHERE id='cce6d1cd-...'`(备份在 `/opt/dsh/docs/dshs.db.bak-20260908-uidfix`)+ `systemctl restart dshs` + provision 脚本 `uid-for-user` 补 `--db /var/lib/dshs/dshs.db`(消除默认库分裂)
|
|||
|
|
|
|||
|
|
**根因二(上游集成缺口,未修复):dsh web 强制浏览器 token,编排器"打开"不带 token**
|
|||
|
|
- dsh web(0.1.2-rc.1,`dsh-client-connection` BrowserAuth)强制:首次访问需 `?token=<launch token>`(进程启动随机、打印到 stdout)换取签名 cookie(绑定 authority=`127.0.0.1:<port>`,30 天)
|
|||
|
|
- 上游 dshs 0.1.0:launch 返回 url **不带 token**,desktop.html "打开 DSH" 直接 `window.open(url)` → 首访 401 `dsh web authentication required`;且端口动态分配,实例重启换端口 → 旧 cookie 失效需重新带 token
|
|||
|
|
- 已实测全链路可行:门户 sid + `?token=` → 303 种 dsh-auth cookie → 双 cookie 访问 `https://admin.dsh.alotbuy.com/` 200(24233B SPA)+ `/api/llm/listProviders` ok:true
|
|||
|
|
- **修复方向(待用户定)**:A. 小改上游——编排器解析子进程 stdout 的 token,launch/status url 自动拼 `?token=`(改 src/supervisor/orchestrator.ts + routes/dsh.ts,tsc 可构建);B. 接受现状——每次启动后手动用 journald 里 token 开一次;C. 等上游修复
|
|||
|
|
|
|||
|
|
**排障经验**:`launch folder` 是相对 **ws/** 根的路径(空串=根);`/api/dsh/status` 返回 url 为子域;子域下所有路径(含 /api/dsh/status)都会被代理到实例,故探测实例要用 `/api/llm/*` 等实例 API;cookie 域 `.dsh.alotbuy.com`(sid)与 dsh-auth 双 cookie 缺一不可。
|
|||
|
|
|
|||
|
|
## 十六、方案 A 落地:launch/status 自动携带 dsh launch token(2026-09-08 19:30)
|
|||
|
|
|
|||
|
|
**决策**:用户选方案 A(小改上游,普通用户无需手动拿 token 即可打开聊天界面)。
|
|||
|
|
|
|||
|
|
**改动(3 文件,已 git commit 29c8907,本地分叉 patch)**:
|
|||
|
|
|
|||
|
|
| 文件 | 改动 |
|
|||
|
|
|------|------|
|
|||
|
|
| `src/supervisor/spawner.ts` | `Instance` 接口加 `launchToken?: string` 字段 |
|
|||
|
|
| `src/supervisor/orchestrator.ts` | ① `trackChild` 监听子进程 stdout,正则 `dsh web: http://127.0.0.1:\d+/\?token=([A-Za-z0-9_-]+)` 解析 token 存入 instance;② 新增 `waitForLaunchToken()`(轮询最多 20s 等 token 出现,崩溃/停止即返回);③ `launch()`/`restartMain()` 返回前等待 token |
|
|||
|
|
| `src/web/routes/dsh.ts` | `dshUrl()` 支持可选 token 参数;launch/restart/status 返回的 `url` 均拼接 `?token=`(status 从 `main.launchToken` 取) |
|
|||
|
|
|
|||
|
|
**验证结果(全链路)**:
|
|||
|
|
|
|||
|
|
| 步骤 | 结果 |
|
|||
|
|
|------|------|
|
|||
|
|
| launch 返回 | `{launchToken, url: https://admin.dsh.alotbuy.com/?token=...}` ✅ |
|
|||
|
|
| 打开带 token url(带 sid,-L 跟随) | 200,24233B SPA,自动 303 种 dsh-auth cookie ✅ |
|
|||
|
|
| 之后裸开 `/` | 200 ✅ |
|
|||
|
|
| `/api/llm/listProviders` | `ok:true, deepseek-official` ✅ |
|
|||
|
|
|
|||
|
|
**构建/部署**:`npm run build`(tsc)+ `systemctl restart dshs`。TS 坑:`RegExp.exec` 返回 `null` 非 `undefined`,判空须 `m !== null && m[1] !== undefined`。
|
|||
|
|
|
|||
|
|
**admin API Key 已配置**:从旧 `/opt/dsh/.env` 取出 `sk-81c...` 经 `/api/me/keys` 注入(name=deepseek-main,AES-GCM 加密入库,自动触发实例重启生效)。**KEY 为每用户独立**——每个用户需在桌面"管理密钥"各自添加;无全局通用 key(原容器 .env 统一 key 模型已废弃)。
|
|||
|
|
|
|||
|
|
## 十七、权限边界实测 + DB 权限安全加固(2026-09-08)
|
|||
|
|
|
|||
|
|
**问题**:admin 登录后能否访问服务器所有目录?AI 对话窗口新建工作空间时能否输入系统根目录?
|
|||
|
|
|
|||
|
|
**两层防护模型(答案分两层)**:
|
|||
|
|
|
|||
|
|
| 层 | 机制 | 结论 |
|
|||
|
|
|----|------|------|
|
|||
|
|
| Web 桌面层 | fs-guard `resolveWithinRoot()`:所有文件浏览/上传/新建工作空间的路径解析被锁在用户自身 home/ws 内,拒绝 `../`、绝对路径、符号链接逃逸 | 输入 `/`、`/root` 等系统路径会被拉回/拒绝;admin 与普通用户同样受限,**不存在"admin 浏览全服务器"入口** |
|
|||
|
|
| AI 进程层 | 每用户 DSH 实例以**降权 uid**(setpriv)运行,该进程可执行 shell/agent 工具,**不受 fs-guard 约束**,能读该 uid 有权限的一切文件 | admin 的实例同样是降权 uid,非 root。实测 uid 114801 边界:`/root`、`/etc/shadow`、`/opt/dsh/.env`、secret.key、dsh db 全部 Permission denied ✅;`/etc/passwd`、`/tmp` 世界可读可写 ✅ |
|
|||
|
|
|
|||
|
|
**结论**:即便 admin 也无法从 AI 对话触达 root-only 敏感文件;真正的服务器管理走 SSH/宝塔 root 通道,与 AI 会话隔离。这是 account 隔离模式的核心价值,无需额外改造。
|
|||
|
|
|
|||
|
|
**⚠️ 实测发现并修复的漏洞:DB 权限 644 → 600**
|
|||
|
|
|
|||
|
|
| 项 | 详情 |
|
|||
|
|
|----|------|
|
|||
|
|
| 漏洞 | `/var/lib/dshs/dshs.db` 权限 644 root——任意降权账号可 `cat` 拖库 |
|
|||
|
|
| 风险 | 库内含全部用户 `pass_hash`(scrypt)+ `api_key_ref`(AES-GCM 密文)+ uid/sid → 离线爆破口令 + 窃取 key 密文 |
|
|||
|
|
| 修复 | `chmod 600 /var/lib/dshs/dshs.db* /var/lib/dshs/dshs.db*` |
|
|||
|
|
| 验证 | 降权账号读 db → Permission denied ✅;服务 600 下正常(portal 401 为未认证正常响应)✅ |
|
|||
|
|
|
|||
|
|
**目录级权限收紧(用户反馈"其他用户也能看就不行",2026-09-08 20:30)**:
|
|||
|
|
|
|||
|
|
普通用户实例同样是降权 uid,盘点发现除主 db 外还有更大泄露面:`/opt/dshs` 整棵树 755、`/opt/dsh/docs/` 内**主库备份**(含全部 pass_hash+key 密文)、`/opt/dsh/backups/` 备份包、`/opt/dsh/users/main` 旧数据残留均 644/755 可读 → AI 会话可拖取。收紧如下:
|
|||
|
|
|
|||
|
|
| 路径 | 改前 | 改后 | 说明 |
|
|||
|
|
|------|------|------|------|
|
|||
|
|
| `/opt/dsh` | 755 | **700** | 覆盖 docs(含 db 备份)、backups、users/main 残留 |
|
|||
|
|
| `/opt/dshs` | 755 | **700** | root 服务运行不受影响(systemd 无 User= 默认 root) |
|
|||
|
|
| `/var/lib/dshs` | 755 | **711** | 保留 o+x:降权进程需穿过根路径进入自己 home,去读防列表泄露 |
|
|||
|
|
| `.../users` | 755 | **711** | 同上;用户目录本身 700 保持隔离 |
|
|||
|
|
|
|||
|
|
**验证(uid 114801 实测)**:读 `/opt/dsh/docs/*.md` → denied ✅;列 `/opt/dshs`、数据根 → denied ✅;**仍可进入自己 `ws/mcnworkspace` ✅**;systemd active、portal 200、admin 实例 `/plugins/events` 正常 ✅。
|
|||
|
|
|
|||
|
|
**⚠️ 经验固化(Linux 目录权限设计)**:数据根这类"root 服务 + 降权子进程都要访问"的目录,权限用 **711/700**(去 r 留 x),不能收 700——降权进程会因无法穿过根路径而崩溃(EACCES)。子目录各自 700 才是真正的隔离边界。
|
|||
|
|
|
|||
|
|
## 十八、统一 KEY:管理员统一设置,用户只能使用管理员 KEY 和模型(2026-09-08 20:40)
|
|||
|
|
|
|||
|
|
**决策**(用户确认):① KEY 存放 = 复用 admin 密钥库(桌面/API 自助换 key 即全局生效);② 用户 dsh 界面自配暴露面先实测再定,本次不做 dsh patch。
|
|||
|
|
|
|||
|
|
**改动(commit df5cc84,6 文件 +49/-14)**:
|
|||
|
|
|
|||
|
|
| 文件 | 改动 |
|
|||
|
|
|------|------|
|
|||
|
|
| `src/web/server.ts` | `resolveApiKey` 忽略入参 userId,改为 `listPublicUsers().filter(role==='admin')` 取 admin 的启用 key——**所有用户 spawn 注入同一把管理员 key** |
|
|||
|
|
| `src/supervisor/spawner.ts` | Spawner 接口新增 `restartAllMains()` |
|
|||
|
|
| `src/supervisor/orchestrator.ts` | LocalSpawner 实现:遍历内存 `mains` 逐个 `restartMain`(单个失败 console.error 不中断广播) |
|
|||
|
|
| `src/supervisor/k8s-spawner.ts` | K8sSpawner 实现:遍历 `db.listInstancesByRole('main')` 逐个重启 |
|
|||
|
|
| `src/web/routes/auth.ts` | `/api/me/keys` GET/POST/select/delete 由 `requireAuth` 改 **`requireAdmin`**(普通用户 403);POST/select 后 `restartAllMains()` 广播(key 是 spawn 时注入 env 的快照,不重启运行实例不刷新) |
|
|||
|
|
| `web/desktop.html` | 非 admin 隐藏"管理密钥"按钮;面板文案改"全局 API 密钥(所有用户共用,仅管理员可改)" |
|
|||
|
|
|
|||
|
|
**验证(全链路实测)**:
|
|||
|
|
- admin GET `/api/me/keys` → 200,deepseek-main 在列 ✅
|
|||
|
|
- 注册 testuser(pending)→ admin approve(active)→ 登录 ✅
|
|||
|
|
- 普通用户 GET/POST `/api/me/keys` → **403 forbidden** ✅
|
|||
|
|
- **testuser launch DSH → 打开 200 SPA → `llm/listProviders` ok:true deepseek-official** ——证明普通用户无自配 key 也拿到 admin 全局 key ✅
|
|||
|
|
|
|||
|
|
**遗留**:用户 dsh 界面内自配 key/模型的能力面未实测(proxy 伪装 loopback 可能放行 dsh Settings)——第二道防线待 P1 实测后再定。testuser 账号保留作普通用户全链路验证种子。
|
|||
|
|
|
|||
|
|
## 十九、界面删除用户功能(2026-09-09 07:00)
|
|||
|
|
|
|||
|
|
**需求**:管理界面在禁用/恢复右侧加删除按钮 → 弹窗输入该用户名相同名称确认 → 删除(admin 不可删)。
|
|||
|
|
|
|||
|
|
**改动(commit 4943c8c,7 文件 +106/-2)**:
|
|||
|
|
|
|||
|
|
| 层 | 文件 | 改动 |
|
|||
|
|
|----|------|------|
|
|||
|
|
| 后端路由 | `src/web/routes/admin.ts` | 新增 `DELETE /api/admin/users/:id`:admin → 409;`supervisor.stop` → `db.deleteUser`(事务)→ `rm users/<id>/` 目录 → `userdel dsh-<id去横线20位>`(OS 账号,失败容忍)→ audit |
|
|||
|
|
| DB 接口 | `src/db/adapter.ts` | +`deleteUser(userId)` |
|
|||
|
|
| SQLite | `src/db/schema→repo.ts` + `sqlite.ts` | 事务按序清 credential_vault/domains/sessions/dsh_instances/audit_log/folder_plugins/workspaces/users(显式删除,不依赖 FK cascade 开关) |
|
|||
|
|
| Postgres | `src/db/pg.ts` | 同序 `withTx` 实现 |
|
|||
|
|
| 前端 | `web/admin.html` + `web/desktop.html` | 非 admin 行尾加 🗑 按钮(红色 ghost);点击 `prompt` 输入用户名比对确认后 `DELETE`;admin 行不渲染按钮 |
|
|||
|
|
|
|||
|
|
**验证(全部实测)**:① 删 admin 自己 → 409 `cannot_delete_admin` ✅ ② 删 testuser → 200 ✅ ③ 删不存在 → 404 ✅ ④ 残留检查:users/credential_vault/sessions/workspaces/dsh_instances/domains/audit_log 7 表全 0、`users/<id>/` 目录不存在、OS 账号 `dsh-2d1f22ffcde04874ba1d` 已删、用户列表仅剩 admin ✅
|
|||
|
|
|
|||
|
|
**注意**:`userdel` 账号名与 provision-new-users.sh 同规则(`dsh-` + uuid 去横线前 20 位);OS 账号删除失败只容忍不报错(DB/目录已清,残留仅 uid 占用)。testuser 已作为功能测试对象删除。
|
|||
|
|
|
|||
|
|
## 附录 A:常用命令速查
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
# 备份 / 恢复
|
|||
|
|
/opt/dsh/backup.sh
|
|||
|
|
/opt/dsh/restore.sh /opt/dsh/backups/dsh-backup-<时间戳>.tar.gz
|
|||
|
|
|
|||
|
|
# 容器日志(看 token / 排障)
|
|||
|
|
docker logs --tail 50 dsh-web
|
|||
|
|
|
|||
|
|
# 进入容器
|
|||
|
|
docker exec -it dsh-web bash
|
|||
|
|
|
|||
|
|
# 插件管理(profile web)
|
|||
|
|
dsh plugin --profile web add <npm包名|github:owner/repo|link:/path>
|
|||
|
|
dsh plugin --profile web remove <包名>
|
|||
|
|
dsh --profile web --dump-config # 打印生效插件树
|
|||
|
|
|
|||
|
|
# 模型 API 直测(服务器本机)
|
|||
|
|
curl -s -X POST http://127.0.0.1:8900/api/llm/listProviders \
|
|||
|
|
-H 'Content-Type: application/json' \
|
|||
|
|
-d '{"type":"client-request","rpcId":"v1","method":"llm/listProviders","payload":{"args":{}}}'
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## 附录 B:参考链接
|
|||
|
|
|
|||
|
|
- npm:https://www.npmjs.com/package/@deepseek-ai/dsh(0.1.2-rc.1)
|
|||
|
|
- 官方仓库:https://github.com/deepseek-ai/deepseek-harness
|
|||
|
|
- CLI 行为参考:apps/cli/reference/README.md(分层优先级/部署默认)
|
|||
|
|
- dsh skills 安装与优先级:`~/.dsh/skills`、`~/.agents/skills`、项目级 `/.dsh/skills`(社区文档,见当日讨论记录)
|
|||
|
|
- 社区多租户方案调研(dshs / dsh-remote / dsh-passwords / dsh-im):见当日讨论记录
|