@zhin.js/adapter-telegram 1.0.69 → 1.1.2
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 +909 -17
- package/PERMITS.md +24 -0
- package/README.md +106 -77
- package/adapters/telegram/index.js +34 -0
- package/adapters/telegram/index.ts +39 -0
- package/agents/telegram/agent.json +20 -0
- package/agents/telegram/boundaries.md +3 -0
- package/agents/telegram/conventions.md +3 -0
- package/agents/telegram/skills/telegram-chat-admin/SKILL.md +24 -0
- package/agents/telegram/skills/telegram-chat-admin/tools/create_invite/index.js +18 -0
- package/agents/telegram/skills/telegram-chat-admin/tools/create_invite/index.ts +18 -0
- package/agents/telegram/skills/telegram-chat-admin/tools/list_admins/index.js +24 -0
- package/agents/telegram/skills/telegram-chat-admin/tools/list_admins/index.ts +24 -0
- package/agents/telegram/skills/telegram-chat-admin/tools/member_count/index.js +16 -0
- package/agents/telegram/skills/telegram-chat-admin/tools/member_count/index.ts +16 -0
- package/agents/telegram/skills/telegram-chat-admin/tools/pin_message/index.js +19 -0
- package/agents/telegram/skills/telegram-chat-admin/tools/pin_message/index.ts +19 -0
- package/agents/telegram/skills/telegram-chat-admin/tools/set_description/index.js +17 -0
- package/agents/telegram/skills/telegram-chat-admin/tools/set_description/index.ts +17 -0
- package/agents/telegram/skills/telegram-chat-admin/tools/set_permissions/index.js +32 -0
- package/agents/telegram/skills/telegram-chat-admin/tools/set_permissions/index.ts +31 -0
- package/agents/telegram/skills/telegram-chat-admin/tools/unpin_message/index.js +19 -0
- package/agents/telegram/skills/telegram-chat-admin/tools/unpin_message/index.ts +19 -0
- package/agents/telegram/skills/telegram-messages/SKILL.md +19 -0
- package/agents/telegram/skills/telegram-messages/tools/react/index.js +18 -0
- package/agents/telegram/skills/telegram-messages/tools/react/index.ts +18 -0
- package/agents/telegram/skills/telegram-messages/tools/send_poll/index.js +30 -0
- package/agents/telegram/skills/telegram-messages/tools/send_poll/index.ts +32 -0
- package/agents/telegram/skills/telegram-messages/tools/send_sticker/index.js +17 -0
- package/agents/telegram/skills/telegram-messages/tools/send_sticker/index.ts +17 -0
- package/agents/telegram/system.md +3 -0
- package/commands/telegram/endpoint/add/[id]/index.js +3 -0
- package/commands/telegram/endpoint/add/[id]/index.ts +3 -0
- package/commands/telegram/endpoint/definition.js +17 -0
- package/commands/telegram/endpoint/definition.ts +17 -0
- package/commands/telegram/endpoint/list/index.js +3 -0
- package/commands/telegram/endpoint/list/index.ts +3 -0
- package/commands/telegram/endpoint/remove/[id]/index.js +3 -0
- package/commands/telegram/endpoint/remove/[id]/index.ts +3 -0
- package/lib/client.d.ts +12 -0
- package/lib/client.js +2 -0
- package/lib/endpoint.d.ts +112 -0
- package/lib/endpoint.js +535 -0
- package/lib/index.d.ts +4 -18
- package/lib/index.js +4 -455
- package/lib/markdown-to-html.d.ts +9 -0
- package/lib/markdown-to-html.js +75 -0
- package/lib/platform-permit.d.ts +17 -0
- package/lib/platform-permit.js +51 -0
- package/lib/polling.d.ts +9 -0
- package/lib/polling.js +57 -0
- package/lib/protocol.d.ts +291 -0
- package/lib/protocol.js +463 -0
- package/lib/telegram-runtime-state.d.ts +1 -0
- package/lib/telegram-runtime-state.js +6 -0
- package/lib/webhook.d.ts +12 -0
- package/lib/webhook.js +55 -0
- package/package.json +72 -29
- package/plugin.js +19 -0
- package/schema.json +141 -0
- package/src/client.ts +16 -0
- package/src/endpoint.ts +679 -0
- package/src/index.ts +39 -426
- package/src/markdown-to-html.ts +86 -0
- package/src/platform-permit.ts +65 -0
- package/src/polling.ts +76 -0
- package/src/protocol.ts +744 -0
- package/src/telegram-runtime-state.ts +7 -0
- package/src/webhook.ts +75 -0
- package/client/Dashboard.tsx +0 -295
- package/client/index.tsx +0 -11
- package/client/tsconfig.json +0 -7
- package/client/utils/api.ts +0 -17
- package/dist/index.js +0 -32
- package/lib/adapter.d.ts +0 -18
- package/lib/adapter.d.ts.map +0 -1
- package/lib/adapter.js +0 -55
- package/lib/adapter.js.map +0 -1
- package/lib/bot.d.ts +0 -140
- package/lib/bot.d.ts.map +0 -1
- package/lib/bot.js +0 -866
- package/lib/bot.js.map +0 -1
- package/lib/index.d.ts.map +0 -1
- package/lib/index.js.map +0 -1
- package/lib/types.d.ts +0 -29
- package/lib/types.d.ts.map +0 -1
- package/lib/types.js +0 -2
- package/lib/types.js.map +0 -1
- package/plugin.yml +0 -3
- package/skills/telegram/SKILL.md +0 -103
- package/src/adapter.ts +0 -64
- package/src/bot.ts +0 -983
- package/src/types.ts +0 -32
package/lib/endpoint.js
ADDED
|
@@ -0,0 +1,535 @@
|
|
|
1
|
+
import { Endpoint, createEndpointLifecycle, } from 'zhin.js/adapter';
|
|
2
|
+
/**
|
|
3
|
+
* TelegramEndpoint — lifecycle, outbound, admit, Bot API helpers for agent tools.
|
|
4
|
+
*/
|
|
5
|
+
import { readFile } from 'node:fs/promises';
|
|
6
|
+
import { formatCompact, getAdapterLogger } from '@zhin.js/logger';
|
|
7
|
+
import { runTelegramPollLoop } from './polling.js';
|
|
8
|
+
import { normalizeTelegramChatMember } from './platform-permit.js';
|
|
9
|
+
import { botApiUrl, buildWebhookUrl, formatCallbackContent, formatCallbackSegments, formatInboundContent, formatInboundSegments, formatOutboundPlan, resolveTelegramChannelType, senderDisplayName, telegramInboundConversation, } from './protocol.js';
|
|
10
|
+
import { registerTelegramWebhookRoutes } from './webhook.js';
|
|
11
|
+
const CHAT_MEMBER_CACHE_TTL_MS = 60_000;
|
|
12
|
+
const CHAT_MEMBER_CACHE_MAX = 2_000;
|
|
13
|
+
/** Telegram Bot API client carried by message, update and lifecycle events. */
|
|
14
|
+
export class TelegramClient {
|
|
15
|
+
api;
|
|
16
|
+
constructor(api) {
|
|
17
|
+
this.api = api;
|
|
18
|
+
}
|
|
19
|
+
callApi = (method, params = {}) => this.api.callApi(method, params);
|
|
20
|
+
callApiForm = (method, form) => this.api.callApiForm(method, form);
|
|
21
|
+
pinMessage = (chatId, messageId) => this.api.pinMessage(chatId, messageId);
|
|
22
|
+
unpinMessage = (chatId, messageId) => this.api.unpinMessage(chatId, messageId);
|
|
23
|
+
setChatDescription = (chatId, description) => this.api.setChatDescription(chatId, description);
|
|
24
|
+
setMessageReaction = (chatId, messageId, reaction) => this.api.setMessageReaction(chatId, messageId, reaction);
|
|
25
|
+
getChatMemberCount = (chatId) => this.api.getChatMemberCount(chatId);
|
|
26
|
+
getChatAdmins = (chatId) => this.api.getChatAdmins(chatId);
|
|
27
|
+
sendStickerMessage = (chatId, sticker) => this.api.sendStickerMessage(chatId, sticker);
|
|
28
|
+
setChatPermissionsAll = (chatId, permissions) => this.api.setChatPermissionsAll(chatId, permissions);
|
|
29
|
+
createInviteLink = (chatId) => this.api.createInviteLink(chatId);
|
|
30
|
+
sendPoll = (chatId, question, options, anonymous, multiple) => this.api.sendPoll(chatId, question, options, anonymous, multiple);
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Telegram Bot API 无列表类接口(无 getMyChats/getChatMembers),
|
|
34
|
+
* 仅 getChat/getChatMember 按已知 id 单查,不构成列表能力;
|
|
35
|
+
* 因此本 endpoint 不暴露 EndpointManagement(Console 社交面 RPC 对该平台保持未接线)。
|
|
36
|
+
*/
|
|
37
|
+
export class TelegramEndpoint extends Endpoint {
|
|
38
|
+
client = new TelegramClient({
|
|
39
|
+
callApi: (method, params) => this.callApi(method, params),
|
|
40
|
+
callApiForm: (method, form) => this.callApiForm(method, form),
|
|
41
|
+
pinMessage: (chatId, messageId) => this.pinMessage(chatId, messageId),
|
|
42
|
+
unpinMessage: (chatId, messageId) => this.unpinMessage(chatId, messageId),
|
|
43
|
+
setChatDescription: (chatId, description) => this.setChatDescription(chatId, description),
|
|
44
|
+
setMessageReaction: (chatId, messageId, reaction) => this.setMessageReaction(chatId, messageId, reaction),
|
|
45
|
+
getChatMemberCount: (chatId) => this.getChatMemberCount(chatId),
|
|
46
|
+
getChatAdmins: (chatId) => this.getChatAdmins(chatId),
|
|
47
|
+
sendStickerMessage: (chatId, sticker) => this.sendStickerMessage(chatId, sticker),
|
|
48
|
+
setChatPermissionsAll: (chatId, permissions) => this.setChatPermissionsAll(chatId, permissions),
|
|
49
|
+
createInviteLink: (chatId) => this.createInviteLink(chatId),
|
|
50
|
+
sendPoll: (chatId, question, options, anonymous, multiple) => this.sendPoll(chatId, question, options, anonymous, multiple),
|
|
51
|
+
});
|
|
52
|
+
#logger;
|
|
53
|
+
#options;
|
|
54
|
+
#fetch;
|
|
55
|
+
#pollAbort;
|
|
56
|
+
#pollPromise;
|
|
57
|
+
#lifecycle;
|
|
58
|
+
#open = false;
|
|
59
|
+
#admission;
|
|
60
|
+
#updateOffset = 0;
|
|
61
|
+
#botUserId;
|
|
62
|
+
#botUsername;
|
|
63
|
+
#chatMemberCache = new Map();
|
|
64
|
+
control = Object.freeze({
|
|
65
|
+
recall: (message) => this.recallMessage(message),
|
|
66
|
+
});
|
|
67
|
+
content = Object.freeze({
|
|
68
|
+
resolve: (reference, context) => this.#resolveContent(reference, context.signal),
|
|
69
|
+
});
|
|
70
|
+
constructor(options) {
|
|
71
|
+
super();
|
|
72
|
+
this.#logger = getAdapterLogger('telegram', options.config.id);
|
|
73
|
+
this.#lifecycle = createEndpointLifecycle({ name: options.config.id, reconnect: false });
|
|
74
|
+
this.#options = options;
|
|
75
|
+
this.#fetch = options.fetch ?? globalThis.fetch;
|
|
76
|
+
}
|
|
77
|
+
/** Used by webhook handler. */
|
|
78
|
+
get isOpen() {
|
|
79
|
+
return this.#open;
|
|
80
|
+
}
|
|
81
|
+
get config() {
|
|
82
|
+
return this.#options.config;
|
|
83
|
+
}
|
|
84
|
+
get allowedUpdates() {
|
|
85
|
+
return this.#options.config.allowedUpdates;
|
|
86
|
+
}
|
|
87
|
+
getUpdateOffset() {
|
|
88
|
+
return this.#updateOffset;
|
|
89
|
+
}
|
|
90
|
+
setUpdateOffset(offset) {
|
|
91
|
+
this.#updateOffset = offset;
|
|
92
|
+
}
|
|
93
|
+
async start() {
|
|
94
|
+
await this.#lifecycle.start(async (handle) => {
|
|
95
|
+
const abort = new AbortController();
|
|
96
|
+
const routes = [];
|
|
97
|
+
this.#pollAbort = abort;
|
|
98
|
+
const cleanup = () => {
|
|
99
|
+
abort.abort();
|
|
100
|
+
for (const release of routes.splice(0))
|
|
101
|
+
release();
|
|
102
|
+
if (this.#pollAbort === abort) {
|
|
103
|
+
this.#botUserId = undefined;
|
|
104
|
+
this.#botUsername = undefined;
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
handle.onForceClose(cleanup);
|
|
108
|
+
try {
|
|
109
|
+
const me = await this.callApi('getMe', {}, abort.signal);
|
|
110
|
+
if (abort.signal.aborted)
|
|
111
|
+
return;
|
|
112
|
+
this.#botUserId = me.id;
|
|
113
|
+
this.#botUsername = me.username;
|
|
114
|
+
if (this.#options.config.mode === 'webhook') {
|
|
115
|
+
if (!this.#options.http) {
|
|
116
|
+
throw new TypeError('Telegram webhook mode requires httpHostToken');
|
|
117
|
+
}
|
|
118
|
+
routes.push(...registerTelegramWebhookRoutes(this.#options.http, this));
|
|
119
|
+
const webhook = this.#options.config.webhook;
|
|
120
|
+
if (!webhook.secretToken) {
|
|
121
|
+
// 未配 secretToken 时 webhook 无鉴权:任何人知道 path 即可注入假 update。
|
|
122
|
+
this.#logger.warn(formatCompact({
|
|
123
|
+
op: 'webhook_no_secret',
|
|
124
|
+
endpoint: this.#options.config.id,
|
|
125
|
+
path: webhook.path,
|
|
126
|
+
hint: 'set webhook.secretToken to authenticate Telegram callbacks',
|
|
127
|
+
}));
|
|
128
|
+
}
|
|
129
|
+
const url = buildWebhookUrl(webhook);
|
|
130
|
+
await this.callApi('setWebhook', {
|
|
131
|
+
url,
|
|
132
|
+
allowed_updates: this.#options.config.allowedUpdates,
|
|
133
|
+
...(webhook.secretToken ? { secret_token: webhook.secretToken } : {}),
|
|
134
|
+
}, abort.signal);
|
|
135
|
+
if (abort.signal.aborted)
|
|
136
|
+
return;
|
|
137
|
+
this.#logger.info(formatCompact({
|
|
138
|
+
op: 'connect',
|
|
139
|
+
endpoint: this.#options.config.id,
|
|
140
|
+
mode: 'webhook',
|
|
141
|
+
path: webhook.path,
|
|
142
|
+
username: me.username,
|
|
143
|
+
}));
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
await this.callApi('deleteWebhook', { drop_pending_updates: false }, abort.signal);
|
|
147
|
+
if (abort.signal.aborted)
|
|
148
|
+
return;
|
|
149
|
+
this.#pollPromise = runTelegramPollLoop(this, abort.signal);
|
|
150
|
+
this.#logger.info(formatCompact({
|
|
151
|
+
op: 'connect',
|
|
152
|
+
endpoint: this.#options.config.id,
|
|
153
|
+
mode: 'polling',
|
|
154
|
+
username: me.username,
|
|
155
|
+
}));
|
|
156
|
+
}
|
|
157
|
+
catch (error) {
|
|
158
|
+
const cancelled = abort.signal.aborted;
|
|
159
|
+
cleanup();
|
|
160
|
+
if (cancelled)
|
|
161
|
+
return;
|
|
162
|
+
this.#logger.error('Failed to connect Telegram bot:', error);
|
|
163
|
+
throw error;
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
open() {
|
|
168
|
+
if (this.#open)
|
|
169
|
+
return;
|
|
170
|
+
this.#admission = new AbortController();
|
|
171
|
+
this.#open = true;
|
|
172
|
+
}
|
|
173
|
+
close() {
|
|
174
|
+
this.#open = false;
|
|
175
|
+
this.#admission?.abort();
|
|
176
|
+
this.#chatMemberCache.clear();
|
|
177
|
+
}
|
|
178
|
+
async stop() {
|
|
179
|
+
this.close();
|
|
180
|
+
const polling = this.#pollPromise;
|
|
181
|
+
this.#pollPromise = undefined;
|
|
182
|
+
await this.#lifecycle.stop();
|
|
183
|
+
try {
|
|
184
|
+
await polling;
|
|
185
|
+
}
|
|
186
|
+
catch {
|
|
187
|
+
/* poll loop exit */
|
|
188
|
+
}
|
|
189
|
+
this.#logger.debug(formatCompact({ op: 'disconnect' }));
|
|
190
|
+
}
|
|
191
|
+
async send({ conversation, payload }) {
|
|
192
|
+
const plan = formatOutboundPlan(conversation.id, payload);
|
|
193
|
+
let lastId = '';
|
|
194
|
+
for (const action of plan.actions) {
|
|
195
|
+
const form = await this.#buildUploadForm(action.params, plan.uploads);
|
|
196
|
+
const result = form
|
|
197
|
+
? await this.callApiForm(action.method, form)
|
|
198
|
+
: await this.callApi(action.method, action.params);
|
|
199
|
+
if (!result || !Number.isSafeInteger(result.message_id) || result.message_id <= 0) {
|
|
200
|
+
throw new Error(`Telegram API ${action.method} returned no valid message_id; delivery is unconfirmed`);
|
|
201
|
+
}
|
|
202
|
+
lastId = String(result.message_id);
|
|
203
|
+
}
|
|
204
|
+
if (!lastId)
|
|
205
|
+
throw new Error('Telegram message contains no sendable actions');
|
|
206
|
+
return lastId;
|
|
207
|
+
}
|
|
208
|
+
async recallMessage(message) {
|
|
209
|
+
if (!message.id || message.id.startsWith('telegram-'))
|
|
210
|
+
return;
|
|
211
|
+
await this.callApi('deleteMessage', {
|
|
212
|
+
chat_id: message.conversation.id,
|
|
213
|
+
message_id: Number(message.id),
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
async #resolveContent(reference, signal) {
|
|
217
|
+
if (reference.kind !== 'media') {
|
|
218
|
+
return Object.freeze({ status: 'unsupported', code: 'telegram_message_lookup_unavailable' });
|
|
219
|
+
}
|
|
220
|
+
if (reference.media.kind !== 'file') {
|
|
221
|
+
return Object.freeze({ status: 'resolved', reference, value: reference.media });
|
|
222
|
+
}
|
|
223
|
+
try {
|
|
224
|
+
signal.throwIfAborted();
|
|
225
|
+
const file = await this.callApi('getFile', {
|
|
226
|
+
file_id: reference.media.value,
|
|
227
|
+
});
|
|
228
|
+
if (!file.file_path)
|
|
229
|
+
return Object.freeze({ status: 'not_found', code: 'telegram_file_not_found' });
|
|
230
|
+
if ((file.file_size ?? 0) > 26_214_400)
|
|
231
|
+
return Object.freeze({ status: 'forbidden', code: 'media_size_limit' });
|
|
232
|
+
const response = await this.#fetch(`${this.#options.config.apiBaseUrl}/file/bot${this.#options.config.token}/${file.file_path}`, { signal });
|
|
233
|
+
if (!response.ok)
|
|
234
|
+
return Object.freeze({ status: 'failed', code: 'telegram_file_download_failed' });
|
|
235
|
+
if (!response.arrayBuffer)
|
|
236
|
+
return Object.freeze({ status: 'failed', code: 'telegram_binary_transport_unavailable' });
|
|
237
|
+
const bytes = Buffer.from(await response.arrayBuffer());
|
|
238
|
+
if (bytes.byteLength > 26_214_400)
|
|
239
|
+
return Object.freeze({ status: 'forbidden', code: 'media_size_limit' });
|
|
240
|
+
return Object.freeze({
|
|
241
|
+
status: 'resolved',
|
|
242
|
+
reference,
|
|
243
|
+
value: Object.freeze({
|
|
244
|
+
kind: 'base64',
|
|
245
|
+
value: bytes.toString('base64'),
|
|
246
|
+
...(response.headers?.get('content-type')?.split(';')[0] || reference.media.mime_type
|
|
247
|
+
? { mime_type: response.headers?.get('content-type')?.split(';')[0] ?? reference.media.mime_type }
|
|
248
|
+
: {}),
|
|
249
|
+
...(reference.media.file_name ? { file_name: reference.media.file_name } : {}),
|
|
250
|
+
size: bytes.byteLength,
|
|
251
|
+
}),
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
catch (error) {
|
|
255
|
+
if (signal.aborted)
|
|
256
|
+
return Object.freeze({ status: 'expired', code: 'turn_aborted' });
|
|
257
|
+
return Object.freeze({ status: 'failed', code: 'telegram_file_resolution_failed', message: error instanceof Error ? error.message : String(error) });
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* 含 `attach://` 占位的媒体参数 → multipart/form-data:
|
|
262
|
+
* 标量参数原样、对象参数 JSON 序列化、attach 占位替换为文件 part
|
|
263
|
+
* (base64 直接解码,本地路径读盘)。无上传时返回 undefined(走 JSON 调用)。
|
|
264
|
+
*/
|
|
265
|
+
async #buildUploadForm(params, uploads) {
|
|
266
|
+
const values = Object.values(params);
|
|
267
|
+
if (!values.some((v) => typeof v === 'string' && v.startsWith('attach://')))
|
|
268
|
+
return undefined;
|
|
269
|
+
const form = new FormData();
|
|
270
|
+
for (const [key, value] of Object.entries(params)) {
|
|
271
|
+
if (value == null)
|
|
272
|
+
continue;
|
|
273
|
+
if (typeof value === 'string' && value.startsWith('attach://')) {
|
|
274
|
+
const upload = uploads.find((item) => `attach://${item.attachName}` === value);
|
|
275
|
+
if (!upload)
|
|
276
|
+
throw new Error(`Telegram upload 未登记: ${value}`);
|
|
277
|
+
const data = upload.source.kind === 'base64'
|
|
278
|
+
? Buffer.from(upload.source.data, 'base64')
|
|
279
|
+
: await readFile(upload.source.path);
|
|
280
|
+
form.append(key, new Blob([data], upload.mimeType ? { type: upload.mimeType } : undefined), upload.filename);
|
|
281
|
+
continue;
|
|
282
|
+
}
|
|
283
|
+
form.append(key, typeof value === 'object' ? JSON.stringify(value) : String(value));
|
|
284
|
+
}
|
|
285
|
+
return form;
|
|
286
|
+
}
|
|
287
|
+
/** Test / internal: admit a message when open. */
|
|
288
|
+
admit(msg) {
|
|
289
|
+
if (!this.#open || !this.#admission)
|
|
290
|
+
return;
|
|
291
|
+
const conversation = telegramInboundConversation(String(this.#options.id), msg.chat);
|
|
292
|
+
void this.#admitWithSenderRole(msg, conversation, this.#admission.signal).catch((err) => {
|
|
293
|
+
this.#logger.warn(formatCompact({
|
|
294
|
+
op: 'telegram_gateway_receive_failed',
|
|
295
|
+
target: `${conversation.kind}:${conversation.id}`,
|
|
296
|
+
error: err instanceof Error ? err.message : String(err),
|
|
297
|
+
}));
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
async #admitWithSenderRole(msg, conversation, signal) {
|
|
301
|
+
const permit = await this.#resolveGroupSenderPermit(msg, signal);
|
|
302
|
+
if (signal.aborted)
|
|
303
|
+
return;
|
|
304
|
+
// 新 Runtime Message.content 为纯文本:@ 本机只能经 metadata 传递
|
|
305
|
+
const mentioned = this.#isBotMentioned(msg);
|
|
306
|
+
await this.emit('message.receive', {
|
|
307
|
+
conversation,
|
|
308
|
+
message: { conversation, id: String(msg.message_id) },
|
|
309
|
+
content: formatInboundContent(msg),
|
|
310
|
+
segments: formatInboundSegments(msg),
|
|
311
|
+
sender: {
|
|
312
|
+
id: String(msg.from?.id ?? ''),
|
|
313
|
+
name: senderDisplayName(msg.from) || undefined,
|
|
314
|
+
...(permit?.role ? { roles: [permit.role] } : {}),
|
|
315
|
+
},
|
|
316
|
+
endpointId: this.#options.config.id,
|
|
317
|
+
...(mentioned ? { mentioned: true } : {}),
|
|
318
|
+
metadata: Object.freeze({
|
|
319
|
+
channelType: resolveTelegramChannelType(msg.chat.type),
|
|
320
|
+
chatType: msg.chat.type,
|
|
321
|
+
userId: msg.from?.id,
|
|
322
|
+
date: msg.date,
|
|
323
|
+
...(permit?.role ? { senderRole: permit.role } : {}),
|
|
324
|
+
...(permit?.permissions.length ? { senderPermissions: [...permit.permissions] } : {}),
|
|
325
|
+
}),
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
/** entities 里 mention 文本命中 bot username(getMe 缓存),或 text_mention 指向 bot 用户。 */
|
|
329
|
+
#isBotMentioned(msg) {
|
|
330
|
+
if (!msg.entities?.length)
|
|
331
|
+
return false;
|
|
332
|
+
for (const entity of msg.entities) {
|
|
333
|
+
if (entity.type === 'text_mention') {
|
|
334
|
+
if (this.#botUserId != null && entity.user?.id === this.#botUserId)
|
|
335
|
+
return true;
|
|
336
|
+
continue;
|
|
337
|
+
}
|
|
338
|
+
if (entity.type !== 'mention' || !this.#botUsername)
|
|
339
|
+
continue;
|
|
340
|
+
const slice = (msg.text ?? '').slice(entity.offset, entity.offset + entity.length);
|
|
341
|
+
if (slice.toLowerCase() === `@${this.#botUsername.toLowerCase()}`)
|
|
342
|
+
return true;
|
|
343
|
+
}
|
|
344
|
+
return false;
|
|
345
|
+
}
|
|
346
|
+
/** 群消息 sender role 解析:getChatMember + 60s 缓存(对齐旧 enrichGroupSender)。 */
|
|
347
|
+
async #resolveGroupSenderPermit(msg, signal) {
|
|
348
|
+
if (msg.chat.type === 'private' || !msg.from?.id)
|
|
349
|
+
return undefined;
|
|
350
|
+
const chatId = Number(msg.chat.id);
|
|
351
|
+
const userId = msg.from.id;
|
|
352
|
+
const key = `${chatId}:${userId}`;
|
|
353
|
+
const now = Date.now();
|
|
354
|
+
this.#sweepChatMemberCache(now);
|
|
355
|
+
const cached = this.#chatMemberCache.get(key);
|
|
356
|
+
if (cached && now - cached.at < CHAT_MEMBER_CACHE_TTL_MS)
|
|
357
|
+
return cached;
|
|
358
|
+
try {
|
|
359
|
+
const member = await this.callApi('getChatMember', {
|
|
360
|
+
chat_id: chatId,
|
|
361
|
+
user_id: userId,
|
|
362
|
+
}, signal);
|
|
363
|
+
if (signal.aborted)
|
|
364
|
+
return undefined;
|
|
365
|
+
const normalized = normalizeTelegramChatMember(member);
|
|
366
|
+
const entry = { at: now, ...normalized };
|
|
367
|
+
this.#chatMemberCache.set(key, entry);
|
|
368
|
+
return entry;
|
|
369
|
+
}
|
|
370
|
+
catch {
|
|
371
|
+
// 保守拒绝:无角色快照
|
|
372
|
+
return undefined;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
#sweepChatMemberCache(now) {
|
|
376
|
+
for (const [key, entry] of this.#chatMemberCache) {
|
|
377
|
+
if (now - entry.at >= CHAT_MEMBER_CACHE_TTL_MS)
|
|
378
|
+
this.#chatMemberCache.delete(key);
|
|
379
|
+
}
|
|
380
|
+
if (this.#chatMemberCache.size > CHAT_MEMBER_CACHE_MAX) {
|
|
381
|
+
const excess = this.#chatMemberCache.size - CHAT_MEMBER_CACHE_MAX;
|
|
382
|
+
let removed = 0;
|
|
383
|
+
for (const [key] of this.#chatMemberCache) {
|
|
384
|
+
if (removed >= excess)
|
|
385
|
+
break;
|
|
386
|
+
this.#chatMemberCache.delete(key);
|
|
387
|
+
removed++;
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
/** Test / internal: admit a callback query when open. */
|
|
392
|
+
admitCallback(query) {
|
|
393
|
+
if (!this.#open)
|
|
394
|
+
return;
|
|
395
|
+
const endpointKey = String(this.#options.id);
|
|
396
|
+
const msg = query.message;
|
|
397
|
+
// 无挂载消息的 callback(如 inline 模式)退化为 sender 私聊会话
|
|
398
|
+
const conversation = msg
|
|
399
|
+
? telegramInboundConversation(endpointKey, msg.chat)
|
|
400
|
+
: telegramInboundConversation(endpointKey, { id: query.from.id, type: 'private' });
|
|
401
|
+
void this.emit('message.receive', {
|
|
402
|
+
conversation,
|
|
403
|
+
message: { conversation, id: query.id },
|
|
404
|
+
content: formatCallbackContent(query),
|
|
405
|
+
segments: formatCallbackSegments(query),
|
|
406
|
+
sender: { id: String(query.from?.id ?? ''), name: senderDisplayName(query.from) || undefined },
|
|
407
|
+
endpointId: this.#options.config.id,
|
|
408
|
+
metadata: Object.freeze({
|
|
409
|
+
eventType: 'callback_query',
|
|
410
|
+
payload: query.data,
|
|
411
|
+
sourceMessageId: msg ? String(msg.message_id) : undefined,
|
|
412
|
+
}),
|
|
413
|
+
}).catch((err) => {
|
|
414
|
+
this.#logger.warn(formatCompact({
|
|
415
|
+
op: 'telegram_gateway_receive_failed',
|
|
416
|
+
target: `${conversation.kind}:${conversation.id}`,
|
|
417
|
+
error: err instanceof Error ? err.message : String(err),
|
|
418
|
+
}));
|
|
419
|
+
});
|
|
420
|
+
}
|
|
421
|
+
/** Used by webhook / polling handlers. */
|
|
422
|
+
handleUpdate(update) {
|
|
423
|
+
const eventName = update.message
|
|
424
|
+
? 'message'
|
|
425
|
+
: update.callback_query
|
|
426
|
+
? 'callback_query'
|
|
427
|
+
: 'update';
|
|
428
|
+
void this.emitPlatform(eventName, update).catch((error) => {
|
|
429
|
+
this.#logger.warn(formatCompact({
|
|
430
|
+
op: 'telegram_platform_event_failed',
|
|
431
|
+
event: eventName,
|
|
432
|
+
error: error instanceof Error ? error.message : String(error),
|
|
433
|
+
}));
|
|
434
|
+
});
|
|
435
|
+
if (update.message) {
|
|
436
|
+
this.admit(update.message);
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
if (update.callback_query) {
|
|
440
|
+
const query = update.callback_query;
|
|
441
|
+
if (query.data) {
|
|
442
|
+
void this.callApi('answerCallbackQuery', { callback_query_id: query.id }).catch(() => {
|
|
443
|
+
/* already answered */
|
|
444
|
+
});
|
|
445
|
+
}
|
|
446
|
+
this.admitCallback(query);
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
async callApi(method, params = {}, signal) {
|
|
450
|
+
const url = botApiUrl(this.#options.config, method);
|
|
451
|
+
const response = await this.#fetch(url, {
|
|
452
|
+
method: 'POST',
|
|
453
|
+
headers: { 'Content-Type': 'application/json' },
|
|
454
|
+
body: JSON.stringify(params),
|
|
455
|
+
signal,
|
|
456
|
+
});
|
|
457
|
+
return this.#parseApiResponse(method, response);
|
|
458
|
+
}
|
|
459
|
+
/** multipart/form-data 变体(attach:// 媒体上传;Content-Type 边界由 FormData 自带)。 */
|
|
460
|
+
async callApiForm(method, form, signal) {
|
|
461
|
+
const url = botApiUrl(this.#options.config, method);
|
|
462
|
+
const response = await this.#fetch(url, {
|
|
463
|
+
method: 'POST',
|
|
464
|
+
body: form,
|
|
465
|
+
signal,
|
|
466
|
+
});
|
|
467
|
+
return this.#parseApiResponse(method, response);
|
|
468
|
+
}
|
|
469
|
+
async #parseApiResponse(method, response) {
|
|
470
|
+
const text = await response.text();
|
|
471
|
+
let body;
|
|
472
|
+
try {
|
|
473
|
+
body = JSON.parse(text);
|
|
474
|
+
}
|
|
475
|
+
catch {
|
|
476
|
+
throw new Error(`Telegram API ${method} invalid JSON (${response.status}): ${text.slice(0, 200)}`);
|
|
477
|
+
}
|
|
478
|
+
if (!body.ok) {
|
|
479
|
+
throw new Error(`Telegram API ${method} failed (${body.error_code ?? response.status}): ${body.description ?? text}`);
|
|
480
|
+
}
|
|
481
|
+
return body.result;
|
|
482
|
+
}
|
|
483
|
+
// ── Agent tool surface ──────────────────────────────────────────────
|
|
484
|
+
async pinMessage(chatId, messageId) {
|
|
485
|
+
await this.callApi('pinChatMessage', { chat_id: chatId, message_id: messageId });
|
|
486
|
+
return true;
|
|
487
|
+
}
|
|
488
|
+
async unpinMessage(chatId, messageId) {
|
|
489
|
+
if (messageId != null) {
|
|
490
|
+
await this.callApi('unpinChatMessage', { chat_id: chatId, message_id: messageId });
|
|
491
|
+
}
|
|
492
|
+
else {
|
|
493
|
+
await this.callApi('unpinAllChatMessages', { chat_id: chatId });
|
|
494
|
+
}
|
|
495
|
+
return true;
|
|
496
|
+
}
|
|
497
|
+
async setChatDescription(chatId, description) {
|
|
498
|
+
await this.callApi('setChatDescription', { chat_id: chatId, description });
|
|
499
|
+
return true;
|
|
500
|
+
}
|
|
501
|
+
async setMessageReaction(chatId, messageId, reaction) {
|
|
502
|
+
await this.callApi('setMessageReaction', {
|
|
503
|
+
chat_id: chatId,
|
|
504
|
+
message_id: messageId,
|
|
505
|
+
reaction: [{ type: 'emoji', emoji: reaction }],
|
|
506
|
+
});
|
|
507
|
+
return true;
|
|
508
|
+
}
|
|
509
|
+
async getChatMemberCount(chatId) {
|
|
510
|
+
return this.callApi('getChatMemberCount', { chat_id: chatId });
|
|
511
|
+
}
|
|
512
|
+
async getChatAdmins(chatId) {
|
|
513
|
+
return this.callApi('getChatAdministrators', { chat_id: chatId });
|
|
514
|
+
}
|
|
515
|
+
async sendStickerMessage(chatId, sticker) {
|
|
516
|
+
return this.callApi('sendSticker', { chat_id: chatId, sticker });
|
|
517
|
+
}
|
|
518
|
+
async setChatPermissionsAll(chatId, permissions) {
|
|
519
|
+
await this.callApi('setChatPermissions', { chat_id: chatId, permissions });
|
|
520
|
+
return true;
|
|
521
|
+
}
|
|
522
|
+
async createInviteLink(chatId) {
|
|
523
|
+
const link = await this.callApi('createChatInviteLink', { chat_id: chatId });
|
|
524
|
+
return link.invite_link;
|
|
525
|
+
}
|
|
526
|
+
async sendPoll(chatId, question, options, isAnonymous = true, allowsMultipleAnswers = false) {
|
|
527
|
+
return this.callApi('sendPoll', {
|
|
528
|
+
chat_id: chatId,
|
|
529
|
+
question,
|
|
530
|
+
options,
|
|
531
|
+
is_anonymous: isAnonymous,
|
|
532
|
+
allows_multiple_answers: allowsMultipleAnswers,
|
|
533
|
+
});
|
|
534
|
+
}
|
|
535
|
+
}
|
package/lib/index.d.ts
CHANGED
|
@@ -1,18 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
interface Contexts {
|
|
6
|
-
web: PageManager;
|
|
7
|
-
}
|
|
8
|
-
}
|
|
9
|
-
}
|
|
10
|
-
declare module "zhin.js" {
|
|
11
|
-
interface Adapters {
|
|
12
|
-
telegram: TelegramAdapter;
|
|
13
|
-
}
|
|
14
|
-
}
|
|
15
|
-
export * from "./types.js";
|
|
16
|
-
export { TelegramBot } from "./bot.js";
|
|
17
|
-
export { TelegramAdapter } from "./adapter.js";
|
|
18
|
-
//# sourceMappingURL=index.d.ts.map
|
|
1
|
+
export { TelegramClient, TelegramEndpoint, type TelegramClientApi, type TelegramEndpointOptions, type TelegramFetch, } from './endpoint.js';
|
|
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 { telegramClient, type TelegramClientEventMap } from './client.js';
|
|
4
|
+
export { checkTelegramPlatformPermit, normalizeTelegramChatMember, platformPermit, telegramGroupPermitResolver, } from './platform-permit.js';
|