Files
dsh-web/DESIGN.md
T

26 KiB
Raw Blame History

DeepSeek Harness 多租户容器管理平台 — 设计文档

当前发布方式为管理员检查Git发布并确认精确commit,本地构建runtime;release.json、image/与初始化测试仅在dockerfile发布分支维护。旧npm/实验构建和构建预约已移除,详见§5。历史验收不自动适用于当前架构,验证范围见ACCEPTANCE.md,部署操作见OPERATIONS.md。

状态:当前代码具备可信团队管理面板核心流程;历史真实 Docker 验收仅适用于各自记录的版本,不代表当前 Git 发布与全部新增功能已验收。本文“当前实现基线”优先于后面的历史设计;未实现规划不作为已交付能力。验收证据与缺口见 ACCEPTANCE.md。 目标项目:deepseek-ai/deepseek-harness(npm CLI @deepseek-ai/dsh,命令 dsh web) 控制面技术栈:Go + 前端 SPA 部署形态:单机 Docker daemon;面板自身亦运行于容器中 鉴权/配额:内置账号 + 目录配额


当前实现基线(本次源码核对)

本节描述仓库代码,而不是部署状态。本次本地测试与构建结果见 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。
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;完整代理以集成测试为准。

针对本体 deepseek-ai/deepseek-harness(npm CLI @deepseek-ai/dsh,命令 dsh web)确认:

事实 来源 对设计的影响
无官方 Docker 镜像/Dockerfile repo 根、apps/、.gitlab-ci.yml 均无 必须自建镜像包装 dsh web
dsh web 默认监听 127.0.0.1:3080,且启动时拒绝 --host 0.0.0.0(安全原因) packages/bundle/web-app/README.md 容器内必须跑反向代理绑 0.0.0.0 转发到 loopback
端口/主机是 CLI flag(--port/--host/--public-url/--trusted-host/--no-open),无 DSH_PORT/DSH_HOST 环境变量 同上 容器启动参数化靠 CMD args,不靠 env
数据根 = $DSH_HOME(默认 ~/.dsh) docs/user/guide/providers.md、CLI README 每实例独立 DSH_HOME,bind-mount 即可保留数据
多实例硬约束:每个容器必须独立 DSH_HOME + 独立端口(task-board 单写者) architecture.md 一用户两容器 ⇒ 两个 DSH_HOME,不能共享
模型/API Key 存储:密钥在 $DSH_HOME/.credentials.yaml(YAML,上游配置接口的 write-only 语义不限制本面板所有者文件访问),模型/provider 配置在 $DSH_HOME/profiles/<profile>/cordis.patch.yml;cordis.patch.yml 只存 apiKeyEnv 引用 providers.md 备份"模型"= 备份这两个文件
"profile 文件" = profiles/web/cordis.patch.yml(YAML,用户可编辑的 patch 层);profile 目录还有 package.json(含插件依赖 + dsh.profile bundle 清单)+ node_modules(pnpm 装的插件,可重建) apps/cli/README.md 编辑 profile = 编辑 cordis.patch.yml;插件清单在 package.json
升级:用户数据全在 DSH_HOME(npm 安装树之外),升级 @deepseek-ai/dsh 后数据保留;node_modules 由 pnpm 重建 README、CLI README "保留数据 + 重启新版容器"可行
公网部署:--public-url <url/mount/> + --trusted-host;代理必须 剥前缀、保留 Host、把 Path=/ 的 cookie 改写为 mount + 加 Secure docs/user/guide/public-deployments.md 面板反代要处理 cookie/前缀,是最复杂点
启动 URL 带 per-process token,首次访问换签名 cookie 同上 面板需从容器日志取 token,服务端代换 cookie

结论:DSH 不是为容器多租户设计,但靠"自建镜像 + 容器内反代 + 面板反代桥接 token/cookie + 每实例独立 DSH_HOME"可稳态支撑。


1. 目标与范围

