docs: synchronize documentation with current implementation

This commit is contained in:
2026-10-16 11:37:18 +08:00
parent a8ba3d74dc
commit 09e202796f
8 changed files with 307 additions and 310 deletions
+128 -232
View File
@@ -1,8 +1,8 @@
# DeepSeek Harness 多租户容器管理平台 — 设计文档
> 发布架构变更(取代下文旧构建方案):新运行镜像仅由管理员通过 Git 发布检查与精确 commit 确认构建。`release.json`、完整 `image/` 及 runtime 初始化测试仅在 `dockerfile` 发布分支维护,main 不再内置或打包运行镜像资产。下文 npm latest 查询、内置构建上下文、实验构建、构建后创建预约及目录示意中的 `image/` 均为历史设计,不再是操作指南。这是 breaking change:移除 `/api/latest`、`/api/compatibility`、`POST /api/builds` 及 `/api/intents` 的全部查询、创建和取消接口,不提供 410 兼容入口。仅保留 `GET /api/builds` 和 `GET /api/builds/{id}` 构建历史读取。不迁移或恢复旧构建及旧创建意图,不在后台续跑或完成旧预约;这不意味着删除已有实例或数据。当前流程以 README.md、API.md、OPERATIONS.md 为准;以下设计与历史证据保留,不表示新架构已完成相同真实环境验收。
> 当前发布方式为管理员检查Git发布并确认精确commit,本地构建runtime;release.json、image/与初始化测试仅在dockerfile发布分支维护。旧npm/实验构建和构建预约已移除,详见§5。历史验收不自动适用于当前架构,验证范围见ACCEPTANCE.md,部署操作见OPERATIONS.md。
> 状态:本机管理面板核心流程已实现并通过真实 Docker 集成检查;生产隔离、硬配额等仍有差距,以 README.md 的实现边界与验证范围为准。下文是目标设计,不代表所有条目已实现。
> 状态:当前代码具备可信团队管理面板核心流程;历史真实 Docker 验收仅适用于各自记录的版本,不代表当前 Git 发布与全部新增功能已验收。本文“当前实现基线”优先于后面的历史设计;未实现规划不作为已交付能力。验收证据与缺口见 ACCEPTANCE.md。
> 目标项目:`deepseek-ai/deepseek-harness`(npm CLI `@deepseek-ai/dsh`,命令 `dsh web`)
> 控制面技术栈:Go + 前端 SPA
> 部署形态:单机 Docker daemon;面板自身亦运行于容器中
@@ -10,7 +10,54 @@
---
## 0. 调研结论与硬约束
## 当前实现基线(本次源码核对)
本节描述仓库代码,而不是部署状态。本次本地测试与构建结果见 ACCEPTANCE.md 当前记录;代码已实现和自动化通过均不等于真实环境已验证。正文标为“当前实现”的章节是现行方案,历史调研与可选规划另行标注。
### 技术栈与历史 Flutter 的边界
- 当前前端是 `web/` 下的 React 19 + TypeScript + Vite,Tailwind CSS v4、Radix 原语的 shadcn 风格组件、lucide 图标及按需加载的本地 Monaco;入口为 `web/src/main.tsx`、`web/src/App.tsx`。登录默认进入内嵌 DSH,桌面侧栏与移动底栏共用响应式页面;主题跟随系统。
- Flutter/Android 属于历史本地工程痕迹:工作区可能仍有 `android/`、`.dart_tool/`、`build/`、IDE 文件,但当前 Git 跟踪文件没有 Dart 源码或 pubspec。它们不是现行应用、构建输入或受支持的 Android 发布产物,不能据目录存在宣称 Flutter 客户端仍在维护。
- 后端模块要求 Go 1.25(`go.mod`),Docker 构建阶段使用 Go 1.26;标准库 `net/http` + 自有 Docker Engine HTTP client(API v1.47),不是官方 Docker SDK。SQLite 使用 `modernc.org/sqlite`,密码为 bcrypt,配置备份采用 Argon2id + AES-GCM。
```text
cmd/server/ Go 启动、静态 web/dist 服务
internal/api/ HTTP、鉴权、发布构建、实例/文件/代理/配置/升级/恢复编排
internal/docker/ 标准库 Docker Engine HTTP client
internal/backup/ 配置打包与加密
internal/store/ SQLite、会话、实例名额事务
web/src/ React 页面、组件、样式与前端测试
scripts/ 本地检查及需显式运行的集成/镜像冒烟脚本
Dockerfile 构建面板及 React 静态资源,不构建 DSH runtime
compose.yaml 当前面板部署模板
```
`internal/store/intents.go` 当前承载实例名额事务,不表示仍有预约 API。runtime 的 `release.json`、`image/` 与初始化测试在 `dockerfile` 发布分支,main 不包含这些构建资产。
### 架构与功能边界索引
现行网络、路径、目录、构建、生命周期与认证详见§2–8;文件/配置/升级见§9–12。特别注意:每实例home独立,实例删除会清理自身数据及备份;共享文件入口与构建预约均已移除;旧不确定创建“宽限期后404释放”策略已被替代。
### 当前发布与准入流程(详见 §5)
1. 管理员通过 `GET /api/releases/latest` 检查配置的 Git 发布仓库/分支及 manifest;通过 `POST /api/releases/build` 确认精确 commit。分支已变化返回冲突,需重新检查。
2. 构建任务绑定仓库、commit、release ID、版本、部署协议与平台;执行时重新验证信任源和管理员权限,获取锁定 commit 的上下文,本地构建并验证,保留分页历史及详情日志(`releases.go`、`releases_git.go`、`images.go`、`builds_read.go`)。不自动升级已有实例。
3. 镜像准入仍区分推荐、兼容、仅存量与禁止;验证记录及实际镜像身份必须匹配。当前 `supportedLayout` 仅接受协议/模板 7(`admission.go`),不能套用历史“模板 3–6 可正向准入并使用双挂载”的记录。
4. 普通用户仅选择可用本地镜像。旧 latest/compatibility、写 builds、全部 intents 接口已经移除;实验构建、构建预约、旧任务续跑不属于当前能力。
### 当前部署、未实现规划与验证边界
- 当前 `compose.yaml` 为面板 UID/GID 1000、socket 附加组、只读根、tmpfs、资源限制,默认发布 `127.0.0.1:8088`;需配置数据根、DockerRootDir、公开 URL 与可信代理。部署说明见 §16、仓库实际 Compose 和 OPERATIONS.md。
- XFS/ZFS 硬配额、特权配额 sidecar、socket-proxy、子域名入口属于未交付的可选设计,不是现行默认能力;实际公网 TLS/代理、真机与长期稳定性需独立验收。软配额不能阻止容器直接写盘瞬时超限。
- `scripts/check-trial.sh` 当前检查 Go 测试/race/vet/build、前端测试/build 和集成脚本语法,不自动运行真实 Docker、浏览器或发布分支初始化测试。完整新 Git 发布链路及部署验证的通过状态以 ACCEPTANCE.md 的当前矩阵为准,不由历史 M1–M7 结案推导。
---
## 详细设计与历史调研
下文当前实现章节已同步;§0为历史上游调研,§9中的硬配额为可选规划。上游链接及版本调研未在本轮重新验证,不能据此推断未来发行版行为。
## 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;完整代理以集成测试为准。
@@ -35,199 +82,113 @@
## 1. 目标与范围
### 已确认的访问与镜像策略(覆盖下文早期假设)
### 已确认的访问与镜像策略
- 面向可信团队,实例所有者可通过文件管理读取、下载、修改完整 DSH_HOME,包括自己的凭据。面板不承诺凭据 write-only;跨用户隔离、路径安全、配额与恢复状态保护保持不变。下载文件/打包目录会提示泄密风险,提示属于交互防误操作,不是阻止所有者访问的安全边界。
- 专用 Profile 编辑器提供 YAML 校验与受控停启流程,但通用文件操作、DSH 终端和 agent 不强制经过该流程;修改运行中配置存在不一致风险,建议先停机并备份。
- 镜像准入分为推荐、仍兼容、仅存量运行、禁止使用。兼容依据绑定精确版本、模板修订、平台与镜像身份;不再仅因历史模板不是当前模板就一律判为不可用,也不能仅因历史构建成功就认定兼容。
- 未验证 DSH 新版仅管理员可显式实验构建;保留既有完整性、依赖与启动检查。实验构建成功不是兼容验收,未明确准入的镜像不可新建实例或作为升级目标。管理员正向准入须填写验收依据并记录审计,不绕过实际镜像身份与验证记录检查。
- 新版本仅通过管理员 Git 发布流程构建;实验构建已移除。管理员正向准入须填写验收依据并记录审计,不绕过布局、平台、实际镜像身份与验证记录检查。
- 通过 docker.sock 在单机上**创建/启停/删除**多个 `dsh web` 容器,**每用户最多 2 个**。
- 每用户一个**独立工作区**,支持文件**上传/下载/删除/重命名**,并有**容量配额**。
- 工作区**映射进每个 dsh 容器**(agent 在工作区内干活)。
- 每实例独立完整 home,支持文件上传/下载/删除/重命名、文本编辑和图片预览,按账号统计软配额。
- 实例 home 挂入自身容器的 /workspace,不与同账号其他实例共享挂载。
- **备份/还原 dsh 配置**(含模型与插件清单)。
- **编辑 profile 文件**(`cordis.patch.yml`)。
- **升级**:保留数据,用新版本镜像重启容器。
- **本地镜像管理**:面板查询 DSH 最新版本,用户手动触发本机构建;创建实例时选择本地已有版本,或手动构建最新版后创建。不需要私有镜像仓库,不推送生成的镜像。
- **本地镜像管理**:管理员检查 Git 发布并确认精确 commit 构建;用户选择经验证且准入的本地镜像创建,不提供构建预约。不推送生成的镜像。
- 控制面:Go 后端 + SPA 前端;内置账号 + 目录配额;**面板自身容器化运行**。
---
## 2. 总体架构
## 2. 总体架构(当前实现)
面板不是宿主进程,而是挂了 `docker.sock` 的**兄弟容器**(sibling),通过 sock 编排 dsh 兄弟容器,并通过同一 bridge 网络反代它们。
面板是通过 docker.sock 编排 sibling runtime 的 Go 容器,提供 React 静态资源、API、路径反代和 SQLite 持久化。每个实例独立 bridge,面板接入对应网络,runtime 不发布主机端口。容器内 DSH 监听 loopback:3080,由发布镜像内 Caddy 在8080转发;镜像内部实现须以精确发布 commit 为准。
```
┌────────────── 宿主 ──────────────────┐
│ 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
│ └─────────────────────┘ └───────────────┘ │
└─────────────────────────────────────────────┘
```
浏览器 → 面板 SPA/API 或 /dsh/<id>/ → 实例专属 bridge → runtime:8080 → DSH:3080。
组件:
1. **面板服务**(Go 容器):REST API + 静态 SPA + 反向代理子系统 + docker client + 配额/备份/升级引擎。
2. **dsh-web 镜像**(自建):Node + `@deepseek-ai/dsh` + caddy + 入口脚本。
3. **dsh-net**:内部 bridge 网络,容器不发布主机端口,仅面板可达。
4. **(可选)socket-proxy**:面板与 sock 之间的白名单代理。
socket-proxy 仍为可选规划,当前 Compose 直接挂 socket,不代表具备宿主级隔离。
---
## 3. 宿主路径 ↔ 面板内路径映射(关键)
## 3. 宿主路径 ↔ 面板内路径映射(当前实现)
Docker bind 的 `Source` 必须是**守护进程所在宿主路径**,不是面板容器内路径。面板容器把宿主 `/var/lib/dsh-mgr` 挂到 `/data`,于是同一文件树有两个视角:
Docker bind.Source 必须使用 daemon 宿主路径。面板文件操作使用 CONTAINER_DATA_ROOT(Compose 为 /data),通过 internal/api/paths.go 的 toHostPath 映射到 HOST_DATA_ROOT。
| 用途 | 面板直接读写(容器视角) | 创建 dsh 容器 bind.Source(宿主视角) |
| 用途 | 面板路径 | Docker bind.Source / Target |
|---|---|---|
| 用户工作区 | `/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>/...` | (不入容器,仅面板读写) |
| 实例完整 home | /data/workspaces/<uid>/instances/<id>/home | <HOST_DATA_ROOT>/workspaces/<uid>/instances/<id>/home → /workspace |
| 配置备份 | /data/backups/<uid>/<id>/ | 不挂入 runtime |
面板配置:
```
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。
构建上下文是从锁定 Git commit 获取并提交的归档流,不应把历史内置 image/ 当作宿主 bind 源。
---
## 4. 数据与目录布局(宿主机)
## 4. 数据与目录布局(当前实现)
```
/var/lib/dsh-mgr/
db.sqlite # 用户、实例、配额、备份索引
<HOST_DATA_ROOT>/
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 记录镜像索引
instances/<instId>/home/ # 完整 DSH_HOME → /workspace
.credentials.yaml
cordis.patch.yml
profiles/web/ # 配置、插件清单、可重建依赖
storages/ # 实例工作区数据等
files/ # 可能遗留;当前无共享入口或新容器挂载
backups/<uid>/<instId>/ # 加密配置备份;不是完整 home 快照备份
```
**配额作用域**:`workspaces/<uid>/` 整棵树(files + 所有 instances 的 home)。每用户一个配额单位。
账号软配额统计 workspaces/<uid>/,并非只统计当前实例。实例删除会清理该实例目录及对应备份;历史共享 files 不自动合并到 home。旧目录结构和旧删除保留策略不能作为现行承诺。
---
## 5. 自建 DSH 镜像设计
## 5. 自建 DSH 镜像(当前 Git 发布流程)
```
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>
```
管理员检查 GET /api/releases/latest,再以 POST /api/releases/build 确认精确 commit;分支变化必须重新确认。后端从受信任仓库锁定 commit 获取 release.json 与 image/,校验部署协议7及当前面板平台,构建本地镜像并验证。构建记录绑定仓库/commit/release ID/版本/平台,任务去重、限频、容量检查与失败记录仍保留。GET /api/builds 及 GET /api/builds/{id} 提供历史与日志。
- `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 包。网络故障不影响选择已存在的本地可用镜像。
不再从npm查询latest,不接受普通用户实验构建,不打包main内置image/,不提供构建后创建预约。创建实例必须另行选择经验证且准入的本地image ID;管理员构建成功不等于已完成完整兼容验收或自动升级实例。
**创建实例交互**:
1. **选择本地版本**:从本地可用列表选择版本,校验 image ID 仍存在后创建。
2. **构建最新版并创建**:查询并展示精确版本 → 用户确认 → 事务预留实例名额 → 构建或复用任务 → 验证成功 → 用该 image ID 创建实例。等待构建的实例也计入每用户 2 个名额;构建/创建失败或取消时清理未完成资源并释放名额,不回退到其他版本。取消某个用户的等待不取消其他用户共享的构建任务。
3. 面板另提供独立的“构建最新版”按钮,仅生成本地镜像,不创建或升级实例,也不占实例名额。
镜像准入绑定实际身份和成功验证记录;supportedLayout只接受模板7,平台须匹配当前面板平台,内置defaultAdmission规则另有限定linux/amd64。旧模板记录可以保留,不意味着可通过填写说明正向准入。镜像清理保护有效引用,不自动全局prune。构建仍可能需要基础镜像和依赖网络,不能宣称离线构建。
---
## 6. 容器生命周期(docker.sock + socket-proxy)
## 6. 容器生命周期(当前实现)
Go 端用 `github.com/docker/docker/client`:
标准库 Docker HTTP client 使用 API v1.47。创建固定 image ID,事务限制每用户最多两个实例,无构建预约。每实例独立网络与 home,UID/GID 1000,工作目录 /workspace,DSH_HOME/HOME 同为 /workspace,XDG 路径位于其内。启动参数为 web --no-open --port 3080 --trusted-host <host>,不传 --public-url。
- **创建**:
- 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。
当前资源约束为只读 rootfs、drop ALL capabilities、no-new-privileges、2GiB 内存、2 CPU、256 PID 及有界可执行 /tmp。实例支持启停、重启、日志、删除;删除回收容器/网络并清理实例数据与备份。创建不确定时保留恢复依据,不能靠一次或超时后的404清除。孤儿资源仅管理员明确确认并核验后处理,不自动纳管。
---
## 7. 网络与访问方式(当前实现)
## 7. 网络与访问方式
默认已经选择路径前缀代理,不是待拍板选项。面板将 /dsh/<id>/ 转发至对应实例网络中的 runtime:8080;归属检查在代理之前。Compose 默认仅向 loopback 发布面板8088,公网需管理员另配 HTTPS 入口与可信代理。子域名、直接发布 runtime 端口不是当前默认能力。
- 面板**启动时 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 并在服务端交换/缓存 DSH cookie;token 按容器身份/启动时间加密持久化。反代剥前缀、处理 Host 与转发头,注入服务端 cookie 并移除上游 Set-Cookie,不把 DSH cookie 改写下发给浏览器。客户端持有的是面板认证 cookie。
## 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 握手是否正常(关键风险)。
WS/SSE 由反代支持;会话续期由面板/runtime 注入脚本协调。上游拒绝 cookie 时失效缓存,下次请求重新交换,不重放写请求。已建立 WS/SSE 不承诺即时撤销;真实部署的长会话、插件与公开入口仍需分别验证。
## 9. 工作区文件管理 + 容量配额
**文件 API**(path 校验:解析后必须在 `files/` 内,拒 `..`/绝对路径):
**文件 API**(除 usage 外必须提供 instance 并校验归属;path 限于该实例 home,拒穿越、绝对路径和不安全符号链接):
- `GET /api/ws/tree?path=` 列目录(带 size/mtime)
- `POST /api/ws/mkdir`、`POST /api/ws/rename`、`DELETE /api/ws/delete`
- `POST /api/ws/upload`(分片/流式,**写前查配额**)
- `POST /api/ws/upload`(有界上传、写前查配额;不是分片续传协议)
- `GET/PUT /api/ws/text`(文本读取/带版本冲突检查的保存)、`GET /api/ws/image`(受控图片读取)
- `GET /api/ws/download?path=`(单文件流;目录自动 zip)
- `GET /api/ws/usage`(当前用量/配额)
**配额实现**(容器化面板的约束):
- **默认:软配额**(无特权,随处可跑)。面板写前 `du` 校验 + 周期巡检 + 写入并发限流。并发大文件可能短暂超限。
- **默认:软配额**。面板写前遍历统计账号目录用量,写入串行化并限制并发,每五分钟巡检记录超限。容器/agent直接写盘不受面板写锁约束,仍可超过软配额;不通过删用户数据恢复容量。
- **可选硬配额**(二选一,需运维确认):
- **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 待拍板。
> 当前仅软配额;上述 XFS/ZFS 和特权 sidecar 是未实现规划,不是可直接启用的配置。
---
@@ -246,21 +207,21 @@ Go 端用 `github.com/docker/docker/client`:
| `dsh-ssh.json` | SSH 凭据(0600,加密入包) |
| `manifest.json` | dsh 版本、备份 schema 版、时间戳 |
**排除**:`node_modules/`(pnpm 重建)、`worktrees/`、`sessions/`、日志。
**范围**:仅上述六个固定配置文件、四类资源树和生成的manifest;不是完整home备份,通常位于白名单外的工作区、会话和插件node_modules不会入包。资源树按路径递归纳入普通文件,不根据文件内容识别运行态或凭据;不安全路径、符号链接及特殊文件会被拒绝。
**备份包加密**:用用户密码派生密钥(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`。
**还原流程**:校验并解密包,受控停机、保存home恢复快照,应用配置与协调插件,启动验证后恢复应有运行状态;失败尝试快照回滚,失败恢复可能需人工介入。实际快照命名和恢复记录以config.go及运维文档为准,不提供任意第三方插件兼容保证。
---
## 11. profile 文件编辑
- 编辑对象:`profiles/web/cordis.patch.yml`(YAML)。
- 前端用 Monaco 编辑器;后端 `GET` 返回原文、`POST` 校验(YAML 合法 + 可选 `--dump-config-schema` 比对)后原子写(临时文件 + rename,`dsh-atomic-write` 风格)。
- 保存后**重启容器**使生效(dsh 有 `dsh-hmr`,但配置类变更稳妥起见重启;后续可探热更)。
- 前端用本地 Monaco;后端 `GET` 返回原文,`PUT` 校验 YAML 后原子写入,专用编辑器修改前需停止实例。未实现的 schema 比对不作为已交付能力。
- 页面“保存并启动”发送 `restart:true`,写入后启动并检查就绪;API省略该参数时仅保存,不自动启动。
- 只读展示 `dsh.profile`(bundle 清单)与 `--dump-config` 预览(面板 `docker exec` 跑 `dsh --profile web --dump-config` 取回渲染)。
- 专用配置预览保持脱敏;完整 home 文件管理允许实例所有者读取 `.credentials.yaml` 原文,不承诺 write-only。
@@ -269,23 +230,23 @@ Go 端用 `github.com/docker/docker/client`:
## 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>、标记回滚、提示用户
1. 用户选择本地可用目标版本;若无目标镜像,先由管理员完成 §5 Git 发布构建。锁定已验证且准入的 image ID 后才进入备份/停机流程
2. 停止原本运行的旧容器并检查确实已停机(不 remove,保留作回滚)
3. 创建有界home快照,再用当前登录密码创建 pre-upgrade 配置备份
4. 保留旧容器回滚依据,创建替换容器:同逻辑实例、同home挂载、新image,不挂共享files
Cmd 使用 web/no-open/port/trusted-host,不传 --public-url
5. 启动新容器;发布镜像入口负责插件协调,其离线能力须针对精确镜像验证
6. 面板抓 token 并验证就绪
7. 成功删除旧容器;失败尝试清理新容器、恢复home快照及旧容器原运行状态,回滚失败保留恢复依据并提示人工处理
```
关键:**bind 不变 ⇒ 数据零丢失**;`node_modules` 在新镜像里按 `package.json` 重建(跨版本兼容性由 pnpm/dsh-plugin-manager 保证)。
历史设计意图:保持 bind 并重建插件依赖以尽量保留数据;**这不保证零丢失或任意插件跨版本兼容**。当前升级还依赖备份、快照、健康检查和回滚,故障及兼容性必须实测。
---
## 13. 安全与资源
- **docker.sock**:面板容器能访问 sock 即等效宿主 root;socket-proxy 白名单 + 面板非 root + 只读 rootfs 缓解。
- **docker.sock**:面板容器能访问 sock 即等效宿主 root。当前非root进程和只读rootfs不能消除该权限;socket-proxy白名单只是尚未实施的可选增强。
- dsh 容器:**不 `--privileged`**、去 CAP_NET_RAW 等、只读 rootfs、tmpfs /tmp、mem/cpu 限制、不挂 sock。
- 面板账号:bcrypt + JWT(短期 access + refresh),失败锁定。
- 配置备份包加密;实例所有者通过文件 API 下载的凭据或目录 ZIP 不属于加密配置备份,应按机密数据保管。
@@ -294,118 +255,53 @@ Go 端用 `github.com/docker/docker/client`:
---
## 14. 技术栈与项目结构
## 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。
当前 Go/React 栈和实际目录见文首基线:Go 模块版本1.25、net/http、自有 Docker HTTP client、modernc SQLite;React 19、TypeScript、Vite、Tailwind v4、Radix 原语、Monaco。不是 Flutter 客户端,也不是独立 auth/workspace/proxy/upgrade 包的原设计拆分;多数编排位于 internal/api。
```
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
```
旧设想中的 main/image/、deploy/ 不能用于当前构建或部署:runtime 资产在 dockerfile 发布分支,面板使用根目录 Dockerfile 和 compose.yaml。
---
## 15. 数据模型(sqlite 概要)
## 15. 数据模型(当前实现概要)
- `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)`
SQLite基础表users、sessions、instances、audit由internal/store初始化;认证轮换、构建/镜像准入、发布身份、runtime创建/网络恢复和升级恢复等表由相应API模块维护。实际字段与迁移以源码为准,不将早期设计的示意DDL当作数据库接口。
image_builds包含任务、验证及发布来源信息;image_admissions绑定精确image ID/version/recipe/platform及验收依据;runtime_creates/runtime_networks保留资源恢复依据。旧表或文件名存在不代表旧预约服务仍运行。
---
## 16. 面板自身部署(compose 示例)
## 16. 面板自身部署(当前实现)
```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]
以根目录 compose.yaml、.env.example 和 OPERATIONS.md 为操作入口,不复制历史80/443或共享bridge样例。默认面板绑定127.0.0.1:8088,运行用户1000:1000,附加宿主socket组;只读rootfs、/tmp tmpfs、1GiB内存、2 CPU、512 PID及健康检查。
# 可选 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 }
```
配置 HOST_DATA_ROOT / CONTAINER_DATA_ROOT、DockerRootDir只读容量探测挂载、PANEL_CONTAINER_ID,以及实际公开URL和可信代理CIDR。裸docker.sock挂载并未被只读rootfs降权,仍需信任面板与账号。当前模板不部署socket-proxy或硬配额sidecar。
---
## 17. 关键风险与待验证
1. **token/cookie 桥接**(§8)最脆——需实测 dsh token 换 cookie 在反代+前缀下的行为,确认 cookie 改写后 **WS/SSE 正常**(dsh web 可能有 WebSocket)。
1. **token/cookie 桥接**(§8):需在实际入口验证服务端cookie交换与注入、前缀代理及WS/SSE,不以本地模拟替代部署验收。
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 查询失败仍可选本地版本、并发构建去重、失败释放实例名额、面板重启对账、镜像被外部删除,以及构建磁盘不足等路径;构建最新版不保证兼容,必须通过启动验证。
7. **Git 发布构建**:须验证精确 commit 确认与分支变化、信任源/权限变化、构建去重及恢复、外部删除镜像、容量及网络故障。构建成功不等于完整兼容验收,不存在旧构建预约释放名额流程。
---
## 18. 待拍板决策点(open)
## 18. 已决策与后续规划
**已确认:镜像仅保存在本机;面板支持查询并手动构建最新版;创建实例可选本地已有版本,或构建最新版后创建。不需要 registry/CI 镜像分发。**
已选择每实例独立bridge、路径前缀反代、服务端cookie桥接、账号软配额、React响应式Web及管理员Git发布本地镜像。旧npm最新构建/预约、共享工作区和Flutter发布不是当前路线。
> 以下访问方式和配额选型仍待确认;此前审查发现的隔离、文件安全和升级回滚问题仍需另行修订。
可选后续增强:XFS/ZFS硬配额或宿主配额sidecar、socket-proxy、独立域名隔离。上述未纳入当前默认交付;是否实施需另行设计、授权和验收,不能以文档列出视为支持。
1. **访问方式**:路径前缀反代 + token 桥接(推荐,面板天然 ingress)/子域名(需泛 DNS+TLS)/直发端口(MVP,隔离弱)。
- **推荐默认**:路径前缀反代。
2. **配额**:宿主 FS 是 XFS / ZFS / 其他?是否接受"特权面板"或"宿主配额 sidecar"做硬配额?还是软配额默认即可?
- **推荐默认**:软配额(du)起步,硬配额作为后续增强;若宿主为 XFS/ZFS 且可接受特权,则上硬配额。
## 19. 里程碑与交付判定
---
M1镜像、M2账号/生命周期、M3代理、M4文件/软配额、M5配置备份、M6升级回滚、M7界面/管理安全保留为能力分组,不再以旧npm构建及预约作为当前M2标准。当前Git发布、实例home独占、删除清理、文本编辑/图片预览与主题等变更须按ACCEPTANCE.md当前矩阵分别补证据。
## 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 完善 + 审计 + 资源限制 + 可选硬配额**。
历史M1–M7结案不等于当前commit或真实部署整体通过;本地测试通过、真实Docker通过、浏览器通过与公网验收必须分开记录。
---