diff --git a/docs/reference/provider-gateway.md b/docs/reference/provider-gateway.md index 51f08b1d..e562c5c8 100644 --- a/docs/reference/provider-gateway.md +++ b/docs/reference/provider-gateway.md @@ -18,9 +18,9 @@ Provider Gateway 是计算节点侧容器。它只主动连出到主 server 暴 ## Deployment Method -当前主 server 公网 IP 是 `74.48.78.17`,`config.json` 中的 `network.publicHost` 必须保持为该地址;公网 frontend 入口是 `http://74.48.78.17:18081/`,provider gateway 对外接入入口是 `ws://74.48.78.17:18082/ws/provider`,provider ingress 健康检查是 `http://74.48.78.17:18082/health`。主 server 本机 provider 由根目录 `docker-compose.yml` 的 `provider-gateway` 服务启动,容器内使用 Docker 内网地址 `ws://backend-core:8081/ws/provider` 自接入;外部计算节点部署 provider-gateway 时必须改用公网 provider ingress URL。 +当前主 server 公网 IP 是 `152.53.229.148`,`config.json` 中的 `network.publicHost` 必须保持为该地址;公网 frontend 入口是 `http://152.53.229.148:18081/`,provider gateway 对外接入入口是 `ws://152.53.229.148:18082/ws/provider`,provider ingress 健康检查是 `http://152.53.229.148:18082/health`。主 server 本机 provider 由根目录 `docker-compose.yml` 的 `provider-gateway` 服务启动,容器内使用 Docker 内网地址 `ws://backend-core:8081/ws/provider` 自接入;外部计算节点部署 provider-gateway 时必须改用公网 provider ingress URL。 -新增计算节点推荐使用两项配置的简化挂载流程:在目标节点的 UniDesk 仓库根目录运行 `bun scripts/cli.ts provider attach --master-server http://74.48.78.17/ --up`;如果主 server 仍是默认地址,`--master-server` 可省略。该命令生成 `.state/provider-.env` 和 `provider-.yml`,env 文件默认只保留 `UNIDESK_MASTER_SERVER` 与 `PROVIDER_ID` 两项,Compose 固定 `restart: always`、`pid: "host"`、Docker socket、只读 `/workspace` 仓库挂载、日志目录、`/run/host-ssh` 维护私钥目录和 `127.0.0.1:18789->18789/tcp` loopback egress proxy 映射。provider-gateway 会从 `UNIDESK_MASTER_SERVER=http://74.48.78.17/` 自动派生 `ws://74.48.78.17:18082/ws/provider`,并自动补齐 `PROVIDER_NAME`、默认 labels、心跳/重连参数、`DOCKER_SOCKET_PATH`、`MONITOR_DISK_PATH`、`PROVIDER_UPGRADE_*`、runner image 和日志路径;远程升级所需的宿主仓库路径会优先通过 Docker inspect 反查当前容器 `/workspace` 挂载源,避免手写 `/home/ubuntu/unidesk`、Compose project、env file 或 runner image 时出错。显式环境变量仍可覆盖这些默认值;如果主 server 的 provider token 已改成非默认值,挂载时只额外传一次 `--provider-token ` 或在 env 文件中加入 `PROVIDER_TOKEN`。 +新增计算节点推荐使用两项输入的简化挂载流程:在目标节点的 UniDesk 仓库根目录运行 `bun scripts/cli.ts provider attach --master-server http://152.53.229.148/ --up`;如果主 server 仍是默认地址,`--master-server` 可省略。该命令生成 `.state/provider-.env` 和 `provider-.yml`,env 文件记录 master 地址、Provider 身份、升级坐标和 Host SSH 目标,Compose 固定 `restart: always`、`pid: "host"`、Docker socket、只读 `/workspace` 仓库挂载、日志目录、`/run/host-ssh` 维护私钥目录和 `127.0.0.1:18789->18789/tcp` loopback egress proxy 映射。provider-gateway 会从 `UNIDESK_MASTER_SERVER=http://152.53.229.148/` 自动派生 `ws://152.53.229.148:18082/ws/provider`,并自动补齐默认 labels、心跳/重连参数、`DOCKER_SOCKET_PATH`、`MONITOR_DISK_PATH` 和日志路径;远程升级所需的宿主仓库路径会优先通过 Docker inspect 反查当前容器 `/workspace` 挂载源,避免手写 `/home/ubuntu/unidesk`、Compose project、env file 或 runner image 时出错。显式环境变量仍可覆盖这些默认值;如果主 server 的 provider token 已改成非默认值,挂载时只额外传一次 `--provider-token ` 或在 env 文件中加入 `PROVIDER_TOKEN`。 手写 Compose 仍必须满足同一部署约束:挂载 `/var/run/docker.sock:/var/run/docker.sock` 作为 Docker 状态采集、任务执行和远程升级的唯一自动化通道,并让 provider-gateway 容器运行在宿主 PID namespace;Compose 写法是 `pid: "host"`,`docker run` 写法是 `--pid host`。缺少该配置时只能看到 provider 容器命名空间内的进程,不能视为完整节点资源监控。provider-gateway 容器必须使用 Docker restart policy `always`;`unless-stopped`、空 restart policy、手动 `docker stop` 后期待 Docker daemon 重启自动拉起,都不符合长期接入要求。长期接入节点必须保留只读 `/workspace` 仓库挂载,使 `provider.upgrade mode=schedule` 能构建候选 gateway 且只重建 `provider-gateway` service,不影响 database、backend-core、frontend 或业务用户服务。provider-gateway 部署必须同时交付 Host SSH / WSL SSH 透传维护桥;WSL 节点默认把私钥目录挂载到 `/run/host-ssh` 后,gateway 会在发现 `/run/host-ssh/id_ed25519` 时自动使用 `host.docker.internal:22`、从仓库路径推断 WSL 用户与默认工作目录,必要时仍可用 `HOST_SSH_HOST`、`HOST_SSH_PORT`、`HOST_SSH_USER`、`HOST_SSH_KEY`、`HOST_REMOTE_CWD` 显式覆盖。 @@ -28,6 +28,8 @@ Provider Gateway 是计算节点侧容器。它只主动连出到主 server 暴 新增 provider-gateway 或 legacy bootstrap 后,Docker socket、远程升级和 SSH 透传必须作为同一个部署包验收,不能只让 provider 在线就认为完成。节点侧应先启动目标宿主或 WSL 的 sshd,把维护公钥写入目标用户 `authorized_keys`,再用只读目录挂载私钥到 provider-gateway 容器内 `/run/host-ssh`;provider-gateway 环境变量必须包含 `HOST_SSH_HOST`、`HOST_SSH_PORT`、`HOST_SSH_USER`、`HOST_SSH_KEY` 和 `HOST_REMOTE_CWD`。注册成功后,主 server 看到的 labels 必须同时满足 `hostSshConfigured=true`、`hostSshKeyPresent=true`、`hostSshTarget` 指向目标 sshd,且 `unideskCapabilities` 包含 `host.ssh`。 +`provider attach --up` 必须把容器内私钥 presence 和真实 SSH probe 纳入 `containerValidation.ok`;只有 `hostSshKeyPresent=true` 且 `hostSshProbeOk=true` 才能接受部署。Docker Desktop 使用 WSL 私钥目录时,必须从拥有该目录的 WSL distro 发起 Compose;从 Windows Docker CLI 直接挂载 `/home//...` 可能落到 Docker Desktop VM 的空目录,容器仍会 running,但维护桥实际不可用。 + 使用初始密码把新增外部节点引导成密钥登录时,本机缺少 `sshpass` 应先安装 `sshpass`,再用受控脚本写入维护公钥;不要长期保留交互式密码登录流程,也不要把密码写入命令输出、文档、Compose env 或仓库文件。 计算节点镜像必须包含 `ssh` 客户端,否则 provider 虽然可以在线并执行 Docker 任务,但 `host.ssh` 调度会在节点侧失败。标准 `src/components/provider-gateway/Dockerfile` 已安装 `openssh-client`;如果节点使用本地兜底镜像或复制 Bun/Docker CLI 的自定义镜像,也必须同时安装或复制可用的 OpenSSH client,并在容器内通过 `ssh -V`、`test -r /run/host-ssh/id_ed25519`、`ssh -i /run/host-ssh/id_ed25519 ... hostname` 做本地烟测后再接入主 server。 @@ -54,7 +56,7 @@ TCP pool 的长期方向是把 `trans`/`tran` 变成真正并发的短连接工 ## WSL Compute Node Deployment -WSL 计算节点和普通外部计算节点使用同一套 provider ingress 协议:节点不暴露入站端口,只需要从 WSL 主动连出到 `ws://74.48.78.17:18082/ws/provider`。已验证的 WSL 节点命名口径是短设备编号,例如 `PROVIDER_ID=D601` 与 `PROVIDER_NAME=D601` 保持一致;新增 WSL 节点必须替换为唯一 ID,避免把旧节点状态覆盖到同一条 `unidesk_nodes.provider_id` 记录。 +WSL 计算节点和普通外部计算节点使用同一套 provider ingress 协议:节点不暴露入站端口,只需要从 WSL 主动连出到 `ws://152.53.229.148:18082/ws/provider`。已验证的 WSL 节点命名口径是短设备编号,例如 `PROVIDER_ID=D601` 与 `PROVIDER_NAME=D601` 保持一致;新增 WSL 节点必须替换为唯一 ID,避免把旧节点状态覆盖到同一条 `unidesk_nodes.provider_id` 记录。 WSL 节点应优先使用 WSL 内部原生 Docker Engine 和 `/var/run/docker.sock`,让资源采样、Docker 状态和任务执行都反映 WSL 计算环境本身,而不是 Windows 或 Docker Desktop 的代理上下文。如果当前 Docker CLI 实际连接 Docker Desktop daemon,也可以先作为可用计算节点接入,但必须在 `PROVIDER_LABELS_JSON` 中显式标注 `dockerContext=docker-desktop` 或等价标签,避免在 frontend 中把 Docker Desktop 资源误判为 WSL 原生资源。UniDesk 仓库建议放在 WSL 原生文件系统,例如 `/home/ubuntu/unidesk`,再按需从 Windows 工作区同步源码;长期运行和升级挂载应使用 WSL 原生路径只读挂到容器内 `/workspace`,减少 `/mnt/c` 权限、性能和路径转换问题。 @@ -74,19 +76,21 @@ WSL 节点出网异常时,先把代理设置固化到目标 WSL 用户的 `~/. Windows 代理程序常见默认只监听 `127.0.0.1:7890`,此时 WSL 访问 `:7890` 会失败。可选修复是让代理程序监听 WSL 可达网卡,或在 Windows 用户态启动一个幂等 TCP relay,把 `:7890` 转发到 `127.0.0.1:7890`;`netsh interface portproxy` 通常需要管理员权限,不应作为普通节点 bootstrap 的唯一方案。`.bashrc` 可以在发现 `:7890` 不可达时调用 Windows 侧脚本启动该 relay,但 relay 脚本、临时 apt source、日志和节点私钥都属于本地运行态,必须留在 `.state` 或用户目录,不能提交到仓库。 -apt 或镜像构建遇到 `archive.ubuntu.com` 超时、代理 `503` 或 registry metadata 拉取失败时,优先使用节点侧临时镜像源或临时 apt sources 完成 bootstrap,例如把 Ubuntu 源临时替换到可达镜像后安装 `openssh-server`。这类网络兜底只解决节点初始化,不改变 `PROVIDER_SERVER_URL`、provider token、Docker socket、Host SSH 透传和 remote CLI 验收标准。 +apt 或镜像构建遇到 `archive.ubuntu.com` 超时、代理 `503` 或 registry metadata 拉取失败时,优先使用节点侧临时镜像源或临时 apt sources 完成 bootstrap,例如把 Ubuntu 源临时替换到可达镜像后安装 `openssh-server`。这类网络兜底只解决节点初始化,不改变 `UNIDESK_MASTER_SERVER`、provider token、Docker socket、Host SSH 透传和 remote CLI 验收标准。 + +Docker build 的 `HTTP_PROXY` / `HTTPS_PROXY` build args 只覆盖 Dockerfile 中的 `RUN` 步骤,不能保证 BuildKit 拉取 `FROM` metadata 时使用代理。若 `auth.docker.io` metadata 超时,应先让 Docker daemon 使用受控代理,或通过 `host.docker.internal:7890` 和可信 image transport 预热精确 base image,再执行标准 Dockerfile 构建;不得把旧 provider 最终镜像冒充 base image。预热完成后仍要用 `--no-cache` 验证 `apk` 与 `bun install` 经代理成功,并由最终 Compose heartbeat 和 Host SSH 原入口验收收口。 ## WSL Image Build Fallback -标准镜像构建路径使用 `src/components/provider-gateway/Dockerfile`,基础镜像为 `oven/bun:1-alpine`,并在镜像内安装 Bun、Docker CLI、Compose plugin、`df` 和 provider-gateway 源码。WSL 或 Docker Desktop 环境中的 registry mirror、DNS 或代理配置可能导致 `oven/bun` 元数据拉取失败;此时不要修改 provider ingress 协议或服务端配置,应改用节点侧镜像交付兜底。 +标准镜像构建路径使用 `src/components/provider-gateway/Dockerfile` 及其中声明的精确 `oven/bun` 基础镜像,并在镜像内安装 Docker CLI、Compose plugin、OpenSSH client 和 provider-gateway 源码。WSL 或 Docker Desktop 环境中的 registry mirror、DNS 或代理配置可能导致 `oven/bun` 元数据拉取失败;此时不要修改 provider ingress 协议或服务端配置,应改用节点侧镜像交付兜底。 -可复用的兜底方式是使用本机已存在的 Debian/Node 基础镜像,复制 WSL 本地 `bun`、`docker` 和 `docker-compose` plugin 到镜像内,再复制 `src/components/provider-gateway/src` 与 `src/components/shared/src`。该镜像必须能在容器内执行 `bun --version`、`docker version`、`docker compose version`、`ssh -V`,并通过挂载的 `/var/run/docker.sock` 执行 `docker info` 和 `docker ps`;只有这些命令通过后才能作为 `unidesk_provider-gateway:` 运行。该兜底镜像只改变节点侧交付方式,不改变 `PROVIDER_SERVER_URL`、token、heartbeat、labels、Docker socket、SSH 私钥只读挂载和服务端验收标准。 +可复用的兜底方式是使用本机已存在的 Debian/Node 基础镜像,复制 WSL 本地 `bun`、`docker` 和 `docker-compose` plugin 到镜像内,再复制 `src/components/provider-gateway/src` 与 `src/components/shared/src`。该镜像必须能在容器内执行 `bun --version`、`docker version`、`docker compose version`、`ssh -V`,并通过挂载的 `/var/run/docker.sock` 执行 `docker info` 和 `docker ps`;只有这些命令通过后才能作为 `unidesk_provider-gateway:` 运行。该兜底镜像只改变节点侧交付方式,不改变 `UNIDESK_MASTER_SERVER`、token、heartbeat、labels、Docker socket、SSH 私钥只读挂载和服务端验收标准。 如果兜底镜像还要承载 WSL SSH 透传,必须同时复制 `/usr/bin/ssh`、`/etc/ssh/ssh_config`、`/etc/ssh/ssh_config.d` 以及 `ldd /usr/bin/ssh` 输出的全部动态库,尤其不能漏掉 `/lib64/ld-linux-x86-64.so.2` 这类 ELF loader。只在 rootfs 中看到 `/usr/bin/ssh` 文件不代表可执行;验收必须运行 `docker run --rm /bin/sh -lc '/usr/bin/ssh -V && /usr/bin/docker compose version'`,再以实际 provider 容器执行 `ssh -T -i /run/host-ssh/id_ed25519 -p 22 ubuntu@host.docker.internal hostname`。若 Compose 使用 Docker bridge 网络,WSL 节点必须在 compose 文件中加入 `extra_hosts: ["host.docker.internal:host-gateway"]`,否则容器内可能无法把 `host.docker.internal` 解析到 WSL 宿主。 ## Deployment Verification -provider-gateway 部署是否成功必须以 UniDesk frontend 中可见的 Provider 信息为准,不能只看节点容器 `running`。验证时访问 `http://74.48.78.17:18081/`,使用配置中的账号密码登录,进入 `资源节点 / 节点清单`,确认目标 `PROVIDER_ID`、`PROVIDER_NAME`、`online` 状态、`lastHeartbeat` 和 labels 可见;点击该节点的 `查看原始JSON`,确认 raw payload 中的 `providerId`、`name`、`status`、`labels` 与部署环境变量一致。随后进入 `资源节点 / 资源监控`,确认该 Provider 有 CPU、实际内存和硬盘采样曲线;进入 `资源节点 / Docker 状态`,确认 Docker daemon、containers、images、volumes、networks 已渲染出来,且 `dockerSocketPresent` 或 Docker ready 状态与预期一致。只有这些前端信息都能通过 UniDesk 正常读取,才说明 provider-gateway 已经真正挂载到主 server。 +provider-gateway 部署是否成功必须以 UniDesk frontend 中可见的 Provider 信息为准,不能只看节点容器 `running`。验证时访问 `http://152.53.229.148:18081/`,使用配置中的账号密码登录,进入 `资源节点 / 节点清单`,确认目标 `PROVIDER_ID`、`PROVIDER_NAME`、`online` 状态、`lastHeartbeat` 和 labels 可见;点击该节点的 `查看原始JSON`,确认 raw payload 中的 `providerId`、`name`、`status`、`labels` 与部署环境变量一致。随后进入 `资源节点 / 资源监控`,确认该 Provider 有 CPU、实际内存和硬盘采样曲线;进入 `资源节点 / Docker 状态`,确认 Docker daemon、containers、images、volumes、networks 已渲染出来,且 `dockerSocketPresent` 或 Docker ready 状态与预期一致。只有这些前端信息都能通过 UniDesk 正常读取,才说明 provider-gateway 已经真正挂载到主 server。 WSL 节点还应补充一次真实调度验证:向该 `PROVIDER_ID` 下发 `docker.ps`,任务必须从 `dispatched` 进入 `succeeded`,并在结果中看到 WSL Docker daemon 返回的容器列表;对于容器化运行的 provider-gateway,列表中通常应包含 `unidesk-provider-gateway-`。这一步可以同时证明 provider WebSocket、服务端任务路由、节点侧 Docker socket 和结果回传链路都已贯通。 @@ -170,7 +174,7 @@ D601 这类长期 WSL provider 不得因为单一路径失败被直接写成全 ## Manual Upgrade Maintenance -手动升级只用于把旧节点 bootstrap 到支持 always-enabled 远程升级的版本;bootstrap 完成后,常规重建/升级必须回到 `provider.upgrade mode=schedule`,不得再用 SSH 透传同步重建 `provider-gateway`。节点侧维护步骤是:进入节点本地 UniDesk 仓库,确认 GitHub 访问走本机 provider-gateway WS egress proxy,例如 `git config --local http.proxy http://127.0.0.1:18789` 和 `git config --local https.proxy http://127.0.0.1:18789` 后再执行 `git pull --ff-only` 获取主 server 已推送版本;不得让 provider 侧 Git 拉取退回直连公网、SSH SOCKS 或公开 master proxy。随后确认 `.state/provider-.env` 中存在 `PROVIDER_SERVER_URL=ws://74.48.78.17:18082/ws/provider`、`PROVIDER_ID=`、`PROVIDER_NAME=`、`PROVIDER_TOKEN`、`PROVIDER_LABELS_JSON`、`PROVIDER_UPGRADE_HOST_PROJECT_ROOT=/home/ubuntu/unidesk`、`PROVIDER_UPGRADE_WORKSPACE_PATH=/workspace`、`PROVIDER_UPGRADE_COMPOSE_FILE`、`PROVIDER_UPGRADE_ENV_FILE`、`PROVIDER_UPGRADE_COMPOSE_PROJECT`、`PROVIDER_UPGRADE_SERVICE=provider-gateway`、`PROVIDER_UPGRADE_RUNNER_IMAGE=unidesk_provider-gateway:`、`DOCKER_SOCKET_PATH=/var/run/docker.sock`、`MONITOR_DISK_PATH=/`、心跳和重连参数。旧 env 文件中如果还残留 `PROVIDER_UPGRADE_ENABLED`,新版 provider-gateway 会忽略它;长期文档和新部署不得再依赖这个键。 +手动升级只用于把旧节点 bootstrap 到支持 always-enabled 远程升级的版本;bootstrap 完成后,常规重建/升级必须回到 `provider.upgrade mode=schedule`,不得再用 SSH 透传同步重建 `provider-gateway`。节点侧维护步骤是:进入节点本地 UniDesk 仓库,确认 GitHub 访问走本机 provider-gateway WS egress proxy,例如 `git config --local http.proxy http://127.0.0.1:18789` 和 `git config --local https.proxy http://127.0.0.1:18789` 后再执行 `git pull --ff-only` 获取主 server 已推送版本;不得让 provider 侧 Git 拉取退回直连公网、SSH SOCKS 或公开 master proxy。随后确认 `.state/provider-.env` 中存在 `UNIDESK_MASTER_SERVER=http://152.53.229.148/`、`PROVIDER_ID=`、`PROVIDER_NAME=`、`PROVIDER_TOKEN`、`PROVIDER_LABELS_JSON`、`PROVIDER_UPGRADE_HOST_PROJECT_ROOT=/home/ubuntu/unidesk`、`PROVIDER_UPGRADE_WORKSPACE_PATH=/workspace`、`PROVIDER_UPGRADE_COMPOSE_FILE`、`PROVIDER_UPGRADE_ENV_FILE`、`PROVIDER_UPGRADE_COMPOSE_PROJECT`、`PROVIDER_UPGRADE_SERVICE=provider-gateway`、`PROVIDER_UPGRADE_RUNNER_IMAGE=unidesk_provider-gateway:`、`DOCKER_SOCKET_PATH=/var/run/docker.sock`、`MONITOR_DISK_PATH=/`、心跳和重连参数。旧 env 文件中如果还残留 `PROVIDER_UPGRADE_ENABLED`,新版 provider-gateway 会忽略它;长期文档和新部署不得再依赖这个键。 如果节点固定 UniDesk 仓库存在并行未提交修改或落后太多,不能为了 provider-gateway 恢复而 stash、reset 或把脏工作区作为构建真相。应在同一节点的固定仓库下从最新 remote 创建独立 clean runtime worktree,例如 `/home/ubuntu/unidesk/.worktree/provider-gateway--runtime`,把节点本地 `.state/provider-.env`、`provider-.yml` 和必要的本地 Dockerfile/Compose 运行态指向该 worktree,并把 `PROVIDER_UPGRADE_HOST_PROJECT_ROOT` 改为该 clean worktree。bootstrap 成功后仍必须立刻执行标准 `provider.upgrade mode=schedule` 和 Host SSH/`trans` 原入口验收;clean runtime worktree 是恢复固定 repo 隔离性的手段,不是新的长期 source truth。 @@ -180,7 +184,7 @@ D601 这类长期 WSL provider 不得因为单一路径失败被直接写成全 如果节点已有专用 Compose,优先用节点本地 Compose 手动重建一次:`docker compose --env-file .state/provider-.env -f -p up -d --no-deps --build --force-recreate provider-gateway`。这条命令必须在节点本地终端、节点自有 Web terminal、系统计划任务或 detached shell 中执行;不得通过正在被重建的 UniDesk provider-gateway 自己提供的 SSH 透传同步执行,否则旧 provider 容器停止时会切断 SSH client,可能导致重建中断在旧容器已停、新容器未起的状态。若只能通过 UniDesk 触达该节点,必须使用 `provider.upgrade mode=schedule` 的 detached updater,或先用节点本地 `nohup`/systemd 启动一个不依赖当前 provider 容器生命周期的重建脚本。老版 `docker-compose` 可能在重建已存在容器时因为 `ContainerConfig` 兼容问题失败;此时只能移除目标 provider-gateway 容器后重新 `up -d --no-deps provider-gateway`,不得执行 `down -v`、`docker volume rm` 或任何会影响 database 命名卷的命令。如果节点当前只有 `docker run` 部署,则先构建镜像 `docker build -f src/components/provider-gateway/Dockerfile -t unidesk_provider-gateway: .`,再以固定容器名重建:使用 `--restart always --pid host`,挂载 `/var/run/docker.sock:/var/run/docker.sock`、`/home/ubuntu/unidesk:/workspace:ro`、节点日志目录到 `/var/log/unidesk`,如需 WSL SSH 维护桥还要把只读私钥目录挂载到 `/run/host-ssh`,并使用同一个 `.state/provider-.env` 启动。无论 Compose 还是 `docker run`,容器名和镜像 tag 都必须带 Provider ID,便于 Docker 状态页、进程资源表、任务历史和节点本地排障互相对应。 -手动升级完成后的判定标准固定为主 server 可观测结果,而不是节点容器 `running`:访问公网 frontend `http://74.48.78.17:18081/`,确认该 Provider 在线;随后在任意装有本仓库且 `config.json` 含正确 frontend 登录凭据的计算节点上执行 `bun scripts/cli.ts --main-server-ip 74.48.78.17 debug dispatch provider.upgrade --mode schedule --wait-ms 300000`,确认任务 `succeeded` 且 result 包含最终 Compose 容器 `containerId`、目标 gateway `version`、`restartPolicy=always`、`pidMode=host` 和新的 `heartbeatTimestamp`;最后再次查看 frontend 或执行 `bun scripts/cli.ts --main-server-ip 74.48.78.17 debug health`,确认节点重连、指标恢复、labels 中 `host.ssh` 能力存在。每个 provider-gateway 手动升级后都必须用 remote CLI 再执行 `bun scripts/cli.ts --main-server-ip 74.48.78.17 debug dispatch host.ssh --wait-ms 15000` 和 `bun scripts/cli.ts --main-server-ip 74.48.78.17 ssh hostname`,验证维护桥没有在升级后丢失;该 remote CLI 默认走公网 frontend,不需要指定 `--main-server-key`。 +手动升级完成后的判定标准固定为主 server 可观测结果,而不是节点容器 `running`:访问公网 frontend `http://152.53.229.148:18081/`,确认该 Provider 在线;随后在任意装有本仓库且 `config.json` 含正确 frontend 登录凭据的计算节点上执行 `bun scripts/cli.ts --main-server-ip 152.53.229.148 debug dispatch provider.upgrade --mode schedule --wait-ms 300000`,确认任务 `succeeded` 且 result 包含最终 Compose 容器 `containerId`、目标 gateway `version`、`restartPolicy=always`、`pidMode=host` 和新的 `heartbeatTimestamp`;最后再次查看 frontend 或执行 `bun scripts/cli.ts --main-server-ip 152.53.229.148 debug health`,确认节点重连、指标恢复、labels 中 `host.ssh` 能力存在。每个 provider-gateway 手动升级后都必须用 remote CLI 再执行 `bun scripts/cli.ts --main-server-ip 152.53.229.148 debug dispatch host.ssh --wait-ms 15000` 和 `bun scripts/cli.ts --main-server-ip 152.53.229.148 ssh hostname`,验证维护桥没有在升级后丢失;该 remote CLI 默认走公网 frontend,不需要指定 `--main-server-key`。 ## Host SSH Maintenance Bridge @@ -196,4 +200,4 @@ Host SSH 的 cwd 语义必须区分默认目录和显式 workspace。未传 cwd 面向人的终端入口是 `trans [ssh-like args...]`。无后续参数时打开远端登录 shell,有后续参数时执行远端命令并返回远端 exit code;该入口的 client 侧仍连接 backend-core 内网 `/ws/ssh` broker,core 只用 provider WebSocket 下发 open/dispatch 控制消息,终端 stdin/stdout/stderr 数据面必须走 provider 主动连接 main server 的 `host.ssh.tcp-pool` TCP warm pool,不新增计算节点入站要求,也不保留旧 WebSocket 数据 fallback。传统 ssh 传输参数由 provider-gateway 环境变量统一控制,CLI 只负责把 Provider ID 后的远端命令和终端 stdin/stdout/stderr 透传过去。非交互单进程远端命令优先使用 argv 入口:`trans D601 argv true`;需要 shell 特性时在 operation 位置显式写 `sh` 或 `bash`,例如 `trans D601 sh -- ''` 或 `trans D601 bash -- ''`。WSL 节点需要同时看清 Linux/WSL 与 Windows 两套 skill 时,使用 `trans skills`,该命令只通过已建立的维护桥读取 `SKILL.md` 元数据,不要求 provider-gateway 新增业务 API。 -验证 WSL SSH 桥时,先在目标 WSL 中启动 sshd 并确保维护公钥写入目标用户的 `authorized_keys`,再确认目标 provider 注册 labels 中 `unideskCapabilities` 包含 `host.ssh`。运行 `bun scripts/cli.ts debug dispatch host.ssh --wait-ms 15000` 后,结果应在 `debug task latest` 或前端任务历史中显示 `status: succeeded`、`probeLine` 含 `UNIDESK_SSH_TEST`、`exitCode: 0`,并且目标节点 labels 中 `hostSshKeyPresent` 为 true;随后运行 `trans argv true` 验证非交互 argv 维护命令,再运行 `trans hostname` 验证近似原生 ssh 的远端命令体验。在计算节点本机自测时,使用 remote CLI 透传同一组命令:`bun scripts/cli.ts --main-server-ip 74.48.78.17 debug health`、`bun scripts/cli.ts --main-server-ip 74.48.78.17 debug dispatch host.ssh --wait-ms 15000`、`bun scripts/cli.ts --main-server-ip 74.48.78.17 ssh argv true` 和 `bun scripts/cli.ts --main-server-ip 74.48.78.17 ssh hostname`;默认 remote CLI 走公网 frontend 登录态,不需要主 server SSH key。健康检查必须能看到该 Provider 在线、`hostSshConfigured=true`、`hostSshKeyPresent=true`、`hostSshTarget` 正确、`unideskCapabilities` 包含 `host.ssh`,probe 必须返回 `UNIDESK_SSH_TEST`,`ssh argv true` 与 `ssh hostname` 必须 exit code 为 0。如果 D518 这类 WSL 节点没有公网 SSH 入口,也必须通过这个 provider-gateway 自连维护桥完成验证,而不是要求主 server 直接连节点公网 22 端口;旧版 provider 未声明 `host.ssh` 时必须先升级 provider-gateway,否则 core 会拒绝 SSH 透传。 +验证 WSL SSH 桥时,先在目标 WSL 中启动 sshd 并确保维护公钥写入目标用户的 `authorized_keys`,再确认目标 provider 注册 labels 中 `unideskCapabilities` 包含 `host.ssh`。运行 `bun scripts/cli.ts debug dispatch host.ssh --wait-ms 15000` 后,结果应在 `debug task latest` 或前端任务历史中显示 `status: succeeded`、`probeLine` 含 `UNIDESK_SSH_TEST`、`exitCode: 0`,并且目标节点 labels 中 `hostSshKeyPresent` 为 true;随后运行 `trans argv true` 验证非交互 argv 维护命令,再运行 `trans hostname` 验证近似原生 ssh 的远端命令体验。在计算节点本机自测时,使用 remote CLI 透传同一组命令:`bun scripts/cli.ts --main-server-ip 152.53.229.148 debug health`、`bun scripts/cli.ts --main-server-ip 152.53.229.148 debug dispatch host.ssh --wait-ms 15000`、`bun scripts/cli.ts --main-server-ip 152.53.229.148 ssh argv true` 和 `bun scripts/cli.ts --main-server-ip 152.53.229.148 ssh hostname`;默认 remote CLI 走公网 frontend 登录态,不需要主 server SSH key。健康检查必须能看到该 Provider 在线、`hostSshConfigured=true`、`hostSshKeyPresent=true`、`hostSshTarget` 正确、`unideskCapabilities` 包含 `host.ssh`,probe 必须返回 `UNIDESK_SSH_TEST`,`ssh argv true` 与 `ssh hostname` 必须 exit code 为 0。如果 D518 这类 WSL 节点没有公网 SSH 入口,也必须通过这个 provider-gateway 自连维护桥完成验证,而不是要求主 server 直接连节点公网 22 端口;旧版 provider 未声明 `host.ssh` 时必须先升级 provider-gateway,否则 core 会拒绝 SSH 透传。 diff --git a/scripts/cli.ts b/scripts/cli.ts index e96567ea..eab3ef26 100644 --- a/scripts/cli.ts +++ b/scripts/cli.ts @@ -631,7 +631,10 @@ async function main(): Promise { } if (top === "provider") { - emitJson(commandName, await runProviderCommand(config, args.slice(1))); + const result = await runProviderCommand(config, args.slice(1)); + const ok = resultOk(result); + emitJson(commandName, result, ok); + if (!ok) process.exitCode = 1; return; } diff --git a/scripts/src/provider-attach.ts b/scripts/src/provider-attach.ts index fb0689ae..d2608169 100644 --- a/scripts/src/provider-attach.ts +++ b/scripts/src/provider-attach.ts @@ -27,6 +27,16 @@ interface ProviderAttachContainerValidation { command: string[]; exitCode: number | null; stderr: string; + hostSshKeyPath: string; + hostSshKeyPresent: boolean; + hostSshKeyCommand: string[]; + hostSshKeyExitCode: number | null; + hostSshTarget: string; + hostSshProbeOk: boolean; + hostSshProbeCommand: string[]; + hostSshProbeExitCode: number | null; + hostSshProbeStdout: string; + hostSshProbeStderr: string; } function optionValue(args: string[], name: string): string | null { @@ -178,6 +188,8 @@ function writeGeneratedFile(path: string, content: string, force: boolean): "cre function inspectAttachedContainer(options: ProviderAttachOptions): ProviderAttachContainerValidation { const containerName = `unidesk-provider-gateway-${safeProviderSlug(options.providerId)}`; + const hostSshKeyPath = "/run/host-ssh/id_ed25519"; + const hostSshTarget = `${defaultHostSshUser(repoRoot)}@host.docker.internal`; const command = [ "docker", "inspect", @@ -189,8 +201,35 @@ function inspectAttachedContainer(options: ProviderAttachOptions): ProviderAttac const [restartPolicy = "", pidMode = "", state = ""] = result.stdout.trim().split("\t"); const restartPolicyOk = restartPolicy === "always"; const pidModeOk = pidMode === "host"; + const containerReady = result.exitCode === 0 && restartPolicyOk && pidModeOk && state === "running"; + const hostSshKeyCommand = ["docker", "exec", containerName, "test", "-s", hostSshKeyPath]; + const hostSshKeyResult = containerReady + ? runCommand(hostSshKeyCommand, repoRoot, { timeoutMs: 10_000 }) + : null; + const hostSshKeyPresent = hostSshKeyResult?.exitCode === 0; + const hostSshProbeCommand = [ + "docker", + "exec", + containerName, + "ssh", + "-T", + "-i", + hostSshKeyPath, + "-o", + "BatchMode=yes", + "-o", + "StrictHostKeyChecking=accept-new", + "-o", + "ConnectTimeout=10", + hostSshTarget, + "true", + ]; + const hostSshProbeResult = hostSshKeyPresent + ? runCommand(hostSshProbeCommand, repoRoot, { timeoutMs: 15_000 }) + : null; + const hostSshProbeOk = hostSshProbeResult?.exitCode === 0; return { - ok: result.exitCode === 0 && restartPolicyOk && pidModeOk && state === "running", + ok: containerReady && hostSshKeyPresent && hostSshProbeOk, containerName, restartPolicy, restartPolicyOk, @@ -200,6 +239,16 @@ function inspectAttachedContainer(options: ProviderAttachOptions): ProviderAttac command, exitCode: result.exitCode, stderr: result.stderr.trim(), + hostSshKeyPath, + hostSshKeyPresent, + hostSshKeyCommand, + hostSshKeyExitCode: hostSshKeyResult?.exitCode ?? null, + hostSshTarget, + hostSshProbeOk, + hostSshProbeCommand, + hostSshProbeExitCode: hostSshProbeResult?.exitCode ?? null, + hostSshProbeStdout: hostSshProbeResult?.stdout.trim() ?? "", + hostSshProbeStderr: hostSshProbeResult?.stderr.trim() ?? "", }; } @@ -237,6 +286,7 @@ export async function runProviderCommand(config: UniDeskConfig, args: string[]): const result = options.up ? runCommand(composeCommand, repoRoot) : null; const containerValidation = options.up ? inspectAttachedContainer(options) : null; return { + ok: result === null || (result.exitCode === 0 && containerValidation?.ok === true), providerId: options.providerId, masterServer: options.masterServer, envFile: options.envFile, @@ -251,9 +301,10 @@ export async function runProviderCommand(config: UniDeskConfig, args: string[]): result, containerValidation, notes: [ - "The generated env file keeps only UNIDESK_MASTER_SERVER and PROVIDER_ID unless --provider-token is supplied.", + "The generated env file records the master address, provider identity, upgrade coordinates, and Host SSH target; PROVIDER_TOKEN is added only when supplied.", "Place a maintenance SSH private key at hostSshKeyPath if this provider should expose host.ssh.", - "When --up is used, containerValidation must show restartPolicy=always and pidMode=host before the node is accepted as durable across Docker daemon restarts.", + "When --up is used, containerValidation must show restartPolicy=always, pidMode=host, hostSshKeyPresent=true, and hostSshProbeOk=true before the node is accepted.", + "For Docker Desktop with a WSL key directory, run provider attach from that WSL distro so the bind mount resolves to the distro filesystem.", envStatus === "skipped" || composeStatus === "skipped" ? "Existing generated files differed and were not overwritten; rerun with --force to replace them." : "Generated files are ready.", ], };