已确认的访问与镜像策略

  • 面向可信团队,实例所有者可通过文件管理读取、下载、修改完整 DSH_HOME,包括自己的凭据。面板不承诺凭据 write-only;跨用户隔离、路径安全、配额与恢复状态保护保持不变。下载文件/打包目录会提示泄密风险,提示属于交互防误操作,不是阻止所有者访问的安全边界。

  • 专用 Profile 编辑器提供 YAML 校验与受控停启流程,但通用文件操作、DSH 终端和 agent 不强制经过该流程;修改运行中配置存在不一致风险,建议先停机并备份。

  • 镜像准入分为推荐、仍兼容、仅存量运行、禁止使用。兼容依据绑定精确版本、模板修订、平台与镜像身份;不再仅因历史模板不是当前模板就一律判为不可用,也不能仅因历史构建成功就认定兼容。

  • 新版本仅通过管理员 Git 发布流程构建;实验构建已移除。管理员正向准入须填写验收依据并记录审计,不绕过布局、平台、实际镜像身份与验证记录检查。

  • 通过 docker.sock 在单机上创建/启停/删除多个 dsh web 容器,每用户最多 2 个。

  • 每实例独立完整 home,支持文件上传/下载/删除/重命名、文本编辑和图片预览,按账号统计软配额。

  • 实例 home 挂入自身容器的 /workspace,不与同账号其他实例共享挂载。

  • 备份/还原 dsh 配置(含模型与插件清单)。

  • 编辑 profile 文件(cordis.patch.yml)。

  • 升级:保留数据,用新版本镜像重启容器。

  • 本地镜像管理:管理员检查 Git 发布并确认精确 commit 构建;用户选择经验证且准入的本地镜像创建,不提供构建预约。不推送生成的镜像。

  • 控制面:Go 后端 + SPA 前端;内置账号 + 目录配额;面板自身容器化运行。


2. 总体架构(当前实现)

面板是通过 docker.sock 编排 sibling runtime 的 Go 容器,提供 React 静态资源、API、路径反代和 SQLite 持久化。每个实例独立 bridge,面板接入对应网络,runtime 不发布主机端口。容器内 DSH 监听 loopback:3080,由发布镜像内 Caddy 在8080转发;镜像内部实现须以精确发布 commit 为准。

浏览器 → 面板 SPA/API 或 /dsh// → 实例专属 bridge → runtime:8080 → DSH:3080。

socket-proxy 仍为可选规划,当前 Compose 直接挂 socket,不代表具备宿主级隔离。


3. 宿主路径 ↔ 面板内路径映射(当前实现)

Docker bind.Source 必须使用 daemon 宿主路径。面板文件操作使用 CONTAINER_DATA_ROOT(Compose 为 /data),通过 internal/api/paths.go 的 toHostPath 映射到 HOST_DATA_ROOT。

用途 面板路径 Docker bind.Source / Target
实例完整 home /data/workspaces//instances//home <HOST_DATA_ROOT>/workspaces//instances//home → /workspace
配置备份 /data/backups/// 不挂入 runtime

构建上下文是从锁定 Git commit 获取并提交的归档流,不应把历史内置 image/ 当作宿主 bind 源。


4. 数据与目录布局(当前实现)

<HOST_DATA_ROOT>/
  db.sqlite
  workspaces/<uid>/
    instances/<instId>/home/       # 完整 DSH_HOME → /workspace
      .credentials.yaml
      cordis.patch.yml
      profiles/web/               # 配置、插件清单、可重建依赖
      storages/                   # 实例工作区数据等
    files/                        # 可能遗留;当前无共享入口或新容器挂载
  backups/<uid>/<instId>/          # 加密配置备份;不是完整 home 快照备份

账号软配额统计 workspaces//,并非只统计当前实例。实例删除会清理该实例目录及对应备份;历史共享 files 不自动合并到 home。旧目录结构和旧删除保留策略不能作为现行承诺。


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} 提供历史与日志。

不再从npm查询latest,不接受普通用户实验构建,不打包main内置image/,不提供构建后创建预约。创建实例必须另行选择经验证且准入的本地image ID;管理员构建成功不等于已完成完整兼容验收或自动升级实例。

