Spaces:
Running
Running
| import type { ChannelAccountSnapshot, ChannelPlugin } from "../channels/plugins/types.js"; | |
| import { listChannelPlugins } from "../channels/plugins/index.js"; | |
| import { type OpenClawConfig, loadConfig } from "../config/config.js"; | |
| import { DEFAULT_ACCOUNT_ID } from "../routing/session-key.js"; | |
| import { theme } from "../terminal/theme.js"; | |
| export type ChannelSummaryOptions = { | |
| colorize?: boolean; | |
| includeAllowFrom?: boolean; | |
| }; | |
| const DEFAULT_OPTIONS: Required<ChannelSummaryOptions> = { | |
| colorize: false, | |
| includeAllowFrom: false, | |
| }; | |
| type ChannelAccountEntry = { | |
| accountId: string; | |
| account: unknown; | |
| enabled: boolean; | |
| configured: boolean; | |
| snapshot: ChannelAccountSnapshot; | |
| }; | |
| const formatAccountLabel = (params: { accountId: string; name?: string }) => { | |
| const base = params.accountId || DEFAULT_ACCOUNT_ID; | |
| if (params.name?.trim()) { | |
| return `${base} (${params.name.trim()})`; | |
| } | |
| return base; | |
| }; | |
| const accountLine = (label: string, details: string[]) => | |
| ` - ${label}${details.length ? ` (${details.join(", ")})` : ""}`; | |
| const resolveAccountEnabled = ( | |
| plugin: ChannelPlugin, | |
| account: unknown, | |
| cfg: OpenClawConfig, | |
| ): boolean => { | |
| if (plugin.config.isEnabled) { | |
| return plugin.config.isEnabled(account, cfg); | |
| } | |
| if (!account || typeof account !== "object") { | |
| return true; | |
| } | |
| const enabled = (account as { enabled?: boolean }).enabled; | |
| return enabled !== false; | |
| }; | |
| const resolveAccountConfigured = async ( | |
| plugin: ChannelPlugin, | |
| account: unknown, | |
| cfg: OpenClawConfig, | |
| ): Promise<boolean> => { | |
| if (plugin.config.isConfigured) { | |
| return await plugin.config.isConfigured(account, cfg); | |
| } | |
| return true; | |
| }; | |
| const buildAccountSnapshot = (params: { | |
| plugin: ChannelPlugin; | |
| account: unknown; | |
| cfg: OpenClawConfig; | |
| accountId: string; | |
| enabled: boolean; | |
| configured: boolean; | |
| }): ChannelAccountSnapshot => { | |
| const described = params.plugin.config.describeAccount | |
| ? params.plugin.config.describeAccount(params.account, params.cfg) | |
| : undefined; | |
| return { | |
| enabled: params.enabled, | |
| configured: params.configured, | |
| ...described, | |
| accountId: params.accountId, | |
| }; | |
| }; | |
| const formatAllowFrom = (params: { | |
| plugin: ChannelPlugin; | |
| cfg: OpenClawConfig; | |
| accountId?: string | null; | |
| allowFrom: Array<string | number>; | |
| }) => { | |
| if (params.plugin.config.formatAllowFrom) { | |
| return params.plugin.config.formatAllowFrom({ | |
| cfg: params.cfg, | |
| accountId: params.accountId, | |
| allowFrom: params.allowFrom, | |
| }); | |
| } | |
| return params.allowFrom.map((entry) => String(entry).trim()).filter(Boolean); | |
| }; | |
| const buildAccountDetails = (params: { | |
| entry: ChannelAccountEntry; | |
| plugin: ChannelPlugin; | |
| cfg: OpenClawConfig; | |
| includeAllowFrom: boolean; | |
| }): string[] => { | |
| const details: string[] = []; | |
| const snapshot = params.entry.snapshot; | |
| if (snapshot.enabled === false) { | |
| details.push("disabled"); | |
| } | |
| if (snapshot.dmPolicy) { | |
| details.push(`dm:${snapshot.dmPolicy}`); | |
| } | |
| if (snapshot.tokenSource && snapshot.tokenSource !== "none") { | |
| details.push(`token:${snapshot.tokenSource}`); | |
| } | |
| if (snapshot.botTokenSource && snapshot.botTokenSource !== "none") { | |
| details.push(`bot:${snapshot.botTokenSource}`); | |
| } | |
| if (snapshot.appTokenSource && snapshot.appTokenSource !== "none") { | |
| details.push(`app:${snapshot.appTokenSource}`); | |
| } | |
| if (snapshot.baseUrl) { | |
| details.push(snapshot.baseUrl); | |
| } | |
| if (snapshot.port != null) { | |
| details.push(`port:${snapshot.port}`); | |
| } | |
| if (snapshot.cliPath) { | |
| details.push(`cli:${snapshot.cliPath}`); | |
| } | |
| if (snapshot.dbPath) { | |
| details.push(`db:${snapshot.dbPath}`); | |
| } | |
| if (params.includeAllowFrom && snapshot.allowFrom?.length) { | |
| const formatted = formatAllowFrom({ | |
| plugin: params.plugin, | |
| cfg: params.cfg, | |
| accountId: snapshot.accountId, | |
| allowFrom: snapshot.allowFrom, | |
| }).slice(0, 2); | |
| if (formatted.length > 0) { | |
| details.push(`allow:${formatted.join(",")}`); | |
| } | |
| } | |
| return details; | |
| }; | |
| export async function buildChannelSummary( | |
| cfg?: OpenClawConfig, | |
| options?: ChannelSummaryOptions, | |
| ): Promise<string[]> { | |
| const effective = cfg ?? loadConfig(); | |
| const lines: string[] = []; | |
| const resolved = { ...DEFAULT_OPTIONS, ...options }; | |
| const tint = (value: string, color?: (input: string) => string) => | |
| resolved.colorize && color ? color(value) : value; | |
| for (const plugin of listChannelPlugins()) { | |
| const accountIds = plugin.config.listAccountIds(effective); | |
| const defaultAccountId = | |
| plugin.config.defaultAccountId?.(effective) ?? accountIds[0] ?? DEFAULT_ACCOUNT_ID; | |
| const resolvedAccountIds = accountIds.length > 0 ? accountIds : [defaultAccountId]; | |
| const entries: ChannelAccountEntry[] = []; | |
| for (const accountId of resolvedAccountIds) { | |
| const account = plugin.config.resolveAccount(effective, accountId); | |
| const enabled = resolveAccountEnabled(plugin, account, effective); | |
| const configured = await resolveAccountConfigured(plugin, account, effective); | |
| const snapshot = buildAccountSnapshot({ | |
| plugin, | |
| account, | |
| cfg: effective, | |
| accountId, | |
| enabled, | |
| configured, | |
| }); | |
| entries.push({ accountId, account, enabled, configured, snapshot }); | |
| } | |
| const configuredEntries = entries.filter((entry) => entry.configured); | |
| const anyEnabled = entries.some((entry) => entry.enabled); | |
| const fallbackEntry = | |
| entries.find((entry) => entry.accountId === defaultAccountId) ?? entries[0]; | |
| const summary = plugin.status?.buildChannelSummary | |
| ? await plugin.status.buildChannelSummary({ | |
| account: fallbackEntry?.account ?? {}, | |
| cfg: effective, | |
| defaultAccountId, | |
| snapshot: | |
| fallbackEntry?.snapshot ?? ({ accountId: defaultAccountId } as ChannelAccountSnapshot), | |
| }) | |
| : undefined; | |
| const summaryRecord = summary; | |
| const linked = | |
| summaryRecord && typeof summaryRecord.linked === "boolean" ? summaryRecord.linked : null; | |
| const configured = | |
| summaryRecord && typeof summaryRecord.configured === "boolean" | |
| ? summaryRecord.configured | |
| : configuredEntries.length > 0; | |
| const status = !anyEnabled | |
| ? "disabled" | |
| : linked !== null | |
| ? linked | |
| ? "linked" | |
| : "not linked" | |
| : configured | |
| ? "configured" | |
| : "not configured"; | |
| const statusColor = | |
| status === "linked" || status === "configured" | |
| ? theme.success | |
| : status === "not linked" | |
| ? theme.error | |
| : theme.muted; | |
| const baseLabel = plugin.meta.label ?? plugin.id; | |
| let line = `${baseLabel}: ${status}`; | |
| const authAgeMs = | |
| summaryRecord && typeof summaryRecord.authAgeMs === "number" ? summaryRecord.authAgeMs : null; | |
| const self = summaryRecord?.self as { e164?: string | null } | undefined; | |
| if (self?.e164) { | |
| line += ` ${self.e164}`; | |
| } | |
| if (authAgeMs != null && authAgeMs >= 0) { | |
| line += ` auth ${formatAge(authAgeMs)}`; | |
| } | |
| lines.push(tint(line, statusColor)); | |
| if (configuredEntries.length > 0) { | |
| for (const entry of configuredEntries) { | |
| const details = buildAccountDetails({ | |
| entry, | |
| plugin, | |
| cfg: effective, | |
| includeAllowFrom: resolved.includeAllowFrom, | |
| }); | |
| lines.push( | |
| accountLine( | |
| formatAccountLabel({ | |
| accountId: entry.accountId, | |
| name: entry.snapshot.name, | |
| }), | |
| details, | |
| ), | |
| ); | |
| } | |
| } | |
| } | |
| return lines; | |
| } | |
| export function formatAge(ms: number): string { | |
| if (ms < 0) { | |
| return "unknown"; | |
| } | |
| const minutes = Math.round(ms / 60_000); | |
| if (minutes < 1) { | |
| return "just now"; | |
| } | |
| if (minutes < 60) { | |
| return `${minutes}m ago`; | |
| } | |
| const hours = Math.round(minutes / 60); | |
| if (hours < 48) { | |
| return `${hours}h ago`; | |
| } | |
| const days = Math.round(hours / 24); | |
| return `${days}d ago`; | |
| } | |