@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.
Files changed (93) hide show
  1. package/CHANGELOG.md +909 -17
  2. package/PERMITS.md +24 -0
  3. package/README.md +106 -77
  4. package/adapters/telegram/index.js +34 -0
  5. package/adapters/telegram/index.ts +39 -0
  6. package/agents/telegram/agent.json +20 -0
  7. package/agents/telegram/boundaries.md +3 -0
  8. package/agents/telegram/conventions.md +3 -0
  9. package/agents/telegram/skills/telegram-chat-admin/SKILL.md +24 -0
  10. package/agents/telegram/skills/telegram-chat-admin/tools/create_invite/index.js +18 -0
  11. package/agents/telegram/skills/telegram-chat-admin/tools/create_invite/index.ts +18 -0
  12. package/agents/telegram/skills/telegram-chat-admin/tools/list_admins/index.js +24 -0
  13. package/agents/telegram/skills/telegram-chat-admin/tools/list_admins/index.ts +24 -0
  14. package/agents/telegram/skills/telegram-chat-admin/tools/member_count/index.js +16 -0
  15. package/agents/telegram/skills/telegram-chat-admin/tools/member_count/index.ts +16 -0
  16. package/agents/telegram/skills/telegram-chat-admin/tools/pin_message/index.js +19 -0
  17. package/agents/telegram/skills/telegram-chat-admin/tools/pin_message/index.ts +19 -0
  18. package/agents/telegram/skills/telegram-chat-admin/tools/set_description/index.js +17 -0
  19. package/agents/telegram/skills/telegram-chat-admin/tools/set_description/index.ts +17 -0
  20. package/agents/telegram/skills/telegram-chat-admin/tools/set_permissions/index.js +32 -0
  21. package/agents/telegram/skills/telegram-chat-admin/tools/set_permissions/index.ts +31 -0
  22. package/agents/telegram/skills/telegram-chat-admin/tools/unpin_message/index.js +19 -0
  23. package/agents/telegram/skills/telegram-chat-admin/tools/unpin_message/index.ts +19 -0
  24. package/agents/telegram/skills/telegram-messages/SKILL.md +19 -0
  25. package/agents/telegram/skills/telegram-messages/tools/react/index.js +18 -0
  26. package/agents/telegram/skills/telegram-messages/tools/react/index.ts +18 -0
  27. package/agents/telegram/skills/telegram-messages/tools/send_poll/index.js +30 -0
  28. package/agents/telegram/skills/telegram-messages/tools/send_poll/index.ts +32 -0
  29. package/agents/telegram/skills/telegram-messages/tools/send_sticker/index.js +17 -0
  30. package/agents/telegram/skills/telegram-messages/tools/send_sticker/index.ts +17 -0
  31. package/agents/telegram/system.md +3 -0
  32. package/commands/telegram/endpoint/add/[id]/index.js +3 -0
  33. package/commands/telegram/endpoint/add/[id]/index.ts +3 -0
  34. package/commands/telegram/endpoint/definition.js +17 -0
  35. package/commands/telegram/endpoint/definition.ts +17 -0
  36. package/commands/telegram/endpoint/list/index.js +3 -0
  37. package/commands/telegram/endpoint/list/index.ts +3 -0
  38. package/commands/telegram/endpoint/remove/[id]/index.js +3 -0
  39. package/commands/telegram/endpoint/remove/[id]/index.ts +3 -0
  40. package/lib/client.d.ts +12 -0
  41. package/lib/client.js +2 -0
  42. package/lib/endpoint.d.ts +112 -0
  43. package/lib/endpoint.js +535 -0
  44. package/lib/index.d.ts +4 -18
  45. package/lib/index.js +4 -455
  46. package/lib/markdown-to-html.d.ts +9 -0
  47. package/lib/markdown-to-html.js +75 -0
  48. package/lib/platform-permit.d.ts +17 -0
  49. package/lib/platform-permit.js +51 -0
  50. package/lib/polling.d.ts +9 -0
  51. package/lib/polling.js +57 -0
  52. package/lib/protocol.d.ts +291 -0
  53. package/lib/protocol.js +463 -0
  54. package/lib/telegram-runtime-state.d.ts +1 -0
  55. package/lib/telegram-runtime-state.js +6 -0
  56. package/lib/webhook.d.ts +12 -0
  57. package/lib/webhook.js +55 -0
  58. package/package.json +72 -29
  59. package/plugin.js +19 -0
  60. package/schema.json +141 -0
  61. package/src/client.ts +16 -0
  62. package/src/endpoint.ts +679 -0
  63. package/src/index.ts +39 -426
  64. package/src/markdown-to-html.ts +86 -0
  65. package/src/platform-permit.ts +65 -0
  66. package/src/polling.ts +76 -0
  67. package/src/protocol.ts +744 -0
  68. package/src/telegram-runtime-state.ts +7 -0
  69. package/src/webhook.ts +75 -0
  70. package/client/Dashboard.tsx +0 -295
  71. package/client/index.tsx +0 -11
  72. package/client/tsconfig.json +0 -7
  73. package/client/utils/api.ts +0 -17
  74. package/dist/index.js +0 -32
  75. package/lib/adapter.d.ts +0 -18
  76. package/lib/adapter.d.ts.map +0 -1
  77. package/lib/adapter.js +0 -55
  78. package/lib/adapter.js.map +0 -1
  79. package/lib/bot.d.ts +0 -140
  80. package/lib/bot.d.ts.map +0 -1
  81. package/lib/bot.js +0 -866
  82. package/lib/bot.js.map +0 -1
  83. package/lib/index.d.ts.map +0 -1
  84. package/lib/index.js.map +0 -1
  85. package/lib/types.d.ts +0 -29
  86. package/lib/types.d.ts.map +0 -1
  87. package/lib/types.js +0 -2
  88. package/lib/types.js.map +0 -1
  89. package/plugin.yml +0 -3
  90. package/skills/telegram/SKILL.md +0 -103
  91. package/src/adapter.ts +0 -64
  92. package/src/bot.ts +0 -983
  93. package/src/types.ts +0 -32
