@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.
- package/CHANGELOG.md +20 -0
- package/dist/antigravity/account_ui.d.ts +34 -0
- package/dist/antigravity/account_ui.js +354 -0
- package/dist/antigravity/adapter.js +1 -1
- package/dist/antigravity/auth.d.ts +4 -1
- package/dist/antigravity/auth.js +17 -5
- package/dist/antigravity/controller.d.ts +3 -108
- package/dist/antigravity/controller.js +2 -2129
- package/dist/antigravity/conversation_ui.d.ts +56 -0
- package/dist/antigravity/conversation_ui.js +509 -0
- package/dist/antigravity/ui_callbacks.d.ts +6 -0
- package/dist/antigravity/ui_callbacks.js +6 -0
- package/dist/bridge/unified_bridge.d.ts +114 -0
- package/dist/bridge/unified_bridge.js +1343 -0
- package/dist/channels/bridge_messaging_router.d.ts +8 -4
- package/dist/channels/bridge_messaging_router.js +15 -4
- package/dist/channels/telegram/telegram_messaging_port.d.ts +16 -0
- package/dist/channels/telegram/telegram_messaging_port.js +22 -2
- package/dist/channels/text_actions.d.ts +14 -0
- package/dist/channels/text_actions.js +48 -0
- package/dist/channels/weixin/weixin_channel_adapter.d.ts +3 -4
- package/dist/channels/weixin/weixin_channel_adapter.js +2 -2
- package/dist/channels/weixin/weixin_messaging_port.d.ts +16 -1
- package/dist/channels/weixin/weixin_messaging_port.js +34 -7
- package/dist/codex_app/activity.d.ts +62 -0
- package/dist/codex_app/activity.js +418 -0
- package/dist/codex_app/adapter.d.ts +2 -2
- package/dist/codex_app/adapter.js +55 -25
- package/dist/codex_app/backend_ui.d.ts +4 -0
- package/dist/codex_app/backend_ui.js +48 -0
- package/dist/codex_app/client.js +0 -12
- package/dist/controller/access.d.ts +1 -11
- package/dist/controller/access.js +1 -33
- package/dist/controller/activity.d.ts +1 -62
- package/dist/controller/activity.js +2 -418
- package/dist/controller/auth_errors.d.ts +11 -0
- package/dist/controller/auth_errors.js +124 -0
- package/dist/controller/auth_files.d.ts +13 -0
- package/dist/controller/auth_files.js +156 -0
- package/dist/controller/auth_presentation.d.ts +77 -0
- package/dist/controller/auth_presentation.js +697 -0
- package/dist/controller/auth_quota_service.d.ts +27 -0
- package/dist/controller/auth_quota_service.js +151 -0
- package/dist/controller/controller.d.ts +30 -56
- package/dist/controller/controller.js +181 -3497
- package/dist/controller/interactions.d.ts +74 -0
- package/dist/controller/interactions.js +560 -0
- package/dist/controller/local_usage_service.d.ts +19 -0
- package/dist/controller/local_usage_service.js +62 -0
- package/dist/controller/native_panel_service.d.ts +55 -0
- package/dist/controller/native_panel_service.js +214 -0
- package/dist/controller/native_panels.d.ts +61 -0
- package/dist/controller/native_panels.js +784 -0
- package/dist/controller/shared_helpers.d.ts +53 -0
- package/dist/controller/shared_helpers.js +399 -0
- package/dist/controller/state_types.d.ts +390 -0
- package/dist/controller/state_types.js +82 -0
- package/dist/controller/turn_rendering.d.ts +22 -0
- package/dist/controller/turn_rendering.js +288 -0
- package/dist/core/access.d.ts +11 -0
- package/dist/core/access.js +33 -0
- package/dist/core/attachment_files.d.ts +13 -0
- package/dist/core/attachment_files.js +180 -0
- package/dist/core/attachment_types.d.ts +22 -0
- package/dist/core/attachment_types.js +1 -0
- package/dist/core/attachments.d.ts +5 -3
- package/dist/core/attachments.js +2 -2
- package/dist/core/backend_registry.d.ts +22 -0
- package/dist/core/backend_registry.js +50 -0
- package/dist/core/backend_ui.d.ts +28 -0
- package/dist/core/backend_ui.js +1 -0
- package/dist/core/channel_events.d.ts +48 -0
- package/dist/core/channel_events.js +1 -0
- package/dist/core/channel_port.d.ts +52 -8
- package/dist/core/engine_spi.d.ts +19 -4
- package/dist/core/listener_scope.d.ts +11 -0
- package/dist/core/listener_scope.js +10 -0
- package/dist/core/message_text.d.ts +2 -0
- package/dist/core/message_text.js +28 -0
- package/dist/core/network_errors.d.ts +1 -0
- package/dist/core/network_errors.js +23 -0
- package/dist/core/orchestrator.d.ts +53 -24
- package/dist/core/orchestrator.js +855 -507
- package/dist/core/queued_engine_input.d.ts +12 -0
- package/dist/core/queued_engine_input.js +16 -0
- package/dist/core/runtime_supervisor.d.ts +19 -0
- package/dist/core/runtime_supervisor.js +45 -0
- package/dist/core/scope_operations.d.ts +7 -0
- package/dist/core/scope_operations.js +19 -0
- package/dist/core/stream_preview.js +21 -9
- package/dist/dsh/adapter.js +18 -5
- package/dist/dsh/backend.d.ts +7 -0
- package/dist/dsh/backend.js +21 -0
- package/dist/dsh/runtime.d.ts +0 -1
- package/dist/dsh/runtime.js +4 -13
- package/dist/dsh/ui.d.ts +8 -8
- package/dist/dsh/ui.js +1 -1
- package/dist/main.js +55 -43
- package/dist/opencode/adapter.js +28 -9
- package/dist/opencode/controller.js +1 -1
- package/dist/store/channel_inbox.d.ts +14 -0
- package/dist/store/channel_inbox.js +21 -0
- package/dist/store/database.d.ts +7 -3
- package/dist/store/database.js +7 -2
- package/dist/store/task_journal.d.ts +32 -0
- package/dist/store/task_journal.js +70 -0
- package/dist/telegram/api.d.ts +2 -2
- package/dist/telegram/api.js +10 -28
- package/dist/telegram/gateway.d.ts +13 -27
- package/dist/telegram/gateway.js +86 -43
- package/dist/telegram/media.d.ts +2 -34
- package/dist/telegram/media.js +1 -180
- package/dist/telegram/task_preview.d.ts +13 -0
- package/dist/telegram/task_preview.js +92 -0
- package/dist/telegram/task_result.d.ts +2 -0
- package/dist/telegram/task_result.js +12 -0
- package/dist/telegram/text.d.ts +1 -1
- package/dist/telegram/text.js +2 -27
- package/dist/types.d.ts +1 -1
- package/docs/release.md +13 -11
- package/docs/zh/architecture-review.md +106 -0
- package/docs/zh/release.md +13 -11
- package/docs/zh/telegram-experience.md +40 -0
- package/package.json +5 -4
package/dist/telegram/gateway.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
315
|
+
await delay(this.pollIntervalMs, undefined, { signal }).catch(() => { });
|
|
283
316
|
continue;
|
|
284
317
|
}
|
|
285
318
|
for (const update of result.result) {
|
|
286
|
-
|
|
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
|
|
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.
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
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.
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
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)
|
package/dist/telegram/media.d.ts
CHANGED
|
@@ -1,34 +1,2 @@
|
|
|
1
|
-
export
|
|
2
|
-
export
|
|
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';
|
package/dist/telegram/media.js
CHANGED
|
@@ -1,180 +1 @@
|
|
|
1
|
-
|
|
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,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, '&').replace(/</g, '<').replace(/>/g, '>'));
|
|
11
|
+
return telegramRichHtml(`${response.trim() ? renderTelegramMarkdownRichHtml(response) : ''}\n${summary}`, { skipEntityDetection: true });
|
|
12
|
+
}
|
package/dist/telegram/text.d.ts
CHANGED
|
@@ -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
|
|
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;
|
package/dist/telegram/text.js
CHANGED
|
@@ -8,33 +8,8 @@ export function sanitizeTelegramPreview(text) {
|
|
|
8
8
|
? `${text.slice(0, TELEGRAM_MESSAGE_LIMIT - 3)}...`
|
|
9
9
|
: text;
|
|
10
10
|
}
|
|
11
|
-
export
|
|
12
|
-
|
|
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:
|
|
20
|
+
reasoningEffort: string | null;
|
|
21
21
|
locale: AppLocale | null;
|
|
22
22
|
accessPreset: AccessPresetValue | null;
|
|
23
23
|
collaborationMode: CollaborationModeValue | null;
|