镜像准入绑定实际身份和成功验证记录;supportedLayout只接受模板7,平台须匹配当前面板平台,内置defaultAdmission规则另有限定linux/amd64。旧模板记录可以保留,不意味着可通过填写说明正向准入。镜像清理保护有效引用,不自动全局prune。构建仍可能需要基础镜像和依赖网络,不能宣称离线构建。


6. 容器生命周期(当前实现)

标准库 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。

当前资源约束为只读 rootfs、drop ALL capabilities、no-new-privileges、2GiB 内存、2 CPU、256 PID 及有界可执行 /tmp。实例支持启停、重启、日志、删除;删除回收容器/网络并清理实例数据与备份。创建不确定时保留恢复依据,不能靠一次或超时后的404清除。孤儿资源仅管理员明确确认并核验后处理,不自动纳管。

7. 网络与访问方式(当前实现)

默认已经选择路径前缀代理,不是待拍板选项。面板将 /dsh// 转发至对应实例网络中的 runtime:8080;归属检查在代理之前。Compose 默认仅向 loopback 发布面板8088,公网需管理员另配 HTTPS 入口与可信代理。子域名、直接发布 runtime 端口不是当前默认能力。

8. 访问与认证(当前实现)

面板校验账号和实例归属,从启动日志取得 token 并在服务端交换/缓存 DSH cookie;token 按容器身份/启动时间加密持久化。反代剥前缀、处理 Host 与转发头,注入服务端 cookie 并移除上游 Set-Cookie,不把 DSH cookie 改写下发给浏览器。客户端持有的是面板认证 cookie。

WS/SSE 由反代支持;会话续期由面板/runtime 注入脚本协调。上游拒绝 cookie 时失效缓存,下次请求重新交换,不重放写请求。已建立 WS/SSE 不承诺即时撤销;真实部署的长会话、插件与公开入口仍需分别验证。

9. 工作区文件管理 + 容量配额

文件 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(有界上传、写前查配额;不是分片续传协议)
  • GET/PUT /api/ws/text(文本读取/带版本冲突检查的保存)、GET /api/ws/image(受控图片读取)
  • GET /api/ws/download?path=(单文件流;目录自动 zip)
  • GET /api/ws/usage(当前用量/配额)

配额实现(容器化面板的约束):

  • 默认:软配额。面板写前遍历统计账号目录用量,写入串行化并限制并发,每五分钟巡检记录超限。容器/agent直接写盘不受面板写锁约束,仍可超过软配额;不通过删用户数据恢复容量。
  • 可选硬配额(二选一,需运维确认):
    • XFS prjquota:面板 --privileged + cap_add:[SYS_ADMIN,CHOWN] + 把宿主 XFS 挂载点 bind 进来,面板内对 /data 跑 xfs_quota/chattr -p。每用户唯一 project ID(uid+1000)。
    • ZFS refquota(若宿主用 ZFS):面板 --privileged + zfsutils,每用户 zfs create pool/users/<uid> && zfs set refquota=10G,再 bind 该 dataset 到 /data/workspaces/<uid>。
    • 或在宿主跑极小特权配额 sidecar(gRPC/HTTP),面板远程调它做 quota 操作——面板自身保持非特权。
  • 配额值存 DB,按用户可配(默认 10GiB)。面板展示用量。

当前仅软配额;上述 XFS/ZFS 和特权 sidecar 是未实现规划,不是可直接启用的配置。


10. 配置备份与还原(含模型与插件)

备份内容(tar.gz,排除可重建/运行态):

包含 说明
.credentials.yaml 模型 API Key(机密)→ 备份包加密
cordis.patch.yml(home 级) home patch
profiles/web/package.json 插件清单(含 dsh.profile bundle 列表)
profiles/web/cordis.patch.yml 模型/provider 配置 + apiKeyEnv 引用
profiles/web/pnpm-lock.yaml(若有) 插件版本锁
skins/ pets/ agent-presets/ .agent-presets/ 用户资源
dsh-ssh.json SSH 凭据(0600,加密入包)
manifest.json dsh 版本、备份 schema 版、时间戳