package/lib/index.js CHANGED
@@ -1,455 +1,4 @@
1
- /**
2
- * Telegram 适配器入口:类型扩展、导出、注册
3
- */
4
- import path from "node:path";
5
- import { usePlugin, createGroupManagementTools } from "zhin.js";
6
- import { TelegramAdapter } from "./adapter.js";
7
- export * from "./types.js";
8
- export { TelegramBot } from "./bot.js";
9
- export { TelegramAdapter } from "./adapter.js";
10
- const plugin = usePlugin();
11
- const { provide, useContext } = plugin;
12
- provide({
13
- name: "telegram",
14
- description: "Telegram Bot Adapter",
15
- mounted: async (p) => {
16
- const adapter = new TelegramAdapter(p);
17
- await adapter.start();
18
- return adapter;
19
- },
20
- dispose: async (adapter) => {
21
- await adapter.stop();
22
- },
23
- });
24
- useContext('tool', 'telegram', (toolService, telegram) => {
25
- const groupTools = createGroupManagementTools(telegram, 'telegram');
26
- const disposers = groupTools.map(t => toolService.addTool(t, plugin.name));
27
- disposers.push(toolService.addTool({
28
- name: 'telegram_pin_message',
29
- description: '置顶 Telegram 群组消息',
30
- parameters: {
31
- type: 'object',
32
- properties: {
33
- bot: { type: 'string', description: 'Bot 名称' },
34
- chat_id: { type: 'string', description: '聊天 ID' },
35
- message_id: { type: 'string', description: '消息 ID' },
36
- },
37
- required: ['bot', 'chat_id', 'message_id'],
38
- },
39
- platforms: ['telegram'],
40
- tags: ['telegram'],
41
- execute: async (args) => {
42
- const bot = telegram.bots.get(args.bot);
43
- if (!bot)
44
- throw new Error(`Bot ${args.bot} 不存在`);
45
- const success = await bot.pinMessage(Number(args.chat_id), Number(args.message_id));
46
- return { success, message: success ? '消息已置顶' : '操作失败' };
47
- },
48
- }, plugin.name));
49
- disposers.push(toolService.addTool({
50
- name: 'telegram_unpin_message',
51
- description: '取消置顶 Telegram 群组消息',
52
- parameters: {
53
- type: 'object',
54
- properties: {
55
- bot: { type: 'string', description: 'Bot 名称' },
56
- chat_id: { type: 'string', description: '聊天 ID' },
57
- message_id: { type: 'string', description: '消息 ID(可选,不提供则取消所有置顶)' },
58
- },
59
- required: ['bot', 'chat_id'],
60
- },
61
- platforms: ['telegram'],
62
- tags: ['telegram'],
63
- execute: async (args) => {
64
- const bot = telegram.bots.get(args.bot);
65
- if (!bot)
66
- throw new Error(`Bot ${args.bot} 不存在`);
67
- const success = await bot.unpinMessage(Number(args.chat_id), args.message_id ? Number(args.message_id) : undefined);
68
- return { success, message: success ? '已取消置顶' : '操作失败' };
69
- },
70
- }, plugin.name));
71
- disposers.push(toolService.addTool({
72
- name: 'telegram_list_admins',
73
- description: '获取 Telegram 群组管理员列表',
74
- parameters: {
75
- type: 'object',
76
- properties: {
77
- bot: { type: 'string', description: 'Bot 名称' },
78
- chat_id: { type: 'string', description: '聊天 ID' },
79
- },
80
- required: ['bot', 'chat_id'],
81
- },
82
- platforms: ['telegram'],
83
- tags: ['telegram'],
84
- execute: async (args) => {
85
- const bot = telegram.bots.get(args.bot);
86
- if (!bot)
87
- throw new Error(`Bot ${args.bot} 不存在`);
88
- const admins = await bot.getChatAdmins(Number(args.chat_id));
89
- return {
90
- admins: admins.map((a) => ({
91
- user_id: a.user.id,
92
- username: a.user.username,
93
- first_name: a.user.first_name,
94
- status: a.status,
95
- })),
96
- count: admins.length,
97
- };
98
- },
99
- }, plugin.name));
100
- disposers.push(toolService.addTool({
101
- name: 'telegram_member_count',
102
- description: '获取 Telegram 群组成员数量',
103
- parameters: {
104
- type: 'object',
105
- properties: {
106
- bot: { type: 'string', description: 'Bot 名称' },
107
- chat_id: { type: 'string', description: '聊天 ID' },
108
- },
109
- required: ['bot', 'chat_id'],
110
- },
111
- platforms: ['telegram'],
112
- tags: ['telegram'],
113
- execute: async (args) => {
114
- const bot = telegram.bots.get(args.bot);
115
- if (!bot)
116
- throw new Error(`Bot ${args.bot} 不存在`);
117
- const count = await bot.getChatMemberCount(Number(args.chat_id));
118
- return { count, message: `群组共有 ${count} 名成员` };
119
- },
120
- }, plugin.name));
121
- disposers.push(toolService.addTool({
122
- name: 'telegram_create_invite',
123
- description: '创建 Telegram 群组邀请链接',
124
- parameters: {
125
- type: 'object',
126
- properties: {
127
- bot: { type: 'string', description: 'Bot 名称' },
128
- chat_id: { type: 'string', description: '聊天 ID' },
129
- },
130
- required: ['bot', 'chat_id'],
131
- },
132
- platforms: ['telegram'],
133
- tags: ['telegram'],
134
- execute: async (args) => {
135
- const bot = telegram.bots.get(args.bot);
136
- if (!bot)
137
- throw new Error(`Bot ${args.bot} 不存在`);
138
- const link = await bot.createInviteLink(Number(args.chat_id));
139
- return { invite_link: link, message: `邀请链接: ${link}` };
140
- },
141
- }, plugin.name));
142
- disposers.push(toolService.addTool({
143
- name: 'telegram_send_poll',
144
- description: '在 Telegram 群组中发起投票',
145
- parameters: {
146
- type: 'object',
147
- properties: {
148
- bot: { type: 'string', description: 'Bot 名称' },
149
- chat_id: { type: 'string', description: '聊天 ID' },
150
- question: { type: 'string', description: '投票问题' },
151
- options: { type: 'string', description: '选项 JSON 数组,如 ["A","B","C"]' },
152
- is_anonymous: { type: 'boolean', description: '是否匿名投票,默认 true' },
153
- allows_multiple: { type: 'boolean', description: '是否允许多选,默认 false' },
154
- },
155
- required: ['bot', 'chat_id', 'question', 'options'],
156
- },
157
- platforms: ['telegram'],
158
- tags: ['telegram'],
159
- execute: async (args) => {
160
- const bot = telegram.bots.get(args.bot);
161
- if (!bot)
162
- throw new Error(`Bot ${args.bot} 不存在`);
163
- let optList;
164
- try {
165
- optList = JSON.parse(args.options);
166
- }
167
- catch {
168
- return { success: false, message: 'options 格式错误,应为 JSON 数组' };
169
- }
170
- if (!Array.isArray(optList) || optList.length < 2) {
171
- return { success: false, message: '至少需要 2 个选项' };
172
- }
173
- const result = await bot.sendPoll(Number(args.chat_id), args.question, optList, args.is_anonymous ?? true, args.allows_multiple ?? false);
174
- return { success: true, message_id: result.message_id, message: '投票已发送' };
175
- },
176
- }, plugin.name));
177
- disposers.push(toolService.addTool({
178
- name: 'telegram_react',
179
- description: '对 Telegram 消息添加表情反应',
180
- parameters: {
181
- type: 'object',
182
- properties: {
183
- bot: { type: 'string', description: 'Bot 名称' },
184
- chat_id: { type: 'string', description: '聊天 ID' },
185
- message_id: { type: 'string', description: '消息 ID' },
186
- reaction: { type: 'string', description: '反应表情(如 👍、❤️、🔥)' },
187
- },
188
- required: ['bot', 'chat_id', 'message_id', 'reaction'],
189
- },
190
- platforms: ['telegram'],
191
- tags: ['telegram'],
192
- execute: async (args) => {
193
- const bot = telegram.bots.get(args.bot);
194
- if (!bot)
195
- throw new Error(`Bot ${args.bot} 不存在`);
196
- const success = await bot.setMessageReaction(Number(args.chat_id), Number(args.message_id), args.reaction);
197
- return { success, message: success ? `已添加反应 ${args.reaction}` : '操作失败' };
198
- },
199
- }, plugin.name));
200
- disposers.push(toolService.addTool({
201
- name: 'telegram_send_sticker',
202
- description: '发送 Telegram 贴纸',
203
- parameters: {
204
- type: 'object',
205
- properties: {
206
- bot: { type: 'string', description: 'Bot 名称' },
207
- chat_id: { type: 'string', description: '聊天 ID' },
208
- sticker: { type: 'string', description: '贴纸 file_id 或 URL' },
209
- },
210
- required: ['bot', 'chat_id', 'sticker'],
211
- },
212
- platforms: ['telegram'],
213
- tags: ['telegram'],
214
- execute: async (args) => {
215
- const bot = telegram.bots.get(args.bot);
216
- if (!bot)
217
- throw new Error(`Bot ${args.bot} 不存在`);
218
- const result = await bot.sendStickerMessage(Number(args.chat_id), args.sticker);
219
- return { success: true, message_id: result.message_id, message: '贴纸已发送' };
220
- },
221
- }, plugin.name));
222
- disposers.push(toolService.addTool({
223
- name: 'telegram_set_permissions',
224
- description: '设置 Telegram 群组的默认成员权限',
225
- parameters: {
226
- type: 'object',
227
- properties: {
228
- bot: { type: 'string', description: 'Bot 名称' },
229
- chat_id: { type: 'string', description: '聊天 ID' },
230
- can_send_messages: { type: 'boolean', description: '是否可以发消息' },
231
- can_send_photos: { type: 'boolean', description: '是否可以发图片' },
232
- can_send_videos: { type: 'boolean', description: '是否可以发视频' },
233
- can_send_polls: { type: 'boolean', description: '是否可以发投票' },
234
- can_send_other_messages: { type: 'boolean', description: '是否可以发贴纸/GIF等' },
235
- can_add_web_page_previews: { type: 'boolean', description: '是否可以添加网页预览' },
236
- can_change_info: { type: 'boolean', description: '是否可以改群信息' },
237
- can_invite_users: { type: 'boolean', description: '是否可以邀请用户' },
238
- can_pin_messages: { type: 'boolean', description: '是否可以置顶消息' },
239
- },
240
- required: ['bot', 'chat_id'],
241
- },
242
- platforms: ['telegram'],
243
- tags: ['telegram'],
244
- execute: async (args) => {
245
- const { bot: botId, chat_id, ...perms } = args;
246
- const bot = telegram.bots.get(botId);
247
- if (!bot)
248
- throw new Error(`Bot ${botId} 不存在`);
249
- const permissions = {};
250
- for (const [k, v] of Object.entries(perms)) {
251
- if (typeof v === 'boolean')
252
- permissions[k] = v;
253
- }
254
- const success = await bot.setChatPermissionsAll(Number(chat_id), permissions);
255
- return { success, message: success ? '群权限已更新' : '操作失败' };
256
- },
257
- }, plugin.name));
258
- disposers.push(toolService.addTool({
259
- name: 'telegram_set_description',
260
- description: '设置 Telegram 群组描述',
261
- parameters: {
262
- type: 'object',
263
- properties: {
264
- bot: { type: 'string', description: 'Bot 名称' },
265
- chat_id: { type: 'string', description: '聊天 ID' },
266
- description: { type: 'string', description: '群描述文字' },
267
- },
268
- required: ['bot', 'chat_id', 'description'],
269
- },
270
- platforms: ['telegram'],
271
- tags: ['telegram'],
272
- execute: async (args) => {
273
- const bot = telegram.bots.get(args.bot);
274
- if (!bot)
275
- throw new Error(`Bot ${args.bot} 不存在`);
276
- const success = await bot.setChatDescription(Number(args.chat_id), args.description);
277
- return { success, message: success ? '群描述已更新' : '操作失败' };
278
- },
279
- }, plugin.name));
280
- return () => disposers.forEach(d => d());
281
- });
282
- // ── Web 控制台 ─────────────────────────────────────────────────────────
283
- useContext("web", (pageManager) => {
284
- pageManager.addEntry({
285
- id: "telegram",
286
- development: path.resolve(import.meta.dirname, "../client/index.tsx"),
287
- production: path.resolve(import.meta.dirname, "../dist/index.js"),
288
- meta: { name: "Telegram" },
289
- });
290
- });
291
- useContext("router", "telegram", (router, telegram) => {
292
- router.get("/api/telegram/bots", async (ctx) => {
293
- try {
294
- const bots = Array.from(telegram.bots.values());
295
- const result = bots.map((bot) => {
296
- try {
297
- return {
298
- name: bot.$config.name,
299
- connected: bot.$connected || false,
300
- mode: bot.$config.polling !== false ? "polling" : "webhook",
301
- status: bot.$connected ? "online" : "offline",
302
- botInfo: bot.botInfo ? { username: bot.botInfo.username, firstName: bot.botInfo.first_name } : null,
303
- };
304
- }
305
- catch {
306
- return { name: bot.$config.name, connected: false, mode: "unknown", status: "error", botInfo: null };
307
- }
308
- });
309
- ctx.body = { success: true, data: result };
310
- }
311
- catch {
312
- ctx.status = 500;
313
- ctx.body = { success: false, error: "获取机器人数据失败" };
314
- }
315
- });
316
- // Bot 连接/断开
317
- router.post("/api/telegram/bots/:name/connect", async (ctx) => {
318
- try {
319
- const bot = telegram.bots.get(ctx.params.name);
320
- if (!bot) {
321
- ctx.status = 404;
322
- ctx.body = { success: false, error: "Bot 不存在" };
323
- return;
324
- }
325
- if (bot.$connected) {
326
- ctx.body = { success: true, message: "已经在线" };
327
- return;
328
- }
329
- await bot.$connect();
330
- ctx.body = { success: true, message: "连接成功" };
331
- }
332
- catch (e) {
333
- ctx.status = 500;
334
- ctx.body = { success: false, error: e?.message || "连接失败" };
335
- }
336
- });
337
- router.post("/api/telegram/bots/:name/disconnect", async (ctx) => {
338
- try {
339
- const bot = telegram.bots.get(ctx.params.name);
340
- if (!bot) {
341
- ctx.status = 404;
342
- ctx.body = { success: false, error: "Bot 不存在" };
343
- return;
344
- }
345
- if (!bot.$connected) {
346
- ctx.body = { success: true, message: "已经离线" };
347
- return;
348
- }
349
- await bot.$disconnect();
350
- ctx.body = { success: true, message: "已断开" };
351
- }
352
- catch (e) {
353
- ctx.status = 500;
354
- ctx.body = { success: false, error: e?.message || "断开失败" };
355
- }
356
- });
357
- // 快捷操作:创建邀请链接
358
- router.post("/api/telegram/bots/:name/invite", async (ctx) => {
359
- try {
360
- const bot = telegram.bots.get(ctx.params.name);
361
- if (!bot) {
362
- ctx.status = 404;
363
- ctx.body = { success: false, error: "Bot 不存在" };
364
- return;
365
- }
366
- if (!bot.$connected) {
367
- ctx.status = 400;
368
- ctx.body = { success: false, error: "Bot 未连接" };
369
- return;
370
- }
371
- const { chat_id } = ctx.request.body || {};
372
- if (!chat_id) {
373
- ctx.status = 400;
374
- ctx.body = { success: false, error: "缺少 chat_id" };
375
- return;
376
- }
377
- const link = await bot.createInviteLink(Number(chat_id));
378
- ctx.body = { success: true, data: { invite_link: link } };
379
- }
380
- catch (e) {
381
- ctx.status = 500;
382
- ctx.body = { success: false, error: e?.message || "创建邀请链接失败" };
383
- }
384
- });
385
- // 快捷操作:发起投票
386
- router.post("/api/telegram/bots/:name/poll", async (ctx) => {
387
- try {
388
- const bot = telegram.bots.get(ctx.params.name);
389
- if (!bot) {
390
- ctx.status = 404;
391
- ctx.body = { success: false, error: "Bot 不存在" };
392
- return;
393
- }
394
- if (!bot.$connected) {
395
- ctx.status = 400;
396
- ctx.body = { success: false, error: "Bot 未连接" };
397
- return;
398
- }
399
- const { chat_id, question, options, is_anonymous, allows_multiple } = ctx.request.body || {};
400
- if (!chat_id || !question || !options?.length) {
401
- ctx.status = 400;
402
- ctx.body = { success: false, error: "缺少 chat_id, question 或 options" };
403
- return;
404
- }
405
- if (options.length < 2) {
406
- ctx.status = 400;
407
- ctx.body = { success: false, error: "至少需要 2 个选项" };
408
- return;
409
- }
410
- const result = await bot.sendPoll(Number(chat_id), question, options, is_anonymous ?? true, allows_multiple ?? false);
411
- ctx.body = { success: true, data: { message_id: result.message_id } };
412
- }
413
- catch (e) {
414
- ctx.status = 500;
415
- ctx.body = { success: false, error: e?.message || "发起投票失败" };
416
- }
417
- });
418
- // 快捷操作:获取群管理员
419
- router.get("/api/telegram/bots/:name/admins", async (ctx) => {
420
- try {
421
- const bot = telegram.bots.get(ctx.params.name);
422
- if (!bot) {
423
- ctx.status = 404;
424
- ctx.body = { success: false, error: "Bot 不存在" };
425
- return;
426
- }
427
- if (!bot.$connected) {
428
- ctx.status = 400;
429
- ctx.body = { success: false, error: "Bot 未连接" };
430
- return;
431
- }
432
- const chat_id = ctx.query.chat_id;
433
- if (!chat_id) {
434
- ctx.status = 400;
435
- ctx.body = { success: false, error: "缺少 chat_id" };
436
- return;
437
- }
438
- const admins = await bot.getChatAdmins(Number(chat_id));
439
- ctx.body = {
440
- success: true,
441
- data: admins.map((a) => ({
442
- user_id: a.user.id,
443
- username: a.user.username,
444
- first_name: a.user.first_name,
445
- status: a.status,
446
- })),
447
- };
448
- }
449
- catch (e) {
450
- ctx.status = 500;
451
- ctx.body = { success: false, error: e?.message || "获取管理员失败" };
452
- }
453
- });
454
- });
455
- //# sourceMappingURL=index.js.map
1
+ export { TelegramClient, TelegramEndpoint, } from './endpoint.js';
2
+ export { botApiUrl, formatCallbackContent, formatInboundContent, formatOutboundActions, formatOutboundPlan, normalizeWebhookPath, resolveTelegramConfig, senderDisplayName, } from './protocol.js';
3
+ export { telegramClient } from './client.js';
4
+ export { checkTelegramPlatformPermit, normalizeTelegramChatMember, platformPermit, telegramGroupPermitResolver, } from './platform-permit.js';
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Canonical Markdown → Telegram Bot API HTML.
3
+ *
4
+ * Telegram's MarkdownV2 is not CommonMark-compatible and rejects unescaped
5
+ * punctuation. HTML gives the adapter a safer dialect seam: user text is
6
+ * escaped first and only the subset supported by Bot API is emitted as tags.
7
+ */
8
+ export declare function markdownToTelegramHtml(markdown: string): string;
9
+ export declare function escapeTelegramHtml(value: string): string;
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Canonical Markdown → Telegram Bot API HTML.
3
+ *
4
+ * Telegram's MarkdownV2 is not CommonMark-compatible and rejects unescaped
5
+ * punctuation. HTML gives the adapter a safer dialect seam: user text is
6
+ * escaped first and only the subset supported by Bot API is emitted as tags.
7
+ */
8
+ export function markdownToTelegramHtml(markdown) {
9
+ const protectedFragments = [];
10
+ const protect = (html) => {
11
+ const token = `\uE000${protectedFragments.length}\uE001`;
12
+ protectedFragments.push(html);
13
+ return token;
14
+ };
15
+ let value = protectTelegramFencedCode(markdown, protect);
16
+ value = value.replace(/`([^`\n]+)`/g, (_match, code) => (protect(`<code>${escapeTelegramHtml(String(code))}</code>`)));
17
+ value = escapeTelegramHtml(value);
18
+ value = value
19
+ .replace(/!\[([^\[\]]*)\]\(([^()\s]+)(?:\s+&quot;[^&]*&quot;)?\)/g, '$1')
20
+ .replace(/\[([^\[\]]+)\]\(([^()\s]+)(?:\s+&quot;[^&]*&quot;)?\)/g, (_match, label, href) => isSafeTelegramHref(href)
21
+ ? `<a href="${href}">${label}</a>`
22
+ : label)
23
+ .replace(/^#{1,6}\s+(.+)$/gm, '<b>$1</b>')
24
+ .replace(/^&gt;\s?(.*)$/gm, '<blockquote>$1</blockquote>')
25
+ .replace(/^\s*[-+*]\s+(.+)$/gm, '• $1')
26
+ .replace(/\*\*([^*\n]+)\*\*/g, '<b>$1</b>')
27
+ .replace(/__([^_\n]+)__/g, '<b>$1</b>')
28
+ .replace(/~~([^~\n]+)~~/g, '<s>$1</s>')
29
+ .replace(/(^|[^*])\*([^*\n]+)\*/g, '$1<i>$2</i>')
30
+ .replace(/(^|[^_])_([^_\n]+)_/g, '$1<i>$2</i>');
31
+ for (let index = 0; index < protectedFragments.length; index += 1) {
32
+ value = value.replace(`\uE000${index}\uE001`, protectedFragments[index]);
33
+ }
34
+ return value;
35
+ }
36
+ function protectTelegramFencedCode(markdown, protect) {
37
+ const chunks = [];
38
+ let cursor = 0;
39
+ while (cursor < markdown.length) {
40
+ const opening = markdown.indexOf('```', cursor);
41
+ if (opening < 0) {
42
+ chunks.push(markdown.slice(cursor));
43
+ break;
44
+ }
45
+ chunks.push(markdown.slice(cursor, opening));
46
+ const contentStart = opening + 3;
47
+ const closing = markdown.indexOf('```', contentStart);
48
+ if (closing < 0) {
49
+ chunks.push(markdown.slice(opening));
50
+ break;
51
+ }
52
+ const newline = markdown.indexOf('\n', contentStart);
53
+ const hasHeader = newline >= 0 && newline < closing;
54
+ const language = markdown.slice(contentStart, hasHeader ? newline : closing).trim();
55
+ let code = hasHeader ? markdown.slice(newline + 1, closing) : '';
56
+ if (code.endsWith('\n'))
57
+ code = code.slice(0, -1);
58
+ const escapedCode = escapeTelegramHtml(code);
59
+ chunks.push(/^[A-Za-z0-9_+-]+$/.test(language)
60
+ ? protect(`<pre><code class="language-${language}">${escapedCode}</code></pre>`)
61
+ : protect(`<pre>${escapedCode}</pre>`));
62
+ cursor = closing + 3;
63
+ }
64
+ return chunks.join('');
65
+ }
66
+ function isSafeTelegramHref(value) {
67
+ return /^(?:https?:\/\/|tg:\/\/|mailto:)[^\s<>]+$/iu.test(value);
68
+ }
69
+ export function escapeTelegramHtml(value) {
70
+ return value
71
+ .replace(/&/g, '&amp;')
72
+ .replace(/</g, '&lt;')
73
+ .replace(/>/g, '&gt;')
74
+ .replace(/"/g, '&quot;');
75
+ }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Telegram platform permit — ChatMember 状态 + 细粒度权限
3
+ */
4
+ import type { PermissionSubject } from '@zhin.js/permission';
5
+ export declare function platformPermit(perm: string): string;
6
+ export declare function telegramGroupPermitResolver(logicalPerm: string): string;
7
+ export declare function normalizeTelegramChatMember(member: {
8
+ status?: string;
9
+ can_restrict_members?: boolean;
10
+ can_pin_messages?: boolean;
11
+ can_delete_messages?: boolean;
12
+ can_manage_chat?: boolean;
13
+ }): {
14
+ role?: string;
15
+ permissions: string[];
16
+ };
17
+ export declare function checkTelegramPlatformPermit(perm: string, subject: PermissionSubject): boolean;
@@ -0,0 +1,51 @@
1
+ const ADAPTER = 'telegram';
2
+ export function platformPermit(perm) {
3
+ return `platform(${ADAPTER},${perm})`;
4
+ }
5
+ const FACTORY_PERM_MAP = {
6
+ scene_admin: 'chat_administrator',
7
+ scene_owner: 'chat_creator',
8
+ };
9
+ export function telegramGroupPermitResolver(logicalPerm) {
10
+ return platformPermit(FACTORY_PERM_MAP[logicalPerm] ?? logicalPerm);
11
+ }
12
+ export function normalizeTelegramChatMember(member) {
13
+ const permissions = [];
14
+ const status = member.status;
15
+ if (status === 'creator') {
16
+ permissions.push('chat_creator', 'chat_administrator', 'restrict_members', 'pin_messages');
17
+ return { role: 'creator', permissions };
18
+ }
19
+ if (status === 'administrator') {
20
+ permissions.push('chat_administrator');
21
+ if (member.can_restrict_members)
22
+ permissions.push('restrict_members');
23
+ if (member.can_pin_messages)
24
+ permissions.push('pin_messages');
25
+ if (member.can_delete_messages)
26
+ permissions.push('delete_messages');
27
+ if (member.can_manage_chat)
28
+ permissions.push('manage_chat');
29
+ return { role: 'administrator', permissions };
30
+ }
31
+ return { role: 'member', permissions };
32
+ }
33
+ export function checkTelegramPlatformPermit(perm, subject) {
34
+ const role = subject.sender?.role?.[0];
35
+ const permissions = subject.sender?.permissions ?? [];
36
+ const has = (t) => permissions.includes(t);
37
+ switch (perm) {
38
+ case 'chat_creator':
39
+ return role === 'creator' || has('chat_creator');
40
+ case 'chat_administrator':
41
+ return role === 'creator' || role === 'administrator' || has('chat_administrator');
42
+ case 'restrict_members':
43
+ return has('restrict_members') || role === 'creator';
44
+ case 'pin_messages':
45
+ return has('pin_messages') || role === 'creator';
46
+ case 'manage_chat':
47
+ return has('manage_chat') || role === 'creator';
48
+ default:
49
+ return false;
50
+ }
51
+ }
@@ -0,0 +1,9 @@
1
+ import type { TelegramUpdate } from './protocol.js';
2
+ export interface TelegramPollingHost {
3
+ readonly allowedUpdates: readonly string[];
4
+ callApi<T>(method: string, params?: Record<string, unknown>, signal?: AbortSignal): Promise<T>;
5
+ getUpdateOffset(): number;
6
+ setUpdateOffset(offset: number): void;
7
+ handleUpdate(update: TelegramUpdate): void;
8
+ }
9
+ export declare function runTelegramPollLoop(host: TelegramPollingHost, abortSignal: AbortSignal): Promise<void>;
package/lib/polling.js ADDED
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Telegram long-polling loop for getUpdates.
3
+ */
4
+ import { formatCompact, getLogger } from '@zhin.js/logger';
5
+ const logger = getLogger('telegram');
6
+ const RETRY_DELAY_MS = 2_000;
7
+ const BACKOFF_DELAY_MS = 10_000;
8
+ const MAX_CONSECUTIVE_FAILURES = 5;
9
+ const DEFAULT_POLL_TIMEOUT_SEC = 30;
10
+ export async function runTelegramPollLoop(host, abortSignal) {
11
+ let consecutiveFailures = 0;
12
+ while (!abortSignal.aborted) {
13
+ try {
14
+ const updates = await host.callApi('getUpdates', {
15
+ offset: host.getUpdateOffset() || undefined,
16
+ timeout: DEFAULT_POLL_TIMEOUT_SEC,
17
+ allowed_updates: host.allowedUpdates,
18
+ }, abortSignal);
19
+ consecutiveFailures = 0;
20
+ for (const update of updates) {
21
+ // A transport may resolve after cancellation; never admit that late batch.
22
+ if (abortSignal.aborted)
23
+ return;
24
+ host.setUpdateOffset(update.update_id + 1);
25
+ host.handleUpdate(update);
26
+ }
27
+ }
28
+ catch (err) {
29
+ if (abortSignal.aborted)
30
+ return;
31
+ consecutiveFailures += 1;
32
+ logger.error(formatCompact({
33
+ op: 'poll',
34
+ ok: false,
35
+ error: err instanceof Error ? err.message : String(err),
36
+ }));
37
+ // 不在退避后清零:对端持续挂时清零会让重试固定打满 RETRY_DELAY_MS,
38
+ // 保持计数才能让 BACKOFF_DELAY_MS 持续生效(成功时上面才清零)。
39
+ await sleep(consecutiveFailures >= MAX_CONSECUTIVE_FAILURES ? BACKOFF_DELAY_MS : RETRY_DELAY_MS, abortSignal);
40
+ }
41
+ }
42
+ }
43
+ function sleep(ms, signal) {
44
+ return new Promise((resolve) => {
45
+ if (signal?.aborted) {
46
+ resolve();
47
+ return;
48
+ }
49
+ const finish = () => {
50
+ clearTimeout(timer);
51
+ signal?.removeEventListener('abort', finish);
52
+ resolve();
53
+ };
54
+ const timer = setTimeout(finish, ms);
55
+ signal?.addEventListener('abort', finish, { once: true });
56
+ });
57
+ }