zardbot-telegram 1.0.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 +116 -0
- package/LICENSE +21 -0
- package/README.md +250 -0
- package/dist/agent/manager.js +88 -0
- package/dist/agent/types.js +26 -0
- package/dist/app/start-bot-app.js +49 -0
- package/dist/bot/commands/abort.js +121 -0
- package/dist/bot/commands/commands.js +480 -0
- package/dist/bot/commands/definitions.js +27 -0
- package/dist/bot/commands/help.js +10 -0
- package/dist/bot/commands/models.js +38 -0
- package/dist/bot/commands/new.js +70 -0
- package/dist/bot/commands/opencode-start.js +101 -0
- package/dist/bot/commands/opencode-stop.js +44 -0
- package/dist/bot/commands/projects.js +223 -0
- package/dist/bot/commands/rename.js +139 -0
- package/dist/bot/commands/sessions.js +351 -0
- package/dist/bot/commands/start.js +43 -0
- package/dist/bot/commands/status.js +95 -0
- package/dist/bot/commands/task.js +399 -0
- package/dist/bot/commands/tasklist.js +220 -0
- package/dist/bot/commands/voice.js +145 -0
- package/dist/bot/handlers/agent.js +118 -0
- package/dist/bot/handlers/context.js +100 -0
- package/dist/bot/handlers/document.js +65 -0
- package/dist/bot/handlers/inline-menu.js +119 -0
- package/dist/bot/handlers/model.js +143 -0
- package/dist/bot/handlers/permission.js +235 -0
- package/dist/bot/handlers/prompt.js +240 -0
- package/dist/bot/handlers/question.js +390 -0
- package/dist/bot/handlers/tts.js +89 -0
- package/dist/bot/handlers/variant.js +138 -0
- package/dist/bot/handlers/voice.js +173 -0
- package/dist/bot/index.js +977 -0
- package/dist/bot/message-patterns.js +4 -0
- package/dist/bot/middleware/auth.js +30 -0
- package/dist/bot/middleware/interaction-guard.js +95 -0
- package/dist/bot/middleware/unknown-command.js +22 -0
- package/dist/bot/streaming/response-streamer.js +286 -0
- package/dist/bot/streaming/tool-call-streamer.js +285 -0
- package/dist/bot/utils/busy-guard.js +15 -0
- package/dist/bot/utils/commands.js +21 -0
- package/dist/bot/utils/file-download.js +91 -0
- package/dist/bot/utils/finalize-assistant-response.js +52 -0
- package/dist/bot/utils/keyboard.js +69 -0
- package/dist/bot/utils/send-with-markdown-fallback.js +165 -0
- package/dist/bot/utils/telegram-text.js +28 -0
- package/dist/bot/utils/thinking-message.js +8 -0
- package/dist/cli/args.js +98 -0
- package/dist/cli.js +80 -0
- package/dist/config.js +97 -0
- package/dist/i18n/de.js +357 -0
- package/dist/i18n/en.js +357 -0
- package/dist/i18n/es.js +357 -0
- package/dist/i18n/fr.js +357 -0
- package/dist/i18n/index.js +109 -0
- package/dist/i18n/ru.js +357 -0
- package/dist/i18n/zh.js +357 -0
- package/dist/index.js +26 -0
- package/dist/interaction/busy.js +8 -0
- package/dist/interaction/cleanup.js +32 -0
- package/dist/interaction/guard.js +140 -0
- package/dist/interaction/manager.js +106 -0
- package/dist/interaction/types.js +1 -0
- package/dist/keyboard/manager.js +172 -0
- package/dist/keyboard/types.js +1 -0
- package/dist/model/capabilities.js +62 -0
- package/dist/model/context-limit.js +57 -0
- package/dist/model/manager.js +259 -0
- package/dist/model/types.js +24 -0
- package/dist/opencode/client.js +13 -0
- package/dist/opencode/events.js +140 -0
- package/dist/permission/manager.js +100 -0
- package/dist/permission/types.js +1 -0
- package/dist/pinned/format.js +29 -0
- package/dist/pinned/manager.js +682 -0
- package/dist/pinned/types.js +1 -0
- package/dist/process/manager.js +273 -0
- package/dist/process/types.js +1 -0
- package/dist/project/manager.js +88 -0
- package/dist/question/manager.js +176 -0
- package/dist/question/types.js +1 -0
- package/dist/rename/manager.js +53 -0
- package/dist/runtime/bootstrap.js +350 -0
- package/dist/runtime/mode.js +74 -0
- package/dist/runtime/paths.js +37 -0
- package/dist/scheduled-task/creation-manager.js +113 -0
- package/dist/scheduled-task/display.js +239 -0
- package/dist/scheduled-task/executor.js +87 -0
- package/dist/scheduled-task/foreground-state.js +32 -0
- package/dist/scheduled-task/next-run.js +207 -0
- package/dist/scheduled-task/runtime.js +368 -0
- package/dist/scheduled-task/schedule-parser.js +169 -0
- package/dist/scheduled-task/store.js +65 -0
- package/dist/scheduled-task/types.js +19 -0
- package/dist/session/cache-manager.js +455 -0
- package/dist/session/manager.js +10 -0
- package/dist/settings/manager.js +158 -0
- package/dist/stt/client.js +97 -0
- package/dist/summary/aggregator.js +1136 -0
- package/dist/summary/formatter.js +491 -0
- package/dist/summary/subagent-formatter.js +63 -0
- package/dist/summary/tool-message-batcher.js +90 -0
- package/dist/tts/client.js +130 -0
- package/dist/utils/error-format.js +29 -0
- package/dist/utils/logger.js +127 -0
- package/dist/utils/safe-background-task.js +33 -0
- package/dist/utils/telegram-rate-limit-retry.js +93 -0
- package/dist/variant/manager.js +103 -0
- package/dist/variant/types.js +1 -0
- package/package.json +79 -0
|
@@ -0,0 +1,977 @@
|
|
|
1
|
+
import { Bot, InputFile } from "grammy";
|
|
2
|
+
import { promises as fs } from "fs";
|
|
3
|
+
import * as path from "path";
|
|
4
|
+
import { fileURLToPath } from "url";
|
|
5
|
+
import { SocksProxyAgent } from "socks-proxy-agent";
|
|
6
|
+
import { HttpsProxyAgent } from "https-proxy-agent";
|
|
7
|
+
import { config } from "../config.js";
|
|
8
|
+
import { authMiddleware } from "./middleware/auth.js";
|
|
9
|
+
import { interactionGuardMiddleware } from "./middleware/interaction-guard.js";
|
|
10
|
+
import { unknownCommandMiddleware } from "./middleware/unknown-command.js";
|
|
11
|
+
import { BOT_COMMANDS } from "./commands/definitions.js";
|
|
12
|
+
import { startCommand } from "./commands/start.js";
|
|
13
|
+
import { helpCommand } from "./commands/help.js";
|
|
14
|
+
import { statusCommand } from "./commands/status.js";
|
|
15
|
+
import { AGENT_MODE_BUTTON_TEXT_PATTERN, MODEL_BUTTON_TEXT_PATTERN, VARIANT_BUTTON_TEXT_PATTERN, } from "./message-patterns.js";
|
|
16
|
+
import { sessionsCommand, handleSessionSelect } from "./commands/sessions.js";
|
|
17
|
+
import { newCommand } from "./commands/new.js";
|
|
18
|
+
import { projectsCommand, handleProjectSelect } from "./commands/projects.js";
|
|
19
|
+
import { abortCommand } from "./commands/abort.js";
|
|
20
|
+
import { opencodeStartCommand } from "./commands/opencode-start.js";
|
|
21
|
+
import { opencodeStopCommand } from "./commands/opencode-stop.js";
|
|
22
|
+
import { renameCommand, handleRenameCancel, handleRenameTextAnswer } from "./commands/rename.js";
|
|
23
|
+
import { handleTaskCallback, handleTaskTextInput, taskCommand } from "./commands/task.js";
|
|
24
|
+
import { handleTaskListCallback, taskListCommand } from "./commands/tasklist.js";
|
|
25
|
+
import { commandsCommand, handleCommandsCallback, handleCommandTextArguments, } from "./commands/commands.js";
|
|
26
|
+
import { voiceCommand, handleVoiceCallback } from "./commands/voice.js";
|
|
27
|
+
import { handleQuestionCallback, showCurrentQuestion, handleQuestionTextAnswer, } from "./handlers/question.js";
|
|
28
|
+
import { handlePermissionCallback, showPermissionRequest } from "./handlers/permission.js";
|
|
29
|
+
import { handleAgentSelect, showAgentSelectionMenu } from "./handlers/agent.js";
|
|
30
|
+
import { handleModelSelect, showModelSelectionMenu } from "./handlers/model.js";
|
|
31
|
+
import { handleVariantSelect, showVariantSelectionMenu } from "./handlers/variant.js";
|
|
32
|
+
import { handleContextButtonPress, handleCompactConfirm } from "./handlers/context.js";
|
|
33
|
+
import { handleInlineMenuCancel } from "./handlers/inline-menu.js";
|
|
34
|
+
import { questionManager } from "../question/manager.js";
|
|
35
|
+
import { interactionManager } from "../interaction/manager.js";
|
|
36
|
+
import { clearAllInteractionState } from "../interaction/cleanup.js";
|
|
37
|
+
import { keyboardManager } from "../keyboard/manager.js";
|
|
38
|
+
import { subscribeToEvents } from "../opencode/events.js";
|
|
39
|
+
import { summaryAggregator } from "../summary/aggregator.js";
|
|
40
|
+
import { formatSummary, formatSummaryWithMode, formatToolInfo, getAssistantParseMode, } from "../summary/formatter.js";
|
|
41
|
+
import { renderSubagentCards } from "../summary/subagent-formatter.js";
|
|
42
|
+
import { ToolMessageBatcher } from "../summary/tool-message-batcher.js";
|
|
43
|
+
import { getCurrentSession } from "../session/manager.js";
|
|
44
|
+
import { ingestSessionInfoForCache } from "../session/cache-manager.js";
|
|
45
|
+
import { logger } from "../utils/logger.js";
|
|
46
|
+
import { safeBackgroundTask } from "../utils/safe-background-task.js";
|
|
47
|
+
import { withTelegramRateLimitRetry } from "../utils/telegram-rate-limit-retry.js";
|
|
48
|
+
import { pinnedMessageManager } from "../pinned/manager.js";
|
|
49
|
+
import { t } from "../i18n/index.js";
|
|
50
|
+
import { processUserPrompt } from "./handlers/prompt.js";
|
|
51
|
+
import { handleVoiceMessage } from "./handlers/voice.js";
|
|
52
|
+
import { handleDocumentMessage } from "./handlers/document.js";
|
|
53
|
+
import { downloadTelegramFile, toDataUri } from "./utils/file-download.js";
|
|
54
|
+
import { finalizeAssistantResponse } from "./utils/finalize-assistant-response.js";
|
|
55
|
+
import { deliverThinkingMessage } from "./utils/thinking-message.js";
|
|
56
|
+
import { sendBotText } from "./utils/telegram-text.js";
|
|
57
|
+
import { getModelCapabilities, supportsInput } from "../model/capabilities.js";
|
|
58
|
+
import { getStoredModel } from "../model/manager.js";
|
|
59
|
+
import { foregroundSessionState } from "../scheduled-task/foreground-state.js";
|
|
60
|
+
import { scheduledTaskRuntime } from "../scheduled-task/runtime.js";
|
|
61
|
+
import { ResponseStreamer } from "./streaming/response-streamer.js";
|
|
62
|
+
import { ToolCallStreamer } from "./streaming/tool-call-streamer.js";
|
|
63
|
+
import { editMessageWithMarkdownFallback, sendMessageWithMarkdownFallback, } from "./utils/send-with-markdown-fallback.js";
|
|
64
|
+
let botInstance = null;
|
|
65
|
+
let chatIdInstance = null;
|
|
66
|
+
let commandsInitialized = false;
|
|
67
|
+
const TELEGRAM_DOCUMENT_CAPTION_MAX_LENGTH = 1024;
|
|
68
|
+
const RESPONSE_STREAM_THROTTLE_MS = config.bot.responseStreamThrottleMs;
|
|
69
|
+
const RESPONSE_STREAM_TEXT_LIMIT = 3800;
|
|
70
|
+
const SESSION_RETRY_PREFIX = "🔁";
|
|
71
|
+
const SUBAGENT_STREAM_PREFIX = "🧩";
|
|
72
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
73
|
+
const __dirname = path.dirname(__filename);
|
|
74
|
+
const TEMP_DIR = path.join(__dirname, "..", ".tmp");
|
|
75
|
+
function getCurrentReplyKeyboard() {
|
|
76
|
+
if (!keyboardManager.isInitialized()) {
|
|
77
|
+
return undefined;
|
|
78
|
+
}
|
|
79
|
+
return keyboardManager.getKeyboard();
|
|
80
|
+
}
|
|
81
|
+
function prepareDocumentCaption(caption) {
|
|
82
|
+
const normalizedCaption = caption.trim();
|
|
83
|
+
if (!normalizedCaption) {
|
|
84
|
+
return "";
|
|
85
|
+
}
|
|
86
|
+
if (normalizedCaption.length <= TELEGRAM_DOCUMENT_CAPTION_MAX_LENGTH) {
|
|
87
|
+
return normalizedCaption;
|
|
88
|
+
}
|
|
89
|
+
return `${normalizedCaption.slice(0, TELEGRAM_DOCUMENT_CAPTION_MAX_LENGTH - 3)}...`;
|
|
90
|
+
}
|
|
91
|
+
function prepareStreamingPayload(messageText) {
|
|
92
|
+
const parts = formatSummaryWithMode(messageText, config.bot.messageFormatMode, RESPONSE_STREAM_TEXT_LIMIT);
|
|
93
|
+
if (parts.length === 0) {
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
return {
|
|
97
|
+
parts,
|
|
98
|
+
format: "raw",
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
const toolMessageBatcher = new ToolMessageBatcher({
|
|
102
|
+
sendText: async (sessionId, text) => {
|
|
103
|
+
if (!botInstance || !chatIdInstance) {
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
const currentSession = getCurrentSession();
|
|
107
|
+
if (!currentSession || currentSession.id !== sessionId) {
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
const keyboard = getCurrentReplyKeyboard();
|
|
111
|
+
await botInstance.api.sendMessage(chatIdInstance, text, {
|
|
112
|
+
disable_notification: true,
|
|
113
|
+
...(keyboard ? { reply_markup: keyboard } : {}),
|
|
114
|
+
});
|
|
115
|
+
},
|
|
116
|
+
sendFile: async (sessionId, fileData) => {
|
|
117
|
+
if (!botInstance || !chatIdInstance) {
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
const currentSession = getCurrentSession();
|
|
121
|
+
if (!currentSession || currentSession.id !== sessionId) {
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
const tempFilePath = path.join(TEMP_DIR, fileData.filename);
|
|
125
|
+
try {
|
|
126
|
+
logger.debug(`[Bot] Sending code file: ${fileData.filename} (${fileData.buffer.length} bytes, session=${sessionId})`);
|
|
127
|
+
await fs.mkdir(TEMP_DIR, { recursive: true });
|
|
128
|
+
await fs.writeFile(tempFilePath, fileData.buffer);
|
|
129
|
+
const keyboard = getCurrentReplyKeyboard();
|
|
130
|
+
await botInstance.api.sendDocument(chatIdInstance, new InputFile(tempFilePath), {
|
|
131
|
+
caption: fileData.caption,
|
|
132
|
+
disable_notification: true,
|
|
133
|
+
...(keyboard ? { reply_markup: keyboard } : {}),
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
finally {
|
|
137
|
+
await fs.unlink(tempFilePath).catch(() => { });
|
|
138
|
+
}
|
|
139
|
+
},
|
|
140
|
+
});
|
|
141
|
+
const responseStreamer = new ResponseStreamer({
|
|
142
|
+
throttleMs: RESPONSE_STREAM_THROTTLE_MS,
|
|
143
|
+
sendText: async (text, format, options) => {
|
|
144
|
+
if (!botInstance || !chatIdInstance || chatIdInstance <= 0) {
|
|
145
|
+
throw new Error("Bot context missing for streamed send");
|
|
146
|
+
}
|
|
147
|
+
const parseMode = format === "markdown_v2" ? "MarkdownV2" : undefined;
|
|
148
|
+
const sentMessage = await sendMessageWithMarkdownFallback({
|
|
149
|
+
api: botInstance.api,
|
|
150
|
+
chatId: chatIdInstance,
|
|
151
|
+
text,
|
|
152
|
+
options,
|
|
153
|
+
parseMode,
|
|
154
|
+
});
|
|
155
|
+
return sentMessage.message_id;
|
|
156
|
+
},
|
|
157
|
+
editText: async (messageId, text, format, options) => {
|
|
158
|
+
if (!botInstance || !chatIdInstance || chatIdInstance <= 0) {
|
|
159
|
+
throw new Error("Bot context missing for streamed edit");
|
|
160
|
+
}
|
|
161
|
+
const parseMode = format === "markdown_v2" ? "MarkdownV2" : undefined;
|
|
162
|
+
try {
|
|
163
|
+
await editMessageWithMarkdownFallback({
|
|
164
|
+
api: botInstance.api,
|
|
165
|
+
chatId: chatIdInstance,
|
|
166
|
+
messageId,
|
|
167
|
+
text,
|
|
168
|
+
options,
|
|
169
|
+
parseMode,
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
catch (error) {
|
|
173
|
+
const errorMessage = error instanceof Error ? error.message.toLowerCase() : String(error).toLowerCase();
|
|
174
|
+
if (errorMessage.includes("message is not modified")) {
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
throw error;
|
|
178
|
+
}
|
|
179
|
+
},
|
|
180
|
+
deleteText: async (messageId) => {
|
|
181
|
+
if (!botInstance || !chatIdInstance || chatIdInstance <= 0) {
|
|
182
|
+
throw new Error("Bot context missing for streamed delete");
|
|
183
|
+
}
|
|
184
|
+
await botInstance.api.deleteMessage(chatIdInstance, messageId).catch((error) => {
|
|
185
|
+
const errorMessage = error instanceof Error ? error.message.toLowerCase() : String(error).toLowerCase();
|
|
186
|
+
if (errorMessage.includes("message to delete not found") ||
|
|
187
|
+
errorMessage.includes("message identifier is not specified")) {
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
throw error;
|
|
191
|
+
});
|
|
192
|
+
},
|
|
193
|
+
});
|
|
194
|
+
const toolCallStreamer = new ToolCallStreamer({
|
|
195
|
+
throttleMs: RESPONSE_STREAM_THROTTLE_MS,
|
|
196
|
+
sendText: async (sessionId, text) => {
|
|
197
|
+
if (!botInstance || !chatIdInstance || chatIdInstance <= 0) {
|
|
198
|
+
throw new Error("Bot context missing for tool stream send");
|
|
199
|
+
}
|
|
200
|
+
const currentSession = getCurrentSession();
|
|
201
|
+
if (!currentSession || currentSession.id !== sessionId) {
|
|
202
|
+
throw new Error(`Tool stream session mismatch for send: ${sessionId}`);
|
|
203
|
+
}
|
|
204
|
+
const sentMessage = await botInstance.api.sendMessage(chatIdInstance, text, {
|
|
205
|
+
disable_notification: true,
|
|
206
|
+
});
|
|
207
|
+
return sentMessage.message_id;
|
|
208
|
+
},
|
|
209
|
+
editText: async (sessionId, messageId, text) => {
|
|
210
|
+
if (!botInstance || !chatIdInstance || chatIdInstance <= 0) {
|
|
211
|
+
throw new Error("Bot context missing for tool stream edit");
|
|
212
|
+
}
|
|
213
|
+
const currentSession = getCurrentSession();
|
|
214
|
+
if (!currentSession || currentSession.id !== sessionId) {
|
|
215
|
+
throw new Error(`Tool stream session mismatch for edit: ${sessionId}`);
|
|
216
|
+
}
|
|
217
|
+
try {
|
|
218
|
+
await botInstance.api.editMessageText(chatIdInstance, messageId, text);
|
|
219
|
+
}
|
|
220
|
+
catch (error) {
|
|
221
|
+
const errorMessage = error instanceof Error ? error.message.toLowerCase() : String(error).toLowerCase();
|
|
222
|
+
if (errorMessage.includes("message is not modified")) {
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
throw error;
|
|
226
|
+
}
|
|
227
|
+
},
|
|
228
|
+
deleteText: async (sessionId, messageId) => {
|
|
229
|
+
if (!botInstance || !chatIdInstance || chatIdInstance <= 0) {
|
|
230
|
+
throw new Error("Bot context missing for tool stream delete");
|
|
231
|
+
}
|
|
232
|
+
const currentSession = getCurrentSession();
|
|
233
|
+
if (!currentSession || currentSession.id !== sessionId) {
|
|
234
|
+
throw new Error(`Tool stream session mismatch for delete: ${sessionId}`);
|
|
235
|
+
}
|
|
236
|
+
await botInstance.api.deleteMessage(chatIdInstance, messageId).catch((error) => {
|
|
237
|
+
const errorMessage = error instanceof Error ? error.message.toLowerCase() : String(error).toLowerCase();
|
|
238
|
+
if (errorMessage.includes("message to delete not found") ||
|
|
239
|
+
errorMessage.includes("message identifier is not specified")) {
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
throw error;
|
|
243
|
+
});
|
|
244
|
+
},
|
|
245
|
+
});
|
|
246
|
+
async function ensureCommandsInitialized(ctx, next) {
|
|
247
|
+
if (commandsInitialized || !ctx.from || ctx.from.id !== config.telegram.allowedUserId) {
|
|
248
|
+
await next();
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
if (!ctx.chat) {
|
|
252
|
+
logger.warn("[Bot] Cannot initialize commands: chat context is missing");
|
|
253
|
+
await next();
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
try {
|
|
257
|
+
await ctx.api.setMyCommands(BOT_COMMANDS, {
|
|
258
|
+
scope: {
|
|
259
|
+
type: "chat",
|
|
260
|
+
chat_id: ctx.chat.id,
|
|
261
|
+
},
|
|
262
|
+
});
|
|
263
|
+
commandsInitialized = true;
|
|
264
|
+
logger.debug(`[Bot] Commands initialized for authorized user (chat_id=${ctx.chat.id})`);
|
|
265
|
+
}
|
|
266
|
+
catch (err) {
|
|
267
|
+
logger.error("[Bot] Failed to set commands:", err);
|
|
268
|
+
}
|
|
269
|
+
await next();
|
|
270
|
+
}
|
|
271
|
+
async function ensureEventSubscription(directory) {
|
|
272
|
+
if (!directory) {
|
|
273
|
+
logger.error("No directory found for event subscription");
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
summaryAggregator.setTypingIndicatorEnabled(true);
|
|
277
|
+
summaryAggregator.setOnCleared(() => {
|
|
278
|
+
toolMessageBatcher.clearAll("summary_aggregator_clear");
|
|
279
|
+
toolCallStreamer.clearAll("summary_aggregator_clear");
|
|
280
|
+
responseStreamer.clearAll("summary_aggregator_clear");
|
|
281
|
+
});
|
|
282
|
+
summaryAggregator.setOnPartial((sessionId, messageId, messageText) => {
|
|
283
|
+
if (!botInstance || !chatIdInstance) {
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
const currentSession = getCurrentSession();
|
|
287
|
+
if (!currentSession || currentSession.id !== sessionId) {
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
const preparedStreamPayload = prepareStreamingPayload(messageText);
|
|
291
|
+
if (!preparedStreamPayload) {
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
preparedStreamPayload.sendOptions = { disable_notification: true };
|
|
295
|
+
preparedStreamPayload.editOptions = undefined;
|
|
296
|
+
responseStreamer.enqueue(sessionId, messageId, preparedStreamPayload);
|
|
297
|
+
});
|
|
298
|
+
summaryAggregator.setOnComplete(async (sessionId, messageId, messageText) => {
|
|
299
|
+
if (!botInstance || !chatIdInstance) {
|
|
300
|
+
logger.error("Bot or chat ID not available for sending message");
|
|
301
|
+
responseStreamer.clearMessage(sessionId, messageId, "bot_context_missing");
|
|
302
|
+
toolCallStreamer.clearSession(sessionId, "bot_context_missing");
|
|
303
|
+
foregroundSessionState.markIdle(sessionId);
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
const currentSession = getCurrentSession();
|
|
307
|
+
if (currentSession?.id !== sessionId) {
|
|
308
|
+
responseStreamer.clearMessage(sessionId, messageId, "session_mismatch");
|
|
309
|
+
toolCallStreamer.clearSession(sessionId, "session_mismatch");
|
|
310
|
+
foregroundSessionState.markIdle(sessionId);
|
|
311
|
+
await scheduledTaskRuntime.flushDeferredDeliveries();
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
const botApi = botInstance.api;
|
|
315
|
+
const chatId = chatIdInstance;
|
|
316
|
+
try {
|
|
317
|
+
await finalizeAssistantResponse({
|
|
318
|
+
sessionId,
|
|
319
|
+
messageId,
|
|
320
|
+
messageText,
|
|
321
|
+
responseStreamer,
|
|
322
|
+
flushPendingServiceMessages: () => Promise.all([
|
|
323
|
+
toolMessageBatcher.flushSession(sessionId, "assistant_message_completed"),
|
|
324
|
+
toolCallStreamer.flushSession(sessionId, "assistant_message_completed"),
|
|
325
|
+
]).then(() => undefined),
|
|
326
|
+
prepareStreamingPayload,
|
|
327
|
+
formatSummary,
|
|
328
|
+
formatRawSummary: (text) => formatSummaryWithMode(text, "raw"),
|
|
329
|
+
resolveFormat: () => (getAssistantParseMode() === "MarkdownV2" ? "markdown_v2" : "raw"),
|
|
330
|
+
getReplyKeyboard: getCurrentReplyKeyboard,
|
|
331
|
+
sendText: async (text, rawFallbackText, options, format) => {
|
|
332
|
+
await sendBotText({
|
|
333
|
+
api: botApi,
|
|
334
|
+
chatId,
|
|
335
|
+
text,
|
|
336
|
+
rawFallbackText,
|
|
337
|
+
options: options,
|
|
338
|
+
format,
|
|
339
|
+
});
|
|
340
|
+
},
|
|
341
|
+
deleteMessages: async (messageIds) => {
|
|
342
|
+
for (const msgId of messageIds) {
|
|
343
|
+
try {
|
|
344
|
+
await botApi.deleteMessage(chatId, msgId);
|
|
345
|
+
}
|
|
346
|
+
catch (err) {
|
|
347
|
+
logger.warn(`[Bot] Failed to delete streamed message ${msgId}:`, err);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
},
|
|
351
|
+
api: botApi,
|
|
352
|
+
chatId,
|
|
353
|
+
enableTts: true,
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
catch (err) {
|
|
357
|
+
logger.error("Failed to send message to Telegram:", err);
|
|
358
|
+
// Stop processing events after critical error to prevent infinite loop
|
|
359
|
+
logger.error("[Bot] CRITICAL: Stopping event processing due to error");
|
|
360
|
+
summaryAggregator.clear();
|
|
361
|
+
}
|
|
362
|
+
finally {
|
|
363
|
+
foregroundSessionState.markIdle(sessionId);
|
|
364
|
+
await scheduledTaskRuntime.flushDeferredDeliveries();
|
|
365
|
+
}
|
|
366
|
+
});
|
|
367
|
+
summaryAggregator.setOnTool(async (toolInfo) => {
|
|
368
|
+
if (!botInstance || !chatIdInstance) {
|
|
369
|
+
logger.error("Bot or chat ID not available for sending tool notification");
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
const currentSession = getCurrentSession();
|
|
373
|
+
if (!currentSession || currentSession.id !== toolInfo.sessionId) {
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
const shouldIncludeToolInfoInFileCaption = toolInfo.hasFileAttachment &&
|
|
377
|
+
(toolInfo.tool === "write" || toolInfo.tool === "edit" || toolInfo.tool === "apply_patch");
|
|
378
|
+
if (config.bot.hideToolCallMessages ||
|
|
379
|
+
shouldIncludeToolInfoInFileCaption ||
|
|
380
|
+
toolInfo.tool === "task") {
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
try {
|
|
384
|
+
const message = formatToolInfo(toolInfo);
|
|
385
|
+
if (message) {
|
|
386
|
+
toolCallStreamer.append(toolInfo.sessionId, message);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
catch (err) {
|
|
390
|
+
logger.error("Failed to send tool notification to Telegram:", err);
|
|
391
|
+
}
|
|
392
|
+
});
|
|
393
|
+
summaryAggregator.setOnSubagent(async (sessionId, subagents) => {
|
|
394
|
+
if (!botInstance || !chatIdInstance) {
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
if (config.bot.hideToolCallMessages) {
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
const currentSession = getCurrentSession();
|
|
401
|
+
if (!currentSession || currentSession.id !== sessionId) {
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
try {
|
|
405
|
+
const renderedCards = await renderSubagentCards(subagents);
|
|
406
|
+
if (!renderedCards) {
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
toolCallStreamer.replaceByPrefix(sessionId, SUBAGENT_STREAM_PREFIX, renderedCards);
|
|
410
|
+
}
|
|
411
|
+
catch (err) {
|
|
412
|
+
logger.error("Failed to render subagent activity for Telegram:", err);
|
|
413
|
+
}
|
|
414
|
+
});
|
|
415
|
+
summaryAggregator.setOnToolFile(async (fileInfo) => {
|
|
416
|
+
if (!botInstance || !chatIdInstance) {
|
|
417
|
+
logger.error("Bot or chat ID not available for sending file");
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
420
|
+
const currentSession = getCurrentSession();
|
|
421
|
+
if (!currentSession || currentSession.id !== fileInfo.sessionId) {
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
424
|
+
try {
|
|
425
|
+
await toolCallStreamer.breakSession(fileInfo.sessionId, "tool_file_boundary");
|
|
426
|
+
const toolMessage = formatToolInfo(fileInfo);
|
|
427
|
+
const caption = prepareDocumentCaption(toolMessage || fileInfo.fileData.caption);
|
|
428
|
+
toolMessageBatcher.enqueueFile(fileInfo.sessionId, {
|
|
429
|
+
...fileInfo.fileData,
|
|
430
|
+
caption,
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
catch (err) {
|
|
434
|
+
logger.error("Failed to send file to Telegram:", err);
|
|
435
|
+
}
|
|
436
|
+
});
|
|
437
|
+
summaryAggregator.setOnQuestion(async (questions, requestID) => {
|
|
438
|
+
if (!botInstance || !chatIdInstance) {
|
|
439
|
+
logger.error("Bot or chat ID not available for showing questions");
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
442
|
+
const currentSession = getCurrentSession();
|
|
443
|
+
if (currentSession) {
|
|
444
|
+
await Promise.all([
|
|
445
|
+
toolMessageBatcher.flushSession(currentSession.id, "question_asked"),
|
|
446
|
+
toolCallStreamer.flushSession(currentSession.id, "question_asked"),
|
|
447
|
+
]);
|
|
448
|
+
}
|
|
449
|
+
if (questionManager.isActive()) {
|
|
450
|
+
logger.warn("[Bot] Replacing active poll with a new one");
|
|
451
|
+
const previousMessageIds = questionManager.getMessageIds();
|
|
452
|
+
for (const messageId of previousMessageIds) {
|
|
453
|
+
await botInstance.api.deleteMessage(chatIdInstance, messageId).catch(() => { });
|
|
454
|
+
}
|
|
455
|
+
clearAllInteractionState("question_replaced_by_new_poll");
|
|
456
|
+
}
|
|
457
|
+
logger.info(`[Bot] Received ${questions.length} questions from agent, requestID=${requestID}`);
|
|
458
|
+
questionManager.startQuestions(questions, requestID);
|
|
459
|
+
await showCurrentQuestion(botInstance.api, chatIdInstance);
|
|
460
|
+
});
|
|
461
|
+
summaryAggregator.setOnQuestionError(async () => {
|
|
462
|
+
logger.info(`[Bot] Question tool failed, clearing active poll and deleting messages`);
|
|
463
|
+
// Delete all messages from the invalid poll
|
|
464
|
+
const messageIds = questionManager.getMessageIds();
|
|
465
|
+
for (const messageId of messageIds) {
|
|
466
|
+
if (chatIdInstance) {
|
|
467
|
+
await botInstance?.api.deleteMessage(chatIdInstance, messageId).catch((err) => {
|
|
468
|
+
logger.error(`[Bot] Failed to delete question message ${messageId}:`, err);
|
|
469
|
+
});
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
clearAllInteractionState("question_error");
|
|
473
|
+
});
|
|
474
|
+
summaryAggregator.setOnPermission(async (request) => {
|
|
475
|
+
if (!botInstance || !chatIdInstance) {
|
|
476
|
+
logger.error("Bot or chat ID not available for showing permission request");
|
|
477
|
+
return;
|
|
478
|
+
}
|
|
479
|
+
await Promise.all([
|
|
480
|
+
toolMessageBatcher.flushSession(request.sessionID, "permission_asked"),
|
|
481
|
+
toolCallStreamer.flushSession(request.sessionID, "permission_asked"),
|
|
482
|
+
]);
|
|
483
|
+
logger.info(`[Bot] Received permission request from agent: type=${request.permission}, requestID=${request.id}`);
|
|
484
|
+
await showPermissionRequest(botInstance.api, chatIdInstance, request);
|
|
485
|
+
});
|
|
486
|
+
summaryAggregator.setOnThinking(async (sessionId) => {
|
|
487
|
+
if (!botInstance || !chatIdInstance) {
|
|
488
|
+
return;
|
|
489
|
+
}
|
|
490
|
+
const currentSession = getCurrentSession();
|
|
491
|
+
if (!currentSession || currentSession.id !== sessionId) {
|
|
492
|
+
return;
|
|
493
|
+
}
|
|
494
|
+
logger.debug("[Bot] Agent started thinking");
|
|
495
|
+
await toolCallStreamer.breakSession(sessionId, "thinking_started");
|
|
496
|
+
deliverThinkingMessage(sessionId, toolMessageBatcher, {
|
|
497
|
+
hideThinkingMessages: config.bot.hideThinkingMessages,
|
|
498
|
+
});
|
|
499
|
+
// Refresh pinned message so it shows the latest in-memory context
|
|
500
|
+
// (accumulated from silent token updates). 1 API call per thinking event.
|
|
501
|
+
if (pinnedMessageManager.isInitialized()) {
|
|
502
|
+
await pinnedMessageManager.refresh();
|
|
503
|
+
}
|
|
504
|
+
});
|
|
505
|
+
summaryAggregator.setOnTokens(async (tokens, isCompleted) => {
|
|
506
|
+
if (!pinnedMessageManager.isInitialized()) {
|
|
507
|
+
return;
|
|
508
|
+
}
|
|
509
|
+
try {
|
|
510
|
+
logger.debug(`[Bot] Received tokens: input=${tokens.input}, output=${tokens.output}, completed=${isCompleted}`);
|
|
511
|
+
const contextSize = tokens.input + tokens.cacheRead;
|
|
512
|
+
const contextLimit = pinnedMessageManager.getContextLimit();
|
|
513
|
+
// Skip non-completed messages with zero context: a new assistant message
|
|
514
|
+
// starts with tokens={input:0, ...} which would overwrite valid context
|
|
515
|
+
// from the previous step. Only accept zeros from completed messages.
|
|
516
|
+
if (!isCompleted && contextSize === 0) {
|
|
517
|
+
logger.debug("[Bot] Skipping zero-token intermediate update");
|
|
518
|
+
return;
|
|
519
|
+
}
|
|
520
|
+
// Update both keyboard and pinned state in memory (keeps them in sync)
|
|
521
|
+
if (contextLimit > 0) {
|
|
522
|
+
keyboardManager.updateContext(contextSize, contextLimit);
|
|
523
|
+
}
|
|
524
|
+
pinnedMessageManager.updateTokensSilent(tokens);
|
|
525
|
+
// Full pinned message update (API call) only on completed messages
|
|
526
|
+
if (isCompleted) {
|
|
527
|
+
await pinnedMessageManager.onMessageComplete(tokens);
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
catch (err) {
|
|
531
|
+
logger.error("[Bot] Error updating pinned message with tokens:", err);
|
|
532
|
+
}
|
|
533
|
+
});
|
|
534
|
+
summaryAggregator.setOnCost(async (cost) => {
|
|
535
|
+
if (!pinnedMessageManager.isInitialized()) {
|
|
536
|
+
return;
|
|
537
|
+
}
|
|
538
|
+
try {
|
|
539
|
+
logger.debug(`[Bot] Cost update: $${cost.toFixed(2)}`);
|
|
540
|
+
await pinnedMessageManager.onCostUpdate(cost);
|
|
541
|
+
}
|
|
542
|
+
catch (err) {
|
|
543
|
+
logger.error("[Bot] Error updating cost:", err);
|
|
544
|
+
}
|
|
545
|
+
});
|
|
546
|
+
summaryAggregator.setOnSessionCompacted(async (sessionId, directory) => {
|
|
547
|
+
if (!pinnedMessageManager.isInitialized()) {
|
|
548
|
+
return;
|
|
549
|
+
}
|
|
550
|
+
try {
|
|
551
|
+
logger.info(`[Bot] Session compacted, reloading context: ${sessionId}`);
|
|
552
|
+
await pinnedMessageManager.onSessionCompacted(sessionId, directory);
|
|
553
|
+
}
|
|
554
|
+
catch (err) {
|
|
555
|
+
logger.error("[Bot] Error reloading context after compaction:", err);
|
|
556
|
+
}
|
|
557
|
+
});
|
|
558
|
+
summaryAggregator.setOnSessionError(async (sessionId, message) => {
|
|
559
|
+
if (!botInstance || !chatIdInstance) {
|
|
560
|
+
foregroundSessionState.markIdle(sessionId);
|
|
561
|
+
return;
|
|
562
|
+
}
|
|
563
|
+
const currentSession = getCurrentSession();
|
|
564
|
+
if (!currentSession || currentSession.id !== sessionId) {
|
|
565
|
+
responseStreamer.clearSession(sessionId, "session_error_not_current");
|
|
566
|
+
toolCallStreamer.clearSession(sessionId, "session_error_not_current");
|
|
567
|
+
foregroundSessionState.markIdle(sessionId);
|
|
568
|
+
await scheduledTaskRuntime.flushDeferredDeliveries();
|
|
569
|
+
return;
|
|
570
|
+
}
|
|
571
|
+
responseStreamer.clearSession(sessionId, "session_error");
|
|
572
|
+
await Promise.all([
|
|
573
|
+
toolMessageBatcher.flushSession(sessionId, "session_error"),
|
|
574
|
+
toolCallStreamer.flushSession(sessionId, "session_error"),
|
|
575
|
+
]);
|
|
576
|
+
const normalizedMessage = message.trim() || t("common.unknown_error");
|
|
577
|
+
const truncatedMessage = normalizedMessage.length > 3500
|
|
578
|
+
? `${normalizedMessage.slice(0, 3497)}...`
|
|
579
|
+
: normalizedMessage;
|
|
580
|
+
await botInstance.api
|
|
581
|
+
.sendMessage(chatIdInstance, t("bot.session_error", { message: truncatedMessage }))
|
|
582
|
+
.catch((err) => {
|
|
583
|
+
logger.error("[Bot] Failed to send session.error message:", err);
|
|
584
|
+
});
|
|
585
|
+
foregroundSessionState.markIdle(sessionId);
|
|
586
|
+
await scheduledTaskRuntime.flushDeferredDeliveries();
|
|
587
|
+
});
|
|
588
|
+
summaryAggregator.setOnSessionRetry(async ({ sessionId, message }) => {
|
|
589
|
+
if (!botInstance || !chatIdInstance) {
|
|
590
|
+
return;
|
|
591
|
+
}
|
|
592
|
+
const currentSession = getCurrentSession();
|
|
593
|
+
if (!currentSession || currentSession.id !== sessionId) {
|
|
594
|
+
return;
|
|
595
|
+
}
|
|
596
|
+
const normalizedMessage = message.trim() || t("common.unknown_error");
|
|
597
|
+
const truncatedMessage = normalizedMessage.length > 3500
|
|
598
|
+
? `${normalizedMessage.slice(0, 3497)}...`
|
|
599
|
+
: normalizedMessage;
|
|
600
|
+
const retryMessage = t("bot.session_retry", { message: truncatedMessage });
|
|
601
|
+
toolCallStreamer.replaceByPrefix(sessionId, SESSION_RETRY_PREFIX, retryMessage);
|
|
602
|
+
});
|
|
603
|
+
summaryAggregator.setOnSessionDiff(async (_sessionId, diffs) => {
|
|
604
|
+
if (!pinnedMessageManager.isInitialized()) {
|
|
605
|
+
return;
|
|
606
|
+
}
|
|
607
|
+
try {
|
|
608
|
+
await pinnedMessageManager.onSessionDiff(diffs);
|
|
609
|
+
}
|
|
610
|
+
catch (err) {
|
|
611
|
+
logger.error("[Bot] Error updating session diff:", err);
|
|
612
|
+
}
|
|
613
|
+
});
|
|
614
|
+
summaryAggregator.setOnFileChange((change) => {
|
|
615
|
+
if (!pinnedMessageManager.isInitialized()) {
|
|
616
|
+
return;
|
|
617
|
+
}
|
|
618
|
+
pinnedMessageManager.addFileChange(change);
|
|
619
|
+
});
|
|
620
|
+
pinnedMessageManager.setOnKeyboardUpdate(async (tokensUsed, tokensLimit) => {
|
|
621
|
+
try {
|
|
622
|
+
logger.debug(`[Bot] Updating keyboard with context: ${tokensUsed}/${tokensLimit}`);
|
|
623
|
+
keyboardManager.updateContext(tokensUsed, tokensLimit);
|
|
624
|
+
// Don't send automatic keyboard updates - keyboard will update naturally with user messages
|
|
625
|
+
}
|
|
626
|
+
catch (err) {
|
|
627
|
+
logger.error("[Bot] Error updating keyboard context:", err);
|
|
628
|
+
}
|
|
629
|
+
});
|
|
630
|
+
logger.info(`[Bot] Subscribing to OpenCode events for project: ${directory}`);
|
|
631
|
+
subscribeToEvents(directory, (event) => {
|
|
632
|
+
if (event.type === "session.created" || event.type === "session.updated") {
|
|
633
|
+
const info = event.properties.info;
|
|
634
|
+
if (info?.directory) {
|
|
635
|
+
safeBackgroundTask({
|
|
636
|
+
taskName: `session.cache.${event.type}`,
|
|
637
|
+
task: () => ingestSessionInfoForCache(info),
|
|
638
|
+
});
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
summaryAggregator.processEvent(event);
|
|
642
|
+
}).catch((err) => {
|
|
643
|
+
logger.error("Failed to subscribe to events:", err);
|
|
644
|
+
});
|
|
645
|
+
}
|
|
646
|
+
export function createBot() {
|
|
647
|
+
clearAllInteractionState("bot_startup");
|
|
648
|
+
const botOptions = {};
|
|
649
|
+
if (config.telegram.proxyUrl) {
|
|
650
|
+
const proxyUrl = config.telegram.proxyUrl;
|
|
651
|
+
let agent;
|
|
652
|
+
if (proxyUrl.startsWith("socks")) {
|
|
653
|
+
agent = new SocksProxyAgent(proxyUrl);
|
|
654
|
+
logger.info(`[Bot] Using SOCKS proxy: ${proxyUrl.replace(/\/\/.*@/, "//***@")}`);
|
|
655
|
+
}
|
|
656
|
+
else {
|
|
657
|
+
agent = new HttpsProxyAgent(proxyUrl);
|
|
658
|
+
logger.info(`[Bot] Using HTTP/HTTPS proxy: ${proxyUrl.replace(/\/\/.*@/, "//***@")}`);
|
|
659
|
+
}
|
|
660
|
+
botOptions.client = {
|
|
661
|
+
baseFetchConfig: {
|
|
662
|
+
agent,
|
|
663
|
+
compress: true,
|
|
664
|
+
},
|
|
665
|
+
};
|
|
666
|
+
}
|
|
667
|
+
const bot = new Bot(config.telegram.token, botOptions);
|
|
668
|
+
// Heartbeat for diagnostics: verify the event loop is not blocked
|
|
669
|
+
let heartbeatCounter = 0;
|
|
670
|
+
setInterval(() => {
|
|
671
|
+
heartbeatCounter++;
|
|
672
|
+
if (heartbeatCounter % 6 === 0) {
|
|
673
|
+
// Log every 30 seconds (5 sec * 6)
|
|
674
|
+
logger.debug(`[Bot] Heartbeat #${heartbeatCounter} - event loop alive`);
|
|
675
|
+
}
|
|
676
|
+
}, 5000);
|
|
677
|
+
// Log all API calls for diagnostics
|
|
678
|
+
let lastGetUpdatesTime = Date.now();
|
|
679
|
+
bot.api.config.use(async (prev, method, payload, signal) => {
|
|
680
|
+
if (method === "getUpdates") {
|
|
681
|
+
const now = Date.now();
|
|
682
|
+
const timeSinceLast = now - lastGetUpdatesTime;
|
|
683
|
+
logger.debug(`[Bot API] getUpdates called (${timeSinceLast}ms since last)`);
|
|
684
|
+
lastGetUpdatesTime = now;
|
|
685
|
+
return prev(method, payload, signal);
|
|
686
|
+
}
|
|
687
|
+
if (method === "sendMessage") {
|
|
688
|
+
logger.debug(`[Bot API] sendMessage to chat ${payload.chat_id}`);
|
|
689
|
+
}
|
|
690
|
+
return withTelegramRateLimitRetry(() => prev(method, payload, signal), {
|
|
691
|
+
maxRetries: 5,
|
|
692
|
+
onRetry: ({ attempt, retryAfterMs, error }) => {
|
|
693
|
+
logger.warn(`[Bot API] Telegram rate limit on ${method}, retrying in ${retryAfterMs}ms (attempt=${attempt})`, error);
|
|
694
|
+
},
|
|
695
|
+
});
|
|
696
|
+
});
|
|
697
|
+
bot.use((ctx, next) => {
|
|
698
|
+
const hasCallbackQuery = !!ctx.callbackQuery;
|
|
699
|
+
const hasMessage = !!ctx.message;
|
|
700
|
+
const callbackData = ctx.callbackQuery?.data || "N/A";
|
|
701
|
+
logger.debug(`[DEBUG] Incoming update: hasCallbackQuery=${hasCallbackQuery}, hasMessage=${hasMessage}, callbackData=${callbackData}`);
|
|
702
|
+
return next();
|
|
703
|
+
});
|
|
704
|
+
bot.use(authMiddleware);
|
|
705
|
+
bot.use(ensureCommandsInitialized);
|
|
706
|
+
bot.use(interactionGuardMiddleware);
|
|
707
|
+
const blockMenuWhileInteractionActive = async (ctx) => {
|
|
708
|
+
const activeInteraction = interactionManager.getSnapshot();
|
|
709
|
+
if (!activeInteraction) {
|
|
710
|
+
return false;
|
|
711
|
+
}
|
|
712
|
+
logger.debug(`[Bot] Blocking menu open while interaction active: kind=${activeInteraction.kind}, expectedInput=${activeInteraction.expectedInput}`);
|
|
713
|
+
await ctx.reply(t("interaction.blocked.finish_current"));
|
|
714
|
+
return true;
|
|
715
|
+
};
|
|
716
|
+
bot.command("start", startCommand);
|
|
717
|
+
bot.command("help", helpCommand);
|
|
718
|
+
bot.command("status", statusCommand);
|
|
719
|
+
bot.command("opencode_start", opencodeStartCommand);
|
|
720
|
+
bot.command("opencode_stop", opencodeStopCommand);
|
|
721
|
+
bot.command("projects", projectsCommand);
|
|
722
|
+
bot.command("sessions", sessionsCommand);
|
|
723
|
+
bot.command("new", newCommand);
|
|
724
|
+
bot.command("abort", abortCommand);
|
|
725
|
+
bot.command("task", taskCommand);
|
|
726
|
+
bot.command("tasklist", taskListCommand);
|
|
727
|
+
bot.command("rename", renameCommand);
|
|
728
|
+
bot.command("voice", voiceCommand);
|
|
729
|
+
bot.command("commands", commandsCommand);
|
|
730
|
+
bot.on("message:text", unknownCommandMiddleware);
|
|
731
|
+
bot.on("callback_query:data", async (ctx) => {
|
|
732
|
+
logger.debug(`[Bot] Received callback_query:data: ${ctx.callbackQuery?.data}`);
|
|
733
|
+
logger.debug(`[Bot] Callback context: from=${ctx.from?.id}, chat=${ctx.chat?.id}`);
|
|
734
|
+
if (ctx.chat) {
|
|
735
|
+
botInstance = bot;
|
|
736
|
+
chatIdInstance = ctx.chat.id;
|
|
737
|
+
}
|
|
738
|
+
try {
|
|
739
|
+
const handledInlineCancel = await handleInlineMenuCancel(ctx);
|
|
740
|
+
const handledSession = await handleSessionSelect(ctx);
|
|
741
|
+
const handledProject = await handleProjectSelect(ctx);
|
|
742
|
+
const handledQuestion = await handleQuestionCallback(ctx);
|
|
743
|
+
const handledPermission = await handlePermissionCallback(ctx);
|
|
744
|
+
const handledAgent = await handleAgentSelect(ctx);
|
|
745
|
+
const handledModel = await handleModelSelect(ctx);
|
|
746
|
+
const handledVariant = await handleVariantSelect(ctx);
|
|
747
|
+
const handledCompactConfirm = await handleCompactConfirm(ctx);
|
|
748
|
+
const handledTask = await handleTaskCallback(ctx);
|
|
749
|
+
const handledTaskList = await handleTaskListCallback(ctx);
|
|
750
|
+
const handledRenameCancel = await handleRenameCancel(ctx);
|
|
751
|
+
const handledCommands = await handleCommandsCallback(ctx, { bot, ensureEventSubscription });
|
|
752
|
+
const handledVoice = await handleVoiceCallback(ctx);
|
|
753
|
+
logger.debug(`[Bot] Callback handled: inlineCancel=${handledInlineCancel}, session=${handledSession}, project=${handledProject}, question=${handledQuestion}, permission=${handledPermission}, agent=${handledAgent}, model=${handledModel}, variant=${handledVariant}, compactConfirm=${handledCompactConfirm}, task=${handledTask}, taskList=${handledTaskList}, rename=${handledRenameCancel}, commands=${handledCommands}, voice=${handledVoice}`);
|
|
754
|
+
if (!handledInlineCancel &&
|
|
755
|
+
!handledSession &&
|
|
756
|
+
!handledProject &&
|
|
757
|
+
!handledQuestion &&
|
|
758
|
+
!handledPermission &&
|
|
759
|
+
!handledAgent &&
|
|
760
|
+
!handledModel &&
|
|
761
|
+
!handledVariant &&
|
|
762
|
+
!handledCompactConfirm &&
|
|
763
|
+
!handledTask &&
|
|
764
|
+
!handledTaskList &&
|
|
765
|
+
!handledRenameCancel &&
|
|
766
|
+
!handledCommands &&
|
|
767
|
+
!handledVoice) {
|
|
768
|
+
logger.debug("Unknown callback query:", ctx.callbackQuery?.data);
|
|
769
|
+
await ctx.answerCallbackQuery({ text: t("callback.unknown_command") });
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
catch (err) {
|
|
773
|
+
logger.error("[Bot] Error handling callback:", err);
|
|
774
|
+
clearAllInteractionState("callback_handler_error");
|
|
775
|
+
await ctx.answerCallbackQuery({ text: t("callback.processing_error") }).catch(() => { });
|
|
776
|
+
}
|
|
777
|
+
});
|
|
778
|
+
// Handle Reply Keyboard button press (agent mode indicator)
|
|
779
|
+
bot.hears(AGENT_MODE_BUTTON_TEXT_PATTERN, async (ctx) => {
|
|
780
|
+
logger.debug(`[Bot] Agent mode button pressed: ${ctx.message?.text}`);
|
|
781
|
+
try {
|
|
782
|
+
if (await blockMenuWhileInteractionActive(ctx)) {
|
|
783
|
+
return;
|
|
784
|
+
}
|
|
785
|
+
await showAgentSelectionMenu(ctx);
|
|
786
|
+
}
|
|
787
|
+
catch (err) {
|
|
788
|
+
logger.error("[Bot] Error showing agent menu:", err);
|
|
789
|
+
await ctx.reply(t("error.load_agents"));
|
|
790
|
+
}
|
|
791
|
+
});
|
|
792
|
+
// Handle Reply Keyboard button press (model selector)
|
|
793
|
+
// Model button text is produced by formatModelForButton() and always starts with "🤖 ".
|
|
794
|
+
bot.hears(MODEL_BUTTON_TEXT_PATTERN, async (ctx) => {
|
|
795
|
+
logger.debug(`[Bot] Model button pressed: ${ctx.message?.text}`);
|
|
796
|
+
try {
|
|
797
|
+
if (await blockMenuWhileInteractionActive(ctx)) {
|
|
798
|
+
return;
|
|
799
|
+
}
|
|
800
|
+
await showModelSelectionMenu(ctx);
|
|
801
|
+
}
|
|
802
|
+
catch (err) {
|
|
803
|
+
logger.error("[Bot] Error showing model menu:", err);
|
|
804
|
+
await ctx.reply(t("error.load_models"));
|
|
805
|
+
}
|
|
806
|
+
});
|
|
807
|
+
// Handle Reply Keyboard button press (context button)
|
|
808
|
+
bot.hears(/^📊(?:\s|$)/, async (ctx) => {
|
|
809
|
+
logger.debug(`[Bot] Context button pressed: ${ctx.message?.text}`);
|
|
810
|
+
try {
|
|
811
|
+
if (await blockMenuWhileInteractionActive(ctx)) {
|
|
812
|
+
return;
|
|
813
|
+
}
|
|
814
|
+
await handleContextButtonPress(ctx);
|
|
815
|
+
}
|
|
816
|
+
catch (err) {
|
|
817
|
+
logger.error("[Bot] Error handling context button:", err);
|
|
818
|
+
await ctx.reply(t("error.context_button"));
|
|
819
|
+
}
|
|
820
|
+
});
|
|
821
|
+
// Handle Reply Keyboard button press (variant selector)
|
|
822
|
+
// Keep support for both legacy "💭" and current "💡" prefix.
|
|
823
|
+
bot.hears(VARIANT_BUTTON_TEXT_PATTERN, async (ctx) => {
|
|
824
|
+
logger.debug(`[Bot] Variant button pressed: ${ctx.message?.text}`);
|
|
825
|
+
try {
|
|
826
|
+
if (await blockMenuWhileInteractionActive(ctx)) {
|
|
827
|
+
return;
|
|
828
|
+
}
|
|
829
|
+
await showVariantSelectionMenu(ctx);
|
|
830
|
+
}
|
|
831
|
+
catch (err) {
|
|
832
|
+
logger.error("[Bot] Error showing variant menu:", err);
|
|
833
|
+
await ctx.reply(t("error.load_variants"));
|
|
834
|
+
}
|
|
835
|
+
});
|
|
836
|
+
bot.on("message:text", async (ctx, next) => {
|
|
837
|
+
const text = ctx.message?.text;
|
|
838
|
+
if (text) {
|
|
839
|
+
const isCommand = text.startsWith("/");
|
|
840
|
+
logger.debug(`[Bot] Received text message: ${isCommand ? `command="${text}"` : `prompt (length=${text.length})`}, chatId=${ctx.chat.id}`);
|
|
841
|
+
}
|
|
842
|
+
await next();
|
|
843
|
+
});
|
|
844
|
+
// Remove any previously set global commands to prevent unauthorized users from seeing them
|
|
845
|
+
safeBackgroundTask({
|
|
846
|
+
taskName: "bot.clearGlobalCommands",
|
|
847
|
+
task: async () => {
|
|
848
|
+
try {
|
|
849
|
+
await Promise.all([
|
|
850
|
+
bot.api.setMyCommands([], { scope: { type: "default" } }),
|
|
851
|
+
bot.api.setMyCommands([], { scope: { type: "all_private_chats" } }),
|
|
852
|
+
]);
|
|
853
|
+
return { success: true };
|
|
854
|
+
}
|
|
855
|
+
catch (error) {
|
|
856
|
+
return { success: false, error };
|
|
857
|
+
}
|
|
858
|
+
},
|
|
859
|
+
onSuccess: (result) => {
|
|
860
|
+
if (result.success) {
|
|
861
|
+
logger.debug("[Bot] Cleared global commands (default and all_private_chats scopes)");
|
|
862
|
+
return;
|
|
863
|
+
}
|
|
864
|
+
logger.warn("[Bot] Could not clear global commands:", result.error);
|
|
865
|
+
},
|
|
866
|
+
});
|
|
867
|
+
// Voice and audio message handlers (STT transcription -> prompt)
|
|
868
|
+
const voicePromptDeps = { bot, ensureEventSubscription };
|
|
869
|
+
bot.on("message:voice", async (ctx) => {
|
|
870
|
+
logger.debug(`[Bot] Received voice message, chatId=${ctx.chat.id}`);
|
|
871
|
+
botInstance = bot;
|
|
872
|
+
chatIdInstance = ctx.chat.id;
|
|
873
|
+
await handleVoiceMessage(ctx, voicePromptDeps);
|
|
874
|
+
});
|
|
875
|
+
bot.on("message:audio", async (ctx) => {
|
|
876
|
+
logger.debug(`[Bot] Received audio message, chatId=${ctx.chat.id}`);
|
|
877
|
+
botInstance = bot;
|
|
878
|
+
chatIdInstance = ctx.chat.id;
|
|
879
|
+
await handleVoiceMessage(ctx, voicePromptDeps);
|
|
880
|
+
});
|
|
881
|
+
// Photo message handler
|
|
882
|
+
bot.on("message:photo", async (ctx) => {
|
|
883
|
+
logger.debug(`[Bot] Received photo message, chatId=${ctx.chat.id}`);
|
|
884
|
+
const photos = ctx.message?.photo;
|
|
885
|
+
if (!photos || photos.length === 0) {
|
|
886
|
+
return;
|
|
887
|
+
}
|
|
888
|
+
const caption = ctx.message.caption || "";
|
|
889
|
+
try {
|
|
890
|
+
// Get the largest photo (last element in array)
|
|
891
|
+
const largestPhoto = photos[photos.length - 1];
|
|
892
|
+
// Check model capabilities
|
|
893
|
+
const storedModel = getStoredModel();
|
|
894
|
+
const capabilities = await getModelCapabilities(storedModel.providerID, storedModel.modelID);
|
|
895
|
+
if (!supportsInput(capabilities, "image")) {
|
|
896
|
+
logger.warn(`[Bot] Model ${storedModel.providerID}/${storedModel.modelID} doesn't support image input`);
|
|
897
|
+
await ctx.reply(t("bot.photo_model_no_image"));
|
|
898
|
+
// Fall back to caption-only if present
|
|
899
|
+
if (caption.trim().length > 0) {
|
|
900
|
+
botInstance = bot;
|
|
901
|
+
chatIdInstance = ctx.chat.id;
|
|
902
|
+
const promptDeps = { bot, ensureEventSubscription };
|
|
903
|
+
await processUserPrompt(ctx, caption, promptDeps);
|
|
904
|
+
}
|
|
905
|
+
return;
|
|
906
|
+
}
|
|
907
|
+
// Download photo
|
|
908
|
+
await ctx.reply(t("bot.photo_downloading"));
|
|
909
|
+
const downloadedFile = await downloadTelegramFile(ctx.api, largestPhoto.file_id);
|
|
910
|
+
// Convert to data URI (Telegram always converts photos to JPEG)
|
|
911
|
+
const dataUri = toDataUri(downloadedFile.buffer, "image/jpeg");
|
|
912
|
+
// Create file part
|
|
913
|
+
const filePart = {
|
|
914
|
+
type: "file",
|
|
915
|
+
mime: "image/jpeg",
|
|
916
|
+
filename: "photo.jpg",
|
|
917
|
+
url: dataUri,
|
|
918
|
+
};
|
|
919
|
+
logger.info(`[Bot] Sending photo (${downloadedFile.buffer.length} bytes) with prompt`);
|
|
920
|
+
botInstance = bot;
|
|
921
|
+
chatIdInstance = ctx.chat.id;
|
|
922
|
+
// Send via processUserPrompt with file part
|
|
923
|
+
const promptDeps = { bot, ensureEventSubscription };
|
|
924
|
+
await processUserPrompt(ctx, caption, promptDeps, [filePart]);
|
|
925
|
+
}
|
|
926
|
+
catch (err) {
|
|
927
|
+
logger.error("[Bot] Error handling photo message:", err);
|
|
928
|
+
await ctx.reply(t("bot.photo_download_error"));
|
|
929
|
+
}
|
|
930
|
+
});
|
|
931
|
+
// Document message handler (PDF and text files)
|
|
932
|
+
bot.on("message:document", async (ctx) => {
|
|
933
|
+
logger.debug(`[Bot] Received document message, chatId=${ctx.chat.id}`);
|
|
934
|
+
botInstance = bot;
|
|
935
|
+
chatIdInstance = ctx.chat.id;
|
|
936
|
+
const deps = { bot, ensureEventSubscription };
|
|
937
|
+
await handleDocumentMessage(ctx, deps);
|
|
938
|
+
});
|
|
939
|
+
bot.on("message:text", async (ctx) => {
|
|
940
|
+
const text = ctx.message?.text;
|
|
941
|
+
if (!text) {
|
|
942
|
+
return;
|
|
943
|
+
}
|
|
944
|
+
botInstance = bot;
|
|
945
|
+
chatIdInstance = ctx.chat.id;
|
|
946
|
+
if (text.startsWith("/")) {
|
|
947
|
+
return;
|
|
948
|
+
}
|
|
949
|
+
if (questionManager.isActive()) {
|
|
950
|
+
await handleQuestionTextAnswer(ctx);
|
|
951
|
+
return;
|
|
952
|
+
}
|
|
953
|
+
const handledTask = await handleTaskTextInput(ctx);
|
|
954
|
+
if (handledTask) {
|
|
955
|
+
return;
|
|
956
|
+
}
|
|
957
|
+
const handledRename = await handleRenameTextAnswer(ctx);
|
|
958
|
+
if (handledRename) {
|
|
959
|
+
return;
|
|
960
|
+
}
|
|
961
|
+
const promptDeps = { bot, ensureEventSubscription };
|
|
962
|
+
const handledCommandArgs = await handleCommandTextArguments(ctx, promptDeps);
|
|
963
|
+
if (handledCommandArgs) {
|
|
964
|
+
return;
|
|
965
|
+
}
|
|
966
|
+
await processUserPrompt(ctx, text, promptDeps);
|
|
967
|
+
logger.debug("[Bot] message:text handler completed (prompt sent in background)");
|
|
968
|
+
});
|
|
969
|
+
bot.catch((err) => {
|
|
970
|
+
logger.error("[Bot] Unhandled error in bot:", err);
|
|
971
|
+
clearAllInteractionState("bot_unhandled_error");
|
|
972
|
+
if (err.ctx) {
|
|
973
|
+
logger.error("[Bot] Error context - update type:", err.ctx.update ? Object.keys(err.ctx.update) : "unknown");
|
|
974
|
+
}
|
|
975
|
+
});
|
|
976
|
+
return bot;
|
|
977
|
+
}
|