mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-09 15:30:43 +00:00
279 lines
8.7 KiB
TypeScript
279 lines
8.7 KiB
TypeScript
import type {
|
|
ChannelAccountSnapshot,
|
|
ChannelStatusIssue,
|
|
} from "openclaw/plugin-sdk/channel-contract";
|
|
import { formatCliCommand } from "openclaw/plugin-sdk/cli-runtime";
|
|
import {
|
|
appendMatchMetadata,
|
|
asString,
|
|
isRecord,
|
|
resolveEnabledConfiguredAccountId,
|
|
} from "openclaw/plugin-sdk/status-helpers";
|
|
|
|
const TELEGRAM_POLLING_CONNECT_GRACE_MS = 120_000;
|
|
const TELEGRAM_POLLING_STALE_TRANSPORT_MS = 30 * 60_000;
|
|
const TELEGRAM_WEBHOOK_CONNECT_GRACE_MS = 120_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;
|
|
};
|
|
|
|
type TelegramGroupMembershipAuditSummary = {
|
|
unresolvedGroups?: number;
|
|
hasWildcardUnmentionedGroups?: boolean;
|
|
groups?: Array<{
|
|
chatId: string;
|
|
ok?: boolean;
|
|
status?: string | null;
|
|
error?: string | null;
|
|
matchKey?: string;
|
|
matchSource?: string;
|
|
}>;
|
|
};
|
|
|
|
function readTelegramAccountStatus(value: ChannelAccountSnapshot): TelegramAccountStatus | null {
|
|
if (!isRecord(value)) {
|
|
return null;
|
|
}
|
|
return {
|
|
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 collectTelegramWebhookRuntimeIssues(params: {
|
|
account: TelegramAccountStatus;
|
|
accountId: string;
|
|
issues: ChannelStatusIssue[];
|
|
now: number;
|
|
}) {
|
|
const { account, accountId, issues, now } = params;
|
|
if (account.running !== true || asString(account.mode) !== "webhook") {
|
|
return;
|
|
}
|
|
|
|
if (account.connected !== false) {
|
|
return;
|
|
}
|
|
|
|
const lastStartAt = asFiniteNumber(account.lastStartAt);
|
|
const withinStartupGrace =
|
|
lastStartAt != null && now - lastStartAt < TELEGRAM_WEBHOOK_CONNECT_GRACE_MS;
|
|
if (withinStartupGrace) {
|
|
return;
|
|
}
|
|
|
|
issues.push({
|
|
channel: "telegram",
|
|
accountId,
|
|
kind: "runtime",
|
|
message: appendTelegramRuntimeError(
|
|
"Telegram webhook listener is running but setWebhook has not completed since startup",
|
|
account.lastError,
|
|
),
|
|
fix: `Run: ${formatCliCommand("openclaw channels status --probe")} (or restart the gateway). Check the webhook URL, secret, TLS/proxy reachability, and Telegram setWebhook logs if it persists.`,
|
|
});
|
|
}
|
|
|
|
function readTelegramGroupMembershipAuditSummary(
|
|
value: unknown,
|
|
): TelegramGroupMembershipAuditSummary {
|
|
if (!isRecord(value)) {
|
|
return {};
|
|
}
|
|
const unresolvedGroups =
|
|
typeof value.unresolvedGroups === "number" && Number.isFinite(value.unresolvedGroups)
|
|
? value.unresolvedGroups
|
|
: undefined;
|
|
const hasWildcardUnmentionedGroups =
|
|
typeof value.hasWildcardUnmentionedGroups === "boolean"
|
|
? value.hasWildcardUnmentionedGroups
|
|
: undefined;
|
|
const groupsRaw = value.groups;
|
|
const groups = Array.isArray(groupsRaw)
|
|
? (groupsRaw
|
|
.map((entry) => {
|
|
if (!isRecord(entry)) {
|
|
return null;
|
|
}
|
|
const chatId = asString(entry.chatId);
|
|
if (!chatId) {
|
|
return null;
|
|
}
|
|
const ok = typeof entry.ok === "boolean" ? entry.ok : undefined;
|
|
const status = asString(entry.status) ?? null;
|
|
const error = asString(entry.error) ?? null;
|
|
const matchKey = asString(entry.matchKey) ?? undefined;
|
|
const matchSource = asString(entry.matchSource) ?? undefined;
|
|
return { chatId, ok, status, error, matchKey, matchSource };
|
|
})
|
|
.filter(Boolean) as TelegramGroupMembershipAuditSummary["groups"])
|
|
: undefined;
|
|
return { unresolvedGroups, hasWildcardUnmentionedGroups, groups };
|
|
}
|
|
|
|
export function collectTelegramStatusIssues(
|
|
accounts: ChannelAccountSnapshot[],
|
|
): ChannelStatusIssue[] {
|
|
const issues: ChannelStatusIssue[] = [];
|
|
for (const entry of accounts) {
|
|
const account = readTelegramAccountStatus(entry);
|
|
if (!account) {
|
|
continue;
|
|
}
|
|
const accountId = resolveEnabledConfiguredAccountId(account);
|
|
if (!accountId) {
|
|
continue;
|
|
}
|
|
const now = Date.now();
|
|
|
|
collectTelegramPollingRuntimeIssues({
|
|
account,
|
|
accountId,
|
|
issues,
|
|
now,
|
|
});
|
|
collectTelegramWebhookRuntimeIssues({
|
|
account,
|
|
accountId,
|
|
issues,
|
|
now,
|
|
});
|
|
|
|
if (account.allowUnmentionedGroups === true) {
|
|
issues.push({
|
|
channel: "telegram",
|
|
accountId,
|
|
kind: "config",
|
|
message:
|
|
"Config allows unmentioned group messages (requireMention=false). Telegram Bot API privacy mode will block most group messages unless disabled.",
|
|
fix: "In BotFather run /setprivacy → Disable for this bot (then restart the gateway).",
|
|
});
|
|
}
|
|
|
|
const audit = readTelegramGroupMembershipAuditSummary(account.audit);
|
|
if (audit.hasWildcardUnmentionedGroups === true) {
|
|
issues.push({
|
|
channel: "telegram",
|
|
accountId,
|
|
kind: "config",
|
|
message:
|
|
'Telegram groups config uses "*" with requireMention=false; membership probing is not possible without explicit group IDs.',
|
|
fix: "Add explicit numeric group ids under channels.telegram.groups (or per-account groups) to enable probing.",
|
|
});
|
|
}
|
|
if (audit.unresolvedGroups && audit.unresolvedGroups > 0) {
|
|
issues.push({
|
|
channel: "telegram",
|
|
accountId,
|
|
kind: "config",
|
|
message: `Some configured Telegram groups are not numeric IDs (unresolvedGroups=${audit.unresolvedGroups}). Membership probe can only check numeric group IDs.`,
|
|
fix: "Use numeric chat IDs (e.g. -100...) as keys in channels.telegram.groups for requireMention=false groups.",
|
|
});
|
|
}
|
|
for (const group of audit.groups ?? []) {
|
|
if (group.ok === true) {
|
|
continue;
|
|
}
|
|
const status = group.status ? ` status=${group.status}` : "";
|
|
const err = group.error ? `: ${group.error}` : "";
|
|
const baseMessage = `Group ${group.chatId} not reachable by bot.${status}${err}`;
|
|
issues.push({
|
|
channel: "telegram",
|
|
accountId,
|
|
kind: "runtime",
|
|
message: appendMatchMetadata(baseMessage, {
|
|
matchKey: group.matchKey,
|
|
matchSource: group.matchSource,
|
|
}),
|
|
fix: "Invite the bot to the group, then DM the bot once (/start) and restart the gateway.",
|
|
});
|
|
}
|
|
}
|
|
return issues;
|
|
}
|