diff --git a/extensions/bluebubbles/src/channel.ts b/extensions/bluebubbles/src/channel.ts index 31788a907c2..45802a9d6ec 100644 --- a/extensions/bluebubbles/src/channel.ts +++ b/extensions/bluebubbles/src/channel.ts @@ -325,19 +325,20 @@ export const bluebubblesPlugin: ChannelPlugin = { buildAccountSnapshot: ({ account, runtime, probe }) => { const running = runtime?.running ?? false; const probeOk = (probe as BlueBubblesProbe | undefined)?.ok; - const base = buildComputedAccountStatusSnapshot({ - accountId: account.accountId, - name: account.name, - enabled: account.enabled, - configured: account.configured, - runtime, - probe, - }); - return { - ...base, - baseUrl: account.baseUrl, - connected: probeOk ?? running, - }; + return buildComputedAccountStatusSnapshot( + { + accountId: account.accountId, + name: account.name, + enabled: account.enabled, + configured: account.configured, + runtime, + probe, + }, + { + baseUrl: account.baseUrl, + connected: probeOk ?? running, + }, + ); }, }, gateway: { diff --git a/extensions/discord/src/channel.ts b/extensions/discord/src/channel.ts index c9145b7adea..680cd0efe3a 100644 --- a/extensions/discord/src/channel.ts +++ b/extensions/discord/src/channel.ts @@ -635,26 +635,27 @@ export const discordPlugin: ChannelPlugin = { resolveConfiguredFromCredentialStatuses(account) ?? Boolean(account.token?.trim()); const app = runtime?.application ?? (probe as { application?: unknown })?.application; const bot = runtime?.bot ?? (probe as { bot?: unknown })?.bot; - const base = buildComputedAccountStatusSnapshot({ - accountId: account.accountId, - name: account.name, - enabled: account.enabled, - configured, - runtime, - probe, - }); - return { - ...base, - ...projectCredentialSnapshotFields(account), - connected: runtime?.connected ?? false, - reconnectAttempts: runtime?.reconnectAttempts, - lastConnectedAt: runtime?.lastConnectedAt ?? null, - lastDisconnect: runtime?.lastDisconnect ?? null, - lastEventAt: runtime?.lastEventAt ?? null, - application: app ?? undefined, - bot: bot ?? undefined, - audit, - }; + return buildComputedAccountStatusSnapshot( + { + accountId: account.accountId, + name: account.name, + enabled: account.enabled, + configured, + runtime, + probe, + }, + { + ...projectCredentialSnapshotFields(account), + connected: runtime?.connected ?? false, + reconnectAttempts: runtime?.reconnectAttempts, + lastConnectedAt: runtime?.lastConnectedAt ?? null, + lastDisconnect: runtime?.lastDisconnect ?? null, + lastEventAt: runtime?.lastEventAt ?? null, + application: app ?? undefined, + bot: bot ?? undefined, + audit, + }, + ); }, }, gateway: { diff --git a/extensions/feishu/src/channel.ts b/extensions/feishu/src/channel.ts index 5ecc3c3e05f..9e7eff674c3 100644 --- a/extensions/feishu/src/channel.ts +++ b/extensions/feishu/src/channel.ts @@ -959,16 +959,19 @@ export const feishuPlugin: ChannelPlugin = { }), probeAccount: async ({ account }) => await (await loadFeishuChannelRuntime()).probeFeishu(account), - buildAccountSnapshot: ({ account, runtime, probe }) => ({ - accountId: account.accountId, - enabled: account.enabled, - configured: account.configured, - name: account.name, - appId: account.appId, - domain: account.domain, - ...buildRuntimeAccountStatusSnapshot({ runtime, probe }), - port: runtime?.port ?? null, - }), + buildAccountSnapshot: ({ account, runtime, probe }) => + buildRuntimeAccountStatusSnapshot( + { runtime, probe }, + { + accountId: account.accountId, + enabled: account.enabled, + configured: account.configured, + name: account.name, + appId: account.appId, + domain: account.domain, + port: runtime?.port ?? null, + }, + ), }, gateway: { startAccount: async (ctx) => { diff --git a/extensions/googlechat/src/channel.ts b/extensions/googlechat/src/channel.ts index c592f715eb9..90d57192c13 100644 --- a/extensions/googlechat/src/channel.ts +++ b/extensions/googlechat/src/channel.ts @@ -254,25 +254,25 @@ export const googlechatPlugin = createChatChannelPlugin({ }), probeAccount: async ({ account }) => (await loadGoogleChatChannelRuntime()).probeGoogleChat(account), - buildAccountSnapshot: ({ account, runtime, probe }) => { - const base = buildComputedAccountStatusSnapshot({ - accountId: account.accountId, - name: account.name, - enabled: account.enabled, - configured: account.credentialSource !== "none", - runtime, - probe, - }); - return { - ...base, - 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", - }; - }, + 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", + }, + ), }, gateway: { startAccount: async (ctx) => { diff --git a/extensions/irc/src/channel.ts b/extensions/irc/src/channel.ts index 1e8f5ed5c7c..a249d7d9172 100644 --- a/extensions/irc/src/channel.ts +++ b/extensions/irc/src/channel.ts @@ -308,14 +308,17 @@ export const ircPlugin: ChannelPlugin = { }), probeAccount: async ({ cfg, account, timeoutMs }) => probeIrc(cfg as CoreConfig, { accountId: account.accountId, timeoutMs }), - buildAccountSnapshot: ({ account, runtime, probe }) => ({ - ...buildBaseAccountStatusSnapshot({ account, runtime, probe }), - host: account.host, - port: account.port, - tls: account.tls, - nick: account.nick, - passwordSource: account.passwordSource, - }), + buildAccountSnapshot: ({ account, runtime, probe }) => + buildBaseAccountStatusSnapshot( + { account, runtime, probe }, + { + host: account.host, + port: account.port, + tls: account.tls, + nick: account.nick, + passwordSource: account.passwordSource, + }, + ), }, gateway: { startAccount: async (ctx) => { diff --git a/extensions/line/src/channel.ts b/extensions/line/src/channel.ts index fd81a4c8f8a..841d380dcf4 100644 --- a/extensions/line/src/channel.ts +++ b/extensions/line/src/channel.ts @@ -361,19 +361,20 @@ export const linePlugin: ChannelPlugin = { const configured = Boolean( account.channelAccessToken?.trim() && account.channelSecret?.trim(), ); - const base = buildComputedAccountStatusSnapshot({ - accountId: account.accountId, - name: account.name, - enabled: account.enabled, - configured, - runtime, - probe, - }); - return { - ...base, - tokenSource: account.tokenSource, - mode: "webhook", - }; + return buildComputedAccountStatusSnapshot( + { + accountId: account.accountId, + name: account.name, + enabled: account.enabled, + configured, + runtime, + probe, + }, + { + tokenSource: account.tokenSource, + mode: "webhook", + }, + ); }, }, gateway: { diff --git a/extensions/mattermost/src/channel.ts b/extensions/mattermost/src/channel.ts index 3073c7756ed..3ef6834e704 100644 --- a/extensions/mattermost/src/channel.ts +++ b/extensions/mattermost/src/channel.ts @@ -445,24 +445,24 @@ export const mattermostPlugin: ChannelPlugin = { } return await probeMattermost(baseUrl, token, timeoutMs); }, - buildAccountSnapshot: ({ account, runtime, probe }) => { - const base = buildComputedAccountStatusSnapshot({ - accountId: account.accountId, - name: account.name, - enabled: account.enabled, - configured: Boolean(account.botToken && account.baseUrl), - runtime, - probe, - }); - return { - ...base, - botTokenSource: account.botTokenSource, - baseUrl: account.baseUrl, - connected: runtime?.connected ?? false, - lastConnectedAt: runtime?.lastConnectedAt ?? null, - lastDisconnect: runtime?.lastDisconnect ?? null, - }; - }, + 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, + }, + ), }, gateway: { startAccount: async (ctx) => { diff --git a/extensions/msteams/src/channel.ts b/extensions/msteams/src/channel.ts index 14637c4cf66..c2cb343a9fb 100644 --- a/extensions/msteams/src/channel.ts +++ b/extensions/msteams/src/channel.ts @@ -490,13 +490,16 @@ export const msteamsPlugin: ChannelPlugin = { } return lines; }, - buildAccountSnapshot: ({ account, runtime, probe }) => ({ - accountId: account.accountId, - enabled: account.enabled, - configured: account.configured, - ...buildRuntimeAccountStatusSnapshot({ runtime, probe }), - port: runtime?.port ?? null, - }), + buildAccountSnapshot: ({ account, runtime, probe }) => + buildRuntimeAccountStatusSnapshot( + { runtime, probe }, + { + accountId: account.accountId, + enabled: account.enabled, + configured: account.configured, + port: runtime?.port ?? null, + }, + ), }, gateway: { startAccount: async (ctx) => { diff --git a/extensions/nextcloud-talk/src/channel.ts b/extensions/nextcloud-talk/src/channel.ts index 12fcdcdbbca..8f3cf8403e9 100644 --- a/extensions/nextcloud-talk/src/channel.ts +++ b/extensions/nextcloud-talk/src/channel.ts @@ -221,22 +221,20 @@ export const nextcloudTalkPlugin: ChannelPlugin = }, buildAccountSnapshot: ({ account, runtime }) => { const configured = Boolean(account.secret?.trim() && account.baseUrl?.trim()); - const runtimeSnapshot = buildRuntimeAccountStatusSnapshot({ runtime }); - return { - accountId: account.accountId, - name: account.name, - enabled: account.enabled, - configured, - secretSource: account.secretSource, - baseUrl: account.baseUrl ? "[set]" : "[missing]", - running: runtimeSnapshot.running, - lastStartAt: runtimeSnapshot.lastStartAt, - lastStopAt: runtimeSnapshot.lastStopAt, - lastError: runtimeSnapshot.lastError, - mode: "webhook", - lastInboundAt: runtime?.lastInboundAt ?? null, - lastOutboundAt: runtime?.lastOutboundAt ?? null, - }; + return buildRuntimeAccountStatusSnapshot( + { runtime }, + { + accountId: account.accountId, + name: account.name, + enabled: account.enabled, + configured, + secretSource: account.secretSource, + baseUrl: account.baseUrl ? "[set]" : "[missing]", + mode: "webhook", + lastInboundAt: runtime?.lastInboundAt ?? null, + lastOutboundAt: runtime?.lastOutboundAt ?? null, + }, + ); }, }, gateway: { diff --git a/extensions/signal/src/channel.ts b/extensions/signal/src/channel.ts index 6950583728a..72a3feda987 100644 --- a/extensions/signal/src/channel.ts +++ b/extensions/signal/src/channel.ts @@ -383,10 +383,8 @@ export const signalPlugin: ChannelPlugin = { (probe as SignalProbe | undefined)?.version ? [{ text: `Signal daemon: ${(probe as SignalProbe).version}` }] : [], - buildAccountSnapshot: ({ account, runtime, probe }) => ({ - ...buildBaseAccountStatusSnapshot({ account, runtime, probe }), - baseUrl: account.baseUrl, - }), + buildAccountSnapshot: ({ account, runtime, probe }) => + buildBaseAccountStatusSnapshot({ account, runtime, probe }, { baseUrl: account.baseUrl }), }, gateway: { startAccount: async (ctx) => { diff --git a/extensions/slack/src/channel.ts b/extensions/slack/src/channel.ts index 386e4a921b1..a6223bd787a 100644 --- a/extensions/slack/src/channel.ts +++ b/extensions/slack/src/channel.ts @@ -617,18 +617,19 @@ export const slackPlugin: ChannelPlugin = { "botTokenStatus", "appTokenStatus", ])) ?? isSlackPluginAccountConfigured(account); - const base = buildComputedAccountStatusSnapshot({ - accountId: account.accountId, - name: account.name, - enabled: account.enabled, - configured, - runtime, - probe, - }); - return { - ...base, - ...projectCredentialSnapshotFields(account), - }; + return buildComputedAccountStatusSnapshot( + { + accountId: account.accountId, + name: account.name, + enabled: account.enabled, + configured, + runtime, + probe, + }, + { + ...projectCredentialSnapshotFields(account), + }, + ); }, }, gateway: { diff --git a/extensions/zalo/src/channel.ts b/extensions/zalo/src/channel.ts index f8df4c78cd0..fb0e10bc24d 100644 --- a/extensions/zalo/src/channel.ts +++ b/extensions/zalo/src/channel.ts @@ -235,21 +235,22 @@ export const zaloPlugin: ChannelPlugin = { await (await loadZaloChannelRuntime()).probeZaloAccount({ account, timeoutMs }), buildAccountSnapshot: ({ account, runtime }) => { const configured = Boolean(account.token?.trim()); - const base = buildBaseAccountStatusSnapshot({ - account: { - accountId: account.accountId, - name: account.name, - enabled: account.enabled, - configured, + return buildBaseAccountStatusSnapshot( + { + account: { + accountId: account.accountId, + name: account.name, + enabled: account.enabled, + configured, + }, + runtime, }, - runtime, - }); - return { - ...base, - tokenSource: account.tokenSource, - mode: account.config.webhookUrl ? "webhook" : "polling", - dmPolicy: account.config.dmPolicy ?? "pairing", - }; + { + tokenSource: account.tokenSource, + mode: account.config.webhookUrl ? "webhook" : "polling", + dmPolicy: account.config.dmPolicy ?? "pairing", + }, + ); }, }, gateway: { diff --git a/extensions/zalouser/src/channel.ts b/extensions/zalouser/src/channel.ts index d5cdbd00352..90cdeea5d49 100644 --- a/extensions/zalouser/src/channel.ts +++ b/extensions/zalouser/src/channel.ts @@ -455,21 +455,22 @@ export const zalouserPlugin: ChannelPlugin = { buildAccountSnapshot: async ({ account, runtime }) => { const configured = await checkZcaAuthenticated(account.profile); const configError = "not authenticated"; - const base = buildBaseAccountStatusSnapshot({ - account: { - accountId: account.accountId, - name: account.name, - enabled: account.enabled, - configured, + return buildBaseAccountStatusSnapshot( + { + account: { + accountId: account.accountId, + name: account.name, + enabled: account.enabled, + configured, + }, + runtime: configured + ? runtime + : { ...runtime, lastError: runtime?.lastError ?? configError }, }, - runtime: configured - ? runtime - : { ...runtime, lastError: runtime?.lastError ?? configError }, - }); - return { - ...base, - dmPolicy: account.config.dmPolicy ?? "pairing", - }; + { + dmPolicy: account.config.dmPolicy ?? "pairing", + }, + ); }, }, gateway: { diff --git a/src/plugin-sdk/status-helpers.test.ts b/src/plugin-sdk/status-helpers.test.ts index b2b75bb1414..318cd57a3f3 100644 --- a/src/plugin-sdk/status-helpers.test.ts +++ b/src/plugin-sdk/status-helpers.test.ts @@ -88,6 +88,34 @@ describe("buildBaseAccountStatusSnapshot", () => { lastOutboundAt: null, }); }); + + it("merges extra snapshot fields after the shared account shape", () => { + expect( + buildBaseAccountStatusSnapshot( + { + account: { accountId: "default", configured: true }, + }, + { + connected: true, + mode: "polling", + }, + ), + ).toEqual({ + accountId: "default", + name: undefined, + enabled: undefined, + configured: true, + running: false, + lastStartAt: null, + lastStopAt: null, + lastError: null, + probe: undefined, + lastInboundAt: null, + lastOutboundAt: null, + connected: true, + mode: "polling", + }); + }); }); describe("buildComputedAccountStatusSnapshot", () => { @@ -112,6 +140,33 @@ describe("buildComputedAccountStatusSnapshot", () => { lastOutboundAt: null, }); }); + + it("merges computed extras after the shared fields", () => { + expect( + buildComputedAccountStatusSnapshot( + { + accountId: "default", + configured: true, + }, + { + connected: true, + }, + ), + ).toEqual({ + accountId: "default", + name: undefined, + enabled: undefined, + configured: true, + running: false, + lastStartAt: null, + lastStopAt: null, + lastError: null, + probe: undefined, + lastInboundAt: null, + lastOutboundAt: null, + connected: true, + }); + }); }); describe("buildRuntimeAccountStatusSnapshot", () => { @@ -124,6 +179,17 @@ describe("buildRuntimeAccountStatusSnapshot", () => { probe: undefined, }); }); + + it("merges extra fields into runtime snapshots", () => { + expect(buildRuntimeAccountStatusSnapshot({}, { port: 3978 })).toEqual({ + running: false, + lastStartAt: null, + lastStopAt: null, + lastError: null, + probe: undefined, + port: 3978, + }); + }); }); describe("buildTokenChannelStatusSummary", () => { diff --git a/src/plugin-sdk/status-helpers.ts b/src/plugin-sdk/status-helpers.ts index 7ae74b14ed6..a467699eaed 100644 --- a/src/plugin-sdk/status-helpers.ts +++ b/src/plugin-sdk/status-helpers.ts @@ -17,6 +17,8 @@ type RuntimeLifecycleSnapshot = { lastOutboundAt?: number | null; }; +type StatusSnapshotExtra = Record; + /** Create the baseline runtime snapshot shape used by channel/account status stores. */ export function createDefaultChannelRuntimeState>( accountId: string, @@ -77,16 +79,19 @@ export function buildProbeChannelStatusSummary( + 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, @@ -96,36 +101,46 @@ export function buildBaseAccountStatusSnapshot(params: { ...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; -}) { +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, + return buildBaseAccountStatusSnapshot( + { + account: { + accountId, + name, + enabled, + configured, + }, + runtime, + probe, }, - runtime, - probe, - }); + extra, + ); } /** Normalize runtime-only account state into the shared status snapshot fields. */ -export function buildRuntimeAccountStatusSnapshot(params: { - runtime?: RuntimeLifecycleSnapshot | null; - probe?: unknown; -}) { +export function buildRuntimeAccountStatusSnapshot( + params: { + runtime?: RuntimeLifecycleSnapshot | null; + probe?: unknown; + }, + extra?: TExtra, +) { const { runtime, probe } = params; return { running: runtime?.running ?? false, @@ -133,6 +148,7 @@ export function buildRuntimeAccountStatusSnapshot(params: { lastStopAt: runtime?.lastStopAt ?? null, lastError: runtime?.lastError ?? null, probe, + ...(extra ?? ({} as TExtra)), }; }