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
+42 -20
View File
@@ -2,7 +2,39 @@
> 发布架构变更说明:下文为各阶段的历史验收证据,保留当时命令、测试数量及结论,不作为当前操作指南。main 已移除内置 runtime 构建资产;`release.json`、完整 `image/` 和初始化测试仅在 `dockerfile` 发布分支维护,新构建仅允许管理员通过 Git 发布检查并确认精确 commit。旧 npm/实验构建及构建后创建预约流程已被取代,这是 breaking change:移除 `/api/latest`、`/api/compatibility`、`POST /api/builds` 及 `/api/intents` 的全部查询、创建和取消接口,不提供 410 兼容入口。仅保留 `GET /api/builds` 和 `GET /api/builds/{id}` 构建历史读取。不迁移或恢复旧构建及旧创建意图,不在后台续跑或完成旧预约;这不意味着删除已有实例或数据。旧 `scripts/build-image.sh` 与 `scripts/test-intent-live.mjs` 已删除;`scripts/test-panel.mjs` 现在复用预先验证且准入的本地镜像。历史通过记录不等于新 Git 发布架构已通过真实部署验收,当前使用方式见 README.md、API.md、OPERATIONS.md。 > 发布架构变更说明:下文为各阶段的历史验收证据,保留当时命令、测试数量及结论,不作为当前操作指南。main 已移除内置 runtime 构建资产;`release.json`、完整 `image/` 和初始化测试仅在 `dockerfile` 发布分支维护,新构建仅允许管理员通过 Git 发布检查并确认精确 commit。旧 npm/实验构建及构建后创建预约流程已被取代,这是 breaking change:移除 `/api/latest`、`/api/compatibility`、`POST /api/builds` 及 `/api/intents` 的全部查询、创建和取消接口,不提供 410 兼容入口。仅保留 `GET /api/builds` 和 `GET /api/builds/{id}` 构建历史读取。不迁移或恢复旧构建及旧创建意图,不在后台续跑或完成旧预约;这不意味着删除已有实例或数据。旧 `scripts/build-image.sh` 与 `scripts/test-intent-live.mjs` 已删除;`scripts/test-panel.mjs` 现在复用预先验证且准入的本地镜像。历史通过记录不等于新 Git 发布架构已通过真实部署验收,当前使用方式见 README.md、API.md、OPERATIONS.md。
## 恢复宽限期与轮询缓存修复(独立复审后) ## 当前实现与验收状态(2026-10-16 文档核对)
本节优先于后面的历史记录。已完整阅读两份文档并对照当前源码;本地验证由本次维护任务的独立验证执行者完成并回报,不将代码存在、模拟测试或旧部署证据冒充当前真实环境通过。
### 本次实际验证
- `bash scripts/check-trial.sh`:退出0;Go普通测试/race各5包通过,vet/build通过;前端25文件198测试通过,TypeScript/Vite生产构建通过;`node --check scripts/test-panel.mjs`通过。
- `node --test scripts/check-trial.test.mjs`:10/10通过。
- 两项Docker live测试因未启用`DSH_DOCKER_LIVE_TEST`跳过,**不计真实Docker通过**。本次未执行真实浏览器、公网入口或完整Git发布部署验收,未调用模型或搜索。
- 历史测试数量、镜像ID、截图及/tmp路径仅证明当时记录;本轮未逐一重新获取这些临时证据,不代表当前线上状态。
### 当前能力矩阵
| 范围 | 当前代码与本地验证 | 当前版本仍缺的真实证据 |
|---|---|---|
| 前端与架构 | React19/TS/Vite、Tailwind4/Radix、Monaco;Go标准库Docker HTTP client、SQLite;本轮自动化/构建通过 | 当前UI浏览器全流程与真机;Flutter/Android仅历史工作区残留,无当前原生发布验收 |
| Git发布与准入 | 管理员检查发布/精确commit、本机构建验证/历史;模板7与当前平台身份检查;本地测试通过 | 真实发布→构建→验证→准入→创建完整链路、故障及重启恢复;不能沿用旧npm/实验构建结论 |
| 实例与存储 | 两实例事务名额、每实例bridge/home,/workspace为DSH_HOME;删除清理实例数据及备份;无预约或共享入口 | 当前删除语义、并发名额与不确定创建恢复的真实Docker复验;旧“删除保留数据”证据不适用 |
| 代理与会话 | 归属鉴权、服务端token/cookie桥接、WS/SSE与续期;本地测试通过 | 当前部署iframe、独立页、长时/后台/多页续期与真实WS/SSE;历史615秒结果不是当前整体重验 |
| 文件与配额 | 完整home、上传下载/ZIP/文本编辑/图片预览、账号软配额;本地测试通过 | 当前浏览器编辑冲突/图片显示/敏感确认、跨用户访问、真实配额耗尽及容器直接写盘边界 |
| 配置与升级 | Profile校验/预览、密码加密配置备份/导入/恢复、快照/回滚;本地测试通过 | 当前跨版本/插件重建/离线失败/旧包兼容及回滚数据保留;同版本替换不证明跨版本迁移 |
| 部署与安全 | 登录/refresh/CSRF/审计/改密、非root/只读/资源限制;本地测试通过 | 实际域名HTTPS/代理、socket权限、真实浏览器;不宣称不可信公网多租户隔离或生产SLA |
| 可选增强 | XFS/ZFS硬配额、配额sidecar、socket-proxy未作为当前默认交付 | 未实现/未验收,不标通过;不列为默认软配额流程失败项 |
关键纠偏:除usage外文件API不带instance返回410,新容器不挂/shared;准入布局仅支持模板7,旧模板3–6不能靠说明正向准入。实例删除会清理自身目录和备份,孤儿回收是另一流程。创建不确定时不凭超时404清除依据。见`internal/api/files.go`、`runtime.go`、`instance_data.go`、`admission.go`、`resource_recovery.go`。
结论:**本次本地自动化与构建通过;当前完整真实部署/浏览器验收未完成,不作M1–M7整体结案。**
## 历史阶段记录(以下均非当前版本整体验收)
各节“当前”“本轮”“最终”“已部署”均指该节当时版本;旧命令可能已移除。旧结案不覆盖文首矩阵;各历史阶段之间的覆盖关系仅适用于对应迭代。
## 恢复宽限期与轮询缓存修复(历史,已被后续修复替代)
范围:本轮为对上述工作树的独立代码审计与全量重测后的修复。审计确认八项修复中六项正确,发现三处残留:创建结果不确定的挂起标志永不过期导致实例永久无法删除(高)、构建校验不确定行无限重试并持续新增审计(中)、意图轮询每秒对每个成功镜像发起 Docker inspect(中)。修复未改变凭据文件管理边界或准入策略;未部署、未操作真实 Docker 实例。 范围:本轮为对上述工作树的独立代码审计与全量重测后的修复。审计确认八项修复中六项正确,发现三处残留:创建结果不确定的挂起标志永不过期导致实例永久无法删除(高)、构建校验不确定行无限重试并持续新增审计(中)、意图轮询每秒对每个成功镜像发起 Docker inspect(中)。修复未改变凭据文件管理边界或准入策略;未部署、未操作真实 Docker 实例。
@@ -185,7 +217,7 @@
用户确认的孤儿策略:管理员二次确认回收身份/挂载核验通过、无实例/升级引用的停止容器;不自动纳管,不触碰运行中或来源不明容器,不删数据/网络。其他架构替代见下表;不声称逐字遵循SDK、组件库或目录示意。 用户确认的孤儿策略:管理员二次确认回收身份/挂载核验通过、无实例/升级引用的停止容器;不自动纳管,不触碰运行中或来源不明容器,不删数据/网络。其他架构替代见下表;不声称逐字遵循SDK、组件库或目录示意。
## 早期进度记录(以下由上方最新结论覆盖) ## 后续UI改版记录(历史独立阶段,不由2026-10-10结案覆盖)
## UI 改版与功能回归(2026-10-15) ## UI 改版与功能回归(2026-10-15)
@@ -199,34 +231,22 @@
- 本次升级仅覆盖 0.2.0-rc.2 到相同版本的容器替换;跨版本升级、插件重建、离线失败回滚、长时会话续期,以及真实配额耗尽/跨用户实例访问未在本轮完整实测,不计为通过。未调用付费模型。 - 本次升级仅覆盖 0.2.0-rc.2 到相同版本的容器替换;跨版本升级、插件重建、离线失败回滚、长时会话续期,以及真实配额耗尽/跨用户实例访问未在本轮完整实测,不计为通过。未调用付费模型。
- 一次性测试实例及其容器/网络均已清理;原有 DSH 实例未重启。测试日志及隔离备份保留于 /tmp/dsh-isolated-integration-BFUCkK/(本地临时证据,不随提交分发)。 - 一次性测试实例及其容器/网络均已清理;原有 DSH 实例未重启。测试日志及隔离备份保留于 /tmp/dsh-isolated-integration-BFUCkK/(本地临时证据,不随提交分发)。
## 当前实现与待验收 ## 早期实现与待验收表(已归档)
| 设计章节 | 代码已接入 | 尚需真实证据 | 原表描述模板2、npm构建及预约阶段,已由文首当前矩阵替代。早期未闭项须按各阶段证据解释,不能推导当前Git发布通过。配置备份为白名单包,不含完整工作区、依赖或会话;旧密钥包仍须保管原key。操作中断或回滚失败不保证自动灾难恢复。
|---|---|---|
| §1–6 | sibling路径映射、每用户两实例、持久预约/取消/失败恢复、生命周期与身份对账 | 模板2非root全流程、刷新/重启预约恢复、并发名额 |
| §5 | 精确npm版本、模板/平台身份、构建去重/限频/磁盘预检、镜像usage/missing及管理员清理 | 真实构建与删除、外部删镜像、空间/网络故障及重启对账 |
| §7–8 | 路径反代、归属鉴权、服务端token/cookie桥接、WS/SSE | 新部署的新浏览器完整流程、重启重新认证、长时续期 |
| §9 | 文件管理、目录ZIP、写前配额检查/串行化、周期巡检、管理员额度更新 | 新UI文件流程、agent实际读写、面板各写入路径配额覆盖 |
| §10 | 密码加密tar.gz v2、配置/资源目录/manifest、外部包导入、旧key包兼容、停启/健康检查/回滚 | 真实资源往返、原密码/错误密码、无key新包、旧key包、插件恢复失败回滚 |
| §11 | 本地Monaco、YAML校验、bundle与脱敏dump-config预览、保存后启动验证 | 新浏览器编辑/预览/错误路径;当前仍需先停止再保存 |
| §12 | 预升级密码备份、home快照、成功删旧、失败回滚及恢复原运行状态 | 不同DSH版本、真实插件重建/离线失败、故障矩阵与数据保留 |
| §13–16 | 短JWT/refresh轮换/失败锁定、审计、资源限制、非root部署配置 | 模板2非root数据写入和socket访问、新浏览器及DSH独立页长JWT续期 |
| §17–19 | 默认路径反代与软配额;已有回归测试及旧版集成记录 | 必需流程未关闭,不标记M1–M7整体完成;硬quota为可选增强 |
备份包含六个固定配置文件及skins、pets、agent-presets、.agent-presets资源树和manifest,不包含node_modules、会话或工作区。新密码包不依赖backup.key;保留旧密钥包时仍须保管原key。备份/恢复/升级不再统一要求用户手动停机;操作中断或回滚失败不保证自动灾难恢复。 ## 历史合理替代方案(不改变验收目标)
## 合理替代方案(不改变验收目标)
| DESIGN选型 | 当前实现 | 等价目标与边界 | | DESIGN选型 | 当前实现 | 等价目标与边界 |
|---|---|---| |---|---|---|
| 官方Docker SDK | stdlib net/http Docker Engine client,API v1.47 | 仍须验证编排、构建流、错误处理和daemon兼容性,不能以未用SDK认定功能缺失 | | 官方Docker SDK | stdlib net/http Docker Engine client,API v1.47 | 仍须验证编排、构建流、错误处理和daemon兼容性,不能以未用SDK认定功能缺失 |
| 共享bridge | 每实例独立bridge,面板接入 | 减少实例直接互通;不等于完整出站或宿主网络隔离 | | 共享bridge | 每实例独立bridge,面板接入 | 减少实例直接互通;不等于完整出站或宿主网络隔离 |
| DSH cookie改写后下发浏览器 | 服务端交换/缓存cookie,反代注入并移除上游Set-Cookie | 保留面板登录与实例归属鉴权;WS/SSE、重启失效与续期仍需实测 | | DSH cookie改写后下发浏览器 | 服务端交换/缓存cookie,反代注入并移除上游Set-Cookie | 保留面板登录与实例归属鉴权;WS/SSE、重启失效与续期仍需实测 |
| shadcn/ui | 普通React组件及CSS,保留本地Monaco | 不要求替换组件库,但交互、错误反馈和可用性仍须浏览器验收 | | shadcn/ui | 当时为普通React/CSS;现为Tailwind v4及Radix原语的shadcn风格组件,保留Monaco | 历史确认不替代当前UI浏览器验收 |
代码主要按internal/api、internal/docker、internal/backup、internal/store组织,与设计目录示意不同;不宣称逐字一致。默认软配额符合§9/§18路线,XFS/ZFS硬quota依赖运维选择,不列为默认必过项。npm 0.2.0-rc.2不支持--public-url,当前使用路径代理而不传该参数。 代码主要按internal/api、internal/docker、internal/backup、internal/store组织,与设计目录示意不同;不宣称逐字一致。默认软配额符合§9/§18路线,XFS/ZFS硬quota依赖运维选择,不列为默认必过项。npm 0.2.0-rc.2不支持--public-url,当前使用路径代理而不传该参数。
## 历史证据与本轮边界 ## 早期历史证据与当时边界(非当前待办结论)
本轮已取得真实证据: 本轮已取得真实证据:
- 非root面板 `dsh-design-check`(1000:1000、只读rootfs、socket附加组989)已启动;33项HTTP认证/刷新/配额/ZIP/Range检查通过,报告 `/tmp/dsh-design-check.8ekWaC/api-check-results.txt`。 - 非root面板 `dsh-design-check`(1000:1000、只读rootfs、socket附加组989)已启动;33项HTTP认证/刷新/配额/ZIP/Range检查通过,报告 `/tmp/dsh-design-check.8ekWaC/api-check-results.txt`。
@@ -247,7 +267,9 @@
- 面板已部署 `dsh-mgr:workspace6`;正常构建 API 记录 `e9a368bbb66e51e6c8f2439a8e94698a` 成功,recipe_rev=6,镜像 `sha256:38e04308da602da22e8df49d54166da1835922d50a3fa485711b7c5f6479d287`。 - 面板已部署 `dsh-mgr:workspace6`;正常构建 API 记录 `e9a368bbb66e51e6c8f2439a8e94698a` 成功,recipe_rev=6,镜像 `sha256:38e04308da602da22e8df49d54166da1835922d50a3fa485711b7c5f6479d287`。
- 面板部署保留配置和挂载,旧面板停止留作回滚;现有 DSH 实例未重启,构建前后实例记录一致。 - 面板部署保留配置和挂载,旧面板停止留作回滚;现有 DSH 实例未重启,构建前后实例记录一致。
## 模板 7:完整 DSH_HOME 改为 /workspace ## 历史模板7迁移:完整 DSH_HOME 改为 /workspace
> 本节的/shared挂载、共享入口与迁移结果仅描述当时方案;当前共享入口和新容器共享挂载均已移除,实例删除会清理自身数据及备份,详见文首矩阵。
### 管理交互优化验收 ### 管理交互优化验收
+49 -22
View File
@@ -1,6 +1,11 @@
# DSH Web 面板 API(MVP) # DSH Web 面板 API(MVP)
默认监听 127.0.0.1:8088。JSON 错误统一为 {error:string}。登录使用 HttpOnly session/access/refresh cookie;写请求需 X-CSRF-Token 和精确同源 Origin(未配置 PANEL_PUBLIC_URL 时也校验)。access JWT 有效期5分钟,refresh轮换且受24小时session限制。 默认监听 127.0.0.1:8088。面板处理器通过 fail 返回的错误为 JSON {error:string};未注册路由的标准 404、静态文件/下载错误及运行时上游响应不保证此格式。面板非 GET 请求(包括 HEAD/OPTIONS)需 X-CSRF-Token 和精确同源 Origin,登录仅要求 Origin(未配置 PANEL_PUBLIC_URL 时按请求 TLS 与 Host 校验)。运行时代理使用下文独立规则。access JWT 有效期5分钟,refresh轮换但不延长 session 的绝对到期时间,session 默认24小时,可用 SESSION_TTL 修改。
除另有说明,成功返回 200;创建用户、实例、备份也不是 201。下述处理器状态码以通过前置认证、Origin/CSRF 和资源检查为前提,前置失败可能优先返回 401/403/409。普通业务接口需要同时有效的 dsh_session 与 dsh_access,不支持以 Authorization Bearer 替代。
- /healthz:无需认证,返回 200 和纯文本 ok;当前实现不限制方法,也不检查 Docker/数据库健康。
- GET /api/runtime-session.js:无需登录,返回 JavaScript、Cache-Control: no-store,用于运行时页面续期。
## 配置 ## 配置
- UPLOAD_ALLOWED_EXTENSIONS:可选逗号分隔扩展名白名单(例如 .txt,.md);空表示不限制。大小写不敏感,非内容/恶意软件检测。 - UPLOAD_ALLOWED_EXTENSIONS:可选逗号分隔扩展名白名单(例如 .txt,.md);空表示不限制。大小写不敏感,非内容/恶意软件检测。
@@ -12,33 +17,39 @@ GET /api/orphans 返回可回收的完整容器ID数组。DELETE /api/orphans?id
## 其他配置 ## 其他配置
- LISTEN_ADDR(默认 127.0.0.1:8088) - LISTEN_ADDR(默认 127.0.0.1:8088)
- DATA_ROOT(默认 ./data) - DATA_ROOT(默认 ./data)
- HOST_DATA_ROOT(默认等于 DATA_ROOT 的绝对路径,用于 Docker bind source;容器部署必须显式设置宿主绝对路径) - HOST_DATA_ROOT(默认等于实际 CONTAINER_DATA_ROOT 的绝对路径,用于 Docker bind source;容器部署必须显式设置宿主绝对路径)
- CONTAINER_DATA_ROOT(默认 DATA_ROOT) - CONTAINER_DATA_ROOT(默认 DATA_ROOT)
- DOCKER_HOST(默认 unix:///var/run/docker.sock) - DOCKER_HOST(默认 unix:///var/run/docker.sock)
- PANEL_PUBLIC_URL(可选) - PANEL_PUBLIC_URL(可选 http/https origin,可带根路径 /,不可带其它路径、用户信息、查询或片段;非法值拒绝启动)
- TRUSTED_PROXY_CIDRS(可选逗号分隔 CIDR;仅信任这些代理的 X-Forwarded-For 来计算登录来源限流,不替代 Origin 校验;非法值拒绝启动)
- SESSION_TTL(默认 24h;Go duration,至少1秒且必须为整秒;非法值拒绝启动,只影响新建 session)
- ADMIN_USERNAME / ADMIN_PASSWORD(首次启动创建管理员;密码必须 12–72 字节,不设置则拒绝启动,不打印密码) - ADMIN_USERNAME / ADMIN_PASSWORD(首次启动创建管理员;密码必须 12–72 字节,不设置则拒绝启动,不打印密码)
- WEB_DIR(可选静态前端目录) - WEB_DIR(静态前端目录,默认 web/dist)
- PANEL_CONTAINER_ID(面板容器名/ID,用于加入实例网络;宿主进程运行时留空) - PANEL_CONTAINER_ID(面板容器名/ID,用于加入实例网络;宿主进程运行时留空)
- DEFAULT_QUOTA_BYTES(默认10GiB) - DEFAULT_QUOTA_BYTES(默认10GiB)
- DSH_DOCKER_ROOT_VISIBLE(与DockerRootDir相同路径的明确映射,用于构建前容量检查;未知容量拒绝新构建) - DSH_DOCKER_ROOT_VISIBLE(与DockerRootDir相同路径的明确映射,用于构建前容量检查;未知容量拒绝新构建)
- DSH_RELEASE_REPOSITORY(默认 https://git.misaka.ren/m1saka/dsh-web.git;仅接受无用户信息、查询或片段的 HTTPS 仓库地址)
- DSH_RELEASE_BRANCH(默认 dockerfile;Git 发布检查和构建使用的分支)
## 认证 ## 认证
- POST /api/login {username,password} → {user,csrf_token};user 字段为 id,username,admin,quota_bytes,created_at - POST /api/login {username,password} → {user,csrf_token};user 字段为 id,username,admin,quota_bytes,created_at
- POST /api/logout → {ok:true} - 登录请求体最多4096字节,非法/多段 JSON 返回400,凭据错误401,Origin错误403;每来源每分钟最多30次、账户累计5次失败锁定15分钟,限流/锁定429,认证不可用或并发槽耗尽503。
- POST /api/refresh → {user,csrf_token}(刷新HttpOnly cookie,需Origin和CSRF;并发轮换冲突且session仍有效时返回409,不签发cookie,客户端可复验 `/api/me` 后重试原请求;会话撤销/过期返回401,存储异常不能当作注销) - cookie 均为 SameSite=Lax:HttpOnly 的 dsh_session/dsh_access 路径 /,dsh_refresh 路径 /api;另有可供脚本读取的 dsh_csrf(路径 /,非 HttpOnly)。请求使用 TLS 或 PANEL_PUBLIC_URL 为 HTTPS 时设置 Secure。
- POST /api/logout → {ok:true}(不要求有效 access,但仍需有效 session、Origin 和 CSRF;撤销当前会话并清除四个 cookie,不是无条件幂等的匿名退出接口)
- POST /api/refresh → {user,csrf_token}(需 session、refresh cookie、Origin 和 CSRF,不要求 access 尚有效;接受当前 refresh 或紧邻的上一代 refresh,上一代重试幂等返回同一个后继 token 并签发 cookie,不返回轮换冲突409。更旧/无效 refresh 返回401;过期/撤销 session 通常先由 guard 返回403,处理器复查失败也可能401;存储异常在处理器返回500/503,但 guard 的 session 查询失败也会403)
- GET /api/me → {user,csrf_token} - GET /api/me → {user,csrf_token}
- POST /api/me/password {current_password,password} → {ok:true,reauthenticate:true}(普通用户及管理员仅修改本人;不接收目标 ID,未知 JSON 字段返回 400)。需有效 access/session、同源 Origin 和 X-CSRF-Token;新密码为 UTF-8 12–72 字节,非法 JSON、缺少当前密码或长度不符返回 400。错误当前密码返回 400 而非 401,客户端不应因此退出登录。未登录 401,Origin/CSRF 不符 403(失效 session 在 guard 中也可能返回 403),已认证的非 POST 请求返回 405。 - POST /api/me/password {current_password,password} → {ok:true,reauthenticate:true}(普通用户及管理员仅修改本人;不接收目标 ID,未知 JSON 字段返回 400)。需有效 access/session、同源 Origin 和 X-CSRF-Token;新密码为 UTF-8 12–72 字节,非法 JSON、缺少当前密码或长度不符返回 400。错误当前密码返回 400 而非 401,客户端不应因此退出登录。未登录 401,Origin/CSRF 不符 403(失效 session 在 guard 中也可能返回 403),已认证的非 POST 请求返回 405。
- 与登录、管理员重设共用 authLoginMu,等待后复验会话与当前密码;沿用账户登录失败计数,累计 5 次错误锁定 15 分钟,锁定期间返回 429(即使当前密码正确),已登录不能绕过。认证存储故障返回 503。 - 与登录、管理员重设共用 authLoginMu,等待后复验会话与当前密码;沿用账户登录失败计数,累计 5 次错误锁定 15 分钟,锁定期间返回 429(即使当前密码正确),已登录不能绕过。认证存储故障返回 503。
- 同一事务保存 bcrypt hash、撤销本人全部 session/refresh、清除失败计数,并记录 user.change_password 审计(操作者和目标均为本人 ID,detail 为空,无密码/hash/token);事务失败返回 500 并完整回滚,其他用户不受影响。成功后客户端应清理登录状态并重新登录;旧密码、旧 access/session/refresh 不再可用。已建立的 runtime WS/SSE 不主动断开,下一次请求或重连重新鉴权。 - 同一事务保存 bcrypt hash、撤销本人全部 session/refresh、清除失败计数,并记录 user.change_password 审计(操作者和目标均为本人 ID,detail 为空,无密码/hash/token);事务失败返回 500 并完整回滚,其他用户不受影响。成功后客户端应清理登录状态并重新登录;旧密码、旧 access/session/refresh 不再可用。已建立的 runtime WS/SSE 不主动断开,下一次请求或重连重新鉴权。
## 管理 ## 管理
- GET /api/users(管理员) - GET /api/users → User[](管理员,按创建时间排序)
- POST /api/users {username,password,admin?,quota_bytes?}(管理员) - POST /api/users {username,password,admin?,quota_bytes?} → User(管理员;username为3–40个字母/数字/_/-,password为12–72字节;admin默认false,quota_bytes省略或0使用默认配额,负数400;用户名重复或创建存储失败409)
- PATCH /api/users {id,quota_bytes}(管理员修改正数配额) - PATCH /api/users {id,quota_bytes} → {ok:true}(管理员修改正数配额;非法输入400,用户不存在404)
- POST /api/users/reset-password {id,password} → {ok:true,reauthenticate:boolean}(仅管理员;需有效 access/session、同源 Origin 和 X-CSRF-Token)。密码按 UTF-8 字节计为 12–72 字节,以 bcrypt 保存;缺少 ID、非法 JSON 或长度不符返回 400,目标不存在 404,未认证 401,非管理员或 Origin/CSRF 不符 403(失效 session 在 guard 中也可能返回 403),已认证的非 POST 请求返回 405。 - POST /api/users/reset-password {id,password} → {ok:true,reauthenticate:boolean}(仅管理员;需有效 access/session、同源 Origin 和 X-CSRF-Token)。密码按 UTF-8 字节计为 12–72 字节,以 bcrypt 保存;缺少 ID、非法 JSON 或长度不符返回 400,目标不存在 404,未认证 401,非管理员或 Origin/CSRF 不符 403(失效 session 在 guard 中也可能返回 403),已认证的非 POST 请求返回 405。
- 同一事务更新密码、删除目标全部 session/refresh、清除目标登录失败锁定,并记录 user.reset_password 审计(操作者 ID、目标 ID、时间,detail 为空,不含密码/hash/token)。任何数据库失败返回 500 并回滚,不影响其他用户会话。与登录串行,等待后重新检查管理员会话和权限。 - 同一事务更新密码、删除目标全部 session/refresh、清除目标登录失败锁定,并记录 user.reset_password 审计(操作者 ID、目标 ID、时间,detail 为空,不含密码/hash/token)。任何数据库失败返回 500 并回滚,不影响其他用户会话。与登录串行,等待后重新检查管理员会话和权限。
- 允许重设自身,此时 reauthenticate:true,当前会话也立即失效,客户端应清理登录状态并回到登录页;重设他人返回 false。旧密码不能再登录,旧 access/refresh 不能发起新的认证请求。已建立的 runtime WS/SSE 不会被主动立即断开:代理仅在请求/握手时鉴权,下一次请求或重连会被拒绝;现有 runtime 页面刷新认证脚本不是即时断连保证。 - 允许重设自身,此时 reauthenticate:true,当前会话也立即失效,客户端应清理登录状态并回到登录页;重设他人返回 false。旧密码不能再登录,旧 access/refresh 不能发起新的认证请求。已建立的 runtime WS/SSE 不会被主动立即断开:代理仅在请求/握手时鉴权,下一次请求或重连会被拒绝;现有 runtime 页面刷新认证脚本不是即时断连保证。
- GET /api/audit(管理员) - GET /api/audit → [{id,user_id,action,target,detail,created_at}](管理员;按id倒序最多500条,无分页)
## 镜像与构建 ## 镜像与构建
@@ -57,28 +68,42 @@ GET /api/orphans 返回可回收的完整容器ID数组。DELETE /api/orphans?id
镜像策略:recommended/compatible可在实际验证与身份检查通过后用于新建/升级;existing_only允许已有实例运行和启停,不能新建/作为升级目标;blocked还拒绝新的start/restart及受控配置非GET操作(含Profile保存、备份和恢复),但不会自动停止已运行容器或断开工作台,通用文件管理仍允许。experimental是成功实验镜像未准入时的展示状态,不是兼容性结论。missing/unverified不可选。 镜像策略:recommended/compatible可在实际验证与身份检查通过后用于新建/升级;existing_only允许已有实例运行和启停,不能新建/作为升级目标;blocked还拒绝新的start/restart及受控配置非GET操作(含Profile保存、备份和恢复),但不会自动停止已运行容器或断开工作台,通用文件管理仍允许。experimental是成功实验镜像未准入时的展示状态,不是兼容性结论。missing/unverified不可选。
历史兼容基线为 linux/amd64、模板7、0.2.0-rc.1(compatible)和0.2.0-rc.2(recommended)。其他平台/版本不继承这些验收结论。模板3–6的历史成功镜像默认仅存量,具有精确验证记录并经管理员验收可准入;未知模板布局不可正向准入,模板7向旧共享布局降级仍拒绝。 历史兼容基线为 linux/amd64、模板7、0.2.0-rc.1(compatible)和0.2.0-rc.2(recommended)。其他平台/版本不继承这些验收结论。当前正向准入仅支持模板7且平台必须匹配面板的 GOOS/GOARCH;模板3–6的历史成功验证记录保留,但不能靠管理员 evidence 正向准入。未知模板布局不可正向准入,模板7向旧共享布局降级仍拒绝。
镜像项字段为 id,image_id,version,status,created_at,usage_count,admission,admission_reason,selectable,以及可省略的 tag,recipe_rev,platform。构建项字段为 id,version,status,created_at,experimental,以及非空时返回的 image_id,error,requested_by,recipe_rev,platform,finished_at,release_id,source_repository,source_commit;详情另有非空时返回的 logs。
发布构建 commit 必须为40位小写十六进制,未知字段或多段 JSON 返回400;相同发布已有 queued/running/validating/success 任务时复用,仍返回202。每管理员一小时最多3个新构建请求,超限429。准入 evidence 不得为空白,原始字符串最多4096 UTF-8字节,成功返回镜像项。镜像删除成功返回 {image_id,status:"missing"};未索引镜像404,引用/活动任务冲突409,Docker异常502;活动构建或未完成升级会全局阻止镜像删除,不仅限于目标镜像引用。构建分页参数重复、非整数或越界返回400,并非自动截断至100。
## 实例 ## 实例
- GET /api/instances → Instance[];字段 id,uid,name,version,image_id,status,container_id,created_at - GET /api/instances → Instance[];字段 id,uid,name,version,image_id,status,container_id,created_at
- POST /api/instances {name,image_id,version} → Instance(每用户最多 2 个;image_id 必须指向已验证且准入的精确本地镜像,不接受构建预约) - POST /api/instances {name,image_id} → Instance(每用户最多 2 个,超限409;image_id 必须为 sha256: 加64位小写十六进制且指向已验证、准入的精确本地镜像。version 由镜像标签推导,提交 version 或其他未知字段返回400,不接受构建预约。name 去除首尾空白后非空,原始长度最多200字节;请求体最多64 KiB)
- POST /api/instances/{id}/start|stop|restart - GET /api/instances/{id} → Instance
- POST /api/instances/{id}/start|stop|restart → Instance
- DELETE /api/instances/{id} → {ok:true} - DELETE /api/instances/{id} → {ok:true}
所有成功 JSON 列表均为顶层数组(不是 data 包装);错误均为 {error:string}。实现与验收仍在进行,不应仅凭接口列在此处认定已验收。 实例列表仅包含当前用户的实例;管理员也不能通过实例接口访问其他用户实例(404)。列表 Docker inspect 失败时该项 status 为 unavailable,详情 inspect 失败则502。生命周期操作遇到恢复状态或文件读租约冲突409,Docker操作失败通常502。
- GET /api/instances/{id}/logs
- /dsh/{id}/...(登录和 owner 校验;需同 Docker 网络) 列表型接口返回顶层数组(不是 data 包装),复合对象内的列表除外。实现与验收仍在进行,不应仅凭接口列在此处认定已验收。
- GET /api/instances/{id}/logs → {logs:string}(一次性脱敏日志,不是 SSE)
## 运行时 HTTP / WebSocket / SSE
- /dsh/{id}/...:校验有效 session/access 和 owner,需同 Docker 网络;不存在或非本人404,恢复状态409,运行时未启动、网络或认证不可用502。/dsh/{id} 重定向至带尾斜杠路径(307)。
- WS/SSE 通过相同前缀透明代理,不是 /api/ws/(后者是文件管理)。WS握手必须携带精确同源 Origin;普通 GET/HEAD 可省略 Origin,但提供时必须匹配;其他方法必须同源。这里不校验 X-CSRF-Token,也不将该头转发给上游。
- 认证只在请求/握手时检查,不因 access 到期、退出或改密主动关闭已建立流。未认证的普通请求/WS返回401;满足浏览器 document/iframe 导航条件的 GET 会303跳转到面板 /#runtime-renew 续期桥。
- 代理剥离面板 Cookie、Authorization、Proxy-Authorization 和查询中的运行时 token,使用服务端交换得到的运行时 Cookie;上游 Set-Cookie 不下发,Location 重写到实例前缀。WS帧及 SSE 事件结构由运行时决定,面板未定义独立消息协议。上游认证失败不会自动重放写请求。
## 工作区 ## 工作区
指定实例时文件根为该所有者实例的完整home;允许下载凭据及目录ZIP,不承诺write-only。下载提示仅在界面防误操作,API不按敏感文件名过滤。通用文件修改不强制走专用Profile的停机/YAML校验流程;归属、路径安全、配额、恢复状态和现有文件操作并发保护仍有效。上传不覆盖同名文件(409)。 除 usage 外,所有 /api/ws/ 请求必须带查询参数 instance={id},缺少或空值返回410(共享文件入口已移除);实例不存在或不属本人返回404,管理员无跨用户豁免。文件根为该实例的完整home;允许下载凭据及目录ZIP,不承诺write-only。下载提示仅在界面防误操作,API不按敏感文件名过滤。通用文件修改不强制走专用Profile的停机/YAML校验流程;归属、路径安全、配额、恢复状态和现有文件操作并发保护仍有效。上传不覆盖同名文件(409)。下列简写路径同样必须补上 instance 查询参数。
- GET /api/ws/tree?path=. - GET /api/ws/tree?path=. → [{name,path,is_dir,size,mod_time}](按名称排序,目录 size 为0)
- GET /api/ws/usage - GET /api/ws/usage → {used_bytes,quota_bytes}(当前用户总用量,不是单实例用量;可不带 instance,带上时仍检查归属)
- POST /api/ws/mkdir {path} - POST /api/ws/mkdir {path}
- POST /api/ws/rename {path,new_path} - POST /api/ws/rename {path,new_path}
- DELETE /api/ws/delete?path=... - DELETE /api/ws/delete?path=...
- POST /api/ws/upload multipart file,path - POST /api/ws/upload multipart file,path
- mkdir/rename/delete/upload 成功均返回 {ok:true};upload 的 path 是目标目录,instance 仍须在 URL 查询中提供。配额/文件大小限制413,扩展名白名单拒绝415,并发上传限流429;multipart解析失败(含部分超限请求)400。
- GET /api/ws/image?instance=...&path=...:只读图片预览,沿用工作区认证、实例归属检查和实例读租约;仅支持 GET。 - GET /api/ws/image?instance=...&path=...:只读图片预览,沿用工作区认证、实例归属检查和实例读租约;仅支持 GET。
- path 与 text 接口一致:保留空白和反斜杠,必须是非空、规范的相对文件路径。Linux openat2 限制在实例 home 内,拒绝符号链接、跨挂载点、多硬链接及特殊文件;不写入文件。 - path 与 text 接口一致:保留空白和反斜杠,必须是非空、规范的相对文件路径。Linux openat2 限制在实例 home 内,拒绝符号链接、跨挂载点、多硬链接及特殊文件;不写入文件。
- 有界读取最多 16 MiB 的内存快照,读取期间检测到文件变化返回 409,超限返回 413。PNG/JPEG/GIF/WebP 按 magic 识别;SVG 必须有 .svg 扩展名(不区分大小写),完整 XML 的唯一根元素必须是 SVG 命名空间的 svg,拒绝 HTML、XML directives/DOCTYPE 和未知实体,不加载外部实体。其他格式或目录返回 415,缺失/不安全文件返回 404,非法路径返回 400。 - 有界读取最多 16 MiB 的内存快照,读取期间检测到文件变化返回 409,超限返回 413。PNG/JPEG/GIF/WebP 按 magic 识别;SVG 必须有 .svg 扩展名(不区分大小写),完整 XML 的唯一根元素必须是 SVG 命名空间的 svg,拒绝 HTML、XML directives/DOCTYPE 和未知实体,不加载外部实体。其他格式或目录返回 415,缺失/不安全文件返回 404,非法路径返回 400。
@@ -96,7 +121,7 @@ GET /api/orphans 返回可回收的完整容器ID数组。DELETE /api/orphans?id
- 下载不是原子快照:流式读取前复核文件身份、大小、类型及修改时间;并发修改、取消或传输错误可能导致连接中断/不完整 ZIP,请停止写入后重试。单文件下载不受 ZIP 数量、大小、并发和时限约束。 - 下载不是原子快照:流式读取前复核文件身份、大小、类型及修改时间;并发修改、取消或传输错误可能导致连接中断/不完整 ZIP,请停止写入后重试。单文件下载不受 ZIP 数量、大小、并发和时限约束。
## profile / 备份 ## profile / 备份
- GET /api/instances/{id}/profile - GET /api/instances/{id}/profile → {content:string}(配置不存在时 content 为空)
- PUT /api/instances/{id}/profile {content,restart?}(停机原子写;restart=true启动并检查就绪) - PUT /api/instances/{id}/profile {content,restart?}(停机原子写;restart=true启动并检查就绪)
- GET /api/instances/{id}/profile-preview(bundle清单和脱敏dump-config,字符串值隐藏;运行中才能dump) - GET /api/instances/{id}/profile-preview(bundle清单和脱敏dump-config,字符串值隐藏;运行中才能dump)
- POST /api/instances/{id}/backups {password}(当前登录密码加密;自动恢复原运行状态) - POST /api/instances/{id}/backups {password}(当前登录密码加密;自动恢复原运行状态)
@@ -106,8 +131,10 @@ GET /api/orphans 返回可回收的完整容器ID数组。DELETE /api/orphans?id
- POST /api/instances/{id}/restore {backup_id,password}(原备份密码;旧平台key包可留空) - POST /api/instances/{id}/restore {backup_id,password}(原备份密码;旧平台key包可留空)
- POST /api/instances/{id}/upgrade {image_id,password}(当前登录密码,加密pre-upgrade备份) - POST /api/instances/{id}/upgrade {image_id,password}(当前登录密码,加密pre-upgrade备份)
Profile写入要求实例先停止。备份/还原/升级会受控停启;恢复保留原运行状态,升级成功运行新版,失败恢复旧状态。模板2入口先准备插件再启动应用。删除实例先清理容器与网络,再永久删除该实例的 home、快照及备份,最后移除实例索引;数据清理失败保留 error 实例以便重试。升级失败恢复实例home,不撤销共享workspace或外部系统副作用。 Profile PUT 与 restore 成功返回 {ok:true},upgrade 返回 Instance。profile-preview 返回 {bundles,config,redacted:true,preview_error?};无法运行 dump 时仍可200并携带 preview_error,不保证 config 非空。备份列表为 [{id,size,created_at,kind,schema}],创建/导入返回同形单个对象,download 返回二进制附件。备份数量上限20,达到上限409;恢复快照达到保留上限也返回409,需管理员清理。配置非GET请求体有界读取,超限413、读取超时408、并发槽耗尽503;Profile JSON最多2 MiB,普通备份/restore JSON最多4096字节。
Profile写入要求实例先停止。备份/还原/升级会受控停启;恢复保留原运行状态,升级成功运行新版,失败尝试恢复旧状态,回滚失败需人工处理。运行镜像的插件准备行为由发布分支维护,应针对精确镜像验证。删除实例先清理容器与网络,再永久删除该实例的 home、快照及备份,最后移除实例索引;数据清理失败保留 error 实例以便重试。升级仅支持实例home到 /workspace 的单一bind布局,不迁移旧共享挂载;home回滚不撤销外部系统副作用。
创建恢复:Docker 创建响应丢失时,首次 inspect 404 不代表原请求已结束。实例保留为可见的 error 状态并继续占用名额,用户可重试删除;后台不会仅凭名称删除身份不匹配资源。不确定创建记录不会自动过期:若资源始终不存在,需管理员确认 Docker 原创建请求已终止后再处理保留记录,不能仅凭等待时长判定安全。构建验证容器在创建前持久登记任务身份,启动时即使没有排队构建也会回收并重试失败清理;持续不存在的未知结果仍保留登记以捕获晚到容器。构建终态和镜像索引事务失败会独立重试并记录日志,不阻塞后续构建;数据库关闭停止重试,重启将未完成构建标记失败。 创建恢复:Docker 创建响应丢失时,首次 inspect 404 不代表原请求已结束。实例保留为可见的 error 状态并继续占用名额,用户可重试删除;后台不会仅凭名称删除身份不匹配资源。不确定创建记录不会自动过期:若资源始终不存在,需管理员确认 Docker 原创建请求已终止后再处理保留记录,不能仅凭等待时长判定安全。构建验证容器在创建前持久登记任务身份,启动时即使没有排队构建也会回收并重试失败清理;持续不存在的未知结果仍保留登记以捕获晚到容器。构建终态和镜像索引事务失败会独立重试并记录日志,不阻塞后续构建;数据库关闭停止重试,重启将未完成构建标记失败。
已实现并通过核心集成检查:镜像复用验证、Docker 生命周期、token/cookie 桥接、加密备份恢复、同镜像替换升级。WS/SSE 代理有单元测试,真实完整会话待验证。硬配额与跨公网租户域名隔离未实现;当前 dsh 发行版不支持 --public-url。默认仅 loopback 监听,不应直接暴露公网。完整限制见 README.md。 镜像验证、Docker 生命周期、token/cookie 桥接、加密备份恢复和同镜像替换升级已有实现与历史集成记录,具体版本和本次验证范围见 ACCEPTANCE.md,不能据此推断当前全部链路已重新验收。WS/SSE 代理有单元测试,真实完整会话需单独验证。硬配额与跨公网租户域名隔离未实现;已验证的 DSH rc.1/rc.2 启动流程不使用 --public-url。默认仅 loopback 监听,不应直接暴露公网。完整限制见 README.md。
+128 -232
View File
@@ -1,8 +1,8 @@
# DeepSeek Harness 多租户容器管理平台 — 设计文档 # 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`) > 目标项目:`deepseek-ai/deepseek-harness`(npm CLI `@deepseek-ai/dsh`,命令 `dsh web`)
> 控制面技术栈:Go + 前端 SPA > 控制面技术栈:Go + 前端 SPA
> 部署形态:单机 Docker daemon;面板自身亦运行于容器中 > 部署形态:单机 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;完整代理以集成测试为准。 > 实现阶段实测更正:本地 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. 目标与范围 ## 1. 目标与范围
### 已确认的访问与镜像策略(覆盖下文早期假设) ### 已确认的访问与镜像策略
- 面向可信团队,实例所有者可通过文件管理读取、下载、修改完整 DSH_HOME,包括自己的凭据。面板不承诺凭据 write-only;跨用户隔离、路径安全、配额与恢复状态保护保持不变。下载文件/打包目录会提示泄密风险,提示属于交互防误操作,不是阻止所有者访问的安全边界。 - 面向可信团队,实例所有者可通过文件管理读取、下载、修改完整 DSH_HOME,包括自己的凭据。面板不承诺凭据 write-only;跨用户隔离、路径安全、配额与恢复状态保护保持不变。下载文件/打包目录会提示泄密风险,提示属于交互防误操作,不是阻止所有者访问的安全边界。
- 专用 Profile 编辑器提供 YAML 校验与受控停启流程,但通用文件操作、DSH 终端和 agent 不强制经过该流程;修改运行中配置存在不一致风险,建议先停机并备份。 - 专用 Profile 编辑器提供 YAML 校验与受控停启流程,但通用文件操作、DSH 终端和 agent 不强制经过该流程;修改运行中配置存在不一致风险,建议先停机并备份。
- 镜像准入分为推荐、仍兼容、仅存量运行、禁止使用。兼容依据绑定精确版本、模板修订、平台与镜像身份;不再仅因历史模板不是当前模板就一律判为不可用,也不能仅因历史构建成功就认定兼容。 - 镜像准入分为推荐、仍兼容、仅存量运行、禁止使用。兼容依据绑定精确版本、模板修订、平台与镜像身份;不再仅因历史模板不是当前模板就一律判为不可用,也不能仅因历史构建成功就认定兼容。
- 未验证 DSH 新版仅管理员可显式实验构建;保留既有完整性、依赖与启动检查。实验构建成功不是兼容验收,未明确准入的镜像不可新建实例或作为升级目标。管理员正向准入须填写验收依据并记录审计,不绕过实际镜像身份与验证记录检查。 - 新版本仅通过管理员 Git 发布流程构建;实验构建已移除。管理员正向准入须填写验收依据并记录审计,不绕过布局、平台、实际镜像身份与验证记录检查。
- 通过 docker.sock 在单机上**创建/启停/删除**多个 `dsh web` 容器,**每用户最多 2 个**。 - 通过 docker.sock 在单机上**创建/启停/删除**多个 `dsh web` 容器,**每用户最多 2 个**。
- 每用户一个**独立工作区**,支持文件**上传/下载/删除/重命名**,并有**容量配额**。 - 每实例独立完整 home,支持文件上传/下载/删除/重命名、文本编辑和图片预览,按账号统计软配额。
- 工作区**映射进每个 dsh 容器**(agent 在工作区内干活)。 - 实例 home 挂入自身容器的 /workspace,不与同账号其他实例共享挂载。
- **备份/还原 dsh 配置**(含模型与插件清单)。 - **备份/还原 dsh 配置**(含模型与插件清单)。
- **编辑 profile 文件**(`cordis.patch.yml`)。 - **编辑 profile 文件**(`cordis.patch.yml`)。
- **升级**:保留数据,用新版本镜像重启容器。 - **升级**:保留数据,用新版本镜像重启容器。
- **本地镜像管理**:面板查询 DSH 最新版本,用户手动触发本机构建;创建实例时选择本地已有版本,或手动构建最新版后创建。不需要私有镜像仓库,不推送生成的镜像。 - **本地镜像管理**:管理员检查 Git 发布并确认精确 commit 构建;用户选择经验证且准入的本地镜像创建,不提供构建预约。不推送生成的镜像。
- 控制面:Go 后端 + SPA 前端;内置账号 + 目录配额;**面板自身容器化运行**。 - 控制面: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 为准。
``` 浏览器 → 面板 SPA/API 或 /dsh/<id>/ → 实例专属 bridge → runtime:8080 → DSH:3080。
┌────────────── 宿主 ──────────────────┐
│ 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
│ └─────────────────────┘ └───────────────┘ │
└─────────────────────────────────────────────┘
```
组件: socket-proxy 仍为可选规划,当前 Compose 直接挂 socket,不代表具备宿主级隔离。
1. **面板服务**(Go 容器):REST API + 静态 SPA + 反向代理子系统 + docker client + 配额/备份/升级引擎。
2. **dsh-web 镜像**(自建):Node + `@deepseek-ai/dsh` + caddy + 入口脚本。
3. **dsh-net**:内部 bridge 网络,容器不发布主机端口,仅面板可达。
4. **(可选)socket-proxy**:面板与 sock 之间的白名单代理。
--- ---
## 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` | | 实例完整 home | /data/workspaces/<uid>/instances/<id>/home | <HOST_DATA_ROOT>/workspaces/<uid>/instances/<id>/home → /workspace |
| 实例 DSH_HOME | `/data/workspaces/<uid>/instances/<id>/home/...` | `/var/lib/dsh-mgr/workspaces/<uid>/instances/<id>/home` | | 配置备份 | /data/backups/<uid>/<id>/ | 不挂入 runtime |
| 备份包 | `/data/backups/<uid>/<id>/...` | (不入容器,仅面板读写) |
面板配置: 构建上下文是从锁定 Git commit 获取并提交的归档流,不应把历史内置 image/ 当作宿主 bind 源。
```
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. 数据与目录布局(宿主机) ## 4. 数据与目录布局(当前实现)
``` ```
/var/lib/dsh-mgr/ <HOST_DATA_ROOT>/
db.sqlite # 用户、实例、配额、备份索引 db.sqlite
workspaces/<uid>/ workspaces/<uid>/
files/ # 用户工作区(面板文件 API + 各 dsh 容器共享挂载) instances/<instId>/home/ # 完整 DSH_HOME → /workspace
└─ <用户自由目录结构> .credentials.yaml
instances/<instId>/ cordis.patch.yml
home/ # DSH_HOME(bind → 容器 /data/dsh-home) profiles/web/ # 配置、插件清单、可重建依赖
.credentials.yaml # API Key(机密) storages/ # 实例工作区数据等
cordis.patch.yml # home 级 patch files/ # 可能遗留;当前无共享入口或新容器挂载
profiles/web/ backups/<uid>/<instId>/ # 加密配置备份;不是完整 home 快照备份
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)。每用户一个配额单位。 账号软配额统计 workspaces/<uid>/,并非只统计当前实例。实例删除会清理该实例目录及对应备份;历史共享 files 不自动合并到 home。旧目录结构和旧删除保留策略不能作为现行承诺。
--- ---
## 5. 自建 DSH 镜像设计 ## 5. 自建 DSH 镜像(当前 Git 发布流程)
``` 管理员检查 GET /api/releases/latest,再以 POST /api/releases/build 确认精确 commit;分支变化必须重新确认。后端从受信任仓库锁定 commit 获取 release.json 与 image/,校验部署协议7及当前面板平台,构建本地镜像并验证。构建记录绑定仓库/commit/release ID/版本/平台,任务去重、限频、容量检查与失败记录仍保留。GET /api/builds 及 GET /api/builds/{id} 提供历史与日志。
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。 不再从npm查询latest,不接受普通用户实验构建,不打包main内置image/,不提供构建后创建预约。创建实例必须另行选择经验证且准入的本地image ID;管理员构建成功不等于已完成完整兼容验收或自动升级实例。
- **存储方式**:生成的镜像仅保存在本机 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 包。网络故障不影响选择已存在的本地可用镜像。
**创建实例交互**: 镜像准入绑定实际身份和成功验证记录;supportedLayout只接受模板7,平台须匹配当前面板平台,内置defaultAdmission规则另有限定linux/amd64。旧模板记录可以保留,不意味着可通过填写说明正向准入。镜像清理保护有效引用,不自动全局prune。构建仍可能需要基础镜像和依赖网络,不能宣称离线构建。
1. **选择本地版本**:从本地可用列表选择版本,校验 image ID 仍存在后创建。
2. **构建最新版并创建**:查询并展示精确版本 → 用户确认 → 事务预留实例名额 → 构建或复用任务 → 验证成功 → 用该 image ID 创建实例。等待构建的实例也计入每用户 2 个名额;构建/创建失败或取消时清理未完成资源并释放名额,不回退到其他版本。取消某个用户的等待不取消其他用户共享的构建任务。
3. 面板另提供独立的“构建最新版”按钮,仅生成本地镜像,不创建或升级实例,也不占实例名额。
--- ---
## 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。
- **创建**: 当前资源约束为只读 rootfs、drop ALL capabilities、no-new-privileges、2GiB 内存、2 CPU、256 PID 及有界可执行 /tmp。实例支持启停、重启、日志、删除;删除回收容器/网络并清理实例数据与备份。创建不确定时保留恢复依据,不能靠一次或超时后的404清除。孤儿资源仅管理员明确确认并核验后处理,不自动纳管。
- 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. 网络与访问方式(当前实现)
## 7. 网络与访问方式 默认已经选择路径前缀代理,不是待拍板选项。面板将 /dsh/<id>/ 转发至对应实例网络中的 runtime:8080;归属检查在代理之前。Compose 默认仅向 loopback 发布面板8088,公网需管理员另配 HTTPS 入口与可信代理。子域名、直接发布 runtime 端口不是当前默认能力。
- 面板**启动时 ensure** `dsh-net`(bridge)存在,并把自身加入(compose 声明或运行时 `network connect`)。 ## 8. 访问与认证(当前实现)
- dsh 容器挂 `dsh-net`,**不发布主机端口**。
- 面板反代 `/dsh/<instId>/` → `http://<container-name>.dsh-net:8080`,容器名 = instId。
- **访问方式(待定,见 §18)**:默认推荐"路径前缀反代 + token 桥接"(§8),面板天然为 ingress。备选:子域名(需泛 DNS+TLS)/ 直发端口(MVP,隔离弱)。
--- 面板校验账号和实例归属,从启动日志取得 token 并在服务端交换/缓存 DSH cookie;token 按容器身份/启动时间加密持久化。反代剥前缀、处理 Host 与转发头,注入服务端 cookie 并移除上游 Set-Cookie,不把 DSH cookie 改写下发给浏览器。客户端持有的是面板认证 cookie。
## 8. 访问与认证(token 桥接 + cookie 改写) WS/SSE 由反代支持;会话续期由面板/runtime 注入脚本协调。上游拒绝 cookie 时失效缓存,下次请求重新交换,不重放写请求。已建立 WS/SSE 不承诺即时撤销;真实部署的长会话、插件与公开入口仍需分别验证。
> 此为最复杂点。推荐方案:路径前缀反代 + 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. 工作区文件管理 + 容量配额 ## 9. 工作区文件管理 + 容量配额
**文件 API**(path 校验:解析后必须在 `files/` 内,拒 `..`/绝对路径): **文件 API**(除 usage 外必须提供 instance 并校验归属;path 限于该实例 home,拒穿越、绝对路径和不安全符号链接):
- `GET /api/ws/tree?path=` 列目录(带 size/mtime) - `GET /api/ws/tree?path=` 列目录(带 size/mtime)
- `POST /api/ws/mkdir`、`POST /api/ws/rename`、`DELETE /api/ws/delete` - `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/download?path=`(单文件流;目录自动 zip)
- `GET /api/ws/usage`(当前用量/配额) - `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)。 - **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>`。 - **ZFS refquota**(若宿主用 ZFS):面板 `--privileged` + zfsutils,每用户 `zfs create pool/users/<uid> && zfs set refquota=10G`,再 bind 该 dataset 到 `/data/workspaces/<uid>`。
- 或在宿主跑**极小特权配额 sidecar**(gRPC/HTTP),面板远程调它做 quota 操作——面板自身保持非特权。 - 或在宿主跑**极小特权配额 sidecar**(gRPC/HTTP),面板远程调它做 quota 操作——面板自身保持非特权。
- 配额值存 DB,按用户可配(默认 10GiB)。面板展示用量。 - 配额值存 DB,按用户可配(默认 10GiB)。面板展示用量。
> 具体选型见 §18 待拍板。 > 当前仅软配额;上述 XFS/ZFS 和特权 sidecar 是未实现规划,不是可直接启用的配置。
--- ---
@@ -246,21 +207,21 @@ Go 端用 `github.com/docker/docker/client`:
| `dsh-ssh.json` | SSH 凭据(0600,加密入包) | | `dsh-ssh.json` | SSH 凭据(0600,加密入包) |
| `manifest.json` | dsh 版本、备份 schema 版、时间戳 | | `manifest.json` | dsh 版本、备份 schema 版、时间戳 |
**排除**:`node_modules/`(pnpm 重建)、`worktrees/`、`sessions/`、日志。 **范围**:仅上述六个固定配置文件、四类资源树和生成的manifest;不是完整home备份,通常位于白名单外的工作区、会话和插件node_modules不会入包。资源树按路径递归纳入普通文件,不根据文件内容识别运行态或凭据;不安全路径、符号链接及特殊文件会被拒绝。
**备份包加密**:用用户密码派生密钥(Argon2id → AES-GCM)加密整包;密钥不落库。下载/上传均加密包。 **备份包加密**:用用户密码派生密钥(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 文件编辑 ## 11. profile 文件编辑
- 编辑对象:`profiles/web/cordis.patch.yml`(YAML)。 - 编辑对象:`profiles/web/cordis.patch.yml`(YAML)。
- 前端用 Monaco 编辑器;后端 `GET` 返回原文、`POST` 校验(YAML 合法 + 可选 `--dump-config-schema` 比对)后原子写(临时文件 + rename,`dsh-atomic-write` 风格)。 - 前端用本地 Monaco;后端 `GET` 返回原文,`PUT` 校验 YAML 后原子写入,专用编辑器修改前需停止实例。未实现的 schema 比对不作为已交付能力。
- 保存后**重启容器**使生效(dsh 有 `dsh-hmr`,但配置类变更稳妥起见重启;后续可探热更)。 - 页面“保存并启动”发送 `restart:true`,写入后启动并检查就绪;API省略该参数时仅保存,不自动启动。
- 只读展示 `dsh.profile`(bundle 清单)与 `--dump-config` 预览(面板 `docker exec` 跑 `dsh --profile web --dump-config` 取回渲染)。 - 只读展示 `dsh.profile`(bundle 清单)与 `--dump-config` 预览(面板 `docker exec` 跑 `dsh --profile web --dump-config` 取回渲染)。
- 专用配置预览保持脱敏;完整 home 文件管理允许实例所有者读取 `.credentials.yaml` 原文,不承诺 write-only。 - 专用配置预览保持脱敏;完整 home 文件管理允许实例所有者读取 `.credentials.yaml` 原文,不承诺 write-only。
@@ -269,23 +230,23 @@ Go 端用 `github.com/docker/docker/client`:
## 12. 升级流程(保留数据 + 重启新版容器) ## 12. 升级流程(保留数据 + 重启新版容器)
``` ```
1. 用户选择本地可用目标版本;若需最新版,先手动触发 §5 构建并等待验证成功。锁定本地 image ID 后才进入备份/停机流程,不自动 pull 成品镜像 1. 用户选择本地可用目标版本;若无目标镜像,先由管理员完成 §5 Git 发布构建。锁定已验证且准入的 image ID 后才进入备份/停机流程
2. 自动备份当前配置(§10)→ 标记 pre-upgrade 备份 2. 停止原本运行的旧容器并检查确实已停机(不 remove,保留作回滚)
3. docker stop <old>(不 remove,保留作回滚) 3. 创建有界home快照,再用当前登录密码创建 pre-upgrade 配置备份
4. docker create <new>:同名 instId、同 binds(home + files)、同 labels、新 image、 4. 保留旧容器回滚依据,创建替换容器:同逻辑实例、同home挂载、新image,不挂共享files
Cmd 按新版本要求重算 --public-url/--trusted-host Cmd 使用 web/no-open/port/trusted-host,不传 --public-url
5. entrypoint 首启:若 profiles/web/node_modules 缺失/与 package.json 不一致 → pnpm install 重建插件 5. 启动新容器;发布镜像入口负责插件协调,其离线能力须针对精确镜像验证
6. docker start <new> → 面板抓 token、健康检查(轮询 caddy :8080) 6. 面板抓 token 并验证就绪
7. 成功:docker rm <old>;失败:stop <new>、start <old>、标记回滚、提示用户 7. 成功删除旧容器;失败尝试清理新容器、恢复home快照及旧容器原运行状态,回滚失败保留恢复依据并提示人工处理
``` ```
关键:**bind 不变 ⇒ 数据零丢失**;`node_modules` 在新镜像里按 `package.json` 重建(跨版本兼容性由 pnpm/dsh-plugin-manager 保证)。 历史设计意图:保持 bind 并重建插件依赖以尽量保留数据;**这不保证零丢失或任意插件跨版本兼容**。当前升级还依赖备份、快照、健康检查和回滚,故障及兼容性必须实测。
--- ---
## 13. 安全与资源 ## 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。 - dsh 容器:**不 `--privileged`**、去 CAP_NET_RAW 等、只读 rootfs、tmpfs /tmp、mem/cpu 限制、不挂 sock。
- 面板账号:bcrypt + JWT(短期 access + refresh),失败锁定。 - 面板账号:bcrypt + JWT(短期 access + refresh),失败锁定。
- 配置备份包加密;实例所有者通过文件 API 下载的凭据或目录 ZIP 不属于加密配置备份,应按机密数据保管。 - 配置备份包加密;实例所有者通过文件 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。 当前 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。
- **前端**:Vite + React + TypeScript,Monaco(profile 编辑),shadcn/ui。
``` 旧设想中的 main/image/、deploy/ 不能用于当前构建或部署:runtime 资产在 dockerfile 发布分支,面板使用根目录 Dockerfile 和 compose.yaml。
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 概要) ## 15. 数据模型(当前实现概要)
- `users(id, username, pass_hash, quota_bytes, created_at)` SQLite基础表users、sessions、instances、audit由internal/store初始化;认证轮换、构建/镜像准入、发布身份、runtime创建/网络恢复和升级恢复等表由相应API模块维护。实际字段与迁移以源码为准,不将早期设计的示意DDL当作数据库接口。
- `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)` image_builds包含任务、验证及发布来源信息;image_admissions绑定精确image ID/version/recipe/platform及验收依据;runtime_creates/runtime_networks保留资源恢复依据。旧表或文件名存在不代表旧预约服务仍运行。
- `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 示例) ## 16. 面板自身部署(当前实现)
```yaml 以根目录 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及健康检查。
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 配置 HOST_DATA_ROOT / CONTAINER_DATA_ROOT、DockerRootDir只读容量探测挂载、PANEL_CONTAINER_ID,以及实际公开URL和可信代理CIDR。裸docker.sock挂载并未被只读rootfs降权,仍需信任面板与账号。当前模板不部署socket-proxy或硬配额sidecar。
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. 关键风险与待验证 ## 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` 联网重建后再切流量。 2. **插件首启重建**:跨 dsh 版本 `pnpm install` 在 entrypoint 无网时失败——可能需升级时由面板 `docker exec` 联网重建后再切流量。
3. **XFS/ZFS 硬配额**依赖宿主 FS 与特权/agent;否则退化为软配额。 3. **XFS/ZFS 硬配额**依赖宿主 FS 与特权/agent;否则退化为软配额。
4. **dsh agent 工作目录**:`dsh-fs-local`/`dsh-fs-sandbox` 的根是否等于 cwd——需确认 `/workspace` 作为 cwd 即可让 agent 在工作区操作,或需额外配置 fs root。 4. **dsh agent 工作目录**:`dsh-fs-local`/`dsh-fs-sandbox` 的根是否等于 cwd——需确认 `/workspace` 作为 cwd 即可让 agent 在工作区操作,或需额外配置 fs root。
5. dsh 版本仍为 `0.2.0-rc.*`(RC),API/flag 可能变动——需维护版本兼容矩阵。 5. dsh 版本仍为 `0.2.0-rc.*`(RC),API/flag 可能变动——需维护版本兼容矩阵。
6. **sibling bind 路径翻译**:`toHostPath` 出错会导致 bind 挂错目录或挂空目录,需单测覆盖。 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,隔离弱)。 ## 19. 里程碑与交付判定
- **推荐默认**:路径前缀反代。
2. **配额**:宿主 FS 是 XFS / ZFS / 其他?是否接受"特权面板"或"宿主配额 sidecar"做硬配额?还是软配额默认即可?
- **推荐默认**:软配额(du)起步,硬配额作为后续增强;若宿主为 XFS/ZFS 且可接受特权,则上硬配额。
--- M1镜像、M2账号/生命周期、M3代理、M4文件/软配额、M5配置备份、M6升级回滚、M7界面/管理安全保留为能力分组,不再以旧npm构建及预约作为当前M2标准。当前Git发布、实例home独占、删除清理、文本编辑/图片预览与主题等变更须按ACCEPTANCE.md当前矩阵分别补证据。
## 19. 里程碑 历史M1–M7结案不等于当前commit或真实部署整体通过;本地测试通过、真实Docker通过、浏览器通过与公网验收必须分开记录。
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 完善 + 审计 + 资源限制 + 可选硬配额**。
--- ---
+9 -14
View File
@@ -7,7 +7,7 @@
## 启动 ## 启动
1. 复制 `.env.example` 为 `.env`,填写专用绝对目录 `DSH_HOST_DATA_ROOT` 和 12–72 字节的随机管理员密码。限制 `.env` 的读取权限,不要提交。管理员账号创建后该密码不再被读取,应从 `.env` 中移除,再运行 `docker compose up -d --force-recreate panel` 重建面板容器,清除容器环境变量中保留的明文密码(`docker inspect` 可见)。仅修改 `.env` 或重启原容器不会更新其环境变量;重建面板会短暂中断面板访问。 1. 复制 `.env.example` 为 `.env`,填写专用绝对目录 `DSH_HOST_DATA_ROOT` 和 12–72 字节的随机管理员密码。限制 `.env` 的读取权限,不要提交。管理员账号创建后该密码不再被读取,应从 `.env` 中移除,再运行 `docker compose up -d --force-recreate panel` 重建面板容器,清除容器环境变量中保留的明文密码(`docker inspect` 可见)。仅修改 `.env` 或重启原容器不会更新其环境变量;重建面板会短暂中断面板访问。
2. 面板以 UID/GID 1000 运行。首次部署前由管理员创建专用数据目录并赋予 1000:1000 所有权、0700 权限;不要对已有未知目录递归改权限。将 `stat -c %g /var/run/docker.sock` 的结果填入 `.env` 的 `DOCKER_GID`,以附加组访问 socket。然后运行 `docker compose up --build -d`。默认访问 `http://127.0.0.1:8088`;远程管理优先使用 SSH 本地端口转发,保留此访问地址。Compose 定义了 `dsh-mgr -healthcheck` 健康检查(探测本机 `/healthz`);进程无响应时容器会被标记为 unhealthy,配合 `restart: unless-stopped` 在退出时自动拉起。 2. 面板以 UID/GID 1000 运行。首次部署前由管理员创建专用数据目录并赋予 1000:1000 所有权、0700 权限;不要对已有未知目录递归改权限。将 `stat -c %g /var/run/docker.sock` 的结果填入 `.env` 的 `DOCKER_GID`,以附加组访问 socket。然后运行 `docker compose up --build -d`。默认访问 `http://127.0.0.1:8088`;远程管理优先使用 SSH 本地端口转发,保留此访问地址。Compose 定义了 `dsh-mgr -healthcheck` 健康检查(探测本机 `/healthz`);连续探测失败时容器会被标记为 unhealthy,但该状态本身不会触发重启。`restart: unless-stopped` 用于进程退出后的自动拉起;`/healthz` 仅检查HTTP存活,不检查数据库、Docker或实例健康。
3. 管理员登录后在镜像页检查 Git 发布并确认精确 commit 后构建;普通用户选择已验证且准入的本地镜像创建实例。没有可选镜像时等待管理员构建,不再按 npm 版本或预约构建后创建。镜像不推送至仓库。 3. 管理员登录后在镜像页检查 Git 发布并确认精确 commit 后构建;普通用户选择已验证且准入的本地镜像创建实例。没有可选镜像时等待管理员构建,不再按 npm 版本或预约构建后创建。镜像不推送至仓库。
4. 创建实例后从面板进入 DSH。不要给 runtime 发布宿主端口或挂载 Docker socket。 4. 创建实例后从面板进入 DSH。不要给 runtime 发布宿主端口或挂载 Docker socket。
@@ -17,9 +17,9 @@
## 公网 HTTPS 入口(仅可信账号) ## 公网 HTTPS 入口(仅可信账号)
反向代理与证书由部署管理员管理,面板仍建议只监听代理可达的私有入口。设置 `PANEL_PUBLIC_URL=https://你的域名`(仅 origin,不含业务子路径);Compose 默认值仍为本机 HTTP。此值决定 Cookie 的 Secure 属性及 Origin/CSRF 校验,不从客户端的 X-Forwarded-Proto/Host 推断。 反向代理与证书由部署管理员管理,面板仍建议只监听代理可达的私有入口。设置 `PANEL_PUBLIC_URL=https://你的域名`(仅 origin,不含业务子路径);Compose 默认值仍为本机 HTTP。此值用于 Origin/CSRF 校验,HTTPS值会启用 Cookie 的 Secure 属性(直接TLS请求也会启用),不从客户端的 X-Forwarded-Proto/Host 推断。
默认只按直连来源 IP 限流,不信任 X-Forwarded-For。经反代部署时,设置 `TRUSTED_PROXY_CIDRS` 为面板实际看到的代理来源地址/CIDR,且确保只有这些代理能连接后端、代理正确维护转发链;不要填写全网 CIDR。未设置时,代理后所有用户将共享该代理来源的每分钟登录额度。公网开放前应实际验证不同客户端来源识别、登录/刷新/退出、DSH iframe、WS/SSE及插件设置接口。 默认只按直连来源 IP 限流,不信任 X-Forwarded-For。经反代部署时,设置 `TRUSTED_PROXY_CIDRS` 为面板实际看到的、逗号分隔的代理来源CIDR(单个IPv4/IPv6地址须写成 `/32` 或 `/128`),且确保只有这些代理能连接后端、代理正确维护转发链;不要填写全网 CIDR。未设置时,代理后所有用户将共享该代理来源的每分钟登录额度。公网开放前应实际验证不同客户端来源识别、登录/刷新/退出、DSH iframe、WS/SSE及插件设置接口。
账号由管理员创建,不支持不可信租户。软配额、同源插件与 Docker socket 权限边界不因 HTTPS 而改变;本版本不承诺抵御分布式拒绝服务。 账号由管理员创建,不支持不可信租户。软配额、同源插件与 Docker socket 权限边界不因 HTTPS 而改变;本版本不承诺抵御分布式拒绝服务。
@@ -41,7 +41,7 @@
在镜像页“准入管理”选择推荐或兼容并填写依据,后端还会检查精确镜像身份、成功验证记录和可支持的挂载布局。审批只作用于该镜像;新镜像需要自己的验证与审批。退为仅存量后不允许新建/升级目标;标为禁止使用还阻止新的启动/重启及受控配置修改、备份/恢复写操作,避免其自动重启绕过禁用。已有配置读取和通用home文件管理仍保留,不会自动停止正在运行的实例。若需要立即隔离已运行实例,管理员必须另行确认并执行停止操作。 在镜像页“准入管理”选择推荐或兼容并填写依据,后端还会检查精确镜像身份、成功验证记录和可支持的挂载布局。审批只作用于该镜像;新镜像需要自己的验证与审批。退为仅存量后不允许新建/升级目标;标为禁止使用还阻止新的启动/重启及受控配置修改、备份/恢复写操作,避免其自动重启绕过禁用。已有配置读取和通用home文件管理仍保留,不会自动停止正在运行的实例。若需要立即隔离已运行实例,管理员必须另行确认并执行停止操作。
历史home布局不同:模板7使用完整home作为 `/workspace`,受支持旧模板使用独立 `/data/dsh-home` 与用户共享 `/workspace`。不要把旧模板准入理解为数据布局可任意互换;不支持的反向迁移仍会拒绝,先做好完整备份。 当前准入仅支持模板7,使用完整home作为 `/workspace`;旧模板的独立 `/data/dsh-home` 与用户共享 `/workspace` 布局不再支持准入。升级也要求原容器只有实例home到 `/workspace` 的单一bind挂载,不会迁移旧共享布局;不符合时返回409。处理历史实例前先做好完整备份。
运行时认证材料由面板首次成功连接实例时提取,并使用 `auth-signing.key` 派生的密钥加密保存在数据根的 `runtime-auth/`(不在用户home内)。按容器 ID 和启动时间绑定,不会将旧启动周期凭据用于新容器;不同 Host 分别缓存 cookie。全量备份应连同签名密钥和该目录保存,不要向用户公开。 运行时认证材料由面板首次成功连接实例时提取,并使用 `auth-signing.key` 派生的密钥加密保存在数据根的 `runtime-auth/`(不在用户home内)。按容器 ID 和启动时间绑定,不会将旧启动周期凭据用于新容器;不同 Host 分别缓存 cookie。全量备份应连同签名密钥和该目录保存,不要向用户公开。
@@ -49,7 +49,7 @@
升级到本版本后应在维护窗口逐个访问现有实例,完成首次认证持久化。若在首次成功连接前,启动token已被Docker日志轮转删除,本版本无法凭空恢复;先保存工作,再由管理员安排重启相关实例并访问验证。不要为此删除或重建实例。初次日志扫描上限64 MiB,常规每文件10 MiB/3份保留在此范围内。 升级到本版本后应在维护窗口逐个访问现有实例,完成首次认证持久化。若在首次成功连接前,启动token已被Docker日志轮转删除,本版本无法凭空恢复;先保存工作,再由管理员安排重启相关实例并访问验证。不要为此删除或重建实例。初次日志扫描上限64 MiB,常规每文件10 MiB/3份保留在此范围内。
- 实例升级选择已经启动验证的本地镜像并提供当前登录密码,面板自动停机、创建密码配置备份与home快照、启动验证及失败回滚,无需先手动停止。home快照最大1 GiB;允许受约束的内部相对链接以支持pnpm,拒绝不安全链接/特殊文件。模板7的 `/workspace` 是实例home,包含在home回滚内;旧共享数据不回滚。跨DSH版本历史验证范围见 ACCEPTANCE.md。 - 实例升级选择已经启动验证且准入的本地模板7镜像并提供当前登录密码,面板自动停机、创建密码配置备份与home快照、启动验证及失败回滚,无需先手动停止。home快照最大1 GiB;允许受约束的内部相对链接以支持pnpm,拒绝不安全链接/特殊文件。模板7的 `/workspace` 是实例home,包含在home回滚内;旧共享挂载布局会在升级前被拒绝。跨DSH版本历史验证范围见 ACCEPTANCE.md。
- Profile是例外:当前需先停止实例,再使用“保存并启动”;页面包含Monaco和脱敏配置预览。不要把备份/升级自动停启理解为Profile可在线改写。 - Profile是例外:当前需先停止实例,再使用“保存并启动”;页面包含Monaco和脱敏配置预览。不要把备份/升级自动停启理解为Profile可在线改写。
- 若实例进入 `error`,不要直接修改数据库状态强制启动。保留原容器、实例目录与 `upgrade_journal` 记录,检查实际容器与 journal 的旧/新容器 ID、阶段、快照路径是否一致。 - 若实例进入 `error`,不要直接修改数据库状态强制启动。保留原容器、实例目录与 `upgrade_journal` 记录,检查实际容器与 journal 的旧/新容器 ID、阶段、快照路径是否一致。
- 人工恢复前停止相关容器并另做完整备份。中断点不同,不能统一用一次目录覆盖或状态修改处理;当前无自动灾难恢复命令。 - 人工恢复前停止相关容器并另做完整备份。中断点不同,不能统一用一次目录覆盖或状态修改处理;当前无自动灾难恢复命令。
@@ -77,7 +77,7 @@ Git 通道直接向 Docker API 发送构建上下文,不执行 Docker CLI 的
} }
``` ```
`version` 是精确 DSH 版本;`release_id` 是构建配置发布编号,修改脚本或 Dockerfile 后即使 DSH 版本不变,也应使用新的编号。`deployment_schema` 声明运行布局兼容协议,当前 Git 发布通道支持协议 7:实例 home 挂载 `/workspace`、共享目录 `/shared`、代理端口 8080,并保留现有认证与启动约定。它不应随普通构建修订递增;不兼容的运行约定需要面板新增支持。`platforms` 只能声明发布者实际验证的平台。 `version` 是精确 DSH 版本;`release_id` 是构建配置发布编号,修改脚本或 Dockerfile 后即使 DSH 版本不变,也应使用新的编号。`deployment_schema` 声明运行布局兼容协议,当前 Git 发布通道支持协议 7:实例 home 是唯一bind挂载,目标为 `/workspace`,不挂载共享目录 `/shared`,代理端口为8080,并保留现有认证与启动约定。它不应随普通构建修订递增;不兼容的运行约定需要面板新增支持。`platforms` 只能声明发布者实际验证的平台。
首次发布时,在独立 checkout 或 worktree 准备 `dockerfile` 分支,将 `release.json` 和完整 `image/` 目录提交并推送到同一远程仓库。无需切换正在开发的主工作目录,也无需 CI。此功能的代码安装本身不会替你创建或推送远程分支;分支未发布前,检查失败是预期状态,旧实例不受影响,但不会回退到内置构建。这是 breaking change:移除 `/api/latest`、`/api/compatibility`、`POST /api/builds` 及 `/api/intents` 的全部查询、创建和取消接口,不提供 410 兼容入口。仅保留 `GET /api/builds` 和 `GET /api/builds/{id}` 构建历史读取。不迁移或恢复旧构建及旧创建意图,不在后台续跑或完成旧预约;这不意味着删除已有实例或数据。 首次发布时,在独立 checkout 或 worktree 准备 `dockerfile` 分支,将 `release.json` 和完整 `image/` 目录提交并推送到同一远程仓库。无需切换正在开发的主工作目录,也无需 CI。此功能的代码安装本身不会替你创建或推送远程分支;分支未发布前,检查失败是预期状态,旧实例不受影响,但不会回退到内置构建。这是 breaking change:移除 `/api/latest`、`/api/compatibility`、`POST /api/builds` 及 `/api/intents` 的全部查询、创建和取消接口,不提供 410 兼容入口。仅保留 `GET /api/builds` 和 `GET /api/builds/{id}` 构建历史读取。不迁移或恢复旧构建及旧创建意图,不在后台续跑或完成旧预约;这不意味着删除已有实例或数据。
@@ -117,12 +117,7 @@ bash scripts/check-trial.sh
该脚本执行本地自动化测试、race、静态检查及前后端构建,不启动容器、不安装依赖、不部署。需提前准备 Go/Node/npm 和前端依赖。可通过 `TEST_TMPDIR` 指定已存在的临时目录。面板构建单测已隔离真实磁盘空闲量;生产磁盘检查仍保留。`image/init-profile.test.mjs` 属于发布分支的 runtime 测试,不在 main 的检查脚本中运行。 该脚本执行本地自动化测试、race、静态检查及前后端构建,不启动容器、不安装依赖、不部署。需提前准备 Go/Node/npm 和前端依赖。可通过 `TEST_TMPDIR` 指定已存在的临时目录。面板构建单测已隔离真实磁盘空闲量;生产磁盘检查仍保留。`image/init-profile.test.mjs` 属于发布分支的 runtime 测试,不在 main 的检查脚本中运行。
`scripts/test-panel.mjs` 必须指向独立测试部署,通过 `TEST_PANEL_URL`、`TEST_USERNAME`、`TEST_PASSWORD` 指定访问参数;`TEST_UPGRADE=1` 包含同镜像替换升级。运行前需由管理员通过 Git 发布构建准备已验证且准入的本地 runtime 镜像;可用 `TEST_FROM_VERSION` / `TEST_TO_VERSION` 选择源和目标版本。脚本不提交构建,只复用这些镜像,会创建及删除测试实例,删除时一并清理实例数据。不要对生产账号运行。`scripts/smoke-image.sh <image-id>` 和 `scripts/test-mobile-image.mjs <image-id>` 仅测试已有镜像的运行行为,不读取 main 的构建源;仅在隔离测试环境使用。旧构建脚本和构建后创建意图的 live 测试已删除。 `scripts/test-panel.mjs` 必须指向独立测试部署,通过 `TEST_PANEL_URL`、`TEST_USERNAME`、`TEST_PASSWORD` 指定访问参数;`TEST_UPGRADE=1` 包含同镜像替换升级。运行前需由管理员通过 Git 发布构建准备已验证且准入的本地 runtime 镜像;可用 `TEST_FROM_VERSION` / `TEST_TO_VERSION` 选择源和目标版本。脚本不提交构建,只复用这些镜像,会创建及删除测试实例,删除时一并清理实例数据。不要对生产账号运行。`scripts/smoke-image.sh <image-id>` 和 `scripts/test-mobile-image.mjs <image-id>` 仅测试已有镜像的运行行为,不读取 main 的构建源;仅在隔离测试环境使用。旧构建脚本和构建后创建意图的 live 测试已删除。
# Session lifetime
Set `SESSION_TTL=24h` in `.env` (default), or `SESSION_TTL=168h` for seven days. ## 会话有效期
Use Go duration units such as `h`, `m`, `s` (not `d`); the value must be a positive
whole number of seconds. Recreate the panel container after changing it: 在 `.env` 设置 `SESSION_TTL=24h`(默认),七天可设置为 `168h`。使用 Go duration 单位 `h`、`m`、`s`,不支持 `d`;必须至少一秒且为整秒,非法值会阻止启动。修改后运行 `docker compose up -d --force-recreate panel`。新登录使用新期限;已有会话保留原绝对到期时间,刷新不会延长。access token签发有效期为五分钟,但数据库会话过期或被撤销后,即使token尚未到期也不能认证。修改自己的密码或管理员重设密码会撤销该用户所有会话,需要重新登录。`.env` 由 Compose 读取,直接运行二进制时应在进程环境中导出 `SESSION_TTL`。
`docker compose up -d --force-recreate panel`. New logins use the new lifetime;
existing sessions retain their absolute expiry and refresh does not extend it.
Access tokens remain valid for five minutes. `.env` is read by Docker Compose;
when running the binary directly, export `SESSION_TTL` in its environment.
+16 -3
View File
@@ -24,7 +24,7 @@ DSH 新运行镜像仅由管理员从本仓库 `dockerfile` 分支获取独立
- 管理员 Git 发布检查、本地镜像异步构建/验证/清理、使用已验证且准入的本地镜像创建实例与事务名额(每用户最多两个实例)、启动对账。 - 管理员 Git 发布检查、本地镜像异步构建/验证/清理、使用已验证且准入的本地镜像创建实例与事务名额(每用户最多两个实例)、启动对账。
- 实例启停删除、独立home(模板7挂载为 `/workspace`)、每实例bridge、归属鉴权及路径反代;DSH认证cookie由服务端保管。 - 实例启停删除、独立home(模板7挂载为 `/workspace`)、每实例bridge、归属鉴权及路径反代;DSH认证cookie由服务端保管。
- 文件上传/下载/重命名/删除及目录ZIP;软配额写前检查、写入串行化与周期巡检。上传最大32 MiB且不覆盖已有文件,删除目录须先清空;删除实例会永久删除其 home、快照及备份。 - 文件上传/下载/重命名/删除及目录ZIP;软配额写前检查、写入串行化与周期巡检。上传最大32 MiB且不覆盖已有文件,删除目录须先清空;删除实例会永久删除其 home、快照及备份。
- 密码加密配置备份、资源目录与manifest、外部加密包导入、恢复健康检查及回滚。备份不再限于六个固定文件;不包含会话、工作区及node_modules。 - 密码加密配置备份、资源目录与manifest、外部加密包导入、恢复健康检查及回滚。备份只包含六个配置文件及 `skins/`、`pets/`、`agent-presets/`、`.agent-presets/` 资源目录,不是整个 home 的备份;会话、普通工作文件及node_modules不在备份范围内。
- 备份/恢复/升级自动编排停启;升级保存密码配置备份和home快照,失败尝试回滚。Profile使用本地Monaco及脱敏预览;目前仍需先停止实例,页面“保存并启动”随后验证就绪。 - 备份/恢复/升级自动编排停启;升级保存密码配置备份和home快照,失败尝试回滚。Profile使用本地Monaco及脱敏预览;目前仍需先停止实例,页面“保存并启动”随后验证就绪。
新备份使用创建时的登录密码经Argon2id派生密钥、AES-GCM加密,不依赖或创建 `backup.key`。导入/恢复须使用原备份密码,修改登录密码不会重加密旧包。旧版密钥包仍兼容,保留这些包时必须保管原 `backup.key`。 新备份使用创建时的登录密码经Argon2id派生密钥、AES-GCM加密,不依赖或创建 `backup.key`。导入/恢复须使用原备份密码,修改登录密码不会重加密旧包。旧版密钥包仍兼容,保留这些包时必须保管原 `backup.key`。
@@ -180,7 +180,7 @@ sudo systemctl reload caddy
管理员在镜像页检查 Git 发布并确认精确 commit 后构建。普通用户只能选择已验证且准入的本地镜像创建实例;没有可选镜像时需等待管理员发布与构建,不再提供 npm 版本、实验构建或构建后自动创建入口。这是 breaking change:移除 `/api/latest`、`/api/compatibility`、`POST /api/builds` 及 `/api/intents` 的全部查询、创建和取消接口,不提供 410 兼容入口。仅保留 `GET /api/builds` 和 `GET /api/builds/{id}` 构建历史读取。不迁移或恢复旧构建及旧创建意图,不在后台续跑或完成旧预约;这不意味着删除已有实例或数据。准入绑定精确镜像ID、版本、模板和平台,不自动信任同tag的新镜像。 管理员在镜像页检查 Git 发布并确认精确 commit 后构建。普通用户只能选择已验证且准入的本地镜像创建实例;没有可选镜像时需等待管理员发布与构建,不再提供 npm 版本、实验构建或构建后自动创建入口。这是 breaking change:移除 `/api/latest`、`/api/compatibility`、`POST /api/builds` 及 `/api/intents` 的全部查询、创建和取消接口,不提供 410 兼容入口。仅保留 `GET /api/builds` 和 `GET /api/builds/{id}` 构建历史读取。不迁移或恢复旧构建及旧创建意图,不在后台续跑或完成旧预约;这不意味着删除已有实例或数据。准入绑定精确镜像ID、版本、模板和平台,不自动信任同tag的新镜像。
历史模板按其自身验证记录与挂载布局判断,不因模板号不是当前版本就标为缺失。有成功验证记录、布局受支持的旧镜像可经管理员验收准入;无法识别的布局不会仅凭一段说明放行。界面展示实际准入原因,以后端校验为准。 历史模板保留其精确身份验证记录,不因模板号不是当前版本就标为缺失。但当前新建/升级及正向准入只支持模板7的挂载布局,且镜像平台必须匹配当前面板平台;旧模板即使有成功验证记录也不能靠管理员填写说明放行。内置默认推荐/兼容策略针对 `linux/amd64`,其他平台不能据此推断已经验收。存量实例的启停仍受实际资源状态与禁止使用策略约束;界面展示具体准入原因,以后端校验为准。
当前模板为修订7,同时内置 `[email protected]` 与 `[email protected]`(MIT,https://github.com/DDDMUC/dsh-free-search)。Free Search及其普通依赖固定版本、校验SHA-512并保留镜像内原始tarball;新profile默认正式安装并启用,已有profile不自动注入。模板4的mobile恢复路径继续保留。仅包含内置固定依赖的profile在启动前离线安装,不限制运行期插件管理器联网。 当前模板为修订7,同时内置 `[email protected]` 与 `[email protected]`(MIT,https://github.com/DDDMUC/dsh-free-search)。Free Search及其普通依赖固定版本、校验SHA-512并保留镜像内原始tarball;新profile默认正式安装并启用,已有profile不自动注入。模板4的mobile恢复路径继续保留。仅包含内置固定依赖的profile在启动前离线安装,不限制运行期插件管理器联网。
@@ -209,4 +209,17 @@ bash scripts/smoke-image.sh "$RUNTIME_IMAGE_ID" # 已由管理员构建的本
合理替代方案为:stdlib HTTP Docker client而非官方SDK;每实例独立bridge而非共享bridge;服务端保管DSH cookie而非下发浏览器。前端已改用 Tailwind CSS + Radix 原语的 shadcn 风格组件(保留本地Monaco),桌面与移动端共用一套响应式界面。界面跟随系统深浅色设置自动切换夜间模式(含 Monaco 编辑器),与内嵌 DSH 行为一致,无独立开关。这些是选型差异,不降低功能、安全或验收标准,详见验收表。 合理替代方案为:stdlib HTTP Docker client而非官方SDK;每实例独立bridge而非共享bridge;服务端保管DSH cookie而非下发浏览器。前端已改用 Tailwind CSS + Radix 原语的 shadcn 风格组件(保留本地Monaco),桌面与移动端共用一套响应式界面。界面跟随系统深浅色设置自动切换夜间模式(含 Monaco 编辑器),与内嵌 DSH 行为一致,无独立开关。这些是选型差异,不降低功能、安全或验收标准,详见验收表。
同源路径代理仅适用于可信用户/插件,尚无独立origin及宿主/局域网出站防护。默认软配额不能阻止agent直接写满磁盘;硬quota是依赖宿主的可选增强,不是默认路线必做项。升级/恢复快照需额外容量,home快照上限1 GiB,模板7的 `/workspace` 就是实例home,会随home快照回滚;旧共享目录不随回滚恢复;中断及回滚失败仍可能需要管理员恢复。 同源路径代理仅适用于可信用户/插件,尚无独立origin及宿主/局域网出站防护。默认软配额不能阻止agent直接写满磁盘;硬quota是依赖宿主的可选增强,不是默认路线必做项。升级/恢复快照需额外容量,home快照上限1 GiB,模板7的 `/workspace` 就是实例home,会随home快照回滚。旧的用户级共享工作区接口和新容器共享挂载已经移除;磁盘上可能遗留的旧共享目录不属于实例home快照,也不会自动迁移。中断及回滚失败仍可能需要管理员恢复。
## 开发验证与文档维护
安装前端依赖后,可运行本地检查:
```sh
bash scripts/check-trial.sh
node --test scripts/check-trial.test.mjs
```
前者依次执行 Go 单测、race 检查、vet、构建、前端单测与生产构建,以及浏览器验收脚本的语法检查;后者验证检查脚本自身的失败传播行为。它们不等于真实 Docker、浏览器或公网 HTTPS 验收,需要外部环境的测试及历史记录见 [ACCEPTANCE.md](ACCEPTANCE.md)。
修改接口时同步 [API.md](API.md) 和 [前端对接说明](web/API_REQUEST.md);修改用户流程时同步本页和 [前端说明](web/README.md);修改部署、认证或数据布局时同步 [OPERATIONS.md](OPERATIONS.md)、[认证说明](internal/api/AUTH_DESIGN.md) 与 [DESIGN.md](DESIGN.md)。验收记录应注明验证范围,不把历史通过记录视为当前版本的全量验证。
+27 -6
View File
@@ -4,10 +4,13 @@
five-minute HS256 access JWT in an additional HttpOnly cookie. JWT verification five-minute HS256 access JWT in an additional HttpOnly cookie. JWT verification
fixes the algorithm and binds subject/session/issuer/audience/expiry. Bare fixes the algorithm and binds subject/session/issuer/audience/expiry. Bare
session IDs never authenticate. Internal callers and test fixtures must also session IDs never authenticate. Internal callers and test fixtures must also
attach a signed access cookie. attach a signed access cookie. Database session expiry or revocation rejects
- Refresh is a random 256-bit HttpOnly cookie, scoped to `/api`, with only its access even if the JWT's five-minute expiry has not yet elapsed.
- The initial refresh token is random 256-bit data in an HttpOnly cookie, scoped to `/api`, with only its
SHA-256 hash stored in SQLite. The session has an absolute lifetime configured SHA-256 hash stored in SQLite. The session has an absolute lifetime configured
by SESSION_TTL (default 24h), applied only to new logins. by SESSION_TTL (default 24h), applied only to new logins. Values use Go duration
syntax (e.g. 168h, not 7d), must be at least one second and a whole number of
seconds; invalid values prevent startup when the store is opened.
Session, refresh and CSRF cookies persist across browser restarts with explicit Session, refresh and CSRF cookies persist across browser restarts with explicit
Max-Age/Expires bounded by that database expiry; rotation does not extend it. Max-Age/Expires bounded by that database expiry; rotation does not extend it.
Transactional rotation stores current and previous token hashes. A keyed, Transactional rotation stores current and previous token hashes. A keyed,
@@ -19,13 +22,31 @@
refresh credentials are never stored. refresh credentials are never stored.
Logout deletes the database session and its refresh binding. Both refresh and Logout deletes the database session and its refresh binding. Both refresh and
logout retain mandatory exact Origin and session-bound CSRF checks, even when logout retain mandatory exact Origin and session-bound CSRF checks, even when
access has expired. Other operations require valid access. access has expired, but require an unexpired database session. Logout does not
require the refresh cookie. Other protected operations require valid access.
Self-service password changes and administrator password resets revoke all of
that user's sessions and refresh bindings, requiring a new login.
- Signing key is generated once under the configured data root, private mode - Signing key is generated once under the configured data root, private mode
0600, and reused across restarts. Initialization errors fail authentication closed. 0600, and reused across restarts. Initialization errors fail authentication closed.
Tokens never appear in JSON, URLs, audit detail, or application logs. Session/access/refresh credentials are issued as cookies, not JSON or URLs,
and are not included in authentication audit detail or application logs. The
CSRF token is intentionally returned as `csrf_token` by login, refresh and
`/api/me`, and is also in the JavaScript-readable `dsh_csrf` cookie. All four
cookies use SameSite=Lax; the other three are HttpOnly. Secure is enabled by
direct TLS or an HTTPS PANEL_PUBLIC_URL, not forwarded protocol headers.
Origin checks compare against the configured public origin, or direct request
scheme/Host if it is unset; forwarded Host/Proto headers are not used.
- Known accounts lock for 15 minutes after five failures; the counter/lock survive - Known accounts lock for 15 minutes after five failures; the counter/lock survive
restart. Successful login resets the counter; expired locks reset the counter. restart. Successful login resets the counter; expired locks reset the counter.
Existing global throttling remains additional protection. Current-password reauthentication failures share this account lock; successful
reauthentication alone does not reset the counter. Additional login protection
allows 30 attempts per source per minute, at most 1024 source buckets, and eight
admitted concurrent login requests (credential checks are serialized). Source
identity uses the direct peer unless it matches TRUSTED_PROXY_CIDRS, a
comma-separated CIDR list; then X-Forwarded-For is scanned right-to-left for
the first untrusted address. These in-memory limits reset on process restart.
The server runs expired-session/refresh and elapsed-lock cleanup immediately
and every ten minutes; expiry checks do not wait for cleanup.
- Tests cover missing/tampered/expired access, database binding, refresh rotation - Tests cover missing/tampered/expired access, database binding, refresh rotation
and replay, CSRF/Origin rejection, logout revocation, key reuse/permissions, and replay, CSRF/Origin rejection, logout revocation, key reuse/permissions,
account isolation, restart persistence, lock expiry, and runtime compatibility. account isolation, restart persistence, lock expiry, and runtime compatibility.
+27 -6
View File
@@ -1,11 +1,32 @@
# 前后端协调 # 前后端接口对接
前端 agent 当前工具没有 actor/send,无法直接发送给 general-2。请后端在根目录 API.md 记录实际接口、鉴权/CSRF、列表 envelope、实例/profile/备份/升级、文件上传和管理员字段。前端将以 API.md 校对实现,不模拟成功。 根目录 API.md 是接口契约。公共请求封装与响应类型在 src/api.ts,实例、备份及管理员等组件也通过 api.request 发起业务请求。本文记录当前前端依赖,不将界面入口或请求成功等同于端到端验收。
Vite 开发监听 127.0.0.1,代理默认 http://127.0.0.1:8088,生产构建 web/dist,同源 /api 和 /dsh。 ## 运行与认证
已完成中文页面与首次构建,当前待确认的响应字段:images 是 id 还是 image_id;builds 日志字段 logs;ws/tree 是否直接数组及 is_dir/mod_time;usage used_bytes/quota_bytes;audit user_id/created_at;backups id/size/created_at。请明确空列表返回 [] 或 null。恢复/下载备份/升级接口尚未列出,UI 将明确提示缺口而不猜测接口。 Vite 开发监听 127.0.0.1,默认端口 5173(占用时自动尝试后续端口),代理至 http://127.0.0.1:8088;/dsh 启用 WebSocket 代理。生产构建目录为 web/dist,需由后端同源提供静态页面、/api 和 /dsh。
11:24 检查 internal/api/api.go:创建实例入参目前 struct{ Name, ImageID, Version string } 无 json 标签,image_id 不会映射到 ImageID,需后端补标签。生命周期仍只是写 start/stop 字符串而非真实 Docker 操作,前端不会把 start 字符串映射成 running。请务必真实实现或返回明确错误,避免假成功。 - 登录和 /api/me 返回 {user,csrf_token}。认证使用 HttpOnly Cookie;浏览器请求携带同源 Cookie,非 GET 请求从可读的 dsh_csrf Cookie 设置 X-CSRF-Token,而不是只使用内存中的旧 token。
- 存在 CSRF token 时,非登录/刷新请求的首次 401 会尝试恢复会话,再重试原请求;使用页内共享 Promise 和可用时的 Web Locks 协调刷新,刷新冲突后复验 /api/me。无法恢复的会话通过 session-expired 通知界面。
- 错误响应读取 {error:string};网络错误和非 JSON 成功响应均显示明确错误,不模拟成功。文件/备份下载和图片预览直接使用同源资源地址,不经过 JSON 请求封装。
验证:npm test 六项全部通过;npm run build 通过。Playwright 真浏览器已验证桌面/390px 移动登录界面、离线登录错误,移动 documentWidth=390 无横向溢出。8088 暂未启动,无法验证登录后页面;请父 agent 启动后端后通知可用临时验证账号,避免操作共享数据。 ## 响应结构与镜像构建
- 实例、镜像、构建历史、工作区目录、用户、审计、备份及孤儿容器的 GET 列表响应为顶层数组,不使用 data envelope;空列表应返回 []。其中 /api/orphans 返回容器 ID 字符串数组;详情、用量和操作结果不适用此列表约定。
- 镜像类型含 id、可选 image_id、version、status、admission、admission_reason、selectable 等字段;前端提交镜像标识时使用 image_id || id,仅允许 status=available 且 selectable=true 的镜像用于创建/升级。
- 构建历史通过 GET /api/builds?limit=...&offset=... 获取摘要;前端每页显示 20 条、多取 1 条判断下一页,不读取 X-Has-More。日志按需通过 GET /api/builds/{id} 的 logs 字段获取。
- 管理员通过 GET /api/releases/latest 检查 Git 发布,确认后 POST /api/releases/build {commit};构建来源字段包括 release_id、source_commit、source_repository。镜像准入使用 POST /api/images/admission {image_id,state,evidence}。不使用旧的 /api/latest、/api/compatibility、POST /api/builds 或创建意图接口。
- 工作区目录项读取 name/path/is_dir/size/mod_time;用量读取 used_bytes/quota_bytes;审计读取 user_id/created_at 等字段;备份读取 id/size/created_at。
## 已接入的业务操作
- 创建实例:POST /api/instances {name,image_id},当前前端不发送 version。启动/停止/重启调用实例对应子路径;删除使用 DELETE /api/instances/{id}。显示后端返回的实例状态,不把操作名称当作运行状态。
- Profile:读取 /profile 和 /profile-preview;停止实例后 PUT /profile {content,restart:true} 保存并启动验证。日志读取 /logs 的 logs 字段。上述路径均位于 /api/instances/{id} 下。
- 备份:GET /api/instances/{id}/backups 列表,POST 同路径 JSON {password} 创建,multipart file,password 导入;GET 同路径 ?download={backup_id} 下载;POST /api/instances/{id}/restore {backup_id,password} 恢复。
- 升级:POST /api/instances/{id}/upgrade {image_id,password}。创建备份和升级使用当前登录密码;导入/恢复使用原包密码,旧平台 key 包可留空。恢复和升级有二次确认。
- 工作区:/api/ws/tree、usage、mkdir、rename、delete、upload、download,并通过 instance 参数选择实例;上传使用 multipart file,path。图片预览使用 /api/ws/image;文本读取 /api/ws/text 返回 {content,version},保存 PUT 同路径 {content,version},冲突不静默覆盖。
- 账户管理:本人 POST /api/me/password;管理员通过 /api/users 创建用户、PATCH 修改配额,通过 /api/users/reset-password 重设密码,并读取 /api/audit。本人改密后重新登录,管理员重设按 reauthenticate 决定是否退出当前会话。孤儿容器回收使用 /api/orphans,删除提交 id 与 confirm。
## 验证边界
在 web/ 执行 npm test 和 npm run build 获取当前测试与构建结果,不沿用早期测试数量或临时服务状态。单元测试包含 fetch 替身,不能证明真实后端、Docker、WS/SSE 或数据恢复流程已验收;真实联调需独立测试环境和授权账号,具体后端限制与验收记录见根目录 API.md、README.md、ACCEPTANCE.md。
+9 -7
View File
@@ -1,10 +1,10 @@
# DSH 中文工作台 # DSH 中文工作台
React 19 + TypeScript + Vite;普通 CSS 响应式布局,没有运行时演示数据。 React 19 + TypeScript + Vite;使用 Tailwind CSS 4、Radix UI 和自定义 CSS 构建响应式界面,Profile 编辑器按需加载 Monaco,没有运行时演示数据。
## 开发与验证 ## 开发与验证
使用 Node.js 22+: 使用 Node.js 22+,在 web/ 目录执行:
~~~bash ~~~bash
npm ci npm ci
@@ -13,14 +13,16 @@ npm test
npm run build npm run build
~~~ ~~~
开发服务只监听 127.0.0.1:5173,/api 和 /dsh 代理至 127.0.0.1:8088。生产产物为 dist/,由后端同源提供;不能直接通过 file:// 使用。 开发服务监听 127.0.0.1,默认端口 5173(占用时 Vite 会尝试后续端口);/api 和 /dsh 代理至 127.0.0.1:8088,/dsh 支持 WebSocket 代理。生产产物为 web/dist/,需配置后端静态目录并同源提供;不能直接通过 file:// 使用。
## API 与安全 ## API 与安全
- 根目录 API.md 是接口契约;API 客户端集中在 src/api.ts,可注入 fetch 进行独立单元测试。 - 根目录 API.md 是接口契约;公共请求封装和类型位于 src/api.ts,可注入 fetch 进行独立单元测试。业务组件也通过 api.request 调用接口;当前对接说明见 API_REQUEST.md。
- HttpOnly Cookie 登录;CSRF token 仅留在内存,刷新通过 /api/me 重新获取。密码、token 不写入 localStorage。 - 使用同源 HttpOnly 认证 Cookie;浏览器写请求从可读的 dsh_csrf Cookie 获取 X-CSRF-Token。启动时通过 /api/me 恢复登录状态,符合条件的 401 会触发 /api/refresh 并重试原请求,支持并发刷新协调。密码、token 不写入 localStorage;工作台所选实例会按用户保存在 localStorage。
- 操作失败保留错误,不模拟成功;删除、重启、构建等操作二次确认。 - 操作失败保留错误,不模拟成功;删除、重启、构建等操作二次确认。
- 备份恢复和升级等待后端契约,当前明确显示不可用说明,不发送猜测接口。 - 已接入 Profile 编辑/预览、加密备份创建/导入/下载/恢复和本地镜像升级;创建备份和升级要求当前登录密码,导入/恢复使用原备份密码。
- 创建实例和升级只允许 status=available 且 selectable=true 的本地镜像;管理员通过 Git 发布检查与精确 commit 提交构建,并可管理镜像准入策略。
- 工作区支持文件管理、目录 ZIP 下载、图片预览和带版本校验的文本编辑;管理员页面提供用户、配额、密码重设、审计及孤儿容器回收,本人可修改密码。
- 单元测试里的 fetch 替身仅用于测试,不打包进生产页面。 - 单元测试里的 fetch 替身仅用于测试,不打包进生产页面。
当前后端仍在实现部分端点,页面会真实显示其 HTTP 错误。请不要把前端入口存在等同于后端能力已交付。 测试使用 Vitest,覆盖 API、认证刷新、组件和构建分块等行为;其中构建测试使用不落盘的 Vite 构建。测试或构建通过不等于真实 Docker 生命周期、代理会话及备份恢复已完成端到端验收;部署限制和后端验收范围以根目录 README.md、API.md 和 ACCEPTANCE.md 为准。