@foxden-app/foxclaw 0.11.1 → 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 (138) hide show
  1. package/.env.example +18 -1
  2. package/CHANGELOG.md +36 -0
  3. package/README.md +4 -3
  4. package/README_EN.md +4 -3
  5. package/dist/antigravity/account_ui.d.ts +34 -0
  6. package/dist/antigravity/account_ui.js +354 -0
  7. package/dist/antigravity/adapter.js +1 -1
  8. package/dist/antigravity/auth.d.ts +4 -1
  9. package/dist/antigravity/auth.js +17 -5
  10. package/dist/antigravity/controller.d.ts +3 -106
  11. package/dist/antigravity/controller.js +2 -2083
  12. package/dist/antigravity/conversation_ui.d.ts +56 -0
  13. package/dist/antigravity/conversation_ui.js +509 -0
  14. package/dist/antigravity/ui_callbacks.d.ts +6 -0
  15. package/dist/antigravity/ui_callbacks.js +6 -0
  16. package/dist/bridge/unified_bridge.d.ts +114 -0
  17. package/dist/bridge/unified_bridge.js +1343 -0
  18. package/dist/channels/bridge_messaging_router.d.ts +8 -4
  19. package/dist/channels/bridge_messaging_router.js +15 -4
  20. package/dist/channels/telegram/telegram_messaging_port.d.ts +16 -0
  21. package/dist/channels/telegram/telegram_messaging_port.js +22 -2
  22. package/dist/channels/text_actions.d.ts +14 -0
  23. package/dist/channels/text_actions.js +48 -0
  24. package/dist/channels/weixin/weixin_channel_adapter.d.ts +3 -4
  25. package/dist/channels/weixin/weixin_channel_adapter.js +2 -2
  26. package/dist/channels/weixin/weixin_messaging_port.d.ts +16 -1
  27. package/dist/channels/weixin/weixin_messaging_port.js +34 -7
  28. package/dist/codex_app/activity.d.ts +62 -0
  29. package/dist/codex_app/activity.js +418 -0
  30. package/dist/codex_app/adapter.d.ts +2 -2
  31. package/dist/codex_app/adapter.js +55 -25
  32. package/dist/codex_app/backend_ui.d.ts +4 -0
  33. package/dist/codex_app/backend_ui.js +48 -0
  34. package/dist/codex_app/client.js +0 -12
  35. package/dist/config.d.ts +4 -0
  36. package/dist/config.js +41 -5
  37. package/dist/controller/access.d.ts +1 -11
  38. package/dist/controller/access.js +1 -33
  39. package/dist/controller/activity.d.ts +1 -62
  40. package/dist/controller/activity.js +2 -418
  41. package/dist/controller/auth_errors.d.ts +11 -0
  42. package/dist/controller/auth_errors.js +124 -0
  43. package/dist/controller/auth_files.d.ts +13 -0
  44. package/dist/controller/auth_files.js +156 -0
  45. package/dist/controller/auth_presentation.d.ts +77 -0
  46. package/dist/controller/auth_presentation.js +697 -0
  47. package/dist/controller/auth_quota_service.d.ts +27 -0
  48. package/dist/controller/auth_quota_service.js +151 -0
  49. package/dist/controller/controller.d.ts +30 -56
  50. package/dist/controller/controller.js +181 -3497
  51. package/dist/controller/interactions.d.ts +74 -0
  52. package/dist/controller/interactions.js +560 -0
  53. package/dist/controller/local_usage_service.d.ts +19 -0
  54. package/dist/controller/local_usage_service.js +62 -0
  55. package/dist/controller/native_panel_service.d.ts +55 -0
  56. package/dist/controller/native_panel_service.js +214 -0
  57. package/dist/controller/native_panels.d.ts +61 -0
  58. package/dist/controller/native_panels.js +784 -0
  59. package/dist/controller/shared_helpers.d.ts +53 -0
  60. package/dist/controller/shared_helpers.js +399 -0
  61. package/dist/controller/state_types.d.ts +390 -0
  62. package/dist/controller/state_types.js +82 -0
  63. package/dist/controller/turn_rendering.d.ts +22 -0
  64. package/dist/controller/turn_rendering.js +288 -0
  65. package/dist/core/access.d.ts +11 -0
  66. package/dist/core/access.js +33 -0
  67. package/dist/core/attachment_files.d.ts +13 -0
  68. package/dist/core/attachment_files.js +180 -0
  69. package/dist/core/attachment_types.d.ts +22 -0
  70. package/dist/core/attachment_types.js +1 -0
  71. package/dist/core/attachments.d.ts +5 -3
  72. package/dist/core/attachments.js +2 -2
  73. package/dist/core/backend_registry.d.ts +22 -0
  74. package/dist/core/backend_registry.js +50 -0
  75. package/dist/core/backend_ui.d.ts +28 -0
  76. package/dist/core/backend_ui.js +1 -0
  77. package/dist/core/channel_events.d.ts +48 -0
  78. package/dist/core/channel_events.js +1 -0
  79. package/dist/core/channel_port.d.ts +52 -8
  80. package/dist/core/engine_spi.d.ts +20 -5
  81. package/dist/core/listener_scope.d.ts +11 -0
  82. package/dist/core/listener_scope.js +10 -0
  83. package/dist/core/message_text.d.ts +2 -0
  84. package/dist/core/message_text.js +28 -0
  85. package/dist/core/network_errors.d.ts +1 -0
  86. package/dist/core/network_errors.js +23 -0
  87. package/dist/core/orchestrator.d.ts +57 -22
  88. package/dist/core/orchestrator.js +877 -477
  89. package/dist/core/queued_engine_input.d.ts +12 -0
  90. package/dist/core/queued_engine_input.js +16 -0
  91. package/dist/core/runtime_supervisor.d.ts +19 -0
  92. package/dist/core/runtime_supervisor.js +45 -0
  93. package/dist/core/scope_operations.d.ts +7 -0
  94. package/dist/core/scope_operations.js +19 -0
  95. package/dist/core/stream_preview.js +21 -9
  96. package/dist/dsh/adapter.d.ts +48 -0
  97. package/dist/dsh/adapter.js +205 -0
  98. package/dist/dsh/backend.d.ts +7 -0
  99. package/dist/dsh/backend.js +21 -0
  100. package/dist/dsh/client.d.ts +46 -0
  101. package/dist/dsh/client.js +184 -0
  102. package/dist/dsh/permissions_plugin.d.ts +30 -0
  103. package/dist/dsh/permissions_plugin.js +21 -0
  104. package/dist/dsh/runtime.d.ts +20 -0
  105. package/dist/dsh/runtime.js +36 -0
  106. package/dist/dsh/ui.d.ts +38 -0
  107. package/dist/dsh/ui.js +342 -0
  108. package/dist/i18n.d.ts +4 -0
  109. package/dist/i18n.js +12 -0
  110. package/dist/main.js +144 -72
  111. package/dist/opencode/adapter.js +28 -9
  112. package/dist/opencode/controller.js +1 -1
  113. package/dist/store/channel_inbox.d.ts +14 -0
  114. package/dist/store/channel_inbox.js +21 -0
  115. package/dist/store/database.d.ts +10 -4
  116. package/dist/store/database.js +13 -3
  117. package/dist/store/task_journal.d.ts +32 -0
  118. package/dist/store/task_journal.js +70 -0
  119. package/dist/telegram/api.d.ts +2 -2
  120. package/dist/telegram/api.js +10 -28
  121. package/dist/telegram/gateway.d.ts +13 -27
  122. package/dist/telegram/gateway.js +86 -43
  123. package/dist/telegram/media.d.ts +2 -34
  124. package/dist/telegram/media.js +1 -180
  125. package/dist/telegram/task_preview.d.ts +13 -0
  126. package/dist/telegram/task_preview.js +92 -0
  127. package/dist/telegram/task_result.d.ts +2 -0
  128. package/dist/telegram/task_result.js +12 -0
  129. package/dist/telegram/text.d.ts +1 -1
  130. package/dist/telegram/text.js +2 -27
  131. package/dist/types.d.ts +2 -2
  132. package/docs/dsh.md +83 -0
  133. package/docs/release.md +13 -11
  134. package/docs/zh/architecture-review.md +106 -0
  135. package/docs/zh/dsh.md +83 -0
  136. package/docs/zh/release.md +13 -11
  137. package/docs/zh/telegram-experience.md +40 -0
  138. package/package.json +7 -5
