@foxden-app/foxclaw 0.2.0

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 (126) hide show
  1. package/.env.example +36 -0
  2. package/LICENSE +22 -0
  3. package/README.md +244 -0
  4. package/README_EN.md +244 -0
  5. package/dist/channels/bridge_messaging_router.d.ts +27 -0
  6. package/dist/channels/bridge_messaging_router.js +85 -0
  7. package/dist/channels/telegram/telegram_channel_adapter.d.ts +12 -0
  8. package/dist/channels/telegram/telegram_channel_adapter.js +21 -0
  9. package/dist/channels/telegram/telegram_messaging_port.d.ts +25 -0
  10. package/dist/channels/telegram/telegram_messaging_port.js +51 -0
  11. package/dist/channels/weixin/account_store.d.ts +15 -0
  12. package/dist/channels/weixin/account_store.js +54 -0
  13. package/dist/channels/weixin/ilink/aes_ecb.d.ts +3 -0
  14. package/dist/channels/weixin/ilink/aes_ecb.js +12 -0
  15. package/dist/channels/weixin/ilink/api.d.ts +44 -0
  16. package/dist/channels/weixin/ilink/api.js +187 -0
  17. package/dist/channels/weixin/ilink/cdn_upload.d.ts +11 -0
  18. package/dist/channels/weixin/ilink/cdn_upload.js +60 -0
  19. package/dist/channels/weixin/ilink/cdn_url.d.ts +7 -0
  20. package/dist/channels/weixin/ilink/cdn_url.js +7 -0
  21. package/dist/channels/weixin/ilink/constants.d.ts +7 -0
  22. package/dist/channels/weixin/ilink/constants.js +27 -0
  23. package/dist/channels/weixin/ilink/context.d.ts +13 -0
  24. package/dist/channels/weixin/ilink/context.js +13 -0
  25. package/dist/channels/weixin/ilink/login_qr.d.ts +34 -0
  26. package/dist/channels/weixin/ilink/login_qr.js +233 -0
  27. package/dist/channels/weixin/ilink/media_image.d.ts +11 -0
  28. package/dist/channels/weixin/ilink/media_image.js +44 -0
  29. package/dist/channels/weixin/ilink/mime.d.ts +3 -0
  30. package/dist/channels/weixin/ilink/mime.js +36 -0
  31. package/dist/channels/weixin/ilink/pic_decrypt.d.ts +2 -0
  32. package/dist/channels/weixin/ilink/pic_decrypt.js +56 -0
  33. package/dist/channels/weixin/ilink/random.d.ts +2 -0
  34. package/dist/channels/weixin/ilink/random.js +7 -0
  35. package/dist/channels/weixin/ilink/redact.d.ts +4 -0
  36. package/dist/channels/weixin/ilink/redact.js +34 -0
  37. package/dist/channels/weixin/ilink/runtime_attach.d.ts +3 -0
  38. package/dist/channels/weixin/ilink/runtime_attach.js +13 -0
  39. package/dist/channels/weixin/ilink/send.d.ts +21 -0
  40. package/dist/channels/weixin/ilink/send.js +108 -0
  41. package/dist/channels/weixin/ilink/session_guard.d.ts +6 -0
  42. package/dist/channels/weixin/ilink/session_guard.js +39 -0
  43. package/dist/channels/weixin/ilink/types.d.ts +155 -0
  44. package/dist/channels/weixin/ilink/types.js +10 -0
  45. package/dist/channels/weixin/ilink/upload.d.ts +15 -0
  46. package/dist/channels/weixin/ilink/upload.js +75 -0
  47. package/dist/channels/weixin/sync_buf_store.d.ts +3 -0
  48. package/dist/channels/weixin/sync_buf_store.js +19 -0
  49. package/dist/channels/weixin/weixin_channel_adapter.d.ts +18 -0
  50. package/dist/channels/weixin/weixin_channel_adapter.js +273 -0
  51. package/dist/channels/weixin/weixin_messaging_port.d.ts +29 -0
  52. package/dist/channels/weixin/weixin_messaging_port.js +113 -0
  53. package/dist/codex_app/client.d.ts +176 -0
  54. package/dist/codex_app/client.js +1230 -0
  55. package/dist/codex_app/deeplink.d.ts +7 -0
  56. package/dist/codex_app/deeplink.js +29 -0
  57. package/dist/codex_app/local_usage.d.ts +16 -0
  58. package/dist/codex_app/local_usage.js +123 -0
  59. package/dist/config.d.ts +44 -0
  60. package/dist/config.js +131 -0
  61. package/dist/controller/access.d.ts +11 -0
  62. package/dist/controller/access.js +33 -0
  63. package/dist/controller/activity.d.ts +62 -0
  64. package/dist/controller/activity.js +330 -0
  65. package/dist/controller/commands.d.ts +6 -0
  66. package/dist/controller/commands.js +17 -0
  67. package/dist/controller/controller.d.ts +326 -0
  68. package/dist/controller/controller.js +7503 -0
  69. package/dist/controller/observer.d.ts +16 -0
  70. package/dist/controller/observer.js +98 -0
  71. package/dist/controller/presentation.d.ts +80 -0
  72. package/dist/controller/presentation.js +568 -0
  73. package/dist/controller/service_tier.d.ts +9 -0
  74. package/dist/controller/service_tier.js +32 -0
  75. package/dist/controller/session_observer.d.ts +22 -0
  76. package/dist/controller/session_observer.js +259 -0
  77. package/dist/controller/status.d.ts +10 -0
  78. package/dist/controller/status.js +28 -0
  79. package/dist/core/bridge_scope.d.ts +18 -0
  80. package/dist/core/bridge_scope.js +46 -0
  81. package/dist/core/channel_port.d.ts +15 -0
  82. package/dist/core/channel_port.js +1 -0
  83. package/dist/i18n.d.ts +1108 -0
  84. package/dist/i18n.js +1154 -0
  85. package/dist/lock.d.ts +7 -0
  86. package/dist/lock.js +80 -0
  87. package/dist/logger.d.ts +12 -0
  88. package/dist/logger.js +57 -0
  89. package/dist/main.d.ts +2 -0
  90. package/dist/main.js +236 -0
  91. package/dist/runtime.d.ts +3 -0
  92. package/dist/runtime.js +14 -0
  93. package/dist/store/database.d.ts +79 -0
  94. package/dist/store/database.js +489 -0
  95. package/dist/store/migrate_bridge_scope.d.ts +6 -0
  96. package/dist/store/migrate_bridge_scope.js +59 -0
  97. package/dist/telegram/addressing.d.ts +33 -0
  98. package/dist/telegram/addressing.js +57 -0
  99. package/dist/telegram/api.d.ts +14 -0
  100. package/dist/telegram/api.js +89 -0
  101. package/dist/telegram/gateway.d.ts +76 -0
  102. package/dist/telegram/gateway.js +383 -0
  103. package/dist/telegram/media.d.ts +34 -0
  104. package/dist/telegram/media.js +180 -0
  105. package/dist/telegram/rendering.d.ts +10 -0
  106. package/dist/telegram/rendering.js +21 -0
  107. package/dist/telegram/scope.d.ts +6 -0
  108. package/dist/telegram/scope.js +24 -0
  109. package/dist/telegram/text.d.ts +7 -0
  110. package/dist/telegram/text.js +47 -0
  111. package/dist/types.d.ts +343 -0
  112. package/dist/types.js +1 -0
  113. package/docs/agent-assisted-install.md +84 -0
  114. package/docs/install-for-beginners.md +287 -0
  115. package/docs/troubleshooting.md +239 -0
  116. package/package.json +62 -0
  117. package/scripts/doctor.sh +3 -0
  118. package/scripts/launchd/install.sh +54 -0
  119. package/scripts/status.sh +3 -0
  120. package/scripts/systemd/install.sh +83 -0
  121. package/scripts/systemd/uninstall.sh +15 -0
  122. package/skills/foxclaw/SKILL.md +167 -0
  123. package/skills/foxclaw/agents/openai.yaml +4 -0
  124. package/skills/foxclaw/references/telegram-setup.md +93 -0
  125. package/skills/foxclaw/scripts/bootstrap_host.py +350 -0
  126. package/skills/foxclaw/scripts/bootstrap_remote.py +67 -0
