mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-20 05:31:30 +00:00
refactor: centralize computed channel status adapters
This commit is contained in:
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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: {
|
||||
|
||||
Reference in New Issue
Block a user