@@ -0,0 +1,70 @@
1
+ const transitions = {
2
+ accepted: ['queued', 'running', 'awaiting_confirmation', 'failed', 'cancelled'],
3
+ queued: ['running', 'failed', 'cancelled'],
4
+ running: ['running', 'awaiting_confirmation', 'delivery_pending', 'failed', 'cancelled'],
5
+ awaiting_confirmation: ['accepted', 'running', 'cancelled'],
6
+ delivery_pending: ['delivery_pending', 'completed', 'failed', 'cancelled'],
7
+ completed: [], failed: [], cancelled: [],
8
+ };
9
+ const unfinished = ['accepted', 'queued', 'running', 'awaiting_confirmation', 'delivery_pending'];
10
+ /** Original inputs, native outcome and delivery progress are distinct durable facts. */
11
+ export class TaskJournal {
12
+ db;
13
+ constructor(db) {
14
+ this.db = db;
15
+ db.exec(`CREATE TABLE IF NOT EXISTS engine_task_journal (
16
+ task_id TEXT PRIMARY KEY, scope_id TEXT NOT NULL, backend_id TEXT NOT NULL,
17
+ state TEXT NOT NULL, queue_id TEXT, record_json TEXT NOT NULL,
18
+ created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL);
19
+ CREATE INDEX IF NOT EXISTS engine_task_journal_scope_state ON engine_task_journal(scope_id, state, created_at);
20
+ CREATE INDEX IF NOT EXISTS engine_task_journal_receipt ON engine_task_journal(scope_id, json_extract(record_json, '$.event.messageId'));`);
21
+ }
22
+ atomic(operation) {
23
+ this.db.exec('SAVEPOINT task_journal_change');
24
+ try {
25
+ const result = operation();
26
+ this.db.exec('RELEASE task_journal_change');
27
+ return result;
28
+ }
29
+ catch (error) {
30
+ this.db.exec('ROLLBACK TO task_journal_change; RELEASE task_journal_change');
31
+ throw error;
32
+ }
33
+ }
34
+ insert(task) {
35
+ this.db.prepare(`INSERT INTO engine_task_journal VALUES (?, ?, ?, ?, ?, ?, ?, ?)`).run(task.id, task.event.scopeId, task.backendId, task.state, task.queueId, JSON.stringify(task), task.createdAt, task.updatedAt);
36
+ }
37
+ get(id) {
38
+ const row = this.db.prepare('SELECT record_json FROM engine_task_journal WHERE task_id = ?').get(id);
39
+ return row ? JSON.parse(String(row.record_json)) : null;
40
+ }
41
+ listUnfinished(scopeId) {
42
+ const placeholders = unfinished.map(() => '?').join(',');
43
+ const rows = this.db.prepare(`SELECT record_json FROM engine_task_journal WHERE state IN (${placeholders})${scopeId ? ' AND scope_id = ?' : ''} ORDER BY created_at, task_id`).all(...unfinished, ...(scopeId ? [scopeId] : []));
44
+ return rows.map(row => JSON.parse(String(row.record_json)));
45
+ }
46
+ findReceipt(event, prompt) {
47
+ if (event.messageId <= 0 || event.userId === 'system')
48
+ return null;
49
+ const row = this.db.prepare(`SELECT record_json FROM engine_task_journal WHERE scope_id = ?
50
+ AND json_extract(record_json, '$.event.messageId') = ? AND json_extract(record_json, '$.event.chatId') = ?
51
+ AND json_extract(record_json, '$.sourcePrompt') = ? LIMIT 1`).get(event.scopeId, event.messageId, event.chatId, prompt);
52
+ return row ? JSON.parse(String(row.record_json)) : null;
53
+ }
54
+ forQueue(queueId) {
55
+ const row = this.db.prepare('SELECT record_json FROM engine_task_journal WHERE queue_id = ?').get(queueId);
56
+ return row ? JSON.parse(String(row.record_json)) : null;
57
+ }
58
+ update(id, state, patch = {}) {
59
+ const previous = this.get(id);
60
+ if (!previous)
61
+ throw new Error(`Unknown task: ${id}`);
62
+ if (previous.state !== state && !transitions[previous.state].includes(state))
63
+ throw new Error(`Invalid task transition: ${previous.state} -> ${state}`);
64
+ if (['completed', 'failed', 'cancelled'].includes(previous.state))
65
+ throw new Error(`Task already resolved: ${id}`);
66
+ const next = { ...previous, ...patch, state, updatedAt: Date.now() };
67
+ this.db.prepare(`UPDATE engine_task_journal SET state = ?, queue_id = ?, record_json = ?, updated_at = ? WHERE task_id = ? AND state = ?`).run(state, next.queueId, JSON.stringify(next), next.updatedAt, id, previous.state);
68
+ return next;
69
+ }
70
+ }
@@ -9,8 +9,8 @@ export interface TelegramRemoteFile {
9
9
  file_size?: number;
10
10
  file_path?: string;
11
11
  }
