fix(telegram): isolate long polling transport

This commit is contained in:
xmanrui 2026-08-31 03:31:07 +08:00
parent 02ddea0054
commit 14519060f0
13 changed files with 603 additions and 181 deletions

View file

@ -105,19 +105,23 @@ export const COMMANDS_MENU_BUTTON = Object.freeze({ type: 'commands' });
export class TelegramApi {
#token;
#fetch;
#FormDataImpl;
#baseUrl;
#fileUploadTimeoutMs;
constructor({
token,
fetchImpl = fetch,
FormDataImpl = globalThis.FormData,
baseUrl = DEFAULT_BASE_URL,
fileUploadTimeoutMs = DEFAULT_FILE_UPLOAD_TIMEOUT_MS,
}) {
if (!validTelegramToken(token)) throw new TypeError('Telegram Bot Token is invalid');
if (typeof fetchImpl !== 'function') throw new TypeError('TelegramApi requires fetch');
if (typeof FormDataImpl !== 'function') throw new TypeError('TelegramApi requires FormData');
this.#token = token.trim();
this.#fetch = fetchImpl;
this.#FormDataImpl = FormDataImpl;
this.#baseUrl = new URL(baseUrl);
this.#fileUploadTimeoutMs = positiveTimeout(fileUploadTimeoutMs, 'fileUploadTimeoutMs');
}
@ -273,7 +277,7 @@ export class TelegramApi {
|| !Buffer.isBuffer(file.bytes)) {
throw new TypeError(`A Telegram ${mediaLabel} is required`);
}
const payload = new FormData();
const payload = new this.#FormDataImpl();
payload.append('chat_id', String(chatId));
payload.append(
fieldName,

View file

@ -0,0 +1,21 @@
import {
EnvHttpProxyAgent,
FormData as UndiciFormData,
fetch as undiciFetch,
} from 'undici';
const TELEGRAM_CONNECTIONS = 4;
export function createTelegramHttpTransport() {
const dispatcher = new EnvHttpProxyAgent({ connections: TELEGRAM_CONNECTIONS });
let destroyed = false;
return {
fetchImpl: (url, options = {}) => undiciFetch(url, { ...options, dispatcher }),
FormDataImpl: UndiciFormData,
async destroy() {
if (destroyed) return;
destroyed = true;
await dispatcher.destroy();
},
};
}

View file

@ -5,6 +5,7 @@ import { createTextDeliveryBlock } from '../shared/semantic/delivery.mjs';
import { t } from '../shared/i18n.mjs';
import { captureContextEnhancement } from '../shared/context-enhancement.mjs';
import { COMMANDS_MENU_BUTTON, TelegramApi } from './telegram-api.mjs';
import { createTelegramHttpTransport } from './telegram-http.mjs';
import { createTelegramBridgeStatus, TelegramHarnessBridge } from './telegram-bridge.mjs';
import {
splitTelegramRegularText,
@ -671,9 +672,11 @@ export class TelegramRuntime {
#logger;
#replyTimeoutMs;
#createApi;
#createHttpTransport;
#accessMode;
#allowedPrivateUserIds;
#status = createTelegramRuntimeStatus();
#httpTransport = null;
#api = null;
#bridge = null;
#abortController = null;
@ -689,6 +692,7 @@ export class TelegramRuntime {
logger = console,
replyTimeoutMs = 600_000,
createApi = (options) => new TelegramApi(options),
createHttpTransport = createTelegramHttpTransport,
}) {
if (!config || !token || !harness || !state) {
throw new TypeError('TelegramRuntime requires config, token, Harness, and state');
@ -701,6 +705,7 @@ export class TelegramRuntime {
this.#logger = logger;
this.#replyTimeoutMs = replyTimeoutMs;
this.#createApi = createApi;
this.#createHttpTransport = createHttpTransport;
const accessPolicy = normalizeTelegramAccessPolicy(config);
this.#accessMode = accessPolicy.accessMode;
this.#allowedPrivateUserIds = new Set(accessPolicy.allowedUsers);
@ -762,9 +767,15 @@ export class TelegramRuntime {
const controller = new AbortController();
this.#abortController = controller;
const api = this.#createApi({ token: this.#token });
this.#api = api;
try {
const transport = this.#createHttpTransport();
this.#httpTransport = transport;
const api = this.#createApi({
token: this.#token,
fetchImpl: transport.fetchImpl,
FormDataImpl: transport.FormDataImpl,
});
this.#api = api;
const bot = await api.getMe({ signal: controller.signal });
if (String(bot?.id ?? '') !== this.#config.platformId || bot?.is_bot !== true) {
throw new Error('Telegram token identity does not match the saved bot');
@ -878,9 +889,11 @@ export class TelegramRuntime {
async stop() {
const pollTask = this.#pollTask;
const bridge = this.#bridge;
const httpTransport = this.#httpTransport;
this.#abortController?.abort();
this.#abortController = null;
this.#pollTask = null;
this.#httpTransport = null;
this.#api = null;
this.#bridge = null;
await Promise.race([
@ -891,6 +904,11 @@ export class TelegramRuntime {
bridge?.waitForIdle() ?? Promise.resolve(),
new Promise((resolve) => setTimeout(resolve, 2_000)),
]);
try {
await httpTransport?.destroy();
} catch {
this.#logger.warn?.('[dsh-im:telegram] Telegram HTTP transport cleanup failed');
}
this.#status.ready = false;
this.#status.connectionState = 'idle';
return this.status;