@@ -0,0 +1,89 @@
1
+ import fs from 'node:fs';
2
+ import https from 'node:https';
3
+ import path from 'node:path';
4
+ import { pipeline } from 'node:stream/promises';
5
+ const API_HOST = 'api.telegram.org';
6
+ export async function callTelegramApi(botToken, method, body) {
7
+ const payload = JSON.stringify(body);
8
+ return new Promise((resolve, reject) => {
9
+ const request = https.request({
10
+ host: API_HOST,
11
+ port: 443,
12
+ path: `/bot${botToken}/${method}`,
13
+ method: 'POST',
14
+ family: 4,
15
+ headers: {
16
+ 'content-type': 'application/json',
17
+ 'content-length': Buffer.byteLength(payload),
18
+ },
19
+ }, (response) => {
20
+ const chunks = [];
21
+ response.on('data', (chunk) => {
22
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
23
+ });
24
+ response.on('end', () => {
25
+ try {
26
+ const text = Buffer.concat(chunks).toString('utf8');
27
+ resolve(JSON.parse(text));
28
+ }
29
+ catch (error) {
30
+ reject(new Error(`Failed to parse Telegram response: ${String(error)}`));
31
+ }
32
+ });
33
+ });
34
+ request.on('error', reject);
35
+ request.setTimeout(20_000, () => {
36
+ request.destroy(new Error(`Telegram API request timed out for ${method}`));
37
+ });
38
+ request.write(payload);
39
+ request.end();
40
+ });
41
+ }
42
+ export async function getTelegramFile(botToken, fileId) {
43
+ const result = await callTelegramApi(botToken, 'getFile', { file_id: fileId });
44
+ if (!result.ok || !result.result) {
45
+ throw new Error(result.description || `Failed to resolve Telegram file ${fileId}`);
46
+ }
47
+ return result.result;
48
+ }
49
+ export async function downloadTelegramFile(botToken, remoteFilePath, destinationPath) {
50
+ await fs.promises.mkdir(path.dirname(destinationPath), { recursive: true });
51
+ const tempPath = `${destinationPath}.tmp-${process.pid}-${Date.now()}`;
52
+ let response = null;
53
+ try {
54
+ response = await new Promise((resolve, reject) => {
55
+ const request = https.get({
56
+ host: API_HOST,
57
+ port: 443,
58
+ path: `/file/bot${botToken}/${remoteFilePath}`,
59
+ family: 4,
60
+ }, (incoming) => {
61
+ const statusCode = incoming.statusCode ?? 500;
62
+ if (statusCode >= 400) {
63
+ incoming.resume();
64
+ reject(new Error(`Telegram file download failed with status ${statusCode}`));
65
+ return;
66
+ }
67
+ resolve(incoming);
68
+ });
69
+ request.on('error', reject);
70
+ request.setTimeout(20_000, () => {
71
+ request.destroy(new Error(`Telegram file download timed out for ${remoteFilePath}`));
72
+ });
73
+ });
74
+ let bytesWritten = 0;
75
+ response.on('data', (chunk) => {
76
+ bytesWritten += Buffer.isBuffer(chunk) ? chunk.length : Buffer.byteLength(chunk);
77
+ });
78
+ await pipeline(response, fs.createWriteStream(tempPath));
79
+ await fs.promises.rename(tempPath, destinationPath);
80
+ return bytesWritten;
81
+ }
82
+ catch (error) {
83
+ await fs.promises.rm(tempPath, { force: true }).catch(() => { });
84
+ throw error;
85
+ }
86
+ finally {
87
+ response?.destroy();
88
+ }
89
+ }
@@ -0,0 +1,76 @@
1
+ import { EventEmitter } from 'node:events';
2
+ import { type TelegramRemoteFile } from './api.js';
3
+ import type { BridgeStore } from '../store/database.js';
4
+ import type { Logger } from '../logger.js';
5
+ import type { TelegramMessageEntity } from './addressing.js';
6
+ import type { TelegramInboundAttachment } from './media.js';
7
+ export interface TelegramTextEvent {
8
+ chatId: string;
9
+ topicId: number | null;
10
+ scopeId: string;
11
+ chatType: string;
12
+ userId: string;
13
+ text: string;
14
+ messageId: number;
15
+ attachments: TelegramInboundAttachment[];
16
+ entities: TelegramMessageEntity[];
17
+ replyToBot: boolean;
18
+ languageCode?: string;
19
+ }
20
+ export interface TelegramCallbackEvent {
21
+ chatId: string;
22
+ topicId: number | null;
23
+ scopeId: string;
24
+ userId: string;
25
+ data: string;
26
+ callbackQueryId: string;
27
+ messageId: number;
28
+ languageCode?: string;
29
+ }
30
+ export declare class TelegramGateway extends EventEmitter {
31
+ private readonly botToken;
32
+ private readonly allowedUserId;
33
+ private readonly allowedChatId;
34
+ private readonly pollIntervalMs;
35
+ private readonly store;
36
+ private readonly logger;
37
+ private running;
38
+ private botKey;
39
+ private botUsername;
40
+ private botUserId;
41
+ constructor(botToken: string, allowedUserId: string, allowedChatId: string | null, pollIntervalMs: number, store: BridgeStore, logger: Logger);
42
+ get username(): string | null;
43
+ start(): Promise<void>;
44
+ stop(): void;
45
+ sendMessage(chatId: string, text: string, inlineKeyboard?: Array<Array<{
46
+ text: string;
47
+ callback_data: string;
48
+ }>>, messageThreadId?: number | null): Promise<number>;
49
+ sendHtmlMessage(chatId: string, text: string, inlineKeyboard?: Array<Array<{
50
+ text: string;
51
+ callback_data: string;
52
+ }>>, messageThreadId?: number | null): Promise<number>;
53
+ sendMessageDraft(chatId: string, draftId: number, text: string, messageThreadId?: number | null): Promise<void>;
54
+ editMessage(chatId: string, messageId: number, text: string, inlineKeyboard?: Array<Array<{
55
+ text: string;
56
+ callback_data: string;
57
+ }>>): Promise<void>;
58
+ editHtmlMessage(chatId: string, messageId: number, text: string, inlineKeyboard?: Array<Array<{
59
+ text: string;
60
+ callback_data: string;
61
+ }>>): Promise<void>;
62
+ clearMessageInlineKeyboard(chatId: string, messageId: number): Promise<void>;
63
+ private sendMessageWithOptions;
64
+ private editMessageWithOptions;
65
+ deleteMessage(chatId: string, messageId: number): Promise<void>;
66
+ answerCallback(callbackQueryId: string, text?: string): Promise<void>;
67
+ sendTyping(chatId: string): Promise<void>;
68
+ sendTypingInThread(chatId: string, messageThreadId?: number | null): Promise<void>;
69
+ getFile(fileId: string): Promise<TelegramRemoteFile>;
70
+ downloadResolvedFile(remoteFilePath: string, destinationPath: string): Promise<number>;
71
+ private resolveBotIdentity;
72
+ private registerCommands;
73
+ private pollLoop;
74
+ private handleUpdate;
75
+ private isAllowedChat;
76
+ }
@@ -0,0 +1,383 @@
1
+ import { EventEmitter } from 'node:events';
2
+ import crypto from 'node:crypto';
3
+ import { callTelegramApi, downloadTelegramFile, getTelegramFile } from './api.js';
4
+ import { getTelegramCommands } from '../i18n.js';
5
+ import { toTelegramBridgeScopeId } from '../core/bridge_scope.js';
6
+ import { createTelegramScopeId } from './scope.js';
7
+ export class TelegramGateway extends EventEmitter {
8
+ botToken;
9
+ allowedUserId;
10
+ allowedChatId;
11
+ pollIntervalMs;
12
+ store;
13
+ logger;
14
+ running = false;
15
+ botKey;
16
+ botUsername = null;
17
+ botUserId = null;
18
+ constructor(botToken, allowedUserId, allowedChatId, pollIntervalMs, store, logger) {
19
+ super();
20
+ this.botToken = botToken;
21
+ this.allowedUserId = allowedUserId;
22
+ this.allowedChatId = allowedChatId;
23
+ this.pollIntervalMs = pollIntervalMs;
24
+ this.store = store;
25
+ this.logger = logger;
26
+ this.botKey = `telegram:${crypto.createHash('sha256').update(this.botToken).digest('hex').slice(0, 8)}`;
27
+ }
28
+ get username() {
29
+ return this.botUsername;
30
+ }
31
+ async start() {
32
+ if (this.running)
33
+ return;
34
+ this.running = true;
35
+ await this.resolveBotIdentity();
36
+ await this.registerCommands();
37
+ void this.pollLoop();
38
+ }
39
+ stop() {
40
+ this.running = false;
41
+ }
42
+ async sendMessage(chatId, text, inlineKeyboard, messageThreadId) {
43
+ return this.sendMessageWithOptions(chatId, text, inlineKeyboard, undefined, messageThreadId);
44
+ }
45
+ async sendHtmlMessage(chatId, text, inlineKeyboard, messageThreadId) {
46
+ return this.sendMessageWithOptions(chatId, text, inlineKeyboard, 'HTML', messageThreadId);
47
+ }
48
+ async sendMessageDraft(chatId, draftId, text, messageThreadId) {
49
+ const result = await callTelegramApi(this.botToken, 'sendMessageDraft', {
50
+ chat_id: chatId,
51
+ draft_id: draftId,
52
+ text,
53
+ ...(messageThreadId !== null && messageThreadId !== undefined ? { message_thread_id: messageThreadId } : {}),
54
+ disable_web_page_preview: true,
55
+ });
56
+ if (!result.ok) {
57
+ throw new Error(result.description || 'Failed to send Telegram draft message');
58
+ }
59
+ }
60
+ async editMessage(chatId, messageId, text, inlineKeyboard) {
61
+ return this.editMessageWithOptions(chatId, messageId, text, inlineKeyboard);
62
+ }
63
+ async editHtmlMessage(chatId, messageId, text, inlineKeyboard) {
64
+ return this.editMessageWithOptions(chatId, messageId, text, inlineKeyboard, 'HTML');
65
+ }
66
+ async clearMessageInlineKeyboard(chatId, messageId) {
67
+ const result = await callTelegramApi(this.botToken, 'editMessageReplyMarkup', {
68
+ chat_id: chatId,
69
+ message_id: messageId,
70
+ reply_markup: { inline_keyboard: [] },
71
+ });
72
+ if (!result.ok && !String(result.description || '').includes('message is not modified')) {
73
+ throw new Error(result.description || 'Failed to clear Telegram message reply markup');
74
+ }
75
+ }
76
+ async sendMessageWithOptions(chatId, text, inlineKeyboard, parseMode, messageThreadId) {
77
+ const result = await callTelegramApi(this.botToken, 'sendMessage', {
78
+ chat_id: chatId,
79
+ text,
80
+ ...(messageThreadId !== null && messageThreadId !== undefined ? { message_thread_id: messageThreadId } : {}),
81
+ ...(inlineKeyboard ? { reply_markup: { inline_keyboard: inlineKeyboard } } : {}),
82
+ ...(parseMode ? { parse_mode: parseMode } : {}),
83
+ disable_web_page_preview: true,
84
+ });
85
+ if (!result.ok || !result.result) {
86
+ throw new Error(result.description || 'Failed to send Telegram message');
87
+ }
88
+ return result.result.message_id;
89
+ }
90
+ async editMessageWithOptions(chatId, messageId, text, inlineKeyboard, parseMode) {
91
+ const result = await callTelegramApi(this.botToken, 'editMessageText', {
92
+ chat_id: chatId,
93
+ message_id: messageId,
94
+ text,
95
+ ...(inlineKeyboard ? { reply_markup: { inline_keyboard: inlineKeyboard } } : {}),
96
+ ...(parseMode ? { parse_mode: parseMode } : {}),
97
+ disable_web_page_preview: true,
98
+ });
99
+ if (!result.ok && !String(result.description || '').includes('message is not modified')) {
100
+ throw new Error(result.description || 'Failed to edit Telegram message');
101
+ }
102
+ }
103
+ async deleteMessage(chatId, messageId) {
104
+ const result = await callTelegramApi(this.botToken, 'deleteMessage', {
105
+ chat_id: chatId,
106
+ message_id: messageId,
107
+ });
108
+ if (!result.ok) {
109
+ throw new Error(result.description || 'Failed to delete Telegram message');
110
+ }
111
+ }
112
+ async answerCallback(callbackQueryId, text = 'OK') {
113
+ await callTelegramApi(this.botToken, 'answerCallbackQuery', {
114
+ callback_query_id: callbackQueryId,
115
+ text,
116
+ });
117
+ }
118
+ async sendTyping(chatId) {
119
+ await callTelegramApi(this.botToken, 'sendChatAction', {
120
+ chat_id: chatId,
121
+ action: 'typing',
122
+ });
123
+ }
124
+ async sendTypingInThread(chatId, messageThreadId) {
125
+ await callTelegramApi(this.botToken, 'sendChatAction', {
126
+ chat_id: chatId,
127
+ ...(messageThreadId !== null && messageThreadId !== undefined ? { message_thread_id: messageThreadId } : {}),
128
+ action: 'typing',
129
+ });
130
+ }
131
+ async getFile(fileId) {
132
+ return getTelegramFile(this.botToken, fileId);
133
+ }
134
+ async downloadResolvedFile(remoteFilePath, destinationPath) {
135
+ return downloadTelegramFile(this.botToken, remoteFilePath, destinationPath);
136
+ }
137
+ async resolveBotIdentity() {
138
+ const result = await callTelegramApi(this.botToken, 'getMe', {});
139
+ if (result.ok && result.result) {
140
+ this.botKey = `telegram:bot${result.result.id}`;
141
+ this.botUserId = result.result.id;
142
+ this.botUsername = result.result.username ?? null;
143
+ }
144
+ }
145
+ async registerCommands() {
146
+ await callTelegramApi(this.botToken, 'setMyCommands', {
147
+ commands: getTelegramCommands('zh'),
148
+ });
149
+ await callTelegramApi(this.botToken, 'setMyCommands', {
150
+ commands: getTelegramCommands('en'),
151
+ language_code: 'en',
152
+ });
153
+ await callTelegramApi(this.botToken, 'setMyCommands', {
154
+ commands: getTelegramCommands('zh'),
155
+ language_code: 'zh',
156
+ });
157
+ }
158
+ async pollLoop() {
159
+ while (this.running) {
160
+ try {
161
+ const offset = this.store.getTelegramOffset(this.botKey) + 1;
162
+ const result = await callTelegramApi(this.botToken, 'getUpdates', {
163
+ timeout: Math.max(1, Math.floor(this.pollIntervalMs / 1000)),
164
+ offset,
165
+ allowed_updates: ['message', 'callback_query']
166
+ });
167
+ if (!result.ok || !result.result) {
168
+ this.logger.warn('telegram.getUpdates failed', result.description);
169
+ await sleep(this.pollIntervalMs);
170
+ continue;
171
+ }
172
+ for (const update of result.result) {
173
+ this.store.setTelegramOffset(this.botKey, update.update_id);
174
+ await this.handleUpdate(update);
175
+ }
176
+ }
177
+ catch (error) {
178
+ this.logger.error('telegram.pollLoop error', toErrorMeta(error));
179
+ await sleep(this.pollIntervalMs);
180
+ }
181
+ }
182
+ }
183
+ async handleUpdate(update) {
184
+ if (update.message && update.message.from && this.isAllowedChat(update.message.chat)) {
185
+ if (String(update.message.from.id) !== this.allowedUserId)
186
+ return;
187
+ const attachments = extractAttachments(update.message);
188
+ const text = update.message.text ?? update.message.caption ?? '';
189
+ const topicId = update.message.message_thread_id ?? null;
190
+ const scopeId = toTelegramBridgeScopeId(createTelegramScopeId(String(update.message.chat.id), topicId));
191
+ const entities = update.message.text ? (update.message.entities ?? []) : (update.message.caption_entities ?? []);
192
+ const replyToBot = this.botUserId !== null && update.message.reply_to_message?.from?.id === this.botUserId;
193
+ if (text || attachments.length > 0) {
194
+ this.emit('text', {
195
+ chatId: String(update.message.chat.id),
196
+ topicId,
197
+ scopeId,
198
+ chatType: update.message.chat.type,
199
+ userId: String(update.message.from.id),
200
+ text,
201
+ messageId: update.message.message_id,
202
+ attachments,
203
+ entities,
204
+ replyToBot,
205
+ ...(update.message.from.language_code ? { languageCode: update.message.from.language_code } : {}),
206
+ });
207
+ return;
208
+ }
209
+ }
210
+ if (update.callback_query?.data && update.callback_query.from && update.callback_query.message) {
211
+ if (String(update.callback_query.from.id) !== this.allowedUserId)
212
+ return;
213
+ if (!this.isAllowedChat(update.callback_query.message.chat))
214
+ return;
215
+ const topicId = update.callback_query.message.message_thread_id ?? null;
216
+ this.emit('callback', {
217
+ chatId: String(update.callback_query.message.chat.id),
218
+ topicId,
219
+ scopeId: toTelegramBridgeScopeId(createTelegramScopeId(String(update.callback_query.message.chat.id), topicId)),
220
+ userId: String(update.callback_query.from.id),
221
+ data: update.callback_query.data,
222
+ callbackQueryId: update.callback_query.id,
223
+ messageId: update.callback_query.message.message_id,
224
+ ...(update.callback_query.from.language_code ? { languageCode: update.callback_query.from.language_code } : {}),
225
+ });
226
+ }
227
+ }
228
+ isAllowedChat(chat) {
229
+ if (chat.type === 'private') {
230
+ return true;
231
+ }
232
+ if (this.allowedChatId) {
233
+ return String(chat.id) === this.allowedChatId;
234
+ }
235
+ return false;
236
+ }
237
+ }
238
+ function sleep(ms) {
239
+ return new Promise(resolve => setTimeout(resolve, ms));
240
+ }
241
+ function extractAttachments(message) {
242
+ const attachments = [];
243
+ const largestPhoto = pickLargestPhoto(message.photo ?? []);
244
+ if (largestPhoto) {
245
+ attachments.push({
246
+ kind: 'photo',
247
+ fileId: largestPhoto.file_id,
248
+ fileUniqueId: largestPhoto.file_unique_id,
249
+ fileName: null,
250
+ mimeType: 'image/jpeg',
251
+ fileSize: largestPhoto.file_size ?? null,
252
+ width: largestPhoto.width,
253
+ height: largestPhoto.height,
254
+ durationSeconds: null,
255
+ isAnimated: false,
256
+ isVideo: false,
257
+ });
258
+ }
259
+ if (message.document) {
260
+ attachments.push({
261
+ kind: 'document',
262
+ fileId: message.document.file_id,
263
+ fileUniqueId: message.document.file_unique_id,
264
+ fileName: message.document.file_name ?? null,
265
+ mimeType: message.document.mime_type ?? null,
266
+ fileSize: message.document.file_size ?? null,
267
+ width: null,
268
+ height: null,
269
+ durationSeconds: null,
270
+ isAnimated: false,
271
+ isVideo: false,
272
+ });
273
+ }
274
+ if (message.audio) {
275
+ attachments.push({
276
+ kind: 'audio',
277
+ fileId: message.audio.file_id,
278
+ fileUniqueId: message.audio.file_unique_id,
279
+ fileName: message.audio.file_name ?? null,
280
+ mimeType: message.audio.mime_type ?? null,
281
+ fileSize: message.audio.file_size ?? null,
282
+ width: null,
283
+ height: null,
284
+ durationSeconds: message.audio.duration ?? null,
285
+ isAnimated: false,
286
+ isVideo: false,
287
+ });
288
+ }
289
+ if (message.voice) {
290
+ attachments.push({
291
+ kind: 'voice',
292
+ fileId: message.voice.file_id,
293
+ fileUniqueId: message.voice.file_unique_id,
294
+ fileName: null,
295
+ mimeType: message.voice.mime_type ?? null,
296
+ fileSize: message.voice.file_size ?? null,
297
+ width: null,
298
+ height: null,
299
+ durationSeconds: message.voice.duration ?? null,
300
+ isAnimated: false,
301
+ isVideo: false,
302
+ });
303
+ }
304
+ if (message.video) {
305
+ attachments.push({
306
+ kind: 'video',
307
+ fileId: message.video.file_id,
308
+ fileUniqueId: message.video.file_unique_id,
309
+ fileName: message.video.file_name ?? null,
310
+ mimeType: message.video.mime_type ?? null,
311
+ fileSize: message.video.file_size ?? null,
312
+ width: message.video.width ?? null,
313
+ height: message.video.height ?? null,
314
+ durationSeconds: message.video.duration ?? null,
315
+ isAnimated: false,
316
+ isVideo: true,
317
+ });
318
+ }
319
+ if (message.animation) {
320
+ attachments.push({
321
+ kind: 'animation',
322
+ fileId: message.animation.file_id,
323
+ fileUniqueId: message.animation.file_unique_id,
324
+ fileName: message.animation.file_name ?? null,
325
+ mimeType: message.animation.mime_type ?? null,
326
+ fileSize: message.animation.file_size ?? null,
327
+ width: message.animation.width ?? null,
328
+ height: message.animation.height ?? null,
329
+ durationSeconds: message.animation.duration ?? null,
330
+ isAnimated: true,
331
+ isVideo: message.animation.mime_type?.startsWith('video/') ?? false,
332
+ });
333
+ }
334
+ if (message.sticker) {
335
+ attachments.push({
336
+ kind: 'sticker',
337
+ fileId: message.sticker.file_id,
338
+ fileUniqueId: message.sticker.file_unique_id,
339
+ fileName: null,
340
+ mimeType: null,
341
+ fileSize: message.sticker.file_size ?? null,
342
+ width: message.sticker.width,
343
+ height: message.sticker.height,
344
+ durationSeconds: null,
345
+ isAnimated: message.sticker.is_animated ?? false,
346
+ isVideo: message.sticker.is_video ?? false,
347
+ });
348
+ }
349
+ if (message.video_note) {
350
+ attachments.push({
351
+ kind: 'videoNote',
352
+ fileId: message.video_note.file_id,
353
+ fileUniqueId: message.video_note.file_unique_id,
354
+ fileName: null,
355
+ mimeType: 'video/mp4',
356
+ fileSize: message.video_note.file_size ?? null,
357
+ width: message.video_note.length ?? null,
358
+ height: message.video_note.length ?? null,
359
+ durationSeconds: message.video_note.duration ?? null,
360
+ isAnimated: false,
361
+ isVideo: true,
362
+ });
363
+ }
364
+ return attachments;
365
+ }
366
+ function pickLargestPhoto(photos) {
367
+ if (photos.length === 0)
368
+ return null;
369
+ return photos.reduce((current, candidate) => {
370
+ const currentArea = current.width * current.height;
371
+ const candidateArea = candidate.width * candidate.height;
372
+ if (candidateArea !== currentArea) {
373
+ return candidateArea > currentArea ? candidate : current;
374
+ }
375
+ return (candidate.file_size ?? 0) > (current.file_size ?? 0) ? candidate : current;
376
+ });
377
+ }
378
+ function toErrorMeta(error) {
379
+ if (error instanceof Error) {
380
+ return { message: error.message, stack: error.stack };
381
+ }
382
+ return { error: String(error) };
383
+ }
@@ -0,0 +1,34 @@
1
+ export declare const TELEGRAM_INBOX_DIR = ".telegram-inbox";
2
+ export declare const TELEGRAM_BOT_API_DOWNLOAD_LIMIT_BYTES: number;
3
+ export type TelegramAttachmentKind = 'photo' | 'document' | 'audio' | 'voice' | 'video' | 'animation' | 'sticker' | 'videoNote';
4
+ export interface TelegramInboundAttachment {
5
+ kind: TelegramAttachmentKind;
6
+ fileId: string;
7
+ fileUniqueId: string;
8
+ fileName: string | null;
9
+ mimeType: string | null;
10
+ fileSize: number | null;
11
+ width: number | null;
12
+ height: number | null;
13
+ durationSeconds: number | null;
14
+ isAnimated: boolean;
15
+ isVideo: boolean;
16
+ /** When set, {@link stageAttachments} copies this file instead of Telegram Bot API download. */
17
+ localPath?: string;
18
+ }
19
+ export interface StagedTelegramAttachment extends TelegramInboundAttachment {
20
+ fileName: string;
21
+ localPath: string;
22
+ relativePath: string;
23
+ nativeImage: boolean;
24
+ }
25
+ interface PlannedAttachmentPath {
26
+ fileName: string;
27
+ localPath: string;
28
+ relativePath: string;
29
+ }
30
+ export declare function isNativeImageAttachment(attachment: TelegramInboundAttachment): boolean;
31
+ export declare function planAttachmentStoragePath(cwd: string, threadId: string, attachment: TelegramInboundAttachment, remoteFilePath?: string | null, now?: Date): PlannedAttachmentPath;
32
+ export declare function buildAttachmentPrompt(userText: string, attachments: readonly StagedTelegramAttachment[]): string;
33
+ export declare function summarizeTelegramInput(text: string, attachments: readonly TelegramInboundAttachment[]): string;
34
+ export {};