UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

203 lines (202 loc) 8.29 kB
import { m as clampTimerTimeoutMs, w as parseStrictPositiveInteger } from "./number-coercion-CLj0HTDM.js"; import { n as isTruthyEnvValue } from "./env-M3R40TOb.js"; import { n as parseTcpPort } from "./tcp-port-C3gLZtJi.js"; import { n as runExec } from "./exec-BIE-3oLG.js"; import { n as pickPrimaryTailnetIPv4, r as pickPrimaryTailnetIPv6 } from "./tailnet-CVdlmJeO.js"; import { r as getTailnetHostname } from "./tailscale-Boe4Jx-9.js"; import { a as resolveWideAreaDiscoveryDomain, o as writeWideAreaGatewayZone } from "./widearea-dns-rDnHQdmm.js"; import fs from "node:fs"; import path from "node:path"; //#region src/gateway/server-discovery.ts /** Formats the Bonjour instance name while preserving user-provided OpenClaw names. */ function formatBonjourInstanceName(displayName) { const trimmed = displayName.trim(); if (!trimmed) return "OpenClaw"; if (/openclaw/i.test(trimmed)) return trimmed; return `${trimmed} (OpenClaw)`; } /** Resolves the CLI path advertised to Bonjour clients, preferring explicit env config. */ function resolveBonjourCliPath(opts = {}) { const envPath = (opts.env ?? process.env).OPENCLAW_CLI_PATH?.trim(); if (envPath) return envPath; const statSync = opts.statSync ?? fs.statSync; const isFile = (candidate) => { try { return statSync(candidate).isFile(); } catch { return false; } }; const execPath = opts.execPath ?? process.execPath; const execDir = path.dirname(execPath); const siblingCli = path.join(execDir, "openclaw"); if (isFile(siblingCli)) return siblingCli; const argvPath = (opts.argv ?? process.argv)[1]; if (argvPath && isFile(argvPath)) return argvPath; const cwd = opts.cwd ?? process.cwd(); const distCli = path.join(cwd, "dist", "index.js"); if (isFile(distCli)) return distCli; const binCli = path.join(cwd, "bin", "openclaw"); if (isFile(binCli)) return binCli; } /** Resolves a Tailnet DNS hint from env or the local tailscale CLI when enabled. */ async function resolveTailnetDnsHint(opts) { const envRaw = (opts?.env ?? process.env).OPENCLAW_TAILNET_DNS?.trim(); const envValue = envRaw && envRaw.length > 0 ? envRaw.replace(/\.$/, "") : ""; if (envValue) return envValue; if (opts?.enabled === false) return; const exec = opts?.exec ?? ((command, args) => runExec(command, args, { timeoutMs: 1500, maxBuffer: 2e5 })); try { return await getTailnetHostname(exec); } catch { return; } } //#endregion //#region src/gateway/server-discovery-runtime.ts /** One owner replaces local advertisements and keeps wide-area TXT policy in sync. */ async function startGatewayDiscovery(params) { let mode = params.discovery?.mdns?.mode ?? "minimal"; const wideAreaDomain = params.discovery?.wideArea?.domain; let services = params.gatewayDiscoveryServices ?? []; let claim = params.pluginRuntimeClaim; let current; let closed = false; let pending = Promise.resolve(); let cleanup = Promise.resolve(); const isCurrent = (generation) => !closed && current === generation; const stopService = (stop) => { cleanup = cleanup.then(stop).catch((err) => { params.logDiscovery.warn(`gateway discovery stop failed: ${String(err)}`); }); return cleanup; }; const stopGeneration = async (generation) => { for (const stop of generation?.stops.splice(0).toReversed() ?? []) stopService(stop); let drained; do { drained = cleanup; await drained; } while (drained !== cleanup); }; const enqueue = (operation) => { const result = pending.then(operation); pending = result.catch(() => {}); return result; }; const waitForClaim = (generation) => { if (generation.waiting) return; generation.waiting = true; generation.claim.waitForUnblocked().then((accepted) => enqueue(async () => { generation.waiting = false; if (isCurrent(generation)) await (accepted ? advertise(generation) : stopGeneration(generation)); })).catch((err) => params.logDiscovery.warn(`gateway discovery refresh failed: ${String(err)}`)); }; const advertise = async (generation) => { if (!isCurrent(generation)) return; const localEnabled = generation.mode !== "off" && !isTruthyEnvValue(process.env.OPENCLAW_DISABLE_BONJOUR) && !process.env.VITEST; const minimal = generation.mode !== "full"; const tailnetDns = localEnabled || wideAreaDomain?.trim() ? await resolveTailnetDnsHint({ enabled: params.tailscaleMode !== "off" }) : void 0; if (!isCurrent(generation)) return; const context = { machineDisplayName: params.machineDisplayName, gatewayPort: params.port, gatewayTlsEnabled: params.gatewayTls?.enabled ?? false, gatewayTlsFingerprintSha256: params.gatewayTls?.fingerprintSha256, gatewayDirectReachable: params.gatewayDirectReachable === true, sshPort: minimal ? void 0 : parseTcpPort(process.env.OPENCLAW_SSH_PORT) ?? void 0, tailnetDns, cliPath: minimal ? void 0 : resolveBonjourCliPath(), minimal }; if (wideAreaDomain?.trim()) { const domain = resolveWideAreaDiscoveryDomain({ configDomain: wideAreaDomain }); const tailnetIPv4 = pickPrimaryTailnetIPv4(); if (!domain) params.logDiscovery.warn("wide-area discovery was requested without a domain; set discovery.wideArea.domain to enable unicast DNS-SD"); else if (!tailnetIPv4) params.logDiscovery.warn("discovery.wideArea.domain is set, but no Tailscale IPv4 address was found; skipping unicast DNS-SD zone update"); else try { const result = await writeWideAreaGatewayZone({ ...context, domain, displayName: formatBonjourInstanceName(params.machineDisplayName), tailnetIPv4, tailnetIPv6: pickPrimaryTailnetIPv6() ?? void 0 }); params.logDiscovery.info(`wide-area DNS-SD ${result.changed ? "updated" : "unchanged"} (${domain}${result.zonePath})`); } catch (err) { params.logDiscovery.warn(`wide-area discovery update failed: ${String(err)}`); } } const advertiseTimeoutMs = clampTimerTimeoutMs(parseStrictPositiveInteger(process.env.OPENCLAW_GATEWAY_DISCOVERY_ADVERTISE_TIMEOUT_MS?.trim())) ?? 5e3; for (;;) { const drained = cleanup; await drained; if (drained !== cleanup) continue; if (!localEnabled || !isCurrent(generation)) return; if (!generation.claim.isCurrent()) { waitForClaim(generation); return; } const next = generation.services.next(); if (next.done) return; const entry = next.value; let timedOut = false; let timer; const started = (async () => entry.service.advertise(context))().then(async (handle) => { if (handle?.stop) { if (isCurrent(generation)) { generation.stops.push(handle.stop); if (!generation.claim.isCurrent()) waitForClaim(generation); } else await stopService(handle.stop); } if (timedOut) params.logDiscovery.warn(`gateway discovery service completed after startup timeout (${entry.service.id}, plugin=${entry.pluginId})`); }).catch((err) => { params.logDiscovery.warn(`gateway discovery service failed${timedOut ? " after startup timeout" : ""} (${entry.service.id}, plugin=${entry.pluginId}): ${String(err)}`); }); await Promise.race([started, new Promise((resolve) => { timer = setTimeout(() => { timedOut = true; params.logDiscovery.warn(`gateway discovery service timed out after ${advertiseTimeoutMs}ms (${entry.service.id}, plugin=${entry.pluginId}); continuing startup`); resolve(); }, advertiseTimeoutMs); timer.unref?.(); })]); clearTimeout(timer); } }; const update = (next, nextClaim = claim) => { const nextMode = "mdnsMode" in next ? next.mdnsMode ?? "minimal" : mode; const nextServices = next.gatewayDiscoveryServices ?? services; if (closed || current && mode === nextMode && services === nextServices && claim === nextClaim) return Promise.resolve(); mode = nextMode; services = nextServices; claim = nextClaim; const previous = current; const generation = current = { mode, services: services.values(), stops: [], claim, waiting: false }; return enqueue(async () => { await stopGeneration(previous); await advertise(generation); }); }; await update({}); return { update, stop: () => { closed = true; const previous = current; current = void 0; return enqueue(() => stopGeneration(previous)); } }; } //#endregion export { startGatewayDiscovery };