UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

864 lines (863 loc) 35.6 kB
import { F as resolveTimerTimeoutMs, f as asSafeIntegerInRange } from "./number-coercion-CLj0HTDM.js"; import { l as toErrorObject } from "./error-coercion-D_-xJ90S.js"; import { t as asNonArrayRecord } from "./record-coerce-DItp3I4t.js"; import { g as readStringValue } from "./string-coerce-CIXf7egm.js"; import { r as createLazyRuntimeModule } from "./lazy-runtime-CgCh8H_K.js"; import { n as getRuntimeConfig } from "./io.runtime-B9iJRs3w.js"; import { n as computeBackoff, s as sleepWithAbort } from "./src-BQ327IOM.js"; import { i as waitForAbortSignal } from "./abort-signal-D2k14JsD.js"; import { t as formatErrorMessage } from "./errors-Db3Ymjlb.js"; import { i as readResponseWithLimit } from "./http-response-body-CwT_cCNz.js"; import { O as resolveAgentMaxConcurrent } from "./io.types-BUCjdS5v.js"; import { n as normalizeMessageChannel } from "./message-channel-core-AaEWic-A.js"; import { r as fetchWithTimeout } from "./fetch-timeout-DGxOi-Tg.js"; import { o as optionalPositiveIntegerSchema } from "./typebox-BdE5GwKs.js"; import { h as readToolStringParam } from "./common-Bm6UTDDA.js"; import { u as resolveExecApprovalRequestAllowedDecisions } from "./exec-approvals-policy-CCFUzTzd.js"; import { c as registerUnhandledRejectionHandler, s as registerUncaughtExceptionHandler } from "./unhandled-rejections-iZOzdBz8.js"; import { r as makeProxyFetch } from "./proxy-fetch-BdRrrR-3.js"; import "./error-runtime-Bz9Tw57Z.js"; import "./response-limit-runtime-BV0RL9tn.js"; import "./runtime-env-0Q-gCyUb.js"; import { t as resolveTelegramAllowedUpdates } from "./allowed-updates-CAdSYdXv.js"; import { g as isTelegramPollingNetworkError, o as isRecoverableTelegramNetworkError, r as resolveTelegramTransport, t as resolveTelegramApiBase } from "./fetch-DtQkd2V_.js"; import "./number-runtime-Cy4drVnh.js"; import "./string-coerce-runtime-GQa0ehRA.js"; import "./routing-adlWg0R3.js"; import { u as buildTypedExecApprovalPendingReplyPayload } from "./exec-approval-reply-ZdeEqz7J.js"; import { t as CHANNEL_APPROVAL_NATIVE_RUNTIME_CONTEXT_CAPABILITY } from "./approval-handler-adapter-runtime-Tv9LYgST.js"; import { t as resolveExecApprovalCommandDisplay } from "./exec-approval-command-display-03IqfHT_.js"; import { r as registerChannelRuntimeContext } from "./channel-runtime-context-gztTEkoq.js"; import "./approval-reply-runtime-CaHYlGF_.js"; import "./runtime-config-snapshot-u1Kj68kx.js"; import "./model-session-runtime-wuwUOMDO.js"; import "./ssrf-runtime-Bum5C6NN.js"; import { n as createUnionActionGate, r as listTokenSourcedAccounts, t as resolveReactionMessageId } from "./channel-actions-CrPVkBaw.js"; import { t as runChannelProbe } from "./text-utility-runtime-BjzvUG99.js"; import { t as extractToolSend } from "./tool-send-CpdY8Wzi.js"; import { c as resolveTelegramPollActionGateState, o as resolveTelegramAccount, r as listTelegramAccountIds, t as createTelegramActionGate } from "./accounts-CgIQ5mFD.js"; import { r as inspectTelegramAccount } from "./account-inspect-3WXhWjZW.js"; import { a as isTelegramExecApprovalHandlerConfigured, i as isTelegramExecApprovalClientEnabled } from "./exec-approvals-CmtRI1wa.js"; import { t as normalizeTelegramBotInfo } from "./bot-info-BabEIyfI.js"; import { t as fingerprintTelegramBotToken } from "./token-fingerprint-z983D2R-.js"; import { t as isTelegramInlineButtonsEnabled } from "./inline-buttons-BTfJdkW_.js"; import { t as rejectTelegramNativeButtonParams } from "./native-button-params-CIR-Zlcc.js"; import { t as resolveTelegramAccountOwnerAgentId } from "./account-owner-7zwE7Wu9.js"; import { Type } from "typebox"; //#region extensions/telegram/src/message-tool-schema.ts function createTelegramPollExtraToolSchemas() { return { pollDurationSeconds: optionalPositiveIntegerSchema(), pollAnonymous: Type.Optional(Type.Boolean({ description: "Send a display-only anonymous poll. Anonymous votes do not create agent turns. This is the default unless pollPublic is true." })), pollPublic: Type.Optional(Type.Boolean({ description: "Send a public poll whose votes route into the originating agent conversation. Voter identities are visible." })) }; } /** Schema additions for Telegram reactions through the existing react action. */ function createTelegramReactionEmojiSchema() { return { emoji: Type.Optional(Type.String({ description: "Telegram reaction emoji: use a supported Unicode reaction, or pass the numeric custom_emoji_id identifier returned by action:\"emoji-list\" directly as emoji. Use action:\"emoji-list\" to inspect reactions allowed in the current chat; arbitrary Unicode may be rejected by Telegram." })) }; } /** Schema additions for Telegram-native rich sends through the existing send action. */ function createTelegramRichSendExtraToolSchemas() { return { asVideoNote: Type.Optional(Type.Boolean({ description: "Send one video attachment as a round Telegram video note. Captions are delivered separately." })), location: Type.Optional(Type.Object({ latitude: Type.Number({ minimum: -90, maximum: 90 }), longitude: Type.Number({ minimum: -180, maximum: 180 }), accuracy: Type.Optional(Type.Number({ description: "Pin uncertainty radius in meters.", minimum: 0, maximum: 1500 })), name: Type.Optional(Type.String({ description: "Venue name; requires address.", minLength: 1 })), address: Type.Optional(Type.String({ description: "Venue address; requires name.", minLength: 1 })) }, { description: "Standalone Telegram location. Coordinates send a pin; name plus address sends a venue. Do not combine with message or media." })) }; } //#endregion //#region extensions/telegram/src/channel-actions.ts const loadTelegramActionRuntime = createLazyRuntimeModule(() => import("./action-runtime-BbxfVXeG.js")); const telegramMessageActionRuntime = { handleTelegramAction: async (...args) => { const { handleTelegramAction } = await loadTelegramActionRuntime(); return await handleTelegramAction(...args); } }; const TELEGRAM_MESSAGE_ACTION_MAP = { delete: "deleteMessage", edit: "editMessage", "emoji-list": "emoji-list", poll: "poll", react: "react", send: "sendMessage", sticker: "sendSticker", "sticker-search": "searchSticker", "topic-create": "createForumTopic", "topic-edit": "editForumTopic" }; const TELEGRAM_TOOL_DELIVERY_ACTIONS = /* @__PURE__ */ new Set([ "createForumTopic", "delete", "deleteMessage", "edit", "editForumTopic", "editMessage", "poll", "react", "send", "sendMessage", "sendSticker", "sticker", "topic-create", "topic-edit" ]); function resolveTelegramMessageActionName(action) { return TELEGRAM_MESSAGE_ACTION_MAP[action]; } async function prepareTelegramSendPayload({ ctx, payload }) { rejectTelegramNativeButtonParams(ctx.params); if (ctx.action !== "send" || !payload.presentation && !payload.location && payload.videoAsNote !== true) return null; const quoteText = readToolStringParam(ctx.params, "quoteText", { trim: false }); if (!quoteText) return payload; const rawTelegramData = payload.channelData?.telegram; const telegramData = asNonArrayRecord(rawTelegramData); return { ...payload, channelData: { ...payload.channelData, telegram: { ...telegramData, quoteText } } }; } function resolveTelegramActionDiscovery(cfg) { const inspected = listTelegramAccountIds(cfg).map((accountId) => inspectTelegramAccount({ cfg, accountId })).filter((account) => account.enabled && account.configured); const accounts = listTokenSourcedAccounts(inspected); if (accounts.length === 0) return null; const unionGate = createUnionActionGate(accounts, (account) => createTelegramActionGate({ cfg, accountId: account.accountId })); return { isEnabled: (key, defaultValue = true) => unionGate(key, defaultValue), pollEnabled: accounts.some((account) => { const accountGate = createTelegramActionGate({ cfg, accountId: account.accountId }); return resolveTelegramPollActionGateState(accountGate).enabled; }), buttonsEnabled: accounts.some((account) => isTelegramInlineButtonsEnabled({ cfg, accountId: account.accountId })) }; } function resolveScopedTelegramActionDiscovery(params) { if (!params.accountId) return resolveTelegramActionDiscovery(params.cfg); const account = inspectTelegramAccount({ cfg: params.cfg, accountId: params.accountId }); if (!account.enabled || !account.configured || account.tokenSource === "none") return null; const gate = createTelegramActionGate({ cfg: params.cfg, accountId: account.accountId }); return { isEnabled: (key, defaultValue = true) => gate(key, defaultValue), pollEnabled: resolveTelegramPollActionGateState(gate).enabled, buttonsEnabled: isTelegramInlineButtonsEnabled({ cfg: params.cfg, accountId: account.accountId }) }; } function describeTelegramMessageTool({ cfg, accountId }) { const discovery = resolveScopedTelegramActionDiscovery({ cfg, accountId }); if (!discovery) return { actions: [], capabilities: [], schema: null }; const actions = /* @__PURE__ */ new Set(); if (discovery.isEnabled("sendMessage")) actions.add("send"); if (discovery.pollEnabled) actions.add("poll"); if (discovery.isEnabled("reactions")) { actions.add("react"); actions.add("emoji-list"); } if (discovery.isEnabled("deleteMessage")) actions.add("delete"); if (discovery.isEnabled("editMessage")) actions.add("edit"); if (discovery.isEnabled("sticker", false)) { actions.add("sticker"); actions.add("sticker-search"); } if (discovery.isEnabled("createForumTopic")) actions.add("topic-create"); if (discovery.isEnabled("editForumTopic")) actions.add("topic-edit"); const schema = []; if (discovery.pollEnabled) schema.push({ properties: createTelegramPollExtraToolSchemas(), visibility: "all-configured" }); if (discovery.isEnabled("reactions")) schema.push({ properties: createTelegramReactionEmojiSchema(), actions: [] }); if (discovery.isEnabled("sendMessage")) schema.push({ properties: createTelegramRichSendExtraToolSchemas(), visibility: "all-configured" }); return { actions: Array.from(actions), capabilities: discovery.buttonsEnabled ? ["presentation", "delivery-pin"] : ["delivery-pin"], schema }; } const telegramMessageActions = { describeMessageTool: describeTelegramMessageTool, providerOwnedReadGates: [ "react", "edit", "delete", "emoji-list" ], resolveExecutionMode: () => "gateway", messageActionTargetAliases: { react: { aliases: ["messageId"], deliveryTargetAliases: [] }, edit: { aliases: ["messageId"], deliveryTargetAliases: [] }, delete: { aliases: ["messageId"], deliveryTargetAliases: [] } }, prepareSendPayload: prepareTelegramSendPayload, resolveCliActionRequest: ({ action, args }) => { if (action !== "thread-create") return { action, args }; const { threadName, ...rest } = args; return { action: "topic-create", args: { ...rest, name: readStringValue(threadName) } }; }, extractToolSend: ({ args }) => { return extractToolSend(args, "sendMessage"); }, isToolDeliveryAction: ({ args }) => typeof args.action === "string" && TELEGRAM_TOOL_DELIVERY_ACTIONS.has(args.action), handleAction: async ({ action, params, reply, cfg, accountId, mediaAccess, mediaLocalRoots, mediaReadFile, sessionKey, inboundEventKind, toolContext, conversationReadOrigin, requesterAccountId, gatewayClientScopes, deliveryRetryOwner }) => { const telegramAction = resolveTelegramMessageActionName(action); if (!telegramAction) throw new Error(`Unsupported Telegram action: ${action}`); const { conversationReadOrigin: _modelConversationReadOrigin, mediaAccess: _modelMediaAccess, requesterAccountId: _modelRequesterAccountId, reply: _modelReply, toolContext: _modelToolContext, ...runtimeParams } = params; return await telegramMessageActionRuntime.handleTelegramAction({ ...runtimeParams, action: telegramAction, accountId: accountId ?? void 0, ...action === "react" ? { messageId: resolveReactionMessageId({ args: runtimeParams, toolContext }) } : {} }, cfg, { ...mediaAccess !== void 0 ? { mediaAccess } : {}, mediaLocalRoots, mediaReadFile, sessionKey, inboundEventKind, gatewayClientScopes, deliveryRetryOwner, ...conversationReadOrigin ? { conversationReadOrigin } : {}, ...requesterAccountId ? { requesterAccountId } : {}, ...reply ? { reply } : {}, ...toolContext ? { toolContext } : {} }); } }; //#endregion //#region extensions/telegram/src/exec-approval-forwarding.ts function shouldSuppressTelegramExecApprovalForwardingFallback(params) { if ((normalizeMessageChannel(params.target.channel) ?? params.target.channel) !== "telegram") return false; if (normalizeMessageChannel(params.request.request.turnSourceChannel ?? "") !== "telegram") return false; const accountId = params.target.accountId?.trim() || params.request.request.turnSourceAccountId?.trim(); return isTelegramExecApprovalClientEnabled({ cfg: params.cfg, accountId }); } function buildTelegramExecApprovalPendingPayload(params) { return buildTypedExecApprovalPendingReplyPayload({ approvalId: params.request.id, approvalSlug: params.request.id.slice(0, 8), approvalCommandId: params.request.id, warningText: params.request.request.warningText ?? void 0, command: resolveExecApprovalCommandDisplay(params.request.request).commandText, cwd: params.request.request.cwd ?? void 0, host: params.request.request.host === "node" ? "node" : "gateway", nodeId: params.request.request.nodeId ?? void 0, scope: params.request.request.scope ?? void 0, allowedDecisions: resolveExecApprovalRequestAllowedDecisions(params.request.request), expiresAtMs: params.request.expiresAtMs, nowMs: params.nowMs }); } //#endregion //#region extensions/telegram/src/polling-lease.ts const TELEGRAM_POLLING_LEASES_KEY = Symbol.for("openclaw.telegram.pollingLeases"); const DEFAULT_TELEGRAM_POLLING_LEASE_WAIT_MS = 5e3; function pollingLeaseRegistry() { const proc = process; proc[TELEGRAM_POLLING_LEASES_KEY] ??= /* @__PURE__ */ new Map(); return proc[TELEGRAM_POLLING_LEASES_KEY]; } function createDuplicatePollingError(params) { const ageMs = Math.max(0, Date.now() - params.existing.startedAt); const ageSeconds = Math.round(ageMs / 1e3); return /* @__PURE__ */ new Error(`Telegram polling already active for bot token ${params.tokenFingerprint} on account "${params.existing.accountId}" (${ageSeconds}s old); refusing duplicate poller for account "${params.accountId}". Stop the existing OpenClaw gateway/poller or use a different bot token.`); } async function waitForPreviousRelease(params) { if (params.signal?.aborted) return "aborted"; if (params.waitMs <= 0) return "timeout"; let timer; let abortListener; try { const waitMs = resolveTimerTimeoutMs(params.waitMs, DEFAULT_TELEGRAM_POLLING_LEASE_WAIT_MS, 0); const timeout = new Promise((resolve) => { timer = setTimeout(() => resolve("timeout"), waitMs); timer.unref?.(); }); const aborted = new Promise((resolve) => { abortListener = () => resolve("aborted"); params.signal?.addEventListener("abort", abortListener, { once: true }); }); const released = params.done.then(() => "released"); return await Promise.race([ released, timeout, aborted ]); } finally { if (timer) clearTimeout(timer); if (abortListener) params.signal?.removeEventListener("abort", abortListener); } } function createLease(params) { let resolveDone; const done = new Promise((resolve) => { resolveDone = resolve; }); const owner = Symbol(`telegram-polling:${params.accountId}`); const entry = { accountId: params.accountId, abortSignal: params.abortSignal, done, owner, resolveDone, startedAt: Date.now() }; params.registry.set(params.tokenFingerprint, entry); let released = false; return { tokenFingerprint: params.tokenFingerprint, waitedForPrevious: params.waitedForPrevious, replacedStoppingPrevious: params.replacedStoppingPrevious, release: () => { if (released) return; released = true; if (params.registry.get(params.tokenFingerprint)?.owner === owner) params.registry.delete(params.tokenFingerprint); resolveDone(); } }; } async function acquireTelegramPollingLease(opts) { const registry = pollingLeaseRegistry(); const fingerprint = fingerprintTelegramBotToken(opts.token); const waitMs = opts.waitMs ?? DEFAULT_TELEGRAM_POLLING_LEASE_WAIT_MS; let waitedForPrevious = false; for (;;) { const existing = registry.get(fingerprint); if (!existing) return createLease({ accountId: opts.accountId, abortSignal: opts.abortSignal, registry, tokenFingerprint: fingerprint, waitedForPrevious, replacedStoppingPrevious: false }); if (!existing.abortSignal?.aborted) throw createDuplicatePollingError({ accountId: opts.accountId, existing, tokenFingerprint: fingerprint }); waitedForPrevious = true; const waitResult = await waitForPreviousRelease({ done: existing.done, signal: opts.abortSignal, waitMs }); if (waitResult === "aborted") throw new Error(`Telegram polling start aborted while waiting for previous poller for bot token ${fingerprint} to stop.`); if (registry.get(fingerprint) !== existing) continue; if (waitResult === "released") continue; return createLease({ accountId: opts.accountId, abortSignal: opts.abortSignal, registry, tokenFingerprint: fingerprint, waitedForPrevious, replacedStoppingPrevious: true }); } } async function releaseStoppedTelegramPollingLease(opts) { const registry = pollingLeaseRegistry(); const fingerprint = fingerprintTelegramBotToken(opts.token); const existing = registry.get(fingerprint); if (!existing || existing.accountId !== opts.accountId) return false; if (!existing.abortSignal?.aborted) return false; if (await waitForPreviousRelease({ done: existing.done, waitMs: opts.waitMs ?? DEFAULT_TELEGRAM_POLLING_LEASE_WAIT_MS }) === "released" || registry.get(fingerprint) !== existing) return false; registry.delete(fingerprint); existing.resolveDone(); return true; } //#endregion //#region extensions/telegram/src/update-offset-persistence.ts const OFFSET_PERSIST_RETRY_POLICY = { initialMs: 250, maxMs: 5e3, factor: 2, jitter: .1 }; function normalizeTelegramUpdateId(value) { return asSafeIntegerInRange(value, { min: 0 }) ?? null; } function createTelegramUpdateOffsetPersistence(options) { const stopController = new AbortController(); const retrySignal = options.abortSignal ? AbortSignal.any([options.abortSignal, stopController.signal]) : stopController.signal; let acceptedUpdateId = options.initialUpdateId; let committedUpdateId = options.initialUpdateId; let pendingUpdateId = null; let activeDrain; const drain = async () => { let attempt = 0; while (pendingUpdateId !== null) { if (retrySignal.aborted) return; const updateId = pendingUpdateId; try { await options.writeUpdateId(updateId); committedUpdateId = updateId; if (pendingUpdateId === updateId) pendingUpdateId = null; attempt = 0; } catch (error) { if (retrySignal.aborted) return; attempt += 1; const delayMs = computeBackoff(OFFSET_PERSIST_RETRY_POLICY, attempt); options.onRetry({ attempt, delayMs, error, updateId }); await sleepWithAbort(delayMs, retrySignal, { ref: false }); } } }; const startDrain = () => { if (activeDrain) return; const run = drain().catch(() => void 0).finally(() => { if (activeDrain === run) { activeDrain = void 0; if (pendingUpdateId !== null && !retrySignal.aborted) startDrain(); } }); activeDrain = run; }; const persistUpdateId = (updateId) => { if (retrySignal.aborted) return; const normalizedUpdateId = normalizeTelegramUpdateId(updateId); if (normalizedUpdateId === null) { options.onInvalidUpdateId(updateId); return; } if (acceptedUpdateId !== null && normalizedUpdateId <= acceptedUpdateId) return; acceptedUpdateId = normalizedUpdateId; pendingUpdateId = normalizedUpdateId; startDrain(); }; const stop = async () => { stopController.abort(/* @__PURE__ */ new Error("Telegram update-offset persistence stopped.")); await activeDrain?.catch(() => void 0); }; return { getAcceptedUpdateId: () => acceptedUpdateId, getCommittedUpdateId: () => committedUpdateId, persistUpdateId, stop }; } //#endregion //#region extensions/telegram/src/monitor.ts function createTelegramRunnerOptions(cfg) { return { sink: { concurrency: resolveAgentMaxConcurrent(cfg) }, runner: { fetch: { timeout: 30, allowed_updates: resolveTelegramAllowedUpdates() }, silent: true, maxRetryTime: 36e5, retryInterval: "exponential" } }; } const TELEGRAM_OFFSET_ROTATION_LABELS = { "bot-id-changed": "bot identity change", "legacy-state": "legacy update offset", "token-rotated": "token rotation" }; function formatTelegramOffsetRotationMessage(accountId, info) { const previousLabel = info.previousBotId ?? "(legacy unscoped offset)"; return `[telegram] Detected ${TELEGRAM_OFFSET_ROTATION_LABELS[info.reason]} for account "${accountId}" (was ${previousLabel}, now ${info.currentBotId}); discarding stale update offset ${info.staleLastUpdateId} and starting fresh.`; } /** Check if error is a Grammy HttpError (used to scope unhandled rejection handling) */ const isGrammyHttpError = (err) => { if (!err || typeof err !== "object") return false; return err.name === "HttpError"; }; const loadTelegramMonitorPollingRuntime = createLazyRuntimeModule(() => import("./monitor-polling.runtime.js")); const loadTelegramMonitorWebhookRuntime = createLazyRuntimeModule(() => import("./monitor-webhook.runtime.js")); async function monitorTelegramProvider(opts = {}) { const logInfo = (line) => (opts.runtime?.log ?? console.log)(line); const logError = (line) => (opts.runtime?.error ?? console.error)(line); const log = (line) => { if (line.includes("[telegram][diag]")) { logInfo(line); return; } logError(line); }; let pollingSession; const handlePollingNetworkFailure = (err, label) => { const isNetworkError = isRecoverableTelegramNetworkError(err, { context: "polling" }); const isTelegramPollingError = isTelegramPollingNetworkError(err); const activeRunner = pollingSession?.activeRunner; if (isNetworkError && isTelegramPollingError && activeRunner && activeRunner.isRunning()) { pollingSession?.markForceRestarted(); pollingSession?.markTransportDirty(); pollingSession?.abortActiveFetch(); activeRunner.stop().catch(() => {}); log("[telegram][diag] marking transport dirty after polling network failure"); log(`[telegram] Restarting polling after ${label}: ${formatErrorMessage(err)}`); return true; } if (isGrammyHttpError(err) && isNetworkError && isTelegramPollingError) { log(`[telegram] Suppressed network error: ${formatErrorMessage(err)}`); return true; } return false; }; const unregisterUnhandledRejectionHandler = registerUnhandledRejectionHandler((err) => handlePollingNetworkFailure(err, "unhandled network error")); const unregisterUncaughtExceptionHandler = registerUncaughtExceptionHandler((err) => handlePollingNetworkFailure(err, "uncaught network error")); try { const cfg = opts.config ?? getRuntimeConfig(); const account = resolveTelegramAccount({ cfg, accountId: opts.accountId }); const ownerAgentId = opts.ownerAgentId?.trim() || resolveTelegramAccountOwnerAgentId({ cfg, accountId: account.accountId }); const token = opts.token?.trim() || account.token; if (!token) throw new Error(`Telegram bot token missing for account "${account.accountId}" (set channels.telegram.accounts.${account.accountId}.botToken/tokenFile or TELEGRAM_BOT_TOKEN for default).`); const proxyFetch = opts.proxyFetch ?? (account.config.proxy ? makeProxyFetch(account.config.proxy) : void 0); const pluginChannelRuntime = opts.channelRuntime; if (opts.useWebhook) { const { startTelegramWebhook } = await loadTelegramMonitorWebhookRuntime(); if (isTelegramExecApprovalHandlerConfigured({ cfg, accountId: account.accountId })) registerChannelRuntimeContext({ channelRuntime: opts.channelRuntime, channelId: "telegram", accountId: account.accountId, capability: CHANNEL_APPROVAL_NATIVE_RUNTIME_CONTEXT_CAPABILITY, context: { token }, abortSignal: opts.abortSignal }); await startTelegramWebhook({ token, accountId: account.accountId, ownerAgentId, config: cfg, path: opts.webhookPath, port: opts.webhookPort, secret: opts.webhookSecret ?? account.config.webhookSecret, host: opts.webhookHost ?? account.config.webhookHost, runtime: opts.runtime, buildContext: pluginChannelRuntime?.inbound.buildContext, dispatchReplyFromConfig: pluginChannelRuntime?.reply?.dispatchReplyFromConfig, fetch: proxyFetch, abortSignal: opts.abortSignal, publicUrl: opts.webhookUrl, webhookCertPath: opts.webhookCertPath, setStatus: opts.setStatus }); await waitForAbortSignal(opts.abortSignal); return; } const { TelegramPollingSession, deleteTelegramUpdateOffset, readTelegramUpdateOffset, writeTelegramUpdateOffset } = await loadTelegramMonitorPollingRuntime(); const pollingLease = await acquireTelegramPollingLease({ token, accountId: account.accountId, abortSignal: opts.abortSignal }); if (pollingLease.waitedForPrevious) log(`[telegram][diag] waited for previous polling session for bot token ${pollingLease.tokenFingerprint} before starting account "${account.accountId}".`); if (pollingLease.replacedStoppingPrevious) log(`[telegram][diag] previous polling session for bot token ${pollingLease.tokenFingerprint} did not stop within the lease wait; starting a replacement for account "${account.accountId}".`); try { if (isTelegramExecApprovalHandlerConfigured({ cfg, accountId: account.accountId })) registerChannelRuntimeContext({ channelRuntime: opts.channelRuntime, channelId: "telegram", accountId: account.accountId, capability: CHANNEL_APPROVAL_NATIVE_RUNTIME_CONTEXT_CAPABILITY, context: { token }, abortSignal: opts.abortSignal }); const persistedOffsetRaw = await readTelegramUpdateOffset({ accountId: account.accountId, botToken: token, onRotationDetected: async (info) => { log(formatTelegramOffsetRotationMessage(account.accountId, info)); try { await deleteTelegramUpdateOffset({ accountId: account.accountId }); } catch (err) { logError(`telegram: failed to delete stale update offset after rotation: ${String(err)}`); } } }); const lastUpdateId = normalizeTelegramUpdateId(persistedOffsetRaw); if (persistedOffsetRaw !== null && lastUpdateId === null) log(`[telegram] Ignoring invalid persisted update offset (${String(persistedOffsetRaw)}); starting without offset confirmation.`); const offsetPersistence = createTelegramUpdateOffsetPersistence({ initialUpdateId: lastUpdateId, writeUpdateId: async (updateId) => { await writeTelegramUpdateOffset({ accountId: account.accountId, updateId, botToken: token }); }, onInvalidUpdateId: (updateId) => { log(`[telegram] Ignoring invalid update_id value: ${String(updateId)}`); }, onRetry: ({ attempt, delayMs, error, updateId }) => { logError(`telegram: failed to persist update offset ${updateId}; retry ${attempt} in ${delayMs}ms: ${formatErrorMessage(error)}`); }, abortSignal: opts.abortSignal }); const createTelegramTransportForPolling = () => resolveTelegramTransport(proxyFetch, { network: account.config.network }); const telegramTransport = createTelegramTransportForPolling(); pollingSession = new TelegramPollingSession({ token, config: cfg, accountId: account.accountId, ownerAgentId, runtime: opts.runtime, buildContext: pluginChannelRuntime?.inbound.buildContext, dispatchReplyFromConfig: pluginChannelRuntime?.reply?.dispatchReplyFromConfig, proxyFetch, botInfo: opts.botInfo, abortSignal: opts.abortSignal, runnerOptions: createTelegramRunnerOptions(cfg), getAcceptedUpdateId: offsetPersistence.getAcceptedUpdateId, getCommittedUpdateId: offsetPersistence.getCommittedUpdateId, persistUpdateId: offsetPersistence.persistUpdateId, log, telegramTransport, createTelegramTransport: createTelegramTransportForPolling, setStatus: opts.setStatus, isolatedIngress: { enabled: opts.isolatedIngress?.enabled ?? true, apiRoot: account.config.apiRoot, proxy: account.config.proxy, network: account.config.network } }); try { await pollingSession.runUntilAbort(); } finally { await offsetPersistence.stop(); } } finally { pollingLease.release(); } } finally { unregisterUnhandledRejectionHandler(); unregisterUncaughtExceptionHandler(); } } //#endregion //#region extensions/telegram/src/probe.ts const probeTransportCache = /* @__PURE__ */ new Map(); const MAX_PROBE_TRANSPORT_CACHE_SIZE = 64; const TELEGRAM_BOT_API_MAX_RESPONSE_BYTES = 4194304; function resolveProbeOptions(proxyOrOptions) { if (!proxyOrOptions) return; if (typeof proxyOrOptions === "string") return { proxyUrl: proxyOrOptions }; return proxyOrOptions; } function buildProbeTransportCacheKey(token, options) { const cacheIdentity = options?.accountId?.trim() || token; const cacheIdentityKind = options?.accountId?.trim() ? "account" : "token"; const proxyKey = options?.proxyUrl?.trim() ?? ""; const autoSelectFamily = options?.network?.autoSelectFamily; return `${cacheIdentityKind}:${cacheIdentity}::${proxyKey}::${typeof autoSelectFamily === "boolean" ? String(autoSelectFamily) : "default"}::${options?.network?.dnsResultOrder ?? "default"}::${options?.apiRoot?.trim() ?? ""}`; } function setCachedProbeTransport(cacheKey, transport) { probeTransportCache.set(cacheKey, transport); if (probeTransportCache.size > MAX_PROBE_TRANSPORT_CACHE_SIZE) { const oldestKey = probeTransportCache.keys().next().value; if (oldestKey !== void 0) { const oldestTransport = probeTransportCache.get(oldestKey); probeTransportCache.delete(oldestKey); oldestTransport?.close(); } } return transport; } function resolveProbeTransport(token, options) { const cacheKey = buildProbeTransportCacheKey(token, options); const cached = probeTransportCache.get(cacheKey); if (cached) return cached; const proxyUrl = options?.proxyUrl?.trim(); const proxyFetch = proxyUrl ? makeProxyFetch(proxyUrl) : void 0; return setCachedProbeTransport(cacheKey, resolveTelegramTransport(proxyFetch, { network: options?.network })); } function normalizeBoolean(value) { return typeof value === "boolean" ? value : null; } async function readTelegramDiagnosticBody(response, timeoutMs) { return await readResponseWithLimit(response, TELEGRAM_BOT_API_MAX_RESPONSE_BYTES, { timeoutMs, chunkTimeoutMs: timeoutMs / 2, onIdleTimeout: ({ chunkTimeoutMs }) => /* @__PURE__ */ new Error(`Telegram diagnostic response body stalled for ${chunkTimeoutMs}ms`), onTimeout: ({ timeoutMs: resolvedTimeoutMs }) => /* @__PURE__ */ new Error(`Telegram diagnostic response body timed out after ${resolvedTimeoutMs}ms`) }); } async function probeTelegram(token, timeoutMs, proxyOrOptions) { return await runChannelProbe(void 0, async ({ startedAt }) => { const timeoutBudgetMs = Math.max(1, Math.floor(timeoutMs)); const deadlineMs = startedAt + timeoutBudgetMs; const options = resolveProbeOptions(proxyOrOptions); const abortSignal = options?.abortSignal; const includeWebhookInfo = options?.includeWebhookInfo !== false; const transport = resolveProbeTransport(token, options); const fetcher = transport.fetch; const base = `${resolveTelegramApiBase(options?.apiRoot)}/bot${token}`; const retryDelayMs = Math.max(50, Math.min(1e3, Math.floor(timeoutBudgetMs / 5))); const resolveRemainingBudgetMs = () => Math.max(0, deadlineMs - Date.now()); const result = { ok: false, status: null, error: null }; let meRes = null; let fetchError = null; for (let i = 0; i < 3; i++) { const remainingBudgetMs = resolveRemainingBudgetMs(); if (remainingBudgetMs <= 0 || abortSignal?.aborted) break; try { meRes = await fetchWithTimeout(`${base}/getMe`, { signal: abortSignal }, Math.max(1, Math.min(timeoutBudgetMs, remainingBudgetMs)), fetcher); break; } catch (err) { fetchError = err; if (abortSignal?.aborted) throw err; transport.forceFallback?.("probe timeout/network error", err); if (i < 2) { const remainingAfterAttemptMs = resolveRemainingBudgetMs(); if (remainingAfterAttemptMs <= 0) break; const delayMs = Math.min(retryDelayMs, remainingAfterAttemptMs); if (delayMs > 0) await sleepWithAbort(delayMs, abortSignal); } } } if (!meRes) throw toErrorObject(fetchError ?? /* @__PURE__ */ new Error(`probe timed out after ${timeoutBudgetMs}ms`), "Non-Error thrown"); const meJson = JSON.parse((await readTelegramDiagnosticBody(meRes, Math.min(timeoutBudgetMs, resolveRemainingBudgetMs()))).toString("utf8")); if (!meRes.ok || !meJson?.ok) { result.status = meRes.status; result.error = meJson?.description ?? `getMe failed (${meRes.status})`; return result; } const botInfo = normalizeTelegramBotInfo(meJson.result); const bot = meJson.result && typeof meJson.result === "object" ? meJson.result : {}; if (botInfo) result.botInfo = botInfo; result.bot = { id: typeof bot.id === "number" ? bot.id : null, isBot: normalizeBoolean(bot.is_bot), firstName: typeof bot.first_name === "string" ? bot.first_name : null, username: typeof bot.username === "string" ? bot.username : null, canJoinGroups: normalizeBoolean(bot.can_join_groups), canReadAllGroupMessages: normalizeBoolean(bot.can_read_all_group_messages), canManageBots: normalizeBoolean(bot.can_manage_bots), supportsInlineQueries: normalizeBoolean(bot.supports_inline_queries), canConnectToBusiness: normalizeBoolean(bot.can_connect_to_business), hasMainWebApp: normalizeBoolean(bot.has_main_web_app), hasTopicsEnabled: normalizeBoolean(bot.has_topics_enabled), allowsUsersToCreateTopics: normalizeBoolean(bot.allows_users_to_create_topics) }; if (includeWebhookInfo) try { const webhookRemainingBudgetMs = resolveRemainingBudgetMs(); if (webhookRemainingBudgetMs > 0) { const webhookRes = await fetchWithTimeout(`${base}/getWebhookInfo`, { signal: abortSignal }, Math.max(1, Math.min(timeoutBudgetMs, webhookRemainingBudgetMs)), fetcher); const webhookJson = JSON.parse((await readTelegramDiagnosticBody(webhookRes, Math.min(timeoutBudgetMs, resolveRemainingBudgetMs()))).toString("utf8")); if (webhookRes.ok && webhookJson?.ok) result.webhook = { url: webhookJson.result?.url ?? null, hasCustomCert: webhookJson.result?.has_custom_certificate ?? null }; } } catch (err) { if (abortSignal?.aborted) throw err; } result.ok = true; result.status = null; result.error = null; return result; }, (error) => ({ ok: false, status: error instanceof Response ? error.status : null, error: formatErrorMessage(error) })); } //#endregion export { shouldSuppressTelegramExecApprovalForwardingFallback as a, buildTelegramExecApprovalPendingPayload as i, monitorTelegramProvider as n, telegramMessageActions as o, releaseStoppedTelegramPollingLease as r, probeTelegram as t };