- add queue merge controls and no-cache overview loading\n- improve runtime probes, judge retries, and task trace rendering\n- extend CLI/E2E/docs coverage for code queue and user services
38 KiB
Provider Gateway Reference
Provider Gateway 是计算节点侧容器。它只主动连出到主 server 暴露的 provider ingress WebSocket,不要求计算节点有公网 IP,适合 NAT、内网和防火墙后的机器。
Main Server Self Provider
当前主 server 也运行一个 provider-gateway,providerId 固定来自 config.json 的 providerGateway.id。这让单机环境也能验证完整的分布式调度闭环:frontend 发起任务,core 写数据库并通过 provider ingress WebSocket 下发,provider gateway 执行后回传状态。
Upgrade Safety Gate
计算节点 provider-gateway 容器的重建和升级权威路径是 provider.upgrade 的 mode: "schedule",或 frontend 中等价的显式升级调度。该路径由在线 provider 通过本地 Docker socket 启动 detached updater 容器,让升级动作脱离当前 WebSocket 与 SSH 透传会话的生命周期;重建目标只能是 provider-gateway service,并且必须带 --no-deps 与 --force-recreate,不得牵连 database、backend-core、frontend 或业务用户服务,也不得因为镜像 tag 未变而 no-op。
远程升级必须采用 sleep-and-validate 回滚保护:旧 gateway 在成功调度 updater 后关闭当前 WebSocket 并进入最长 5 分钟的助眠期;updater 先构建新镜像,再用旧容器的环境变量、挂载、网络和 extra_hosts 拉起候选 gateway;候选 gateway 必须在日志中出现 connect_open 和 register ack 成功,才允许把候选容器 restart policy 改为 always、删除旧 gateway、并把候选容器改名为原容器名。候选验证失败时 updater 必须删除候选容器并退出失败,旧 gateway 到达助眠上限后自动重连主 server,形成自动回滚。backend-core 必须在同一 Provider ID 被新 WebSocket 替换后忽略旧 WebSocket 的 close 事件,避免候选已上线后又被旧连接关闭标记为 offline。
禁止通过 UniDesk 自己的 Host SSH / WSL SSH 透传同步执行 docker compose up -d --build provider-gateway、docker compose restart provider-gateway、docker rm -f <provider-gateway> 后再启动等自重建命令。原因是这条 SSH 透传连接正由被重建的旧 provider-gateway 容器承载;旧容器停止后会切断控制通道,可能把节点留在旧容器已停、新容器未起的不可达状态。SSH 透传只允许用于诊断、修复升级前置条件、查看本地状态和升级后验证,不允许作为计算节点 provider-gateway 正式重建/升级通道。
只有旧节点尚不支持 provider.upgrade mode=schedule 时,才允许使用节点本地终端、节点自有 Web terminal、systemd、计划任务或 detached shell 做一次 bootstrap;bootstrap 完成后必须立刻回到 provider.upgrade mode=schedule 做真实升级验证,并通过公网 frontend 或 remote CLI 确认节点重新上线、远程更新可用性和 SSH 透传可用性。
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。
新增计算节点推荐使用两项配置的简化挂载流程:在目标节点的 UniDesk 仓库根目录运行 bun scripts/cli.ts provider attach <PROVIDER_ID> --master-server http://74.48.78.17/ --up;如果主 server 仍是默认地址,--master-server 可省略。该命令生成 .state/provider-<ID>.env 和 provider-<ID>.yml,env 文件默认只保留 UNIDESK_MASTER_SERVER 与 PROVIDER_ID 两项,Compose 固定 restart: always、pid: "host"、Docker socket、只读 /workspace 仓库挂载、日志目录和 /run/host-ssh 维护私钥目录。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 <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 显式覆盖。
Mandatory SSH Passthrough Bundle
新增 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。
计算节点镜像必须包含 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 ... <target> hostname 做本地烟测后再接入主 server。
SSH 透传只作为维护桥,不作为普通计算任务和自动升级的执行通道。普通任务、Docker 状态采集和 provider.upgrade 仍必须走本地 Docker socket;SSH 透传用于节点诊断、人工修复和验证 WSL/宿主维护入口是否可达。
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 节点应优先使用 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 权限、性能和路径转换问题。
WSL provider 的最小环境文件应放在节点本地私有路径,例如 /home/ubuntu/unidesk/.state/provider-<ID>.env,并由生成的 provider-<ID>.yml 或 docker run --env-file 读取。新挂载默认只写 UNIDESK_MASTER_SERVER 与 PROVIDER_ID;如果需要覆盖 labels,PROVIDER_LABELS_JSON 在 Docker env-file 中可以写成单行 JSON,临时用 shell source 调试时必须对整段 JSON 加引号,否则 shell 会按 {} 和逗号拆分导致 JSON 解析失败。不写 labels 时 provider-gateway 会根据 Provider ID、Docker、WSL 内核和 /etc/os-release 自动生成 host、role、docker、wsl、distro 与 attachMode=simple,并在运行时追加 runtime、dockerSocketPresent、gateway 版本和 gatewayUptimeSeconds。.state/provider-<ID>.env、provider-<ID>.yml、logs/provider-<ID>/ 和容器日志属于节点本地运行态,必须保持在 .gitignore 覆盖范围内,不能提交 provider token、登录态或运行日志。
长期运行推荐用 systemd 管理 provider-gateway 容器,而不是只在交互 shell 中运行 Bun 进程。systemd unit 的稳定形态是:ExecStartPre=-docker rm -f unidesk-provider-gateway-<ID> 清理同名旧容器,ExecStart=docker run --restart always --pid host --name unidesk-provider-gateway-<ID> --env-file ... -v /var/run/docker.sock:/var/run/docker.sock -v /home/ubuntu/unidesk:/workspace:ro -v /home/ubuntu/unidesk/logs/provider-<ID>:/var/log/unidesk -v <ssh-key-dir>:/run/host-ssh:ro unidesk_provider-gateway:<id>,ExecStop=docker stop unidesk-provider-gateway-<ID>,并设置 Restart=always。临时部署也必须使用 docker run -d --restart always --pid host,并保证容器名、env 文件、日志目录、SSH 私钥只读挂载和镜像 tag 都带上节点 ID,便于 frontend、Docker 状态、SSH 透传、进程资源表和本地排障互相对应。provider.upgrade 是长期接入节点的必备能力,provider-gateway 不提供 PROVIDER_UPGRADE_ENABLED 或等价禁用开关;如果节点缺少升级环境变量或 SSH 透传环境变量,必须修正节点部署,而不是在服务端接受只能预检、不能升级或不能维护透传的半成品状态。
WSL 本身会在没有前台进程时被 Windows 回收;如果该节点要作为长期在线算力,必须通过 Windows 启动项、计划任务或后台 wsl.exe -d <distro> -u root -- bash -lc "systemctl start docker unidesk-provider-gateway-<ID>.service; exec sleep infinity" 这类 keepalive 进程保持发行版运行。仅启用 WSL 内 systemd service 不等价于 Windows 层面的常驻守护。
Docker daemon 重启后的恢复验收必须看两个层级。第一层是 Docker 自身:docker inspect --format '{{.HostConfig.RestartPolicy.Name}} {{.HostConfig.PidMode}} {{.State.Status}}' unidesk-provider-gateway-<ID> 必须返回 always host running;D601/D518 这类 Docker Desktop daemon 上还要记录 docker info --format '{{.Name}} {{.LiveRestoreEnabled}}',当前 LiveRestore=false 意味着 daemon 重启会停止容器,恢复完全依赖 restart policy,因此不能把容器曾经是 running 当作已具备重启恢复能力。第二层是宿主/WSL 常驻:节点应有 systemd、Windows 计划任务、Docker Desktop 自启动或等价 keepalive 证明 Docker daemon 会随机器/WSL 启动;如果 systemctl list-unit-files '*unidesk*' 为空,必须在验收记录中明确该节点暂时只依赖 Docker Desktop daemon 自身,不能声称有 WSL 内 systemd watchdog。provider-gateway 新版本会在启动与 heartbeat 中自检当前容器 restart policy,发现不是 always 时通过 Docker socket 尝试 docker update --restart always <self> 并在 labels 中上报 providerGatewayRestartPolicyOk、providerGatewayPidModeOk 与 providerGatewayRuntimeGuardOk;这只是最后一道自愈,不能替代 Compose/systemd 的正确配置。
D601 的已验证常驻链路是 Windows 登录计划任务 UniDesk-D601-Autostart -> C:\Users\liang\AppData\Local\UniDesk\d601-autostart.cmd -> wsl.exe -d Ubuntu -u ubuntu -- /bin/bash -lc "/home/ubuntu/.local/bin/unidesk-d601-autostart task"。WSL 脚本负责启动 sshd、等待 Docker Desktop daemon、确保 unidesk-provider-gateway-D601 为 restart=always 且 running,并调用 /home/ubuntu/.local/bin/unidesk-microservice-autorecover 恢复 D601 用户服务。后续 WSL 节点可以复用这个模式,但必须把 Windows 用户、distro 名、provider ID、业务 autorecover 脚本和日志目录替换为节点自己的值;该模式只用于节点启动后的守护恢复,provider-gateway 版本升级仍必须走 provider.upgrade mode=schedule。
WSL Network And Proxy Bootstrap
WSL 节点出网异常时,先把代理设置固化到目标 WSL 用户的 ~/.bashrc,而不是只在当前 shell 临时 export。长期可复用的写法是在每次交互 shell 启动时从 /etc/resolv.conf 读取 Windows 宿主在 WSL NAT 中的 nameserver IP,再导出 http_proxy、https_proxy、HTTP_PROXY、HTTPS_PROXY、all_proxy 和 ALL_PROXY 指向 http://<windows-host-ip>:7890 / socks5://<windows-host-ip>:7890,并保留 no_proxy=localhost,127.0.0.1,::1,host.docker.internal。不要把某次启动看到的宿主 IP 当成永远不变的常量;动态读取可以避免 WSL 网络重建后代理失效。
Windows 代理程序常见默认只监听 127.0.0.1:7890,此时 WSL 访问 <windows-host-ip>:7890 会失败。可选修复是让代理程序监听 WSL 可达网卡,或在 Windows 用户态启动一个幂等 TCP relay,把 <windows-host-ip>:7890 转发到 127.0.0.1:7890;netsh interface portproxy 通常需要管理员权限,不应作为普通节点 bootstrap 的唯一方案。.bashrc 可以在发现 <windows-host-ip>: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 验收标准。
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 协议或服务端配置,应改用节点侧镜像交付兜底。
可复用的兜底方式是使用本机已存在的 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-id> 运行。该兜底镜像只改变节点侧交付方式,不改变 PROVIDER_SERVER_URL、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 <image> /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。
WSL 节点还应补充一次真实调度验证:向该 PROVIDER_ID 下发 docker.ps,任务必须从 dispatched 进入 succeeded,并在结果中看到 WSL Docker daemon 返回的容器列表;对于容器化运行的 provider-gateway,列表中通常应包含 unidesk-provider-gateway-<PROVIDER_ID>。这一步可以同时证明 provider WebSocket、服务端任务路由、节点侧 Docker socket 和结果回传链路都已贯通。
SSH 透传自测是 provider-gateway 部署验收的一部分。目标 Provider 在线后,先确认 frontend 节点清单或 debug health 中该节点 labels 显示 hostSshConfigured=true、hostSshKeyPresent=true 且能力包含 host.ssh;再运行 bun scripts/cli.ts debug dispatch <PROVIDER_ID> host.ssh --wait-ms 15000,任务必须 succeeded,result 中 probeLine 必须包含 UNIDESK_SSH_TEST 且 exitCode=0;最后运行 bun scripts/cli.ts ssh <PROVIDER_ID> hostname,输出必须是目标宿主或 WSL 的 hostname,进程退出码必须为 0。任何 provider 在线但不声明 host.ssh 的状态都只能算未完成部署。
如果该节点承载用户服务,还必须声明 microservice.http capability,并通过 bun scripts/cli.ts microservice health <id> 或 remote CLI 等价命令验证 backend-core 能经 provider-gateway 访问节点本机后端。用户服务后端端口不得映射到公网;provider-gateway 只允许代理节点本地 HTTP 地址或主 server 显式 Compose 服务名,业务 API 路径和 HTTP 方法还要受 backend-core allowedPathPrefixes 与 allowedMethods 限制。
自动化验证必须使用 Playwright 访问公网 frontend,而不是在容器内直接调 core API 代替浏览器验收。标准命令是 bun scripts/cli.ts e2e run;该命令会让 Playwright 打开公网 http://74.48.78.17:18081/、登录、抓取页面中的 Provider 信息和 查看原始JSON 内容,并检查 Provider 自接入、资源指标、Docker 状态和 provider.upgrade 预检。外部新增节点的人工验收应复用同一套前端路径:先确认 Provider 信息出现在节点清单,再确认资源监控和 Docker 状态页面有该节点的数据,最后通过任务调度向该 Provider 下发 echo、docker.ps 或维护专用 host.ssh probe,并在任务历史中查看耗时、状态、stdout/stderr 摘要和失败原因。
Provider Ingress
provider ingress 是唯一允许公网暴露的 provider 连接接口,当前由 backend-core 容器的独立端口提供 /ws/provider 和 /health。backend-core REST API 仍只在 Docker 内网开放,外部计算节点只应连接 provider ingress。
Docker Socket Path
自动任务执行只允许走本地 Docker socket。Compose 将 /var/run/docker.sock 挂入 provider-gateway,provider 标签会报告 dockerSocketPresent,docker.ps 调试任务会通过该 socket 查询宿主 Docker 容器。
User Service HTTP Proxy
microservice.http 是 provider-gateway 给 UniDesk 用户服务使用的私有后端访问能力。backend-core 通过真实 WebSocket dispatch 下发目标 service id、节点本机 targetBaseUrl、path、query、method、request body、timeout 和可选 JSON 数组裁剪参数;provider-gateway 支持 GET、HEAD、POST、PUT、PATCH、DELETE,但最终允许方法必须由每个用户服务的 backend.allowedMethods 显式配置。provider-gateway 只允许访问 http://127.0.0.1、http://localhost、http://host.docker.internal 这些节点本地地址;主 server 内置 Todo Note 与 Code Queue 后端可分别使用 Compose 服务名 http://todo-note:4211 与 http://code-queue:4222。该能力不打开 provider-gateway 入站端口,也不替代业务仓库自身 Dockerfile/docker-compose。
超大 JSON 响应可以使用 jsonArrayLimits 在 provider-gateway 返回前裁剪指定数组,并在响应体中写入 _unidesk.arrayLimits 元数据,便于 UniDesk frontend 预览列表而不展示裸 JSON。长期应优先推动业务后端提供分页 API;裁剪只是 UniDesk 集成层的展示保护。
Gateway Version Metadata
provider-gateway 必须从自身 package.json 读取版本号,并在 register 与 heartbeat labels 中上报 providerGatewayName、providerGatewayVersion、providerGatewayStartedAt 和 providerGatewayUpgradePolicy。backend-core 将这些 labels 合并到 unidesk_nodes.labels,frontend 在节点清单、资源监控和 资源节点 / 网关版本 中展示;旧节点缺少这些字段时只能显示版本未知,不能用猜测值替代。unideskCapabilities、hostSshConfigured、hostSshKeyPresent 和 hostSshTarget 也是 WebUI 运维可用性徽标的数据源,用于直接显示每个计算节点的 SSH 透传可用性与远程更新可用性。
src/components/provider-gateway 下任何代码或行为变更都必须在同一变更集中递增 src/components/provider-gateway/package.json 的 version,不得只改实现而沿用旧版本号。验收时必须通过公网 frontend 的 资源节点 / 网关版本 或 bun scripts/cli.ts debug health 确认目标 provider 的 providerGatewayVersion 已上报新版本;如果线上节点仍显示旧版本,该 provider-gateway 变更不能视为交付完成。
任何远程升级预检、执行升级和远程更新记录都必须显式显示指定 Provider 的 gateway 版本号。provider.upgrade result 的 plan 中必须包含 providerId、providerName、当前运行中的 providerGatewayVersion 和从升级 workspace 的 src/components/provider-gateway/package.json 读取到的 targetProviderGatewayVersion;frontend 的升级控件与 资源节点 / 网关版本 远程更新记录必须把该版本渲染为结构化字段,不能只把版本埋在原始 JSON 中。
Docker Status Telemetry
provider-gateway 连接成功后必须周期性上报 Docker daemon 状态,数据来源是本地 Docker socket 上的 docker info、docker ps -a、docker images、docker volume ls 和 docker network ls。backend-core 将最新快照保存到 unidesk_node_docker_status,frontend 的资源节点 Docker 状态 子标签用该快照渲染 Docker Desktop 风格视图;该能力仍然只通过 provider 主动上报,不要求主 server 反向连接计算节点。
System Status Telemetry
provider-gateway 连接成功后必须周期性上报节点 CPU、内存、硬盘和进程资源占用。整体采集来源是节点本地 /proc/stat、/proc/loadavg、/proc/meminfo 与 df -PB1,进程表来源是 /proc/[pid]/stat、/proc/[pid]/status、/proc/[pid]/cmdline 和 /proc/[pid]/io;backend-core 将最新快照保存到 unidesk_node_system_status,并将历史采样保存到 unidesk_node_metric_samples 供 frontend 绘制任务管理器风格曲线。内存使用量采用实际占用口径:MemTotal - MemFree - Buffers - Cached - SReclaimable + Shmem,也就是不把 Linux page cache / buffer 计入占用;上报中同时保留 cacheBytes 便于排查。进程表的 rssBytes 是实际常驻内存,默认按 rssBytes 降序截取前 120 个进程;cpuPercent 使用相邻采样 CPU tick 差值,首个采样用进程生命周期平均值兜底;磁盘 I/O 速率使用相邻 /proc/[pid]/io 的 read_bytes/write_bytes 差值。该链路仍然由 provider 主动上报,主 server 不反向探测计算节点。
Remote Provider Upgrade
backend-core 可以通过真实 WebSocket 调度向在线 provider 下发 provider.upgrade。mode: "plan" 只返回升级计划,用于 E2E 和人工预检;mode: "schedule" 会要求 provider-gateway 通过本地 Docker socket 启动一个 detached updater 容器。updater 的固定策略是先执行 docker compose build provider-gateway,构建成功后只按 com.docker.compose.project 与 com.docker.compose.service=provider-gateway label 删除旧 provider-gateway 容器,最后执行 docker compose up -d --no-deps --force-recreate provider-gateway。--no-deps 是强制要求,--force-recreate 用于保证 provider 重新注册能力标签并避免 compose no-op;升级 provider-gateway 时不得重建或停止 database、backend-core、frontend。对 D518、D601 这类计算节点,mode: "schedule" 是正式重建/升级 provider-gateway 容器的唯一标准路径;升级执行路径使用 Docker socket 和只读仓库挂载,不使用 Host SSH 维护桥作为自动调度通道。
远程升级策略固定为 always-enabled:只要 provider-gateway 在线并声明 provider.upgrade,mode: "schedule" 就必须真正调度升级容器,不允许被 PROVIDER_UPGRADE_ENABLED=false、前端隐藏按钮或服务端特殊名单禁用。升级能力的安全边界不是开关,而是显式 PROVIDER_UPGRADE_* 配置、Docker socket 权限、只读仓库挂载、固定 Compose service 和 --no-deps 约束。升级计划中必须展示 policy: "always-enabled"、updater 容器名、runner image、workspace、Compose project/service、env file、compose file 和实际 docker run 命令,方便前端任务历史与 CLI debug 直接诊断。
mode: "schedule" 的成功返回只代表 updater 已被调度,最终升级成败由候选 gateway 自验证决定。updater 必须先按 Compose 构建新镜像,再用旧容器的 Config.Env 生成候选 env-file,并复用旧容器的 Docker socket、日志目录、SSH 私钥只读挂载、Compose 网络和 extra_hosts;候选容器启动时 restart policy 必须先是 no,并显式使用 --pid host 保持节点级进程资源采集,验证通过后才能改成 always 并删除旧容器。升级计划的 replacementStrategy 必须包含 oldGatewaySleepMs、validationTimeoutMs、promoteOnlyAfterCandidateValidation、candidateRestartPolicyAfterPromotion: "always"、candidateUsesOldContainerEnvironment、candidateUsesOldContainerMounts、candidateUsesOldContainerNetworks、candidateUsesOldContainerExtraHosts 和 candidateUsesHostPidNamespace,并且必须在 plan 中显示指定 Provider 的当前/目标 gateway 版本号,便于前端和 CLI 判断这不是旧的先删旧容器再 up 的危险流程。
远程更新记录的权威来源是 backend-core 保存的 provider.upgrade 任务历史,而不是 provider-gateway 容器日志文件。frontend 必须按 Provider 聚合这些任务,并把状态、模式、task id、来源、耗时、策略、updater 容器摘要、失败原因和更新时间渲染为表格或卡片;完整 task/result JSON 只能由操作员点击 查看原始JSON 后查看。
旧版 provider-gateway 如果只能返回 plan 或因为旧环境中的 PROVIDER_UPGRADE_ENABLED=false 拒绝 schedule,需要先通过任意现有维护通道手动 bootstrap 一次。bootstrap 的目标不是长期流程,而是把节点更新到支持 always-enabled 远程升级和 Host SSH / WSL SSH 维护桥的版本;完成后必须立刻用 bun scripts/cli.ts debug dispatch <PROVIDER_ID> provider.upgrade --mode schedule --wait-ms 15000 做一次真实一键升级验证,再用 bun scripts/cli.ts debug health 或公网 frontend 确认该节点仍在线、unideskCapabilities 包含 provider.upgrade,需要 SSH 维护的 WSL 节点还必须包含 host.ssh。
Manual Upgrade Maintenance
手动升级只用于把旧节点 bootstrap 到支持 always-enabled 远程升级的版本;bootstrap 完成后,常规重建/升级必须回到 provider.upgrade mode=schedule,不得再用 SSH 透传同步重建 provider-gateway。节点侧维护步骤是:进入节点本地 UniDesk 仓库,执行 git pull --ff-only 获取主 server 已推送版本;确认 .state/provider-<ID>.env 中存在 PROVIDER_SERVER_URL=ws://74.48.78.17:18082/ws/provider、PROVIDER_ID=<ID>、PROVIDER_NAME=<ID>、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:<id>、DOCKER_SOCKET_PATH=/var/run/docker.sock、MONITOR_DISK_PATH=/、心跳和重连参数。旧 env 文件中如果还残留 PROVIDER_UPGRADE_ENABLED,新版 provider-gateway 会忽略它;长期文档和新部署不得再依赖这个键。
如果节点已有专用 Compose,优先用节点本地 Compose 手动重建一次:docker compose --env-file .state/provider-<ID>.env -f <compose-file> -p <compose-project> 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:<id> .,再以固定容器名重建:使用 --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-<ID>.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_ID> provider.upgrade --mode schedule --wait-ms 15000,确认任务 succeeded 且 result 包含 updater 容器信息;最后再次查看 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 <PROVIDER_ID> host.ssh --wait-ms 15000 和 bun scripts/cli.ts --main-server-ip 74.48.78.17 ssh <PROVIDER_ID> hostname,验证维护桥没有在升级后丢失;该 remote CLI 默认走公网 frontend,不需要指定 --main-server-key。
Host SSH Maintenance Bridge
宿主 SSH / WSL SSH 转发是 provider-gateway 部署的必备维护能力,但只作为应急维护辅助路径,不用于自动计算任务调度。实现参考 ../web-terminal 的经验:容器内使用只读挂载的私钥,主动连接宿主或 WSL sshd,并设置 BatchMode=yes、StrictHostKeyChecking=accept-new、ServerAliveInterval=20 和 ServerAliveCountMax=3。TTY 策略必须按会话类型区分:无远端命令的交互登录 shell 使用 ssh -tt,带远端命令的会话使用 ssh -T,避免脚本 stdin 被伪终端回显或把 Ctrl-D 当成业务输入。主 server Compose 会把 config.json 的 sshForwarding.keyDir 只读挂载为 /run/host-ssh,provider 标签会上报 hostSshConfigured、hostSshKeyPresent 和 hostSshTarget,便于在前端节点清单确认维护桥是否具备条件。
WSL 计算节点使用 Docker Desktop daemon 时,provider-gateway 容器通常应连接 host.docker.internal:22,目标是当前 WSL 发行版里的 sshd,而不是给节点开放公网 SSH。节点侧必须确认 sshd 监听 22、目标用户可用维护公钥免密登录、authorized_keys 与挂载到 /run/host-ssh/id_ed25519 的私钥匹配;如果容器内直连 host.docker.internal 都失败,先修复 WSL sshd、Docker Desktop host gateway 或密钥权限,再排查 UniDesk WebSocket 透传。
WSL provider 需要调用 Windows-only 工具链时,优先在 WSL 用户的 ~/.local/bin 放轻量 shim,而不是维护一套完全分叉的 Windows skill。稳定形态是:win-powershell 负责 Windows 发现和诊断,win-cmd 通过临时 .cmd 文件执行命令以规避 UNC 当前目录和带空格路径的 cmd 引号问题,win-py 将 /mnt/<drive>/... 参数转换为 Windows 路径后调用 py.exe,win-npm 通过 npm.cmd --prefix <skill_dir> 运行需要访问 COM 口的 TypeScript skill。Keil 这类依赖 Windows GUI/驱动/注册表的 skill 应由 WSL wrapper 调用 Windows 侧 skill 脚本;board-comm 这类纯 TCP JSON-RPC skill 可以直接用 WSL Python 运行;serial-monitor 需要访问 Windows COM 口时应走 Windows Node/npm。不要用宽泛的 find /mnt/* 扫 Windows 盘,skill 位置先用 bun scripts/cli.ts ssh <PROVIDER_ID> skills,项目位置优先用 PowerShell 或结构化 glob/find 缩小范围。Windows 透传 wrapper 属于节点 bootstrap,不等于修改 WSL skill 业务代码;完整规则见 docs/reference/windows-passthrough.md。
维护桥通过真实 WebSocket dispatch 暴露为 host.ssh 命令。默认 payload 使用 mode: "probe",远端只执行一个短命令并返回 UNIDESK_SSH_TEST user=... host=... bridge=host.ssh cwd=...;需要人工诊断时可以显式使用 mode: "exec" 与 command 字段执行有界命令。所有 host.ssh 执行都必须有超时,stdout/stderr 在 task result 中截断展示;自动升级和普通任务仍必须使用 Docker socket 与 provider.upgrade,不得把 WSL SSH 维护桥当成调度通道。
面向人的终端入口是 bun scripts/cli.ts ssh <PROVIDER_ID> [ssh-like args...]。无后续参数时打开远端登录 shell,有后续参数时执行远端命令并返回远端 exit code;该入口走 backend-core 内网 /ws/ssh broker 和 provider 既有 WebSocket,不新增公网 core 端口。传统 ssh 传输参数由 provider-gateway 环境变量统一控制,CLI 只负责把 Provider ID 后的远端命令和终端 stdin/stdout/stderr 透传过去。WSL 节点需要同时看清 Linux/WSL 与 Windows 两套 skill 时,使用 bun scripts/cli.ts ssh <PROVIDER_ID> skills,该命令只通过已建立的维护桥读取 SKILL.md 元数据,不要求 provider-gateway 新增业务 API。
验证 WSL SSH 桥时,先在目标 WSL 中启动 sshd 并确保维护公钥写入目标用户的 authorized_keys,再确认目标 provider 注册 labels 中 unideskCapabilities 包含 host.ssh。运行 bun scripts/cli.ts debug dispatch <PROVIDER_ID> host.ssh --wait-ms 15000 后,结果应在 debug task latest 或前端任务历史中显示 status: succeeded、probeLine 含 UNIDESK_SSH_TEST、exitCode: 0,并且目标节点 labels 中 hostSshKeyPresent 为 true;随后运行 bun scripts/cli.ts ssh <PROVIDER_ID> 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 <PROVIDER_ID> host.ssh --wait-ms 15000 和 bun scripts/cli.ts --main-server-ip 74.48.78.17 ssh <PROVIDER_ID> hostname;默认 remote CLI 走公网 frontend 登录态,不需要主 server SSH key。健康检查必须能看到该 Provider 在线、hostSshConfigured=true、hostSshKeyPresent=true、hostSshTarget 正确、unideskCapabilities 包含 host.ssh,probe 必须返回 UNIDESK_SSH_TEST,ssh <PROVIDER_ID> hostname 必须输出目标 WSL/宿主 hostname 且 exit code 为 0。如果 D518 这类 WSL 节点没有公网 SSH 入口,也必须通过这个 provider-gateway 自连维护桥完成验证,而不是要求主 server 直接连节点公网 22 端口;旧版 provider 未声明 host.ssh 时必须先升级 provider-gateway,否则 core 会拒绝 SSH 透传。