Files
pikasTech-unidesk/docs/reference/provider-gateway.md
T
2026-05-17 16:30:02 +00:00

46 KiB
Raw Blame History

Provider Gateway Reference

Provider Gateway 是计算节点侧容器。它只主动连出到主 server 暴露的 provider ingress WebSocket,不要求计算节点有公网 IP,适合 NAT、内网和防火墙后的机器。

Main Server Self Provider

当前主 server 也运行一个 provider-gatewayproviderId 固定来自 config.jsonproviderGateway.id。这让单机环境也能验证完整的分布式调度闭环:frontend 发起任务,core 写数据库并通过 provider ingress WebSocket 下发,provider gateway 执行后回传状态。

Upgrade Safety Gate

计算节点 provider-gateway 容器的重建和升级权威路径是 provider.upgrademode: "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 容器。最终容器必须重新验证 restart=alwayspid=host,避免候选临时容器命名或端口映射残留成第二条运行路径。候选验证失败时 updater 必须删除候选容器并退出失败,旧 gateway 到达助眠上限后自动重连主 server,形成自动回滚。backend-core 必须在同一 Provider ID 被新 WebSocket 替换后忽略旧 WebSocket 的 close 事件,避免候选已上线后又被旧连接关闭标记为 offline。

禁止通过 UniDesk 自己的 Host SSH / WSL SSH 透传同步执行 docker compose up -d --build provider-gatewaydocker compose restart provider-gatewaydocker rm -f <provider-gateway> 后再启动等自重建命令。原因是这条 SSH 透传连接正由被重建的旧 provider-gateway 容器承载;旧容器停止后会切断控制通道,可能把节点留在旧容器已停、新容器未起的不可达状态。SSH 透传只允许用于诊断、修复升级前置条件、查看本地状态和升级后验证,不允许作为计算节点 provider-gateway 正式重建/升级通道。

只有旧节点尚不支持 provider.upgrade mode=schedule 时,才允许使用节点本地终端、节点自有 Web terminal、systemd、计划任务或 detached shell 做一次 bootstrapbootstrap 完成后必须立刻回到 provider.upgrade mode=schedule 做真实升级验证,并通过公网 frontend 或 remote CLI 确认节点重新上线、远程更新可用性和 SSH 透传可用性。

Deployment Method

当前主 server 公网 IP 是 74.48.78.17config.json 中的 network.publicHost 必须保持为该地址;公网 frontend 入口是 http://74.48.78.17:18081/provider gateway 对外接入入口是 ws://74.48.78.17:18082/ws/providerprovider ingress 健康检查是 http://74.48.78.17:18082/health。主 server 本机 provider 由根目录 docker-compose.ymlprovider-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>.envprovider-<ID>.ymlenv 文件默认只保留 UNIDESK_MASTER_SERVERPROVIDER_ID 两项,Compose 固定 restart: alwayspid: "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_PATHMONITOR_DISK_PATHPROVIDER_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 namespaceCompose 写法是 pid: "host"docker run 写法是 --pid host。缺少该配置时只能看到 provider 容器命名空间内的进程,不能视为完整节点资源监控。provider-gateway 容器必须使用 Docker restart policy alwaysunless-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_HOSTHOST_SSH_PORTHOST_SSH_USERHOST_SSH_KEYHOST_REMOTE_CWD 显式覆盖。

Mandatory SSH Passthrough Bundle

新增 provider-gateway 或 legacy bootstrap 后,Docker socket、远程升级和 SSH 透传必须作为同一个部署包验收,不能只让 provider 在线就认为完成。节点侧应先启动目标宿主或 WSL 的 sshd,把维护公钥写入目标用户 authorized_keys,再用只读目录挂载私钥到 provider-gateway 容器内 /run/host-sshprovider-gateway 环境变量必须包含 HOST_SSH_HOSTHOST_SSH_PORTHOST_SSH_USERHOST_SSH_KEYHOST_REMOTE_CWD。注册成功后,主 server 看到的 labels 必须同时满足 hostSshConfigured=truehostSshKeyPresent=truehostSshTarget 指向目标 sshd,且 unideskCapabilities 包含 host.ssh

