@zhin.js/adapter-telegram 1.1.17 → 1.1.19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +65 -0
- package/README.md +19 -2
- package/adapters/telegram/index.js +3 -17
- package/adapters/telegram/index.ts +2 -17
- package/agents/telegram/skills/telegram-messages/tools/send_poll/index.js +2 -1
- package/agents/telegram/skills/telegram-messages/tools/send_poll/index.ts +2 -4
- package/commands/telegram/endpoint/definition.js +0 -2
- package/commands/telegram/endpoint/definition.ts +0 -2
- package/lib/adapter.d.ts +15 -0
- package/lib/adapter.js +33 -0
- package/lib/client.d.ts +3 -7
- package/lib/client.js +1 -2
- package/lib/console-spec.d.ts +12 -0
- package/lib/console-spec.js +15 -0
- package/lib/console.d.ts +2 -0
- package/lib/console.js +13 -0
- package/lib/endpoint.d.ts +7 -84
- package/lib/endpoint.js +126 -289
- package/lib/index.d.ts +3 -1
- package/lib/index.js +1 -1
- package/lib/telegram-client.d.ts +79 -0
- package/lib/telegram-client.js +159 -0
- package/package.json +26 -17
- package/pages/index/index.js +15 -0
- package/pages/index/index.ts +16 -0
- package/plugin.js +3 -4
- package/src/adapter.ts +27 -0
- package/src/client.ts +2 -10
- package/src/console-spec.ts +20 -0
- package/src/console.ts +17 -0
- package/src/endpoint.ts +107 -359
- package/src/index.ts +4 -1
- package/src/telegram-client.ts +250 -0
- package/lib/telegram-runtime-state.d.ts +0 -1
- package/lib/telegram-runtime-state.js +0 -6
- package/src/telegram-runtime-state.ts +0 -7
package/lib/endpoint.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { TelegramClient } from './telegram-client.js';
|
|
2
|
+
import { createEndpointSpecification, endpointSpec, createEndpointLifecycle, } from 'zhin.js/adapter';
|
|
2
3
|
/**
|
|
3
4
|
* TelegramEndpoint — lifecycle, outbound, admit, Bot API helpers for agent tools.
|
|
4
5
|
*/
|
|
@@ -6,64 +7,30 @@ import { readFile } from 'node:fs/promises';
|
|
|
6
7
|
import { EndpointDeliveryError, } from '@zhin.js/im-contract';
|
|
7
8
|
import { formatCompact, getAdapterLogger } from '@zhin.js/logger';
|
|
8
9
|
import { runTelegramPollLoop } from './polling.js';
|
|
9
|
-
import { TelegramApiError } from './api-error.js';
|
|
10
10
|
export { TelegramApiError } from './api-error.js';
|
|
11
11
|
import { normalizeTelegramChatMember } from './platform-permit.js';
|
|
12
|
-
import {
|
|
12
|
+
import { buildWebhookUrl, formatCallbackContent, formatCallbackSegments, formatInboundContent, formatInboundSegments, formatOutboundPlan, resolveTelegramChannelType, senderDisplayName, telegramInboundConversation, } from './protocol.js';
|
|
13
13
|
import { registerTelegramWebhookRoutes } from './webhook.js';
|
|
14
14
|
const CHAT_MEMBER_CACHE_TTL_MS = 60_000;
|
|
15
15
|
const CHAT_MEMBER_CACHE_MAX = 2_000;
|
|
16
16
|
const ADMITTED_UPDATE_LIMIT = 2_048;
|
|
17
17
|
const INFLIGHT_UPDATE_LIMIT = 256;
|
|
18
|
-
|
|
19
|
-
/** Telegram Bot API client carried by message, update and lifecycle events. */
|
|
20
|
-
export class TelegramClient {
|
|
21
|
-
api;
|
|
22
|
-
constructor(api) {
|
|
23
|
-
this.api = api;
|
|
24
|
-
}
|
|
25
|
-
callApi = (method, params = {}, signal) => this.api.callApi(method, params, signal);
|
|
26
|
-
callApiForm = (method, form, signal) => this.api.callApiForm(method, form, signal);
|
|
27
|
-
pinMessage = (chatId, messageId) => this.api.pinMessage(chatId, messageId);
|
|
28
|
-
unpinMessage = (chatId, messageId) => this.api.unpinMessage(chatId, messageId);
|
|
29
|
-
setChatDescription = (chatId, description) => this.api.setChatDescription(chatId, description);
|
|
30
|
-
setMessageReaction = (chatId, messageId, reaction) => this.api.setMessageReaction(chatId, messageId, reaction);
|
|
31
|
-
getChatMemberCount = (chatId) => this.api.getChatMemberCount(chatId);
|
|
32
|
-
getChatAdmins = (chatId) => this.api.getChatAdmins(chatId);
|
|
33
|
-
sendStickerMessage = (chatId, sticker) => this.api.sendStickerMessage(chatId, sticker);
|
|
34
|
-
setChatPermissionsAll = (chatId, permissions) => this.api.setChatPermissionsAll(chatId, permissions);
|
|
35
|
-
createInviteLink = (chatId) => this.api.createInviteLink(chatId);
|
|
36
|
-
sendPoll = (chatId, question, options, anonymous, multiple) => this.api.sendPoll(chatId, question, options, anonymous, multiple);
|
|
37
|
-
}
|
|
18
|
+
export { TelegramClient } from './telegram-client.js';
|
|
38
19
|
/**
|
|
39
20
|
* Telegram Bot API 无列表类接口(无 getMyChats/getChatMembers),
|
|
40
21
|
* 仅 getChat/getChatMember 按已知 id 单查,不构成列表能力;
|
|
41
22
|
* 因此本 endpoint 不暴露 EndpointManagement(Console 社交面 RPC 对该平台保持未接线)。
|
|
42
23
|
*/
|
|
43
|
-
export class TelegramEndpoint extends
|
|
44
|
-
|
|
45
|
-
callApi: (method, params, signal) => this.callApi(method, params, signal),
|
|
46
|
-
callApiForm: (method, form, signal) => this.callApiForm(method, form, signal),
|
|
47
|
-
pinMessage: (chatId, messageId) => this.pinMessage(chatId, messageId),
|
|
48
|
-
unpinMessage: (chatId, messageId) => this.unpinMessage(chatId, messageId),
|
|
49
|
-
setChatDescription: (chatId, description) => this.setChatDescription(chatId, description),
|
|
50
|
-
setMessageReaction: (chatId, messageId, reaction) => this.setMessageReaction(chatId, messageId, reaction),
|
|
51
|
-
getChatMemberCount: (chatId) => this.getChatMemberCount(chatId),
|
|
52
|
-
getChatAdmins: (chatId) => this.getChatAdmins(chatId),
|
|
53
|
-
sendStickerMessage: (chatId, sticker) => this.sendStickerMessage(chatId, sticker),
|
|
54
|
-
setChatPermissionsAll: (chatId, permissions) => this.setChatPermissionsAll(chatId, permissions),
|
|
55
|
-
createInviteLink: (chatId) => this.createInviteLink(chatId),
|
|
56
|
-
sendPoll: (chatId, question, options, anonymous, multiple) => this.sendPoll(chatId, question, options, anonymous, multiple),
|
|
57
|
-
});
|
|
24
|
+
export class TelegramEndpoint extends TelegramClient {
|
|
25
|
+
[endpointSpec];
|
|
58
26
|
#logger;
|
|
59
27
|
#options;
|
|
60
|
-
#fetch;
|
|
61
|
-
#apiAbort = new AbortController();
|
|
62
28
|
#pollAbort;
|
|
63
29
|
#pollPromise;
|
|
64
30
|
#lifecycle;
|
|
65
31
|
#open = false;
|
|
66
32
|
#admission;
|
|
33
|
+
#executionSignal;
|
|
67
34
|
#updateOffset = 0;
|
|
68
35
|
#pollingHealthy = true;
|
|
69
36
|
#admittedUpdates = new Set();
|
|
@@ -71,27 +38,30 @@ export class TelegramEndpoint extends Endpoint {
|
|
|
71
38
|
#botUserId;
|
|
72
39
|
#botUsername;
|
|
73
40
|
#chatMemberCache = new Map();
|
|
74
|
-
control = Object.freeze({
|
|
75
|
-
recall: (message) => this.recallMessage(message),
|
|
41
|
+
#control = Object.freeze({
|
|
42
|
+
recall: ({ message, signal }) => this.recallMessage(message, signal),
|
|
76
43
|
});
|
|
77
|
-
content = Object.freeze({
|
|
44
|
+
#content = Object.freeze({
|
|
78
45
|
resolve: (reference, context) => this.#resolveContent(reference, context.signal),
|
|
79
46
|
});
|
|
80
47
|
constructor(options) {
|
|
81
|
-
super();
|
|
82
|
-
if (options.requestTimeoutMs !== undefined && (!Number.isFinite(options.requestTimeoutMs) || options.requestTimeoutMs <= 0)) {
|
|
83
|
-
throw new TypeError('Telegram requestTimeoutMs must be a positive finite number');
|
|
84
|
-
}
|
|
48
|
+
super(options);
|
|
85
49
|
this.#logger = getAdapterLogger('telegram', options.config.id);
|
|
86
50
|
this.#lifecycle = createEndpointLifecycle({ name: options.config.id, reconnect: false });
|
|
87
51
|
this.#options = options;
|
|
88
|
-
|
|
52
|
+
const endpoint = this;
|
|
53
|
+
this[endpointSpec] = createEndpointSpecification(this, {
|
|
54
|
+
start: signal => this.#startFramework(signal), open: () => this.#openFramework(), close: () => this.#closeFramework(),
|
|
55
|
+
stop: () => this.#stopFramework(), send: request => this.#sendFramework(request),
|
|
56
|
+
get name() { return options.config.id; }, get transportState() { return endpoint.#transportState; },
|
|
57
|
+
control: this.#control, content: this.#content,
|
|
58
|
+
});
|
|
89
59
|
}
|
|
90
60
|
/** Used by webhook handler. */
|
|
91
61
|
get isOpen() {
|
|
92
62
|
return this.#open;
|
|
93
63
|
}
|
|
94
|
-
get transportState() {
|
|
64
|
+
get #transportState() {
|
|
95
65
|
const state = this.#lifecycle.state;
|
|
96
66
|
return state === 'open' && this.#options.config.mode === 'polling' && !this.#pollingHealthy
|
|
97
67
|
? 'reconnecting' : state;
|
|
@@ -111,13 +81,14 @@ export class TelegramEndpoint extends Endpoint {
|
|
|
111
81
|
setUpdateOffset(offset) {
|
|
112
82
|
this.#updateOffset = offset;
|
|
113
83
|
}
|
|
114
|
-
async
|
|
115
|
-
|
|
116
|
-
|
|
84
|
+
async #startFramework(signal) {
|
|
85
|
+
this.#executionSignal = signal;
|
|
86
|
+
this.restartRequests();
|
|
117
87
|
await this.#lifecycle.start(async (handle) => {
|
|
118
88
|
const abort = new AbortController();
|
|
119
89
|
const routes = [];
|
|
120
90
|
this.#pollAbort = abort;
|
|
91
|
+
this.requestSignal = AbortSignal.any([abort.signal, signal]);
|
|
121
92
|
const cleanup = () => {
|
|
122
93
|
abort.abort();
|
|
123
94
|
for (const release of routes.splice(0))
|
|
@@ -187,20 +158,21 @@ export class TelegramEndpoint extends Endpoint {
|
|
|
187
158
|
}
|
|
188
159
|
});
|
|
189
160
|
}
|
|
190
|
-
|
|
161
|
+
#openFramework() {
|
|
191
162
|
if (this.#open)
|
|
192
163
|
return;
|
|
193
164
|
this.#admission = new AbortController();
|
|
165
|
+
this.#chatMemberCache.clear();
|
|
194
166
|
this.#open = true;
|
|
195
167
|
}
|
|
196
|
-
|
|
168
|
+
#closeFramework() {
|
|
197
169
|
this.#open = false;
|
|
198
|
-
this.#admission?.abort();
|
|
199
170
|
this.#chatMemberCache.clear();
|
|
200
171
|
}
|
|
201
|
-
async
|
|
202
|
-
this
|
|
203
|
-
this.#
|
|
172
|
+
async #stopFramework() {
|
|
173
|
+
this.#admission?.abort();
|
|
174
|
+
this.#closeFramework();
|
|
175
|
+
this.cancelRequests();
|
|
204
176
|
const polling = this.#pollPromise;
|
|
205
177
|
this.#pollPromise = undefined;
|
|
206
178
|
await this.#lifecycle.stop();
|
|
@@ -212,16 +184,18 @@ export class TelegramEndpoint extends Endpoint {
|
|
|
212
184
|
}
|
|
213
185
|
this.#logger.debug(formatCompact({ op: 'disconnect' }));
|
|
214
186
|
}
|
|
215
|
-
async
|
|
187
|
+
async #sendFramework({ conversation, payload, signal }) {
|
|
216
188
|
let lastId = '';
|
|
217
189
|
let completedActions = 0;
|
|
218
190
|
try {
|
|
219
191
|
const plan = formatOutboundPlan(conversation.id, payload);
|
|
220
192
|
for (const action of plan.actions) {
|
|
221
|
-
|
|
193
|
+
signal?.throwIfAborted();
|
|
194
|
+
const form = await this.#buildUploadForm(action.params, plan.uploads, signal);
|
|
195
|
+
signal?.throwIfAborted();
|
|
222
196
|
const result = form
|
|
223
|
-
? await this.callApiForm(action.method, form)
|
|
224
|
-
: await this.callApi(action.method, action.params);
|
|
197
|
+
? await this.callApiForm(action.method, form, signal)
|
|
198
|
+
: await this.callApi(action.method, action.params, signal);
|
|
225
199
|
if (!result || !Number.isSafeInteger(result.message_id) || result.message_id <= 0) {
|
|
226
200
|
throw new EndpointDeliveryError('delivery_unconfirmed', `Telegram API ${action.method} returned no valid message_id; delivery is unconfirmed`, 'unknown');
|
|
227
201
|
}
|
|
@@ -241,13 +215,13 @@ export class TelegramEndpoint extends Endpoint {
|
|
|
241
215
|
throw new EndpointDeliveryError('invalid_payload', 'Telegram message could not be prepared', 'not_sent', { cause: error });
|
|
242
216
|
}
|
|
243
217
|
}
|
|
244
|
-
async recallMessage(message) {
|
|
218
|
+
async recallMessage(message, signal) {
|
|
245
219
|
if (!message.id || message.id.startsWith('telegram-'))
|
|
246
220
|
return;
|
|
247
221
|
await this.callApi('deleteMessage', {
|
|
248
222
|
chat_id: message.conversation.id,
|
|
249
223
|
message_id: Number(message.id),
|
|
250
|
-
});
|
|
224
|
+
}, signal);
|
|
251
225
|
}
|
|
252
226
|
async #resolveContent(reference, signal) {
|
|
253
227
|
if (reference.kind !== 'media') {
|
|
@@ -265,29 +239,29 @@ export class TelegramEndpoint extends Endpoint {
|
|
|
265
239
|
return Object.freeze({ status: 'not_found', code: 'telegram_file_not_found' });
|
|
266
240
|
if ((file.file_size ?? 0) > 26_214_400)
|
|
267
241
|
return Object.freeze({ status: 'forbidden', code: 'media_size_limit' });
|
|
268
|
-
return await this
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
242
|
+
return await this.withRequestDeadline({ method: 'downloadFile', run: async (downloadSignal) => {
|
|
243
|
+
const response = await this.fetchTransport(`${this.#options.config.apiBaseUrl}/file/bot${this.#options.config.token}/${file.file_path}`, { signal: downloadSignal });
|
|
244
|
+
if (!response.ok)
|
|
245
|
+
return Object.freeze({ status: 'failed', code: 'telegram_file_download_failed' });
|
|
246
|
+
if (!response.arrayBuffer)
|
|
247
|
+
return Object.freeze({ status: 'failed', code: 'telegram_binary_transport_unavailable' });
|
|
248
|
+
const bytes = Buffer.from(await response.arrayBuffer());
|
|
249
|
+
if (bytes.byteLength > 26_214_400)
|
|
250
|
+
return Object.freeze({ status: 'forbidden', code: 'media_size_limit' });
|
|
251
|
+
return Object.freeze({
|
|
252
|
+
status: 'resolved',
|
|
253
|
+
reference,
|
|
254
|
+
value: Object.freeze({
|
|
255
|
+
kind: 'base64',
|
|
256
|
+
value: bytes.toString('base64'),
|
|
257
|
+
...(response.headers?.get('content-type')?.split(';')[0] || reference.media.mime_type
|
|
258
|
+
? { mime_type: response.headers?.get('content-type')?.split(';')[0] ?? reference.media.mime_type }
|
|
259
|
+
: {}),
|
|
260
|
+
...(reference.media.file_name ? { file_name: reference.media.file_name } : {}),
|
|
261
|
+
size: bytes.byteLength,
|
|
262
|
+
}),
|
|
263
|
+
});
|
|
264
|
+
}, signal });
|
|
291
265
|
}
|
|
292
266
|
catch {
|
|
293
267
|
if (signal.aborted)
|
|
@@ -300,7 +274,7 @@ export class TelegramEndpoint extends Endpoint {
|
|
|
300
274
|
* 标量参数原样、对象参数 JSON 序列化、attach 占位替换为文件 part
|
|
301
275
|
* (base64 直接解码,本地路径读盘)。无上传时返回 undefined(走 JSON 调用)。
|
|
302
276
|
*/
|
|
303
|
-
async #buildUploadForm(params, uploads) {
|
|
277
|
+
async #buildUploadForm(params, uploads, signal) {
|
|
304
278
|
const values = Object.values(params);
|
|
305
279
|
if (!values.some((v) => typeof v === 'string' && v.startsWith('attach://')))
|
|
306
280
|
return undefined;
|
|
@@ -314,7 +288,7 @@ export class TelegramEndpoint extends Endpoint {
|
|
|
314
288
|
throw new Error(`Telegram upload 未登记: ${value}`);
|
|
315
289
|
const data = upload.source.kind === 'base64'
|
|
316
290
|
? Buffer.from(upload.source.data, 'base64')
|
|
317
|
-
: await readFile(upload.source.path);
|
|
291
|
+
: await readFile(upload.source.path, { signal });
|
|
318
292
|
form.append(key, new Blob([data], upload.mimeType ? { type: upload.mimeType } : undefined), upload.filename);
|
|
319
293
|
continue;
|
|
320
294
|
}
|
|
@@ -324,23 +298,27 @@ export class TelegramEndpoint extends Endpoint {
|
|
|
324
298
|
}
|
|
325
299
|
/** Test / internal: admit a message when open. */
|
|
326
300
|
admit(msg) {
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
this.#
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
301
|
+
void this[endpointSpec].events.run(() => {
|
|
302
|
+
if (!this.#admission)
|
|
303
|
+
return;
|
|
304
|
+
const conversation = telegramInboundConversation(String(this.#options.id), msg.chat);
|
|
305
|
+
return this.#admitWithSenderRole({ message: msg, conversation, signal: AbortSignal.any([this.#admission.signal, this.#executionSignal]) }).catch((err) => {
|
|
306
|
+
this.#logger.warn(formatCompact({
|
|
307
|
+
op: 'telegram_gateway_receive_failed',
|
|
308
|
+
target: `${conversation.kind}:${conversation.id}`,
|
|
309
|
+
error: err instanceof Error ? err.message : String(err),
|
|
310
|
+
}));
|
|
311
|
+
});
|
|
312
|
+
}).catch(() => this.#logger.debug('Inbound callback was not admitted'));
|
|
337
313
|
}
|
|
338
|
-
async #admitWithSenderRole(msg, conversation, signal) {
|
|
314
|
+
async #admitWithSenderRole({ message: msg, conversation, signal, event = msg }) {
|
|
339
315
|
const permit = await this.#resolveGroupSenderPermit(msg, signal);
|
|
340
316
|
signal.throwIfAborted();
|
|
341
317
|
// 新 Runtime Message.content 为纯文本:@ 本机只能经 metadata 传递
|
|
342
318
|
const mentioned = this.#isBotMentioned(msg);
|
|
343
|
-
await this.emitAccepted('message.receive', {
|
|
319
|
+
await this[endpointSpec].events.emitAccepted('message.receive', {
|
|
320
|
+
event,
|
|
321
|
+
...(typeof msg.date === 'number' ? { platformSentAt: msg.date * 1_000 } : {}),
|
|
344
322
|
conversation,
|
|
345
323
|
message: { conversation, id: String(msg.message_id) },
|
|
346
324
|
content: formatInboundContent(msg),
|
|
@@ -427,23 +405,24 @@ export class TelegramEndpoint extends Endpoint {
|
|
|
427
405
|
}
|
|
428
406
|
/** Test / internal: admit a callback query when open. */
|
|
429
407
|
admitCallback(query) {
|
|
430
|
-
|
|
431
|
-
return
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
})
|
|
437
|
-
});
|
|
408
|
+
void this[endpointSpec].events.run(() => {
|
|
409
|
+
return this.#admitCallback(query).catch((err) => {
|
|
410
|
+
this.#logger.warn(formatCompact({
|
|
411
|
+
op: 'telegram_gateway_receive_failed',
|
|
412
|
+
error: err instanceof Error ? err.message : String(err),
|
|
413
|
+
}));
|
|
414
|
+
});
|
|
415
|
+
}).catch(() => this.#logger.debug('Inbound callback was not admitted'));
|
|
438
416
|
}
|
|
439
|
-
async #admitCallback(query) {
|
|
417
|
+
async #admitCallback(query, event = query) {
|
|
440
418
|
const endpointKey = String(this.#options.id);
|
|
441
419
|
const msg = query.message;
|
|
442
420
|
// 无挂载消息的 callback(如 inline 模式)退化为 sender 私聊会话
|
|
443
421
|
const conversation = msg
|
|
444
422
|
? telegramInboundConversation(endpointKey, msg.chat)
|
|
445
423
|
: telegramInboundConversation(endpointKey, { id: query.from.id, type: 'private' });
|
|
446
|
-
await this.emitAccepted('message.receive', {
|
|
424
|
+
await this[endpointSpec].events.emitAccepted('message.receive', {
|
|
425
|
+
event,
|
|
447
426
|
conversation,
|
|
448
427
|
message: { conversation, id: query.id },
|
|
449
428
|
content: formatCallbackContent(query),
|
|
@@ -459,36 +438,38 @@ export class TelegramEndpoint extends Endpoint {
|
|
|
459
438
|
}
|
|
460
439
|
/** Used by webhook / polling handlers. */
|
|
461
440
|
async handleUpdate(update) {
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
if (this.#admittedUpdates.has(update.update_id))
|
|
468
|
-
return;
|
|
469
|
-
const pending = this.#inflightUpdates.get(update.update_id);
|
|
470
|
-
if (pending)
|
|
471
|
-
return pending;
|
|
472
|
-
if (this.#inflightUpdates.size >= INFLIGHT_UPDATE_LIMIT) {
|
|
473
|
-
throw new Error('Telegram update admission capacity exhausted');
|
|
474
|
-
}
|
|
475
|
-
const admission = this.#admission;
|
|
476
|
-
const receiving = (async () => {
|
|
477
|
-
// Once canonical dispatch enters Core, its generation lease drains the work.
|
|
478
|
-
// Closing the transport cannot turn successful business processing into a retry.
|
|
479
|
-
await this.#receiveUpdate(update, admission.signal);
|
|
480
|
-
this.#admittedUpdates.add(update.update_id);
|
|
481
|
-
if (this.#admittedUpdates.size > ADMITTED_UPDATE_LIMIT) {
|
|
482
|
-
this.#admittedUpdates.delete(this.#admittedUpdates.values().next().value);
|
|
441
|
+
return this[endpointSpec].events.run(async () => {
|
|
442
|
+
if (!this.#admission)
|
|
443
|
+
throw new Error('Telegram endpoint is not accepting updates');
|
|
444
|
+
if (!Number.isSafeInteger(update.update_id) || update.update_id < 0) {
|
|
445
|
+
throw new TypeError('Telegram update_id must be a non-negative safe integer');
|
|
483
446
|
}
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
447
|
+
if (this.#admittedUpdates.has(update.update_id))
|
|
448
|
+
return;
|
|
449
|
+
const pending = this.#inflightUpdates.get(update.update_id);
|
|
450
|
+
if (pending)
|
|
451
|
+
return pending;
|
|
452
|
+
if (this.#inflightUpdates.size >= INFLIGHT_UPDATE_LIMIT) {
|
|
453
|
+
throw new Error('Telegram update admission capacity exhausted');
|
|
454
|
+
}
|
|
455
|
+
const admission = this.#admission;
|
|
456
|
+
const receiving = (async () => {
|
|
457
|
+
// Once canonical dispatch enters Core, its generation lease drains the work.
|
|
458
|
+
// Closing the transport cannot turn successful business processing into a retry.
|
|
459
|
+
await this.#receiveUpdate(update, AbortSignal.any([admission.signal, this.#executionSignal]));
|
|
460
|
+
this.#admittedUpdates.add(update.update_id);
|
|
461
|
+
if (this.#admittedUpdates.size > ADMITTED_UPDATE_LIMIT) {
|
|
462
|
+
this.#admittedUpdates.delete(this.#admittedUpdates.values().next().value);
|
|
463
|
+
}
|
|
464
|
+
})();
|
|
465
|
+
this.#inflightUpdates.set(update.update_id, receiving);
|
|
466
|
+
try {
|
|
467
|
+
await receiving;
|
|
468
|
+
}
|
|
469
|
+
finally {
|
|
470
|
+
this.#inflightUpdates.delete(update.update_id);
|
|
471
|
+
}
|
|
472
|
+
});
|
|
492
473
|
}
|
|
493
474
|
async #receiveUpdate(update, signal) {
|
|
494
475
|
const eventName = update.message
|
|
@@ -497,170 +478,26 @@ export class TelegramEndpoint extends Endpoint {
|
|
|
497
478
|
? 'callback_query'
|
|
498
479
|
: 'update';
|
|
499
480
|
if (update.message || update.callback_query) {
|
|
500
|
-
await this.
|
|
481
|
+
await this[endpointSpec].events.platform(eventName, update);
|
|
501
482
|
}
|
|
502
483
|
else {
|
|
503
|
-
await this.emitAccepted('platform.receive', Object.freeze({ name: eventName, event: update }));
|
|
484
|
+
await this[endpointSpec].events.emitAccepted('platform.receive', Object.freeze({ name: eventName, event: update }));
|
|
504
485
|
return;
|
|
505
486
|
}
|
|
506
487
|
signal.throwIfAborted();
|
|
507
488
|
if (update.message) {
|
|
508
489
|
const conversation = telegramInboundConversation(String(this.#options.id), update.message.chat);
|
|
509
|
-
await this.#admitWithSenderRole(update.message, conversation, signal);
|
|
490
|
+
await this.#admitWithSenderRole({ message: update.message, conversation, signal, event: update });
|
|
510
491
|
return;
|
|
511
492
|
}
|
|
512
493
|
if (update.callback_query) {
|
|
513
494
|
const query = update.callback_query;
|
|
514
495
|
if (query.data) {
|
|
515
|
-
void this.callApi('answerCallbackQuery', { callback_query_id: query.id }).catch(() => {
|
|
496
|
+
void this.callApi('answerCallbackQuery', { callback_query_id: query.id }, signal).catch(() => {
|
|
516
497
|
/* already answered */
|
|
517
498
|
});
|
|
518
499
|
}
|
|
519
|
-
await this.#admitCallback(query);
|
|
500
|
+
await this.#admitCallback(query, update);
|
|
520
501
|
}
|
|
521
502
|
}
|
|
522
|
-
async callApi(method, params = {}, signal) {
|
|
523
|
-
return this.#requestApi(method, {
|
|
524
|
-
method: 'POST',
|
|
525
|
-
headers: { 'Content-Type': 'application/json' },
|
|
526
|
-
body: JSON.stringify(params),
|
|
527
|
-
}, signal, method === 'getUpdates'
|
|
528
|
-
? Math.max(this.#options.requestTimeoutMs ?? DEFAULT_API_TIMEOUT_MS, (Number(params.timeout) || 0) * 1_000 + 15_000)
|
|
529
|
-
: undefined);
|
|
530
|
-
}
|
|
531
|
-
/** multipart/form-data 变体(attach:// 媒体上传;Content-Type 边界由 FormData 自带)。 */
|
|
532
|
-
async callApiForm(method, form, signal) {
|
|
533
|
-
return this.#requestApi(method, {
|
|
534
|
-
method: 'POST',
|
|
535
|
-
body: form,
|
|
536
|
-
}, signal);
|
|
537
|
-
}
|
|
538
|
-
async #requestApi(method, init, signal, timeoutMs = this.#options.requestTimeoutMs ?? DEFAULT_API_TIMEOUT_MS) {
|
|
539
|
-
return this.#withRequestDeadline(method, async (requestSignal) => {
|
|
540
|
-
const response = await this.#fetch(botApiUrl(this.#options.config, method), { ...init, signal: requestSignal });
|
|
541
|
-
return this.#parseApiResponse(method, response);
|
|
542
|
-
}, signal, timeoutMs);
|
|
543
|
-
}
|
|
544
|
-
async #withRequestDeadline(method, requestOperation, signal, timeoutMs = this.#options.requestTimeoutMs ?? DEFAULT_API_TIMEOUT_MS) {
|
|
545
|
-
const abort = new AbortController();
|
|
546
|
-
let issued = false;
|
|
547
|
-
const sources = [...new Set([signal, this.#pollAbort?.signal, this.#apiAbort.signal].filter((item) => !!item))];
|
|
548
|
-
let rejectAbort;
|
|
549
|
-
const cancelled = new Promise((_resolve, reject) => { rejectAbort = reject; });
|
|
550
|
-
const onCancel = () => {
|
|
551
|
-
const error = new TelegramApiError('cancelled', `Telegram API ${method} cancelled`, {
|
|
552
|
-
disposition: issued ? 'unknown' : 'not_sent',
|
|
553
|
-
});
|
|
554
|
-
abort.abort(error);
|
|
555
|
-
rejectAbort(error);
|
|
556
|
-
};
|
|
557
|
-
const timer = setTimeout(() => {
|
|
558
|
-
const error = new TelegramApiError('timeout', `Telegram API ${method} timed out`);
|
|
559
|
-
abort.abort(error);
|
|
560
|
-
rejectAbort(error);
|
|
561
|
-
}, timeoutMs);
|
|
562
|
-
for (const source of sources)
|
|
563
|
-
source.addEventListener('abort', onCancel, { once: true });
|
|
564
|
-
try {
|
|
565
|
-
if (sources.some((source) => source.aborted))
|
|
566
|
-
onCancel();
|
|
567
|
-
const request = Promise.resolve().then(async () => {
|
|
568
|
-
abort.signal.throwIfAborted();
|
|
569
|
-
issued = true;
|
|
570
|
-
return requestOperation(abort.signal);
|
|
571
|
-
});
|
|
572
|
-
return await Promise.race([request, cancelled]);
|
|
573
|
-
}
|
|
574
|
-
catch (error) {
|
|
575
|
-
if (abort.signal.aborted)
|
|
576
|
-
throw abort.signal.reason;
|
|
577
|
-
if (error instanceof TelegramApiError)
|
|
578
|
-
throw error;
|
|
579
|
-
// Network exceptions can contain credential-bearing URLs. Keep those out of errors/logs.
|
|
580
|
-
const detail = error instanceof Error
|
|
581
|
-
? error.message.replaceAll(this.#options.config.token, '[redacted]') : 'transport failure';
|
|
582
|
-
throw new TelegramApiError('network', `Telegram API ${method} network request failed: ${detail}`);
|
|
583
|
-
}
|
|
584
|
-
finally {
|
|
585
|
-
clearTimeout(timer);
|
|
586
|
-
for (const source of sources)
|
|
587
|
-
source.removeEventListener('abort', onCancel);
|
|
588
|
-
}
|
|
589
|
-
}
|
|
590
|
-
async #parseApiResponse(method, response) {
|
|
591
|
-
const text = await response.text();
|
|
592
|
-
let body;
|
|
593
|
-
try {
|
|
594
|
-
body = JSON.parse(text);
|
|
595
|
-
}
|
|
596
|
-
catch {
|
|
597
|
-
throw new TelegramApiError('invalid_response', `Telegram API ${method} invalid JSON (${response.status})`, { status: response.status });
|
|
598
|
-
}
|
|
599
|
-
if (!body || typeof body !== 'object' || typeof body.ok !== 'boolean') {
|
|
600
|
-
throw new TelegramApiError('invalid_response', `Telegram API ${method} invalid response envelope`, { status: response.status });
|
|
601
|
-
}
|
|
602
|
-
if (!body.ok || response.status < 200 || response.status >= 300) {
|
|
603
|
-
const status = !body.ok ? body.error_code ?? response.status : response.status;
|
|
604
|
-
const kind = status === 401 || status === 403 ? 'authentication'
|
|
605
|
-
: status === 429 ? 'rate_limit' : response.status >= 400 ? 'http' : 'api';
|
|
606
|
-
const description = !body.ok ? body.description?.replaceAll(this.#options.config.token, '[redacted]') : undefined;
|
|
607
|
-
throw new TelegramApiError(kind, `Telegram API ${method} failed (${status}): ${description ?? 'request rejected'}`, { status, disposition: !body.ok ? 'rejected' : 'unknown', ...(!body.ok && body.parameters?.retry_after != null
|
|
608
|
-
? { retryAfterSeconds: body.parameters.retry_after } : {}) });
|
|
609
|
-
}
|
|
610
|
-
if (!('result' in body))
|
|
611
|
-
throw new TelegramApiError('invalid_response', `Telegram API ${method} missing result`);
|
|
612
|
-
return body.result;
|
|
613
|
-
}
|
|
614
|
-
// ── Agent tool surface ──────────────────────────────────────────────
|
|
615
|
-
async pinMessage(chatId, messageId) {
|
|
616
|
-
await this.callApi('pinChatMessage', { chat_id: chatId, message_id: messageId });
|
|
617
|
-
return true;
|
|
618
|
-
}
|
|
619
|
-
async unpinMessage(chatId, messageId) {
|
|
620
|
-
if (messageId != null) {
|
|
621
|
-
await this.callApi('unpinChatMessage', { chat_id: chatId, message_id: messageId });
|
|
622
|
-
}
|
|
623
|
-
else {
|
|
624
|
-
await this.callApi('unpinAllChatMessages', { chat_id: chatId });
|
|
625
|
-
}
|
|
626
|
-
return true;
|
|
627
|
-
}
|
|
628
|
-
async setChatDescription(chatId, description) {
|
|
629
|
-
await this.callApi('setChatDescription', { chat_id: chatId, description });
|
|
630
|
-
return true;
|
|
631
|
-
}
|
|
632
|
-
async setMessageReaction(chatId, messageId, reaction) {
|
|
633
|
-
await this.callApi('setMessageReaction', {
|
|
634
|
-
chat_id: chatId,
|
|
635
|
-
message_id: messageId,
|
|
636
|
-
reaction: [{ type: 'emoji', emoji: reaction }],
|
|
637
|
-
});
|
|
638
|
-
return true;
|
|
639
|
-
}
|
|
640
|
-
async getChatMemberCount(chatId) {
|
|
641
|
-
return this.callApi('getChatMemberCount', { chat_id: chatId });
|
|
642
|
-
}
|
|
643
|
-
async getChatAdmins(chatId) {
|
|
644
|
-
return this.callApi('getChatAdministrators', { chat_id: chatId });
|
|
645
|
-
}
|
|
646
|
-
async sendStickerMessage(chatId, sticker) {
|
|
647
|
-
return this.callApi('sendSticker', { chat_id: chatId, sticker });
|
|
648
|
-
}
|
|
649
|
-
async setChatPermissionsAll(chatId, permissions) {
|
|
650
|
-
await this.callApi('setChatPermissions', { chat_id: chatId, permissions });
|
|
651
|
-
return true;
|
|
652
|
-
}
|
|
653
|
-
async createInviteLink(chatId) {
|
|
654
|
-
const link = await this.callApi('createChatInviteLink', { chat_id: chatId });
|
|
655
|
-
return link.invite_link;
|
|
656
|
-
}
|
|
657
|
-
async sendPoll(chatId, question, options, isAnonymous = true, allowsMultipleAnswers = false) {
|
|
658
|
-
return this.callApi('sendPoll', {
|
|
659
|
-
chat_id: chatId,
|
|
660
|
-
question,
|
|
661
|
-
options,
|
|
662
|
-
is_anonymous: isAnonymous,
|
|
663
|
-
allows_multiple_answers: allowsMultipleAnswers,
|
|
664
|
-
});
|
|
665
|
-
}
|
|
666
503
|
}
|
package/lib/index.d.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
export { TelegramClient, TelegramEndpoint, type TelegramClientApi, type TelegramEndpointOptions, type TelegramFetch, } from './endpoint.js';
|
|
2
2
|
export { botApiUrl, formatCallbackContent, formatInboundContent, formatOutboundActions, formatOutboundPlan, normalizeWebhookPath, resolveTelegramConfig, senderDisplayName, type ResolvedTelegramConfig, type TelegramEndpointConfig, type TelegramCallbackQuery, type TelegramChat, type TelegramChatMember, type TelegramMessage, type TelegramOutboundAction, type TelegramOutboundPlan, type TelegramOutboundUpload, type TelegramUpdate, type TelegramUser, type TelegramWireSegment, } from './protocol.js';
|
|
3
|
-
export {
|
|
3
|
+
export { type TelegramClientEventMap } from './client.js';
|
|
4
|
+
export { TelegramAdapter } from './adapter.js';
|
|
4
5
|
export { checkTelegramPlatformPermit, normalizeTelegramChatMember, platformPermit, telegramGroupPermitResolver, } from './platform-permit.js';
|
|
6
|
+
export type { TelegramPollRequest } from './telegram-client.js';
|
package/lib/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
export { TelegramClient, TelegramEndpoint, } from './endpoint.js';
|
|
2
2
|
export { botApiUrl, formatCallbackContent, formatInboundContent, formatOutboundActions, formatOutboundPlan, normalizeWebhookPath, resolveTelegramConfig, senderDisplayName, } from './protocol.js';
|
|
3
|
-
export {
|
|
3
|
+
export { TelegramAdapter } from './adapter.js';
|
|
4
4
|
export { checkTelegramPlatformPermit, normalizeTelegramChatMember, platformPermit, telegramGroupPermitResolver, } from './platform-permit.js';
|