12
- export declare function isTransientNetworkError(err: unknown): boolean;
13
- export declare function callTelegramApi<T>(botToken: string, method: string, body: Record<string, unknown>): Promise<TelegramApiResult<T>>;
12
+ export { isTransientNetworkError } from '../core/network_errors.js';
13
+ export declare function callTelegramApi<T>(botToken: string, method: string, body: Record<string, unknown>, signal?: AbortSignal): Promise<TelegramApiResult<T>>;
14
14
  export declare function callTelegramMultipartApi<T>(botToken: string, method: string, fields: Record<string, string>, files: Array<{
15
15
  fieldName: string;
16
16
  filename: string;
@@ -1,33 +1,13 @@
1
+ import { setTimeout as delay } from 'node:timers/promises';
1
2
  import fs from 'node:fs';
2
3
  import http from 'node:http';
3
4
  import https from 'node:https';
4
5
  import path from 'node:path';
5
6
  import { pipeline } from 'node:stream/promises';
6
7
  const DEFAULT_API_BASE_URL = 'https://api.telegram.org';
7
- export function isTransientNetworkError(err) {
8
- if (!err)
9
- return false;
10
- const msg = String(err.message || err).toLowerCase();
11
- return (msg.includes('client network socket disconnected') ||
12
- msg.includes('econnreset') ||
13
- msg.includes('etimedout') ||
14
- msg.includes('eai_again') ||
15
- msg.includes('enotfound') ||
16
- msg.includes('econnrefused') ||
17
- msg.includes('socket hang up') ||
18
- msg.includes('timed out') ||
19
- msg.includes('timeout') ||
20
- msg.includes('network socket disconnected') ||
21
- msg.includes('network error') ||
22
- msg.includes('fetch failed') ||
23
- msg.includes('und_err_') ||
24
- msg.includes('read tcp') ||
25
- msg.includes('connection timed out') ||
26
- msg.includes('broken pipe') ||
27
- msg.includes('ehostunreach') ||
28
- msg.includes('enetunreach'));
29
- }
30
- function callTelegramApiOnce(botToken, method, body) {
8
+ export { isTransientNetworkError } from '../core/network_errors.js';
9
+ import { isTransientNetworkError } from '../core/network_errors.js';
10
+ function callTelegramApiOnce(botToken, method, body, signal) {
31
11
  const payload = JSON.stringify(body);
32
12
  const endpoint = telegramApiUrl(botToken, method);
33
13
  return new Promise((resolve, reject) => {
@@ -37,7 +17,7 @@ function callTelegramApiOnce(botToken, method, body) {
37
17
  'content-type': 'application/json',
38
18
  'content-length': Buffer.byteLength(payload),
39
19
  },
40
- signal: AbortSignal.timeout(telegramApiTimeoutMs(false)),
20
+ signal: signal ? AbortSignal.any([signal, AbortSignal.timeout(telegramApiTimeoutMs(false))]) : AbortSignal.timeout(telegramApiTimeoutMs(false)),
41
21
  }, (response) => {
42
22
  response.on('error', reject);
43
23
  const chunks = [];
@@ -62,17 +42,19 @@ function callTelegramApiOnce(botToken, method, body) {
62
42
  request.end();
63
43
  });
64
44
  }
65
- export async function callTelegramApi(botToken, method, body) {
45
+ export async function callTelegramApi(botToken, method, body, signal) {
66
46
  const maxAttempts = method === 'getUpdates' ? 1 : 4;
67
47
  let lastError;
68
48
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
69
49
  try {
70
- return await callTelegramApiOnce(botToken, method, body);
50
+ signal?.throwIfAborted();
51
+ return await callTelegramApiOnce(botToken, method, body, signal);
71
52
  }
72
53
  catch (err) {
54
+ signal?.throwIfAborted();
73
55
  lastError = err;
74
56
  if (attempt < maxAttempts && isTransientNetworkError(err)) {
75
- await new Promise((r) => setTimeout(r, Math.min(3000, 500 * Math.pow(2, attempt - 1))));
57
+ await delay(Math.min(3000, 500 * Math.pow(2, attempt - 1)), undefined, { signal });
76
58
  continue;
77
59
  }
78
60
  throw err;
@@ -1,28 +1,15 @@
1
+ import type { ChannelInbound } from '../core/channel_events.js';
1
2
  import { EventEmitter } from 'node:events';
2
3
  import { type TelegramRemoteFile } from './api.js';
3
4
  import type { BridgeStore } from '../store/database.js';
4
5
  import type { Logger } from '../logger.js';
5
- import type { TelegramMessageEntity } from './addressing.js';
6
6
  import type { TelegramInboundAttachment } from './media.js';
7
7
  import type { TelegramInputRichMessage } from './rich.js';
8
8
  export type TelegramCommandProvider = (locale: 'en' | 'zh') => Array<{
9
9
  command: string;
10
10
  description: string;
11
11
  }>;
12
- export interface TelegramTextEvent {
13
- chatId: string;
14
- topicId: number | null;
15
- scopeId: string;
16
- chatType: string;
17
- userId: string;
18
- text: string;
19
- messageId: number;
20
- mediaGroupId?: string | null;
21
- attachments: TelegramInboundAttachment[];
22
- entities: TelegramMessageEntity[];
23
- replyToBot: boolean;
24
- languageCode?: string;
25
- }
12
+ export type { ChannelTextEvent as TelegramTextEvent } from '../core/channel_events.js';
26
13
  export interface TelegramPeerDocumentEvent {
27
14
  chatId: string;
28
15
  userId: string;
@@ -31,16 +18,7 @@ export interface TelegramPeerDocumentEvent {
31
18
  text: string;
32
19
  attachment: TelegramInboundAttachment;
33
20
  }
34
- export interface TelegramCallbackEvent {
35
- chatId: string;
36
- topicId: number | null;
37
- scopeId: string;
38
- userId: string;
39
- data: string;
40
- callbackQueryId: string;
41
- messageId: number;
42
- languageCode?: string;
43
- }
21
+ export type { ChannelCallbackEvent as TelegramCallbackEvent } from '../core/channel_events.js';
44
22
  export declare class TelegramGateway extends EventEmitter {
45
23
  private readonly botToken;
46
24
  private readonly allowedUserId;
@@ -50,7 +28,15 @@ export declare class TelegramGateway extends EventEmitter {
50
28
  private readonly logger;
51
29
  private readonly namespacedScopes;
52
30
  private readonly commandProvider;
31
+ private inboundConsumer;
32
+ private readonly generations;
33
+ setInboundConsumer(consumer: (id: string, inbound: ChannelInbound) => Promise<void>): () => void;
34
+ registerGeneration(draftId: number, scopeId: string, taskId: string, chatId: string, topicId: number | null): void;
35
+ releaseGeneration(draftId: number): void;
36
+ private dispatchInbound;
53
37
  private running;
38
+ private polling;
39
+ private pollAbort;
54
40
  private botKey;
55
41
  private botUsername;
56
42
  private botUserId;
@@ -60,7 +46,7 @@ export declare class TelegramGateway extends EventEmitter {
60
46
  initializeIdentity(): Promise<string>;
61
47
  resolveUsername(): Promise<string | null>;
62
48
  start(): Promise<void>;
63
- stop(): void;
49
+ stop(): Promise<void>;
64
50
  sendMessage(chatId: string, text: string, inlineKeyboard?: Array<Array<{
65
51
  text: string;
66
52
  callback_data: string;
@@ -76,7 +62,7 @@ export declare class TelegramGateway extends EventEmitter {
76
62
  sendDocument(chatId: string, filename: string, contents: Buffer, caption?: string): Promise<number>;
77
63
  sendVoice(chatId: string, filename: string, contents: Buffer, caption?: string, messageThreadId?: number | null, contentType?: string): Promise<number>;
78
64
  sendMessageDraft(chatId: string, draftId: number, text: string, messageThreadId?: number | null): Promise<void>;
79
- sendRichMessageDraft(chatId: string, draftId: number, richMessage: TelegramInputRichMessage, messageThreadId?: number | null): Promise<void>;
65
+ sendRichMessageDraft(chatId: string, draftId: number, richMessage: TelegramInputRichMessage, messageThreadId?: number | null, canStop?: boolean): Promise<void>;
80
66
  editMessage(chatId: string, messageId: number, text: string, inlineKeyboard?: Array<Array<{
81
67
  text: string;
82
68
  callback_data: string;
@@ -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';