@grinev/opencode-telegram-bot 0.12.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 +6 -0
- package/README.md +2 -0
- package/dist/bot/commands/commands.js +87 -5
- package/dist/bot/commands/new.js +5 -0
- package/dist/bot/commands/projects.js +9 -0
- package/dist/bot/commands/sessions.js +9 -0
- package/dist/bot/index.js +128 -21
- package/dist/bot/middleware/interaction-guard.js +6 -2
- package/dist/bot/streaming/response-streamer.js +284 -0
- package/dist/bot/utils/busy-guard.js +15 -0
- package/dist/bot/utils/finalize-assistant-response.js +23 -0
- package/dist/bot/utils/send-with-markdown-fallback.js +6 -8
- package/dist/bot/utils/thinking-message.js +12 -0
- package/dist/config.js +2 -0
- package/dist/i18n/de.js +5 -0
- package/dist/i18n/en.js +5 -0
- package/dist/i18n/es.js +5 -0
- package/dist/i18n/fr.js +5 -0
- package/dist/i18n/ru.js +5 -0
- package/dist/i18n/zh.js +8 -3
- package/dist/interaction/busy.js +8 -0
- package/dist/interaction/guard.js +42 -6
- package/dist/pinned/manager.js +8 -9
- package/dist/summary/aggregator.js +186 -36
- package/dist/summary/formatter.js +5 -3
- package/dist/summary/tool-message-batcher.js +9 -0
- package/package.json +1 -1
package/.env.example
CHANGED
|
@@ -40,6 +40,9 @@ OPENCODE_MODEL_ID=big-pickle
|
|
|
40
40
|
# Maximum number of projects shown in /projects (default: 10)
|
|
41
41
|
# PROJECTS_LIST_LIMIT=10
|
|
42
42
|
|
|
43
|
+
# Maximum number of commands shown in /commands (default: 10)
|
|
44
|
+
# COMMANDS_LIST_LIMIT=10
|
|
45
|
+
|
|
43
46
|
# Maximum number of scheduled tasks allowed at once (default: 10)
|
|
44
47
|
# TASK_LIMIT=10
|
|
45
48
|
|
|
@@ -58,6 +61,9 @@ OPENCODE_MODEL_ID=big-pickle
|
|
|
58
61
|
# Hide tool call service messages (default: false)
|
|
59
62
|
# HIDE_TOOL_CALL_MESSAGES=false
|
|
60
63
|
|
|
64
|
+
# Stream assistant responses while they are being generated (default: true)
|
|
65
|
+
# RESPONSE_STREAMING=true
|
|
66
|
+
|
|
61
67
|
# Assistant message formatting mode (default: markdown)
|
|
62
68
|
# markdown = convert assistant replies to Telegram MarkdownV2
|
|
63
69
|
# raw = show assistant replies as plain text
|
package/README.md
CHANGED
|
@@ -159,10 +159,12 @@ When installed via npm, the configuration wizard handles the initial setup. The
|
|
|
159
159
|
| `BOT_LOCALE` | Bot UI language (supported locale code, e.g. `en`, `de`, `es`, `fr`, `ru`, `zh`) | No | `en` |
|
|
160
160
|
| `SESSIONS_LIST_LIMIT` | Sessions per page in `/sessions` | No | `10` |
|
|
161
161
|
| `PROJECTS_LIST_LIMIT` | Projects per page in `/projects` | No | `10` |
|
|
162
|
+
| `COMMANDS_LIST_LIMIT` | Commands per page in `/commands` | No | `10` |
|
|
162
163
|
| `TASK_LIMIT` | Maximum number of scheduled tasks that can exist at once | No | `10` |
|
|
163
164
|
| `SERVICE_MESSAGES_INTERVAL_SEC` | Service messages interval (thinking + tool calls); keep `>=2` to avoid Telegram rate limits, `0` = immediate | No | `5` |
|
|
164
165
|
| `HIDE_THINKING_MESSAGES` | Hide `💭 Thinking...` service messages | No | `false` |
|
|
165
166
|
| `HIDE_TOOL_CALL_MESSAGES` | Hide tool-call service messages (`💻 bash ...`, `📖 read ...`, etc.) | No | `false` |
|
|
167
|
+
| `RESPONSE_STREAMING` | Stream assistant replies while they are generated across one or more Telegram messages | No | `true` |
|
|
166
168
|
| `MESSAGE_FORMAT_MODE` | Assistant reply formatting mode: `markdown` (Telegram MarkdownV2) or `raw` | No | `markdown` |
|
|
167
169
|
| `CODE_FILE_MAX_SIZE_KB` | Max file size (KB) to send as document | No | `100` |
|
|
168
170
|
| `STT_API_URL` | Whisper-compatible API base URL (enables voice/audio transcription) | No | — |
|
|
@@ -11,8 +11,10 @@ import { safeBackgroundTask } from "../../utils/safe-background-task.js";
|
|
|
11
11
|
import { logger } from "../../utils/logger.js";
|
|
12
12
|
import { t } from "../../i18n/index.js";
|
|
13
13
|
import { foregroundSessionState } from "../../scheduled-task/foreground-state.js";
|
|
14
|
+
import { config } from "../../config.js";
|
|
14
15
|
const COMMANDS_CALLBACK_PREFIX = "commands:";
|
|
15
16
|
const COMMANDS_CALLBACK_SELECT_PREFIX = `${COMMANDS_CALLBACK_PREFIX}select:`;
|
|
17
|
+
const COMMANDS_CALLBACK_PAGE_PREFIX = `${COMMANDS_CALLBACK_PREFIX}page:`;
|
|
16
18
|
const COMMANDS_CALLBACK_CANCEL = `${COMMANDS_CALLBACK_PREFIX}cancel`;
|
|
17
19
|
const COMMANDS_CALLBACK_EXECUTE = `${COMMANDS_CALLBACK_PREFIX}execute`;
|
|
18
20
|
const MAX_INLINE_BUTTON_LABEL_LENGTH = 64;
|
|
@@ -21,6 +23,26 @@ function formatExecutingCommandMessage(commandName, args) {
|
|
|
21
23
|
const argsSuffix = args ? ` ${args}` : "";
|
|
22
24
|
return `${t("commands.executing_prefix")}\n${commandText}${argsSuffix}`;
|
|
23
25
|
}
|
|
26
|
+
export function buildCommandPageCallback(page) {
|
|
27
|
+
return `${COMMANDS_CALLBACK_PAGE_PREFIX}${page}`;
|
|
28
|
+
}
|
|
29
|
+
export function parseCommandPageCallback(data) {
|
|
30
|
+
if (!data.startsWith(COMMANDS_CALLBACK_PAGE_PREFIX)) {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
const rawPage = data.slice(COMMANDS_CALLBACK_PAGE_PREFIX.length);
|
|
34
|
+
const page = Number(rawPage);
|
|
35
|
+
if (!Number.isInteger(page) || page < 0) {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
return page;
|
|
39
|
+
}
|
|
40
|
+
export function formatCommandsSelectText(page) {
|
|
41
|
+
if (page === 0) {
|
|
42
|
+
return t("commands.select");
|
|
43
|
+
}
|
|
44
|
+
return t("commands.select_page", { page: page + 1 });
|
|
45
|
+
}
|
|
24
46
|
function normalizeDirectoryForCommandApi(directory) {
|
|
25
47
|
return directory.replace(/\\/g, "/");
|
|
26
48
|
}
|
|
@@ -40,13 +62,37 @@ function formatCommandButtonLabel(command) {
|
|
|
40
62
|
}
|
|
41
63
|
return `${rawLabel.slice(0, MAX_INLINE_BUTTON_LABEL_LENGTH - 3)}...`;
|
|
42
64
|
}
|
|
43
|
-
function
|
|
65
|
+
export function calculateCommandsPaginationRange(totalCommands, page, pageSize) {
|
|
66
|
+
const safePageSize = Math.max(1, pageSize);
|
|
67
|
+
const totalPages = Math.max(1, Math.ceil(totalCommands / safePageSize));
|
|
68
|
+
const normalizedPage = Math.min(Math.max(0, page), totalPages - 1);
|
|
69
|
+
const startIndex = normalizedPage * safePageSize;
|
|
70
|
+
const endIndex = Math.min(startIndex + safePageSize, totalCommands);
|
|
71
|
+
return {
|
|
72
|
+
page: normalizedPage,
|
|
73
|
+
totalPages,
|
|
74
|
+
startIndex,
|
|
75
|
+
endIndex,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
function buildCommandsListKeyboard(commands, page, pageSize) {
|
|
44
79
|
const keyboard = new InlineKeyboard();
|
|
45
|
-
commands.
|
|
80
|
+
const { page: normalizedPage, totalPages, startIndex, endIndex, } = calculateCommandsPaginationRange(commands.length, page, pageSize);
|
|
81
|
+
commands.slice(startIndex, endIndex).forEach((command, index) => {
|
|
82
|
+
const globalIndex = startIndex + index;
|
|
46
83
|
keyboard
|
|
47
|
-
.text(formatCommandButtonLabel(command), `${COMMANDS_CALLBACK_SELECT_PREFIX}${
|
|
84
|
+
.text(formatCommandButtonLabel(command), `${COMMANDS_CALLBACK_SELECT_PREFIX}${globalIndex}`)
|
|
48
85
|
.row();
|
|
49
86
|
});
|
|
87
|
+
if (totalPages > 1) {
|
|
88
|
+
if (normalizedPage > 0) {
|
|
89
|
+
keyboard.text(t("commands.button.prev_page"), buildCommandPageCallback(normalizedPage - 1));
|
|
90
|
+
}
|
|
91
|
+
if (normalizedPage < totalPages - 1) {
|
|
92
|
+
keyboard.text(t("commands.button.next_page"), buildCommandPageCallback(normalizedPage + 1));
|
|
93
|
+
}
|
|
94
|
+
keyboard.row();
|
|
95
|
+
}
|
|
50
96
|
keyboard.text(t("commands.button.cancel"), COMMANDS_CALLBACK_CANCEL);
|
|
51
97
|
return keyboard;
|
|
52
98
|
}
|
|
@@ -94,12 +140,16 @@ function parseCommandsMetadata(state) {
|
|
|
94
140
|
if (!commands) {
|
|
95
141
|
return null;
|
|
96
142
|
}
|
|
143
|
+
const page = typeof state.metadata.page === "number" && Number.isInteger(state.metadata.page)
|
|
144
|
+
? Math.max(0, state.metadata.page)
|
|
145
|
+
: 0;
|
|
97
146
|
return {
|
|
98
147
|
flow,
|
|
99
148
|
stage,
|
|
100
149
|
messageId,
|
|
101
150
|
projectDirectory,
|
|
102
151
|
commands,
|
|
152
|
+
page,
|
|
103
153
|
};
|
|
104
154
|
}
|
|
105
155
|
if (stage === "confirm") {
|
|
@@ -270,8 +320,9 @@ export async function commandsCommand(ctx) {
|
|
|
270
320
|
await ctx.reply(t("commands.empty"));
|
|
271
321
|
return;
|
|
272
322
|
}
|
|
273
|
-
const
|
|
274
|
-
const
|
|
323
|
+
const pageSize = config.bot.commandsListLimit;
|
|
324
|
+
const keyboard = buildCommandsListKeyboard(commands, 0, pageSize);
|
|
325
|
+
const message = await ctx.reply(formatCommandsSelectText(0), {
|
|
275
326
|
reply_markup: keyboard,
|
|
276
327
|
});
|
|
277
328
|
interactionManager.start({
|
|
@@ -283,6 +334,7 @@ export async function commandsCommand(ctx) {
|
|
|
283
334
|
messageId: message.message_id,
|
|
284
335
|
projectDirectory: currentProject.worktree,
|
|
285
336
|
commands,
|
|
337
|
+
page: 0,
|
|
286
338
|
},
|
|
287
339
|
});
|
|
288
340
|
}
|
|
@@ -324,6 +376,36 @@ export async function handleCommandsCallback(ctx, deps) {
|
|
|
324
376
|
});
|
|
325
377
|
return true;
|
|
326
378
|
}
|
|
379
|
+
const page = parseCommandPageCallback(data);
|
|
380
|
+
if (page !== null) {
|
|
381
|
+
if (metadata.stage !== "list") {
|
|
382
|
+
await ctx.answerCallbackQuery({ text: t("callback.processing_error"), show_alert: true });
|
|
383
|
+
return true;
|
|
384
|
+
}
|
|
385
|
+
const pageSize = config.bot.commandsListLimit;
|
|
386
|
+
const { page: normalizedPage, totalPages } = calculateCommandsPaginationRange(metadata.commands.length, page, pageSize);
|
|
387
|
+
if (page >= totalPages || page < 0) {
|
|
388
|
+
await ctx.answerCallbackQuery({ text: t("commands.page_empty_callback") });
|
|
389
|
+
return true;
|
|
390
|
+
}
|
|
391
|
+
const keyboard = buildCommandsListKeyboard(metadata.commands, normalizedPage, pageSize);
|
|
392
|
+
await ctx.editMessageText(formatCommandsSelectText(normalizedPage), {
|
|
393
|
+
reply_markup: keyboard,
|
|
394
|
+
});
|
|
395
|
+
await ctx.answerCallbackQuery();
|
|
396
|
+
interactionManager.transition({
|
|
397
|
+
expectedInput: "callback",
|
|
398
|
+
metadata: {
|
|
399
|
+
flow: "commands",
|
|
400
|
+
stage: "list",
|
|
401
|
+
messageId: metadata.messageId,
|
|
402
|
+
projectDirectory: metadata.projectDirectory,
|
|
403
|
+
commands: metadata.commands,
|
|
404
|
+
page: normalizedPage,
|
|
405
|
+
},
|
|
406
|
+
});
|
|
407
|
+
return true;
|
|
408
|
+
}
|
|
327
409
|
const commandIndex = parseSelectIndex(data);
|
|
328
410
|
if (commandIndex === null || metadata.stage !== "list") {
|
|
329
411
|
await ctx.answerCallbackQuery({ text: t("callback.processing_error"), show_alert: true });
|
package/dist/bot/commands/new.js
CHANGED
|
@@ -10,10 +10,15 @@ import { getStoredAgent } from "../../agent/manager.js";
|
|
|
10
10
|
import { getStoredModel } from "../../model/manager.js";
|
|
11
11
|
import { formatVariantForButton } from "../../variant/manager.js";
|
|
12
12
|
import { createMainKeyboard } from "../utils/keyboard.js";
|
|
13
|
+
import { isForegroundBusy, replyBusyBlocked } from "../utils/busy-guard.js";
|
|
13
14
|
import { logger } from "../../utils/logger.js";
|
|
14
15
|
import { t } from "../../i18n/index.js";
|
|
15
16
|
export async function newCommand(ctx) {
|
|
16
17
|
try {
|
|
18
|
+
if (isForegroundBusy()) {
|
|
19
|
+
await replyBusyBlocked(ctx);
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
17
22
|
const currentProject = getCurrentProject();
|
|
18
23
|
if (!currentProject) {
|
|
19
24
|
await ctx.reply(t("new.project_not_selected"));
|
|
@@ -12,6 +12,7 @@ import { formatVariantForButton } from "../../variant/manager.js";
|
|
|
12
12
|
import { clearAllInteractionState } from "../../interaction/cleanup.js";
|
|
13
13
|
import { createMainKeyboard } from "../utils/keyboard.js";
|
|
14
14
|
import { appendInlineMenuCancelButton, ensureActiveInlineMenu, replyWithInlineMenu, } from "../handlers/inline-menu.js";
|
|
15
|
+
import { isForegroundBusy, replyBusyBlocked } from "../utils/busy-guard.js";
|
|
15
16
|
import { logger } from "../../utils/logger.js";
|
|
16
17
|
import { t } from "../../i18n/index.js";
|
|
17
18
|
import { config } from "../../config.js";
|
|
@@ -108,6 +109,10 @@ function buildProjectsMenuView(projects, page) {
|
|
|
108
109
|
}
|
|
109
110
|
export async function projectsCommand(ctx) {
|
|
110
111
|
try {
|
|
112
|
+
if (isForegroundBusy()) {
|
|
113
|
+
await replyBusyBlocked(ctx);
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
111
116
|
await syncSessionDirectoryCache();
|
|
112
117
|
const projects = await getProjects();
|
|
113
118
|
if (projects.length === 0) {
|
|
@@ -131,6 +136,10 @@ export async function handleProjectSelect(ctx) {
|
|
|
131
136
|
if (!callbackQuery?.data) {
|
|
132
137
|
return false;
|
|
133
138
|
}
|
|
139
|
+
if (isForegroundBusy()) {
|
|
140
|
+
await replyBusyBlocked(ctx);
|
|
141
|
+
return true;
|
|
142
|
+
}
|
|
134
143
|
const page = parseProjectPageCallback(callbackQuery.data);
|
|
135
144
|
if (page !== null) {
|
|
136
145
|
const isActiveMenu = await ensureActiveInlineMenu(ctx, "project");
|
|
@@ -7,6 +7,7 @@ import { summaryAggregator } from "../../summary/aggregator.js";
|
|
|
7
7
|
import { pinnedMessageManager } from "../../pinned/manager.js";
|
|
8
8
|
import { keyboardManager } from "../../keyboard/manager.js";
|
|
9
9
|
import { appendInlineMenuCancelButton, ensureActiveInlineMenu, replyWithInlineMenu, } from "../handlers/inline-menu.js";
|
|
10
|
+
import { isForegroundBusy, replyBusyBlocked } from "../utils/busy-guard.js";
|
|
10
11
|
import { logger } from "../../utils/logger.js";
|
|
11
12
|
import { safeBackgroundTask } from "../../utils/safe-background-task.js";
|
|
12
13
|
import { config } from "../../config.js";
|
|
@@ -85,6 +86,10 @@ function buildSessionsKeyboard(pageData, pageSize) {
|
|
|
85
86
|
}
|
|
86
87
|
export async function sessionsCommand(ctx) {
|
|
87
88
|
try {
|
|
89
|
+
if (isForegroundBusy()) {
|
|
90
|
+
await replyBusyBlocked(ctx);
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
88
93
|
const pageSize = config.bot.sessionsListLimit;
|
|
89
94
|
const currentProject = getCurrentProject();
|
|
90
95
|
if (!currentProject) {
|
|
@@ -118,6 +123,10 @@ export async function handleSessionSelect(ctx) {
|
|
|
118
123
|
if (!callbackQuery?.data || !callbackQuery.data.startsWith(SESSION_CALLBACK_PREFIX)) {
|
|
119
124
|
return false;
|
|
120
125
|
}
|
|
126
|
+
if (isForegroundBusy()) {
|
|
127
|
+
await replyBusyBlocked(ctx);
|
|
128
|
+
return true;
|
|
129
|
+
}
|
|
121
130
|
const page = parseSessionPageCallback(callbackQuery.data);
|
|
122
131
|
const sessionId = parseSessionIdCallback(callbackQuery.data);
|
|
123
132
|
const isActiveMenu = await ensureActiveInlineMenu(ctx, "session");
|
package/dist/bot/index.js
CHANGED
|
@@ -36,7 +36,7 @@ import { clearAllInteractionState } from "../interaction/cleanup.js";
|
|
|
36
36
|
import { keyboardManager } from "../keyboard/manager.js";
|
|
37
37
|
import { subscribeToEvents } from "../opencode/events.js";
|
|
38
38
|
import { summaryAggregator } from "../summary/aggregator.js";
|
|
39
|
-
import { formatSummary, formatToolInfo, getAssistantParseMode } from "../summary/formatter.js";
|
|
39
|
+
import { formatSummary, formatSummaryWithMode, formatToolInfo, getAssistantParseMode, } from "../summary/formatter.js";
|
|
40
40
|
import { ToolMessageBatcher } from "../summary/tool-message-batcher.js";
|
|
41
41
|
import { getCurrentSession } from "../session/manager.js";
|
|
42
42
|
import { ingestSessionInfoForCache } from "../session/cache-manager.js";
|
|
@@ -48,15 +48,21 @@ import { processUserPrompt } from "./handlers/prompt.js";
|
|
|
48
48
|
import { handleVoiceMessage } from "./handlers/voice.js";
|
|
49
49
|
import { handleDocumentMessage } from "./handlers/document.js";
|
|
50
50
|
import { downloadTelegramFile, toDataUri } from "./utils/file-download.js";
|
|
51
|
+
import { finalizeAssistantResponse } from "./utils/finalize-assistant-response.js";
|
|
52
|
+
import { deliverThinkingMessage } from "./utils/thinking-message.js";
|
|
51
53
|
import { sendBotText } from "./utils/telegram-text.js";
|
|
52
54
|
import { getModelCapabilities, supportsInput } from "../model/capabilities.js";
|
|
53
55
|
import { getStoredModel } from "../model/manager.js";
|
|
54
56
|
import { foregroundSessionState } from "../scheduled-task/foreground-state.js";
|
|
55
57
|
import { scheduledTaskRuntime } from "../scheduled-task/runtime.js";
|
|
58
|
+
import { ResponseStreamer } from "./streaming/response-streamer.js";
|
|
59
|
+
import { editMessageWithMarkdownFallback, sendMessageWithMarkdownFallback, } from "./utils/send-with-markdown-fallback.js";
|
|
56
60
|
let botInstance = null;
|
|
57
61
|
let chatIdInstance = null;
|
|
58
62
|
let commandsInitialized = false;
|
|
59
63
|
const TELEGRAM_DOCUMENT_CAPTION_MAX_LENGTH = 1024;
|
|
64
|
+
const RESPONSE_STREAM_THROTTLE_MS = 200;
|
|
65
|
+
const RESPONSE_STREAM_TEXT_LIMIT = 3800;
|
|
60
66
|
const SESSION_RETRY_PREFIX = "🔁";
|
|
61
67
|
const __filename = fileURLToPath(import.meta.url);
|
|
62
68
|
const __dirname = path.dirname(__filename);
|
|
@@ -77,6 +83,16 @@ function prepareDocumentCaption(caption) {
|
|
|
77
83
|
}
|
|
78
84
|
return `${normalizedCaption.slice(0, TELEGRAM_DOCUMENT_CAPTION_MAX_LENGTH - 3)}...`;
|
|
79
85
|
}
|
|
86
|
+
function prepareStreamingPayload(messageText) {
|
|
87
|
+
const parts = formatSummaryWithMode(messageText, config.bot.messageFormatMode, RESPONSE_STREAM_TEXT_LIMIT);
|
|
88
|
+
if (parts.length === 0) {
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
return {
|
|
92
|
+
parts,
|
|
93
|
+
format: config.bot.messageFormatMode === "markdown" ? "markdown_v2" : "raw",
|
|
94
|
+
};
|
|
95
|
+
}
|
|
80
96
|
const toolMessageBatcher = new ToolMessageBatcher({
|
|
81
97
|
intervalSeconds: 5,
|
|
82
98
|
sendText: async (sessionId, text) => {
|
|
@@ -118,6 +134,59 @@ const toolMessageBatcher = new ToolMessageBatcher({
|
|
|
118
134
|
}
|
|
119
135
|
},
|
|
120
136
|
});
|
|
137
|
+
const responseStreamer = new ResponseStreamer({
|
|
138
|
+
throttleMs: RESPONSE_STREAM_THROTTLE_MS,
|
|
139
|
+
sendText: async (text, format, options) => {
|
|
140
|
+
if (!botInstance || !chatIdInstance || chatIdInstance <= 0) {
|
|
141
|
+
throw new Error("Bot context missing for streamed send");
|
|
142
|
+
}
|
|
143
|
+
const parseMode = format === "markdown_v2" ? "MarkdownV2" : undefined;
|
|
144
|
+
const sentMessage = await sendMessageWithMarkdownFallback({
|
|
145
|
+
api: botInstance.api,
|
|
146
|
+
chatId: chatIdInstance,
|
|
147
|
+
text,
|
|
148
|
+
options,
|
|
149
|
+
parseMode,
|
|
150
|
+
});
|
|
151
|
+
return sentMessage.message_id;
|
|
152
|
+
},
|
|
153
|
+
editText: async (messageId, text, format, options) => {
|
|
154
|
+
if (!botInstance || !chatIdInstance || chatIdInstance <= 0) {
|
|
155
|
+
throw new Error("Bot context missing for streamed edit");
|
|
156
|
+
}
|
|
157
|
+
const parseMode = format === "markdown_v2" ? "MarkdownV2" : undefined;
|
|
158
|
+
try {
|
|
159
|
+
await editMessageWithMarkdownFallback({
|
|
160
|
+
api: botInstance.api,
|
|
161
|
+
chatId: chatIdInstance,
|
|
162
|
+
messageId,
|
|
163
|
+
text,
|
|
164
|
+
options,
|
|
165
|
+
parseMode,
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
catch (error) {
|
|
169
|
+
const errorMessage = error instanceof Error ? error.message.toLowerCase() : String(error).toLowerCase();
|
|
170
|
+
if (errorMessage.includes("message is not modified")) {
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
throw error;
|
|
174
|
+
}
|
|
175
|
+
},
|
|
176
|
+
deleteText: async (messageId) => {
|
|
177
|
+
if (!botInstance || !chatIdInstance || chatIdInstance <= 0) {
|
|
178
|
+
throw new Error("Bot context missing for streamed delete");
|
|
179
|
+
}
|
|
180
|
+
await botInstance.api.deleteMessage(chatIdInstance, messageId).catch((error) => {
|
|
181
|
+
const errorMessage = error instanceof Error ? error.message.toLowerCase() : String(error).toLowerCase();
|
|
182
|
+
if (errorMessage.includes("message to delete not found") ||
|
|
183
|
+
errorMessage.includes("message identifier is not specified")) {
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
throw error;
|
|
187
|
+
});
|
|
188
|
+
},
|
|
189
|
+
});
|
|
121
190
|
async function ensureCommandsInitialized(ctx, next) {
|
|
122
191
|
if (commandsInitialized || !ctx.from || ctx.from.id !== config.telegram.allowedUserId) {
|
|
123
192
|
await next();
|
|
@@ -149,37 +218,73 @@ async function ensureEventSubscription(directory) {
|
|
|
149
218
|
return;
|
|
150
219
|
}
|
|
151
220
|
toolMessageBatcher.setIntervalSeconds(config.bot.serviceMessagesIntervalSec);
|
|
221
|
+
summaryAggregator.setTypingIndicatorEnabled(true);
|
|
152
222
|
summaryAggregator.setOnCleared(() => {
|
|
153
223
|
toolMessageBatcher.clearAll("summary_aggregator_clear");
|
|
224
|
+
responseStreamer.clearAll("summary_aggregator_clear");
|
|
154
225
|
});
|
|
155
|
-
summaryAggregator.
|
|
226
|
+
summaryAggregator.setOnPartial((sessionId, messageId, messageText) => {
|
|
227
|
+
if (!config.bot.responseStreaming) {
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
if (!botInstance || !chatIdInstance) {
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
const currentSession = getCurrentSession();
|
|
234
|
+
if (!currentSession || currentSession.id !== sessionId) {
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
const preparedStreamPayload = prepareStreamingPayload(messageText);
|
|
238
|
+
if (!preparedStreamPayload) {
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
preparedStreamPayload.sendOptions = undefined;
|
|
242
|
+
preparedStreamPayload.editOptions = undefined;
|
|
243
|
+
responseStreamer.enqueue(sessionId, messageId, preparedStreamPayload);
|
|
244
|
+
});
|
|
245
|
+
summaryAggregator.setOnComplete(async (sessionId, messageId, messageText) => {
|
|
156
246
|
if (!botInstance || !chatIdInstance) {
|
|
157
247
|
logger.error("Bot or chat ID not available for sending message");
|
|
248
|
+
responseStreamer.clearMessage(sessionId, messageId, "bot_context_missing");
|
|
158
249
|
foregroundSessionState.markIdle(sessionId);
|
|
159
250
|
return;
|
|
160
251
|
}
|
|
161
252
|
const currentSession = getCurrentSession();
|
|
162
253
|
if (currentSession?.id !== sessionId) {
|
|
254
|
+
responseStreamer.clearMessage(sessionId, messageId, "session_mismatch");
|
|
163
255
|
foregroundSessionState.markIdle(sessionId);
|
|
164
256
|
await scheduledTaskRuntime.flushDeferredDeliveries();
|
|
165
257
|
return;
|
|
166
258
|
}
|
|
167
|
-
|
|
259
|
+
const botApi = botInstance.api;
|
|
260
|
+
const chatId = chatIdInstance;
|
|
168
261
|
try {
|
|
169
|
-
const
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
262
|
+
const streamedViaMessages = await finalizeAssistantResponse({
|
|
263
|
+
responseStreaming: config.bot.responseStreaming,
|
|
264
|
+
sessionId,
|
|
265
|
+
messageId,
|
|
266
|
+
messageText,
|
|
267
|
+
responseStreamer,
|
|
268
|
+
flushPendingServiceMessages: () => toolMessageBatcher.flushSession(sessionId, "assistant_message_completed"),
|
|
269
|
+
prepareStreamingPayload,
|
|
270
|
+
formatSummary,
|
|
271
|
+
resolveFormat: () => (getAssistantParseMode() === "MarkdownV2" ? "markdown_v2" : "raw"),
|
|
272
|
+
getReplyKeyboard: getCurrentReplyKeyboard,
|
|
273
|
+
sendText: async (text, options, format) => {
|
|
274
|
+
await sendBotText({
|
|
275
|
+
api: botApi,
|
|
276
|
+
chatId,
|
|
277
|
+
text,
|
|
278
|
+
options: options,
|
|
279
|
+
format,
|
|
280
|
+
});
|
|
281
|
+
},
|
|
282
|
+
});
|
|
283
|
+
if (streamedViaMessages) {
|
|
284
|
+
logger.debug(`[Bot] Final assistant message already streamed (session=${sessionId}, message=${messageId})`);
|
|
285
|
+
foregroundSessionState.markIdle(sessionId);
|
|
286
|
+
await scheduledTaskRuntime.flushDeferredDeliveries();
|
|
287
|
+
return;
|
|
183
288
|
}
|
|
184
289
|
}
|
|
185
290
|
catch (err) {
|
|
@@ -282,9 +387,6 @@ async function ensureEventSubscription(directory) {
|
|
|
282
387
|
await showPermissionRequest(botInstance.api, chatIdInstance, request);
|
|
283
388
|
});
|
|
284
389
|
summaryAggregator.setOnThinking(async (sessionId) => {
|
|
285
|
-
if (config.bot.hideThinkingMessages) {
|
|
286
|
-
return;
|
|
287
|
-
}
|
|
288
390
|
if (!botInstance || !chatIdInstance) {
|
|
289
391
|
return;
|
|
290
392
|
}
|
|
@@ -293,7 +395,10 @@ async function ensureEventSubscription(directory) {
|
|
|
293
395
|
return;
|
|
294
396
|
}
|
|
295
397
|
logger.debug("[Bot] Agent started thinking");
|
|
296
|
-
|
|
398
|
+
deliverThinkingMessage(sessionId, toolMessageBatcher, {
|
|
399
|
+
responseStreaming: config.bot.responseStreaming,
|
|
400
|
+
hideThinkingMessages: config.bot.hideThinkingMessages,
|
|
401
|
+
});
|
|
297
402
|
});
|
|
298
403
|
summaryAggregator.setOnTokens(async (tokens) => {
|
|
299
404
|
if (!pinnedMessageManager.isInitialized()) {
|
|
@@ -345,10 +450,12 @@ async function ensureEventSubscription(directory) {
|
|
|
345
450
|
}
|
|
346
451
|
const currentSession = getCurrentSession();
|
|
347
452
|
if (!currentSession || currentSession.id !== sessionId) {
|
|
453
|
+
responseStreamer.clearSession(sessionId, "session_error_not_current");
|
|
348
454
|
foregroundSessionState.markIdle(sessionId);
|
|
349
455
|
await scheduledTaskRuntime.flushDeferredDeliveries();
|
|
350
456
|
return;
|
|
351
457
|
}
|
|
458
|
+
responseStreamer.clearSession(sessionId, "session_error");
|
|
352
459
|
await toolMessageBatcher.flushSession(sessionId, "session_error");
|
|
353
460
|
const normalizedMessage = message.trim() || t("common.unknown_error");
|
|
354
461
|
const truncatedMessage = normalizedMessage.length > 3500
|
|
@@ -77,8 +77,12 @@ export async function interactionGuardMiddleware(ctx, next) {
|
|
|
77
77
|
await next();
|
|
78
78
|
return;
|
|
79
79
|
}
|
|
80
|
-
const message =
|
|
81
|
-
|
|
80
|
+
const message = decision.busy
|
|
81
|
+
? decision.state?.kind === "question" || decision.state?.kind === "permission"
|
|
82
|
+
? getInteractionBlockedMessage(decision.reason, decision.state.kind)
|
|
83
|
+
: t("interaction.blocked.finish_current")
|
|
84
|
+
: getInteractionBlockedMessage(decision.reason, decision.state?.kind);
|
|
85
|
+
logger.debug(`[InteractionGuard] Blocked input: interactionKind=${decision.state?.kind || "none"}, inputType=${decision.inputType}, reason=${decision.reason || "unknown"}, command=${decision.command || "-"}, busy=${decision.busy ? "yes" : "no"}`);
|
|
82
86
|
if (ctx.callbackQuery) {
|
|
83
87
|
await ctx.answerCallbackQuery({ text: message }).catch(() => { });
|
|
84
88
|
return;
|