fix: propagate webhook mode to health monitor snapshot

Webhook channels (LINE, Zalo, Nextcloud Talk, BlueBubbles) are
incorrectly flagged as stale-socket during quiet periods because
snapshot.mode is always undefined, making the mode !== "webhook"
guard in evaluateChannelHealth dead code.

Add mode: "webhook" to each webhook plugin's describeAccount and
propagate described.mode in getRuntimeSnapshot.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
karesansui 2026-03-16 01:45:00 +09:00 committed by Peter Steinberger
parent acca306665
commit acbdafc4f4
6 changed files with 32 additions and 5 deletions

View File

@ -10,6 +10,7 @@ Docs: https://docs.openclaw.ai
- Memory/QMD: resolve slugified `memory_search` file hints back to the indexed filesystem path before returning search hits, so `memory_get` works again for mixed-case and spaced paths. (#50313) Thanks @erra9x.
- Security/LINE: make webhook signature validation run the timing-safe compare even when the supplied signature length is wrong, closing a small timing side-channel. (#55663) Thanks @gavyngong.
- LINE/status: stop `openclaw status` from warning about missing credentials when sanitized LINE snapshots are already configured, while still surfacing whether the missing field is the token or secret. (#45701) Thanks @tamaosamu.
- Gateway/health: carry webhook-vs-polling account mode from channel descriptors into runtime snapshots so passive channels like LINE and BlueBubbles skip false stale-socket health failures. (#47488) Thanks @karesansui-u.
## 2026.3.28-beta.1

View File

@ -63,6 +63,7 @@ export function describeBlueBubblesAccount(account: ResolvedBlueBubblesAccount)
configured: account.configured,
extra: {
baseUrl: account.baseUrl,
mode: "webhook",
},
});
}

View File

@ -43,6 +43,7 @@ export const lineChannelPluginCommon = {
enabled: account.enabled,
configured: hasLineCredentials(account),
tokenSource: account.tokenSource ?? undefined,
mode: "webhook",
}),
},
} satisfies Pick<

View File

@ -177,6 +177,7 @@ export const zaloPlugin: ChannelPlugin<ResolvedZaloAccount, ZaloProbeResult> =
configured: Boolean(account.token?.trim()),
extra: {
tokenSource: account.tokenSource,
mode: account.config.webhookUrl ? "webhook" : "polling",
},
}),
},

View File

@ -46,6 +46,7 @@ function createTestPlugin(params?: {
account?: TestAccount;
startAccount?: NonNullable<ChannelPlugin<TestAccount>["gateway"]>["startAccount"];
includeDescribeAccount?: boolean;
describeAccount?: ChannelPlugin<TestAccount>["config"]["describeAccount"];
resolveAccount?: ChannelPlugin<TestAccount>["config"]["resolveAccount"];
isConfigured?: ChannelPlugin<TestAccount>["config"]["isConfigured"];
}): ChannelPlugin<TestAccount> {
@ -59,11 +60,13 @@ function createTestPlugin(params?: {
...(params?.isConfigured ? { isConfigured: params.isConfigured } : {}),
};
if (includeDescribeAccount) {
config.describeAccount = (resolved) => ({
accountId: DEFAULT_ACCOUNT_ID,
enabled: resolved.enabled !== false,
configured: resolved.configured !== false,
});
config.describeAccount =
params?.describeAccount ??
((resolved) => ({
accountId: DEFAULT_ACCOUNT_ID,
enabled: resolved.enabled !== false,
configured: resolved.configured !== false,
}));
}
const gateway: NonNullable<ChannelPlugin<TestAccount>["gateway"]> = {};
if (params?.startAccount) {
@ -198,6 +201,23 @@ describe("server-channels auto restart", () => {
expect(account?.configured).toBe(true);
});
it("forwards described mode into runtime snapshots", () => {
installTestRegistry(
createTestPlugin({
describeAccount: (resolved) => ({
accountId: DEFAULT_ACCOUNT_ID,
enabled: resolved.enabled !== false,
configured: resolved.configured !== false,
mode: "webhook",
}),
}),
);
const manager = createManager();
const snapshot = manager.getRuntimeSnapshot();
const account = snapshot.channelAccounts.discord?.[DEFAULT_ACCOUNT_ID];
expect(account?.mode).toBe("webhook");
});
it("passes channelRuntime through channel gateway context when provided", async () => {
const channelRuntime = { marker: "channel-runtime" } as unknown as PluginRuntime["channel"];
const startAccount = vi.fn(async (ctx) => {

View File

@ -555,6 +555,9 @@ export function createChannelManager(opts: ChannelManagerOptions): ChannelManage
const next = { ...current, accountId: id };
next.enabled = enabled;
next.configured = typeof configured === "boolean" ? configured : (next.configured ?? true);
if (described?.mode !== undefined) {
next.mode = described.mode;
}
if (!next.running) {
if (!enabled) {
next.lastError ??= plugin.config.disabledReason?.(account, cfg) ?? "disabled";