范围:仅上述六个固定配置文件、四类资源树和生成的manifest;不是完整home备份,通常位于白名单外的工作区、会话和插件node_modules不会入包。资源树按路径递归纳入普通文件,不根据文件内容识别运行态或凭据;不安全路径、符号链接及特殊文件会被拒绝。

备份包加密:用用户密码派生密钥(Argon2id → AES-GCM)加密整包;密钥不落库。下载/上传均加密包。

备份流程:按受控流程停止原本运行的容器,打包白名单并加密落盘,再恢复原运行状态;支持下载。不是任意运行中文件的一致性全量快照。

还原流程:校验并解密包,受控停机、保存home恢复快照,应用配置与协调插件,启动验证后恢复应有运行状态;失败尝试快照回滚,失败恢复可能需人工介入。实际快照命名和恢复记录以config.go及运维文档为准,不提供任意第三方插件兼容保证。


11. profile 文件编辑

  • 编辑对象:profiles/web/cordis.patch.yml(YAML)。
  • 前端用本地 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。

12. 升级流程(保留数据 + 重启新版容器)

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 并重建插件依赖以尽量保留数据;这不保证零丢失或任意插件跨版本兼容。当前升级还依赖备份、快照、健康检查和回滚,故障及兼容性必须实测。


13. 安全与资源

  • 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 不属于加密配置备份,应按机密数据保管。
  • 路径穿越校验、上传大小/类型白名单(可配)。
  • 审计日志:容器操作/备份/还原/升级/profile 改动入库。

14. 技术栈与项目结构(当前实现)

当前 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。

旧设想中的 main/image/、deploy/ 不能用于当前构建或部署:runtime 资产在 dockerfile 发布分支,面板使用根目录 Dockerfile 和 compose.yaml。


15. 数据模型(当前实现概要)

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.yaml、.env.example 和 OPERATIONS.md 为操作入口,不复制历史80/443或共享bridge样例。默认面板绑定127.0.0.1:8088,运行用户1000:1000,附加宿主socket组;只读rootfs、/tmp tmpfs、1GiB内存、2 CPU、512 PID及健康检查。

配置 HOST_DATA_ROOT / CONTAINER_DATA_ROOT、DockerRootDir只读容量探测挂载、PANEL_CONTAINER_ID,以及实际公开URL和可信代理CIDR。裸docker.sock挂载并未被只读rootfs降权,仍需信任面板与账号。当前模板不部署socket-proxy或硬配额sidecar。


17. 关键风险与待验证

  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. Git 发布构建:须验证精确 commit 确认与分支变化、信任源/权限变化、构建去重及恢复、外部删除镜像、容量及网络故障。构建成功不等于完整兼容验收,不存在旧构建预约释放名额流程。

18. 已决策与后续规划

已选择每实例独立bridge、路径前缀反代、服务端cookie桥接、账号软配额、React响应式Web及管理员Git发布本地镜像。旧npm最新构建/预约、共享工作区和Flutter发布不是当前路线。

可选后续增强:XFS/ZFS硬配额或宿主配额sidecar、socket-proxy、独立域名隔离。上述未纳入当前默认交付;是否实施需另行设计、授权和验收,不能以文档列出视为支持。

19. 里程碑与交付判定

M1镜像、M2账号/生命周期、M3代理、M4文件/软配额、M5配置备份、M6升级回滚、M7界面/管理安全保留为能力分组,不再以旧npm构建及预约作为当前M2标准。当前Git发布、实例home独占、删除清理、文本编辑/图片预览与主题等变更须按ACCEPTANCE.md当前矩阵分别补证据。

历史M1–M7结案不等于当前commit或真实部署整体通过;本地测试通过、真实Docker通过、浏览器通过与公网验收必须分开记录。


附:调研依据(关键 URL)