@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.
- package/.env.example +18 -1
- package/CHANGELOG.md +36 -0
- package/README.md +4 -3
- package/README_EN.md +4 -3
- 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 -106
- package/dist/antigravity/controller.js +2 -2083
- 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/config.d.ts +4 -0
- package/dist/config.js +41 -5
- 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 +20 -5
- 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 +57 -22
- package/dist/core/orchestrator.js +877 -477
- 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.d.ts +48 -0
- package/dist/dsh/adapter.js +205 -0
- package/dist/dsh/backend.d.ts +7 -0
- package/dist/dsh/backend.js +21 -0
- package/dist/dsh/client.d.ts +46 -0
- package/dist/dsh/client.js +184 -0
- package/dist/dsh/permissions_plugin.d.ts +30 -0
- package/dist/dsh/permissions_plugin.js +21 -0
- package/dist/dsh/runtime.d.ts +20 -0
- package/dist/dsh/runtime.js +36 -0
- package/dist/dsh/ui.d.ts +38 -0
- package/dist/dsh/ui.js +342 -0
- package/dist/i18n.d.ts +4 -0
- package/dist/i18n.js +12 -0
- package/dist/main.js +144 -72
- 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 +10 -4
- package/dist/store/database.js +13 -3
- 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 +2 -2
- package/docs/dsh.md +83 -0
- package/docs/release.md +13 -11
- package/docs/zh/architecture-review.md +106 -0
- package/docs/zh/dsh.md +83 -0
- package/docs/zh/release.md +13 -11
- package/docs/zh/telegram-experience.md +40 -0
- 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
|
+
}
|
package/dist/telegram/api.d.ts
CHANGED
|
@@ -9,8 +9,8 @@ export interface TelegramRemoteFile {
|
|
|
9
9
|
file_size?: number;
|
|
10
10
|
file_path?: string;
|
|
11
11
|
}
|
|
12
|
-
export
|
|
13
|
-
export declare function callTelegramApi<T>(botToken: string, method: string, body: Record<string, unknown
|
|
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;
|
package/dist/telegram/api.js
CHANGED
|
@@ -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
|
|
8
|
-
|
|
9
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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;
|
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';
|