mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 06:40:44 +00:00
fix(telegram): warn on stale polling status
This commit is contained in:
@@ -22,6 +22,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Channels/Telegram: honor `ALL_PROXY` / `all_proxy` and service-level `OPENCLAW_PROXY_URL` when constructing the HTTP/1-only Telegram Bot API transport, so Windows and service installs that rely on those proxy settings no longer fall back to direct egress. Fixes #74014; refs #74086. Thanks @SymbolStar.
|
||||
- Channels/Telegram: continue polling when `deleteWebhook` hits a transient network failure but `getWebhookInfo` confirms no webhook is configured, so startup does not retry cleanup forever after the webhook was already removed. Refs #74086; carries forward #47384. Thanks @clovericbot.
|
||||
- Channels/Telegram: apply strict safe-send retry to inbound final replies when grammY wraps a pre-connect failure, while leaving ambiguous plain network envelopes single-shot to avoid duplicate visible messages. Fixes #74203. Thanks @nanli2000cn.
|
||||
- Channels/Telegram: surface polling liveness warnings in channel status and doctor when a running long-poller has not completed `getUpdates` after startup grace or its transport activity is stale, so silent polling failures no longer look clean. Refs #74299. Thanks @lolaopenclaw.
|
||||
- ACP/commands: accept forwarded ACP timeout config controls in the OpenClaw bridge, treat unsupported discard-close controls as recoverable cleanup, and restore native `/verbose full` plus no-arg status behavior, so Discord command menus and nested ACP turns no longer fail on supported session controls. Thanks @vincentkoc.
|
||||
- TUI/status: clear stale `streaming` footer state when a final event arrives after the active run was already cleared and no tracked runs remain, while preserving concurrent-run ownership and inactive local `/btw` terminal handling. Fixes #64825; carries forward #64842, #64843, #64847, and #64862. Thanks @briandevans and @Yanhu007.
|
||||
- Channels/Discord: fail startup closed when Discord cannot resolve the bot's own identity and keep mention gating active when only configured mention patterns can detect mentions, so the provider no longer continues with a missing bot id. Fixes #42219; carries forward #46856 and #49218. Thanks @education-01 and @BenediktSchackenberg.
|
||||
|
||||
@@ -867,6 +867,7 @@ Per-account, per-group, and per-topic overrides are supported (same inheritance
|
||||
- Some hosts resolve `api.telegram.org` to IPv6 first; broken IPv6 egress can cause intermittent Telegram API failures.
|
||||
- If logs include `TypeError: fetch failed` or `Network request for 'getUpdates' failed!`, OpenClaw now retries these as recoverable network errors.
|
||||
- If logs include `Polling stall detected`, OpenClaw restarts polling and rebuilds the Telegram transport after 120 seconds without completed long-poll liveness by default.
|
||||
- `openclaw channels status --probe` and `openclaw doctor` warn when a running polling account has not completed `getUpdates` after startup grace, or when its last successful polling transport activity is stale.
|
||||
- Increase `channels.telegram.pollingStallThresholdMs` only when long-running `getUpdates` calls are healthy but your host still reports false polling-stall restarts. Persistent stalls usually point to proxy, DNS, IPv6, or TLS egress issues between the host and `api.telegram.org`.
|
||||
- Telegram also honors process proxy env for Bot API transport, including `HTTP_PROXY`, `HTTPS_PROXY`, `ALL_PROXY`, and their lowercase variants. `NO_PROXY` / `no_proxy` can still bypass `api.telegram.org`.
|
||||
- If the OpenClaw managed proxy is configured through `OPENCLAW_PROXY_URL` for a service environment and no standard proxy env is present, Telegram uses that URL for Bot API transport too.
|
||||
|
||||
@@ -2,6 +2,7 @@ import type {
|
||||
ChannelAccountSnapshot,
|
||||
ChannelStatusIssue,
|
||||
} from "openclaw/plugin-sdk/channel-contract";
|
||||
import { formatCliCommand } from "openclaw/plugin-sdk/cli-runtime";
|
||||
import {
|
||||
appendMatchMetadata,
|
||||
asString,
|
||||
@@ -9,10 +10,19 @@ import {
|
||||
resolveEnabledConfiguredAccountId,
|
||||
} from "openclaw/plugin-sdk/status-helpers";
|
||||
|
||||
const TELEGRAM_POLLING_CONNECT_GRACE_MS = 120_000;
|
||||
const TELEGRAM_POLLING_STALE_TRANSPORT_MS = 30 * 60_000;
|
||||
|
||||
type TelegramAccountStatus = {
|
||||
accountId?: unknown;
|
||||
enabled?: unknown;
|
||||
configured?: unknown;
|
||||
running?: unknown;
|
||||
connected?: unknown;
|
||||
mode?: unknown;
|
||||
lastStartAt?: unknown;
|
||||
lastTransportActivityAt?: unknown;
|
||||
lastError?: unknown;
|
||||
allowUnmentionedGroups?: unknown;
|
||||
audit?: unknown;
|
||||
};
|
||||
@@ -38,11 +48,82 @@ function readTelegramAccountStatus(value: ChannelAccountSnapshot): TelegramAccou
|
||||
accountId: value.accountId,
|
||||
enabled: value.enabled,
|
||||
configured: value.configured,
|
||||
running: value.running,
|
||||
connected: value.connected,
|
||||
mode: value.mode,
|
||||
lastStartAt: value.lastStartAt,
|
||||
lastTransportActivityAt: value.lastTransportActivityAt,
|
||||
lastError: value.lastError,
|
||||
allowUnmentionedGroups: value.allowUnmentionedGroups,
|
||||
audit: value.audit,
|
||||
};
|
||||
}
|
||||
|
||||
function asFiniteNumber(value: unknown): number | null {
|
||||
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
||||
}
|
||||
|
||||
function appendTelegramRuntimeError(message: string, lastError: unknown): string {
|
||||
const error = asString(lastError);
|
||||
return error ? `${message}: ${error}` : message;
|
||||
}
|
||||
|
||||
function collectTelegramPollingRuntimeIssues(params: {
|
||||
account: TelegramAccountStatus;
|
||||
accountId: string;
|
||||
issues: ChannelStatusIssue[];
|
||||
now: number;
|
||||
}) {
|
||||
const { account, accountId, issues, now } = params;
|
||||
if (account.running !== true || asString(account.mode) !== "polling") {
|
||||
return;
|
||||
}
|
||||
|
||||
const lastStartAt = asFiniteNumber(account.lastStartAt);
|
||||
const lastTransportActivityAt = asFiniteNumber(account.lastTransportActivityAt);
|
||||
const fix = `Run: ${formatCliCommand("openclaw channels status --probe")} (or restart the gateway). Check the bot token, proxy/network settings, and logs if it persists.`;
|
||||
|
||||
if (account.connected === false) {
|
||||
const withinStartupGrace =
|
||||
lastStartAt != null && now - lastStartAt < TELEGRAM_POLLING_CONNECT_GRACE_MS;
|
||||
if (!withinStartupGrace) {
|
||||
issues.push({
|
||||
channel: "telegram",
|
||||
accountId,
|
||||
kind: "runtime",
|
||||
message: appendTelegramRuntimeError(
|
||||
"Telegram polling is running but has not completed a successful getUpdates call since startup",
|
||||
account.lastError,
|
||||
),
|
||||
fix,
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (account.connected === true && lastTransportActivityAt != null) {
|
||||
if (lastStartAt != null && lastTransportActivityAt < lastStartAt) {
|
||||
const lifecycleAgeMs = Math.max(0, now - lastStartAt);
|
||||
if (lifecycleAgeMs <= TELEGRAM_POLLING_STALE_TRANSPORT_MS) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
const ageMs = now - lastTransportActivityAt;
|
||||
if (ageMs > TELEGRAM_POLLING_STALE_TRANSPORT_MS) {
|
||||
issues.push({
|
||||
channel: "telegram",
|
||||
accountId,
|
||||
kind: "runtime",
|
||||
message: appendTelegramRuntimeError(
|
||||
`Telegram polling transport is stale (last successful getUpdates ${Math.max(0, Math.floor(ageMs / 60_000))}m ago)`,
|
||||
account.lastError,
|
||||
),
|
||||
fix,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function readTelegramGroupMembershipAuditSummary(
|
||||
value: unknown,
|
||||
): TelegramGroupMembershipAuditSummary {
|
||||
@@ -94,6 +175,13 @@ export function collectTelegramStatusIssues(
|
||||
continue;
|
||||
}
|
||||
|
||||
collectTelegramPollingRuntimeIssues({
|
||||
account,
|
||||
accountId,
|
||||
issues,
|
||||
now: Date.now(),
|
||||
});
|
||||
|
||||
if (account.allowUnmentionedGroups === true) {
|
||||
issues.push({
|
||||
channel: "telegram",
|
||||
|
||||
@@ -73,6 +73,105 @@ describe("collectTelegramStatusIssues", () => {
|
||||
expect(issues[0]?.message).toContain("channels.telegram.groups");
|
||||
});
|
||||
|
||||
it("reports polling runtime state that never completed getUpdates after startup grace", () => {
|
||||
const issues = collectTelegramStatusIssues([
|
||||
{
|
||||
accountId: "main",
|
||||
enabled: true,
|
||||
configured: true,
|
||||
running: true,
|
||||
mode: "polling",
|
||||
connected: false,
|
||||
lastStartAt: Date.now() - 121_000,
|
||||
lastError: "network timeout",
|
||||
} as ChannelAccountSnapshot,
|
||||
]);
|
||||
|
||||
expect(issues).toEqual([
|
||||
expect.objectContaining({
|
||||
channel: "telegram",
|
||||
accountId: "main",
|
||||
kind: "runtime",
|
||||
message: expect.stringContaining("has not completed a successful getUpdates call"),
|
||||
}),
|
||||
]);
|
||||
expect(issues[0]?.message).toContain("network timeout");
|
||||
expect(issues[0]?.fix).toContain("channels status --probe");
|
||||
});
|
||||
|
||||
it("does not report polling startup before the connect grace expires", () => {
|
||||
const issues = collectTelegramStatusIssues([
|
||||
{
|
||||
accountId: "main",
|
||||
enabled: true,
|
||||
configured: true,
|
||||
running: true,
|
||||
mode: "polling",
|
||||
connected: false,
|
||||
lastStartAt: Date.now() - 60_000,
|
||||
} as ChannelAccountSnapshot,
|
||||
]);
|
||||
|
||||
expect(issues).toEqual([]);
|
||||
});
|
||||
|
||||
it("reports stale polling transport activity after successful getUpdates stops refreshing", () => {
|
||||
const issues = collectTelegramStatusIssues([
|
||||
{
|
||||
accountId: "main",
|
||||
enabled: true,
|
||||
configured: true,
|
||||
running: true,
|
||||
mode: "polling",
|
||||
connected: true,
|
||||
lastStartAt: Date.now() - 60 * 60_000,
|
||||
lastTransportActivityAt: Date.now() - 31 * 60_000,
|
||||
} as ChannelAccountSnapshot,
|
||||
]);
|
||||
|
||||
expect(issues).toEqual([
|
||||
expect.objectContaining({
|
||||
channel: "telegram",
|
||||
accountId: "main",
|
||||
kind: "runtime",
|
||||
message: expect.stringContaining("polling transport is stale"),
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
it("does not report inherited stale transport activity during a fresh polling lifecycle", () => {
|
||||
const issues = collectTelegramStatusIssues([
|
||||
{
|
||||
accountId: "main",
|
||||
enabled: true,
|
||||
configured: true,
|
||||
running: true,
|
||||
mode: "polling",
|
||||
connected: true,
|
||||
lastStartAt: Date.now() - 60_000,
|
||||
lastTransportActivityAt: Date.now() - 2 * 60 * 60_000,
|
||||
} as ChannelAccountSnapshot,
|
||||
]);
|
||||
|
||||
expect(issues).toEqual([]);
|
||||
});
|
||||
|
||||
it("does not report webhook accounts with polling-only runtime diagnostics", () => {
|
||||
const issues = collectTelegramStatusIssues([
|
||||
{
|
||||
accountId: "main",
|
||||
enabled: true,
|
||||
configured: true,
|
||||
running: true,
|
||||
mode: "webhook",
|
||||
connected: false,
|
||||
lastStartAt: Date.now() - 10 * 60_000,
|
||||
} as ChannelAccountSnapshot,
|
||||
]);
|
||||
|
||||
expect(issues).toEqual([]);
|
||||
});
|
||||
|
||||
it("ignores accounts that are not both enabled and configured", () => {
|
||||
expect(
|
||||
collectTelegramStatusIssues([
|
||||
|
||||
Reference in New Issue
Block a user