fix: run cli core probes via frontend container

This commit is contained in:
Codex
2026-05-18 03:45:29 +00:00
parent c514b6ca9e
commit 54c32be901
4 changed files with 14 additions and 12 deletions
+2 -2
View File
@@ -58,11 +58,11 @@ UniDesk 的统一 CLI 入口是根目录 `scripts/cli.ts`,运行方式固定
## Debug Contract
`debug` 子命令必须复用真实模块与真实端点,禁止维护平行实现。`debug health` 会摘要展示 `/api/nodes/system-status``/api/nodes/docker-status`,避免输出完整快照造成信息爆炸。`debug dispatch` backend-core 容器内调用内部 `/api/dispatch`core 再通过 WebSocket 将 `docker.ps``provider.upgrade``host.ssh``microservice.http``echo` 任务下发给 provider gateway,因此它可以验证核心调度闭环,同时不需要公开 core REST API。`provider.upgrade` 默认使用 `mode: "plan"` 预检;需要验证一键升级时必须显式加 `--mode schedule`,并通过 `--wait-ms``debug task` 确认任务进入 `succeeded`、result 中包含 updater 容器信息和 `policy: "always-enabled"``host.ssh` 默认使用 `mode: "probe"` 做短超时维护桥自检;需要执行明确命令时使用 `--ssh-command` 进入 `mode: "exec"`,并配合 `--wait-ms``debug task` 查看 stdout、stderr、exitCode 与 probeLine。`microservice.http` 只用于开发调试 provider-gateway 私有 HTTP 代理,正式用户入口应使用 `microservice` CLI 或 frontend 的用户服务页面。
`debug` 子命令必须复用真实模块与真实端点,禁止维护平行实现。`debug health` 会摘要展示 `/api/nodes/system-status``/api/nodes/docker-status`,避免输出完整快照造成信息爆炸。`debug dispatch`通过 frontend 容器的 Docker 内网访问 backend-core `/api/dispatch`core 再通过 WebSocket 将 `docker.ps``provider.upgrade``host.ssh``microservice.http``echo` 任务下发给 provider gateway,因此它可以验证核心调度闭环,同时不需要公开 core REST API,也不要求 backend-core 运行镜像携带 Bun CLI`provider.upgrade` 默认使用 `mode: "plan"` 预检;需要验证一键升级时必须显式加 `--mode schedule`,并通过 `--wait-ms``debug task` 确认任务进入 `succeeded`、result 中包含 updater 容器信息和 `policy: "always-enabled"``host.ssh` 默认使用 `mode: "probe"` 做短超时维护桥自检;需要执行明确命令时使用 `--ssh-command` 进入 `mode: "exec"`,并配合 `--wait-ms``debug task` 查看 stdout、stderr、exitCode 与 probeLine。`microservice.http` 只用于开发调试 provider-gateway 私有 HTTP 代理,正式用户入口应使用 `microservice` CLI 或 frontend 的用户服务页面。
## SSH Command
`ssh <providerId> [ssh-like args...]` 是面向人的终端透传入口,不包装 JSON 输出。CLI 会在宿主机启动一个 `docker exec -i unidesk-backend-core bun -e ...` brokerbroker 只连接 backend-core 的 Docker 内网 `/ws/ssh`core 再把 stdin/stdout/stderr 流量通过目标 provider 的既有 WebSocket 转发到 provider-gatewayprovider-gateway 最终执行维护用 SSH 连接宿主或 WSL sshd。TTY 策略固定为交互登录 shell 使用 `ssh -tt`,带远端命令的会话使用 `ssh -T`;脚本 stdin、`apply-patch``py` 这类命令模式不得被伪终端回显或注入控制字符。该入口不新增 core 公网端口,不暴露 database,也不改变 frontend/provider ingress 之外的公网边界。
`ssh <providerId> [ssh-like args...]` 是面向人的终端透传入口,不包装 JSON 输出。CLI 会在宿主机启动一个 `docker exec -i unidesk-frontend bun -e ...` brokerbroker 只连接 backend-core 的 Docker 内网 `/ws/ssh`core 再把 stdin/stdout/stderr 流量通过目标 provider 的既有 WebSocket 转发到 provider-gatewayprovider-gateway 最终执行维护用 SSH 连接宿主或 WSL sshd。TTY 策略固定为交互登录 shell 使用 `ssh -tt`,带远端命令的会话使用 `ssh -T`;脚本 stdin、`apply-patch``py` 这类命令模式不得被伪终端回显或注入控制字符。该入口不新增 core 公网端口,不暴露 database,也不改变 frontend/provider ingress 之外的公网边界。
`bun scripts/cli.ts ssh D518` 应表现为登录 D518 WSL 的 shell`bun scripts/cli.ts ssh D518 hostname` 应像 `ssh D518 hostname` 一样只输出远端命令结果并返回远端 exit code。Provider ID 前的目标选择由 UniDesk 节点清单决定,`-p``-i``-l``-o` 等传统 ssh 传输参数由 provider-gateway 部署配置统一管理,CLI 会兼容性消费这些参数但不会覆盖节点侧维护桥配置。
+6 -6
View File
@@ -24,7 +24,7 @@ async function readJson(url: string, init?: RequestInit): Promise<unknown> {
function coreInternalFetch(path: string, init?: { method?: string; body?: unknown }): unknown {
const code = `
const res = await fetch(${JSON.stringify(`http://127.0.0.1:8080${path}`)}, ${JSON.stringify({
const res = await fetch(${JSON.stringify(`http://backend-core:8080${path}`)}, ${JSON.stringify({
method: init?.method ?? "GET",
headers: init?.body === undefined ? undefined : { "content-type": "application/json" },
body: init?.body === undefined ? undefined : JSON.stringify(init.body),
@@ -34,7 +34,7 @@ function coreInternalFetch(path: string, init?: { method?: string; body?: unknow
try { body = text ? JSON.parse(text) : null; } catch { body = { text }; }
console.log(JSON.stringify({ ok: res.ok, status: res.status, body }));
`;
const result = runCommand(["docker", "exec", "unidesk-backend-core", "bun", "-e", code], repoRoot);
const result = runCommand(["docker", "exec", "unidesk-frontend", "bun", "-e", code], repoRoot);
if (result.exitCode !== 0) {
return { ok: false, exitCode: result.exitCode, stdoutTail: result.stdout.slice(-1200), stderrTail: result.stderr.slice(-1200) };
}
@@ -47,7 +47,7 @@ function coreInternalFetch(path: string, init?: { method?: string; body?: unknow
function coreDockerStatusSummary(): unknown {
const code = `
const res = await fetch('http://127.0.0.1:8080/api/nodes/docker-status');
const res = await fetch('http://backend-core:8080/api/nodes/docker-status');
const text = await res.text();
let body = null;
try { body = text ? JSON.parse(text) : null; } catch { body = { text }; }
@@ -77,7 +77,7 @@ function coreDockerStatusSummary(): unknown {
});
console.log(JSON.stringify({ ok: res.ok, status: res.status, body: { ok: body?.ok === true, dockerStatuses } }));
`;
const result = runCommand(["docker", "exec", "unidesk-backend-core", "bun", "-e", code], repoRoot);
const result = runCommand(["docker", "exec", "unidesk-frontend", "bun", "-e", code], repoRoot);
if (result.exitCode !== 0) {
return { ok: false, exitCode: result.exitCode, stdoutTail: result.stdout.slice(-1200), stderrTail: result.stderr.slice(-1200) };
}
@@ -90,7 +90,7 @@ function coreDockerStatusSummary(): unknown {
function coreSystemStatusSummary(): unknown {
const code = `
const res = await fetch('http://127.0.0.1:8080/api/nodes/system-status?limit=24');
const res = await fetch('http://backend-core:8080/api/nodes/system-status?limit=24');
const text = await res.text();
let body = null;
try { body = text ? JSON.parse(text) : null; } catch { body = { text }; }
@@ -114,7 +114,7 @@ function coreSystemStatusSummary(): unknown {
});
console.log(JSON.stringify({ ok: res.ok, status: res.status, body: { ok: body?.ok === true, systemStatuses } }));
`;
const result = runCommand(["docker", "exec", "unidesk-backend-core", "bun", "-e", code], repoRoot);
const result = runCommand(["docker", "exec", "unidesk-frontend", "bun", "-e", code], repoRoot);
if (result.exitCode !== 0) {
return { ok: false, exitCode: result.exitCode, stdoutTail: result.stdout.slice(-1200), stderrTail: result.stderr.slice(-1200) };
}
+2 -2
View File
@@ -7,7 +7,7 @@ export function coreInternalFetch(path: string, init?: { method?: string; body?:
if (!path.startsWith("/")) throw new Error("core internal path must start with /");
const maxResponseBytes = Math.max(1024, Math.floor(init?.maxResponseBytes ?? 5_000_000));
const code = `
const res = await fetch(${JSON.stringify(`http://127.0.0.1:8080${path}`)}, ${JSON.stringify({
const res = await fetch(${JSON.stringify(`http://backend-core:8080${path}`)}, ${JSON.stringify({
method: init?.method ?? "GET",
headers: init?.body === undefined ? undefined : { "content-type": "application/json" },
body: init?.body === undefined ? undefined : JSON.stringify(init.body),
@@ -49,7 +49,7 @@ export function coreInternalFetch(path: string, init?: { method?: string; body?:
}
console.log(JSON.stringify({ ok: res.ok, status: res.status, responseTruncated, responseBytesRead: bytes, responseContentLength: res.headers.get("content-length"), body }));
`;
const result = runCommand(["docker", "exec", "unidesk-backend-core", "bun", "-e", code], repoRoot);
const result = runCommand(["docker", "exec", "unidesk-frontend", "bun", "-e", code], repoRoot);
if (result.exitCode !== 0) {
return { ok: false, exitCode: result.exitCode, stdoutTail: result.stdout.slice(-1200), stderrTail: result.stderr.slice(-1200) };
}
+4 -2
View File
@@ -662,7 +662,7 @@ function brokerSource(): string {
return String.raw`
const open = JSON.parse(process.argv[2] || process.argv[1] || "{}");
const token = process.env.PROVIDER_TOKEN || "";
const url = "ws://127.0.0.1:8080/ws/ssh?token=" + encodeURIComponent(token);
const url = "ws://backend-core:8080/ws/ssh?token=" + encodeURIComponent(token);
const ws = new WebSocket(url);
let exitCode = 255;
let canSend = false;
@@ -805,7 +805,9 @@ export async function runSsh(config: UniDeskConfig, providerId: string, args: st
const child = spawn("docker", [
"exec",
"-i",
"unidesk-backend-core",
"-e",
`PROVIDER_TOKEN=${config.providerGateway.token}`,
"unidesk-frontend",
"bun",
"-e",
brokerSource(),