Spaces:
Paused
Paused
| import type { ChannelAccountSnapshot, ChannelPlugin } from "../../channels/plugins/types.js"; | |
| import type { OpenClawConfig } from "../../config/config.js"; | |
| import type { GatewayRequestContext, GatewayRequestHandlers } from "./types.js"; | |
| import { buildChannelUiCatalog } from "../../channels/plugins/catalog.js"; | |
| import { resolveChannelDefaultAccountId } from "../../channels/plugins/helpers.js"; | |
| import { | |
| type ChannelId, | |
| getChannelPlugin, | |
| listChannelPlugins, | |
| normalizeChannelId, | |
| } from "../../channels/plugins/index.js"; | |
| import { buildChannelAccountSnapshot } from "../../channels/plugins/status.js"; | |
| import { loadConfig, readConfigFileSnapshot } from "../../config/config.js"; | |
| import { getChannelActivity } from "../../infra/channel-activity.js"; | |
| import { DEFAULT_ACCOUNT_ID } from "../../routing/session-key.js"; | |
| import { defaultRuntime } from "../../runtime.js"; | |
| import { | |
| ErrorCodes, | |
| errorShape, | |
| formatValidationErrors, | |
| validateChannelsLogoutParams, | |
| validateChannelsStatusParams, | |
| } from "../protocol/index.js"; | |
| import { formatForLog } from "../ws-log.js"; | |
| type ChannelLogoutPayload = { | |
| channel: ChannelId; | |
| accountId: string; | |
| cleared: boolean; | |
| [key: string]: unknown; | |
| }; | |
| export async function logoutChannelAccount(params: { | |
| channelId: ChannelId; | |
| accountId?: string | null; | |
| cfg: OpenClawConfig; | |
| context: GatewayRequestContext; | |
| plugin: ChannelPlugin; | |
| }): Promise<ChannelLogoutPayload> { | |
| const resolvedAccountId = | |
| params.accountId?.trim() || | |
| params.plugin.config.defaultAccountId?.(params.cfg) || | |
| params.plugin.config.listAccountIds(params.cfg)[0] || | |
| DEFAULT_ACCOUNT_ID; | |
| const account = params.plugin.config.resolveAccount(params.cfg, resolvedAccountId); | |
| await params.context.stopChannel(params.channelId, resolvedAccountId); | |
| const result = await params.plugin.gateway?.logoutAccount?.({ | |
| cfg: params.cfg, | |
| accountId: resolvedAccountId, | |
| account, | |
| runtime: defaultRuntime, | |
| }); | |
| if (!result) { | |
| throw new Error(`Channel ${params.channelId} does not support logout`); | |
| } | |
| const cleared = Boolean(result.cleared); | |
| const loggedOut = typeof result.loggedOut === "boolean" ? result.loggedOut : cleared; | |
| if (loggedOut) { | |
| params.context.markChannelLoggedOut(params.channelId, true, resolvedAccountId); | |
| } | |
| return { | |
| channel: params.channelId, | |
| accountId: resolvedAccountId, | |
| ...result, | |
| cleared, | |
| }; | |
| } | |
| export const channelsHandlers: GatewayRequestHandlers = { | |
| "channels.status": async ({ params, respond, context }) => { | |
| if (!validateChannelsStatusParams(params)) { | |
| respond( | |
| false, | |
| undefined, | |
| errorShape( | |
| ErrorCodes.INVALID_REQUEST, | |
| `invalid channels.status params: ${formatValidationErrors(validateChannelsStatusParams.errors)}`, | |
| ), | |
| ); | |
| return; | |
| } | |
| const probe = (params as { probe?: boolean }).probe === true; | |
| const timeoutMsRaw = (params as { timeoutMs?: unknown }).timeoutMs; | |
| const timeoutMs = typeof timeoutMsRaw === "number" ? Math.max(1000, timeoutMsRaw) : 10_000; | |
| const cfg = loadConfig(); | |
| const runtime = context.getRuntimeSnapshot(); | |
| const plugins = listChannelPlugins(); | |
| const pluginMap = new Map<ChannelId, ChannelPlugin>( | |
| plugins.map((plugin) => [plugin.id, plugin]), | |
| ); | |
| const resolveRuntimeSnapshot = ( | |
| channelId: ChannelId, | |
| accountId: string, | |
| defaultAccountId: string, | |
| ): ChannelAccountSnapshot | undefined => { | |
| const accounts = runtime.channelAccounts[channelId]; | |
| const defaultRuntime = runtime.channels[channelId]; | |
| const raw = | |
| accounts?.[accountId] ?? (accountId === defaultAccountId ? defaultRuntime : undefined); | |
| if (!raw) { | |
| return undefined; | |
| } | |
| return raw; | |
| }; | |
| const isAccountEnabled = (plugin: ChannelPlugin, account: unknown) => | |
| plugin.config.isEnabled | |
| ? plugin.config.isEnabled(account, cfg) | |
| : !account || | |
| typeof account !== "object" || | |
| (account as { enabled?: boolean }).enabled !== false; | |
| const buildChannelAccounts = async (channelId: ChannelId) => { | |
| const plugin = pluginMap.get(channelId); | |
| if (!plugin) { | |
| return { | |
| accounts: [] as ChannelAccountSnapshot[], | |
| defaultAccountId: DEFAULT_ACCOUNT_ID, | |
| defaultAccount: undefined as ChannelAccountSnapshot | undefined, | |
| resolvedAccounts: {} as Record<string, unknown>, | |
| }; | |
| } | |
| const accountIds = plugin.config.listAccountIds(cfg); | |
| const defaultAccountId = resolveChannelDefaultAccountId({ | |
| plugin, | |
| cfg, | |
| accountIds, | |
| }); | |
| const accounts: ChannelAccountSnapshot[] = []; | |
| const resolvedAccounts: Record<string, unknown> = {}; | |
| for (const accountId of accountIds) { | |
| const account = plugin.config.resolveAccount(cfg, accountId); | |
| const enabled = isAccountEnabled(plugin, account); | |
| resolvedAccounts[accountId] = account; | |
| let probeResult: unknown; | |
| let lastProbeAt: number | null = null; | |
| if (probe && enabled && plugin.status?.probeAccount) { | |
| let configured = true; | |
| if (plugin.config.isConfigured) { | |
| configured = await plugin.config.isConfigured(account, cfg); | |
| } | |
| if (configured) { | |
| probeResult = await plugin.status.probeAccount({ | |
| account, | |
| timeoutMs, | |
| cfg, | |
| }); | |
| lastProbeAt = Date.now(); | |
| } | |
| } | |
| let auditResult: unknown; | |
| if (probe && enabled && plugin.status?.auditAccount) { | |
| let configured = true; | |
| if (plugin.config.isConfigured) { | |
| configured = await plugin.config.isConfigured(account, cfg); | |
| } | |
| if (configured) { | |
| auditResult = await plugin.status.auditAccount({ | |
| account, | |
| timeoutMs, | |
| cfg, | |
| probe: probeResult, | |
| }); | |
| } | |
| } | |
| const runtimeSnapshot = resolveRuntimeSnapshot(channelId, accountId, defaultAccountId); | |
| const snapshot = await buildChannelAccountSnapshot({ | |
| plugin, | |
| cfg, | |
| accountId, | |
| runtime: runtimeSnapshot, | |
| probe: probeResult, | |
| audit: auditResult, | |
| }); | |
| if (lastProbeAt) { | |
| snapshot.lastProbeAt = lastProbeAt; | |
| } | |
| const activity = getChannelActivity({ | |
| channel: channelId as never, | |
| accountId, | |
| }); | |
| if (snapshot.lastInboundAt == null) { | |
| snapshot.lastInboundAt = activity.inboundAt; | |
| } | |
| if (snapshot.lastOutboundAt == null) { | |
| snapshot.lastOutboundAt = activity.outboundAt; | |
| } | |
| accounts.push(snapshot); | |
| } | |
| const defaultAccount = | |
| accounts.find((entry) => entry.accountId === defaultAccountId) ?? accounts[0]; | |
| return { accounts, defaultAccountId, defaultAccount, resolvedAccounts }; | |
| }; | |
| const uiCatalog = buildChannelUiCatalog(plugins); | |
| const payload: Record<string, unknown> = { | |
| ts: Date.now(), | |
| channelOrder: uiCatalog.order, | |
| channelLabels: uiCatalog.labels, | |
| channelDetailLabels: uiCatalog.detailLabels, | |
| channelSystemImages: uiCatalog.systemImages, | |
| channelMeta: uiCatalog.entries, | |
| channels: {} as Record<string, unknown>, | |
| channelAccounts: {} as Record<string, unknown>, | |
| channelDefaultAccountId: {} as Record<string, unknown>, | |
| }; | |
| const channelsMap = payload.channels as Record<string, unknown>; | |
| const accountsMap = payload.channelAccounts as Record<string, unknown>; | |
| const defaultAccountIdMap = payload.channelDefaultAccountId as Record<string, unknown>; | |
| for (const plugin of plugins) { | |
| const { accounts, defaultAccountId, defaultAccount, resolvedAccounts } = | |
| await buildChannelAccounts(plugin.id); | |
| const fallbackAccount = | |
| resolvedAccounts[defaultAccountId] ?? plugin.config.resolveAccount(cfg, defaultAccountId); | |
| const summary = plugin.status?.buildChannelSummary | |
| ? await plugin.status.buildChannelSummary({ | |
| account: fallbackAccount, | |
| cfg, | |
| defaultAccountId, | |
| snapshot: | |
| defaultAccount ?? | |
| ({ | |
| accountId: defaultAccountId, | |
| } as ChannelAccountSnapshot), | |
| }) | |
| : { | |
| configured: defaultAccount?.configured ?? false, | |
| }; | |
| channelsMap[plugin.id] = summary; | |
| accountsMap[plugin.id] = accounts; | |
| defaultAccountIdMap[plugin.id] = defaultAccountId; | |
| } | |
| respond(true, payload, undefined); | |
| }, | |
| "channels.logout": async ({ params, respond, context }) => { | |
| if (!validateChannelsLogoutParams(params)) { | |
| respond( | |
| false, | |
| undefined, | |
| errorShape( | |
| ErrorCodes.INVALID_REQUEST, | |
| `invalid channels.logout params: ${formatValidationErrors(validateChannelsLogoutParams.errors)}`, | |
| ), | |
| ); | |
| return; | |
| } | |
| const rawChannel = (params as { channel?: unknown }).channel; | |
| const channelId = typeof rawChannel === "string" ? normalizeChannelId(rawChannel) : null; | |
| if (!channelId) { | |
| respond( | |
| false, | |
| undefined, | |
| errorShape(ErrorCodes.INVALID_REQUEST, "invalid channels.logout channel"), | |
| ); | |
| return; | |
| } | |
| const plugin = getChannelPlugin(channelId); | |
| if (!plugin?.gateway?.logoutAccount) { | |
| respond( | |
| false, | |
| undefined, | |
| errorShape(ErrorCodes.INVALID_REQUEST, `channel ${channelId} does not support logout`), | |
| ); | |
| return; | |
| } | |
| const accountIdRaw = (params as { accountId?: unknown }).accountId; | |
| const accountId = typeof accountIdRaw === "string" ? accountIdRaw.trim() : undefined; | |
| const snapshot = await readConfigFileSnapshot(); | |
| if (!snapshot.valid) { | |
| respond( | |
| false, | |
| undefined, | |
| errorShape(ErrorCodes.INVALID_REQUEST, "config invalid; fix it before logging out"), | |
| ); | |
| return; | |
| } | |
| try { | |
| const payload = await logoutChannelAccount({ | |
| channelId, | |
| accountId, | |
| cfg: snapshot.config ?? {}, | |
| context, | |
| plugin, | |
| }); | |
| respond(true, payload, undefined); | |
| } catch (err) { | |
| respond(false, undefined, errorShape(ErrorCodes.UNAVAILABLE, formatForLog(err))); | |
| } | |
| }, | |
| }; | |