UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

155 lines (154 loc) 10.2 kB
import { r as asNullableRecord } from "./record-coerce-DItp3I4t.js"; import { n as sanitizeTerminalText } from "./safe-text-BGBqp1a4.js"; import { n as isRich, r as theme, t as colorize } from "./theme-vjDs9tao.js"; import { r as isGatewayTransportError } from "./transport-error-D_LRKgla.js"; import { n as formatDurationHuman } from "./format-duration-CeDWULoS.js"; import { t as formatChannelStatusState } from "./status-state-DzEZ8yAJ.js"; //#region src/commands/health-format.ts /** Shared CLI formatting for gateway health failures, channels, and delivery queues. */ function formatGatewayClosedDiagnostic(err) { if (!isGatewayTransportError(err) || err.kind !== "closed" || err.code === void 0) return; return `Gateway connect failed: ${sanitizeTerminalText(err.message.split("\n", 1)[0] ?? "")}`; } const formatKv = (line, rich) => { const idx = line.indexOf(": "); if (idx <= 0) return colorize(rich, theme.muted, line); const key = line.slice(0, idx); const value = line.slice(idx + 2); const valueColor = key === "Gateway target" || key === "Config" ? theme.command : key === "Source" ? theme.muted : theme.info; return `${colorize(rich, theme.muted, `${key}:`)} ${colorize(rich, valueColor, value)}`; }; /** Formats thrown health errors with rich detail lines when terminal color is enabled. */ function formatHealthCheckFailure(err, opts = {}) { const rich = opts.rich ?? isRich(); const raw = String(err); const message = err instanceof Error ? err.message : raw; if (!rich) return `Health check failed: ${raw}`; const lines = message.split("\n").map((l) => l.trimEnd()).filter(Boolean); const detailsIdx = lines.findIndex((l) => l.startsWith("Gateway target: ")); const summaryLines = (detailsIdx >= 0 ? lines.slice(0, detailsIdx) : lines).map((l) => l.trim()).filter(Boolean); const detailLines = detailsIdx >= 0 ? lines.slice(detailsIdx) : []; const summary = summaryLines.length > 0 ? summaryLines.join(" ") : message; const out = [`${colorize(rich, theme.error.bold, "Health check failed")}: ${summary}`]; for (const line of detailLines) out.push(` ${formatKv(line, rich)}`); return out.join("\n"); } const formatProbeLine = (probe, opts = {}) => { const record = asNullableRecord(probe); if (!record) return null; const ok = typeof record.ok === "boolean" ? record.ok : void 0; if (ok === void 0) return null; const elapsedMs = typeof record.elapsedMs === "number" ? record.elapsedMs : null; const status = typeof record.status === "number" ? record.status : null; const error = typeof record.error === "string" ? record.error : null; const bot = asNullableRecord(record.bot); const botUsername = bot && typeof bot.username === "string" ? bot.username : null; const webhook = asNullableRecord(record.webhook); const webhookUrl = webhook && typeof webhook.url === "string" ? webhook.url : null; const usernames = /* @__PURE__ */ new Set(); if (botUsername) usernames.add(botUsername); for (const extra of opts.botUsernames ?? []) if (extra) usernames.add(extra); if (ok) { let label = "ok"; if (usernames.size > 0) label += ` (@${Array.from(usernames).join(", @")})`; if (elapsedMs != null) label += ` (${elapsedMs}ms)`; if (webhookUrl) label += ` - webhook ${webhookUrl}`; return label; } let label = `failed (${status ?? "unknown"})`; if (error) label += ` - ${error}`; return label; }; const formatAccountProbeTiming = (summary) => { const probe = asNullableRecord(summary.probe); if (!probe) return null; const elapsedMs = typeof probe.elapsedMs === "number" ? Math.round(probe.elapsedMs) : null; const ok = typeof probe.ok === "boolean" ? probe.ok : null; if (elapsedMs == null && ok !== true) return null; const accountId = summary.accountId || "default"; const botRecord = asNullableRecord(probe.bot); const botUsername = botRecord && typeof botRecord.username === "string" ? botRecord.username : null; return `${botUsername ? `@${botUsername}` : accountId}:${accountId}:${elapsedMs != null ? `${elapsedMs}ms` : "ok"}`; }; /** Formats terse channel and activated-plugin health lines for shared CLI surfaces. */ const formatHealthChannelLines = (summary, opts = {}) => { const channels = summary.channels ?? {}; const channelOrder = summary.channelOrder?.length > 0 ? summary.channelOrder : Object.keys(channels); const accountMode = opts.accountMode ?? "default"; const lines = []; for (const channelId of channelOrder) { const channelSummary = channels[channelId]; if (!channelSummary) continue; const label = summary.channelLabels?.[channelId] ?? channelId; const accountSummaries = channelSummary.accounts ?? {}; const accountIds = accountMode === "all" ? void 0 : opts.accountIdsByChannel?.[channelId]; const listSummaries = accountIds?.length ? accountIds.flatMap((accountId) => accountSummaries[accountId] ?? []) : Object.values(accountSummaries); const preferredSummary = accountIds?.length ? listSummaries[0] ?? channelSummary : channelSummary; const activeSummaries = listSummaries.filter((account) => account.enabled !== false && account.configured !== false && account.linked !== false && account.statusState !== "disabled" && account.statusState !== "unconfigured"); const selectedSummary = activeSummaries.find((account) => account.healthState && account.healthState !== "healthy" || account.statusState && account.statusState !== "linked" && account.statusState !== "configured") ?? activeSummaries.find((account) => account.accountId === preferredSummary.accountId) ?? activeSummaries[0] ?? preferredSummary; const botUsernames = activeSummaries.map((account) => { const probeRecord = asNullableRecord(account.probe); const bot = probeRecord ? asNullableRecord(probeRecord.bot) : null; return bot && typeof bot.username === "string" ? bot.username : null; }).filter((value) => Boolean(value)); const statusState = typeof selectedSummary.statusState === "string" ? selectedSummary.statusState : null; const healthState = typeof selectedSummary.healthState === "string" && selectedSummary.healthState ? selectedSummary.healthState : null; const linked = typeof selectedSummary.linked === "boolean" ? selectedSummary.linked : null; const configured = typeof selectedSummary.configured === "boolean" ? selectedSummary.configured : null; const inactiveState = statusState === "disabled" || statusState === "unconfigured" ? formatChannelStatusState(statusState) : configured === false ? "not configured" : null; const preProbeState = inactiveState ? inactiveState : healthState && healthState !== "healthy" ? healthState : statusState && statusState !== "linked" && statusState !== "configured" ? formatChannelStatusState(statusState) : linked === false ? "not linked" : null; if (preProbeState) { lines.push(`${label}: ${preProbeState}`); continue; } const accountTimings = accountMode === "all" ? activeSummaries.map((account) => formatAccountProbeTiming(account)).filter((value) => Boolean(value)) : []; const failedSummary = activeSummaries.find((account) => asNullableRecord(account.probe)?.ok === false); if (failedSummary) { const failureLine = formatProbeLine(failedSummary.probe, { botUsernames }); if (failureLine) { lines.push(`${label}: ${failureLine}`); continue; } } if (accountTimings.length > 0) { lines.push(`${label}: ok (${accountTimings.join(", ")})`); continue; } const probeLine = formatProbeLine(selectedSummary.probe, { botUsernames }); if (probeLine) { lines.push(`${label}: ${probeLine}`); continue; } const authAgeMs = typeof selectedSummary.authAgeMs === "number" ? selectedSummary.authAgeMs : null; const authLabel = authAgeMs != null ? ` (auth age ${Math.round(authAgeMs / 6e4)}m)` : ""; const passiveState = healthState ? healthState : statusState ? `${formatChannelStatusState(statusState)}${statusState === "linked" ? authLabel : ""}` : linked === true ? `linked${authLabel}` : configured === true ? "configured" : "unknown"; lines.push(`${label}: ${passiveState}`); } const failedPlugins = (summary.plugins?.errors ?? []).filter((plugin) => plugin.activated); for (const plugin of failedPlugins.slice(0, 20)) { const id = sanitizeTerminalText(plugin.id).slice(0, 120); const error = sanitizeTerminalText(plugin.error).slice(0, 500); lines.push(`Plugin ${id}: failed - ${error}; run openclaw doctor`); } if (failedPlugins.length > 20) lines.push(`Plugins: failed - ${failedPlugins.length - 20} additional activated failures; run openclaw doctor`); return lines; }; /** Formats dead-lettered and pressured delivery queue entries for text health output. */ function formatDeliveryQueueHealthLine(summary, now = Date.now()) { const failed = summary.deliveryQueues?.failed ?? []; const ingressFailed = summary.deliveryQueues?.ingressFailed ?? []; const ingressPressure = summary.deliveryQueues?.ingressPressure ?? []; const warnings = []; const deadLetterCounts = [...failed.map((queue) => `${queue.queueName}: ${queue.count}`), ...ingressFailed.map((queue) => `inbound ${queue.channelId}/${queue.accountId}: ${queue.count}`)].join(", "); const oldest = [...failed, ...ingressFailed].map((queue) => queue.oldestFailedAt).filter((value) => typeof value === "number"); const oldestNote = oldest.length > 0 ? `; oldest ${formatDurationHuman(now - Math.min(...oldest))} ago` : ""; if (deadLetterCounts) warnings.push(`dead-lettered entries — ${deadLetterCounts}${oldestNote}`); if (ingressPressure.length > 0) { const pressureCounts = ingressPressure.map((queue) => `inbound ${queue.channelId}/${queue.accountId}: ${queue.laneCount} pressured ${queue.laneCount === 1 ? "lane" : "lanes"}, ${queue.pendingCount} pending, ${queue.claimedCount} claimed, ${queue.blockedCount} blocked`).join(", "); const oldestPressure = Math.min(...ingressPressure.map((queue) => queue.oldestReceivedAt)); warnings.push(`ingress pressure — ${pressureCounts}; oldest ${formatDurationHuman(now - oldestPressure)} ago`); } return warnings.length > 0 ? `Delivery queue: warning (${warnings.join("; ")})` : null; } //#endregion export { formatHealthCheckFailure as i, formatGatewayClosedDiagnostic as n, formatHealthChannelLines as r, formatDeliveryQueueHealthLine as t };