@zhin.js/adapter-telegram 1.1.18 → 1.1.20
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 +45 -0
- package/README.md +10 -3
- 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.js +5 -0
- package/lib/console.d.ts +1 -7
- package/lib/console.js +2 -5
- 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 +19 -19
- package/plugin.js +1 -4
- package/src/adapter.ts +27 -0
- package/src/client.ts +2 -10
- package/src/console-spec.ts +5 -0
- package/src/console.ts +2 -6
- 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/src/endpoint.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import { TelegramClient, type TelegramFetch } from './telegram-client.js';
|
|
1
2
|
import {
|
|
2
|
-
Endpoint, createEndpointLifecycle,
|
|
3
|
+
createEndpointSpecification, endpointSpec, type Endpoint, type EndpointSpecification, createEndpointLifecycle,
|
|
3
4
|
type EndpointLifecycle, type EndpointContentPort, type EndpointContentResolveContext,
|
|
4
5
|
type EndpointControl, type EndpointSendRequest, type EndpointTransportState,
|
|
5
6
|
} from 'zhin.js/adapter';
|
|
@@ -22,7 +23,6 @@ import { TelegramApiError } from './api-error.js';
|
|
|
22
23
|
export { TelegramApiError } from './api-error.js';
|
|
23
24
|
import { normalizeTelegramChatMember } from './platform-permit.js';
|
|
24
25
|
import {
|
|
25
|
-
botApiUrl,
|
|
26
26
|
buildWebhookUrl,
|
|
27
27
|
formatCallbackContent,
|
|
28
28
|
formatCallbackSegments,
|
|
@@ -45,7 +45,6 @@ const CHAT_MEMBER_CACHE_TTL_MS = 60_000;
|
|
|
45
45
|
const CHAT_MEMBER_CACHE_MAX = 2_000;
|
|
46
46
|
const ADMITTED_UPDATE_LIMIT = 2_048;
|
|
47
47
|
const INFLIGHT_UPDATE_LIMIT = 256;
|
|
48
|
-
const DEFAULT_API_TIMEOUT_MS = 30_000;
|
|
49
48
|
|
|
50
49
|
interface ChatMemberPermit {
|
|
51
50
|
readonly at: number;
|
|
@@ -53,22 +52,13 @@ interface ChatMemberPermit {
|
|
|
53
52
|
readonly permissions: string[];
|
|
54
53
|
}
|
|
55
54
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
},
|
|
64
|
-
) => Promise<{
|
|
65
|
-
readonly ok: boolean;
|
|
66
|
-
readonly status: number;
|
|
67
|
-
text(): Promise<string>;
|
|
68
|
-
json(): Promise<unknown>;
|
|
69
|
-
arrayBuffer?(): Promise<ArrayBuffer>;
|
|
70
|
-
readonly headers?: { get(name: string): string | null };
|
|
71
|
-
}>;
|
|
55
|
+
/** One admitted platform message and its original framework lifetime. */
|
|
56
|
+
interface TelegramMessageAdmission {
|
|
57
|
+
readonly message: TelegramMessage;
|
|
58
|
+
readonly conversation: ConversationRef;
|
|
59
|
+
readonly signal: AbortSignal;
|
|
60
|
+
readonly event?: unknown;
|
|
61
|
+
}
|
|
72
62
|
|
|
73
63
|
export interface TelegramEndpointOptions {
|
|
74
64
|
readonly id: CapabilityId;
|
|
@@ -79,101 +69,24 @@ export interface TelegramEndpointOptions {
|
|
|
79
69
|
readonly requestTimeoutMs?: number;
|
|
80
70
|
}
|
|
81
71
|
|
|
82
|
-
export
|
|
83
|
-
callApi<T = unknown>(method: string, params?: Record<string, unknown>, signal?: AbortSignal): Promise<T>;
|
|
84
|
-
callApiForm<T = unknown>(method: string, form: FormData, signal?: AbortSignal): Promise<T>;
|
|
85
|
-
pinMessage(chatId: number, messageId: number): Promise<boolean>;
|
|
86
|
-
unpinMessage(chatId: number, messageId?: number): Promise<boolean>;
|
|
87
|
-
setChatDescription(chatId: number, description: string): Promise<boolean>;
|
|
88
|
-
setMessageReaction(chatId: number, messageId: number, reaction: string): Promise<boolean>;
|
|
89
|
-
getChatMemberCount(chatId: number): Promise<number>;
|
|
90
|
-
getChatAdmins(chatId: number): Promise<TelegramChatMember[]>;
|
|
91
|
-
sendStickerMessage(chatId: number, sticker: string): Promise<{ message_id: number }>;
|
|
92
|
-
setChatPermissionsAll(chatId: number, permissions: Record<string, boolean | undefined>): Promise<boolean>;
|
|
93
|
-
createInviteLink(chatId: number): Promise<string>;
|
|
94
|
-
sendPoll(
|
|
95
|
-
chatId: number,
|
|
96
|
-
question: string,
|
|
97
|
-
options: string[],
|
|
98
|
-
isAnonymous?: boolean,
|
|
99
|
-
allowsMultipleAnswers?: boolean,
|
|
100
|
-
): Promise<{ message_id: number }>;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
/** Telegram Bot API client carried by message, update and lifecycle events. */
|
|
104
|
-
export class TelegramClient implements TelegramClientApi {
|
|
105
|
-
constructor(readonly api: TelegramClientApi) {}
|
|
106
|
-
callApi = <T = unknown>(method: string, params: Record<string, unknown> = {}, signal?: AbortSignal) =>
|
|
107
|
-
this.api.callApi<T>(method, params, signal);
|
|
108
|
-
callApiForm = <T = unknown>(method: string, form: FormData, signal?: AbortSignal) =>
|
|
109
|
-
this.api.callApiForm<T>(method, form, signal);
|
|
110
|
-
pinMessage = (chatId: number, messageId: number) => this.api.pinMessage(chatId, messageId);
|
|
111
|
-
unpinMessage = (chatId: number, messageId?: number) => this.api.unpinMessage(chatId, messageId);
|
|
112
|
-
setChatDescription = (chatId: number, description: string) =>
|
|
113
|
-
this.api.setChatDescription(chatId, description);
|
|
114
|
-
setMessageReaction = (chatId: number, messageId: number, reaction: string) =>
|
|
115
|
-
this.api.setMessageReaction(chatId, messageId, reaction);
|
|
116
|
-
getChatMemberCount = (chatId: number) => this.api.getChatMemberCount(chatId);
|
|
117
|
-
getChatAdmins = (chatId: number) => this.api.getChatAdmins(chatId);
|
|
118
|
-
sendStickerMessage = (chatId: number, sticker: string) =>
|
|
119
|
-
this.api.sendStickerMessage(chatId, sticker);
|
|
120
|
-
setChatPermissionsAll = (chatId: number, permissions: Record<string, boolean | undefined>) =>
|
|
121
|
-
this.api.setChatPermissionsAll(chatId, permissions);
|
|
122
|
-
createInviteLink = (chatId: number) => this.api.createInviteLink(chatId);
|
|
123
|
-
sendPoll = (
|
|
124
|
-
chatId: number,
|
|
125
|
-
question: string,
|
|
126
|
-
options: string[],
|
|
127
|
-
anonymous?: boolean,
|
|
128
|
-
multiple?: boolean,
|
|
129
|
-
) => this.api.sendPoll(chatId, question, options, anonymous, multiple);
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
interface TelegramApiOk<T> {
|
|
133
|
-
readonly ok: true;
|
|
134
|
-
readonly result: T;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
interface TelegramApiErr {
|
|
138
|
-
readonly ok: false;
|
|
139
|
-
readonly description?: string;
|
|
140
|
-
readonly error_code?: number;
|
|
141
|
-
readonly parameters?: { readonly retry_after?: number };
|
|
142
|
-
}
|
|
72
|
+
export { TelegramClient, type TelegramClientApi, type TelegramFetch } from './telegram-client.js';
|
|
143
73
|
|
|
144
74
|
/**
|
|
145
75
|
* Telegram Bot API 无列表类接口(无 getMyChats/getChatMembers),
|
|
146
76
|
* 仅 getChat/getChatMember 按已知 id 单查,不构成列表能力;
|
|
147
77
|
* 因此本 endpoint 不暴露 EndpointManagement(Console 社交面 RPC 对该平台保持未接线)。
|
|
148
78
|
*/
|
|
149
|
-
export class TelegramEndpoint extends
|
|
150
|
-
readonly
|
|
151
|
-
callApi: (method, params, signal) => this.callApi(method, params, signal),
|
|
152
|
-
callApiForm: (method, form, signal) => this.callApiForm(method, form, signal),
|
|
153
|
-
pinMessage: (chatId, messageId) => this.pinMessage(chatId, messageId),
|
|
154
|
-
unpinMessage: (chatId, messageId) => this.unpinMessage(chatId, messageId),
|
|
155
|
-
setChatDescription: (chatId, description) => this.setChatDescription(chatId, description),
|
|
156
|
-
setMessageReaction: (chatId, messageId, reaction) =>
|
|
157
|
-
this.setMessageReaction(chatId, messageId, reaction),
|
|
158
|
-
getChatMemberCount: (chatId) => this.getChatMemberCount(chatId),
|
|
159
|
-
getChatAdmins: (chatId) => this.getChatAdmins(chatId),
|
|
160
|
-
sendStickerMessage: (chatId, sticker) => this.sendStickerMessage(chatId, sticker),
|
|
161
|
-
setChatPermissionsAll: (chatId, permissions) =>
|
|
162
|
-
this.setChatPermissionsAll(chatId, permissions),
|
|
163
|
-
createInviteLink: (chatId) => this.createInviteLink(chatId),
|
|
164
|
-
sendPoll: (chatId, question, options, anonymous, multiple) =>
|
|
165
|
-
this.sendPoll(chatId, question, options, anonymous, multiple),
|
|
166
|
-
});
|
|
79
|
+
export class TelegramEndpoint extends TelegramClient implements Endpoint {
|
|
80
|
+
readonly [endpointSpec]: EndpointSpecification;
|
|
167
81
|
readonly #logger!: ReturnType<typeof getAdapterLogger>;
|
|
168
82
|
|
|
169
83
|
readonly #options: TelegramEndpointOptions;
|
|
170
|
-
readonly #fetch: TelegramFetch;
|
|
171
|
-
#apiAbort = new AbortController();
|
|
172
84
|
#pollAbort?: AbortController;
|
|
173
85
|
#pollPromise?: Promise<void>;
|
|
174
86
|
readonly #lifecycle: EndpointLifecycle;
|
|
175
87
|
#open = false;
|
|
176
88
|
#admission?: AbortController;
|
|
89
|
+
#executionSignal?: AbortSignal;
|
|
177
90
|
#updateOffset = 0;
|
|
178
91
|
#pollingHealthy = true;
|
|
179
92
|
readonly #admittedUpdates = new Set<number>();
|
|
@@ -181,22 +94,25 @@ export class TelegramEndpoint extends Endpoint<TelegramClient> {
|
|
|
181
94
|
#botUserId?: number;
|
|
182
95
|
#botUsername?: string;
|
|
183
96
|
readonly #chatMemberCache = new Map<string, ChatMemberPermit>();
|
|
184
|
-
readonly control: EndpointControl = Object.freeze({
|
|
185
|
-
recall: (message
|
|
97
|
+
readonly #control: EndpointControl = Object.freeze<EndpointControl>({
|
|
98
|
+
recall: ({ message, signal }) => this.recallMessage(message, signal),
|
|
186
99
|
});
|
|
187
|
-
readonly content: EndpointContentPort = Object.freeze({
|
|
100
|
+
readonly #content: EndpointContentPort = Object.freeze({
|
|
188
101
|
resolve: (reference: ConversationReference, context: EndpointContentResolveContext) => this.#resolveContent(reference, context.signal),
|
|
189
102
|
});
|
|
190
103
|
|
|
191
104
|
constructor(options: TelegramEndpointOptions) {
|
|
192
|
-
super();
|
|
193
|
-
if (options.requestTimeoutMs !== undefined && (!Number.isFinite(options.requestTimeoutMs) || options.requestTimeoutMs <= 0)) {
|
|
194
|
-
throw new TypeError('Telegram requestTimeoutMs must be a positive finite number');
|
|
195
|
-
}
|
|
105
|
+
super(options);
|
|
196
106
|
this.#logger = getAdapterLogger('telegram', options.config.id);
|
|
197
107
|
this.#lifecycle = createEndpointLifecycle({ name: options.config.id, reconnect: false });
|
|
198
108
|
this.#options = options;
|
|
199
|
-
|
|
109
|
+
const endpoint = this;
|
|
110
|
+
this[endpointSpec] = createEndpointSpecification(this, {
|
|
111
|
+
start: signal => this.#startFramework(signal), open: () => this.#openFramework(), close: () => this.#closeFramework(),
|
|
112
|
+
stop: () => this.#stopFramework(), send: request => this.#sendFramework(request),
|
|
113
|
+
get name() { return options.config.id; }, get transportState() { return endpoint.#transportState; },
|
|
114
|
+
control: this.#control, content: this.#content,
|
|
115
|
+
});
|
|
200
116
|
}
|
|
201
117
|
|
|
202
118
|
/** Used by webhook handler. */
|
|
@@ -204,7 +120,7 @@ export class TelegramEndpoint extends Endpoint<TelegramClient> {
|
|
|
204
120
|
return this.#open;
|
|
205
121
|
}
|
|
206
122
|
|
|
207
|
-
|
|
123
|
+
get #transportState(): EndpointTransportState {
|
|
208
124
|
const state = this.#lifecycle.state;
|
|
209
125
|
return state === 'open' && this.#options.config.mode === 'polling' && !this.#pollingHealthy
|
|
210
126
|
? 'reconnecting' : state;
|
|
@@ -230,12 +146,14 @@ export class TelegramEndpoint extends Endpoint<TelegramClient> {
|
|
|
230
146
|
this.#updateOffset = offset;
|
|
231
147
|
}
|
|
232
148
|
|
|
233
|
-
async
|
|
234
|
-
|
|
149
|
+
async #startFramework(signal: AbortSignal): Promise<void> {
|
|
150
|
+
this.#executionSignal = signal;
|
|
151
|
+
this.restartRequests();
|
|
235
152
|
await this.#lifecycle.start(async (handle) => {
|
|
236
153
|
const abort = new AbortController();
|
|
237
154
|
const routes: HttpRouteRegistration[] = [];
|
|
238
155
|
this.#pollAbort = abort;
|
|
156
|
+
this.requestSignal = AbortSignal.any([abort.signal, signal]);
|
|
239
157
|
const cleanup = () => {
|
|
240
158
|
abort.abort();
|
|
241
159
|
for (const release of routes.splice(0)) release();
|
|
@@ -302,21 +220,22 @@ export class TelegramEndpoint extends Endpoint<TelegramClient> {
|
|
|
302
220
|
});
|
|
303
221
|
}
|
|
304
222
|
|
|
305
|
-
|
|
223
|
+
#openFramework(): void {
|
|
306
224
|
if (this.#open) return;
|
|
307
225
|
this.#admission = new AbortController();
|
|
226
|
+
this.#chatMemberCache.clear();
|
|
308
227
|
this.#open = true;
|
|
309
228
|
}
|
|
310
229
|
|
|
311
|
-
|
|
230
|
+
#closeFramework(): void {
|
|
312
231
|
this.#open = false;
|
|
313
|
-
this.#admission?.abort();
|
|
314
232
|
this.#chatMemberCache.clear();
|
|
315
233
|
}
|
|
316
234
|
|
|
317
|
-
async
|
|
318
|
-
this
|
|
319
|
-
this.#
|
|
235
|
+
async #stopFramework(): Promise<void> {
|
|
236
|
+
this.#admission?.abort();
|
|
237
|
+
this.#closeFramework();
|
|
238
|
+
this.cancelRequests();
|
|
320
239
|
const polling = this.#pollPromise;
|
|
321
240
|
this.#pollPromise = undefined;
|
|
322
241
|
await this.#lifecycle.stop();
|
|
@@ -328,16 +247,18 @@ export class TelegramEndpoint extends Endpoint<TelegramClient> {
|
|
|
328
247
|
this.#logger.debug(formatCompact({ op: 'disconnect' }));
|
|
329
248
|
}
|
|
330
249
|
|
|
331
|
-
async
|
|
250
|
+
async #sendFramework({ conversation, payload, signal }: EndpointSendRequest): Promise<string> {
|
|
332
251
|
let lastId = '';
|
|
333
252
|
let completedActions = 0;
|
|
334
253
|
try {
|
|
335
254
|
const plan = formatOutboundPlan(conversation.id, payload);
|
|
336
255
|
for (const action of plan.actions) {
|
|
337
|
-
|
|
256
|
+
signal?.throwIfAborted();
|
|
257
|
+
const form = await this.#buildUploadForm(action.params, plan.uploads, signal);
|
|
258
|
+
signal?.throwIfAborted();
|
|
338
259
|
const result = form
|
|
339
|
-
? await this.callApiForm<{ message_id?: number }>(action.method, form)
|
|
340
|
-
: await this.callApi<{ message_id?: number }>(action.method, action.params);
|
|
260
|
+
? await this.callApiForm<{ message_id?: number }>(action.method, form, signal)
|
|
261
|
+
: await this.callApi<{ message_id?: number }>(action.method, action.params, signal);
|
|
341
262
|
if (!result || !Number.isSafeInteger(result.message_id) || result.message_id! <= 0) {
|
|
342
263
|
throw new EndpointDeliveryError('delivery_unconfirmed',
|
|
343
264
|
`Telegram API ${action.method} returned no valid message_id; delivery is unconfirmed`, 'unknown');
|
|
@@ -358,12 +279,12 @@ export class TelegramEndpoint extends Endpoint<TelegramClient> {
|
|
|
358
279
|
}
|
|
359
280
|
}
|
|
360
281
|
|
|
361
|
-
async recallMessage(message: MessageRef): Promise<void> {
|
|
282
|
+
async recallMessage(message: MessageRef, signal?: AbortSignal): Promise<void> {
|
|
362
283
|
if (!message.id || message.id.startsWith('telegram-')) return;
|
|
363
284
|
await this.callApi('deleteMessage', {
|
|
364
285
|
chat_id: message.conversation.id,
|
|
365
286
|
message_id: Number(message.id),
|
|
366
|
-
});
|
|
287
|
+
}, signal);
|
|
367
288
|
}
|
|
368
289
|
|
|
369
290
|
async #resolveContent(
|
|
@@ -383,8 +304,8 @@ export class TelegramEndpoint extends Endpoint<TelegramClient> {
|
|
|
383
304
|
}, signal);
|
|
384
305
|
if (!file.file_path) return Object.freeze({ status: 'not_found', code: 'telegram_file_not_found' });
|
|
385
306
|
if ((file.file_size ?? 0) > 26_214_400) return Object.freeze({ status: 'forbidden', code: 'media_size_limit' });
|
|
386
|
-
return await this
|
|
387
|
-
const response = await this
|
|
307
|
+
return await this.withRequestDeadline<ConversationResolution>({ method: 'downloadFile', run: async (downloadSignal) => {
|
|
308
|
+
const response = await this.fetchTransport(
|
|
388
309
|
`${this.#options.config.apiBaseUrl}/file/bot${this.#options.config.token}/${file.file_path}`,
|
|
389
310
|
{ signal: downloadSignal },
|
|
390
311
|
);
|
|
@@ -405,7 +326,7 @@ export class TelegramEndpoint extends Endpoint<TelegramClient> {
|
|
|
405
326
|
size: bytes.byteLength,
|
|
406
327
|
}),
|
|
407
328
|
});
|
|
408
|
-
}, signal);
|
|
329
|
+
}, signal });
|
|
409
330
|
} catch {
|
|
410
331
|
if (signal.aborted) return Object.freeze({ status: 'expired', code: 'turn_aborted' });
|
|
411
332
|
return Object.freeze({ status: 'failed', code: 'telegram_file_resolution_failed', message: 'Telegram file resolution failed' });
|
|
@@ -420,6 +341,7 @@ export class TelegramEndpoint extends Endpoint<TelegramClient> {
|
|
|
420
341
|
async #buildUploadForm(
|
|
421
342
|
params: Record<string, unknown>,
|
|
422
343
|
uploads: readonly TelegramOutboundUpload[],
|
|
344
|
+
signal?: AbortSignal,
|
|
423
345
|
): Promise<FormData | undefined> {
|
|
424
346
|
const values = Object.values(params);
|
|
425
347
|
if (!values.some((v) => typeof v === 'string' && v.startsWith('attach://'))) return undefined;
|
|
@@ -431,7 +353,7 @@ export class TelegramEndpoint extends Endpoint<TelegramClient> {
|
|
|
431
353
|
if (!upload) throw new Error(`Telegram upload 未登记: ${value}`);
|
|
432
354
|
const data = upload.source.kind === 'base64'
|
|
433
355
|
? Buffer.from(upload.source.data, 'base64')
|
|
434
|
-
: await readFile(upload.source.path);
|
|
356
|
+
: await readFile(upload.source.path, { signal });
|
|
435
357
|
form.append(
|
|
436
358
|
key,
|
|
437
359
|
new Blob([data], upload.mimeType ? { type: upload.mimeType } : undefined),
|
|
@@ -446,23 +368,27 @@ export class TelegramEndpoint extends Endpoint<TelegramClient> {
|
|
|
446
368
|
|
|
447
369
|
/** Test / internal: admit a message when open. */
|
|
448
370
|
admit(msg: TelegramMessage): void {
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
this.#
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
371
|
+
void this[endpointSpec].events.run(() => {
|
|
372
|
+
if (!this.#admission) return;
|
|
373
|
+
const conversation = telegramInboundConversation(String(this.#options.id), msg.chat);
|
|
374
|
+
return this.#admitWithSenderRole({ message: msg, conversation, signal: AbortSignal.any([this.#admission.signal, this.#executionSignal!]) }).catch((err) => {
|
|
375
|
+
this.#logger.warn(formatCompact({
|
|
376
|
+
op: 'telegram_gateway_receive_failed',
|
|
377
|
+
target: `${conversation.kind}:${conversation.id}`,
|
|
378
|
+
error: err instanceof Error ? err.message : String(err),
|
|
379
|
+
}));
|
|
380
|
+
});
|
|
381
|
+
}).catch(() => this.#logger.debug('Inbound callback was not admitted'));
|
|
458
382
|
}
|
|
459
383
|
|
|
460
|
-
async #admitWithSenderRole(
|
|
384
|
+
async #admitWithSenderRole({ message: msg, conversation, signal, event = msg }: TelegramMessageAdmission): Promise<void> {
|
|
461
385
|
const permit = await this.#resolveGroupSenderPermit(msg, signal);
|
|
462
386
|
signal.throwIfAborted();
|
|
463
387
|
// 新 Runtime Message.content 为纯文本:@ 本机只能经 metadata 传递
|
|
464
388
|
const mentioned = this.#isBotMentioned(msg);
|
|
465
|
-
await this.emitAccepted('message.receive', {
|
|
389
|
+
await this[endpointSpec].events.emitAccepted('message.receive', {
|
|
390
|
+
event,
|
|
391
|
+
...(typeof msg.date === 'number' ? { platformSentAt: msg.date * 1_000 } : {}),
|
|
466
392
|
conversation,
|
|
467
393
|
message: { conversation, id: String(msg.message_id) },
|
|
468
394
|
content: formatInboundContent(msg),
|
|
@@ -543,23 +469,25 @@ export class TelegramEndpoint extends Endpoint<TelegramClient> {
|
|
|
543
469
|
|
|
544
470
|
/** Test / internal: admit a callback query when open. */
|
|
545
471
|
admitCallback(query: TelegramCallbackQuery): void {
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
472
|
+
void this[endpointSpec].events.run(() => {
|
|
473
|
+
return this.#admitCallback(query).catch((err) => {
|
|
474
|
+
this.#logger.warn(formatCompact({
|
|
475
|
+
op: 'telegram_gateway_receive_failed',
|
|
476
|
+
error: err instanceof Error ? err.message : String(err),
|
|
477
|
+
}));
|
|
478
|
+
});
|
|
479
|
+
}).catch(() => this.#logger.debug('Inbound callback was not admitted'));
|
|
553
480
|
}
|
|
554
481
|
|
|
555
|
-
async #admitCallback(query: TelegramCallbackQuery): Promise<void> {
|
|
482
|
+
async #admitCallback(query: TelegramCallbackQuery, event: unknown = query): Promise<void> {
|
|
556
483
|
const endpointKey = String(this.#options.id);
|
|
557
484
|
const msg = query.message;
|
|
558
485
|
// 无挂载消息的 callback(如 inline 模式)退化为 sender 私聊会话
|
|
559
486
|
const conversation = msg
|
|
560
487
|
? telegramInboundConversation(endpointKey, msg.chat)
|
|
561
488
|
: telegramInboundConversation(endpointKey, { id: query.from.id, type: 'private' });
|
|
562
|
-
await this.emitAccepted('message.receive', {
|
|
489
|
+
await this[endpointSpec].events.emitAccepted('message.receive', {
|
|
490
|
+
event,
|
|
563
491
|
conversation,
|
|
564
492
|
message: { conversation, id: query.id },
|
|
565
493
|
content: formatCallbackContent(query),
|
|
@@ -576,32 +504,34 @@ export class TelegramEndpoint extends Endpoint<TelegramClient> {
|
|
|
576
504
|
|
|
577
505
|
/** Used by webhook / polling handlers. */
|
|
578
506
|
async handleUpdate(update: TelegramUpdate): Promise<void> {
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
if (this.#admittedUpdates.has(update.update_id)) return;
|
|
584
|
-
const pending = this.#inflightUpdates.get(update.update_id);
|
|
585
|
-
if (pending) return pending;
|
|
586
|
-
if (this.#inflightUpdates.size >= INFLIGHT_UPDATE_LIMIT) {
|
|
587
|
-
throw new Error('Telegram update admission capacity exhausted');
|
|
588
|
-
}
|
|
589
|
-
const admission = this.#admission;
|
|
590
|
-
const receiving = (async () => {
|
|
591
|
-
// Once canonical dispatch enters Core, its generation lease drains the work.
|
|
592
|
-
// Closing the transport cannot turn successful business processing into a retry.
|
|
593
|
-
await this.#receiveUpdate(update, admission.signal);
|
|
594
|
-
this.#admittedUpdates.add(update.update_id);
|
|
595
|
-
if (this.#admittedUpdates.size > ADMITTED_UPDATE_LIMIT) {
|
|
596
|
-
this.#admittedUpdates.delete(this.#admittedUpdates.values().next().value!);
|
|
507
|
+
return this[endpointSpec].events.run(async () => {
|
|
508
|
+
if (!this.#admission) throw new Error('Telegram endpoint is not accepting updates');
|
|
509
|
+
if (!Number.isSafeInteger(update.update_id) || update.update_id < 0) {
|
|
510
|
+
throw new TypeError('Telegram update_id must be a non-negative safe integer');
|
|
597
511
|
}
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
512
|
+
if (this.#admittedUpdates.has(update.update_id)) return;
|
|
513
|
+
const pending = this.#inflightUpdates.get(update.update_id);
|
|
514
|
+
if (pending) return pending;
|
|
515
|
+
if (this.#inflightUpdates.size >= INFLIGHT_UPDATE_LIMIT) {
|
|
516
|
+
throw new Error('Telegram update admission capacity exhausted');
|
|
517
|
+
}
|
|
518
|
+
const admission = this.#admission;
|
|
519
|
+
const receiving = (async () => {
|
|
520
|
+
// Once canonical dispatch enters Core, its generation lease drains the work.
|
|
521
|
+
// Closing the transport cannot turn successful business processing into a retry.
|
|
522
|
+
await this.#receiveUpdate(update, AbortSignal.any([admission.signal, this.#executionSignal!]));
|
|
523
|
+
this.#admittedUpdates.add(update.update_id);
|
|
524
|
+
if (this.#admittedUpdates.size > ADMITTED_UPDATE_LIMIT) {
|
|
525
|
+
this.#admittedUpdates.delete(this.#admittedUpdates.values().next().value!);
|
|
526
|
+
}
|
|
527
|
+
})();
|
|
528
|
+
this.#inflightUpdates.set(update.update_id, receiving);
|
|
529
|
+
try {
|
|
530
|
+
await receiving;
|
|
531
|
+
} finally {
|
|
532
|
+
this.#inflightUpdates.delete(update.update_id);
|
|
533
|
+
}
|
|
534
|
+
});
|
|
605
535
|
}
|
|
606
536
|
|
|
607
537
|
async #receiveUpdate(update: TelegramUpdate, signal: AbortSignal): Promise<void> {
|
|
@@ -611,208 +541,26 @@ export class TelegramEndpoint extends Endpoint<TelegramClient> {
|
|
|
611
541
|
? 'callback_query'
|
|
612
542
|
: 'update';
|
|
613
543
|
if (update.message || update.callback_query) {
|
|
614
|
-
await this.
|
|
544
|
+
await this[endpointSpec].events.platform(eventName, update);
|
|
615
545
|
} else {
|
|
616
|
-
await this.emitAccepted('platform.receive', Object.freeze({ name: eventName, event: update }));
|
|
546
|
+
await this[endpointSpec].events.emitAccepted('platform.receive', Object.freeze({ name: eventName, event: update }));
|
|
617
547
|
return;
|
|
618
548
|
}
|
|
619
549
|
signal.throwIfAborted();
|
|
620
550
|
if (update.message) {
|
|
621
551
|
const conversation = telegramInboundConversation(String(this.#options.id), update.message.chat);
|
|
622
|
-
await this.#admitWithSenderRole(update.message, conversation, signal);
|
|
552
|
+
await this.#admitWithSenderRole({ message: update.message, conversation, signal, event: update });
|
|
623
553
|
return;
|
|
624
554
|
}
|
|
625
555
|
if (update.callback_query) {
|
|
626
556
|
const query = update.callback_query;
|
|
627
557
|
if (query.data) {
|
|
628
|
-
void this.callApi('answerCallbackQuery', { callback_query_id: query.id }).catch(() => {
|
|
558
|
+
void this.callApi('answerCallbackQuery', { callback_query_id: query.id }, signal).catch(() => {
|
|
629
559
|
/* already answered */
|
|
630
560
|
});
|
|
631
561
|
}
|
|
632
|
-
await this.#admitCallback(query);
|
|
633
|
-
}
|
|
634
|
-
}
|
|
635
|
-
|
|
636
|
-
async callApi<T = unknown>(
|
|
637
|
-
method: string,
|
|
638
|
-
params: Record<string, unknown> = {},
|
|
639
|
-
signal?: AbortSignal,
|
|
640
|
-
): Promise<T> {
|
|
641
|
-
return this.#requestApi<T>(method, {
|
|
642
|
-
method: 'POST',
|
|
643
|
-
headers: { 'Content-Type': 'application/json' },
|
|
644
|
-
body: JSON.stringify(params),
|
|
645
|
-
}, signal, method === 'getUpdates'
|
|
646
|
-
? Math.max(this.#options.requestTimeoutMs ?? DEFAULT_API_TIMEOUT_MS, (Number(params.timeout) || 0) * 1_000 + 15_000)
|
|
647
|
-
: undefined);
|
|
648
|
-
}
|
|
649
|
-
|
|
650
|
-
/** multipart/form-data 变体(attach:// 媒体上传;Content-Type 边界由 FormData 自带)。 */
|
|
651
|
-
async callApiForm<T = unknown>(
|
|
652
|
-
method: string,
|
|
653
|
-
form: FormData,
|
|
654
|
-
signal?: AbortSignal,
|
|
655
|
-
): Promise<T> {
|
|
656
|
-
return this.#requestApi<T>(method, {
|
|
657
|
-
method: 'POST',
|
|
658
|
-
body: form,
|
|
659
|
-
}, signal);
|
|
660
|
-
}
|
|
661
|
-
|
|
662
|
-
async #requestApi<T>(
|
|
663
|
-
method: string,
|
|
664
|
-
init: NonNullable<Parameters<TelegramFetch>[1]>,
|
|
665
|
-
signal?: AbortSignal,
|
|
666
|
-
timeoutMs = this.#options.requestTimeoutMs ?? DEFAULT_API_TIMEOUT_MS,
|
|
667
|
-
): Promise<T> {
|
|
668
|
-
return this.#withRequestDeadline(method, async (requestSignal) => {
|
|
669
|
-
const response = await this.#fetch(botApiUrl(this.#options.config, method), { ...init, signal: requestSignal });
|
|
670
|
-
return this.#parseApiResponse<T>(method, response);
|
|
671
|
-
}, signal, timeoutMs);
|
|
672
|
-
}
|
|
673
|
-
|
|
674
|
-
async #withRequestDeadline<T>(
|
|
675
|
-
method: string,
|
|
676
|
-
requestOperation: (signal: AbortSignal) => Promise<T>,
|
|
677
|
-
signal?: AbortSignal,
|
|
678
|
-
timeoutMs = this.#options.requestTimeoutMs ?? DEFAULT_API_TIMEOUT_MS,
|
|
679
|
-
): Promise<T> {
|
|
680
|
-
const abort = new AbortController();
|
|
681
|
-
let issued = false;
|
|
682
|
-
const sources = [...new Set([signal, this.#pollAbort?.signal, this.#apiAbort.signal].filter((item): item is AbortSignal => !!item))];
|
|
683
|
-
let rejectAbort!: (error: unknown) => void;
|
|
684
|
-
const cancelled = new Promise<never>((_resolve, reject) => { rejectAbort = reject; });
|
|
685
|
-
const onCancel = () => {
|
|
686
|
-
const error = new TelegramApiError('cancelled', `Telegram API ${method} cancelled`, {
|
|
687
|
-
disposition: issued ? 'unknown' : 'not_sent',
|
|
688
|
-
});
|
|
689
|
-
abort.abort(error);
|
|
690
|
-
rejectAbort(error);
|
|
691
|
-
};
|
|
692
|
-
const timer = setTimeout(() => {
|
|
693
|
-
const error = new TelegramApiError('timeout', `Telegram API ${method} timed out`);
|
|
694
|
-
abort.abort(error);
|
|
695
|
-
rejectAbort(error);
|
|
696
|
-
}, timeoutMs);
|
|
697
|
-
for (const source of sources) source.addEventListener('abort', onCancel, { once: true });
|
|
698
|
-
try {
|
|
699
|
-
if (sources.some((source) => source.aborted)) onCancel();
|
|
700
|
-
const request = Promise.resolve().then(async () => {
|
|
701
|
-
abort.signal.throwIfAborted();
|
|
702
|
-
issued = true;
|
|
703
|
-
return requestOperation(abort.signal);
|
|
704
|
-
});
|
|
705
|
-
return await Promise.race([request, cancelled]);
|
|
706
|
-
} catch (error) {
|
|
707
|
-
if (abort.signal.aborted) throw abort.signal.reason;
|
|
708
|
-
if (error instanceof TelegramApiError) throw error;
|
|
709
|
-
// Network exceptions can contain credential-bearing URLs. Keep those out of errors/logs.
|
|
710
|
-
const detail = error instanceof Error
|
|
711
|
-
? error.message.replaceAll(this.#options.config.token, '[redacted]') : 'transport failure';
|
|
712
|
-
throw new TelegramApiError('network', `Telegram API ${method} network request failed: ${detail}`);
|
|
713
|
-
} finally {
|
|
714
|
-
clearTimeout(timer);
|
|
715
|
-
for (const source of sources) source.removeEventListener('abort', onCancel);
|
|
562
|
+
await this.#admitCallback(query, update);
|
|
716
563
|
}
|
|
717
564
|
}
|
|
718
565
|
|
|
719
|
-
async #parseApiResponse<T>(
|
|
720
|
-
method: string,
|
|
721
|
-
response: { readonly status: number; text(): Promise<string> },
|
|
722
|
-
): Promise<T> {
|
|
723
|
-
const text = await response.text();
|
|
724
|
-
let body: TelegramApiOk<T> | TelegramApiErr;
|
|
725
|
-
try {
|
|
726
|
-
body = JSON.parse(text) as TelegramApiOk<T> | TelegramApiErr;
|
|
727
|
-
} catch {
|
|
728
|
-
throw new TelegramApiError('invalid_response', `Telegram API ${method} invalid JSON (${response.status})`, { status: response.status });
|
|
729
|
-
}
|
|
730
|
-
if (!body || typeof body !== 'object' || typeof body.ok !== 'boolean') {
|
|
731
|
-
throw new TelegramApiError('invalid_response', `Telegram API ${method} invalid response envelope`, { status: response.status });
|
|
732
|
-
}
|
|
733
|
-
if (!body.ok || response.status < 200 || response.status >= 300) {
|
|
734
|
-
const status = !body.ok ? body.error_code ?? response.status : response.status;
|
|
735
|
-
const kind = status === 401 || status === 403 ? 'authentication'
|
|
736
|
-
: status === 429 ? 'rate_limit' : response.status >= 400 ? 'http' : 'api';
|
|
737
|
-
const description = !body.ok ? body.description?.replaceAll(this.#options.config.token, '[redacted]') : undefined;
|
|
738
|
-
throw new TelegramApiError(kind,
|
|
739
|
-
`Telegram API ${method} failed (${status}): ${description ?? 'request rejected'}`,
|
|
740
|
-
{ status, disposition: !body.ok ? 'rejected' : 'unknown', ...(!body.ok && body.parameters?.retry_after != null
|
|
741
|
-
? { retryAfterSeconds: body.parameters.retry_after } : {}) },
|
|
742
|
-
);
|
|
743
|
-
}
|
|
744
|
-
if (!('result' in body)) throw new TelegramApiError('invalid_response', `Telegram API ${method} missing result`);
|
|
745
|
-
return body.result;
|
|
746
|
-
}
|
|
747
|
-
|
|
748
|
-
// ── Agent tool surface ──────────────────────────────────────────────
|
|
749
|
-
|
|
750
|
-
async pinMessage(chatId: number, messageId: number): Promise<boolean> {
|
|
751
|
-
await this.callApi('pinChatMessage', { chat_id: chatId, message_id: messageId });
|
|
752
|
-
return true;
|
|
753
|
-
}
|
|
754
|
-
|
|
755
|
-
async unpinMessage(chatId: number, messageId?: number): Promise<boolean> {
|
|
756
|
-
if (messageId != null) {
|
|
757
|
-
await this.callApi('unpinChatMessage', { chat_id: chatId, message_id: messageId });
|
|
758
|
-
} else {
|
|
759
|
-
await this.callApi('unpinAllChatMessages', { chat_id: chatId });
|
|
760
|
-
}
|
|
761
|
-
return true;
|
|
762
|
-
}
|
|
763
|
-
|
|
764
|
-
async setChatDescription(chatId: number, description: string): Promise<boolean> {
|
|
765
|
-
await this.callApi('setChatDescription', { chat_id: chatId, description });
|
|
766
|
-
return true;
|
|
767
|
-
}
|
|
768
|
-
|
|
769
|
-
async setMessageReaction(chatId: number, messageId: number, reaction: string): Promise<boolean> {
|
|
770
|
-
await this.callApi('setMessageReaction', {
|
|
771
|
-
chat_id: chatId,
|
|
772
|
-
message_id: messageId,
|
|
773
|
-
reaction: [{ type: 'emoji', emoji: reaction }],
|
|
774
|
-
});
|
|
775
|
-
return true;
|
|
776
|
-
}
|
|
777
|
-
|
|
778
|
-
async getChatMemberCount(chatId: number): Promise<number> {
|
|
779
|
-
return this.callApi<number>('getChatMemberCount', { chat_id: chatId });
|
|
780
|
-
}
|
|
781
|
-
|
|
782
|
-
async getChatAdmins(chatId: number): Promise<TelegramChatMember[]> {
|
|
783
|
-
return this.callApi<TelegramChatMember[]>('getChatAdministrators', { chat_id: chatId });
|
|
784
|
-
}
|
|
785
|
-
|
|
786
|
-
async sendStickerMessage(chatId: number, sticker: string): Promise<{ message_id: number }> {
|
|
787
|
-
return this.callApi<{ message_id: number }>('sendSticker', { chat_id: chatId, sticker });
|
|
788
|
-
}
|
|
789
|
-
|
|
790
|
-
async setChatPermissionsAll(
|
|
791
|
-
chatId: number,
|
|
792
|
-
permissions: Record<string, boolean | undefined>,
|
|
793
|
-
): Promise<boolean> {
|
|
794
|
-
await this.callApi('setChatPermissions', { chat_id: chatId, permissions });
|
|
795
|
-
return true;
|
|
796
|
-
}
|
|
797
|
-
|
|
798
|
-
async createInviteLink(chatId: number): Promise<string> {
|
|
799
|
-
const link = await this.callApi<{ invite_link: string }>('createChatInviteLink', { chat_id: chatId });
|
|
800
|
-
return link.invite_link;
|
|
801
|
-
}
|
|
802
|
-
|
|
803
|
-
async sendPoll(
|
|
804
|
-
chatId: number,
|
|
805
|
-
question: string,
|
|
806
|
-
options: string[],
|
|
807
|
-
isAnonymous = true,
|
|
808
|
-
allowsMultipleAnswers = false,
|
|
809
|
-
): Promise<{ message_id: number }> {
|
|
810
|
-
return this.callApi<{ message_id: number }>('sendPoll', {
|
|
811
|
-
chat_id: chatId,
|
|
812
|
-
question,
|
|
813
|
-
options,
|
|
814
|
-
is_anonymous: isAnonymous,
|
|
815
|
-
allows_multiple_answers: allowsMultipleAnswers,
|
|
816
|
-
});
|
|
817
|
-
}
|
|
818
566
|
}
|