refactor: extend shared account status snapshot helpers

This commit is contained in:
Peter Steinberger 2026-03-22 20:50:22 +00:00
parent 00b2f10dec
commit 87722d6327
15 changed files with 292 additions and 199 deletions

View File

@ -325,19 +325,20 @@ export const bluebubblesPlugin: ChannelPlugin<ResolvedBlueBubblesAccount> = {
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: {

View File

@ -635,26 +635,27 @@ export const discordPlugin: ChannelPlugin<ResolvedDiscordAccount> = {
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: {

View File

@ -959,16 +959,19 @@ export const feishuPlugin: ChannelPlugin<ResolvedFeishuAccount> = {
}),
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) => {

View File

@ -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) => {

View File

@ -308,14 +308,17 @@ export const ircPlugin: ChannelPlugin<ResolvedIrcAccount, IrcProbe> = {
}),
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) => {

View File

@ -361,19 +361,20 @@ export const linePlugin: ChannelPlugin<ResolvedLineAccount> = {
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: {

View File

@ -445,24 +445,24 @@ export const mattermostPlugin: ChannelPlugin<ResolvedMattermostAccount> = {
}
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) => {

View File

@ -490,13 +490,16 @@ export const msteamsPlugin: ChannelPlugin<ResolvedMSTeamsAccount> = {
}
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) => {

View File

@ -221,22 +221,20 @@ export const nextcloudTalkPlugin: ChannelPlugin<ResolvedNextcloudTalkAccount> =
},
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: {

View File

@ -383,10 +383,8 @@ export const signalPlugin: ChannelPlugin<ResolvedSignalAccount> = {
(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) => {

View File

@ -617,18 +617,19 @@ export const slackPlugin: ChannelPlugin<ResolvedSlackAccount> = {
"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: {

View File

@ -235,21 +235,22 @@ export const zaloPlugin: ChannelPlugin<ResolvedZaloAccount> = {
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: {

View File

@ -455,21 +455,22 @@ export const zalouserPlugin: ChannelPlugin<ResolvedZalouserAccount> = {
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: {

View File

@ -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", () => {

View File

@ -17,6 +17,8 @@ type RuntimeLifecycleSnapshot = {
lastOutboundAt?: number | null;
};
type StatusSnapshotExtra = Record<string, unknown>;
/** Create the baseline runtime snapshot shape used by channel/account status stores. */
export function createDefaultChannelRuntimeState<T extends Record<string, unknown>>(
accountId: string,
@ -77,16 +79,19 @@ export function buildProbeChannelStatusSummary<TExtra extends Record<string, unk
}
/** 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;
}) {
export function buildBaseAccountStatusSnapshot<TExtra extends StatusSnapshotExtra>(
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<TExtra extends StatusSnapshotExtra>(
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<TExtra extends StatusSnapshotExtra>(
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)),
};
}