@foxden-app/foxclaw 0.12.0 → 0.13.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 (124) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/dist/antigravity/account_ui.d.ts +34 -0
  3. package/dist/antigravity/account_ui.js +354 -0
  4. package/dist/antigravity/adapter.js +1 -1
  5. package/dist/antigravity/auth.d.ts +4 -1
  6. package/dist/antigravity/auth.js +17 -5
  7. package/dist/antigravity/controller.d.ts +3 -108
  8. package/dist/antigravity/controller.js +2 -2129
  9. package/dist/antigravity/conversation_ui.d.ts +56 -0
  10. package/dist/antigravity/conversation_ui.js +509 -0
  11. package/dist/antigravity/ui_callbacks.d.ts +6 -0
  12. package/dist/antigravity/ui_callbacks.js +6 -0
  13. package/dist/bridge/unified_bridge.d.ts +114 -0
  14. package/dist/bridge/unified_bridge.js +1343 -0
  15. package/dist/channels/bridge_messaging_router.d.ts +8 -4
  16. package/dist/channels/bridge_messaging_router.js +15 -4
  17. package/dist/channels/telegram/telegram_messaging_port.d.ts +16 -0
  18. package/dist/channels/telegram/telegram_messaging_port.js +22 -2
  19. package/dist/channels/text_actions.d.ts +14 -0
  20. package/dist/channels/text_actions.js +48 -0
  21. package/dist/channels/weixin/weixin_channel_adapter.d.ts +3 -4
  22. package/dist/channels/weixin/weixin_channel_adapter.js +2 -2
  23. package/dist/channels/weixin/weixin_messaging_port.d.ts +16 -1
  24. package/dist/channels/weixin/weixin_messaging_port.js +34 -7
  25. package/dist/codex_app/activity.d.ts +62 -0
  26. package/dist/codex_app/activity.js +418 -0
  27. package/dist/codex_app/adapter.d.ts +2 -2
  28. package/dist/codex_app/adapter.js +55 -25
  29. package/dist/codex_app/backend_ui.d.ts +4 -0
  30. package/dist/codex_app/backend_ui.js +48 -0
  31. package/dist/codex_app/client.js +0 -12
  32. package/dist/controller/access.d.ts +1 -11
  33. package/dist/controller/access.js +1 -33
  34. package/dist/controller/activity.d.ts +1 -62
  35. package/dist/controller/activity.js +2 -418
  36. package/dist/controller/auth_errors.d.ts +11 -0
  37. package/dist/controller/auth_errors.js +124 -0
  38. package/dist/controller/auth_files.d.ts +13 -0
  39. package/dist/controller/auth_files.js +156 -0
  40. package/dist/controller/auth_presentation.d.ts +77 -0
  41. package/dist/controller/auth_presentation.js +697 -0
  42. package/dist/controller/auth_quota_service.d.ts +27 -0
  43. package/dist/controller/auth_quota_service.js +151 -0
  44. package/dist/controller/controller.d.ts +30 -56
  45. package/dist/controller/controller.js +181 -3497
  46. package/dist/controller/interactions.d.ts +74 -0
  47. package/dist/controller/interactions.js +560 -0
  48. package/dist/controller/local_usage_service.d.ts +19 -0
  49. package/dist/controller/local_usage_service.js +62 -0
  50. package/dist/controller/native_panel_service.d.ts +55 -0
  51. package/dist/controller/native_panel_service.js +214 -0
  52. package/dist/controller/native_panels.d.ts +61 -0
  53. package/dist/controller/native_panels.js +784 -0
  54. package/dist/controller/shared_helpers.d.ts +53 -0
  55. package/dist/controller/shared_helpers.js +399 -0
  56. package/dist/controller/state_types.d.ts +390 -0
  57. package/dist/controller/state_types.js +82 -0
  58. package/dist/controller/turn_rendering.d.ts +22 -0
  59. package/dist/controller/turn_rendering.js +288 -0
  60. package/dist/core/access.d.ts +11 -0
  61. package/dist/core/access.js +33 -0
  62. package/dist/core/attachment_files.d.ts +13 -0
  63. package/dist/core/attachment_files.js +180 -0
  64. package/dist/core/attachment_types.d.ts +22 -0
  65. package/dist/core/attachment_types.js +1 -0
  66. package/dist/core/attachments.d.ts +5 -3
  67. package/dist/core/attachments.js +2 -2
  68. package/dist/core/backend_registry.d.ts +22 -0
  69. package/dist/core/backend_registry.js +50 -0
  70. package/dist/core/backend_ui.d.ts +28 -0
  71. package/dist/core/backend_ui.js +1 -0
  72. package/dist/core/channel_events.d.ts +48 -0
  73. package/dist/core/channel_events.js +1 -0
  74. package/dist/core/channel_port.d.ts +52 -8
  75. package/dist/core/engine_spi.d.ts +19 -4
  76. package/dist/core/listener_scope.d.ts +11 -0
  77. package/dist/core/listener_scope.js +10 -0
  78. package/dist/core/message_text.d.ts +2 -0
  79. package/dist/core/message_text.js +28 -0
  80. package/dist/core/network_errors.d.ts +1 -0
  81. package/dist/core/network_errors.js +23 -0
  82. package/dist/core/orchestrator.d.ts +53 -24
  83. package/dist/core/orchestrator.js +855 -507
  84. package/dist/core/queued_engine_input.d.ts +12 -0
  85. package/dist/core/queued_engine_input.js +16 -0
  86. package/dist/core/runtime_supervisor.d.ts +19 -0
  87. package/dist/core/runtime_supervisor.js +45 -0
  88. package/dist/core/scope_operations.d.ts +7 -0
  89. package/dist/core/scope_operations.js +19 -0
  90. package/dist/core/stream_preview.js +21 -9
  91. package/dist/dsh/adapter.js +18 -5
  92. package/dist/dsh/backend.d.ts +7 -0
  93. package/dist/dsh/backend.js +21 -0
  94. package/dist/dsh/runtime.d.ts +0 -1
  95. package/dist/dsh/runtime.js +4 -13
  96. package/dist/dsh/ui.d.ts +8 -8
  97. package/dist/dsh/ui.js +1 -1
  98. package/dist/main.js +55 -43
  99. package/dist/opencode/adapter.js +28 -9
  100. package/dist/opencode/controller.js +1 -1
  101. package/dist/store/channel_inbox.d.ts +14 -0
  102. package/dist/store/channel_inbox.js +21 -0
  103. package/dist/store/database.d.ts +7 -3
  104. package/dist/store/database.js +7 -2
  105. package/dist/store/task_journal.d.ts +32 -0
  106. package/dist/store/task_journal.js +70 -0
  107. package/dist/telegram/api.d.ts +2 -2
  108. package/dist/telegram/api.js +10 -28
  109. package/dist/telegram/gateway.d.ts +13 -27
  110. package/dist/telegram/gateway.js +86 -43
  111. package/dist/telegram/media.d.ts +2 -34
  112. package/dist/telegram/media.js +1 -180
  113. package/dist/telegram/task_preview.d.ts +13 -0
  114. package/dist/telegram/task_preview.js +92 -0
  115. package/dist/telegram/task_result.d.ts +2 -0
  116. package/dist/telegram/task_result.js +12 -0
  117. package/dist/telegram/text.d.ts +1 -1
  118. package/dist/telegram/text.js +2 -27
  119. package/dist/types.d.ts +1 -1
  120. package/docs/release.md +13 -11
  121. package/docs/zh/architecture-review.md +106 -0
  122. package/docs/zh/release.md +13 -11
  123. package/docs/zh/telegram-experience.md +40 -0
  124. package/package.json +5 -4
