UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

254 lines (253 loc) 13 kB
import { i as formatErrorMessage } from "./errors-BXgSefBE.js"; import { t as formatCliCommand } from "./command-format-CKGmlpAQ.js"; import { t as formatDocsLink } from "./links-CsLBrRff.js"; import { r as theme } from "./theme-vjDs9tao.js"; import { n as defaultRuntime, r as writeRuntimeJson } from "./runtime-B4lgFmsS.js"; import { u as readConfigFileSnapshot } from "./io-Gi7-pyU-.js"; import { O as listConfiguredChannelIdsForReadOnlyScope, k as listExplicitConfiguredChannelIdsForConfig } from "./gateway-startup-plugin-ids-3X_G47Qt.js"; import "./config-C9RxTsn1.js"; import { o as redactSensitiveUrlLikeString } from "./redact-sensitive-url-Cf2Fdzd5.js"; import { o as callGateway } from "./call-B5-GYOlf.js"; import { a as normalizeChannelId } from "./registry-MkxNn1Ue.js"; import "./plugins-t2ejWcVy.js"; import "./channel-plugin-ids-yxLDmEfb.js"; import { t as resolveMissingOfficialExternalChannelPluginRepairHint } from "./official-external-plugin-repair-hints-C_AGrH5r.js"; import { n as listReadOnlyChannelPluginsForConfig } from "./read-only-7UA2_pQ4.js"; import { s as getConfiguredChannelsCommandSecretTargetIds } from "./command-secret-targets-DY8QOUoE.js"; import { n as formatTimeAgo } from "./format-relative-Bjc3l98W.js"; import { r as withProgress } from "./progress-CN3xUiCO.js"; import { n as hasResolvedCredentialValue, t as hasConfiguredUnavailableCredentialStatus } from "./account-snapshot-fields-VbzFdu8Y.js"; import { t as resolveCommandConfigWithSecrets } from "./command-config-resolution-Ce9AmmGx.js"; import { n as parseTimeoutMsWithFallback } from "./parse-timeout-DZMZaF9B.js"; import { n as requireValidConfigSnapshot } from "./config-validation-Bz2qAeof.js"; import { a as buildChannelAccountLine, i as appendTokenSourceBits, n as appendEnabledConfiguredLinkedBits, r as appendModeBit, t as appendBaseUrlBit } from "./shared-vtv0TMAo.js"; import { r as buildReadOnlySourceChannelAccountSnapshot, t as buildChannelAccountSnapshot } from "./status-W16cTRep.js"; import { t as collectChannelStatusIssues } from "./channels-status-issues-C4n-rwtS.js"; //#region src/commands/channels/status-config-format.ts /** Render channel status lines from config snapshots without calling the gateway. */ async function formatConfigChannelsStatusLines(cfg, meta, opts) { const lines = []; lines.push(theme.warn("Gateway not reachable; showing config-only status.")); if (meta.path) lines.push(`Config: ${meta.path}`); if (meta.mode) lines.push(`Mode: ${meta.mode}`); if (meta.path || meta.mode) lines.push(""); const accountLines = (plugin, accounts) => accounts.map((account) => { const bits = []; appendEnabledConfiguredLinkedBits(bits, account); appendModeBit(bits, account); appendTokenSourceBits(bits, account); appendBaseUrlBit(bits, account); return buildChannelAccountLine(plugin.id, account, bits, { channelLabel: plugin.meta.label ?? plugin.id }); }); const sourceConfig = opts?.sourceConfig ?? cfg; const requestedChannel = opts?.channel ? normalizeChannelId(opts.channel) : null; const plugins = listReadOnlyChannelPluginsForConfig(cfg, { activationSourceConfig: sourceConfig, includeSetupFallbackPlugins: true }).filter((plugin) => !requestedChannel || plugin.id === requestedChannel); const visibleChannelIds = /* @__PURE__ */ new Set(); for (const plugin of plugins) { visibleChannelIds.add(plugin.id); const accountIds = plugin.config.listAccountIds(cfg); if (!accountIds.length) continue; const snapshots = []; for (const accountId of accountIds) { const sourceSnapshot = await buildReadOnlySourceChannelAccountSnapshot({ plugin, cfg: sourceConfig, accountId }); const resolvedSnapshot = await buildChannelAccountSnapshot({ plugin, cfg, accountId }); snapshots.push(sourceSnapshot && hasConfiguredUnavailableCredentialStatus(sourceSnapshot) && (!hasResolvedCredentialValue(resolvedSnapshot) || sourceSnapshot.configured === true && resolvedSnapshot.configured === false) ? sourceSnapshot : resolvedSnapshot); } if (snapshots.length > 0) lines.push(...accountLines(plugin, snapshots)); } const missingHints = []; const missingChannelIds = [...new Set([...listExplicitConfiguredChannelIdsForConfig(sourceConfig), ...listExplicitConfiguredChannelIdsForConfig(cfg)])]; for (const channelId of missingChannelIds) { if (requestedChannel && channelId !== requestedChannel) continue; if (visibleChannelIds.has(channelId)) continue; const hint = resolveMissingOfficialExternalChannelPluginRepairHint({ config: cfg, activationSourceConfig: sourceConfig, channelId }); if (!hint?.channelId || visibleChannelIds.has(hint.channelId)) continue; missingHints.push(hint); visibleChannelIds.add(hint.channelId); } if (missingHints.length > 0) { lines.push(""); lines.push(theme.warn("Missing official external plugins:")); for (const hint of missingHints) lines.push(`- ${hint.label}: ${hint.repairHint}`); } lines.push(""); lines.push(`Tip: ${formatDocsLink("/cli#status", "status --deep")} adds gateway health probes to status output (requires a reachable gateway).`); return lines; } //#endregion //#region src/commands/channels/status.ts function redactGatewayUrlSecretsInText(text) { return text.replace(/\b(?:wss?|https?):\/\/[^\s"'<>]+/gi, (rawUrl) => { return redactSensitiveUrlLikeString(rawUrl); }); } function formatChannelsStatusError(err) { return redactGatewayUrlSecretsInText(formatErrorMessage(err)); } function formatEventLoopBits(value) { if (!value || typeof value !== "object") return null; const record = value; if (record.degraded !== true) return null; const reasons = Array.isArray(record.reasons) ? record.reasons.filter((reason) => typeof reason === "string") : []; const delayMaxMs = typeof record.delayMaxMs === "number" && Number.isFinite(record.delayMaxMs) ? Math.round(record.delayMaxMs) : null; const utilization = typeof record.utilization === "number" && Number.isFinite(record.utilization) ? record.utilization : null; const cpuCoreRatio = typeof record.cpuCoreRatio === "number" && Number.isFinite(record.cpuCoreRatio) ? record.cpuCoreRatio : null; return [ reasons.length ? `reasons=${reasons.join(",")}` : null, delayMaxMs != null ? `eventLoopDelayMaxMs=${delayMaxMs}` : null, utilization != null ? `eventLoopUtilization=${utilization}` : null, cpuCoreRatio != null ? `cpuCoreRatio=${cpuCoreRatio}` : null ].filter((part) => Boolean(part)).join(" "); } /** Render gateway channel status payloads into terminal-friendly lines. */ function formatGatewayChannelsStatusLines(payload) { const lines = []; lines.push(theme.success("Gateway reachable.")); const eventLoopLine = formatEventLoopBits(payload.eventLoop); if (eventLoopLine) lines.push(theme.warn(`Gateway event loop degraded: ${eventLoopLine}`)); const channelLabels = payload.channelLabels && typeof payload.channelLabels === "object" ? payload.channelLabels : {}; const accountLines = (provider, accounts) => accounts.map((account) => { const bits = []; appendEnabledConfiguredLinkedBits(bits, account); if (typeof account.running === "boolean") bits.push(account.running ? "running" : "stopped"); if (typeof account.connected === "boolean") bits.push(account.connected ? "connected" : "disconnected"); const inboundAt = typeof account.lastInboundAt === "number" && Number.isFinite(account.lastInboundAt) ? account.lastInboundAt : null; const outboundAt = typeof account.lastOutboundAt === "number" && Number.isFinite(account.lastOutboundAt) ? account.lastOutboundAt : null; const transportAt = typeof account.lastTransportActivityAt === "number" && Number.isFinite(account.lastTransportActivityAt) ? account.lastTransportActivityAt : null; if (inboundAt) bits.push(`in:${formatTimeAgo(Date.now() - inboundAt)}`); if (outboundAt) bits.push(`out:${formatTimeAgo(Date.now() - outboundAt)}`); if (transportAt) bits.push(`transport:${formatTimeAgo(Date.now() - transportAt)}`); appendModeBit(bits, account); const botUsername = (() => { const bot = account.bot; const probeBot = account.probe?.bot; const raw = bot?.username ?? probeBot?.username ?? ""; if (typeof raw !== "string") return ""; const trimmed = raw.trim(); if (!trimmed) return ""; return trimmed.startsWith("@") ? trimmed : `@${trimmed}`; })(); if (botUsername) bits.push(`bot:${botUsername}`); if (typeof account.dmPolicy === "string" && account.dmPolicy.length > 0) bits.push(`dm:${account.dmPolicy}`); if (Array.isArray(account.allowFrom) && account.allowFrom.length > 0) bits.push(`allow:${account.allowFrom.slice(0, 2).join(",")}`); appendTokenSourceBits(bits, account); const messageContent = account.application?.intents?.messageContent; if (typeof messageContent === "string" && messageContent.length > 0 && messageContent !== "enabled") bits.push(`intents:content=${messageContent}`); if (account.allowUnmentionedGroups === true) bits.push("groups:unmentioned"); if (typeof account.healthState === "string" && account.healthState) bits.push(`health:${account.healthState}`); appendBaseUrlBit(bits, account); const probe = account.probe; if (probe && typeof probe.ok === "boolean") bits.push(probe.ok ? "works" : "probe failed"); const audit = account.audit; if (audit && typeof audit.ok === "boolean") bits.push(audit.ok ? "audit ok" : "audit failed"); if (typeof account.lastError === "string" && account.lastError) bits.push(`error:${account.lastError}`); const rawChannelLabel = channelLabels[provider]; return buildChannelAccountLine(provider, account, bits, { channelLabel: typeof rawChannelLabel === "string" ? rawChannelLabel : provider }); }); const accountsByChannel = payload.channelAccounts; const accountPayloads = {}; for (const channelId of Object.keys(accountsByChannel ?? {}).toSorted()) { const raw = accountsByChannel?.[channelId]; if (Array.isArray(raw)) accountPayloads[channelId] = raw; } for (const channelId of Object.keys(accountPayloads).toSorted()) { const accounts = accountPayloads[channelId]; if (accounts && accounts.length > 0) lines.push(...accountLines(channelId, accounts)); } lines.push(""); const issues = collectChannelStatusIssues(payload); if (issues.length > 0) { lines.push(theme.warn("Warnings:")); for (const issue of issues) lines.push(`- ${issue.channel} ${issue.accountId}: ${issue.message}${issue.fix ? ` (${issue.fix})` : ""}`); lines.push(`- Run: ${formatCliCommand("openclaw doctor")}`); lines.push(""); } lines.push(`Tip: ${formatDocsLink("/cli#status", "status --deep")} adds gateway health probes to status output (requires a reachable gateway).`); return lines; } /** Query gateway channel status, falling back to config-only output when unreachable. */ async function channelsStatusCommand(opts, runtime = defaultRuntime) { const timeoutMs = parseTimeoutMsWithFallback(opts.timeout, opts.probe ? 3e4 : 1e4, { invalidType: "error" }); const requestedChannel = opts.channel ? normalizeChannelId(opts.channel) : null; const statusLabel = opts.probe ? "Checking channel status (probe)…" : "Checking channel status…"; if (opts.json !== true && !process.stderr.isTTY) runtime.log(statusLabel); try { const payload = await withProgress({ label: statusLabel, indeterminate: true, enabled: opts.json !== true }, async () => { const params = { probe: Boolean(opts.probe), timeoutMs }; if (opts.channel) params.channel = opts.channel; return await callGateway({ method: "channels.status", params, timeoutMs }); }); if (opts.json) { writeRuntimeJson(runtime, payload); return; } runtime.log(formatGatewayChannelsStatusLines(payload).join("\n")); } catch (err) { const safeError = formatChannelsStatusError(err); runtime.error(`Gateway not reachable: ${safeError}`); const cfg = await requireValidConfigSnapshot(runtime); if (!cfg) return; const { resolvedConfig } = await resolveCommandConfigWithSecrets({ config: cfg, commandName: "channels status", targetIds: getConfiguredChannelsCommandSecretTargetIds(cfg), mode: "read_only_status", runtime }); const snapshot = await readConfigFileSnapshot(); const mode = cfg.gateway?.mode === "remote" ? "remote" : "local"; if (opts.json) { writeRuntimeJson(runtime, { gatewayReachable: false, error: safeError, configOnly: true, config: { path: snapshot.path, mode }, configuredChannels: listConfiguredChannelIdsForReadOnlyScope({ config: resolvedConfig, activationSourceConfig: cfg, env: process.env, includePersistedAuthState: false }).filter((channelId) => !requestedChannel || channelId === requestedChannel) }); return; } runtime.log((await formatConfigChannelsStatusLines(resolvedConfig, { path: snapshot.path, mode }, { sourceConfig: cfg, channel: opts.channel })).join("\n")); } } //#endregion export { formatGatewayChannelsStatusLines as n, channelsStatusCommand as t };