计算节点镜像必须包含 ssh 客户端,否则 provider 虽然可以在线并执行 Docker 任务,但 host.ssh 调度会在节点侧失败。标准 src/components/provider-gateway/Dockerfile 已安装 openssh-client;如果节点使用本地兜底镜像或复制 Bun/Docker CLI 的自定义镜像,也必须同时安装或复制可用的 OpenSSH client,并在容器内通过 ssh -Vtest -r /run/host-ssh/id_ed25519ssh -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=D601PROVIDER_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>.ymldocker run --env-file 读取。新挂载默认只写 UNIDESK_MASTER_SERVERPROVIDER_ID;如果需要覆盖 labelsPROVIDER_LABELS_JSON 在 Docker env-file 中可以写成单行 JSON,临时用 shell source 调试时必须对整段 JSON 加引号,否则 shell 会按 {} 和逗号拆分导致 JSON 解析失败。不写 labels 时 provider-gateway 会根据 Provider ID、Docker、WSL 内核和 /etc/os-release 自动生成 hostroledockerwsldistroattachMode=simple,并在运行时追加 runtimedockerSocketPresent、gateway 版本和 gatewayUptimeSeconds.state/provider-<ID>.envprovider-<ID>.ymllogs/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 runningD601/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 中上报 providerGatewayRestartPolicyOkproviderGatewayPidModeOkproviderGatewayRuntimeGuardOk;这只是最后一道自愈,不能替代 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-D601restart=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_proxyhttps_proxyHTTP_PROXYHTTPS_PROXYall_proxyALL_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:7890netsh 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 本地 bundockerdocker-compose plugin 到镜像内,再复制 src/components/provider-gateway/srcsrc/components/shared/src。该镜像必须能在容器内执行 bun --versiondocker versiondocker compose versionssh -V,并通过挂载的 /var/run/docker.sock 执行 docker infodocker 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_IDPROVIDER_NAMEonline 状态、lastHeartbeat 和 labels 可见;点击该节点的 查看原始JSON,确认 raw payload 中的 providerIdnamestatuslabels 与部署环境变量一致。随后进入 资源节点 / 资源监控,确认该 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=truehostSshKeyPresent=true 且能力包含 host.ssh;再运行 bun scripts/cli.ts debug dispatch <PROVIDER_ID> host.ssh --wait-ms 15000,任务必须 succeededresult 中 probeLine 必须包含 UNIDESK_SSH_TESTexitCode=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 还应声明 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 allowedPathPrefixesallowedMethods 限制。无论走 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 下发 echodocker.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-gatewayprovider 标签会报告 dockerSocketPresentdocker.ps 调试任务会通过该 socket 查询宿主 Docker 容器。

User Service HTTP Proxy

microservice.http 是 provider-gateway 给 deployment.mode=unidesk-direct 用户服务使用的私有后端访问能力。新 provider 必须同时声明 microservice.http.tunnelbackend-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 支持 GETHEADPOSTPUTPATCHDELETE,但最终允许方法必须由每个用户服务的 backend.allowedMethods 显式配置。provider-gateway 只允许访问 http://127.0.0.1http://localhosthttp://host.docker.internal 这些节点本地地址;主 server 内置 Todo Note 后端可使用 Compose 服务名 http://todo-note:4211deployment.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:失败响应至少包含 requestIdproviderIdserviceIdstagefailureReason 或 provider result,并带 x-unidesk-request-idx-unidesk-tunnel-errorGET/HEAD 非 stream 请求允许短超时分层重试;POSTPATCHPUTDELETE 这类可能产生副作用的请求不得自动重复。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_openegress_tcp_dataegress_tcp_close 消息;backend-core 在主 server 侧建立真实 TCP 连接并把数据回传,避免 D601 等计算节点本地网络不可达时卡死 Codex/Git/NPM/apt/Playwright。

该能力属于 provider-gateway 通道能力,register/heartbeat 的 unideskCapabilities 必须包含 network.egress-proxylabels 必须上报 providerGatewayEgressProxy* 状态。不得再为某个用户服务单独注册伪 provider 来实现出网代理;否则节点列表会出现虚假 provider,且代理、统计、升级路径会形成多套通道。代理健康检查使用 GET /__unidesk/egress-proxy/health,返回 connectedproviderIdactiveTunnelspendingTunnelsoldestTunnelAgeMsopenTimeoutMsidleTimeoutMs 和监听端口;业务服务自己的 /health 应把该结果作为排障证据透出。