@@ -1,3 +1,4 @@
1
+ import { setTimeout as delay } from 'node:timers/promises';
1
2
  import { EventEmitter } from 'node:events';
2
3
  import crypto from 'node:crypto';
3
4
  import { callTelegramApi, callTelegramMultipartApi, downloadTelegramFile, getTelegramFile } from './api.js';
@@ -13,7 +14,28 @@ export class TelegramGateway extends EventEmitter {
13
14
  logger;
14
15
  namespacedScopes;
15
16
  commandProvider;
17
+ inboundConsumer = null;
18
+ generations = new Map();
19
+ setInboundConsumer(consumer) {
20
+ if (this.inboundConsumer && this.inboundConsumer !== consumer)
21
+ throw new Error('Telegram inbound consumer already owned');
22
+ this.inboundConsumer = consumer;
23
+ return () => { if (this.inboundConsumer === consumer)
24
+ this.inboundConsumer = null; };
25
+ }
26
+ registerGeneration(draftId, scopeId, taskId, chatId, topicId) {
27
+ this.generations.set(draftId, { scopeId, taskId, chatId, topicId });
28
+ }
29
+ releaseGeneration(draftId) { this.generations.delete(draftId); }
30
+ async dispatchInbound(updateId, inbound) {
31
+ if (this.inboundConsumer)
32
+ await this.inboundConsumer(`${this.botKey}:${updateId}`, inbound);
33
+ else
34
+ this.emit(inbound.kind, inbound.event);
35
+ }
16
36
  running = false;
37
+ polling = null;
38
+ pollAbort = null;
17
39
  botKey;
18
40
  botUsername = null;
19
41
  botUserId = null;
@@ -57,11 +79,16 @@ export class TelegramGateway extends EventEmitter {
57
79
  async start() {
58
80
  if (this.running)
59
81
  return;
82
+ await this.polling;
60
83
  this.running = true;
61
- void this.pollLoop();
84
+ this.pollAbort = new AbortController();
85
+ this.polling = this.pollLoop();
62
86
  }
63
- stop() {
87
+ async stop() {
64
88
  this.running = false;
89
+ this.pollAbort?.abort();
90
+ await this.polling;
91
+ this.generations.clear();
65
92
  }
66
93
  async sendMessage(chatId, text, inlineKeyboard, messageThreadId) {
67
94
  return this.sendMessageWithOptions(chatId, text, inlineKeyboard, undefined, messageThreadId);
@@ -124,11 +151,12 @@ export class TelegramGateway extends EventEmitter {
124
151
  throw new Error(result.description || 'Failed to send Telegram draft message');
125
152
  }
126
153
  }
127
- async sendRichMessageDraft(chatId, draftId, richMessage, messageThreadId) {
154
+ async sendRichMessageDraft(chatId, draftId, richMessage, messageThreadId, canStop = false) {
128
155
  const result = await callTelegramApi(this.botToken, 'sendRichMessageDraft', {
129
156
  chat_id: chatId,
130
157
  draft_id: draftId,
131
158
  rich_message: richMessage,
159
+ ...(canStop ? { can_stop: true, keep_on_stop: true } : {}),
132
160
  ...(messageThreadId !== null && messageThreadId !== undefined ? { message_thread_id: messageThreadId } : {}),
133
161
  });
134
162
  if (!result.ok) {
@@ -223,8 +251,8 @@ export class TelegramGateway extends EventEmitter {
223
251
  async downloadResolvedFile(remoteFilePath, destinationPath) {
224
252
  return downloadTelegramFile(this.botToken, remoteFilePath, destinationPath);
225
253
  }
226
- async resolveBotIdentity(required = false) {
227
- const result = await callTelegramApi(this.botToken, 'getMe', {});
254
+ async resolveBotIdentity(required = false, signal) {
255
+ const result = await callTelegramApi(this.botToken, 'getMe', {}, signal);
228
256
  if (result.ok && result.result) {
229
257
  this.botKey = `telegram:bot${result.result.id}`;
230
258
  this.botUserId = result.result.id;
@@ -235,18 +263,18 @@ export class TelegramGateway extends EventEmitter {
235
263
  throw new Error(result.description || 'Failed to resolve Telegram bot identity');
236
264
  }
237
265
  }
238
- async registerCommands() {
266
+ async registerCommands(signal) {
239
267
  await callTelegramApi(this.botToken, 'setMyCommands', {
240
268
  commands: this.commandProvider('zh'),
241
- });
269
+ }, signal);
242
270
  await callTelegramApi(this.botToken, 'setMyCommands', {
243
271
  commands: this.commandProvider('en'),
244
272
  language_code: 'en',
245
- });
273
+ }, signal);
246
274
  await callTelegramApi(this.botToken, 'setMyCommands', {
247
275
  commands: this.commandProvider('zh'),
248
276
  language_code: 'zh',
249
- });
277
+ }, signal);
250
278
  }
251
279
  async setChatCommands(chatId, commands) {
252
280
  try {
@@ -261,11 +289,14 @@ export class TelegramGateway extends EventEmitter {
261
289
  }
262
290
  async pollLoop() {
263
291
  let remoteInitialized = false;
292
+ const signal = this.pollAbort?.signal;
264
293
  while (this.running) {
265
294
  try {
266
295
  if (!remoteInitialized) {
267
- await this.resolveBotIdentity(true);
268
- await this.registerCommands();
296
+ await this.resolveBotIdentity(true, signal);
297
+ if (!this.running)
298
+ return;
299
+ await this.registerCommands(signal);
269
300
  remoteInitialized = true;
270
301
  this.emit('remoteReady');
271
302
  if (!this.running)
@@ -275,25 +306,40 @@ export class TelegramGateway extends EventEmitter {
275
306
  const result = await callTelegramApi(this.botToken, 'getUpdates', {
276
307
  timeout: Math.max(1, Math.floor(this.pollIntervalMs / 1000)),
277
308
  offset,
278
- allowed_updates: ['message', 'callback_query']
279
- });
309
+ allowed_updates: ['message', 'callback_query', 'stopped_message_generation']
310
+ }, signal);
311
+ if (!this.running)
312
+ return;
280
313
  if (!result.ok || !result.result) {
281
314
  this.logger.warn('telegram.getUpdates failed', result.description);
282
- await sleep(this.pollIntervalMs);
315
+ await delay(this.pollIntervalMs, undefined, { signal }).catch(() => { });
283
316
  continue;
284
317
  }
285
318
  for (const update of result.result) {
286
- this.store.setTelegramOffset(this.botKey, update.update_id);
319
+ if (!this.running)
320
+ return;
287
321
  await this.handleUpdate(update);
322
+ this.store.setTelegramOffset(this.botKey, update.update_id);
288
323
  }
289
324
  }
290
325
  catch (error) {
326
+ if (!this.running)
327
+ return;
291
328
  this.logger.error('telegram.pollLoop error', toErrorMeta(error));
292
- await sleep(this.pollIntervalMs);
329
+ await delay(this.pollIntervalMs, undefined, { signal }).catch(() => { });
293
330
  }
294
331
  }
295
332
  }
296
333
  async handleUpdate(update) {
334
+ const stopped = update.stopped_message_generation;
335
+ if (stopped) {
336
+ const generation = this.generations.get(stopped.draft_id);
337
+ // Native drafts are private-chat only; the private chat id identifies the authorized user.
338
+ if (!generation || stopped.chat.type !== 'private' || String(stopped.chat.id) !== this.allowedUserId || generation.chatId !== String(stopped.chat.id) || generation.topicId !== (stopped.message_thread_id ?? null))
339
+ return;
340
+ await this.dispatchInbound(update.update_id, { kind: 'stop', event: { scopeId: generation.scopeId, taskId: generation.taskId } });
341
+ return;
342
+ }
297
343
  if (update.message && update.message.from && this.isAllowedChat(update.message.chat)) {
298
344
  if (String(update.message.from.id) !== this.allowedUserId) {
299
345
  if (update.message.chat.type === 'private') {
@@ -320,20 +366,20 @@ export class TelegramGateway extends EventEmitter {
320
366
  const entities = update.message.text ? (update.message.entities ?? []) : (update.message.caption_entities ?? []);
321
367
  const replyToBot = this.botUserId !== null && update.message.reply_to_message?.from?.id === this.botUserId;
322
368
  if (text || attachments.length > 0) {
323
- this.emit('text', {
324
- chatId: String(update.message.chat.id),
325
- topicId,
326
- scopeId,
327
- chatType: update.message.chat.type,
328
- userId: String(update.message.from.id),
329
- text,
330
- messageId: update.message.message_id,
331
- mediaGroupId: update.message.media_group_id ?? null,
332
- attachments,
333
- entities,
334
- replyToBot,
335
- ...(update.message.from.language_code ? { languageCode: update.message.from.language_code } : {}),
336
- });
369
+ await this.dispatchInbound(update.update_id, { kind: 'text', event: {
370
+ chatId: String(update.message.chat.id),
371
+ topicId,
372
+ scopeId,
373
+ chatType: update.message.chat.type,
374
+ userId: String(update.message.from.id),
375
+ text,
376
+ messageId: update.message.message_id,
377
+ mediaGroupId: update.message.media_group_id ?? null,
378
+ attachments,
379
+ entities,
380
+ replyToBot,
381
+ ...(update.message.from.language_code ? { languageCode: update.message.from.language_code } : {}),
382
+ } });
337
383
  if (update.message.chat.type === 'private' && this.identity) {
338
384
  this.store.rememberTelegramPrivateScope(this.identity, scopeId, String(update.message.chat.id));
339
385
  }
@@ -346,16 +392,16 @@ export class TelegramGateway extends EventEmitter {
346
392
  if (!this.isAllowedChat(update.callback_query.message.chat))
347
393
  return;
348
394
  const topicId = update.callback_query.message.message_thread_id ?? null;
349
- this.emit('callback', {
350
- chatId: String(update.callback_query.message.chat.id),
351
- topicId,
352
- scopeId: toTelegramBridgeScopeId(createTelegramScopeId(String(update.callback_query.message.chat.id), topicId), this.namespacedScopes ? this.identity : null),
353
- userId: String(update.callback_query.from.id),
354
- data: update.callback_query.data,
355
- callbackQueryId: update.callback_query.id,
356
- messageId: update.callback_query.message.message_id,
357
- ...(update.callback_query.from.language_code ? { languageCode: update.callback_query.from.language_code } : {}),
358
- });
395
+ await this.dispatchInbound(update.update_id, { kind: 'callback', event: {
396
+ chatId: String(update.callback_query.message.chat.id),
397
+ topicId,
398
+ scopeId: toTelegramBridgeScopeId(createTelegramScopeId(String(update.callback_query.message.chat.id), topicId), this.namespacedScopes ? this.identity : null),
399
+ userId: String(update.callback_query.from.id),
400
+ data: update.callback_query.data,
401
+ callbackQueryId: update.callback_query.id,
402
+ messageId: update.callback_query.message.message_id,
403
+ ...(update.callback_query.from.language_code ? { languageCode: update.callback_query.from.language_code } : {}),
404
+ } });
359
405
  }
360
406
  }
361
407
  isAllowedChat(chat) {
@@ -368,9 +414,6 @@ export class TelegramGateway extends EventEmitter {
368
414
  return false;
369
415
  }
370
416
  }
371
- function sleep(ms) {
372
- return new Promise(resolve => setTimeout(resolve, ms));
373
- }
374
417
  export function parseTelegramBotId(botToken) {
375
418
  const match = /^(\d+):/.exec(botToken.trim());
376
419
  if (!match)
@@ -1,34 +1,2 @@
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 {};
1
+ export type { AttachmentKind as TelegramAttachmentKind, InboundAttachment as TelegramInboundAttachment, StagedAttachment as StagedTelegramAttachment } from '../core/attachment_types.js';
2
+ export { ATTACHMENT_INBOX_DIR as TELEGRAM_INBOX_DIR, DEFAULT_REMOTE_DOWNLOAD_LIMIT_BYTES as TELEGRAM_BOT_API_DOWNLOAD_LIMIT_BYTES, isNativeImageAttachment, planAttachmentStoragePath, buildAttachmentPrompt, summarizeInput as summarizeTelegramInput } from '../core/attachment_files.js';
@@ -1,180 +1 @@
1
- import path from 'node:path';
2
- export const TELEGRAM_INBOX_DIR = '.telegram-inbox';
3
- export const TELEGRAM_BOT_API_DOWNLOAD_LIMIT_BYTES = 20 * 1024 * 1024;
4
- const FALLBACK_EXTENSION_BY_KIND = {
5
- photo: '.jpg',
6
- document: '',
7
- audio: '.mp3',
8
- voice: '.ogg',
9
- video: '.mp4',
10
- animation: '.mp4',
11
- sticker: '.webp',
12
- videoNote: '.mp4',
13
- };
14
- const MIME_EXTENSION_MAP = {
15
- 'application/gzip': '.gz',
16
- 'application/json': '.json',
17
- 'application/pdf': '.pdf',
18
- 'application/zip': '.zip',
19
- 'audio/mpeg': '.mp3',
20
- 'audio/mp4': '.m4a',
21
- 'audio/ogg': '.ogg',
22
- 'audio/wav': '.wav',
23
- 'image/gif': '.gif',
24
- 'image/jpeg': '.jpg',
25
- 'image/png': '.png',
26
- 'image/webp': '.webp',
27
- 'text/csv': '.csv',
28
- 'text/html': '.html',
29
- 'text/markdown': '.md',
30
- 'text/plain': '.txt',
31
- 'video/mp4': '.mp4',
32
- 'video/quicktime': '.mov',
33
- };
34
- export function isNativeImageAttachment(attachment) {
35
- if (attachment.kind === 'photo') {
36
- return true;
37
- }
38
- if (attachment.kind === 'sticker') {
39
- return !attachment.isAnimated && !attachment.isVideo;
40
- }
41
- if (attachment.kind !== 'document') {
42
- return false;
43
- }
44
- const lowerMime = attachment.mimeType?.toLowerCase() ?? '';
45
- if (lowerMime === 'image/jpeg' || lowerMime === 'image/png' || lowerMime === 'image/webp' || lowerMime === 'image/gif') {
46
- return true;
47
- }
48
- const extension = extensionFromName(attachment.fileName);
49
- return extension === '.jpg' || extension === '.jpeg' || extension === '.png' || extension === '.webp' || extension === '.gif';
50
- }
51
- export function planAttachmentStoragePath(cwd, threadId, attachment, remoteFilePath, now = new Date()) {
52
- const dateSegment = now.toISOString().slice(0, 10);
53
- const threadSegment = sanitizePathSegment(threadId);
54
- const timestamp = now.toISOString().slice(0, 19).replace(/[-:]/g, '').replace('T', '-');
55
- const resolvedFileName = resolveAttachmentFileName(attachment, remoteFilePath);
56
- const uniquePrefix = `${timestamp}-${sanitizePathSegment(attachment.fileUniqueId).slice(0, 16)}`;
57
- const joinedName = truncateFileName(`${uniquePrefix}-${resolvedFileName}`, 160);
58
- const relativePath = path.join(TELEGRAM_INBOX_DIR, dateSegment, threadSegment, joinedName);
59
- return {
60
- fileName: resolvedFileName,
61
- localPath: path.resolve(cwd, relativePath),
62
- relativePath,
63
- };
64
- }
65
- export function buildAttachmentPrompt(userText, attachments) {
66
- const normalizedText = userText.trim();
67
- const lines = [];
68
- if (normalizedText) {
69
- lines.push(normalizedText, '');
70
- }
71
- else {
72
- lines.push('User sent Telegram attachments without a caption.', '');
73
- }
74
- lines.push('Telegram attachments:');
75
- attachments.forEach((attachment, index) => {
76
- lines.push(`${index + 1}. ${describeAttachment(attachment)}`);
77
- lines.push(` filename: ${attachment.fileName}`);
78
- lines.push(` path: ${attachment.localPath}`);
79
- if (attachment.mimeType)
80
- lines.push(` mime: ${attachment.mimeType}`);
81
- if (attachment.fileSize !== null)
82
- lines.push(` size_bytes: ${attachment.fileSize}`);
83
- if (attachment.width !== null && attachment.height !== null) {
84
- lines.push(` dimensions: ${attachment.width}x${attachment.height}`);
85
- }
86
- if (attachment.durationSeconds !== null) {
87
- lines.push(` duration_seconds: ${attachment.durationSeconds}`);
88
- }
89
- if (attachment.nativeImage) {
90
- lines.push(' attached_as: localImage');
91
- }
92
- });
93
- lines.push('', 'Use the local file paths above when you inspect these attachments.');
94
- return lines.join('\n');
95
- }
96
- export function summarizeTelegramInput(text, attachments) {
97
- const lines = [];
98
- const normalizedText = text.trim();
99
- if (normalizedText) {
100
- lines.push(normalizedText);
101
- }
102
- if (attachments.length > 0) {
103
- lines.push(`[attachments: ${attachments.map((attachment) => `${attachment.kind}:${attachment.fileName ?? attachment.fileUniqueId}`).join(', ')}]`);
104
- }
105
- return truncateSummary(lines.join(' '), 500);
106
- }
107
- function resolveAttachmentFileName(attachment, remoteFilePath) {
108
- const providedName = attachment.fileName ? path.basename(attachment.fileName) : '';
109
- const remoteName = remoteFilePath ? path.basename(remoteFilePath) : '';
110
- const baseCandidate = providedName || remoteName || `${attachment.kind}-${attachment.fileUniqueId}`;
111
- const sanitizedBase = sanitizeFileName(baseCandidate);
112
- const extension = extensionFromName(sanitizedBase)
113
- || extensionFromName(remoteName)
114
- || extensionFromMimeType(attachment.mimeType)
115
- || fallbackExtensionByKind(attachment.kind);
116
- if (!extension) {
117
- return sanitizedBase;
118
- }
119
- const withoutExtension = sanitizedBase.slice(0, sanitizedBase.length - extensionFromName(sanitizedBase).length) || `${attachment.kind}-${attachment.fileUniqueId}`;
120
- return truncateFileName(`${withoutExtension}${extension}`, 120);
121
- }
122
- function describeAttachment(attachment) {
123
- switch (attachment.kind) {
124
- case 'photo':
125
- return 'photo';
126
- case 'document':
127
- return 'document';
128
- case 'audio':
129
- return 'audio file';
130
- case 'voice':
131
- return 'voice message';
132
- case 'video':
133
- return 'video';
134
- case 'animation':
135
- return 'animation';
136
- case 'sticker':
137
- return attachment.isAnimated || attachment.isVideo ? 'animated sticker' : 'sticker';
138
- case 'videoNote':
139
- return 'video note';
140
- }
141
- }
142
- function sanitizeFileName(fileName) {
143
- const trimmed = fileName.trim().replace(/[\\/]+/g, '-');
144
- const sanitized = trimmed
145
- .replace(/[^\w.\-()+@]+/g, '-')
146
- .replace(/-+/g, '-')
147
- .replace(/^\.+/, '')
148
- .replace(/^-+/, '')
149
- .replace(/[.-]+$/, '');
150
- return sanitized || 'attachment';
151
- }
152
- function sanitizePathSegment(value) {
153
- return value.replace(/[^\w.-]+/g, '-').replace(/-+/g, '-').replace(/^-+|-+$/g, '') || 'thread';
154
- }
155
- function extensionFromMimeType(mimeType) {
156
- if (!mimeType)
157
- return '';
158
- return MIME_EXTENSION_MAP[mimeType.toLowerCase()] ?? '';
159
- }
160
- function extensionFromName(fileName) {
161
- if (!fileName)
162
- return '';
163
- const extension = path.extname(fileName).toLowerCase();
164
- return extension === '.' ? '' : extension;
165
- }
166
- function truncateFileName(fileName, maxLength) {
167
- if (fileName.length <= maxLength)
168
- return fileName;
169
- const extension = extensionFromName(fileName);
170
- const base = extension ? fileName.slice(0, -extension.length) : fileName;
171
- return `${base.slice(0, Math.max(1, maxLength - extension.length))}${extension}`;
172
- }
173
- function truncateSummary(text, maxLength) {
174
- if (text.length <= maxLength)
175
- return text;
176
- return `${text.slice(0, maxLength - 3)}...`;
177
- }
178
- function fallbackExtensionByKind(kind) {
179
- return FALLBACK_EXTENSION_BY_KIND[kind] ?? '';
180
- }
1
+ export { ATTACHMENT_INBOX_DIR as TELEGRAM_INBOX_DIR, DEFAULT_REMOTE_DOWNLOAD_LIMIT_BYTES as TELEGRAM_BOT_API_DOWNLOAD_LIMIT_BYTES, isNativeImageAttachment, planAttachmentStoragePath, buildAttachmentPrompt, summarizeInput as summarizeTelegramInput } from '../core/attachment_files.js';
@@ -0,0 +1,13 @@
1
+ import type { TelegramGateway } from './gateway.js';
2
+ /** Owns live drafts and fallback cards; draft ids are never persistent message ids. */
3
+ export declare class TelegramTaskPreviews {
4
+ private readonly gateway;
5
+ private readonly previews;
6
+ private readonly draftIds;
7
+ constructor(gateway: TelegramGateway);
8
+ private keyboard;
9
+ private sendCard;
10
+ begin(scopeId: string, taskId: string, text: string, reuseMessageId?: number): Promise<number>;
11
+ update(scopeId: string, taskId: string, messageId: number, html: string): Promise<number>;
12
+ end(scopeId: string, taskId: string): Promise<void>;
13
+ }
@@ -0,0 +1,92 @@
1
+ import { randomInt } from 'node:crypto';
2
+ import { parseTelegramTargetFromBridgeScope } from '../core/bridge_scope.js';
3
+ import { telegramRichHtml } from './rich.js';
4
+ import { renderTelegramMarkdownRichHtml } from './rich_markdown.js';
5
+ /** Owns live drafts and fallback cards; draft ids are never persistent message ids. */
6
+ export class TelegramTaskPreviews {
7
+ gateway;
8
+ previews = new Map();
9
+ draftIds = new Set();
10
+ constructor(gateway) {
11
+ this.gateway = gateway;
12
+ }
13
+ keyboard(taskId) { return [[{ text: '🛑 停止 / Stop', callback_data: `engine:stop:${taskId}` }]]; }
14
+ async sendCard(scopeId, taskId, html) {
15
+ const target = parseTelegramTargetFromBridgeScope(scopeId);
16
+ try {
17
+ return await this.gateway.sendRichMessage(target.chatId, telegramRichHtml(html), this.keyboard(taskId), target.topicId);
18
+ }
19
+ catch {
20
+ return this.gateway.sendHtmlMessage(target.chatId, html.replace(/<\/?p>/g, ''), this.keyboard(taskId), target.topicId);
21
+ }
22
+ }
23
+ async begin(scopeId, taskId, text, reuseMessageId = 0) {
24
+ const target = parseTelegramTargetFromBridgeScope(scopeId);
25
+ const html = renderTelegramMarkdownRichHtml(text);
26
+ const preview = { scopeId, draftId: null, messageId: reuseMessageId };
27
+ this.previews.set(taskId, preview);
28
+ if (reuseMessageId > 0) {
29
+ await this.gateway.editRichMessage(target.chatId, reuseMessageId, telegramRichHtml(html), this.keyboard(taskId));
30
+ return reuseMessageId;
31
+ }
32
+ if (/^[1-9]\d*$/.test(target.chatId)) {
33
+ let draftId = randomInt(1, 2 ** 31);
34
+ while (this.draftIds.has(draftId))
35
+ draftId = randomInt(1, 2 ** 31);
36
+ this.draftIds.add(draftId);
37
+ preview.draftId = draftId;
38
+ this.gateway.registerGeneration(draftId, scopeId, taskId, target.chatId, target.topicId);
39
+ try {
40
+ await this.gateway.sendRichMessageDraft(target.chatId, draftId, telegramRichHtml(html), target.topicId, true);
41
+ return 0;
42
+ }
43
+ catch {
44
+ this.gateway.releaseGeneration(draftId);
45
+ this.draftIds.delete(draftId);
46
+ preview.draftId = null;
47
+ }
48
+ }
49
+ preview.messageId = await this.sendCard(scopeId, taskId, html);
50
+ return preview.messageId;
51
+ }
52
+ async update(scopeId, taskId, messageId, html) {
53
+ const preview = this.previews.get(taskId);
54
+ if (!preview || preview.scopeId !== scopeId)
55
+ return messageId;
56
+ const target = parseTelegramTargetFromBridgeScope(scopeId);
57
+ if (preview.draftId !== null) {
58
+ try {
59
+ await this.gateway.sendRichMessageDraft(target.chatId, preview.draftId, telegramRichHtml(html), target.topicId, true);
60
+ return 0;
61
+ }
62
+ catch {
63
+ const draftId = preview.draftId;
64
+ this.gateway.releaseGeneration(draftId);
65
+ this.draftIds.delete(draftId);
66
+ preview.draftId = null;
67
+ await this.gateway.sendMessageDraft(target.chatId, draftId, '', target.topicId).catch(() => { });
68
+ preview.messageId = await this.sendCard(scopeId, taskId, html);
69
+ return preview.messageId;
70
+ }
71
+ }
72
+ try {
73
+ await this.gateway.editRichMessage(target.chatId, preview.messageId, telegramRichHtml(html), this.keyboard(taskId));
74
+ }
75
+ catch {
76
+ await this.gateway.editHtmlMessage(target.chatId, preview.messageId, html, this.keyboard(taskId));
77
+ }
78
+ return preview.messageId;
79
+ }
80
+ async end(scopeId, taskId) {
81
+ const preview = this.previews.get(taskId);
82
+ if (!preview || preview.scopeId !== scopeId)
83
+ return;
84
+ this.previews.delete(taskId);
85
+ if (preview.draftId !== null) {
86
+ this.gateway.releaseGeneration(preview.draftId);
87
+ this.draftIds.delete(preview.draftId);
88
+ const target = parseTelegramTargetFromBridgeScope(scopeId);
89
+ await this.gateway.sendMessageDraft(target.chatId, preview.draftId, '', target.topicId).catch(() => { });
90
+ }
91
+ }
92
+ }
@@ -0,0 +1,2 @@
1
+ /** Only the generated fold formatting is accepted; arbitrary model HTML remains escaped. */
2
+ export declare function telegramTaskResult(content: string): import("./rich.js").TelegramInputRichMessage;
@@ -0,0 +1,12 @@
1
+ import { telegramRichHtml, telegramRichMarkdown } from './rich.js';
2
+ import { renderTelegramMarkdownRichHtml } from './rich_markdown.js';
3
+ /** Only the generated fold formatting is accepted; arbitrary model HTML remains escaped. */
4
+ export function telegramTaskResult(content) {
5
+ const folded = /^(<blockquote expandable>[\s\S]*?<\/blockquote>)(?:\s*|$)/.exec(content);
6
+ if (!folded)
7
+ return telegramRichMarkdown(content, { skipEntityDetection: true });
8
+ const response = content.slice(folded[0].length);
9
+ const summary = folded[1].replace(/<[^>]*>/g, tag => /^(?:<\/?(?:b|i|code)>|<blockquote expandable>|<\/blockquote>)$/.test(tag)
10
+ ? tag : tag.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;'));
11
+ return telegramRichHtml(`${response.trim() ? renderTelegramMarkdownRichHtml(response) : ''}\n${summary}`, { skipEntityDetection: true });
12
+ }
@@ -2,6 +2,6 @@ export declare const TELEGRAM_MESSAGE_LIMIT = 4000;
2
2
  export declare const TELEGRAM_STREAM_MESSAGE_LIMIT = 1200;
3
3
  export declare const TELEGRAM_DRAFT_LIMIT = 4000;
4
4
  export declare function sanitizeTelegramPreview(text: string): string;
5
- export declare function chunkTelegramMessage(text: string, limit?: number, fallbackText?: string): string[];
5
+ export { chunkMessage as chunkTelegramMessage } from '../core/message_text.js';
6
6
  export declare function chunkTelegramStreamMessage(text: string, limit?: number): string[];
7
7
  export declare function clipTelegramDraftMessage(text: string, fallbackText?: string): string;
@@ -8,33 +8,8 @@ export function sanitizeTelegramPreview(text) {
8
8
  ? `${text.slice(0, TELEGRAM_MESSAGE_LIMIT - 3)}...`
9
9
  : text;
10
10
  }
11
- export function chunkTelegramMessage(text, limit = TELEGRAM_MESSAGE_LIMIT, fallbackText = 'Completed.') {
12
- const source = text.trim() ? text : fallbackText;
13
- if (!source) {
14
- return [];
15
- }
16
- if (source.length <= limit) {
17
- return [source];
18
- }
19
- const chunks = [];
20
- let start = 0;
21
- while (start < source.length) {
22
- const remaining = source.length - start;
23
- if (remaining <= limit) {
24
- chunks.push(source.slice(start));
25
- break;
26
- }
27
- const tentativeEnd = start + limit;
28
- const window = source.slice(start, tentativeEnd);
29
- const splitAt = Math.max(window.lastIndexOf('\n\n'), window.lastIndexOf('\n'));
30
- const end = splitAt >= Math.floor(limit / 2)
31
- ? start + splitAt + 1
32
- : tentativeEnd;
33
- chunks.push(source.slice(start, end));
34
- start = end;
35
- }
36
- return chunks.filter(chunk => chunk.length > 0);
37
- }
11
+ export { chunkMessage as chunkTelegramMessage } from '../core/message_text.js';
12
+ import { chunkMessage as chunkTelegramMessage } from '../core/message_text.js';
38
13
  export function chunkTelegramStreamMessage(text, limit = TELEGRAM_STREAM_MESSAGE_LIMIT) {
39
14
  return chunkTelegramMessage(text, limit, '');
40
15
  }
package/dist/types.d.ts CHANGED
@@ -17,7 +17,7 @@ export interface ChatSessionSettings {
17
17
  /** Bridge scope id (e.g. `telegram:…`). */
18
18
  chatId: string;
19
19
  model: string | null;
20
- reasoningEffort: ReasoningEffortValue | null;
20
+ reasoningEffort: string | null;
21
21
  locale: AppLocale | null;
22
22
  accessPreset: AccessPresetValue | null;
23
23
  collaborationMode: CollaborationModeValue | null;