@zhin.js/adapter-telegram 1.1.22 → 1.1.24
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 +51 -0
- package/lib/endpoint.js +30 -40
- package/lib/protocol.d.ts +2 -0
- package/lib/protocol.js +3 -0
- package/package.json +19 -20
- package/pages/index/index.js +1 -2
- package/pages/index/index.ts +1 -2
- package/plugin.js +8 -6
- package/src/endpoint.ts +27 -36
- package/src/protocol.ts +5 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,56 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.1.24
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- cbf1fb1: Unify Console page blueprints (`definePageBlueprint`, `defineAdapterPage`, `defineCommentReviewPage`, `projectBlueprintToImCard`, `projectBlueprintToCliPanel`) into `@zhin.js/console-contract` and `@zhin.js/page`, add zero-esbuild pure Blueprint synthesis + static client bundle mounting to `@zhin.js/pagemanager`, remove `@zhin.js/client` (React 19) and `.tsx` from all 30 server-side plugins and adapters (including `bilibili` and `juejin`), and remove legacy Era-1 Koa `PageManager` code.
|
|
8
|
+
- Updated dependencies [cbf1fb1]
|
|
9
|
+
- Updated dependencies [008ccad]
|
|
10
|
+
- Updated dependencies [a1f1028]
|
|
11
|
+
- @zhin.js/console-contract@1.1.4
|
|
12
|
+
- @zhin.js/page@1.1.16
|
|
13
|
+
- @zhin.js/core@1.1.58
|
|
14
|
+
- @zhin.js/im-contract@1.1.13
|
|
15
|
+
- @zhin.js/host-http@1.1.16
|
|
16
|
+
- @zhin.js/adapter@1.1.30
|
|
17
|
+
- @zhin.js/tool@1.1.17
|
|
18
|
+
- @zhin.js/agent-feature@1.1.15
|
|
19
|
+
- @zhin.js/feature-kit@1.1.14
|
|
20
|
+
- @zhin.js/skill@1.1.15
|
|
21
|
+
|
|
22
|
+
## 1.1.23
|
|
23
|
+
|
|
24
|
+
### Patch Changes
|
|
25
|
+
|
|
26
|
+
- bc6ccb1: Deep audit bug fixes and 7 architectural improvements:
|
|
27
|
+
- Sink Agent Turn ingress pipeline from `@zhin.js/cli` into `@zhin.js/agent/runtime` (`plugin-runtime/ingress/`) and decompose `ingress-route.ts` into modular pipeline stages.
|
|
28
|
+
- Bind Session State Plane (`JsxButtonRegistry`, `InteractionRuntime`, `RuntimeInteractiveRouter`) to `PluginIncarnationRef` so active buttons and interactions survive unrelated-plugin HMR while expiring when the owning plugin reloads, and support per-conversation fallback numbering (`numberingScope: 'conversation'`).
|
|
29
|
+
- Establish `conversation_events` as the canonical message SSOT and reference `conversation_event_id`, `conversation_key`, `platform_message_id`, and `message_key` in `agent_messages` (`AGENT_MESSAGE_QUERY_VERSION = 2`), threading `conversationSequence` across inbound and outbound delivery receipts.
|
|
30
|
+
- Push down `DatabaseContextRepository` and `DatabaseScheduleJobStorage` full-session/full-table memory scans to indexed SQL queries.
|
|
31
|
+
- Add kernel-level inbound `replyTo` and `mentioned` auto-promotion (`promoteInboundMessagePayload`) plus reusable `InboundDeduper` and `TtlLookupCache` primitives in `@zhin.js/adapter`.
|
|
32
|
+
- Add `MiddlewareContext.dispatch` (`InboundDispatchSummary`) and `onAfterDispatch` hooks for `after-dispatch` middlewares, and support incremental rendering of in-place `envelope.payload` array mutations in `OutboundDeliveryRuntime`.
|
|
33
|
+
- Unify `HistoryWindowTracker` and `AgentCompactionRuntime` token/message budgets (`resolveUnifiedHistoryAndCompactionBudget`).
|
|
34
|
+
- Fix schema validation, database transaction connection cleanup and SQLite WAL mode, schedule timer wheel head-of-line blocking, security policy bash read paths and subagent guard propagation, group merge window permission carrier handoff, micro-compact namespaced tool matching, and WebSocket force-close / inbound segment mapping across adapters.
|
|
35
|
+
- Updated dependencies [3a8504b]
|
|
36
|
+
- Updated dependencies [6163abc]
|
|
37
|
+
- Updated dependencies [fed2c27]
|
|
38
|
+
- Updated dependencies [bc6ccb1]
|
|
39
|
+
- Updated dependencies [72b5a12]
|
|
40
|
+
- Updated dependencies [aa41889]
|
|
41
|
+
- Updated dependencies [4427e72]
|
|
42
|
+
- Updated dependencies [22bd14a]
|
|
43
|
+
- @zhin.js/agent-feature@1.1.14
|
|
44
|
+
- @zhin.js/core@1.1.57
|
|
45
|
+
- @zhin.js/client@1.1.25
|
|
46
|
+
- @zhin.js/page@1.1.15
|
|
47
|
+
- @zhin.js/im-contract@1.1.12
|
|
48
|
+
- @zhin.js/adapter@1.1.29
|
|
49
|
+
- @zhin.js/host-http@1.1.15
|
|
50
|
+
- @zhin.js/feature-kit@1.1.13
|
|
51
|
+
- @zhin.js/tool@1.1.16
|
|
52
|
+
- @zhin.js/skill@1.1.14
|
|
53
|
+
|
|
3
54
|
## 1.1.22
|
|
4
55
|
|
|
5
56
|
### Patch Changes
|
package/lib/endpoint.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { TelegramClient } from './telegram-client.js';
|
|
2
2
|
import { createTelegramStreaming } from './streaming.js';
|
|
3
|
-
import { createEndpointSpecification, endpointSpec, createEndpointLifecycle, } from 'zhin.js/adapter';
|
|
3
|
+
import { createEndpointSpecification, endpointSpec, createEndpointLifecycle, InboundDeduper, TtlLookupCache, } from 'zhin.js/adapter';
|
|
4
4
|
/**
|
|
5
5
|
* TelegramEndpoint — lifecycle, outbound, admit, Bot API helpers for agent tools.
|
|
6
6
|
*/
|
|
@@ -35,12 +35,15 @@ export class TelegramEndpoint extends TelegramClient {
|
|
|
35
35
|
#executionSignal;
|
|
36
36
|
#updateOffset = 0;
|
|
37
37
|
#pollingHealthy = true;
|
|
38
|
-
#admittedUpdates = new
|
|
38
|
+
#admittedUpdates = new InboundDeduper({ maxEntries: ADMITTED_UPDATE_LIMIT });
|
|
39
39
|
#inflightUpdates = new Map();
|
|
40
40
|
#botUserId;
|
|
41
41
|
#botUsername;
|
|
42
42
|
#botName;
|
|
43
|
-
#chatMemberCache = new
|
|
43
|
+
#chatMemberCache = new TtlLookupCache({
|
|
44
|
+
ttlMs: CHAT_MEMBER_CACHE_TTL_MS,
|
|
45
|
+
maxEntries: CHAT_MEMBER_CACHE_MAX,
|
|
46
|
+
});
|
|
44
47
|
#control = Object.freeze({
|
|
45
48
|
recall: ({ message, signal }) => this.recallMessage(message, signal),
|
|
46
49
|
});
|
|
@@ -341,6 +344,7 @@ export class TelegramEndpoint extends TelegramClient {
|
|
|
341
344
|
},
|
|
342
345
|
endpointId: this.#options.config.id,
|
|
343
346
|
...(mentioned ? { mentioned: true } : {}),
|
|
347
|
+
...(msg.reply_to_message?.message_id != null ? { replyTo: { id: String(msg.reply_to_message.message_id) } } : {}),
|
|
344
348
|
metadata: Object.freeze({
|
|
345
349
|
channelType: resolveTelegramChannelType(msg.chat.type),
|
|
346
350
|
chatType: msg.chat.type,
|
|
@@ -351,21 +355,27 @@ export class TelegramEndpoint extends TelegramClient {
|
|
|
351
355
|
}),
|
|
352
356
|
});
|
|
353
357
|
}
|
|
354
|
-
/** entities 里 mention 文本命中 bot username(getMe 缓存),或 text_mention 指向 bot 用户。 */
|
|
358
|
+
/** entities / caption_entities 里 mention 文本命中 bot username(getMe 缓存),或 text_mention 指向 bot 用户。 */
|
|
355
359
|
#isBotMentioned(msg) {
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
360
|
+
const sources = [
|
|
361
|
+
{ text: msg.text ?? '', entities: msg.entities },
|
|
362
|
+
{ text: msg.caption ?? '', entities: msg.caption_entities },
|
|
363
|
+
];
|
|
364
|
+
for (const { text, entities } of sources) {
|
|
365
|
+
if (!entities?.length)
|
|
362
366
|
continue;
|
|
367
|
+
for (const entity of entities) {
|
|
368
|
+
if (entity.type === 'text_mention') {
|
|
369
|
+
if (this.#botUserId != null && entity.user?.id === this.#botUserId)
|
|
370
|
+
return true;
|
|
371
|
+
continue;
|
|
372
|
+
}
|
|
373
|
+
if (entity.type !== 'mention' || !this.#botUsername)
|
|
374
|
+
continue;
|
|
375
|
+
const slice = text.slice(entity.offset, entity.offset + entity.length);
|
|
376
|
+
if (slice.toLowerCase() === `@${this.#botUsername.toLowerCase()}`)
|
|
377
|
+
return true;
|
|
363
378
|
}
|
|
364
|
-
if (entity.type !== 'mention' || !this.#botUsername)
|
|
365
|
-
continue;
|
|
366
|
-
const slice = (msg.text ?? '').slice(entity.offset, entity.offset + entity.length);
|
|
367
|
-
if (slice.toLowerCase() === `@${this.#botUsername.toLowerCase()}`)
|
|
368
|
-
return true;
|
|
369
379
|
}
|
|
370
380
|
return false;
|
|
371
381
|
}
|
|
@@ -376,10 +386,8 @@ export class TelegramEndpoint extends TelegramClient {
|
|
|
376
386
|
const chatId = Number(msg.chat.id);
|
|
377
387
|
const userId = msg.from.id;
|
|
378
388
|
const key = `${chatId}:${userId}`;
|
|
379
|
-
const now = Date.now();
|
|
380
|
-
this.#sweepChatMemberCache(now);
|
|
381
389
|
const cached = this.#chatMemberCache.get(key);
|
|
382
|
-
if (cached
|
|
390
|
+
if (cached)
|
|
383
391
|
return cached;
|
|
384
392
|
try {
|
|
385
393
|
const member = await this.callApi('getChatMember', {
|
|
@@ -389,7 +397,7 @@ export class TelegramEndpoint extends TelegramClient {
|
|
|
389
397
|
if (signal.aborted)
|
|
390
398
|
return undefined;
|
|
391
399
|
const normalized = normalizeTelegramChatMember(member);
|
|
392
|
-
const entry = {
|
|
400
|
+
const entry = { ...normalized };
|
|
393
401
|
this.#chatMemberCache.set(key, entry);
|
|
394
402
|
return entry;
|
|
395
403
|
}
|
|
@@ -398,22 +406,6 @@ export class TelegramEndpoint extends TelegramClient {
|
|
|
398
406
|
return undefined;
|
|
399
407
|
}
|
|
400
408
|
}
|
|
401
|
-
#sweepChatMemberCache(now) {
|
|
402
|
-
for (const [key, entry] of this.#chatMemberCache) {
|
|
403
|
-
if (now - entry.at >= CHAT_MEMBER_CACHE_TTL_MS)
|
|
404
|
-
this.#chatMemberCache.delete(key);
|
|
405
|
-
}
|
|
406
|
-
if (this.#chatMemberCache.size > CHAT_MEMBER_CACHE_MAX) {
|
|
407
|
-
const excess = this.#chatMemberCache.size - CHAT_MEMBER_CACHE_MAX;
|
|
408
|
-
let removed = 0;
|
|
409
|
-
for (const [key] of this.#chatMemberCache) {
|
|
410
|
-
if (removed >= excess)
|
|
411
|
-
break;
|
|
412
|
-
this.#chatMemberCache.delete(key);
|
|
413
|
-
removed++;
|
|
414
|
-
}
|
|
415
|
-
}
|
|
416
|
-
}
|
|
417
409
|
/** Test / internal: admit a callback query when open. */
|
|
418
410
|
admitCallback(query) {
|
|
419
411
|
void this[endpointSpec].events.run(() => {
|
|
@@ -455,7 +447,8 @@ export class TelegramEndpoint extends TelegramClient {
|
|
|
455
447
|
if (!Number.isSafeInteger(update.update_id) || update.update_id < 0) {
|
|
456
448
|
throw new TypeError('Telegram update_id must be a non-negative safe integer');
|
|
457
449
|
}
|
|
458
|
-
|
|
450
|
+
const updateKey = String(update.update_id);
|
|
451
|
+
if (this.#admittedUpdates.has(updateKey))
|
|
459
452
|
return;
|
|
460
453
|
const pending = this.#inflightUpdates.get(update.update_id);
|
|
461
454
|
if (pending)
|
|
@@ -468,10 +461,7 @@ export class TelegramEndpoint extends TelegramClient {
|
|
|
468
461
|
// Once canonical dispatch enters Core, its generation lease drains the work.
|
|
469
462
|
// Closing the transport cannot turn successful business processing into a retry.
|
|
470
463
|
await this.#receiveUpdate(update, AbortSignal.any([admission.signal, this.#executionSignal]));
|
|
471
|
-
this.#admittedUpdates.
|
|
472
|
-
if (this.#admittedUpdates.size > ADMITTED_UPDATE_LIMIT) {
|
|
473
|
-
this.#admittedUpdates.delete(this.#admittedUpdates.values().next().value);
|
|
474
|
-
}
|
|
464
|
+
this.#admittedUpdates.shouldProcess(updateKey);
|
|
475
465
|
})();
|
|
476
466
|
this.#inflightUpdates.set(update.update_id, receiving);
|
|
477
467
|
try {
|
package/lib/protocol.d.ts
CHANGED
|
@@ -73,6 +73,7 @@ export interface TelegramMessage {
|
|
|
73
73
|
readonly text?: string;
|
|
74
74
|
readonly caption?: string;
|
|
75
75
|
readonly entities?: readonly TelegramMessageEntity[];
|
|
76
|
+
readonly caption_entities?: readonly TelegramMessageEntity[];
|
|
76
77
|
readonly reply_to_message?: TelegramMessage;
|
|
77
78
|
readonly photo?: readonly TelegramPhotoSize[];
|
|
78
79
|
readonly video?: {
|
|
@@ -145,6 +146,7 @@ export interface TelegramWireSegment {
|
|
|
145
146
|
export interface TelegramInlineButton {
|
|
146
147
|
readonly text: string;
|
|
147
148
|
readonly callback_data?: string;
|
|
149
|
+
readonly url?: string;
|
|
148
150
|
/** Bot API 10.3+: mutually exclusive with callback_data. */
|
|
149
151
|
readonly disabled?: Record<string, never>;
|
|
150
152
|
}
|
package/lib/protocol.js
CHANGED
|
@@ -349,12 +349,15 @@ function buildOutboundActions(target, payload, uploads) {
|
|
|
349
349
|
: {};
|
|
350
350
|
const text = String(record.label ?? record.text ?? '');
|
|
351
351
|
const callback = String(record.payload ?? record.callback_data ?? '');
|
|
352
|
+
const url = typeof record.url === 'string' && record.url.trim() ? record.url.trim() : undefined;
|
|
352
353
|
if (record.mode === 'command')
|
|
353
354
|
throw new EndpointDeliveryError('unsupported_operation', 'Telegram command buttons are not implemented', 'not_sent');
|
|
354
355
|
if (!text.trim())
|
|
355
356
|
throw new EndpointDeliveryError('invalid_payload', 'Telegram button label must be nonempty', 'not_sent');
|
|
356
357
|
if (record.disabled)
|
|
357
358
|
return { text, disabled: {} };
|
|
359
|
+
if (url && /^https?:\/\//i.test(url))
|
|
360
|
+
return { text, url };
|
|
358
361
|
if (Buffer.byteLength(callback, 'utf8') < 1 || Buffer.byteLength(callback, 'utf8') > 64)
|
|
359
362
|
throw new EndpointDeliveryError('invalid_payload', 'Telegram callback payload must be 1-64 UTF-8 bytes and label nonempty', 'not_sent');
|
|
360
363
|
return {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zhin.js/adapter-telegram",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.24",
|
|
4
4
|
"description": "Zhin.js Telegram Bot API adapter for Plugin Runtime (long-poll getUpdates)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./lib/index.js",
|
|
@@ -31,26 +31,25 @@
|
|
|
31
31
|
"directory": "plugins/adapters/telegram"
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
|
-
"@zhin.js/adapter": "1.1.
|
|
35
|
-
"@zhin.js/agent-feature": "1.1.
|
|
36
|
-
"@zhin.js/
|
|
37
|
-
"@zhin.js/
|
|
38
|
-
"@zhin.js/
|
|
39
|
-
"@zhin.js/
|
|
40
|
-
"@zhin.js/
|
|
41
|
-
"@zhin.js/im-contract": "1.1.11",
|
|
34
|
+
"@zhin.js/adapter": "1.1.30",
|
|
35
|
+
"@zhin.js/agent-feature": "1.1.15",
|
|
36
|
+
"@zhin.js/console-contract": "1.1.4",
|
|
37
|
+
"@zhin.js/core": "1.1.58",
|
|
38
|
+
"@zhin.js/feature-kit": "1.1.14",
|
|
39
|
+
"@zhin.js/host-http": "1.1.16",
|
|
40
|
+
"@zhin.js/im-contract": "1.1.13",
|
|
42
41
|
"@zhin.js/logger": "1.1.2",
|
|
43
|
-
"@zhin.js/page": "1.1.
|
|
44
|
-
"@zhin.js/skill": "1.1.
|
|
45
|
-
"@zhin.js/tool": "1.1.
|
|
42
|
+
"@zhin.js/page": "1.1.16",
|
|
43
|
+
"@zhin.js/skill": "1.1.15",
|
|
44
|
+
"@zhin.js/tool": "1.1.17"
|
|
46
45
|
},
|
|
47
46
|
"peerDependencies": {
|
|
48
47
|
"zod": "^4.0.0",
|
|
49
|
-
"@zhin.js/adapter": "^1.1.
|
|
50
|
-
"@zhin.js/command": "^1.1.
|
|
51
|
-
"@zhin.js/core": "^1.1.
|
|
52
|
-
"@zhin.js/permission": "^1.1.
|
|
53
|
-
"zhin.js": "^1.1.
|
|
48
|
+
"@zhin.js/adapter": "^1.1.30",
|
|
49
|
+
"@zhin.js/command": "^1.1.16",
|
|
50
|
+
"@zhin.js/core": "^1.1.58",
|
|
51
|
+
"@zhin.js/permission": "^1.1.14",
|
|
52
|
+
"zhin.js": "^1.1.23"
|
|
54
53
|
},
|
|
55
54
|
"peerDependenciesMeta": {
|
|
56
55
|
"@zhin.js/command": {
|
|
@@ -69,9 +68,9 @@
|
|
|
69
68
|
"typescript": "^6.0.3",
|
|
70
69
|
"vitest": "^4.1.10",
|
|
71
70
|
"zod": "^4.4.3",
|
|
72
|
-
"@zhin.js/host-http": "1.1.
|
|
73
|
-
"@zhin.js/pagemanager": "1.1.
|
|
74
|
-
"zhin.js": "1.1.
|
|
71
|
+
"@zhin.js/host-http": "1.1.16",
|
|
72
|
+
"@zhin.js/pagemanager": "1.1.23",
|
|
73
|
+
"zhin.js": "1.1.23"
|
|
75
74
|
},
|
|
76
75
|
"files": [
|
|
77
76
|
"adapters",
|
package/pages/index/index.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
// Generated by build-plugin-runtime-entries.mjs. Do not edit.
|
|
2
|
-
import { definePage } from '@zhin.js/console-contract';
|
|
3
|
-
import { createAdapterConsolePage } from '@zhin.js/client';
|
|
2
|
+
import { definePage, createAdapterConsolePage } from '@zhin.js/console-contract';
|
|
4
3
|
export const meta = definePage({
|
|
5
4
|
title: 'Telegram',
|
|
6
5
|
titleI18n: { en: 'Telegram' },
|
package/pages/index/index.ts
CHANGED
package/plugin.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Generated by build-plugin-runtime-entries.mjs. Do not edit.
|
|
2
|
-
import { definePlugin
|
|
2
|
+
import { definePlugin } from 'zhin.js';
|
|
3
3
|
import { permissionHostToken } from '@zhin.js/permission';
|
|
4
4
|
import { checkTelegramPlatformPermit } from "./lib/platform-permit.js";
|
|
5
5
|
import { createTelegramConsoleRpc } from "./lib/console.js";
|
|
@@ -10,10 +10,12 @@ export default definePlugin({
|
|
|
10
10
|
displayName: 'Telegram Bot API Adapter',
|
|
11
11
|
},
|
|
12
12
|
setup(context) {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
13
|
+
const dispose = context.resources.has(permissionHostToken)
|
|
14
|
+
? context.resources.use(permissionHostToken).registerPlatform('telegram', checkTelegramPlatformPermit)
|
|
15
|
+
: undefined;
|
|
16
|
+
return {
|
|
17
|
+
consoleRpc: createTelegramConsoleRpc(() => context.config.get()),
|
|
18
|
+
...(dispose ? { dispose } : {}),
|
|
19
|
+
};
|
|
18
20
|
},
|
|
19
21
|
});
|
package/src/endpoint.ts
CHANGED
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
createEndpointSpecification, endpointSpec, type Endpoint, type EndpointSpecification, createEndpointLifecycle,
|
|
5
5
|
type EndpointLifecycle, type EndpointContentPort, type EndpointContentResolveContext,
|
|
6
6
|
type EndpointControl, type EndpointSendRequest, type EndpointTransportState,
|
|
7
|
+
InboundDeduper, TtlLookupCache,
|
|
7
8
|
} from 'zhin.js/adapter';
|
|
8
9
|
/**
|
|
9
10
|
* TelegramEndpoint — lifecycle, outbound, admit, Bot API helpers for agent tools.
|
|
@@ -49,7 +50,6 @@ const ADMITTED_UPDATE_LIMIT = 2_048;
|
|
|
49
50
|
const INFLIGHT_UPDATE_LIMIT = 256;
|
|
50
51
|
|
|
51
52
|
interface ChatMemberPermit {
|
|
52
|
-
readonly at: number;
|
|
53
53
|
readonly role?: string;
|
|
54
54
|
readonly permissions: string[];
|
|
55
55
|
}
|
|
@@ -91,12 +91,15 @@ export class TelegramEndpoint extends TelegramClient implements Endpoint {
|
|
|
91
91
|
#executionSignal?: AbortSignal;
|
|
92
92
|
#updateOffset = 0;
|
|
93
93
|
#pollingHealthy = true;
|
|
94
|
-
readonly #admittedUpdates = new
|
|
94
|
+
readonly #admittedUpdates = new InboundDeduper({ maxEntries: ADMITTED_UPDATE_LIMIT });
|
|
95
95
|
readonly #inflightUpdates = new Map<number, Promise<void>>();
|
|
96
96
|
#botUserId?: number;
|
|
97
97
|
#botUsername?: string;
|
|
98
98
|
#botName?: string;
|
|
99
|
-
readonly #chatMemberCache = new
|
|
99
|
+
readonly #chatMemberCache = new TtlLookupCache<string, ChatMemberPermit>({
|
|
100
|
+
ttlMs: CHAT_MEMBER_CACHE_TTL_MS,
|
|
101
|
+
maxEntries: CHAT_MEMBER_CACHE_MAX,
|
|
102
|
+
});
|
|
100
103
|
readonly #control: EndpointControl = Object.freeze<EndpointControl>({
|
|
101
104
|
recall: ({ message, signal }) => this.recallMessage(message, signal),
|
|
102
105
|
});
|
|
@@ -410,6 +413,7 @@ export class TelegramEndpoint extends TelegramClient implements Endpoint {
|
|
|
410
413
|
},
|
|
411
414
|
endpointId: this.#options.config.id,
|
|
412
415
|
...(mentioned ? { mentioned: true } : {}),
|
|
416
|
+
...(msg.reply_to_message?.message_id != null ? { replyTo: { id: String(msg.reply_to_message.message_id) } } : {}),
|
|
413
417
|
metadata: Object.freeze({
|
|
414
418
|
channelType: resolveTelegramChannelType(msg.chat.type),
|
|
415
419
|
chatType: msg.chat.type,
|
|
@@ -421,17 +425,23 @@ export class TelegramEndpoint extends TelegramClient implements Endpoint {
|
|
|
421
425
|
});
|
|
422
426
|
}
|
|
423
427
|
|
|
424
|
-
/** entities 里 mention 文本命中 bot username(getMe 缓存),或 text_mention 指向 bot 用户。 */
|
|
428
|
+
/** entities / caption_entities 里 mention 文本命中 bot username(getMe 缓存),或 text_mention 指向 bot 用户。 */
|
|
425
429
|
#isBotMentioned(msg: TelegramMessage): boolean {
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
430
|
+
const sources: Array<{ readonly text: string; readonly entities: readonly import('./protocol.js').TelegramMessageEntity[] | undefined }> = [
|
|
431
|
+
{ text: msg.text ?? '', entities: msg.entities },
|
|
432
|
+
{ text: msg.caption ?? '', entities: msg.caption_entities },
|
|
433
|
+
];
|
|
434
|
+
for (const { text, entities } of sources) {
|
|
435
|
+
if (!entities?.length) continue;
|
|
436
|
+
for (const entity of entities) {
|
|
437
|
+
if (entity.type === 'text_mention') {
|
|
438
|
+
if (this.#botUserId != null && entity.user?.id === this.#botUserId) return true;
|
|
439
|
+
continue;
|
|
440
|
+
}
|
|
441
|
+
if (entity.type !== 'mention' || !this.#botUsername) continue;
|
|
442
|
+
const slice = text.slice(entity.offset, entity.offset + entity.length);
|
|
443
|
+
if (slice.toLowerCase() === `@${this.#botUsername.toLowerCase()}`) return true;
|
|
431
444
|
}
|
|
432
|
-
if (entity.type !== 'mention' || !this.#botUsername) continue;
|
|
433
|
-
const slice = (msg.text ?? '').slice(entity.offset, entity.offset + entity.length);
|
|
434
|
-
if (slice.toLowerCase() === `@${this.#botUsername.toLowerCase()}`) return true;
|
|
435
445
|
}
|
|
436
446
|
return false;
|
|
437
447
|
}
|
|
@@ -442,10 +452,8 @@ export class TelegramEndpoint extends TelegramClient implements Endpoint {
|
|
|
442
452
|
const chatId = Number(msg.chat.id);
|
|
443
453
|
const userId = msg.from.id;
|
|
444
454
|
const key = `${chatId}:${userId}`;
|
|
445
|
-
const now = Date.now();
|
|
446
|
-
this.#sweepChatMemberCache(now);
|
|
447
455
|
const cached = this.#chatMemberCache.get(key);
|
|
448
|
-
if (cached
|
|
456
|
+
if (cached) return cached;
|
|
449
457
|
try {
|
|
450
458
|
const member = await this.callApi<TelegramChatMember>('getChatMember', {
|
|
451
459
|
chat_id: chatId,
|
|
@@ -453,7 +461,7 @@ export class TelegramEndpoint extends TelegramClient implements Endpoint {
|
|
|
453
461
|
}, signal);
|
|
454
462
|
if (signal.aborted) return undefined;
|
|
455
463
|
const normalized = normalizeTelegramChatMember(member);
|
|
456
|
-
const entry: ChatMemberPermit = {
|
|
464
|
+
const entry: ChatMemberPermit = { ...normalized };
|
|
457
465
|
this.#chatMemberCache.set(key, entry);
|
|
458
466
|
return entry;
|
|
459
467
|
} catch {
|
|
@@ -462,21 +470,6 @@ export class TelegramEndpoint extends TelegramClient implements Endpoint {
|
|
|
462
470
|
}
|
|
463
471
|
}
|
|
464
472
|
|
|
465
|
-
#sweepChatMemberCache(now: number): void {
|
|
466
|
-
for (const [key, entry] of this.#chatMemberCache) {
|
|
467
|
-
if (now - entry.at >= CHAT_MEMBER_CACHE_TTL_MS) this.#chatMemberCache.delete(key);
|
|
468
|
-
}
|
|
469
|
-
if (this.#chatMemberCache.size > CHAT_MEMBER_CACHE_MAX) {
|
|
470
|
-
const excess = this.#chatMemberCache.size - CHAT_MEMBER_CACHE_MAX;
|
|
471
|
-
let removed = 0;
|
|
472
|
-
for (const [key] of this.#chatMemberCache) {
|
|
473
|
-
if (removed >= excess) break;
|
|
474
|
-
this.#chatMemberCache.delete(key);
|
|
475
|
-
removed++;
|
|
476
|
-
}
|
|
477
|
-
}
|
|
478
|
-
}
|
|
479
|
-
|
|
480
473
|
/** Test / internal: admit a callback query when open. */
|
|
481
474
|
admitCallback(query: TelegramCallbackQuery): void {
|
|
482
475
|
void this[endpointSpec].events.run(() => {
|
|
@@ -519,7 +512,8 @@ export class TelegramEndpoint extends TelegramClient implements Endpoint {
|
|
|
519
512
|
if (!Number.isSafeInteger(update.update_id) || update.update_id < 0) {
|
|
520
513
|
throw new TypeError('Telegram update_id must be a non-negative safe integer');
|
|
521
514
|
}
|
|
522
|
-
|
|
515
|
+
const updateKey = String(update.update_id);
|
|
516
|
+
if (this.#admittedUpdates.has(updateKey)) return;
|
|
523
517
|
const pending = this.#inflightUpdates.get(update.update_id);
|
|
524
518
|
if (pending) return pending;
|
|
525
519
|
if (this.#inflightUpdates.size >= INFLIGHT_UPDATE_LIMIT) {
|
|
@@ -530,10 +524,7 @@ export class TelegramEndpoint extends TelegramClient implements Endpoint {
|
|
|
530
524
|
// Once canonical dispatch enters Core, its generation lease drains the work.
|
|
531
525
|
// Closing the transport cannot turn successful business processing into a retry.
|
|
532
526
|
await this.#receiveUpdate(update, AbortSignal.any([admission.signal, this.#executionSignal!]));
|
|
533
|
-
this.#admittedUpdates.
|
|
534
|
-
if (this.#admittedUpdates.size > ADMITTED_UPDATE_LIMIT) {
|
|
535
|
-
this.#admittedUpdates.delete(this.#admittedUpdates.values().next().value!);
|
|
536
|
-
}
|
|
527
|
+
this.#admittedUpdates.shouldProcess(updateKey);
|
|
537
528
|
})();
|
|
538
529
|
this.#inflightUpdates.set(update.update_id, receiving);
|
|
539
530
|
try {
|
package/src/protocol.ts
CHANGED
|
@@ -85,6 +85,7 @@ export interface TelegramMessage {
|
|
|
85
85
|
readonly text?: string;
|
|
86
86
|
readonly caption?: string;
|
|
87
87
|
readonly entities?: readonly TelegramMessageEntity[];
|
|
88
|
+
readonly caption_entities?: readonly TelegramMessageEntity[];
|
|
88
89
|
readonly reply_to_message?: TelegramMessage;
|
|
89
90
|
readonly photo?: readonly TelegramPhotoSize[];
|
|
90
91
|
readonly video?: {
|
|
@@ -162,6 +163,7 @@ export interface TelegramWireSegment {
|
|
|
162
163
|
export interface TelegramInlineButton {
|
|
163
164
|
readonly text: string;
|
|
164
165
|
readonly callback_data?: string;
|
|
166
|
+
readonly url?: string;
|
|
165
167
|
/** Bot API 10.3+: mutually exclusive with callback_data. */
|
|
166
168
|
readonly disabled?: Record<string, never>;
|
|
167
169
|
}
|
|
@@ -625,13 +627,15 @@ function buildOutboundActions(
|
|
|
625
627
|
const buttons = Array.isArray(row) ? row : [];
|
|
626
628
|
return buttons.map((btn) => {
|
|
627
629
|
const record = btn && typeof btn === 'object'
|
|
628
|
-
? btn as { label?: string; text?: string; payload?: string; callback_data?: string; disabled?: boolean; mode?: string }
|
|
630
|
+
? btn as { label?: string; text?: string; payload?: string; callback_data?: string; url?: string; disabled?: boolean; mode?: string }
|
|
629
631
|
: {};
|
|
630
632
|
const text = String(record.label ?? record.text ?? '');
|
|
631
633
|
const callback = String(record.payload ?? record.callback_data ?? '');
|
|
634
|
+
const url = typeof record.url === 'string' && record.url.trim() ? record.url.trim() : undefined;
|
|
632
635
|
if (record.mode === 'command') throw new EndpointDeliveryError('unsupported_operation', 'Telegram command buttons are not implemented', 'not_sent');
|
|
633
636
|
if (!text.trim()) throw new EndpointDeliveryError('invalid_payload', 'Telegram button label must be nonempty', 'not_sent');
|
|
634
637
|
if (record.disabled) return { text, disabled: {} };
|
|
638
|
+
if (url && /^https?:\/\//i.test(url)) return { text, url };
|
|
635
639
|
if (Buffer.byteLength(callback, 'utf8') < 1 || Buffer.byteLength(callback, 'utf8') > 64) throw new EndpointDeliveryError('invalid_payload', 'Telegram callback payload must be 1-64 UTF-8 bytes and label nonempty', 'not_sent');
|
|
636
640
|
return {
|
|
637
641
|
text,
|