egress proxy 的长期边界是“统一 provider 通道,不引入第二控制面”。backend-core 只接受在线 provider socket 上的 egress_tcp_* 消息,并在该 socket 关闭时销毁全部对应 TCP relayprovider-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 Kubernetes Service 连接 D601 provider-gateway egress endpoint,这是 Pod 内的出网入口,不是业务 HTTP 代理入口,也不能替代 Kubernetes API service proxy。部署构建同样不得新建 SSH SOCKS、公网 master proxy 或宿主全局代理;构建脚本只能把 provider-gateway WS egress 作为短生命周期环境变量和 Docker build-arg 注入,并配合目标节点本地 BuildKit/image cache 避免重复下载大依赖层。

egress tunnel 必须有生命周期边界:provider-gateway 发出 egress_tcp_open 后如果主 server 未在 openTimeoutMs 内返回 egress_tcp_opened 或 close,必须主动关闭本地 client 并向 core 发送 egress_tcp_closeprovider-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_PROXYhyueapi.com,.hyueapi.com,api.minimaxi.com,.minimaxi.com 配成直连,其它模型 API 仍不得默认绕过 proxy。验收必须同时证明 provider-gateway labels、业务服务 /health 和执行容器内 curl -I https://... 都走同一 proxy pathhyueapi/MiniMax 例外则以 Code Queue /health.egressProxy.noProxy、MiniMax judge 探测和目标任务成功完成作为证据。

Gateway Version Metadata

provider-gateway 必须从自身 package.json 读取版本号,并在 register 与 heartbeat labels 中上报 providerGatewayNameproviderGatewayVersionproviderGatewayStartedAtproviderGatewayUpgradePolicy。backend-core 将这些 labels 合并到 unidesk_nodes.labels,frontend 在节点清单、资源监控和 资源节点 / 网关版本 中展示;旧节点缺少这些字段时只能显示版本未知,不能用猜测值替代。unideskCapabilitieshostSshConfiguredhostSshKeyPresenthostSshTarget 也是 WebUI 运维可用性徽标的数据源,用于直接显示每个计算节点的 SSH 透传可用性与远程更新可用性。

src/components/provider-gateway 下任何代码或行为变更都必须在同一变更集中递增 src/components/provider-gateway/package.jsonversion,不得只改实现而沿用旧版本号。验收时必须通过公网 frontend 的 资源节点 / 网关版本bun scripts/cli.ts debug health 确认目标 provider 的 providerGatewayVersion 已上报新版本;如果线上节点仍显示旧版本,该 provider-gateway 变更不能视为交付完成。

任何远程升级预检、执行升级和远程更新记录都必须显式显示指定 Provider 的 gateway 版本号。provider.upgrade result 的 plan 中必须包含 providerIdproviderName、当前运行中的 providerGatewayVersion 和从升级 workspace 的 src/components/provider-gateway/package.json 读取到的 targetProviderGatewayVersionfrontend 的升级控件与 资源节点 / 网关版本 远程更新记录必须把该版本渲染为结构化字段,不能只把版本埋在原始 JSON 中。

Docker Status Telemetry

provider-gateway 连接成功后必须周期性上报 Docker daemon 状态,数据来源是本地 Docker socket 上的 docker infodocker ps -adocker imagesdocker volume lsdocker network ls。backend-core 将最新快照保存到 unidesk_node_docker_statusfrontend 的资源节点 Docker 状态 子标签用该快照渲染 Docker Desktop 风格视图;该能力仍然只通过 provider 主动上报,不要求主 server 反向连接计算节点。

System Status Telemetry

provider-gateway 连接成功后必须周期性上报节点 CPU、内存、硬盘和进程资源占用。整体采集来源是节点本地 /proc/stat/proc/loadavg/proc/meminfodf -PB1,进程表来源是 /proc/[pid]/stat/proc/[pid]/status/proc/[pid]/cmdline/proc/[pid]/iobackend-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]/ioread_bytes/write_bytes 差值。该链路仍然由 provider 主动上报,主 server 不反向探测计算节点。

