53 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 成功,才允许删除旧 service 容器和候选容器,并用原 Compose service 重新创建最终 provider-gateway 容器。候选验证成功不是升级终态;最终 Compose 容器必须本地重新验证 restart=always、pid=host 和 backend-core reconnect,并且 backend-core 还必须看到同一 Provider ID、目标 providerGatewayVersion、非 candidate 容器名、足够新的真实 heartbeat(providerGatewayHeartbeatAt)后,provider.upgrade 任务才能进入 succeeded。最终容器未在窗口内重连时,updater 应优先恢复 last-known-good 容器,backend-core 必须把任务写成结构化 failure/blocker,不能把 candidate 曾经成功当作最终成功。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 维护私钥目录和 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 <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/宿主维护入口是否可达。
SSH Data TCP Pool
Provider WebSocket 是注册、heartbeat、dispatch、provider.upgrade 和短控制消息的主管理通道,不再承载 trans/tran/ssh 的大块 stdin/stdout/stderr 数据面。Host SSH 透传的数据面必须走 provider 主动连到 main server 的 TCP warm pool;provider 仍然只主动连出,不要求计算节点暴露入站端口,也不改变公网 frontend/provider ingress 之外的业务入口。
标准能力名是 host.ssh.tcp-pool。支持该能力的 provider-gateway 会在连接 WebSocket 后按 PROVIDER_DATA_HOST、PROVIDER_DATA_PORT 和 PROVIDER_DATA_POOL_SIZE 建立常驻 TCP 通道池,默认 10 条通道;main server 默认公开 provider data port 18084,backend-core 容器内监听 8082。每条 SSH 会话或文件传输占用一条 data channel,大文件传输不能阻塞其他已空闲通道上的 SSH、小文件或脚本请求;这里不再做应用层队列、fair scheduling 或把大文件分发到控制 WebSocket 的 fallback。池耗尽时必须返回结构化错误,例如 provider-data-pool-exhausted,调用方按短查询/重试语义处理,而不是让单个 trans 长挂。
旧 provider 没有声明 host.ssh.tcp-pool 时,backend-core 必须让 /ws/ssh 快速失败并返回 provider-gateway-upgrade-required,提示升级 provider-gateway;不得保留旧的 WebSocket 数据路径作为兼容分支。provider.upgrade mode=schedule 本身仍走 provider WebSocket 和 Docker socket,因为升级动作必须在旧 provider 上可达,用于把节点推进到支持 TCP pool 的版本。
可见性必须跟随通道池。provider-gateway labels 应上报 providerGatewaySshDataTransport=tcp-pool、目标 host/port、desired pool size、ready/claimed/connecting/total channel 数和最近错误;debug health、frontend raw JSON 和 issue 证据应优先用这些字段判断是 provider 未升级、data port 不通、池耗尽还是目标 host SSH 本身失败。只看到 provider 在线或 host.ssh 可用,不等于 TCP data pool 已经可用。
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 ... -p 127.0.0.1:18789:18789 -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 -p 127.0.0.1:18789:18789,并保证容器名、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;最后运行 trans <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 还应声明 microservice.http.tunnel,表示 backend-core 可以在同一 provider WebSocket 上发起短生命周期 HTTP tunnel request 并等待 provider 直接回传 upstream 响应;这一路径避免为每个 HTTP 代理请求创建、轮询并读取任务结果,降低高频只读接口出现 “upstream socket closed” 或 pending task 清理竞态导致 502 的概率。旧 gateway 未声明 tunnel 时,backend-core 可以继续使用原 microservice.http dispatch 兼容路径,但运行健康验收应以 tunnel capability 为目标状态。
用户服务后端端口不得映射到公网;provider-gateway 只允许代理节点本地 HTTP 地址或主 server 显式 Compose 服务名,业务 API 路径和 HTTP 方法还要受 backend-core allowedPathPrefixes 与 allowedMethods 限制。无论走 tunnel 还是旧 dispatch,响应必须保留 upstream status、content-type、截断标记和代理模式诊断头;provider WebSocket 断开时 backend-core 必须显式返回 504/502,不得静默 fallback 到公网直连、旧缓存或 provider-gateway 以外的业务 HTTP 入口。
自动化验证必须使用 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 给 deployment.mode=unidesk-direct 用户服务使用的私有后端访问能力。新 provider 必须同时声明 microservice.http.tunnel:backend-core 对 UI 高频读请求优先复用既有 provider WebSocket 发送 http_tunnel_request 并等待 http_tunnel_response,不再为每个轮询创建 unidesk_tasks 调度记录;旧 provider 未声明该能力时才回落到原 dispatch 任务路径。响应头会标记 x-unidesk-proxy-mode=provider-ws-http-tunnel 或旧 provider-task,用于性能验收。
backend-core 下发目标 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 后端可使用 Compose 服务名 http://todo-note:4211。deployment.mode=k3sctl-managed 的 Code Queue 不得通过 provider-gateway 直连业务容器,正式路径只能是 backend-core -> provider WebSocket HTTP tunnel -> k3sctl-adapter -> Kubernetes native Service/DNS,必要时显式 fallback 到 Kubernetes API service proxy -> k3s/k8s Service。该能力不打开 provider-gateway 入站端口,也不替代业务仓库自身 Dockerfile/docker-compose。
backend-core 必须把 provider WebSocket HTTP tunnel 的失败分类到响应 body 和 headers:失败响应至少包含 requestId、providerId、serviceId、stage、failureReason 或 provider result,并带 x-unidesk-request-id 与 x-unidesk-tunnel-error。GET/HEAD 非 stream 请求允许短超时分层重试;POST、PATCH、PUT、DELETE 这类可能产生副作用的请求不得自动重复。Provider 重连时 backend-core 必须先确认 close 事件来自当前 active socket,旧 socket 被新 socket 替换后的迟到 close 不得清理新连接上的 tunnel waiter,也不得把节点误标 offline。
超大 JSON 响应可以使用 jsonArrayLimits 在 provider-gateway 返回前裁剪指定数组,并在响应体中写入 _unidesk.arrayLimits 元数据,便于 UniDesk frontend 预览列表而不展示裸 JSON。长期应优先推动业务后端提供分页 API;裁剪只是 UniDesk 集成层的展示保护。
Egress Proxy
provider-gateway 可以提供 egress HTTP CONNECT 代理,用于让 Code Queue、Pipeline runner、target-side Docker build 等节点侧执行环境通过既有 provider WebSocket 通道出网。代理默认监听容器内 0.0.0.0:18789,节点部署必须只发布为宿主 loopback 127.0.0.1:18789->18789/tcp,不得开放公网端口;普通 Docker 执行容器可通过同一私有 Docker network 访问 provider-gateway 容器名,k3s/k8s Pod 必须通过显式 Kubernetes Service 暴露同节点 provider-gateway 私有 endpoint,例如 D601 Code Queue 使用 selector 指向 hostNetwork 桥接 Pod 的 d601-provider-egress-proxy.unidesk.svc.cluster.local:18789,不得把固定 Docker bridge IP、手工 EndpointSlice 或该 egress Service 当作业务 HTTP 入口。代理只负责把本地 CONNECT/absolute HTTP 请求转换为 egress_tcp_open、egress_tcp_data、egress_tcp_close 消息;backend-core 在主 server 侧建立真实 TCP 连接并把数据回传,避免 D601 等计算节点本地网络不可达时卡死 Codex/Git/NPM/apt/Playwright。
该能力属于 provider-gateway 通道能力,register/heartbeat 的 unideskCapabilities 必须包含 network.egress-proxy,labels 必须上报 providerGatewayEgressProxy* 状态。不得再为某个用户服务单独注册伪 provider 来实现出网代理;否则节点列表会出现虚假 provider,且代理、统计、升级路径会形成多套通道。代理健康检查使用 GET /__unidesk/egress-proxy/health,返回 connected、providerId、activeTunnels、pendingTunnels、oldestTunnelAgeMs、openTimeoutMs、idleTimeoutMs 和监听端口;业务服务自己的 /health 应把该结果作为排障证据透出。
egress proxy 的长期边界是“统一 provider 通道,不引入第二控制面”。backend-core 只接受在线 provider socket 上的 egress_tcp_* 消息,并在该 socket 关闭时销毁全部对应 TCP relay;provider-gateway 只维护本地 HTTP proxy 与 WebSocket 消息映射,不保存业务状态,不参与任务调度、统计或节点注册以外的控制面。执行容器、用户服务、Pipeline runner 和 provider-side deploy build 不允许直接连接 backend-core provider ingress,也不允许携带 provider token 自行注册;需要出网时只能连接同节点 provider-gateway 的私有 proxy endpoint。当前 k3s/k8s Code Queue 通过 d601-provider-egress-proxy 或 g14-provider-egress-proxy Kubernetes Service 连接同节点 provider-gateway egress endpoint,这是 Pod 内的出网入口,不是业务 HTTP 代理入口,也不能替代 Kubernetes API service proxy。部署构建同样不得新建 SSH SOCKS、公网 master proxy 或未登记的宿主全局代理;构建脚本默认只能把 provider-gateway WS egress 作为短生命周期环境变量和 Docker build-arg 注入,并配合目标节点本地 BuildKit/image cache 避免重复下载大依赖层。G14 的节点本地 VPN proxy 是已登记的基础设施 bootstrap 例外,只允许按 docs/reference/g14.md 用于 G14 host-side 镜像构建、cache prewarm 或恢复下载;k3s runtime Pod 仍必须使用 g14-provider-egress-proxy 和 g14-tcp-egress-gateway。
egress tunnel 必须有生命周期边界:provider-gateway 发出 egress_tcp_open 后如果主 server 未在 openTimeoutMs 内返回 egress_tcp_opened 或 close,必须主动关闭本地 client 并向 core 发送 egress_tcp_close;provider-gateway 与 backend-core 都必须对长时间无数据的 relay 执行 idle 清理,避免 provider WebSocket 抖动、TCP connect 卡住或上游未关闭时留下 stale tunnel。排障时如果 activeTunnels 持续增长、pendingTunnels 非零或 oldestTunnelAgeMs 明显超过业务请求耗时,应先看 provider-gateway 与 backend-core egress 清理日志,再判断 Code Queue、PostgreSQL 或 OA Event Flow 本身是否慢。
故障语义必须显式,不允许静默 fallback。provider-gateway 到 backend-core 的 WebSocket 未连接时,本地 proxy 必须返回 503;执行容器不能自动绕过到 D601 本地直连公网、外部公共代理或主 server 公网 HTTP 端口。NO_PROXY 只用于 PostgreSQL、OA Event Flow、ClaudeQQ、frontend/backend-core 内网代理、provider-gateway health 等明确内网链路,不能把 GitHub、npm registry 等外部目标加入绕过列表。hyueapi.com 与 MiniMax judge 上游 api.minimaxi.com 是明确的模型 API 例外:前者会拒绝 provider-gateway egress proxy 出口,后者在 judge 高频短请求上容易受 provider egress 抖动影响导致任务误重试;Code Queue 必须用 CODE_QUEUE_EGRESS_PROXY_NO_PROXY / NO_PROXY 将 hyueapi.com,.hyueapi.com,api.minimaxi.com,.minimaxi.com 配成直连,其它模型 API 仍不得默认绕过 proxy。验收必须同时证明 provider-gateway labels、业务服务 /health 和执行容器内 curl -I https://... 都走同一 proxy path,hyueapi/MiniMax 例外则以 Code Queue /health.egressProxy.noProxy、MiniMax judge 探测和目标任务成功完成作为证据。
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、可用时的 /proc/[pid]/smaps_rollup 和 fallback 用的 /proc/[pid]/statm;backend-core 将最新快照保存到 unidesk_node_system_status,并将历史采样保存到 unidesk_node_metric_samples 供 frontend 绘制任务管理器风格曲线。内存使用量采用实际占用口径:MemTotal - MemFree - Buffers - Cached - SReclaimable + Shmem,也就是不把 Linux page cache / buffer 计入占用;上报中同时保留 cacheBytes 便于排查。进程表的 memoryBytes 优先使用 smaps_rollup 中的 PSS,避免 PostgreSQL shared buffer 等共享页在多个进程之间重复计入,同时保留 rssBytes、privateBytes 和 sharedBytes 便于排查;如果目标内核或权限不支持 smaps_rollup,则回退为 rssBytes - statm.shared,仍避免把共享页全部计入单个进程,只有 statm 也不可读时才退回原始 RSS。默认按 memoryBytes 降序截取前 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 维护桥作为自动调度通道。
使用 compact build context 的节点必须把 context 视为派生缓存,不得让它成为版本真相。D601 这类节点可能让 Compose 从 .state/provider-<ID>-build-context 构建兜底镜像,以便复用本地基础镜像和缩小构建输入;provider.upgrade mode=schedule 在 docker compose build 前必须从只读 /workspace/src/components/provider-gateway 与 /workspace/src/components/shared 刷新该 context,并把宿主 .state 以可写方式只挂给 detached updater。验收不能只看 updater 日志中的 Built、candidate reconnect 或本地 promote 文本,必须以 backend-core 任务结果中最终非 candidate Compose 容器的 providerGatewayVersion、unideskCapabilities、containerId、restartPolicy、pidMode 和 heartbeatTimestamp 为准;如果线上仍上报旧版本,说明运行镜像没有真正更新,必须先修复构建 context 再重跑标准升级。
远程升级策略固定为 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" 调度成功后,provider-gateway 只能把任务推进到 running,不能返回终态成功;backend-core 必须接管最终终态判定。updater 必须先按 Compose 构建新镜像,再用旧容器的 Config.Env 生成候选 env-file,并复用旧容器的 Docker socket、日志目录、SSH 私钥只读挂载、Compose 网络和 extra_hosts;候选容器启动时 restart policy 必须先是 no,并显式使用 --pid host 保持节点级进程资源采集。candidate 通过日志级 register ack 后仍只是 promotion 前置条件;最终成功必须等 docker compose up -d --no-deps --force-recreate provider-gateway 创建出的非 candidate Compose 容器向 backend-core 上报目标版本 heartbeat。升级计划的 replacementStrategy 必须包含 oldGatewaySleepMs、validationTimeoutMs、finalPromotedReconnectTimeoutMs、finalPromotedHeartbeatTimeoutMs、promoteOnlyAfterCandidateValidation、candidateValidationIsNotTerminalSuccess、finalPromotedComposeHeartbeatRequired、candidateUsesOldContainerEnvironment、candidateUsesOldContainerMounts、candidateUsesOldContainerNetworks、candidateUsesOldContainerExtraHosts 和 candidateUsesHostPidNamespace,并且必须在 plan 中显示指定 Provider 的当前/目标 gateway 版本号,便于前端和 CLI 判断这不是旧的先删旧容器再 up 的危险流程。
provider.upgrade 的终态 result 必须包含最终 Compose 容器的 containerId、version、restartPolicy、pidMode 和 heartbeatTimestamp;字段不可得时必须给出稳定的 *UnavailableReason,不能省略或用空字符串伪装成功。若 backend-core 在窗口内只观察到 candidate 容器 heartbeat、旧版本 heartbeat、离线状态或缺失 heartbeat,任务必须 failed,reason 使用结构化 blocker 语义,并包含 last-known-good/rollback 语义,提示以旧容器恢复或回滚为优先处置。
远程更新记录的权威来源是 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。
Provider Triage
bun scripts/cli.ts provider triage <PROVIDER_ID> 是 provider 运行状态的只读多信号裁决入口。输出必须包含 decision、retryable、healthyScopes、failedScopes、degradedScopes、blockingDisposition、rationale、signals 和 recommendedCrossChecks。decision 的长期语义是:global-offline 表示 provider heartbeat、Host SSH、k3s 或 scheduler 等多个独立关键面同时失败且没有健康交叉证据;service-degraded 表示 registry、service proxy 或单个用户服务局部退化但仍存在 provider 级健康信号;retryable-transient 表示单次 runner-local、SSH、proxy 或 API timeout 证据不足,应重试或补交叉验证;healthy 表示未观察到失败或退化信号。
recommendedCrossChecks 必须保留 argv 形态的 Host SSH 自检:trans <PROVIDER_ID> argv true。这条命令用于证明非交互维护桥仍可用;如果自由 ssh-like 形态出现 timeout、kex_exchange_identification 或 Connection closed by remote host,应先按 CLI 输出的 UNIDESK_SSH_HINT 改用 trans D601 argv bash -lc '<command>' 复测,再结合 provider triage 判断是否真是 provider 级故障。
D601 这类长期 WSL provider 不得因为单一路径失败被直接写成全局离线。典型局部退化包括 artifact registry 的 unidesk-artifact-registry.service inactive,但 registry container 仍 running、listener 仍绑定 loopback、http://127.0.0.1:5000/v2/ 返回 200;这种状态应在 registry scope 内显示 degraded,并在 provider triage 中落到 decision=service-degraded,只提示修复 systemd drift,不阻断所有 D601 上的 Code Queue、k3sctl-adapter 或业务 API 判断。
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-<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 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 <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 位置先用 trans <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 维护桥当成调度通道。
面向人的终端入口是 trans <PROVIDER_ID> [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 特性时使用 trans D601 argv bash -lc '<command>'。WSL 节点需要同时看清 Linux/WSL 与 Windows 两套 skill 时,使用 trans <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;随后运行 trans <PROVIDER_ID> argv true 验证非交互 argv 维护命令,再运行 trans <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> argv true 和 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> argv true 与 ssh <PROVIDER_ID> hostname 必须 exit code 为 0。如果 D518 这类 WSL 节点没有公网 SSH 入口,也必须通过这个 provider-gateway 自连维护桥完成验证,而不是要求主 server 直接连节点公网 22 端口;旧版 provider 未声明 host.ssh 时必须先升级 provider-gateway,否则 core 会拒绝 SSH 透传。