refactor: centralize computed channel status adapters

This commit is contained in:
Peter Steinberger
2026-03-22 21:15:09 +00:00
parent 29c0e783b1
commit 87b2672126
8 changed files with 185 additions and 104 deletions

View File

@@ -16,6 +16,7 @@ import {
} from "openclaw/plugin-sdk/channel-policy";
import { createAttachedChannelResultAdapter } from "openclaw/plugin-sdk/channel-send-result";
import { createLazyRuntimeNamedExport } from "openclaw/plugin-sdk/lazy-runtime";
import { createComputedAccountStatusAdapter } from "openclaw/plugin-sdk/status-helpers";
import {
listBlueBubblesAccountIds,
type ResolvedBlueBubblesAccount,
@@ -32,7 +33,6 @@ import {
import type { ChannelAccountSnapshot, ChannelPlugin } from "./runtime-api.js";
import {
buildChannelConfigSchema,
buildComputedAccountStatusSnapshot,
buildProbeChannelStatusSummary,
collectBlueBubblesStatusIssues,
DEFAULT_ACCOUNT_ID,
@@ -305,7 +305,7 @@ export const bluebubblesPlugin: ChannelPlugin<ResolvedBlueBubblesAccount> = {
},
}),
},
status: {
status: createComputedAccountStatusAdapter<ResolvedBlueBubblesAccount, BlueBubblesProbe>({
defaultRuntime: {
accountId: DEFAULT_ACCOUNT_ID,
running: false,
@@ -322,25 +322,21 @@ export const bluebubblesPlugin: ChannelPlugin<ResolvedBlueBubblesAccount> = {
password: account.config.password ?? null,
timeoutMs,
}),
buildAccountSnapshot: ({ account, runtime, probe }) => {
resolveAccountSnapshot: ({ account, runtime, probe }) => {
const running = runtime?.running ?? false;
const probeOk = (probe as BlueBubblesProbe | undefined)?.ok;
return buildComputedAccountStatusSnapshot(
{
accountId: account.accountId,
name: account.name,
enabled: account.enabled,
configured: account.configured,
runtime,
probe,
},
{
const probeOk = probe?.ok;
return {
accountId: account.accountId,
name: account.name,
enabled: account.enabled,
configured: account.configured,
extra: {
baseUrl: account.baseUrl,
connected: probeOk ?? running,
},
);
};
},
},
}),
gateway: {
startAccount: async (ctx) => {
const runtime = await loadBlueBubblesChannelRuntime();

View File

@@ -28,6 +28,7 @@ import {
resolveThreadSessionKeys,
type RoutePeer,
} from "openclaw/plugin-sdk/routing";
import { createComputedAccountStatusAdapter } from "openclaw/plugin-sdk/status-helpers";
import {
listDiscordAccountIds,
resolveDiscordAccount,
@@ -54,7 +55,6 @@ import {
import { probeDiscord, type DiscordProbe } from "./probe.js";
import { resolveDiscordUserAllowlist } from "./resolve-users.js";
import {
buildComputedAccountStatusSnapshot,
buildTokenChannelStatusSummary,
type ChannelMessageActionAdapter,
type ChannelPlugin,
@@ -499,7 +499,7 @@ export const discordPlugin: ChannelPlugin<ResolvedDiscordAccount> = {
parentConversationId,
}),
},
status: {
status: createComputedAccountStatusAdapter<ResolvedDiscordAccount, DiscordProbe, unknown>({
defaultRuntime: {
accountId: DEFAULT_ACCOUNT_ID,
running: false,
@@ -630,21 +630,17 @@ export const discordPlugin: ChannelPlugin<ResolvedDiscordAccount> = {
});
return { ...audit, unresolvedChannels };
},
buildAccountSnapshot: ({ account, runtime, probe, audit }) => {
resolveAccountSnapshot: ({ account, runtime, probe, audit }) => {
const configured =
resolveConfiguredFromCredentialStatuses(account) ?? Boolean(account.token?.trim());
const app = runtime?.application ?? (probe as { application?: unknown })?.application;
const bot = runtime?.bot ?? (probe as { bot?: unknown })?.bot;
return buildComputedAccountStatusSnapshot(
{
accountId: account.accountId,
name: account.name,
enabled: account.enabled,
configured,
runtime,
probe,
},
{
return {
accountId: account.accountId,
name: account.name,
enabled: account.enabled,
configured,
extra: {
...projectCredentialSnapshotFields(account),
connected: runtime?.connected ?? false,
reconnectAttempts: runtime?.reconnectAttempts,
@@ -655,9 +651,9 @@ export const discordPlugin: ChannelPlugin<ResolvedDiscordAccount> = {
bot: bot ?? undefined,
audit,
},
);
};
},
},
}),
gateway: {
startAccount: async (ctx) => {
const account = ctx.account;

View File

@@ -18,8 +18,8 @@ import {
} from "openclaw/plugin-sdk/directory-runtime";
import { buildPassiveProbedChannelStatusSummary } from "openclaw/plugin-sdk/extension-shared";
import { createLazyRuntimeNamedExport } from "openclaw/plugin-sdk/lazy-runtime";
import { createComputedAccountStatusAdapter } from "openclaw/plugin-sdk/status-helpers";
import {
buildComputedAccountStatusSnapshot,
buildChannelConfigSchema,
DEFAULT_ACCOUNT_ID,
createAccountStatusSink,
@@ -207,7 +207,7 @@ export const googlechatPlugin = createChatChannelPlugin({
},
},
actions: googlechatActions,
status: {
status: createComputedAccountStatusAdapter<ResolvedGoogleChatAccount>({
defaultRuntime: {
accountId: DEFAULT_ACCOUNT_ID,
running: false,
@@ -254,26 +254,21 @@ export const googlechatPlugin = createChatChannelPlugin({
}),
probeAccount: async ({ account }) =>
(await loadGoogleChatChannelRuntime()).probeGoogleChat(account),
buildAccountSnapshot: ({ account, runtime, probe }) =>
buildComputedAccountStatusSnapshot(
{
accountId: account.accountId,
name: account.name,
enabled: account.enabled,
configured: account.credentialSource !== "none",
runtime,
probe,
},
{
credentialSource: account.credentialSource,
audienceType: account.config.audienceType,
audience: account.config.audience,
webhookPath: account.config.webhookPath,
webhookUrl: account.config.webhookUrl,
dmPolicy: account.config.dm?.policy ?? "pairing",
},
),
},
resolveAccountSnapshot: ({ account }) => ({
accountId: account.accountId,
name: account.name,
enabled: account.enabled,
configured: account.credentialSource !== "none",
extra: {
credentialSource: account.credentialSource,
audienceType: account.config.audienceType,
audience: account.config.audience,
webhookPath: account.config.webhookPath,
webhookUrl: account.config.webhookUrl,
dmPolicy: account.config.dm?.policy ?? "pairing",
},
}),
}),
gateway: {
startAccount: async (ctx) => {
const account = ctx.account;

View File

@@ -10,8 +10,8 @@ import {
} from "openclaw/plugin-sdk/channel-send-result";
import { createEmptyChannelDirectoryAdapter } from "openclaw/plugin-sdk/directory-runtime";
import { resolveOutboundMediaUrls } from "openclaw/plugin-sdk/reply-payload";
import { createComputedAccountStatusAdapter } from "openclaw/plugin-sdk/status-helpers";
import {
buildComputedAccountStatusSnapshot,
buildTokenChannelStatusSummary,
clearAccountEntryFields,
DEFAULT_ACCOUNT_ID,
@@ -323,7 +323,7 @@ export const linePlugin: ChannelPlugin<ResolvedLineAccount> = {
}),
}),
},
status: {
status: createComputedAccountStatusAdapter<ResolvedLineAccount>({
defaultRuntime: {
accountId: DEFAULT_ACCOUNT_ID,
running: false,
@@ -357,26 +357,22 @@ export const linePlugin: ChannelPlugin<ResolvedLineAccount> = {
buildChannelSummary: ({ snapshot }) => buildTokenChannelStatusSummary(snapshot),
probeAccount: async ({ account, timeoutMs }) =>
getLineRuntime().channel.line.probeLineBot(account.channelAccessToken, timeoutMs),
buildAccountSnapshot: ({ account, runtime, probe }) => {
resolveAccountSnapshot: ({ account }) => {
const configured = Boolean(
account.channelAccessToken?.trim() && account.channelSecret?.trim(),
);
return buildComputedAccountStatusSnapshot(
{
accountId: account.accountId,
name: account.name,
enabled: account.enabled,
configured,
runtime,
probe,
},
{
return {
accountId: account.accountId,
name: account.name,
enabled: account.enabled,
configured,
extra: {
tokenSource: account.tokenSource,
mode: "webhook",
},
);
};
},
},
}),
gateway: {
startAccount: async (ctx) => {
const account = ctx.account;

View File

@@ -17,6 +17,7 @@ import { createAttachedChannelResultAdapter } from "openclaw/plugin-sdk/channel-
import { createScopedAccountReplyToModeResolver } from "openclaw/plugin-sdk/conversation-runtime";
import { createChannelDirectoryAdapter } from "openclaw/plugin-sdk/directory-runtime";
import { buildPassiveProbedChannelStatusSummary } from "openclaw/plugin-sdk/extension-shared";
import { createComputedAccountStatusAdapter } from "openclaw/plugin-sdk/status-helpers";
import { MattermostConfigSchema } from "./config-schema.js";
import { resolveMattermostGroupRequireMention } from "./group-mentions.js";
import {
@@ -37,7 +38,6 @@ import { sendMessageMattermost } from "./mattermost/send.js";
import { resolveMattermostOpaqueTarget } from "./mattermost/target-resolution.js";
import { looksLikeMattermostTargetId, normalizeMattermostMessagingTarget } from "./normalize.js";
import {
buildComputedAccountStatusSnapshot,
buildChannelConfigSchema,
createAccountStatusSink,
DEFAULT_ACCOUNT_ID,
@@ -420,7 +420,7 @@ export const mattermostPlugin: ChannelPlugin<ResolvedMattermostAccount> = {
}),
}),
},
status: {
status: createComputedAccountStatusAdapter<ResolvedMattermostAccount>({
defaultRuntime: {
accountId: DEFAULT_ACCOUNT_ID,
running: false,
@@ -445,25 +445,20 @@ export const mattermostPlugin: ChannelPlugin<ResolvedMattermostAccount> = {
}
return await probeMattermost(baseUrl, token, timeoutMs);
},
buildAccountSnapshot: ({ account, runtime, probe }) =>
buildComputedAccountStatusSnapshot(
{
accountId: account.accountId,
name: account.name,
enabled: account.enabled,
configured: Boolean(account.botToken && account.baseUrl),
runtime,
probe,
},
{
botTokenSource: account.botTokenSource,
baseUrl: account.baseUrl,
connected: runtime?.connected ?? false,
lastConnectedAt: runtime?.lastConnectedAt ?? null,
lastDisconnect: runtime?.lastDisconnect ?? null,
},
),
},
resolveAccountSnapshot: ({ account, runtime }) => ({
accountId: account.accountId,
name: account.name,
enabled: account.enabled,
configured: Boolean(account.botToken && account.baseUrl),
extra: {
botTokenSource: account.botTokenSource,
baseUrl: account.baseUrl,
connected: runtime?.connected ?? false,
lastConnectedAt: runtime?.lastConnectedAt ?? null,
lastDisconnect: runtime?.lastDisconnect ?? null,
},
}),
}),
gateway: {
startAccount: async (ctx) => {
const account = ctx.account;

View File

@@ -30,6 +30,7 @@ import {
resolveThreadSessionKeys,
type RoutePeer,
} from "openclaw/plugin-sdk/routing";
import { createComputedAccountStatusAdapter } from "openclaw/plugin-sdk/status-helpers";
import {
listEnabledSlackAccounts,
resolveSlackAccount,
@@ -50,7 +51,6 @@ import { normalizeAllowListLower } from "./monitor/allow-list.js";
import type { SlackProbe } from "./probe.js";
import { resolveSlackUserAllowlist } from "./resolve-users.js";
import {
buildComputedAccountStatusSnapshot,
DEFAULT_ACCOUNT_ID,
looksLikeSlackTargetId,
normalizeSlackMessagingTarget,
@@ -556,7 +556,7 @@ export const slackPlugin: ChannelPlugin<ResolvedSlackAccount> = {
},
}),
},
status: {
status: createComputedAccountStatusAdapter<ResolvedSlackAccount, SlackProbe>({
defaultRuntime: {
accountId: DEFAULT_ACCOUNT_ID,
running: false,
@@ -605,7 +605,7 @@ export const slackPlugin: ChannelPlugin<ResolvedSlackAccount> = {
}
return { lines, details };
},
buildAccountSnapshot: ({ account, runtime, probe }) => {
resolveAccountSnapshot: ({ account }) => {
const mode = account.config.mode ?? "socket";
const configured =
(mode === "http"
@@ -617,21 +617,17 @@ export const slackPlugin: ChannelPlugin<ResolvedSlackAccount> = {
"botTokenStatus",
"appTokenStatus",
])) ?? isSlackPluginAccountConfigured(account);
return buildComputedAccountStatusSnapshot(
{
accountId: account.accountId,
name: account.name,
enabled: account.enabled,
configured,
runtime,
probe,
},
{
return {
accountId: account.accountId,
name: account.name,
enabled: account.enabled,
configured,
extra: {
...projectCredentialSnapshotFields(account),
},
);
};
},
},
}),
gateway: {
startAccount: async (ctx) => {
const account = ctx.account;

View File

@@ -4,6 +4,7 @@ import {
buildBaseChannelStatusSummary,
buildComputedAccountStatusSnapshot,
buildRuntimeAccountStatusSnapshot,
createComputedAccountStatusAdapter,
buildTokenChannelStatusSummary,
collectStatusIssuesFromLastError,
createDefaultChannelRuntimeState,
@@ -191,6 +192,50 @@ describe("buildComputedAccountStatusSnapshot", () => {
});
});
describe("createComputedAccountStatusAdapter", () => {
it("builds account snapshots from computed account metadata and extras", () => {
const status = createComputedAccountStatusAdapter<
{ accountId: string; enabled: boolean; profileUrl: string },
{ ok: boolean }
>({
defaultRuntime: createDefaultChannelRuntimeState("default"),
resolveAccountSnapshot: ({ account, runtime, probe }) => ({
accountId: account.accountId,
enabled: account.enabled,
configured: true,
extra: {
profileUrl: account.profileUrl,
connected: runtime?.running ?? false,
probe,
},
}),
});
expect(
status.buildAccountSnapshot?.({
account: { accountId: "default", enabled: true, profileUrl: "https://example.test" },
cfg: {} as never,
runtime: { accountId: "default", running: true },
probe: { ok: true },
}),
).toEqual({
accountId: "default",
name: undefined,
enabled: true,
configured: true,
running: true,
lastStartAt: null,
lastStopAt: null,
lastError: null,
probe: { ok: true },
lastInboundAt: null,
lastOutboundAt: null,
profileUrl: "https://example.test",
connected: true,
});
});
});
describe("buildRuntimeAccountStatusSnapshot", () => {
it("builds runtime lifecycle fields with defaults", () => {
expect(buildRuntimeAccountStatusSnapshot({})).toEqual({

View File

@@ -1,4 +1,7 @@
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,
@@ -19,6 +22,21 @@ type RuntimeLifecycleSnapshot = {
type StatusSnapshotExtra = Record<string, unknown>;
type ComputedAccountStatusBase = {
accountId: string;
name?: string;
enabled?: boolean;
configured?: boolean;
};
type ComputedAccountStatusAdapterParams<ResolvedAccount, Probe, Audit> = {
account: ResolvedAccount;
cfg: OpenClawConfig;
runtime?: ChannelAccountSnapshot;
probe?: Probe;
audit?: Audit;
};
/** Create the baseline runtime snapshot shape used by channel/account status stores. */
export function createDefaultChannelRuntimeState<T extends Record<string, unknown>>(
accountId: string,
@@ -136,6 +154,50 @@ export function buildComputedAccountStatusSnapshot<TExtra extends StatusSnapshot
);
}
/** 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<ChannelStatusAdapter<ResolvedAccount, Probe, Audit>, "buildAccountSnapshot"> & {
resolveAccountSnapshot: (
params: ComputedAccountStatusAdapterParams<ResolvedAccount, Probe, Audit>,
) => ComputedAccountStatusBase & { extra?: TExtra };
},
): ChannelStatusAdapter<ResolvedAccount> {
return {
defaultRuntime: options.defaultRuntime,
buildChannelSummary: options.buildChannelSummary,
probeAccount: options.probeAccount,
formatCapabilitiesProbe:
options.formatCapabilitiesProbe as ChannelStatusAdapter<ResolvedAccount>["formatCapabilitiesProbe"],
auditAccount: options.auditAccount as ChannelStatusAdapter<ResolvedAccount>["auditAccount"],
buildCapabilitiesDiagnostics:
options.buildCapabilitiesDiagnostics as ChannelStatusAdapter<ResolvedAccount>["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,
);
},
};
}
/** Normalize runtime-only account state into the shared status snapshot fields. */
export function buildRuntimeAccountStatusSnapshot<TExtra extends StatusSnapshotExtra>(
params: {