mirror of https://github.com/openclaw/openclaw.git
fix: restore Telegram webhook-mode health after restarts
Landed from contributor PR #39313 by @fellanH. Co-authored-by: Felix Hellström <30758862+fellanH@users.noreply.github.com>
This commit is contained in:
parent
1ef8d6a01b
commit
9d7d961db8
|
|
@ -340,6 +340,7 @@ Docs: https://docs.openclaw.ai
|
|||
- Agents/codex-cli sandbox defaults: switch the built-in Codex backend from `read-only` to `workspace-write` so spawned coding runs can edit files out of the box. Landed from contributor PR #39336 by @0xtangping. Thanks @0xtangping.
|
||||
- Gateway/health-monitor restart reason labeling: report `disconnected` instead of `stuck` for clean channel disconnect restarts, so operator logs distinguish socket drops from genuinely stuck channels. (#36436) Thanks @Sid-Qin.
|
||||
- Control UI/agents-page overrides: auto-create minimal per-agent config entries when editing inherited agents, so model/tool/skill changes enable Save and inherited model fallbacks can be cleared by writing a primary-only override. Landed from contributor PR #39326 by @dunamismax. Thanks @dunamismax.
|
||||
- Gateway/Telegram webhook-mode recovery: add `webhookCertPath` to re-upload self-signed certificates during webhook registration and skip stale-socket detection for webhook-mode channels, so Telegram webhook setups survive health-monitor restarts. Landed from contributor PR #39313 by @fellanH. Thanks @fellanH.
|
||||
|
||||
## 2026.3.2
|
||||
|
||||
|
|
|
|||
|
|
@ -508,6 +508,7 @@ export const telegramPlugin: ChannelPlugin<ResolvedTelegramAccount, TelegramProb
|
|||
webhookPath: account.config.webhookPath,
|
||||
webhookHost: account.config.webhookHost,
|
||||
webhookPort: account.config.webhookPort,
|
||||
webhookCertPath: account.config.webhookCertPath,
|
||||
});
|
||||
},
|
||||
logoutAccount: async ({ accountId, cfg }) => {
|
||||
|
|
|
|||
|
|
@ -140,6 +140,8 @@ export type TelegramAccountConfig = {
|
|||
webhookHost?: string;
|
||||
/** Local webhook listener bind port (default: 8787). */
|
||||
webhookPort?: number;
|
||||
/** Path to the self-signed certificate (PEM) to upload to Telegram during webhook registration. */
|
||||
webhookCertPath?: string;
|
||||
/** Per-action tool gating (default: true for all). */
|
||||
actions?: TelegramActionConfig;
|
||||
/** Telegram thread/conversation binding overrides. */
|
||||
|
|
|
|||
|
|
@ -221,6 +221,12 @@ export const TelegramAccountSchemaBase = z
|
|||
.describe(
|
||||
"Local bind port for the webhook listener. Defaults to 8787; set to 0 to let the OS assign an ephemeral port.",
|
||||
),
|
||||
webhookCertPath: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe(
|
||||
"Path to the self-signed certificate (PEM) to upload to Telegram during webhook registration. Required for self-signed certs (direct IP or no domain).",
|
||||
),
|
||||
actions: z
|
||||
.object({
|
||||
reactions: z.boolean().optional(),
|
||||
|
|
|
|||
|
|
@ -143,6 +143,27 @@ describe("evaluateChannelHealth", () => {
|
|||
expect(evaluation).toEqual({ healthy: true, reason: "healthy" });
|
||||
});
|
||||
|
||||
it("skips stale-socket detection for channels in webhook mode", () => {
|
||||
const evaluation = evaluateChannelHealth(
|
||||
{
|
||||
running: true,
|
||||
connected: true,
|
||||
enabled: true,
|
||||
configured: true,
|
||||
lastStartAt: 0,
|
||||
lastEventAt: 0,
|
||||
mode: "webhook",
|
||||
},
|
||||
{
|
||||
channelId: "discord",
|
||||
now: 100_000,
|
||||
channelConnectGraceMs: 10_000,
|
||||
staleEventThresholdMs: 30_000,
|
||||
},
|
||||
);
|
||||
expect(evaluation).toEqual({ healthy: true, reason: "healthy" });
|
||||
});
|
||||
|
||||
it("does not flag stale sockets for channels without event tracking", () => {
|
||||
const evaluation = evaluateChannelHealth(
|
||||
{
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ export type ChannelHealthSnapshot = {
|
|||
lastEventAt?: number | null;
|
||||
lastStartAt?: number | null;
|
||||
reconnectAttempts?: number;
|
||||
mode?: string;
|
||||
};
|
||||
|
||||
export type ChannelHealthEvaluationReason =
|
||||
|
|
@ -105,11 +106,13 @@ export function evaluateChannelHealth(
|
|||
if (snapshot.connected === false) {
|
||||
return { healthy: false, reason: "disconnected" };
|
||||
}
|
||||
// Skip stale-socket check for Telegram (long-polling mode). Each polling request
|
||||
// acts as a heartbeat, so the half-dead WebSocket scenario this check is designed
|
||||
// to catch does not apply to Telegram's long-polling architecture.
|
||||
// Skip stale-socket check for Telegram (long-polling mode) and any channel
|
||||
// explicitly operating in webhook mode. In these cases, there is no persistent
|
||||
// outgoing socket that can go half-dead, so the lack of incoming events
|
||||
// does not necessarily indicate a connection failure.
|
||||
if (
|
||||
policy.channelId !== "telegram" &&
|
||||
snapshot.mode !== "webhook" &&
|
||||
snapshot.connected === true &&
|
||||
snapshot.lastEventAt != null
|
||||
) {
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ export type MonitorTelegramOpts = {
|
|||
webhookHost?: string;
|
||||
proxyFetch?: typeof fetch;
|
||||
webhookUrl?: string;
|
||||
webhookCertPath?: string;
|
||||
};
|
||||
|
||||
export function createTelegramRunnerOptions(cfg: OpenClawConfig): RunOptions<unknown> {
|
||||
|
|
@ -199,6 +200,7 @@ export async function monitorTelegramProvider(opts: MonitorTelegramOpts = {}) {
|
|||
fetch: proxyFetch,
|
||||
abortSignal: opts.abortSignal,
|
||||
publicUrl: opts.webhookUrl,
|
||||
webhookCertPath: opts.webhookCertPath,
|
||||
});
|
||||
await waitForAbortSignal(opts.abortSignal);
|
||||
return;
|
||||
|
|
|
|||
|
|
@ -353,6 +353,27 @@ describe("startTelegramWebhook", () => {
|
|||
);
|
||||
});
|
||||
|
||||
it("registers webhook with certificate when webhookCertPath is provided", async () => {
|
||||
setWebhookSpy.mockClear();
|
||||
await withStartedWebhook(
|
||||
{
|
||||
secret: TELEGRAM_SECRET,
|
||||
path: TELEGRAM_WEBHOOK_PATH,
|
||||
webhookCertPath: "/path/to/cert.pem",
|
||||
},
|
||||
async () => {
|
||||
expect(setWebhookSpy).toHaveBeenCalledWith(
|
||||
expect.any(String),
|
||||
expect.objectContaining({
|
||||
certificate: expect.objectContaining({
|
||||
fileData: "/path/to/cert.pem",
|
||||
}),
|
||||
}),
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it("invokes webhook handler on matching path", async () => {
|
||||
handlerSpy.mockClear();
|
||||
createTelegramBotSpy.mockClear();
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { createServer } from "node:http";
|
||||
import { webhookCallback } from "grammy";
|
||||
import { InputFile, webhookCallback } from "grammy";
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import { isDiagnosticsEnabled } from "../infra/diagnostic-events.js";
|
||||
import { formatErrorMessage } from "../infra/errors.js";
|
||||
|
|
@ -87,6 +87,7 @@ export async function startTelegramWebhook(opts: {
|
|||
abortSignal?: AbortSignal;
|
||||
healthPath?: string;
|
||||
publicUrl?: string;
|
||||
webhookCertPath?: string;
|
||||
}) {
|
||||
const path = opts.path ?? "/telegram-webhook";
|
||||
const healthPath = opts.healthPath ?? "/healthz";
|
||||
|
|
@ -241,6 +242,7 @@ export async function startTelegramWebhook(opts: {
|
|||
bot.api.setWebhook(publicUrl, {
|
||||
secret_token: secret,
|
||||
allowed_updates: resolveTelegramAllowedUpdates(),
|
||||
certificate: opts.webhookCertPath ? new InputFile(opts.webhookCertPath) : undefined,
|
||||
}),
|
||||
});
|
||||
} catch (err) {
|
||||
|
|
|
|||
Loading…
Reference in New Issue