diff --git a/extensions/qa-lab/src/gateway-child.ts b/extensions/qa-lab/src/gateway-child.ts index 19a62f3e8e8..0243ca58256 100644 --- a/extensions/qa-lab/src/gateway-child.ts +++ b/extensions/qa-lab/src/gateway-child.ts @@ -54,6 +54,13 @@ export type QaGatewayChildStateMutationContext = { tempRoot: string; }; +export type QaGatewayChildCommand = { + executablePath: string; + argsPrefix?: string[]; + cwd?: string; + usePackagedPlugins?: boolean; +}; + async function getFreePort() { return await new Promise((resolve, reject) => { const server = net.createServer(); @@ -435,6 +442,7 @@ export function resolveQaControlUiRoot(params: { repoRoot: string; controlUiEnab export async function startQaGatewayChild(params: { repoRoot: string; + command?: QaGatewayChildCommand; providerBaseUrl?: string; transport: Pick; transportBaseUrl: string; @@ -455,6 +463,10 @@ export async function startQaGatewayChild(params: { ); const runtimeCwd = tempRoot; const distEntryPath = path.join(params.repoRoot, "dist", "index.js"); + const gatewayCommand = params.command; + const gatewayExecutablePath = gatewayCommand?.executablePath; + const gatewayArgsPrefix = gatewayCommand?.argsPrefix ?? []; + const gatewayCwd = gatewayCommand?.cwd ?? runtimeCwd; const workspaceDir = path.join(tempRoot, "workspace"); const stateDir = path.join(tempRoot, "state"); const homeDir = path.join(tempRoot, "home"); @@ -558,7 +570,17 @@ export async function startQaGatewayChild(params: { let env: NodeJS.ProcessEnv | null = null; try { - const nodeExecPath = await resolveQaNodeExecPath(); + const nodeExecPath = gatewayExecutablePath ?? (await resolveQaNodeExecPath()); + const buildGatewayArgs = () => [ + ...(gatewayExecutablePath ? gatewayArgsPrefix : [distEntryPath, ...gatewayArgsPrefix]), + "gateway", + "run", + "--port", + String(gatewayPort), + "--bind", + "loopback", + "--allow-unconfigured", + ]; for (let attempt = 1; attempt <= QA_GATEWAY_CHILD_STARTUP_MAX_ATTEMPTS; attempt += 1) { gatewayPort = await getFreePort(); baseUrl = `http://127.0.0.1:${gatewayPort}`; @@ -574,16 +596,22 @@ export async function startQaGatewayChild(params: { ); }, ); - const { bundledPluginsDir, stagedRoot } = await createQaBundledPluginsDir({ - repoRoot: params.repoRoot, - tempRoot, - allowedPluginIds, - }); - stagedBundledPluginsRoot = stagedRoot; - const runtimeHostVersion = await resolveQaRuntimeHostVersion({ - repoRoot: params.repoRoot, - allowedPluginIds, - }); + const stagedPluginRuntime = gatewayCommand?.usePackagedPlugins + ? { bundledPluginsDir: undefined, runtimeHostVersion: undefined } + : { + ...(await createQaBundledPluginsDir({ + repoRoot: params.repoRoot, + tempRoot, + allowedPluginIds, + })), + runtimeHostVersion: await resolveQaRuntimeHostVersion({ + repoRoot: params.repoRoot, + allowedPluginIds, + }), + }; + if ("stagedRoot" in stagedPluginRuntime) { + stagedBundledPluginsRoot = stagedPluginRuntime.stagedRoot; + } env = buildQaRuntimeEnv({ configPath, gatewayToken, @@ -593,8 +621,8 @@ export async function startQaGatewayChild(params: { xdgConfigHome, xdgDataHome, xdgCacheHome, - bundledPluginsDir, - compatibilityHostVersion: runtimeHostVersion, + bundledPluginsDir: stagedPluginRuntime.bundledPluginsDir, + compatibilityHostVersion: stagedPluginRuntime.runtimeHostVersion, providerMode, forwardHostHomeForClaudeCli: liveProviderIds.includes("claude-cli"), claudeCliAuthMode: params.claudeCliAuthMode, @@ -608,25 +636,12 @@ export async function startQaGatewayChild(params: { throw new Error("qa gateway runtime env not initialized"); } - const attemptChild = spawn( - nodeExecPath, - [ - distEntryPath, - "gateway", - "run", - "--port", - String(gatewayPort), - "--bind", - "loopback", - "--allow-unconfigured", - ], - { - cwd: runtimeCwd, - env, - detached: process.platform !== "win32", - stdio: ["ignore", "pipe", "pipe"], - }, - ); + const attemptChild = spawn(nodeExecPath, buildGatewayArgs(), { + cwd: gatewayCwd, + env, + detached: process.platform !== "win32", + stdio: ["ignore", "pipe", "pipe"], + }); attemptChild.stdout.on("data", (chunk) => { const buffer = Buffer.from(chunk); stdout.push(buffer); @@ -714,25 +729,12 @@ export async function startQaGatewayChild(params: { const runningEnv = env; const spawnReplacementGatewayChild = async () => { - const nextChild = spawn( - nodeExecPath, - [ - distEntryPath, - "gateway", - "run", - "--port", - String(gatewayPort), - "--bind", - "loopback", - "--allow-unconfigured", - ], - { - cwd: runtimeCwd, - env: runningEnv, - detached: process.platform !== "win32", - stdio: ["ignore", "pipe", "pipe"], - }, - ); + const nextChild = spawn(nodeExecPath, buildGatewayArgs(), { + cwd: gatewayCwd, + env: runningEnv, + detached: process.platform !== "win32", + stdio: ["ignore", "pipe", "pipe"], + }); nextChild.stdout.on("data", (chunk) => { const buffer = Buffer.from(chunk); stdout.push(buffer); diff --git a/extensions/qa-lab/src/live-transports/shared/live-gateway.runtime.ts b/extensions/qa-lab/src/live-transports/shared/live-gateway.runtime.ts index 3dd23511fda..7fca2761da7 100644 --- a/extensions/qa-lab/src/live-transports/shared/live-gateway.runtime.ts +++ b/extensions/qa-lab/src/live-transports/shared/live-gateway.runtime.ts @@ -1,5 +1,9 @@ import type { OpenClawConfig } from "openclaw/plugin-sdk/config-runtime"; -import { startQaGatewayChild, type QaCliBackendAuthMode } from "../../gateway-child.js"; +import { + startQaGatewayChild, + type QaCliBackendAuthMode, + type QaGatewayChildCommand, +} from "../../gateway-child.js"; import type { QaProviderMode } from "../../model-selection.js"; import { startQaProviderServer } from "../../providers/server-runtime.js"; import type { QaThinkingLevel } from "../../qa-gateway-config.js"; @@ -32,6 +36,7 @@ async function stopQaLiveLaneResources( export async function startQaLiveLaneGateway(params: { repoRoot: string; + command?: QaGatewayChildCommand; transport: { requiredPluginIds: readonly string[]; createGatewayConfig: (params: { @@ -53,6 +58,7 @@ export async function startQaLiveLaneGateway(params: { try { const gateway = await startQaGatewayChild({ repoRoot: params.repoRoot, + command: params.command, providerBaseUrl: mock ? `${mock.baseUrl}/v1` : undefined, transport: params.transport, transportBaseUrl: params.transportBaseUrl, diff --git a/extensions/qa-lab/src/live-transports/telegram/telegram-live.runtime.ts b/extensions/qa-lab/src/live-transports/telegram/telegram-live.runtime.ts index da2524c2dc6..9c25e20a956 100644 --- a/extensions/qa-lab/src/live-transports/telegram/telegram-live.runtime.ts +++ b/extensions/qa-lab/src/live-transports/telegram/telegram-live.runtime.ts @@ -1,6 +1,8 @@ +import { execFile } from "node:child_process"; import { randomUUID } from "node:crypto"; import fs from "node:fs/promises"; import path from "node:path"; +import { promisify } from "node:util"; import type { OpenClawConfig } from "openclaw/plugin-sdk/config-runtime"; import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime"; import { fetchWithSsrFGuard } from "openclaw/plugin-sdk/ssrf-runtime"; @@ -306,6 +308,7 @@ const QA_REDACT_PUBLIC_METADATA_ENV = "OPENCLAW_QA_REDACT_PUBLIC_METADATA"; const QA_SUITE_PROGRESS_ENV = "OPENCLAW_QA_SUITE_PROGRESS"; const TELEGRAM_QA_PROGRESS_DETAIL_LIMIT = 240; const TELEGRAM_QA_PROGRESS_PREFIX = "[qa-telegram-live]"; +const execFileAsync = promisify(execFile); const telegramQaCredentialPayloadSchema = z.object({ groupId: z.string().trim().min(1), @@ -962,9 +965,63 @@ function canaryFailureMessage(params: { ].join("\n"); } +async function runInstalledOpenClawTelegramOnboardingPreflight(params: { + openClawCommand: string; + sutToken: string; +}) { + const tempRoot = await fs.mkdtemp(path.join(process.cwd(), ".tmp-openclaw-npm-telegram-")); + const homeDir = path.join(tempRoot, "home"); + const stateDir = path.join(homeDir, ".openclaw"); + await fs.mkdir(stateDir, { recursive: true }); + const env = { + ...process.env, + HOME: homeDir, + OPENCLAW_HOME: stateDir, + OPENCLAW_CONFIG_PATH: path.join(stateDir, "openclaw.json"), + OPENCLAW_STATE_DIR: stateDir, + OPENCLAW_GATEWAY_TOKEN: "npm-telegram-live-onboard", + }; + try { + await execFileAsync( + params.openClawCommand, + [ + "onboard", + "--non-interactive", + "--accept-risk", + "--mode", + "local", + "--auth-choice", + "openai-api-key", + "--secret-input-mode", + "ref", + "--gateway-port", + "18789", + "--gateway-bind", + "loopback", + "--skip-daemon", + "--skip-ui", + "--skip-skills", + "--skip-health", + "--json", + ], + { env }, + ); + await execFileAsync( + params.openClawCommand, + ["channels", "add", "--channel", "telegram", "--token", params.sutToken], + { env }, + ); + await execFileAsync(params.openClawCommand, ["doctor", "--non-interactive"], { env }); + } finally { + await fs.rm(tempRoot, { recursive: true, force: true }).catch(() => {}); + } +} + export async function runTelegramQaLive(params: { repoRoot?: string; outputDir?: string; + sutOpenClawCommand?: string; + preflightInstalledOnboarding?: boolean; providerMode?: QaProviderModeInput; primaryModel?: string; alternateModel?: string; @@ -1022,6 +1079,15 @@ export async function runTelegramQaLive(params: { const cleanupIssues: string[] = []; let canaryFailure: string | null = null; try { + if (params.sutOpenClawCommand && params.preflightInstalledOnboarding === true) { + writeTelegramQaProgress(progressEnabled, "installed package onboarding preflight start"); + await runInstalledOpenClawTelegramOnboardingPreflight({ + openClawCommand: params.sutOpenClawCommand, + sutToken: runtimeEnv.sutToken, + }); + writeTelegramQaProgress(progressEnabled, "installed package onboarding preflight pass"); + } + const driverIdentity = await getBotIdentity(runtimeEnv.driverToken); const sutIdentity = await getBotIdentity(runtimeEnv.sutToken); const sutUsername = sutIdentity.username?.trim(); @@ -1040,6 +1106,12 @@ export async function runTelegramQaLive(params: { const gatewayHarness = await startQaLiveLaneGateway({ repoRoot, + command: params.sutOpenClawCommand + ? { + executablePath: params.sutOpenClawCommand, + usePackagedPlugins: true, + } + : undefined, transport: { requiredPluginIds: [], createGatewayConfig: () => ({}),