Files
dsh-web/API.md
T
m1saka 602917698a feat: add safe backup management and theme preferences
Add snapshot usage and protected cleanup, preserve unsaved profile edits, avoid holding SQL connections during snapshot deletion, and fix download filenames. Include regression tests and API documentation.
2026-10-17 00:36:21 +08:00

154 lines
25 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# DSH Web 面板 API(MVP)
默认监听 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);空表示不限制。大小写不敏感,非内容/恶意软件检测。
## 实例备份与快照管理
- `DELETE /api/instances/:id/backups?id=<32hex>` 删除当前用户实例的加密 `.dshbak` 包及其索引,记录 `backup.delete` 审计。ID 为32位小写十六进制;支持无索引的旧包,以及文件已不存在的残留索引。
- `GET /api/instances/:id/snapshots` 返回数组 `{id,size,created_at,protected,protection_reason,size_error?}`。`size` 为普通文件逻辑字节总和(与配额口径相同,硬链接逐路径计数,不跟随符号链接);扫描失败为 `-1` 并提供 `size_error`,不能显示为零。`created_at` 为目录修改时间的 RFC3339 字符串,不保证真实创建时间。未保护时 `protection_reason` 是空字符串。
- `DELETE /api/instances/:id/snapshots?id=<id>` 删除选定快照。先持久化 `snapshot.delete.started`,删除成功后记录 `snapshot.delete.completed`,删除失败则记录 `snapshot.delete.failed`;target 为实例 ID,detail 为快照 ID。初始审计失败时不会删除,终态审计失败返回500并保留 started 记录。不提供快照下载。
仅识别实例目录直属的 `pre-restore-<32hex>`、`failed-restore-<32hex>`、`upgrade-<32hex>-snapshot`、`failed-home-<32hex>` 真实目录;不列出或删除 `home`、其它命名或符号链接目录。删除成功返回 `200 {ok:true}`,非法ID/不安全路径400,不存在/所有权不符404,恢复保护409,存储/索引/审计错误500。不支持的方法405。删除仍要求同源 Origin 与 CSRF,不要求备份密码,不调用 Docker inspect/stop/restart。
实例 `error`、未完成升级(仅 `complete` / `rolled_back` 为终态)、恢复日志查询错误时拒绝两类删除。快照列表仍可读取并报告保护原因。所有权在实例锁与用户锁内重新检查。无自动清理;文件系统和数据库无法原子提交,包删除后的索引提交失败会返回500,重试同一删除可清理残留索引。
快照递归删除期间保留实例锁与用户锁,但不持有 SQL 连接或事务。仅明确识别的非法/不安全路径返回400;权限、I/O 等操作系统故障返回500。快照目录不存在时列表返回空数组,删除返回404。
## 管理员孤儿回收
GET /api/orphans 返回可回收的完整容器ID数组。DELETE /api/orphans?id=<id>&confirm=<相同完整id> 二次核验后非强制删除,保留bind数据及网络。仅接受本项目身份/挂载匹配、已停止、无实例或升级引用的容器;普通用户403,确认不匹配400,状态不安全409。
## 其他配置
- LISTEN_ADDR(默认 127.0.0.1:8088)
- DATA_ROOT(默认 ./data)
- HOST_DATA_ROOT(默认等于实际 CONTAINER_DATA_ROOT 的绝对路径,用于 Docker bind source;容器部署必须显式设置宿主绝对路径)
- CONTAINER_DATA_ROOT(默认 DATA_ROOT)
- DOCKER_HOST(默认 unix:///var/run/docker.sock)
- 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/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
- 登录请求体最多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 → 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 → [{id,user_id,action,target,detail,created_at}](管理员;按id倒序最多500条,无分页)
## 镜像与构建
这是 breaking change:`/api/latest`、`/api/compatibility`、`POST /api/builds` 及 `/api/intents`(包括子路径的查询、创建、取消)已移除,不提供 410 兼容入口。`GET /api/builds` 与 `GET /api/builds/{id}` 仅用于构建历史读取。旧构建及旧创建意图不迁移、不恢复执行,不在后台续跑或完成旧预约;不因此删除已有实例或数据。管理员改用 Git 发布接口构建,用户以已验证且准入的精确本地镜像 ID 创建实例。
已认证请求中,`POST /api/builds` 返回 405;`/api/latest`、`/api/compatibility` 和 `/api/intents`(含子路径)返回 404。历史意图记录留在数据库中,但不再提供存储操作、轮询或恢复流程,也不计入配额或作为镜像删除引用。
- GET /api/releases/latest(仅管理员;获取并校验配置的 Git 发布分支,返回 release_id、version、commit、repository、branch、recipe_rev、platforms、compatible、reason,以及可选的 build。检查会下载构建文件,但不执行 Dockerfile;未知部署协议或平台不允许构建)
- POST /api/releases/build {commit}(仅管理员、同源和 CSRF;提交检查时看到的精确 commit,分支已变化返回 409,需重新检查。返回 202 构建任务;不自动升级实例)
- Git 构建任务附带 release_id、source_commit、source_repository 来源信息;发布身份独立于 DSH 版本及部署布局,允许同一 DSH 版本构建新的脚本修订。可信发布通过现有镜像身份和启动验证后可准入,显式禁用仍优先。新构建仅允许管理员通过 Git 发布接口提交;main 不再包含内置 runtime 构建资产。
- GET /api/images(仅返回 Docker 本地实际存在的镜像;每项含 admission、admission_reason、selectable;只有 status=available 且 selectable=true 才可新建/升级。inspect 确认 404 后原子删除镜像索引行并将成功构建记录标为 missing,保留构建历史、显式准入策略和实例;missing 历史不会重建索引。Docker 列表或检查失败时请求失败,不删除索引行)
- POST /api/images/admission {image_id,state,evidence}(管理员、同源和CSRF;state为recommended/compatible/existing_only/blocked,evidence必填。正向准入要求精确镜像身份、成功验证记录、受支持挂载布局与平台;策略与审计持久化。单镜像准入不授权新构建)
- DELETE /api/images?image_id=sha256:...(管理员;保护实例、活动任务、升级及全部Docker容器引用,不force)
- GET /api/builds?limit=20&offset=0(摘要数组,不含日志正文;limit默认20、最大100,offset非负;X-Has-More响应头表示后续是否有记录)
- GET /api/builds/{id}(按需获取单个任务及脱敏日志;仅已登录用户,日志最多64 KiB)
镜像策略: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)。其他平台/版本不继承这些验收结论。当前正向准入仅支持模板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} → 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}
实例列表仅包含当前用户的实例;管理员也不能通过实例接口访问其他用户实例(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 事件结构由运行时决定,面板未定义独立消息协议。上游认证失败不会自动重放写请求。
## 工作区
除 usage 外,所有 /api/ws/ 请求必须带查询参数 instance={id},缺少或空值返回410(共享文件入口已移除);实例不存在或不属本人返回404,管理员无跨用户豁免。文件根为该实例的完整home;允许下载凭据及目录ZIP,不承诺write-only。下载提示仅在界面防误操作,API不按敏感文件名过滤。通用文件修改不强制走专用Profile的停机/YAML校验流程;归属、路径安全、配额、恢复状态和现有文件操作并发保护仍有效。上传不覆盖同名文件(409)。下列简写路径同样必须补上 instance 查询参数。
- 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。
- 返回原始图片字节及 image MIME;固定 `X-Content-Type-Options: nosniff`、`Content-Security-Policy: sandbox; default-src 'none'; style-src 'unsafe-inline'`、`Content-Disposition: attachment`、`Cache-Control: no-store`。SVG 不做内容改写,依靠图片上下文和 CSP 限制脚本及外部资源;直接导航下载附件。
- GET /api/ws/text?instance=...&path=... → {content:string,version:string}
- PUT /api/ws/text?instance=...&path=... JSON {content:string,version:string} → {version:string}
- 仅编辑已有普通文件;沿用工作区认证、实例归属和恢复保护。必须提交 GET 返回的不透明 version;缺少 content/version 返回 400,版本不匹配或保存前文件已删除返回 409,不创建新文件。GET 不存在/不安全路径返回 404;非法相对路径返回 400。
- 内容最多 1 MiB(UTF-8 字节数,含边界),超限 413;无效 UTF-8、二进制/控制字符返回 415(仅允许 tab、LF、CR 控制字符)。JSON 请求上限为 6 MiB + 4096 字节以容纳转义,超限 413。
- Linux openat2 限定访问工作区内路径,拒绝符号链接、跨挂载点和多硬链接文件;不支持 openat2 时安全失败。固定父目录描述符,在同目录写临时文件、保留原文件 mode/UID/GID、同步文件,再原子 rename 替换;不保留扩展属性/ACL。审计仅记录操作和路径,不记录内容。
- 用户锁串行化面板保存;锁内读取当前配额和用量,要求现有用量 + 新内容大小不超配额(包括临时文件开销,否则 413)。因此接近配额时即使缩小文件也可能无法保存。外部进程不受该锁和配额检查约束。
- rename 前立即重新读取目标并核对最新内容/版本,但检查与 rename 不是文件系统 CAS:外部写入者仍可能在两者之间写入或删除目标,不能保证绝不覆盖外部修改。父目录被外部重命名时操作仍针对固定目录。需严格避免丢失修改时,先停止外部写入;不承诺断电后的目录项持久性。
- GET /api/ws/download?path=...:普通文件保持原始流与 Range 支持;目录返回 `application/zip` 附件(目录名.zip),空 path 或 `.` 下载整个工作区(workspace.zip)。ZIP 条目相对所选目录,保留空目录,不额外包裹顶层目录,使用 Store 模式(不压缩),不支持 ZIP Range。
- ZIP 先校验有界文件清单,再逐文件流式输出,不生成临时归档,不持 workspace/global 锁跨网络传输。每次最多 10,000 个文件/目录条目、64 层、10 GiB 文件总大小;进程最多同时 4 个目录下载,超出分别返回 413 / 429;扫描和传输合计限时 2 分钟。
- 路径必须位于工作区内;下载拒绝任意路径分量的符号链接、跨挂载点、特殊文件和多硬链接普通文件。目录含上述条目或含反斜杠/冒号的不安全 ZIP 文件名时整体拒绝(400),不静默漏文件。依赖 Linux openat2,内核不支持时安全失败。
- 下载不是原子快照:流式读取前复核文件身份、大小、类型及修改时间;并发修改、取消或传输错误可能导致连接中断/不完整 ZIP,请停止写入后重试。单文件下载不受 ZIP 数量、大小、并发和时限约束。
## 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}(当前登录密码加密;自动恢复原运行状态)
- POST /api/instances/{id}/backups multipart file,password(导入外部包,password为原包密码;不立即覆盖home)
- GET /api/instances/{id}/backups
- GET /api/instances/{id}/backups?download={backup_id}
- POST /api/instances/{id}/restore {backup_id,password}(原备份密码;旧平台key包可留空)
- POST /api/instances/{id}/upgrade {image_id,password}(当前登录密码,加密pre-upgrade备份)
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 桥接、加密备份恢复和同镜像替换升级已有实现与历史集成记录,具体版本和本次验证范围见 ACCEPTANCE.md,不能据此推断当前全部链路已重新验收。WS/SSE 代理有单元测试,真实完整会话需单独验证。硬配额与跨公网租户域名隔离未实现;已验证的 DSH rc.1/rc.2 启动流程不使用 --public-url。默认仅 loopback 监听,不应直接暴露公网。完整限制见 README.md。