Remote Provider Upgrade

backend-core 可以通过真实 WebSocket 调度向在线 provider 下发 provider.upgrademode: "plan" 只返回升级计划,用于 E2E 和人工预检;mode: "schedule" 会要求 provider-gateway 通过本地 Docker socket 启动一个 detached updater 容器。updater 的固定策略是先执行 docker compose build provider-gateway,构建成功后只按 com.docker.compose.projectcom.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=scheduledocker compose build 前必须从只读 /workspace/src/components/provider-gateway/workspace/src/components/shared 刷新该 context,并把宿主 .state 以可写方式只挂给 detached updater。验收不能只看 updater 日志中的 Builtcandidate provider-gateway validated and promoted,必须以主 server 可观测的 providerGatewayVersionunideskCapabilities 和目标能力字段为准;如果线上仍上报旧版本,说明运行镜像没有真正更新,必须先修复构建 context 再重跑标准升级。

远程升级策略固定为 always-enabled:只要 provider-gateway 在线并声明 provider.upgrademode: "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 必须包含 oldGatewaySleepMsvalidationTimeoutMspromoteOnlyAfterCandidateValidationcandidateRestartPolicyAfterPromotion: "always"candidateUsesOldContainerEnvironmentcandidateUsesOldContainerMountscandidateUsesOldContainerNetworkscandidateUsesOldContainerExtraHostscandidateUsesHostPidNamespace,并且必须在 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 仓库,确认 GitHub 访问走本机 provider-gateway WS egress proxy,例如 git config --local http.proxy http://127.0.0.1:18789git 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/providerPROVIDER_ID=<ID>PROVIDER_NAME=<ID>PROVIDER_TOKENPROVIDER_LABELS_JSONPROVIDER_UPGRADE_HOST_PROJECT_ROOT=/home/ubuntu/unideskPROVIDER_UPGRADE_WORKSPACE_PATH=/workspacePROVIDER_UPGRADE_COMPOSE_FILEPROVIDER_UPGRADE_ENV_FILEPROVIDER_UPGRADE_COMPOSE_PROJECTPROVIDER_UPGRADE_SERVICE=provider-gatewayPROVIDER_UPGRADE_RUNNER_IMAGE=unidesk_provider-gateway:<id>DOCKER_SOCKET_PATH=/var/run/docker.sockMONITOR_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 -vdocker 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 15000bun 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=yesStrictHostKeyChecking=accept-newServerAliveInterval=20ServerAliveCountMax=3。TTY 策略必须按会话类型区分:无远端命令的交互登录 shell 使用 ssh -tt,带远端命令的会话使用 ssh -T,避免脚本 stdin 被伪终端回显或把 Ctrl-D 当成业务输入。主 server Compose 会把 config.jsonsshForwarding.keyDir 只读挂载为 /run/host-sshprovider 标签会上报 hostSshConfiguredhostSshKeyPresenthostSshTarget,便于在前端节点清单确认维护桥是否具备条件。

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.exewin-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: succeededprobeLineUNIDESK_SSH_TESTexitCode: 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 healthbun scripts/cli.ts --main-server-ip 74.48.78.17 debug dispatch <PROVIDER_ID> host.ssh --wait-ms 15000bun scripts/cli.ts --main-server-ip 74.48.78.17 ssh <PROVIDER_ID> hostname;默认 remote CLI 走公网 frontend 登录态,不需要主 server SSH key。健康检查必须能看到该 Provider 在线、hostSshConfigured=truehostSshKeyPresent=truehostSshTarget 正确、unideskCapabilities 包含 host.sshprobe 必须返回 UNIDESK_SSH_TESTssh <PROVIDER_ID> hostname 必须输出目标 WSL/宿主 hostname 且 exit code 为 0。如果 D518 这类 WSL 节点没有公网 SSH 入口,也必须通过这个 provider-gateway 自连维护桥完成验证,而不是要求主 server 直接连节点公网 22 端口;旧版 provider 未声明 host.ssh 时必须先升级 provider-gateway,否则 core 会拒绝 SSH 透传。