From 09e202796f1c0a679a49deaeffc1e3b9fe8620f9 Mon Sep 17 00:00:00 2001 From: m1saka Date: Fri, 16 Oct 2026 11:37:18 +0800 Subject: [PATCH] docs: synchronize documentation with current implementation --- ACCEPTANCE.md | 62 +++++-- API.md | 71 ++++--- DESIGN.md | 360 +++++++++++++----------------------- OPERATIONS.md | 23 +-- README.md | 19 +- internal/api/AUTH_DESIGN.md | 33 +++- web/API_REQUEST.md | 33 +++- web/README.md | 16 +- 8 files changed, 307 insertions(+), 310 deletions(-) diff --git a/ACCEPTANCE.md b/ACCEPTANCE.md index 850c20b..d362270 100644 --- a/ACCEPTANCE.md +++ b/ACCEPTANCE.md @@ -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。 -## 恢复宽限期与轮询缓存修复(独立复审后) +## 当前实现与验收状态(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 实例。 @@ -185,7 +217,7 @@ 用户确认的孤儿策略:管理员二次确认回收身份/挂载核验通过、无实例/升级引用的停止容器;不自动纳管,不触碰运行中或来源不明容器,不删数据/网络。其他架构替代见下表;不声称逐字遵循SDK、组件库或目录示意。 -## 早期进度记录(以下由上方最新结论覆盖) +## 后续UI改版记录(历史独立阶段,不由2026-10-10结案覆盖) ## UI 改版与功能回归(2026-10-15) @@ -199,34 +231,22 @@ - 本次升级仅覆盖 0.2.0-rc.2 到相同版本的容器替换;跨版本升级、插件重建、离线失败回滚、长时会话续期,以及真实配额耗尽/跨用户实例访问未在本轮完整实测,不计为通过。未调用付费模型。 - 一次性测试实例及其容器/网络均已清理;原有 DSH 实例未重启。测试日志及隔离备份保留于 /tmp/dsh-isolated-integration-BFUCkK/(本地临时证据,不随提交分发)。 -## 当前实现与待验收 +## 早期实现与待验收表(已归档) -| 设计章节 | 代码已接入 | 尚需真实证据 | -|---|---|---| -| §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为可选增强 | +原表描述模板2、npm构建及预约阶段,已由文首当前矩阵替代。早期未闭项须按各阶段证据解释,不能推导当前Git发布通过。配置备份为白名单包,不含完整工作区、依赖或会话;旧密钥包仍须保管原key。操作中断或回滚失败不保证自动灾难恢复。 -备份包含六个固定配置文件及skins、pets、agent-presets、.agent-presets资源树和manifest,不包含node_modules、会话或工作区。新密码包不依赖backup.key;保留旧密钥包时仍须保管原key。备份/恢复/升级不再统一要求用户手动停机;操作中断或回滚失败不保证自动灾难恢复。 - -## 合理替代方案(不改变验收目标) +## 历史合理替代方案(不改变验收目标) | DESIGN选型 | 当前实现 | 等价目标与边界 | |---|---|---| | 官方Docker SDK | stdlib net/http Docker Engine client,API v1.47 | 仍须验证编排、构建流、错误处理和daemon兼容性,不能以未用SDK认定功能缺失 | | 共享bridge | 每实例独立bridge,面板接入 | 减少实例直接互通;不等于完整出站或宿主网络隔离 | | 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,当前使用路径代理而不传该参数。 -## 历史证据与本轮边界 +## 早期历史证据与当时边界(非当前待办结论) 本轮已取得真实证据: - 非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 实例未重启,构建前后实例记录一致。 -## 模板 7:完整 DSH_HOME 改为 /workspace +## 历史模板7迁移:完整 DSH_HOME 改为 /workspace + +> 本节的/shared挂载、共享入口与迁移结果仅描述当时方案;当前共享入口和新容器共享挂载均已移除,实例删除会清理自身数据及备份,详见文首矩阵。 ### 管理交互优化验收 diff --git a/API.md b/API.md index fbfc2fa..920298c 100644 --- a/API.md +++ b/API.md @@ -1,6 +1,11 @@ # 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);空表示不限制。大小写不敏感,非内容/恶意软件检测。 @@ -12,33 +17,39 @@ GET /api/orphans 返回可回收的完整容器ID数组。DELETE /api/orphans?id ## 其他配置 - LISTEN_ADDR(默认 127.0.0.1:8088) - 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) - 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 字节,不设置则拒绝启动,不打印密码) -- WEB_DIR(可选静态前端目录) +- WEB_DIR(静态前端目录,默认 web/dist) - PANEL_CONTAINER_ID(面板容器名/ID,用于加入实例网络;宿主进程运行时留空) - DEFAULT_QUOTA_BYTES(默认10GiB) - 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/logout → {ok:true} -- POST /api/refresh → {user,csrf_token}(刷新HttpOnly cookie,需Origin和CSRF;并发轮换冲突且session仍有效时返回409,不签发cookie,客户端可复验 `/api/me` 后重试原请求;会话撤销/过期返回401,存储异常不能当作注销) +- 登录请求体最多4096字节,非法/多段 JSON 返回400,凭据错误401,Origin错误403;每来源每分钟最多30次、账户累计5次失败锁定15分钟,限流/锁定429,认证不可用或并发槽耗尽503。 +- 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} - 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。 - 同一事务保存 bcrypt hash、撤销本人全部 session/refresh、清除失败计数,并记录 user.change_password 审计(操作者和目标均为本人 ID,detail 为空,无密码/hash/token);事务失败返回 500 并完整回滚,其他用户不受影响。成功后客户端应清理登录状态并重新登录;旧密码、旧 access/session/refresh 不再可用。已建立的 runtime WS/SSE 不主动断开,下一次请求或重连重新鉴权。 ## 管理 -- GET /api/users(管理员) -- POST /api/users {username,password,admin?,quota_bytes?}(管理员) -- PATCH /api/users {id,quota_bytes}(管理员修改正数配额) +- GET /api/users → User[](管理员,按创建时间排序) +- 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} → {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。 - 同一事务更新密码、删除目标全部 session/refresh、清除目标登录失败锁定,并记录 user.reset_password 审计(操作者 ID、目标 ID、时间,detail 为空,不含密码/hash/token)。任何数据库失败返回 500 并回滚,不影响其他用户会话。与登录串行,等待后重新检查管理员会话和权限。 - 允许重设自身,此时 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不可选。 -历史兼容基线为 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 -- POST /api/instances {name,image_id,version} → Instance(每用户最多 2 个;image_id 必须指向已验证且准入的精确本地镜像,不接受构建预约) -- POST /api/instances/{id}/start|stop|restart +- POST /api/instances {name,image_id} → Instance(每用户最多 2 个,超限409;image_id 必须为 sha256: 加64位小写十六进制且指向已验证、准入的精确本地镜像。version 由镜像标签推导,提交 version 或其他未知字段返回400,不接受构建预约。name 去除首尾空白后非空,原始长度最多200字节;请求体最多64 KiB) +- GET /api/instances/{id} → Instance +- POST /api/instances/{id}/start|stop|restart → Instance - DELETE /api/instances/{id} → {ok:true} -所有成功 JSON 列表均为顶层数组(不是 data 包装);错误均为 {error:string}。实现与验收仍在进行,不应仅凭接口列在此处认定已验收。 -- GET /api/instances/{id}/logs -- /dsh/{id}/...(登录和 owner 校验;需同 Docker 网络) +实例列表仅包含当前用户的实例;管理员也不能通过实例接口访问其他用户实例(404)。列表 Docker inspect 失败时该项 status 为 unavailable,详情 inspect 失败则502。生命周期操作遇到恢复状态或文件读租约冲突409,Docker操作失败通常502。 + +列表型接口返回顶层数组(不是 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/usage +- GET /api/ws/tree?path=. → [{name,path,is_dir,size,mod_time}](按名称排序,目录 size 为0) +- GET /api/ws/usage → {used_bytes,quota_bytes}(当前用户总用量,不是单实例用量;可不带 instance,带上时仍检查归属) - POST /api/ws/mkdir {path} - POST /api/ws/rename {path,new_path} - DELETE /api/ws/delete?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。 - 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。 @@ -96,7 +121,7 @@ GET /api/orphans 返回可回收的完整容器ID数组。DELETE /api/orphans?id - 下载不是原子快照:流式读取前复核文件身份、大小、类型及修改时间;并发修改、取消或传输错误可能导致连接中断/不完整 ZIP,请停止写入后重试。单文件下载不受 ZIP 数量、大小、并发和时限约束。 ## profile / 备份 -- GET /api/instances/{id}/profile +- GET /api/instances/{id}/profile → {content:string}(配置不存在时 content 为空) - PUT /api/instances/{id}/profile {content,restart?}(停机原子写;restart=true启动并检查就绪) - GET /api/instances/{id}/profile-preview(bundle清单和脱敏dump-config,字符串值隐藏;运行中才能dump) - 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}/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 生命周期、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。 diff --git a/DESIGN.md b/DESIGN.md index a55a081..fb78b7d 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -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: 容器1 │ │ 容器2 ... │ │ 不发布端口 -│ │ caddy:8080 → 3080 │ │ │ │ /data/dsh-home、/workspace ← 宿主 bind -│ └─────────────────────┘ └───────────────┘ │ -└─────────────────────────────────────────────┘ -``` +浏览器 → 面板 SPA/API 或 /dsh// → 实例专属 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//files/...` | `/var/lib/dsh-mgr/workspaces//files` | -| 实例 DSH_HOME | `/data/workspaces//instances//home/...` | `/var/lib/dsh-mgr/workspaces//instances//home` | -| 备份包 | `/data/backups///...` | (不入容器,仅面板读写) | +| 实例完整 home | /data/workspaces//instances//home | /workspaces//instances//home → /workspace | +| 配置备份 | /data/backups/// | 不挂入 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 # 用户、实例、配额、备份索引 +/ + db.sqlite workspaces// - files/ # 用户工作区(面板文件 API + 各 dsh 容器共享挂载) - └─ <用户自由目录结构> - instances// - 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/// - .tar.gz # 配置备份包 - # 镜像层由本机 Docker daemon 管理,不作为此目录下的文件保存;DB 记录镜像索引 + instances//home/ # 完整 DSH_HOME → /workspace + .credentials.yaml + cordis.patch.yml + profiles/web/ # 配置、插件清单、可重建依赖 + storages/ # 实例工作区数据等 + files/ # 可能遗留;当前无共享入口或新容器挂载 + backups/// # 加密配置备份;不是完整 home 快照备份 ``` -**配额作用域**:`workspaces//` 整棵树(files + 所有 instances 的 home)。每用户一个配额单位。 +账号软配额统计 workspaces//,并非只统计当前实例。实例删除会清理该实例目录及对应备份;历史共享 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 --trusted-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:-`,其中 `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 ,不传 --public-url。 -- **创建**: - - Image = 本地可用镜像的固定 image ID(对应精确 DSH 版本及模板修订,见 §5) - - Env:`DSH_HOME=/data/dsh-home`(密钥不入 env;用户在 dsh UI 里设 key,落到 `.credentials.yaml`) - - Binds(Source 用 **HOST 路径**): - - `workspaces//instances//home` → `/data/dsh-home` - - `workspaces//files` → `/workspace` - - Network = `dsh-net`(不发布端口) - - Cmd = `web --no-open --port 3080 --public-url https:///dsh// --trusted-host ` - - 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// 转发至对应实例网络中的 runtime:8080;归属检查在代理之前。Compose 默认仅向 loopback 发布面板8088,公网需管理员另配 HTTPS 入口与可信代理。子域名、直接发布 runtime 端口不是当前默认能力。 -- 面板**启动时 ensure** `dsh-net`(bridge)存在,并把自身加入(compose 声明或运行时 `network connect`)。 -- dsh 容器挂 `dsh-net`,**不发布主机端口**。 -- 面板反代 `/dsh//` → `http://.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//*` → 反代到 `http://.dsh-net:8080`,**剥掉 `/dsh/` 前缀**,**保留浏览器 Host**,加 `X-Forwarded-*`。 -- **token 桥接**:容器启动后,面板从 `docker logs` 流式抓 dsh 打印的 `http://127.0.0.1:3080/?token=xxx` 里的 token;用户首次打开 `/dsh//` 时,面板服务端用该 token 打一次容器换 cookie,再把 `Set-Cookie` 的 `Path=/` 改写为 `Path=/dsh/`、加 `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/ && zfs set refquota=10G`,再 bind 该 dataset 到 `/data/workspaces/`。 - 或在宿主跑**极小特权配额 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///` + 索引入库 → 重启容器。支持立即下载。 +**备份流程**:按受控流程停止原本运行的容器,打包白名单并加密落盘,再恢复原运行状态;支持下载。不是任意运行中文件的一致性全量快照。 -**还原流程**:校验+解密包 → 停容器 → 备份当前 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 (不 remove,保留作回滚) -4. docker create :同名 instId、同 binds(home + files)、同 labels、新 image、 - Cmd 按新版本要求重算 --public-url/--trusted-host -5. entrypoint 首启:若 profiles/web/node_modules 缺失/与 package.json 不一致 → pnpm install 重建插件 -6. docker start → 面板抓 token、健康检查(轮询 caddy :8080) -7. 成功:docker rm ;失败:stop 、start 、标记回滚、提示用户 +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/ 反代 + 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//` 可用(含 WS)。 -4. **M4 工作区文件 API + 软配额**。 -5. **M5 备份/还原 + profile 编辑**。 -6. **M6 升级流程 + 回滚**。 -7. **M7 SPA 完善 + 审计 + 资源限制 + 可选硬配额**。 +历史M1–M7结案不等于当前commit或真实部署整体通过;本地测试通过、真实Docker通过、浏览器通过与公网验收必须分开记录。 --- diff --git a/OPERATIONS.md b/OPERATIONS.md index 3a7d995..20bb5ee 100644 --- a/OPERATIONS.md +++ b/OPERATIONS.md @@ -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` 或重启原容器不会更新其环境变量;重建面板会短暂中断面板访问。 -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 版本或预约构建后创建。镜像不推送至仓库。 4. 创建实例后从面板进入 DSH。不要给 runtime 发布宿主端口或挂载 Docker socket。 @@ -17,9 +17,9 @@ ## 公网 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 而改变;本版本不承诺抵御分布式拒绝服务。 @@ -41,7 +41,7 @@ 在镜像页“准入管理”选择推荐或兼容并填写依据,后端还会检查精确镜像身份、成功验证记录和可支持的挂载布局。审批只作用于该镜像;新镜像需要自己的验证与审批。退为仅存量后不允许新建/升级目标;标为禁止使用还阻止新的启动/重启及受控配置修改、备份/恢复写操作,避免其自动重启绕过禁用。已有配置读取和通用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。全量备份应连同签名密钥和该目录保存,不要向用户公开。 @@ -49,7 +49,7 @@ 升级到本版本后应在维护窗口逐个访问现有实例,完成首次认证持久化。若在首次成功连接前,启动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可在线改写。 - 若实例进入 `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}` 构建历史读取。不迁移或恢复旧构建及旧创建意图,不在后台续跑或完成旧预约;这不意味着删除已有实例或数据。 @@ -117,12 +117,7 @@ bash scripts/check-trial.sh 该脚本执行本地自动化测试、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 ` 和 `scripts/test-mobile-image.mjs ` 仅测试已有镜像的运行行为,不读取 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: -`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. +## 会话有效期 + +在 `.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`。 diff --git a/README.md b/README.md index 826200b..38b1a6e 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ DSH 新运行镜像仅由管理员从本仓库 `dockerfile` 分支获取独立 - 管理员 Git 发布检查、本地镜像异步构建/验证/清理、使用已验证且准入的本地镜像创建实例与事务名额(每用户最多两个实例)、启动对账。 - 实例启停删除、独立home(模板7挂载为 `/workspace`)、每实例bridge、归属鉴权及路径反代;DSH认证cookie由服务端保管。 - 文件上传/下载/重命名/删除及目录ZIP;软配额写前检查、写入串行化与周期巡检。上传最大32 MiB且不覆盖已有文件,删除目录须先清空;删除实例会永久删除其 home、快照及备份。 -- 密码加密配置备份、资源目录与manifest、外部加密包导入、恢复健康检查及回滚。备份不再限于六个固定文件;不包含会话、工作区及node_modules。 +- 密码加密配置备份、资源目录与manifest、外部加密包导入、恢复健康检查及回滚。备份只包含六个配置文件及 `skins/`、`pets/`、`agent-presets/`、`.agent-presets/` 资源目录,不是整个 home 的备份;会话、普通工作文件及node_modules不在备份范围内。 - 备份/恢复/升级自动编排停启;升级保存密码配置备份和home快照,失败尝试回滚。Profile使用本地Monaco及脱敏预览;目前仍需先停止实例,页面“保存并启动”随后验证就绪。 新备份使用创建时的登录密码经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的新镜像。 -历史模板按其自身验证记录与挂载布局判断,不因模板号不是当前版本就标为缺失。有成功验证记录、布局受支持的旧镜像可经管理员验收准入;无法识别的布局不会仅凭一段说明放行。界面展示实际准入原因,以后端校验为准。 +历史模板保留其精确身份验证记录,不因模板号不是当前版本就标为缺失。但当前新建/升级及正向准入只支持模板7的挂载布局,且镜像平台必须匹配当前面板平台;旧模板即使有成功验证记录也不能靠管理员填写说明放行。内置默认推荐/兼容策略针对 `linux/amd64`,其他平台不能据此推断已经验收。存量实例的启停仍受实际资源状态与禁止使用策略约束;界面展示具体准入原因,以后端校验为准。 当前模板为修订7,同时内置 `dsh-web-mobile@3.0.5` 与 `dsh-free-search@0.8.5`(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 行为一致,无独立开关。这些是选型差异,不降低功能、安全或验收标准,详见验收表。 -同源路径代理仅适用于可信用户/插件,尚无独立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)。验收记录应注明验证范围,不把历史通过记录视为当前版本的全量验证。 diff --git a/internal/api/AUTH_DESIGN.md b/internal/api/AUTH_DESIGN.md index bfaeaf7..9f7bc32 100644 --- a/internal/api/AUTH_DESIGN.md +++ b/internal/api/AUTH_DESIGN.md @@ -4,10 +4,13 @@ five-minute HS256 access JWT in an additional HttpOnly cookie. JWT verification fixes the algorithm and binds subject/session/issuer/audience/expiry. Bare session IDs never authenticate. Internal callers and test fixtures must also - attach a signed access cookie. -- Refresh is a random 256-bit HttpOnly cookie, scoped to `/api`, with only its + attach a signed access cookie. Database session expiry or revocation rejects + 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 - 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 Max-Age/Expires bounded by that database expiry; rotation does not extend it. Transactional rotation stores current and previous token hashes. A keyed, @@ -19,13 +22,31 @@ refresh credentials are never stored. Logout deletes the database session and its refresh binding. Both refresh and 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 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 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 and replay, CSRF/Origin rejection, logout revocation, key reuse/permissions, account isolation, restart persistence, lock expiry, and runtime compatibility. diff --git a/web/API_REQUEST.md b/web/API_REQUEST.md index bd03bf3..bdaee7d 100644 --- a/web/API_REQUEST.md +++ b/web/API_REQUEST.md @@ -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。 diff --git a/web/README.md b/web/README.md index 250f3a9..993e020 100644 --- a/web/README.md +++ b/web/README.md @@ -1,10 +1,10 @@ # 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 npm ci @@ -13,14 +13,16 @@ npm test 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.md 是接口契约;API 客户端集中在 src/api.ts,可注入 fetch 进行独立单元测试。 -- HttpOnly Cookie 登录;CSRF token 仅留在内存,刷新通过 /api/me 重新获取。密码、token 不写入 localStorage。 +- 根目录 API.md 是接口契约;公共请求封装和类型位于 src/api.ts,可注入 fetch 进行独立单元测试。业务组件也通过 api.request 调用接口;当前对接说明见 API_REQUEST.md。 +- 使用同源 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 替身仅用于测试,不打包进生产页面。 -当前后端仍在实现部分端点,页面会真实显示其 HTTP 错误。请不要把前端入口存在等同于后端能力已交付。 +测试使用 Vitest,覆盖 API、认证刷新、组件和构建分块等行为;其中构建测试使用不落盘的 Vite 构建。测试或构建通过不等于真实 Docker 生命周期、代理会话及备份恢复已完成端到端验收;部署限制和后端验收范围以根目录 README.md、API.md 和 ACCEPTANCE.md 为准。