Files
dsh-web/DESIGN.md
T

412 lines
26 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.
# DeepSeek Harness 多租户容器管理平台 — 设计文档
> 状态:本机管理面板核心流程已实现并通过真实 Docker 集成检查;生产隔离、硬配额等仍有差距,以 README.md 的实现边界与验证范围为准。下文是目标设计,不代表所有条目已实现。
> 目标项目:`deepseek-ai/deepseek-harness`(npm CLI `@deepseek-ai/dsh`,命令 `dsh web`)
> 控制面技术栈:Go + 前端 SPA
> 部署形态:单机 Docker daemon;面板自身亦运行于容器中
> 鉴权/配额:内置账号 + 目录配额
---
## 0. 调研结论与硬约束
> 实现阶段实测更正:本地 npm `0.2.0-rc.2` 支持 `--trusted-host`,但拒绝 `--public-url`;下文基于 upstream master 的 public-url 描述不能直接用于该发行版。实际启动仅传 web/no-open/port/trusted-host,代理负责外部路径。`scripts/probe-runtime.mjs` 已验证默认 token 交换为 303 + cookie、认证首页 200;完整代理以集成测试为准。
针对本体 `deepseek-ai/deepseek-harness`(npm CLI `@deepseek-ai/dsh`,命令 `dsh web`)确认:
| 事实 | 来源 | 对设计的影响 |
|---|---|---|
| **无官方 Docker 镜像/Dockerfile** | repo 根、`apps/`、`.gitlab-ci.yml` 均无 | 必须自建镜像包装 `dsh web` |
| **`dsh web` 默认监听 `127.0.0.1:3080`,且启动时拒绝 `--host 0.0.0.0`**(安全原因) | `packages/bundle/web-app/README.md` | 容器内必须跑反向代理绑 `0.0.0.0` 转发到 loopback |
| 端口/主机是 **CLI flag**(`--port/--host/--public-url/--trusted-host/--no-open`),**无 `DSH_PORT/DSH_HOST` 环境变量** | 同上 | 容器启动参数化靠 CMD args,不靠 env |
| 数据根 = `$DSH_HOME`(默认 `~/.dsh`) | `docs/user/guide/providers.md`、CLI README | 每实例独立 `DSH_HOME`,bind-mount 即可保留数据 |
| **多实例硬约束:每个容器必须独立 `DSH_HOME` + 独立端口**(task-board 单写者) | architecture.md | 一用户两容器 ⇒ 两个 `DSH_HOME`,不能共享 |
| 模型/API Key 存储:密钥在 `$DSH_HOME/.credentials.yaml`(YAML,write-only),模型/provider 配置在 `$DSH_HOME/profiles/<profile>/cordis.patch.yml`;`cordis.patch.yml` 只存 `apiKeyEnv` 引用 | `providers.md` | 备份"模型"= 备份这两个文件 |
| **"profile 文件" = `profiles/web/cordis.patch.yml`(YAML,用户可编辑的 patch 层)**;profile 目录还有 `package.json`(含插件依赖 + `dsh.profile` bundle 清单)+ `node_modules`(pnpm 装的插件,可重建) | `apps/cli/README.md` | 编辑 profile = 编辑 `cordis.patch.yml`;插件清单在 `package.json` |
| 升级:用户数据全在 `DSH_HOME`(npm 安装树之外),升级 `@deepseek-ai/dsh` 后数据保留;`node_modules` 由 pnpm 重建 | README、CLI README | "保留数据 + 重启新版容器"可行 |
| 公网部署:`--public-url <url/mount/>` + `--trusted-host`;代理必须 **剥前缀、保留 Host、把 `Path=/` 的 cookie 改写为 mount + 加 Secure** | `docs/user/guide/public-deployments.md` | 面板反代要处理 cookie/前缀,是最复杂点 |
| 启动 URL 带 **per-process token**,首次访问换签名 cookie | 同上 | 面板需从容器日志取 token,服务端代换 cookie |
> 结论:DSH 不是为容器多租户设计,但靠"自建镜像 + 容器内反代 + 面板反代桥接 token/cookie + 每实例独立 DSH_HOME"可稳态支撑。
---
## 1. 目标与范围
- 通过 docker.sock 在单机上**创建/启停/删除**多个 `dsh web` 容器,**每用户最多 2 个**。
- 每用户一个**独立工作区**,支持文件**上传/下载/删除/重命名**,并有**容量配额**。
- 工作区**映射进每个 dsh 容器**(agent 在工作区内干活)。
- **备份/还原 dsh 配置**(含模型与插件清单)。
- **编辑 profile 文件**(`cordis.patch.yml`)。
- **升级**:保留数据,用新版本镜像重启容器。
- **本地镜像管理**:面板查询 DSH 最新版本,用户手动触发本机构建;创建实例时选择本地已有版本,或手动构建最新版后创建。不需要私有镜像仓库,不推送生成的镜像。
- 控制面:Go 后端 + SPA 前端;内置账号 + 目录配额;**面板自身容器化运行**。
---
## 2. 总体架构
面板不是宿主进程,而是挂了 `docker.sock` 的**兄弟容器**(sibling),通过 sock 编排 dsh 兄弟容器,并通过同一 bridge 网络反代它们。
```
┌────────────── 宿主 ──────────────────┐
│ docker.sock ──┐ │
│ /var/lib/dsh-mgr ──┐ (bind) │
│ │ │
│ ┌─────────────────▼─────────────▼───┐ │ dsh-net (bridge)
│ │ 面板容器 dsh-mgr │ │ :80/:443 发布
│ │ - /data ← /var/lib/dsh-mgr │ │
│ │ - /var/run/docker.sock │ │
│ │ - SPA + API + 反代 + 编排引擎 │ │
│ └─────────┬───────────────────────────┘ │
│ │ sock │
│ ┌─────────▼───────────┐ ┌───────────────┐ │
│ │ dsh-web:<v> 容器1 │ │ 容器2 ... │ │ 不发布端口
│ │ caddy:8080 → 3080 │ │ │ │ /data/dsh-home、/workspace ← 宿主 bind
│ └─────────────────────┘ └───────────────┘ │
└─────────────────────────────────────────────┘
```
组件:
1. **面板服务**(Go 容器):REST API + 静态 SPA + 反向代理子系统 + docker client + 配额/备份/升级引擎。
2. **dsh-web 镜像**(自建):Node + `@deepseek-ai/dsh` + caddy + 入口脚本。
3. **dsh-net**:内部 bridge 网络,容器不发布主机端口,仅面板可达。
4. **(可选)socket-proxy**:面板与 sock 之间的白名单代理。
---
## 3. 宿主路径 ↔ 面板内路径映射(关键)
Docker bind 的 `Source` 必须是**守护进程所在宿主路径**,不是面板容器内路径。面板容器把宿主 `/var/lib/dsh-mgr` 挂到 `/data`,于是同一文件树有两个视角:
| 用途 | 面板直接读写(容器视角) | 创建 dsh 容器 bind.Source(宿主视角) |
|---|---|---|
| 用户工作区 | `/data/workspaces/<uid>/files/...` | `/var/lib/dsh-mgr/workspaces/<uid>/files` |
| 实例 DSH_HOME | `/data/workspaces/<uid>/instances/<id>/home/...` | `/var/lib/dsh-mgr/workspaces/<uid>/instances/<id>/home` |
| 备份包 | `/data/backups/<uid>/<id>/...` | (不入容器,仅面板读写) |
面板配置:
```
HOST_DATA_ROOT=/var/lib/dsh-mgr # 传给 docker API 的 bind 源
CONTAINER_DATA_ROOT=/data # 面板自己 fs 操作的根
```
- 所有"面板直接动文件"(文件 API、备份 tar、profile 读写)用 `CONTAINER_DATA_ROOT`。
- 所有"喂给 docker API 的 bind.Source / build context"用 `HOST_DATA_ROOT` 拼接。
- 封装 `toHostPath(containerPath)` 翻译函数集中调用,避免散落拼接出错——这是 sibling 容器最常见的 bug。
---
## 4. 数据与目录布局(宿主机)
```
/var/lib/dsh-mgr/
db.sqlite # 用户、实例、配额、备份索引
workspaces/<uid>/
files/ # 用户工作区(面板文件 API + 各 dsh 容器共享挂载)
└─ <用户自由目录结构>
instances/<instId>/
home/ # DSH_HOME(bind → 容器 /data/dsh-home)
.credentials.yaml # API Key(机密)
cordis.patch.yml # home 级 patch
profiles/web/
package.json # 插件依赖 + dsh.profile bundle 清单
cordis.patch.yml # profile 级 patch(= "profile 文件")
node_modules/ # 插件实体(可重建,不入备份)
skins/ pets/ agent-presets/ .agent-presets/
dsh-ssh.json # 0600
sessions/ worktrees/ # 运行态(不入配置备份)
backups/<uid>/<instId>/
<timestamp>.tar.gz # 配置备份包
# 镜像层由本机 Docker daemon 管理,不作为此目录下的文件保存;DB 记录镜像索引
```
**配额作用域**:`workspaces/<uid>/` 整棵树(files + 所有 instances 的 home)。每用户一个配额单位。
---
## 5. 自建 DSH 镜像设计
```
FROM node:22-slim
# 1) 装 dsh CLI(版本参数化)
ARG DSH_VERSION=0.2.0-rc.2
RUN npm i -g @deepseek-ai/dsh@${DSH_VERSION}
# 2) 装 caddy(容器内反代,绕过 dsh 拒绝 0.0.0.0)
COPY caddy /usr/bin/caddy
COPY Caddyfile /etc/caddy/Caddyfile # :8080 { reverse_proxy 127.0.0.1:3080 }
COPY entrypoint.sh /entrypoint.sh
ENV DSH_HOME=/data/dsh-home
WORKDIR /workspace
ENTRYPOINT ["/entrypoint.sh"]
# CMD 由面板按实例注入:dsh web --no-open --port 3080 --public-url <url> --trusted-host <host>
```
- `entrypoint.sh`:`tini` 接管信号 → 起 caddy(后台)→ `exec dsh "$@"`。dsh 绑 loopback:3080,caddy 绑 0.0.0.0:8080。
- **存储方式**:生成的镜像仅保存在本机 Docker daemon,不推送 registry,也不从 registry 拉取成品 `dsh-web` 镜像。所有用户共享面板管理的本地镜像目录。
- **版本查询**:面板从 npm 查询 `@deepseek-ai/dsh` 的 `dist-tags.latest`,展示精确版本和查询时间;查询不触发构建或升级。不通过版本字符串排序猜测最新版。查询失败时保留本地版本列表,缓存结果需标明过期。
- **版本固定**:用户确认构建时锁定页面展示的精确版本,后端校验其确实存在于 npm 元数据中;构建过程中不再次解析 `latest`,Dockerfile 不安装浮动的 `@latest`。
- **本机构建**:Docker API `POST /build` 接受 tar 流;面板将内置 `/image/` 的 Dockerfile+caddy+entrypoint+Caddyfile 打包后提交给 daemon,并通过 Docker API 的 buildargs 传入 `DSH_VERSION`。用户不能提交任意 Dockerfile、构建上下文、镜像地址或其他构建参数。
- **镜像身份**:tag 使用 `dsh-web:<dshver>-<recipeRev>`,其中 `recipeRev` 标识镜像模板修订;记录精确 DSH 版本、模板修订、架构、本地 image ID 和构建时间。创建实例使用已确认的 image ID,不依赖可能被覆盖的 tag。
- **构建任务**:手动触发后异步执行,展示排队、构建中、验证中、成功、失败状态及脱敏日志;任务持久化。同版本、模板修订和架构的并发请求复用同一任务,已有可用镜像直接复用。默认全局串行构建,并限制用户触发频率、任务时长及日志大小,构建前检查磁盘空间。
- **可用性判定**:Docker build 成功后,用临时隔离容器验证版本和 DSH 启动就绪;验证通过才加入可选择列表。失败保留错误记录,不自动替换已有可用镜像。面板重启后对账未完成任务及本地镜像,不盲目重复构建。
- **本地清单**:以 Docker inspect/list 和面板管理标签对账,展示精确版本、构建时间、可用状态及使用情况。外部删除导致 image ID 不存在时标记缺失,不再允许新建;管理员清理时保护被实例、待执行任务或回滚流程引用的镜像,不自动运行全局 prune。
- **网络要求**:仅本地保存不代表离线构建;首次构建仍可能需要拉取基础镜像及下载 npm 包。网络故障不影响选择已存在的本地可用镜像。
**创建实例交互**:
1. **选择本地版本**:从本地可用列表选择版本,校验 image ID 仍存在后创建。
2. **构建最新版并创建**:查询并展示精确版本 → 用户确认 → 事务预留实例名额 → 构建或复用任务 → 验证成功 → 用该 image ID 创建实例。等待构建的实例也计入每用户 2 个名额;构建/创建失败或取消时清理未完成资源并释放名额,不回退到其他版本。取消某个用户的等待不取消其他用户共享的构建任务。
3. 面板另提供独立的“构建最新版”按钮,仅生成本地镜像,不创建或升级实例,也不占实例名额。
---
## 6. 容器生命周期(docker.sock + socket-proxy)
Go 端用 `github.com/docker/docker/client`:
- **创建**:
- Image = 本地可用镜像的固定 image ID(对应精确 DSH 版本及模板修订,见 §5)
- Env:`DSH_HOME=/data/dsh-home`(密钥不入 env;用户在 dsh UI 里设 key,落到 `.credentials.yaml`)
- Binds(Source 用 **HOST 路径**):
- `workspaces/<uid>/instances/<instId>/home` → `/data/dsh-home`
- `workspaces/<uid>/files` → `/workspace`
- Network = `dsh-net`(不发布端口)
- Cmd = `web --no-open --port 3080 --public-url https://<panel>/dsh/<instId>/ --trusted-host <panel>`
- Labels:`dsh-mgr.uid`、`dsh-mgr.instId`、`dsh-mgr.version`、`dsh-mgr.role=dsh`
- 资源:`--memory=2g --cpus=2`(可配);`--tmpfs /tmp`;只读 rootfs + 必要写入挂载
- **操作**:start/stop/restart/remove/logs/inspect/exec,全走 SDK。
- **2 实例上限**:按逻辑实例在 DB 事务中预留名额,等待构建/创建中的实例也占名额;失败清理后释放。Docker labels 用于对账,不作为并发名额控制。
- **对账**:面板启动时按 label 枚举容器与 DB 对账(孤儿容器回收或纳管)。
- **sock 安全**:面板挂裸 sock = 等效宿主 root。缓解:
1. **socket-proxy**(推荐):只放行白名单 API(containers/exec/images/networks/volumes/logs/build),关 plugins/daemon/swarm。面板连 proxy 而非裸 sock。
2. 面板自身非 root、只读 rootfs + tmpfs /tmp、资源限制。dsh 容器不 `--privileged`、去 cap。
---
## 7. 网络与访问方式
- 面板**启动时 ensure** `dsh-net`(bridge)存在,并把自身加入(compose 声明或运行时 `network connect`)。
- dsh 容器挂 `dsh-net`,**不发布主机端口**。
- 面板反代 `/dsh/<instId>/` → `http://<container-name>.dsh-net:8080`,容器名 = instId。
- **访问方式(待定,见 §18)**:默认推荐"路径前缀反代 + token 桥接"(§8),面板天然为 ingress。备选:子域名(需泛 DNS+TLS)/ 直发端口(MVP,隔离弱)。
---
## 8. 访问与认证(token 桥接 + cookie 改写)
> 此为最复杂点。推荐方案:路径前缀反代 + token 桥接。
- 面板路由 `/dsh/<instId>/*` → 反代到 `http://<instId>.dsh-net:8080`,**剥掉 `/dsh/<instId>` 前缀**,**保留浏览器 Host**,加 `X-Forwarded-*`。
- **token 桥接**:容器启动后,面板从 `docker logs` 流式抓 dsh 打印的 `http://127.0.0.1:3080/?token=xxx` 里的 token;用户首次打开 `/dsh/<instId>/` 时,面板服务端用该 token 打一次容器换 cookie,再把 `Set-Cookie` 的 `Path=/` 改写为 `Path=/dsh/<instId>`、加 `Secure`/`HttpOnly`/`SameSite=Lax` 下发给浏览器。后续请求带该 cookie 透传。
- **鉴权**:面板先校验用户登录 + 该 instId 归属当前 uid,再放行反代。dsh 自身 token cookie 为第二层。
- **WebSocket/SSE**:反代需支持 WS 升级;cookie 改写后需实测 WS 握手是否正常(关键风险)。
## 9. 工作区文件管理 + 容量配额
**文件 API**(path 校验:解析后必须在 `files/` 内,拒 `..`/绝对路径):
- `GET /api/ws/tree?path=` 列目录(带 size/mtime)
- `POST /api/ws/mkdir`、`POST /api/ws/rename`、`DELETE /api/ws/delete`
- `POST /api/ws/upload`(分片/流式,**写前查配额**)
- `GET /api/ws/download?path=`(单文件流;目录自动 zip)
- `GET /api/ws/usage`(当前用量/配额)
**配额实现**(容器化面板的约束):
- **默认:软配额**(无特权,随处可跑)。面板写前 `du` 校验 + 周期巡检 + 写入并发限流。并发大文件可能短暂超限。
- **可选硬配额**(二选一,需运维确认):
- **XFS prjquota**:面板 `--privileged` + `cap_add:[SYS_ADMIN,CHOWN]` + 把宿主 XFS 挂载点 bind 进来,面板内对 `/data` 跑 `xfs_quota`/`chattr -p`。每用户唯一 project ID(uid+1000)。
- **ZFS refquota**(若宿主用 ZFS):面板 `--privileged` + zfsutils,每用户 `zfs create pool/users/<uid> && zfs set refquota=10G`,再 bind 该 dataset 到 `/data/workspaces/<uid>`。
- 或在宿主跑**极小特权配额 sidecar**(gRPC/HTTP),面板远程调它做 quota 操作——面板自身保持非特权。
- 配额值存 DB,按用户可配(默认 10GiB)。面板展示用量。
> 具体选型见 §18 待拍板。
---
## 10. 配置备份与还原(含模型与插件)
**备份内容**(tar.gz,**排除可重建/运行态**):
| 包含 | 说明 |
|---|---|
| `.credentials.yaml` | 模型 API Key(机密)→ 备份包加密 |
| `cordis.patch.yml`(home 级) | home patch |
| `profiles/web/package.json` | **插件清单**(含 dsh.profile bundle 列表) |
| `profiles/web/cordis.patch.yml` | **模型/provider 配置** + apiKeyEnv 引用 |
| `profiles/web/pnpm-lock.yaml`(若有) | 插件版本锁 |
| `skins/ pets/ agent-presets/ .agent-presets/` | 用户资源 |
| `dsh-ssh.json` | SSH 凭据(0600,加密入包) |
| `manifest.json` | dsh 版本、备份 schema 版、时间戳 |
**排除**:`node_modules/`(pnpm 重建)、`worktrees/`、`sessions/`、日志。
**备份包加密**:用用户密码派生密钥(Argon2id → AES-GCM)加密整包;密钥不落库。下载/上传均加密包。
**备份流程**:停容器(或仅停写)→ tar 指定文件 → 加密 → 落 `backups/<uid>/<instId>/` + 索引入库 → 重启容器。支持立即下载。
**还原流程**:校验+解密包 → 停容器 → 备份当前 home 为 `.pre-restore.bak` → 覆盖配置文件(不动 node_modules)→ **按 `package.json` 重建插件**(`dsh plugin --profile web install` 或 entrypoint 首启时 pnpm reconcile)→ 启容器 → 健康检查 → 失败则回滚 `.pre-restore.bak`。
---
## 11. profile 文件编辑
- 编辑对象:`profiles/web/cordis.patch.yml`(YAML)。
- 前端用 Monaco 编辑器;后端 `GET` 返回原文、`POST` 校验(YAML 合法 + 可选 `--dump-config-schema` 比对)后原子写(临时文件 + rename,`dsh-atomic-write` 风格)。
- 保存后**重启容器**使生效(dsh 有 `dsh-hmr`,但配置类变更稳妥起见重启;后续可探热更)。
- 只读展示 `dsh.profile`(bundle 清单)与 `--dump-config` 预览(面板 `docker exec` 跑 `dsh --profile web --dump-config` 取回渲染)。
- **不暴露** `.credentials.yaml` 原文(write-only 语义)。
---
## 12. 升级流程(保留数据 + 重启新版容器)
```
1. 用户选择本地可用目标版本;若需最新版,先手动触发 §5 构建并等待验证成功。锁定本地 image ID 后才进入备份/停机流程,不自动 pull 成品镜像
2. 自动备份当前配置(§10)→ 标记 pre-upgrade 备份
3. docker stop <old>(不 remove,保留作回滚)
4. docker create <new>:同名 instId、同 binds(home + files)、同 labels、新 image、
Cmd 按新版本要求重算 --public-url/--trusted-host
5. entrypoint 首启:若 profiles/web/node_modules 缺失/与 package.json 不一致 → pnpm install 重建插件
6. docker start <new> → 面板抓 token、健康检查(轮询 caddy :8080)
7. 成功:docker rm <old>;失败:stop <new>、start <old>、标记回滚、提示用户
```
关键:**bind 不变 ⇒ 数据零丢失**;`node_modules` 在新镜像里按 `package.json` 重建(跨版本兼容性由 pnpm/dsh-plugin-manager 保证)。
---
## 13. 安全与资源
- **docker.sock**:面板容器能访问 sock 即等效宿主 root;socket-proxy 白名单 + 面板非 root + 只读 rootfs 缓解。
- dsh 容器:**不 `--privileged`**、去 CAP_NET_RAW 等、只读 rootfs、tmpfs /tmp、mem/cpu 限制、不挂 sock。
- 面板账号:bcrypt + JWT(短期 access + refresh),失败锁定。
- 备份包加密、`.credentials.yaml` 不经 API 明文返回。
- 路径穿越校验、上传大小/类型白名单(可配)。
- 审计日志:容器操作/备份/还原/升级/profile 改动入库。
---
## 14. 技术栈与项目结构
- **后端**:Go 1.22+,`net/http`(或 chi/gin),`github.com/docker/docker/client`,`github.com/docker/go-connections`,sqlite(`modernc.org/sqlite` 纯 Go 驱动,免 CGO),Argon2id、AES-GCM。
- **前端**:Vite + React + TypeScript,Monaco(profile 编辑),shadcn/ui。
```
dsh-mgr/
cmd/server/ # Go 入口
internal/
auth/ # 账号/JWT
docker/ # 容器编排(镜像、生命周期、对账、路径翻译)
workspace/ # 文件 API、配额(软 du + 可选 xfs/zfs)
proxy/ # /dsh/<id> 反代 + token 桥接 + cookie 改写
backup/ # 备份/还原/加密
upgrade/ # 升级编排
profile/ # cordis.patch.yml 编辑
store/ # sqlite schema/查询
api/ # HTTP handlers
web/ # SPA
image/ # 自建 dsh-web 镜像(Dockerfile + Caddyfile + entrypoint.sh + caddy 二进制)
deploy/ # 面板自身 docker-compose / socket-proxy / systemd
```
---
## 15. 数据模型(sqlite 概要)
- `users(id, username, pass_hash, quota_bytes, created_at)`
- `instances(id, uid, name, dsh_version, image_id, build_id, status, container_id, created_at, ...)`
- `backups(id, uid, inst_id, path, size, schema_ver, created_at, kind=auto|manual|pre-upgrade|pre-restore)`
- `images(image_id, version, recipe_rev, platform, tag, status, built_at)`(本地镜像索引,status=validating|available|failed|missing;与 Docker 实际状态对账)
- `image_builds(id, requested_by, version, recipe_rev, platform, status, image_id, error, created_at, finished_at)`(持久化构建任务;同版本+模板修订+架构的活动任务去重)
- `audit(id, uid, action, target, detail, at)`
---
## 16. 面板自身部署(compose 示例)
```yaml
services:
dsh-mgr:
image: dsh-mgr:latest
restart: unless-stopped
ports:
- "80:80" # 前置 TLS 反代或直接 443
user: "65532:65532" # 非 root
environment:
HOST_DATA_ROOT: /var/lib/dsh-mgr
CONTAINER_DATA_ROOT: /data
PANEL_PUBLIC_URL: https://dsh.example.com
DSH_NET_NAME: dsh-net
DEFAULT_QUOTA_BYTES: 10737418240
DOCKER_HOST: unix:///var/run/docker.sock # 或指向 socket-proxy
volumes:
- /var/lib/dsh-mgr:/data
- /var/run/docker.sock:/var/run/docker.sock:ro # 或 socket-proxy
networks: [dsh-net]
read_only: true
tmpfs: [/tmp]
deploy: { resources: { limits: { memory: 1g } } }
# 硬配额(XFS)可选: privileged: true; cap_add: [SYS_ADMIN,CHOWN]; volumes: [xfs mount]
# 可选 socket-proxy
socket-proxy:
image: tecnativa/docker-socket-proxy
environment:
CONTAINERS: 1; EXEC: 1; IMAGES: 1; NETWORKS: 1; VOLUMES: 1; LOGS: 1; BUILD: 1
POST: 1
volumes: [/var/run/docker.sock:/var/run/docker.sock:ro]
networks: [dsh-net]
restart: unless-stopped
networks:
dsh-net: { driver: bridge }
```
---
## 17. 关键风险与待验证
1. **token/cookie 桥接**(§8)最脆——需实测 dsh token 换 cookie 在反代+前缀下的行为,确认 cookie 改写后 **WS/SSE 正常**(dsh web 可能有 WebSocket)。
2. **插件首启重建**:跨 dsh 版本 `pnpm install` 在 entrypoint 无网时失败——可能需升级时由面板 `docker exec` 联网重建后再切流量。
3. **XFS/ZFS 硬配额**依赖宿主 FS 与特权/agent;否则退化为软配额。
4. **dsh agent 工作目录**:`dsh-fs-local`/`dsh-fs-sandbox` 的根是否等于 cwd——需确认 `/workspace` 作为 cwd 即可让 agent 在工作区操作,或需额外配置 fs root。
5. dsh 版本仍为 `0.2.0-rc.*`(RC),API/flag 可能变动——需维护版本兼容矩阵。
6. **sibling bind 路径翻译**:`toHostPath` 出错会导致 bind 挂错目录或挂空目录,需单测覆盖。
7. **本地镜像构建**:验证 npm 查询失败仍可选本地版本、并发构建去重、失败释放实例名额、面板重启对账、镜像被外部删除,以及构建磁盘不足等路径;构建最新版不保证兼容,必须通过启动验证。
---
## 18. 待拍板决策点(open)
**已确认:镜像仅保存在本机;面板支持查询并手动构建最新版;创建实例可选本地已有版本,或构建最新版后创建。不需要 registry/CI 镜像分发。**
> 以下访问方式和配额选型仍待确认;此前审查发现的隔离、文件安全和升级回滚问题仍需另行修订。
1. **访问方式**:路径前缀反代 + token 桥接(推荐,面板天然 ingress)/子域名(需泛 DNS+TLS)/直发端口(MVP,隔离弱)。
- **推荐默认**:路径前缀反代。
2. **配额**:宿主 FS 是 XFS / ZFS / 其他?是否接受"特权面板"或"宿主配额 sidecar"做硬配额?还是软配额默认即可?
- **推荐默认**:软配额(du)起步,硬配额作为后续增强;若宿主为 XFS/ZFS 且可接受特权,则上硬配额。
---
## 19. 里程碑
1. **M1 镜像 + 单容器手动跑通**:自建本地 `dsh-web` 镜像,验证精确版本、caddy+dsh 启动与就绪。
2. **M2 面板骨架**:账号/鉴权 + npm 最新版查询 + 本地镜像列表/手动构建任务 + 创建时选本地版本或构建最新版 + docker 编排(创建/启停/删除/2 上限)+ 对账 + 路径翻译。
3. **M3 反代 + token 桥接**:`/dsh/<id>/` 可用(含 WS)。
4. **M4 工作区文件 API + 软配额**。
5. **M5 备份/还原 + profile 编辑**。
6. **M6 升级流程 + 回滚**。
7. **M7 SPA 完善 + 审计 + 资源限制 + 可选硬配额**。
---
## 附:调研依据(关键 URL)
- 仓库:https://github.com/deepseek-ai/deepseek-harness
- README:https://raw.githubusercontent.com/deepseek-ai/deepseek-harness/master/README.md
- CLI README:https://raw.githubusercontent.com/deepseek-ai/deepseek-harness/master/apps/cli/README.md
- web-app README:https://raw.githubusercontent.com/deepseek-ai/deepseek-harness/master/packages/bundle/web-app/README.md
- providers 指南:https://raw.githubusercontent.com/deepseek-ai/deepseek-harness/master/docs/user/guide/providers.md
- 公网部署指南:https://raw.githubusercontent.com/deepseek-ai/deepseek-harness/master/docs/user/guide/public-deployments.md
- npm 包:https://registry.npmjs.org/@deepseek-ai/dsh/latest