import type { ChannelStatusAdapter } from "../channels/plugins/types.adapters.js"; import type { ChannelAccountSnapshot } from "../channels/plugins/types.core.js"; import type { ChannelStatusIssue } from "../channels/plugins/types.js"; import type { OpenClawConfig } from "../config/config.js"; export { isRecord } from "../channels/plugins/status-issues/shared.js"; export { appendMatchMetadata, asString, collectIssuesForEnabledAccounts, formatMatchMetadata, resolveEnabledConfiguredAccountId, } from "../channels/plugins/status-issues/shared.js"; type RuntimeLifecycleSnapshot = { running?: boolean | null; lastStartAt?: number | null; lastStopAt?: number | null; lastError?: string | null; lastInboundAt?: number | null; lastOutboundAt?: number | null; }; type StatusSnapshotExtra = Record; type ComputedAccountStatusBase = { accountId: string; name?: string; enabled?: boolean; configured?: boolean; }; type ComputedAccountStatusAdapterParams = { account: ResolvedAccount; cfg: OpenClawConfig; runtime?: ChannelAccountSnapshot; probe?: Probe; audit?: Audit; }; type ComputedAccountStatusSnapshot = ComputedAccountStatusBase & { extra?: TExtra }; /** Create the baseline runtime snapshot shape used by channel/account status stores. */ export function createDefaultChannelRuntimeState>( accountId: string, extra?: T, ): { accountId: string; running: false; lastStartAt: null; lastStopAt: null; lastError: null; } & T { return { accountId, running: false, lastStartAt: null, lastStopAt: null, lastError: null, ...(extra ?? ({} as T)), }; } /** Normalize a channel-level status summary so missing lifecycle fields become explicit nulls. */ export function buildBaseChannelStatusSummary( snapshot: { configured?: boolean | null; running?: boolean | null; lastStartAt?: number | null; lastStopAt?: number | null; lastError?: string | null; }, extra?: TExtra, ) { return { configured: snapshot.configured ?? false, ...(extra ?? ({} as TExtra)), running: snapshot.running ?? false, lastStartAt: snapshot.lastStartAt ?? null, lastStopAt: snapshot.lastStopAt ?? null, lastError: snapshot.lastError ?? null, }; } /** Extend the base summary with probe fields while preserving stable null defaults. */ export function buildProbeChannelStatusSummary>( snapshot: { configured?: boolean | null; running?: boolean | null; lastStartAt?: number | null; lastStopAt?: number | null; lastError?: string | null; probe?: unknown; lastProbeAt?: number | null; }, extra?: TExtra, ) { return { ...buildBaseChannelStatusSummary(snapshot, extra), probe: snapshot.probe, lastProbeAt: snapshot.lastProbeAt ?? null, }; } /** Build the standard per-account status payload from config metadata plus runtime state. */ export function buildBaseAccountStatusSnapshot( params: { account: { accountId: string; name?: string; enabled?: boolean; configured?: boolean; }; runtime?: RuntimeLifecycleSnapshot | null; probe?: unknown; }, extra?: TExtra, ) { const { account, runtime, probe } = params; return { accountId: account.accountId, name: account.name, enabled: account.enabled, configured: account.configured, ...buildRuntimeAccountStatusSnapshot({ runtime, probe }), lastInboundAt: runtime?.lastInboundAt ?? null, lastOutboundAt: runtime?.lastOutboundAt ?? null, ...(extra ?? ({} as TExtra)), }; } /** Convenience wrapper when the caller already has flattened account fields instead of an account object. */ export function buildComputedAccountStatusSnapshot( params: { accountId: string; name?: string; enabled?: boolean; configured?: boolean; runtime?: RuntimeLifecycleSnapshot | null; probe?: unknown; }, extra?: TExtra, ) { const { accountId, name, enabled, configured, runtime, probe } = params; return buildBaseAccountStatusSnapshot( { account: { accountId, name, enabled, configured, }, runtime, probe, }, extra, ); } /** Build a full status adapter when only configured/extras vary per account. */ export function createComputedAccountStatusAdapter< ResolvedAccount, Probe = unknown, Audit = unknown, TExtra extends StatusSnapshotExtra = StatusSnapshotExtra, >( options: Omit, "buildAccountSnapshot"> & { resolveAccountSnapshot: ( params: ComputedAccountStatusAdapterParams, ) => ComputedAccountStatusSnapshot; }, ): ChannelStatusAdapter { return { defaultRuntime: options.defaultRuntime, buildChannelSummary: options.buildChannelSummary, probeAccount: options.probeAccount, formatCapabilitiesProbe: options.formatCapabilitiesProbe, auditAccount: options.auditAccount, buildCapabilitiesDiagnostics: options.buildCapabilitiesDiagnostics, logSelfId: options.logSelfId, resolveAccountState: options.resolveAccountState, collectStatusIssues: options.collectStatusIssues, buildAccountSnapshot: (params) => { const typedParams = params as ComputedAccountStatusAdapterParams< ResolvedAccount, Probe, Audit >; const { extra, ...snapshot } = options.resolveAccountSnapshot(typedParams); return buildComputedAccountStatusSnapshot( { ...snapshot, runtime: typedParams.runtime, probe: typedParams.probe, }, extra, ); }, }; } /** Async variant for channels that compute configured state or snapshot extras from I/O. */ export function createAsyncComputedAccountStatusAdapter< ResolvedAccount, Probe = unknown, Audit = unknown, TExtra extends StatusSnapshotExtra = StatusSnapshotExtra, >( options: Omit, "buildAccountSnapshot"> & { resolveAccountSnapshot: ( params: ComputedAccountStatusAdapterParams, ) => Promise>; }, ): ChannelStatusAdapter { return { defaultRuntime: options.defaultRuntime, buildChannelSummary: options.buildChannelSummary, probeAccount: options.probeAccount, formatCapabilitiesProbe: options.formatCapabilitiesProbe, auditAccount: options.auditAccount, buildCapabilitiesDiagnostics: options.buildCapabilitiesDiagnostics, logSelfId: options.logSelfId, resolveAccountState: options.resolveAccountState, collectStatusIssues: options.collectStatusIssues, buildAccountSnapshot: async (params) => { const typedParams = params as ComputedAccountStatusAdapterParams< ResolvedAccount, Probe, Audit >; const { extra, ...snapshot } = await options.resolveAccountSnapshot(typedParams); return buildComputedAccountStatusSnapshot( { ...snapshot, runtime: typedParams.runtime, probe: typedParams.probe, }, extra, ); }, }; } /** Normalize runtime-only account state into the shared status snapshot fields. */ export function buildRuntimeAccountStatusSnapshot( params: { runtime?: RuntimeLifecycleSnapshot | null; probe?: unknown; }, extra?: TExtra, ) { const { runtime, probe } = params; return { running: runtime?.running ?? false, lastStartAt: runtime?.lastStartAt ?? null, lastStopAt: runtime?.lastStopAt ?? null, lastError: runtime?.lastError ?? null, probe, ...(extra ?? ({} as TExtra)), }; } /** Build token-based channel status summaries with optional mode reporting. */ export function buildTokenChannelStatusSummary( snapshot: { configured?: boolean | null; tokenSource?: string | null; running?: boolean | null; mode?: string | null; lastStartAt?: number | null; lastStopAt?: number | null; lastError?: string | null; probe?: unknown; lastProbeAt?: number | null; }, opts?: { includeMode?: boolean }, ) { const base = { ...buildBaseChannelStatusSummary(snapshot), tokenSource: snapshot.tokenSource ?? "none", probe: snapshot.probe, lastProbeAt: snapshot.lastProbeAt ?? null, }; if (opts?.includeMode === false) { return base; } return { ...base, mode: snapshot.mode ?? null, }; } /** Convert account runtime errors into the generic channel status issue format. */ export function collectStatusIssuesFromLastError( channel: string, accounts: Array<{ accountId: string; lastError?: unknown }>, ): ChannelStatusIssue[] { return accounts.flatMap((account) => { const lastError = typeof account.lastError === "string" ? account.lastError.trim() : ""; if (!lastError) { return []; } return [ { channel, accountId: account.accountId, kind: "runtime", message: `Channel error: ${lastError}`, }, ]; }); }