UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

515 lines (514 loc) 22.1 kB
import { c as normalizeOptionalString } from "./string-coerce-mnp54Vah.js"; import { a as asRecord } from "./record-coerce-DHZ4bFlT.js"; import { t as sanitizeForLog } from "./ansi-BI9w76Cm.js"; import { k as listExplicitConfiguredChannelIdsForConfig } from "./gateway-startup-plugin-ids-3X_G47Qt.js"; import { n as sha256HexPrefix } from "./redact-identifier-CXp_wA0R.js"; import "./channel-plugin-ids-yxLDmEfb.js"; import { t as resolveMissingOfficialExternalChannelPluginRepairHint } from "./official-external-plugin-repair-hints-C_AGrH5r.js"; import { i as resolveChannelDefaultAccountId } from "./helpers-BLGguEiN.js"; import { r as resolveReadOnlyChannelPluginsForConfig } from "./read-only-7UA2_pQ4.js"; import { n as formatTimeAgo } from "./format-relative-Bjc3l98W.js"; import { t as hasConfiguredUnavailableCredentialStatus } from "./account-snapshot-fields-VbzFdu8Y.js"; import { i as formatChannelAllowFrom, n as resolveInspectedChannelAccount, r as buildChannelAccountSnapshot } from "./account-inspection-DG-22xoO.js"; import { t as formatChannelStatusState } from "./status-state-DzEZ8yAJ.js"; import { n as hasRuntimeCredentialAvailable, r as markConfiguredUnavailableCredentialStatusesAvailable, t as getRuntimeChannelAccounts } from "./read-model-BmEjOuR_.js"; import { t as collectChannelStatusIssues } from "./channels-status-issues-C4n-rwtS.js"; import "./format-Bar-LTbU.js"; import fs from "node:fs"; //#region src/commands/status-all/channels-token-summary.ts /** Collapses credential sources into a stable count label such as `env×2+file`. */ function summarizeSources(sources) { const counts = /* @__PURE__ */ new Map(); for (const s of sources) { const key = s?.trim() ? s.trim() : "unknown"; counts.set(key, (counts.get(key) ?? 0) + 1); } const parts = [...counts.entries()].toSorted((a, b) => b[1] - a[1]).map(([key, n]) => `${key}${n > 1 ? ${n}` : ""}`); return { label: parts.length > 0 ? parts.join("+") : "unknown", parts }; } function formatTokenHint(token, opts) { const t = token.trim(); if (!t) return "empty"; if (!opts.showSecrets) return `sha256:${sha256HexPrefix(t, 8)} · len ${t.length}`; const head = t.slice(0, 4); const tail = t.slice(-4); if (t.length <= 10) return `${t} · len ${t.length}`; return `${head}${tail} · len ${t.length}`; } /** Returns the credential status sentence for enabled channel accounts, if the plugin exposes token fields. */ function summarizeTokenConfig(params) { const enabled = params.accounts.filter((a) => a.enabled); if (enabled.length === 0) return { state: null, detail: null }; const accountRecs = enabled.map((a) => asRecord(a.account)); const hasBotTokenField = accountRecs.some((r) => "botToken" in r); const hasAppTokenField = accountRecs.some((r) => "appToken" in r); const hasSigningSecretField = accountRecs.some((r) => "signingSecret" in r || "signingSecretSource" in r || "signingSecretStatus" in r); const hasTokenField = accountRecs.some((r) => "token" in r); if (!hasBotTokenField && !hasAppTokenField && !hasSigningSecretField && !hasTokenField) return { state: null, detail: null }; const accountIsHttpMode = (rec) => typeof rec.mode === "string" && rec.mode.trim() === "http"; const hasCredentialAvailable = (rec, valueKey, statusKey) => { const value = rec[valueKey]; if (typeof value === "string" && value.trim()) return true; return rec[statusKey] === "available"; }; if (hasBotTokenField && hasSigningSecretField && enabled.every((a) => accountIsHttpMode(asRecord(a.account)))) { const unavailable = enabled.filter((a) => hasConfiguredUnavailableCredentialStatus(a.account)); const ready = enabled.filter((a) => { const rec = asRecord(a.account); return hasCredentialAvailable(rec, "botToken", "botTokenStatus") && hasCredentialAvailable(rec, "signingSecret", "signingSecretStatus"); }); const partial = enabled.filter((a) => { const rec = asRecord(a.account); const hasBot = hasCredentialAvailable(rec, "botToken", "botTokenStatus"); const hasSigning = hasCredentialAvailable(rec, "signingSecret", "signingSecretStatus"); return hasBot && !hasSigning || !hasBot && hasSigning; }); if (unavailable.length > 0) return { state: "warn", detail: `configured http credentials unavailable in this command path · accounts ${unavailable.length}` }; if (partial.length > 0) return { state: "warn", detail: `partial credentials (need bot+signing) · accounts ${partial.length}` }; if (ready.length === 0) return { state: "setup", detail: "no credentials (need bot+signing)" }; const botSources = summarizeSources(ready.map((a) => a.snapshot.botTokenSource ?? "none")); const signingSources = summarizeSources(ready.map((a) => a.snapshot.signingSecretSource ?? "none")); const sample = ready[0]?.account ? asRecord(ready[0].account) : {}; const botToken = typeof sample.botToken === "string" ? sample.botToken : ""; const signingSecret = typeof sample.signingSecret === "string" ? sample.signingSecret : ""; const botHint = botToken.trim() ? formatTokenHint(botToken, { showSecrets: params.showSecrets }) : ""; const signingHint = signingSecret.trim() ? formatTokenHint(signingSecret, { showSecrets: params.showSecrets }) : ""; const hint = botHint || signingHint ? ` (bot ${botHint || "?"}, signing ${signingHint || "?"})` : ""; return { state: "ok", detail: `credentials ok (bot ${botSources.label}, signing ${signingSources.label})${hint} · accounts ${ready.length}/${enabled.length || 1}` }; } if (hasBotTokenField && hasAppTokenField) { const unavailable = enabled.filter((a) => hasConfiguredUnavailableCredentialStatus(a.account)); const ready = enabled.filter((a) => { const rec = asRecord(a.account); const bot = normalizeOptionalString(rec.botToken) ?? ""; const app = normalizeOptionalString(rec.appToken) ?? ""; return Boolean(bot) && Boolean(app); }); const partial = enabled.filter((a) => { const rec = asRecord(a.account); const bot = normalizeOptionalString(rec.botToken) ?? ""; const app = normalizeOptionalString(rec.appToken) ?? ""; const hasBot = Boolean(bot); const hasApp = Boolean(app); return hasBot && !hasApp || !hasBot && hasApp; }); if (partial.length > 0) return { state: "warn", detail: `partial tokens (need bot+app) · accounts ${partial.length}` }; if (unavailable.length > 0) return { state: "warn", detail: `configured tokens unavailable in this command path · accounts ${unavailable.length}` }; if (ready.length === 0) return { state: "setup", detail: "no tokens (need bot+app)" }; const botSources = summarizeSources(ready.map((a) => a.snapshot.botTokenSource ?? "none")); const appSources = summarizeSources(ready.map((a) => a.snapshot.appTokenSource ?? "none")); const sample = ready[0]?.account ? asRecord(ready[0].account) : {}; const botToken = typeof sample.botToken === "string" ? sample.botToken : ""; const appToken = typeof sample.appToken === "string" ? sample.appToken : ""; const botHint = botToken.trim() ? formatTokenHint(botToken, { showSecrets: params.showSecrets }) : ""; const appHint = appToken.trim() ? formatTokenHint(appToken, { showSecrets: params.showSecrets }) : ""; const hint = botHint || appHint ? ` (bot ${botHint || "?"}, app ${appHint || "?"})` : ""; return { state: "ok", detail: `tokens ok (bot ${botSources.label}, app ${appSources.label})${hint} · accounts ${ready.length}/${enabled.length || 1}` }; } if (hasBotTokenField) { const unavailable = enabled.filter((a) => hasConfiguredUnavailableCredentialStatus(a.account)); const ready = enabled.filter((a) => { const bot = normalizeOptionalString(asRecord(a.account).botToken) ?? ""; return Boolean(bot); }); if (unavailable.length > 0) return { state: "warn", detail: `configured bot token unavailable in this command path · accounts ${unavailable.length}` }; if (ready.length === 0) return { state: "setup", detail: "no bot token" }; const sample = ready[0]?.account ? asRecord(ready[0].account) : {}; const botToken = typeof sample.botToken === "string" ? sample.botToken : ""; const botHint = botToken.trim() ? formatTokenHint(botToken, { showSecrets: params.showSecrets }) : ""; return { state: "ok", detail: `bot token config${botHint ? ` (${botHint})` : ""} · accounts ${ready.length}/${enabled.length || 1}` }; } const unavailable = enabled.filter((a) => hasConfiguredUnavailableCredentialStatus(a.account)); const ready = enabled.filter((a) => { const rec = asRecord(a.account); return Boolean(normalizeOptionalString(rec.token)); }); if (unavailable.length > 0) return { state: "warn", detail: `configured token unavailable in this command path · accounts ${unavailable.length}` }; if (ready.length === 0) return { state: "setup", detail: "no token" }; const sources = summarizeSources(ready.map((a) => a.snapshot.tokenSource)); const sample = ready[0]?.account ? asRecord(ready[0].account) : {}; const token = typeof sample.token === "string" ? sample.token : ""; const hint = token.trim() ? ` (${formatTokenHint(token, { showSecrets: params.showSecrets })})` : ""; return { state: "ok", detail: `token ${sources.label}${hint} · accounts ${ready.length}/${enabled.length || 1}` }; } //#endregion //#region src/commands/status-all/channels.ts function existsSyncMaybe(p) { const path = normalizeOptionalString(p) ?? ""; if (!path) return null; try { return fs.existsSync(path); } catch { return null; } } /** Resolves one configured/default account into the normalized row shape used by status rendering. */ async function resolveChannelAccountRow(params) { const { plugin, cfg, sourceConfig, accountId } = params; const { account, enabled, configured } = await resolveInspectedChannelAccount({ plugin, cfg, sourceConfig, accountId }); return { accountId, account, enabled, configured, snapshot: buildChannelAccountSnapshot({ plugin, cfg, accountId, account, enabled, configured }) }; } const formatAccountLabel = (params) => { const base = params.accountId || "default"; if (params.name?.trim()) return `${base} (${params.name.trim()})`; return base; }; const buildAccountNotes = (params) => { const { plugin, cfg, entry } = params; const notes = []; const snapshot = entry.snapshot; if (snapshot.enabled === false) notes.push("disabled"); if (snapshot.dmPolicy) notes.push(`dm:${snapshot.dmPolicy}`); if (snapshot.tokenSource && snapshot.tokenSource !== "none") notes.push(`token:${snapshot.tokenSource}`); if (snapshot.botTokenSource && snapshot.botTokenSource !== "none") notes.push(`bot:${snapshot.botTokenSource}`); if (snapshot.appTokenSource && snapshot.appTokenSource !== "none") notes.push(`app:${snapshot.appTokenSource}`); if (snapshot.signingSecretSource && snapshot.signingSecretSource !== "none") notes.push(`signing:${snapshot.signingSecretSource}`); if (params.liveCredentialAvailable) notes.push("credential available in gateway runtime"); else if (params.credentialResolutionSkipped && hasConfiguredUnavailableCredentialStatus(entry.account)) notes.push("credential not checked"); else if (hasConfiguredUnavailableCredentialStatus(entry.account)) notes.push("secret unavailable in this command path"); if (snapshot.baseUrl) notes.push(snapshot.baseUrl); if (snapshot.port != null) notes.push(`port:${snapshot.port}`); if (snapshot.cliPath) notes.push(`cli:${snapshot.cliPath}`); if (snapshot.dbPath) notes.push(`db:${snapshot.dbPath}`); const allowFrom = plugin.config.resolveAllowFrom?.({ cfg, accountId: snapshot.accountId }) ?? snapshot.allowFrom; if (allowFrom?.length) { const formatted = formatChannelAllowFrom({ plugin, cfg, accountId: snapshot.accountId, allowFrom }).slice(0, 3); if (formatted.length > 0) notes.push(`allow:${formatted.join(",")}`); } return notes; }; function resolveLinkFields(summary) { const rec = asRecord(summary); const statusState = typeof rec.statusState === "string" ? rec.statusState : null; const linked = typeof rec.linked === "boolean" ? rec.linked : null; const authAgeMs = typeof rec.authAgeMs === "number" ? rec.authAgeMs : null; const self = asRecord(rec.self); return { statusState, linked, authAgeMs, selfE164: typeof self.e164 === "string" && self.e164.trim() ? self.e164.trim() : null }; } function collectMissingPaths(accounts) { const missing = []; for (const entry of accounts) { const accountRec = asRecord(entry.account); const snapshotRec = asRecord(entry.snapshot); for (const key of [ "tokenFile", "botTokenFile", "appTokenFile", "cliPath", "dbPath", "authDir" ]) { const raw = accountRec[key] ?? snapshotRec[key]; if (existsSyncMaybe(raw) === false) missing.push(String(raw)); } } return missing; } function isLikelyDependencyTreeCorruption(message) { return /(?:cannot find (?:module|package)|module_not_found|err_module_not_found|enoent|enotempty|missing package|failed to resolve)/iu.test(message); } function formatLoadFailureDetail(message) { return `plugin load failed: ${isLikelyDependencyTreeCorruption(message) ? "dependency tree corrupted" : "registration failed"}; run openclaw doctor --fix`; } /** Builds the `status --all` channel summary and per-account detail tables. */ async function buildChannelsTable(cfg, opts) { const showSecrets = opts?.showSecrets === true; const rows = []; const details = []; const sourceConfig = opts?.sourceConfig ?? cfg; const includeSetupFallbackPlugins = opts?.includeSetupFallbackPlugins ?? true; const credentialResolutionSkipped = opts?.credentialResolutionSkipped === true; const readOnlyPlugins = resolveReadOnlyChannelPluginsForConfig(cfg, { activationSourceConfig: sourceConfig, includeSetupFallbackPlugins }); for (const plugin of readOnlyPlugins.plugins) { const accountIds = plugin.config.listAccountIds(cfg); const defaultAccountId = resolveChannelDefaultAccountId({ plugin, cfg, accountIds }); const resolvedAccountIds = accountIds.length > 0 ? accountIds : [defaultAccountId]; const accounts = []; for (const accountId of resolvedAccountIds) accounts.push(await resolveChannelAccountRow({ plugin, cfg, sourceConfig, accountId })); const liveAccounts = getRuntimeChannelAccounts({ payload: opts?.liveChannelStatus, channelId: plugin.id }); const anyEnabled = accounts.some((a) => a.enabled); const enabledAccounts = accounts.filter((a) => a.enabled); const configuredAccounts = enabledAccounts.filter((a) => a.configured); const unavailableConfiguredAccounts = enabledAccounts.filter((a) => hasConfiguredUnavailableCredentialStatus(a.account) && !credentialResolutionSkipped && !hasRuntimeCredentialAvailable({ liveAccounts, accountId: a.accountId })); const accountsForTokenSummary = accounts.map((entry) => hasConfiguredUnavailableCredentialStatus(entry.account) && (credentialResolutionSkipped || hasRuntimeCredentialAvailable({ liveAccounts, accountId: entry.accountId })) ? { ...entry, account: markConfiguredUnavailableCredentialStatusesAvailable(entry.account) } : entry); const defaultEntry = accounts.find((a) => a.accountId === defaultAccountId) ?? accounts[0]; const link = resolveLinkFields(plugin.status?.buildChannelSummary ? await plugin.status.buildChannelSummary({ account: defaultEntry?.account ?? {}, cfg, defaultAccountId, snapshot: defaultEntry?.snapshot ?? { accountId: defaultAccountId } }) : void 0); const missingPaths = collectMissingPaths(enabledAccounts); const tokenSummary = summarizeTokenConfig({ accounts: accountsForTokenSummary, showSecrets }); const issues = plugin.status?.collectStatusIssues ? plugin.status.collectStatusIssues(accounts.map((a) => a.snapshot)) : []; const label = plugin.meta.label ?? plugin.id; const state = (() => { if (!anyEnabled) return "off"; if (missingPaths.length > 0) return "warn"; if (issues.length > 0) return "warn"; if (unavailableConfiguredAccounts.length > 0) return "warn"; if (link.statusState === "unstable") return "warn"; if (link.linked === false) return "setup"; if (tokenSummary.state) return tokenSummary.state; if (link.linked === true) return "ok"; if (configuredAccounts.length > 0) return "ok"; return "setup"; })(); const detail = (() => { if (!anyEnabled) { if (!defaultEntry) return "disabled"; return plugin.config.disabledReason?.(defaultEntry.account, cfg) ?? "disabled"; } if (missingPaths.length > 0) return `missing file (${missingPaths[0]})`; if (issues.length > 0) return issues[0]?.message ?? "misconfigured"; if (link.statusState) { if (link.statusState === "linked") { const extra = []; if (link.selfE164) extra.push(link.selfE164); if (link.authAgeMs != null && link.authAgeMs >= 0) extra.push(`auth ${formatTimeAgo(link.authAgeMs)}`); if (accounts.length > 1 || plugin.meta.forceAccountBinding) extra.push(`accounts ${accounts.length || 1}`); return extra.length > 0 ? `${formatChannelStatusState(link.statusState)} · ${extra.join(" · ")}` : formatChannelStatusState(link.statusState); } return formatChannelStatusState(link.statusState); } if (link.linked !== null) { const base = link.linked ? "linked" : "not linked"; const extra = []; if (link.linked && link.selfE164) extra.push(link.selfE164); if (link.linked && link.authAgeMs != null && link.authAgeMs >= 0) extra.push(`auth ${formatTimeAgo(link.authAgeMs)}`); if (accounts.length > 1 || plugin.meta.forceAccountBinding) extra.push(`accounts ${accounts.length || 1}`); return extra.length > 0 ? `${base} · ${extra.join(" · ")}` : base; } if (unavailableConfiguredAccounts.length > 0) { if (tokenSummary.detail?.includes("unavailable")) return tokenSummary.detail; return `configured credentials unavailable in this command path · accounts ${unavailableConfiguredAccounts.length}`; } if (tokenSummary.detail) return tokenSummary.detail; if (configuredAccounts.length > 0) { const head = "configured"; if (accounts.length <= 1 && !plugin.meta.forceAccountBinding) return head; return `${head} · accounts ${configuredAccounts.length}/${enabledAccounts.length || 1}`; } return (defaultEntry && plugin.config.unconfiguredReason ? plugin.config.unconfiguredReason(defaultEntry.account, cfg) : null) ?? "not configured"; })(); rows.push({ id: plugin.id, label, enabled: anyEnabled, state, detail }); if (configuredAccounts.length > 0) details.push({ title: `${label} accounts`, columns: [ "Account", "Status", "Notes" ], rows: configuredAccounts.map((entry) => { const liveCredentialAvailable = hasRuntimeCredentialAvailable({ liveAccounts, accountId: entry.accountId }); const credentialUnknown = credentialResolutionSkipped && hasConfiguredUnavailableCredentialStatus(entry.account); const notes = buildAccountNotes({ plugin, cfg, entry, liveCredentialAvailable, credentialResolutionSkipped }); return { Account: formatAccountLabel({ accountId: entry.accountId, name: entry.snapshot.name }), Status: entry.enabled && (!hasConfiguredUnavailableCredentialStatus(entry.account) || liveCredentialAvailable) ? "OK" : credentialUnknown ? "UNKNOWN" : "WARN", Notes: notes.join(" · ") }; }) }); } const visibleChannelIds = new Set(rows.map((row) => row.id)); const loadFailuresByChannel = new Map(readOnlyPlugins.loadFailures.map((failure) => [failure.channelId, failure])); for (const channelId of readOnlyPlugins.missingConfiguredChannelIds.toSorted((left, right) => left.localeCompare(right))) { if (visibleChannelIds.has(channelId)) continue; const failure = loadFailuresByChannel.get(channelId); if (!failure) continue; rows.push({ id: channelId, label: channelId, enabled: true, state: "warn", detail: formatLoadFailureDetail(failure.message) }); visibleChannelIds.add(channelId); } const missingCandidateChannelIds = [...new Set([ ...readOnlyPlugins.missingConfiguredChannelIds, ...listExplicitConfiguredChannelIdsForConfig(sourceConfig), ...listExplicitConfiguredChannelIdsForConfig(cfg) ])].toSorted((left, right) => left.localeCompare(right)); const explicitConfiguredChannelIds = new Set([...listExplicitConfiguredChannelIdsForConfig(sourceConfig), ...listExplicitConfiguredChannelIdsForConfig(cfg)]); for (const channelId of missingCandidateChannelIds) { if (visibleChannelIds.has(channelId)) continue; const hint = resolveMissingOfficialExternalChannelPluginRepairHint({ config: cfg, activationSourceConfig: sourceConfig, channelId }); if (!hint || hint.channelId !== channelId) { if (!includeSetupFallbackPlugins && explicitConfiguredChannelIds.has(channelId)) { rows.push({ id: channelId, label: sanitizeForLog(channelId).trim() || "configured-channel", enabled: true, state: "setup", detail: "configured; status unavailable in fast mode" }); visibleChannelIds.add(channelId); } continue; } rows.push({ id: channelId, label: hint.label, enabled: true, state: "warn", detail: `plugin not installed - run ${hint.installCommand} or ${hint.doctorFixCommand}` }); visibleChannelIds.add(channelId); } if (!includeSetupFallbackPlugins) for (const channelId of readOnlyPlugins.missingConfiguredChannelIds.toSorted((left, right) => left.localeCompare(right))) { if (visibleChannelIds.has(channelId)) continue; rows.push({ id: channelId, label: sanitizeForLog(channelId).trim() || "configured-channel", enabled: true, state: "setup", detail: "configured; status unavailable in fast mode" }); visibleChannelIds.add(channelId); } return { rows, details }; } //#endregion //#region src/commands/status.scan.runtime.ts const statusScanRuntime = { collectChannelStatusIssues, buildChannelsTable }; //#endregion export { statusScanRuntime };