@grinev/opencode-telegram-bot 0.13.0 → 0.13.2
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/dist/bot/commands/commands.js +18 -4
- package/dist/bot/index.js +77 -6
- package/dist/bot/streaming/tool-call-streamer.js +285 -0
- package/dist/bot/utils/thinking-message.js +2 -5
- package/dist/i18n/de.js +1 -1
- package/dist/i18n/en.js +1 -1
- package/dist/i18n/es.js +1 -1
- package/dist/i18n/fr.js +1 -1
- package/dist/i18n/ru.js +1 -1
- package/dist/i18n/zh.js +1 -1
- package/dist/scheduled-task/runtime.js +27 -21
- package/dist/summary/formatter.js +34 -1
- package/package.json +1 -1
|
@@ -19,9 +19,19 @@ const COMMANDS_CALLBACK_CANCEL = `${COMMANDS_CALLBACK_PREFIX}cancel`;
|
|
|
19
19
|
const COMMANDS_CALLBACK_EXECUTE = `${COMMANDS_CALLBACK_PREFIX}execute`;
|
|
20
20
|
const MAX_INLINE_BUTTON_LABEL_LENGTH = 64;
|
|
21
21
|
function formatExecutingCommandMessage(commandName, args) {
|
|
22
|
-
const
|
|
22
|
+
const prefix = t("commands.executing_prefix");
|
|
23
|
+
const commandText = `/${commandName}`;
|
|
23
24
|
const argsSuffix = args ? ` ${args}` : "";
|
|
24
|
-
return
|
|
25
|
+
return {
|
|
26
|
+
text: `${prefix}\n${commandText}${argsSuffix}`,
|
|
27
|
+
entities: [
|
|
28
|
+
{
|
|
29
|
+
type: "code",
|
|
30
|
+
offset: prefix.length + 1,
|
|
31
|
+
length: commandText.length,
|
|
32
|
+
},
|
|
33
|
+
],
|
|
34
|
+
};
|
|
25
35
|
}
|
|
26
36
|
export function buildCommandPageCallback(page) {
|
|
27
37
|
return `${COMMANDS_CALLBACK_PAGE_PREFIX}${page}`;
|
|
@@ -181,7 +191,10 @@ async function getCommandList(projectDirectory) {
|
|
|
181
191
|
throw error || new Error("No command data received");
|
|
182
192
|
}
|
|
183
193
|
return data
|
|
184
|
-
.filter((command) =>
|
|
194
|
+
.filter((command) => {
|
|
195
|
+
const source = command.source;
|
|
196
|
+
return (typeof command.name === "string" && command.name.trim().length > 0 && source === "command");
|
|
197
|
+
})
|
|
185
198
|
.map((command) => ({
|
|
186
199
|
name: command.name,
|
|
187
200
|
description: command.description,
|
|
@@ -252,7 +265,8 @@ async function executeCommand(ctx, deps, params) {
|
|
|
252
265
|
return;
|
|
253
266
|
}
|
|
254
267
|
const args = params.argumentsText.trim();
|
|
255
|
-
|
|
268
|
+
const executingMessage = formatExecutingCommandMessage(params.commandName, args);
|
|
269
|
+
await ctx.reply(executingMessage.text, { entities: executingMessage.entities });
|
|
256
270
|
const session = await ensureSessionForProject(ctx, params.projectDirectory);
|
|
257
271
|
if (!session) {
|
|
258
272
|
return;
|
package/dist/bot/index.js
CHANGED
|
@@ -56,6 +56,7 @@ import { getStoredModel } from "../model/manager.js";
|
|
|
56
56
|
import { foregroundSessionState } from "../scheduled-task/foreground-state.js";
|
|
57
57
|
import { scheduledTaskRuntime } from "../scheduled-task/runtime.js";
|
|
58
58
|
import { ResponseStreamer } from "./streaming/response-streamer.js";
|
|
59
|
+
import { ToolCallStreamer } from "./streaming/tool-call-streamer.js";
|
|
59
60
|
import { editMessageWithMarkdownFallback, sendMessageWithMarkdownFallback, } from "./utils/send-with-markdown-fallback.js";
|
|
60
61
|
let botInstance = null;
|
|
61
62
|
let chatIdInstance = null;
|
|
@@ -187,6 +188,58 @@ const responseStreamer = new ResponseStreamer({
|
|
|
187
188
|
});
|
|
188
189
|
},
|
|
189
190
|
});
|
|
191
|
+
const toolCallStreamer = new ToolCallStreamer({
|
|
192
|
+
throttleMs: RESPONSE_STREAM_THROTTLE_MS,
|
|
193
|
+
sendText: async (sessionId, text) => {
|
|
194
|
+
if (!botInstance || !chatIdInstance || chatIdInstance <= 0) {
|
|
195
|
+
throw new Error("Bot context missing for tool stream send");
|
|
196
|
+
}
|
|
197
|
+
const currentSession = getCurrentSession();
|
|
198
|
+
if (!currentSession || currentSession.id !== sessionId) {
|
|
199
|
+
throw new Error(`Tool stream session mismatch for send: ${sessionId}`);
|
|
200
|
+
}
|
|
201
|
+
const sentMessage = await botInstance.api.sendMessage(chatIdInstance, text, {
|
|
202
|
+
disable_notification: true,
|
|
203
|
+
});
|
|
204
|
+
return sentMessage.message_id;
|
|
205
|
+
},
|
|
206
|
+
editText: async (sessionId, messageId, text) => {
|
|
207
|
+
if (!botInstance || !chatIdInstance || chatIdInstance <= 0) {
|
|
208
|
+
throw new Error("Bot context missing for tool stream edit");
|
|
209
|
+
}
|
|
210
|
+
const currentSession = getCurrentSession();
|
|
211
|
+
if (!currentSession || currentSession.id !== sessionId) {
|
|
212
|
+
throw new Error(`Tool stream session mismatch for edit: ${sessionId}`);
|
|
213
|
+
}
|
|
214
|
+
try {
|
|
215
|
+
await botInstance.api.editMessageText(chatIdInstance, messageId, text);
|
|
216
|
+
}
|
|
217
|
+
catch (error) {
|
|
218
|
+
const errorMessage = error instanceof Error ? error.message.toLowerCase() : String(error).toLowerCase();
|
|
219
|
+
if (errorMessage.includes("message is not modified")) {
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
throw error;
|
|
223
|
+
}
|
|
224
|
+
},
|
|
225
|
+
deleteText: async (sessionId, messageId) => {
|
|
226
|
+
if (!botInstance || !chatIdInstance || chatIdInstance <= 0) {
|
|
227
|
+
throw new Error("Bot context missing for tool stream delete");
|
|
228
|
+
}
|
|
229
|
+
const currentSession = getCurrentSession();
|
|
230
|
+
if (!currentSession || currentSession.id !== sessionId) {
|
|
231
|
+
throw new Error(`Tool stream session mismatch for delete: ${sessionId}`);
|
|
232
|
+
}
|
|
233
|
+
await botInstance.api.deleteMessage(chatIdInstance, messageId).catch((error) => {
|
|
234
|
+
const errorMessage = error instanceof Error ? error.message.toLowerCase() : String(error).toLowerCase();
|
|
235
|
+
if (errorMessage.includes("message to delete not found") ||
|
|
236
|
+
errorMessage.includes("message identifier is not specified")) {
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
throw error;
|
|
240
|
+
});
|
|
241
|
+
},
|
|
242
|
+
});
|
|
190
243
|
async function ensureCommandsInitialized(ctx, next) {
|
|
191
244
|
if (commandsInitialized || !ctx.from || ctx.from.id !== config.telegram.allowedUserId) {
|
|
192
245
|
await next();
|
|
@@ -221,6 +274,7 @@ async function ensureEventSubscription(directory) {
|
|
|
221
274
|
summaryAggregator.setTypingIndicatorEnabled(true);
|
|
222
275
|
summaryAggregator.setOnCleared(() => {
|
|
223
276
|
toolMessageBatcher.clearAll("summary_aggregator_clear");
|
|
277
|
+
toolCallStreamer.clearAll("summary_aggregator_clear");
|
|
224
278
|
responseStreamer.clearAll("summary_aggregator_clear");
|
|
225
279
|
});
|
|
226
280
|
summaryAggregator.setOnPartial((sessionId, messageId, messageText) => {
|
|
@@ -246,12 +300,14 @@ async function ensureEventSubscription(directory) {
|
|
|
246
300
|
if (!botInstance || !chatIdInstance) {
|
|
247
301
|
logger.error("Bot or chat ID not available for sending message");
|
|
248
302
|
responseStreamer.clearMessage(sessionId, messageId, "bot_context_missing");
|
|
303
|
+
toolCallStreamer.clearSession(sessionId, "bot_context_missing");
|
|
249
304
|
foregroundSessionState.markIdle(sessionId);
|
|
250
305
|
return;
|
|
251
306
|
}
|
|
252
307
|
const currentSession = getCurrentSession();
|
|
253
308
|
if (currentSession?.id !== sessionId) {
|
|
254
309
|
responseStreamer.clearMessage(sessionId, messageId, "session_mismatch");
|
|
310
|
+
toolCallStreamer.clearSession(sessionId, "session_mismatch");
|
|
255
311
|
foregroundSessionState.markIdle(sessionId);
|
|
256
312
|
await scheduledTaskRuntime.flushDeferredDeliveries();
|
|
257
313
|
return;
|
|
@@ -265,7 +321,10 @@ async function ensureEventSubscription(directory) {
|
|
|
265
321
|
messageId,
|
|
266
322
|
messageText,
|
|
267
323
|
responseStreamer,
|
|
268
|
-
flushPendingServiceMessages: () =>
|
|
324
|
+
flushPendingServiceMessages: () => Promise.all([
|
|
325
|
+
toolMessageBatcher.flushSession(sessionId, "assistant_message_completed"),
|
|
326
|
+
toolCallStreamer.flushSession(sessionId, "assistant_message_completed"),
|
|
327
|
+
]).then(() => undefined),
|
|
269
328
|
prepareStreamingPayload,
|
|
270
329
|
formatSummary,
|
|
271
330
|
resolveFormat: () => (getAssistantParseMode() === "MarkdownV2" ? "markdown_v2" : "raw"),
|
|
@@ -315,7 +374,7 @@ async function ensureEventSubscription(directory) {
|
|
|
315
374
|
try {
|
|
316
375
|
const message = formatToolInfo(toolInfo);
|
|
317
376
|
if (message) {
|
|
318
|
-
|
|
377
|
+
toolCallStreamer.append(toolInfo.sessionId, message);
|
|
319
378
|
}
|
|
320
379
|
}
|
|
321
380
|
catch (err) {
|
|
@@ -332,6 +391,7 @@ async function ensureEventSubscription(directory) {
|
|
|
332
391
|
return;
|
|
333
392
|
}
|
|
334
393
|
try {
|
|
394
|
+
await toolCallStreamer.breakSession(fileInfo.sessionId, "tool_file_boundary");
|
|
335
395
|
const toolMessage = formatToolInfo(fileInfo);
|
|
336
396
|
const caption = prepareDocumentCaption(toolMessage || fileInfo.fileData.caption);
|
|
337
397
|
toolMessageBatcher.enqueueFile(fileInfo.sessionId, {
|
|
@@ -350,7 +410,10 @@ async function ensureEventSubscription(directory) {
|
|
|
350
410
|
}
|
|
351
411
|
const currentSession = getCurrentSession();
|
|
352
412
|
if (currentSession) {
|
|
353
|
-
await
|
|
413
|
+
await Promise.all([
|
|
414
|
+
toolMessageBatcher.flushSession(currentSession.id, "question_asked"),
|
|
415
|
+
toolCallStreamer.flushSession(currentSession.id, "question_asked"),
|
|
416
|
+
]);
|
|
354
417
|
}
|
|
355
418
|
if (questionManager.isActive()) {
|
|
356
419
|
logger.warn("[Bot] Replacing active poll with a new one");
|
|
@@ -382,7 +445,10 @@ async function ensureEventSubscription(directory) {
|
|
|
382
445
|
logger.error("Bot or chat ID not available for showing permission request");
|
|
383
446
|
return;
|
|
384
447
|
}
|
|
385
|
-
await
|
|
448
|
+
await Promise.all([
|
|
449
|
+
toolMessageBatcher.flushSession(request.sessionID, "permission_asked"),
|
|
450
|
+
toolCallStreamer.flushSession(request.sessionID, "permission_asked"),
|
|
451
|
+
]);
|
|
386
452
|
logger.info(`[Bot] Received permission request from agent: type=${request.permission}, requestID=${request.id}`);
|
|
387
453
|
await showPermissionRequest(botInstance.api, chatIdInstance, request);
|
|
388
454
|
});
|
|
@@ -395,6 +461,7 @@ async function ensureEventSubscription(directory) {
|
|
|
395
461
|
return;
|
|
396
462
|
}
|
|
397
463
|
logger.debug("[Bot] Agent started thinking");
|
|
464
|
+
await toolCallStreamer.breakSession(sessionId, "thinking_started");
|
|
398
465
|
deliverThinkingMessage(sessionId, toolMessageBatcher, {
|
|
399
466
|
responseStreaming: config.bot.responseStreaming,
|
|
400
467
|
hideThinkingMessages: config.bot.hideThinkingMessages,
|
|
@@ -451,12 +518,16 @@ async function ensureEventSubscription(directory) {
|
|
|
451
518
|
const currentSession = getCurrentSession();
|
|
452
519
|
if (!currentSession || currentSession.id !== sessionId) {
|
|
453
520
|
responseStreamer.clearSession(sessionId, "session_error_not_current");
|
|
521
|
+
toolCallStreamer.clearSession(sessionId, "session_error_not_current");
|
|
454
522
|
foregroundSessionState.markIdle(sessionId);
|
|
455
523
|
await scheduledTaskRuntime.flushDeferredDeliveries();
|
|
456
524
|
return;
|
|
457
525
|
}
|
|
458
526
|
responseStreamer.clearSession(sessionId, "session_error");
|
|
459
|
-
await
|
|
527
|
+
await Promise.all([
|
|
528
|
+
toolMessageBatcher.flushSession(sessionId, "session_error"),
|
|
529
|
+
toolCallStreamer.flushSession(sessionId, "session_error"),
|
|
530
|
+
]);
|
|
460
531
|
const normalizedMessage = message.trim() || t("common.unknown_error");
|
|
461
532
|
const truncatedMessage = normalizedMessage.length > 3500
|
|
462
533
|
? `${normalizedMessage.slice(0, 3497)}...`
|
|
@@ -482,7 +553,7 @@ async function ensureEventSubscription(directory) {
|
|
|
482
553
|
? `${normalizedMessage.slice(0, 3497)}...`
|
|
483
554
|
: normalizedMessage;
|
|
484
555
|
const retryMessage = t("bot.session_retry", { message: truncatedMessage });
|
|
485
|
-
|
|
556
|
+
toolCallStreamer.replaceByPrefix(sessionId, SESSION_RETRY_PREFIX, retryMessage);
|
|
486
557
|
});
|
|
487
558
|
summaryAggregator.setOnSessionDiff(async (_sessionId, diffs) => {
|
|
488
559
|
if (!pinnedMessageManager.isInitialized()) {
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
import { logger } from "../../utils/logger.js";
|
|
2
|
+
const TELEGRAM_MESSAGE_SAFE_LENGTH = 4000;
|
|
3
|
+
function getErrorMessage(error) {
|
|
4
|
+
if (error instanceof Error) {
|
|
5
|
+
return error.message;
|
|
6
|
+
}
|
|
7
|
+
return String(error);
|
|
8
|
+
}
|
|
9
|
+
function getRetryAfterMs(error) {
|
|
10
|
+
const message = getErrorMessage(error);
|
|
11
|
+
if (!/\b429\b/.test(message)) {
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
const retryMatch = message.match(/retry after\s+(\d+)/i);
|
|
15
|
+
if (!retryMatch) {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
const seconds = Number.parseInt(retryMatch[1], 10);
|
|
19
|
+
if (!Number.isFinite(seconds) || seconds <= 0) {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
return seconds * 1000;
|
|
23
|
+
}
|
|
24
|
+
function delay(ms) {
|
|
25
|
+
return new Promise((resolve) => {
|
|
26
|
+
setTimeout(resolve, ms);
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
function splitLongText(text, limit) {
|
|
30
|
+
if (text.length <= limit) {
|
|
31
|
+
return [text];
|
|
32
|
+
}
|
|
33
|
+
const chunks = [];
|
|
34
|
+
let remaining = text;
|
|
35
|
+
while (remaining.length > limit) {
|
|
36
|
+
let splitIndex = remaining.lastIndexOf("\n", limit);
|
|
37
|
+
if (splitIndex <= 0 || splitIndex < Math.floor(limit * 0.5)) {
|
|
38
|
+
splitIndex = limit;
|
|
39
|
+
}
|
|
40
|
+
chunks.push(remaining.slice(0, splitIndex));
|
|
41
|
+
remaining = remaining.slice(splitIndex).replace(/^\n+/, "");
|
|
42
|
+
}
|
|
43
|
+
if (remaining.length > 0) {
|
|
44
|
+
chunks.push(remaining);
|
|
45
|
+
}
|
|
46
|
+
return chunks;
|
|
47
|
+
}
|
|
48
|
+
function buildParts(entries) {
|
|
49
|
+
const text = entries
|
|
50
|
+
.map((entry) => entry.text.trim())
|
|
51
|
+
.filter(Boolean)
|
|
52
|
+
.join("\n\n");
|
|
53
|
+
if (!text) {
|
|
54
|
+
return [];
|
|
55
|
+
}
|
|
56
|
+
return splitLongText(text, TELEGRAM_MESSAGE_SAFE_LENGTH).filter(Boolean);
|
|
57
|
+
}
|
|
58
|
+
export class ToolCallStreamer {
|
|
59
|
+
throttleMs;
|
|
60
|
+
sendText;
|
|
61
|
+
editText;
|
|
62
|
+
deleteText;
|
|
63
|
+
states = new Map();
|
|
64
|
+
allStates = new Set();
|
|
65
|
+
constructor(options) {
|
|
66
|
+
this.throttleMs = Math.max(0, Math.floor(options.throttleMs));
|
|
67
|
+
this.sendText = options.sendText;
|
|
68
|
+
this.editText = options.editText;
|
|
69
|
+
this.deleteText = options.deleteText;
|
|
70
|
+
}
|
|
71
|
+
append(sessionId, text) {
|
|
72
|
+
const normalizedText = text.trim();
|
|
73
|
+
if (!sessionId || !normalizedText) {
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
const state = this.getOrCreateState(sessionId);
|
|
77
|
+
state.entries.push({ text: normalizedText });
|
|
78
|
+
state.latestParts = buildParts(state.entries);
|
|
79
|
+
this.ensureTimer(state);
|
|
80
|
+
}
|
|
81
|
+
replaceByPrefix(sessionId, prefix, text) {
|
|
82
|
+
const normalizedPrefix = prefix.trim();
|
|
83
|
+
const normalizedText = text.trim();
|
|
84
|
+
if (!sessionId || !normalizedPrefix || !normalizedText) {
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
const state = this.getOrCreateState(sessionId);
|
|
88
|
+
const existingEntry = state.entries.find((entry) => entry.prefix === normalizedPrefix);
|
|
89
|
+
if (existingEntry) {
|
|
90
|
+
existingEntry.text = normalizedText;
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
state.entries.push({ prefix: normalizedPrefix, text: normalizedText });
|
|
94
|
+
}
|
|
95
|
+
state.latestParts = buildParts(state.entries);
|
|
96
|
+
this.ensureTimer(state);
|
|
97
|
+
}
|
|
98
|
+
async flushSession(sessionId, reason) {
|
|
99
|
+
const state = this.states.get(sessionId);
|
|
100
|
+
if (!state) {
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
this.clearTimer(state);
|
|
104
|
+
await this.enqueueTask(state, () => this.syncState(state, reason));
|
|
105
|
+
}
|
|
106
|
+
async breakSession(sessionId, reason) {
|
|
107
|
+
const state = this.states.get(sessionId);
|
|
108
|
+
if (!state) {
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
state.isBreaking = true;
|
|
112
|
+
this.getOrCreateState(sessionId);
|
|
113
|
+
this.clearTimer(state);
|
|
114
|
+
await this.enqueueTask(state, () => this.syncState(state, reason));
|
|
115
|
+
this.cancelState(state);
|
|
116
|
+
this.removeState(state);
|
|
117
|
+
logger.debug(`[ToolCallStreamer] Broke session stream: session=${sessionId}, reason=${reason}`);
|
|
118
|
+
}
|
|
119
|
+
clearSession(sessionId, reason) {
|
|
120
|
+
let clearedAny = false;
|
|
121
|
+
for (const state of Array.from(this.allStates)) {
|
|
122
|
+
if (state.sessionId !== sessionId) {
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
this.cancelState(state);
|
|
126
|
+
this.removeState(state);
|
|
127
|
+
clearedAny = true;
|
|
128
|
+
}
|
|
129
|
+
if (clearedAny) {
|
|
130
|
+
logger.debug(`[ToolCallStreamer] Cleared session stream: session=${sessionId}, reason=${reason}`);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
clearAll(reason) {
|
|
134
|
+
for (const state of Array.from(this.allStates)) {
|
|
135
|
+
this.cancelState(state);
|
|
136
|
+
}
|
|
137
|
+
const count = this.allStates.size;
|
|
138
|
+
this.states.clear();
|
|
139
|
+
this.allStates.clear();
|
|
140
|
+
if (count > 0) {
|
|
141
|
+
logger.debug(`[ToolCallStreamer] Cleared all streams: count=${count}, reason=${reason}`);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
getOrCreateState(sessionId) {
|
|
145
|
+
const existing = this.states.get(sessionId);
|
|
146
|
+
if (existing && !existing.isBroken && !existing.cancelled && !existing.isBreaking) {
|
|
147
|
+
return existing;
|
|
148
|
+
}
|
|
149
|
+
if (existing && (existing.isBroken || existing.cancelled)) {
|
|
150
|
+
this.clearTimer(existing);
|
|
151
|
+
this.removeState(existing);
|
|
152
|
+
}
|
|
153
|
+
const state = {
|
|
154
|
+
sessionId,
|
|
155
|
+
entries: [],
|
|
156
|
+
latestParts: [],
|
|
157
|
+
lastSentParts: [],
|
|
158
|
+
telegramMessageIds: [],
|
|
159
|
+
timer: null,
|
|
160
|
+
task: Promise.resolve(true),
|
|
161
|
+
cancelled: false,
|
|
162
|
+
isBroken: false,
|
|
163
|
+
isBreaking: false,
|
|
164
|
+
fatalErrorMessage: null,
|
|
165
|
+
fatalErrorLogged: false,
|
|
166
|
+
};
|
|
167
|
+
this.states.set(sessionId, state);
|
|
168
|
+
this.allStates.add(state);
|
|
169
|
+
return state;
|
|
170
|
+
}
|
|
171
|
+
clearTimer(state) {
|
|
172
|
+
if (!state.timer) {
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
clearTimeout(state.timer);
|
|
176
|
+
state.timer = null;
|
|
177
|
+
}
|
|
178
|
+
ensureTimer(state) {
|
|
179
|
+
if (state.timer || state.isBroken || state.cancelled) {
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
if (this.throttleMs === 0) {
|
|
183
|
+
void this.enqueueTask(state, () => this.syncState(state, "immediate")).catch((error) => {
|
|
184
|
+
logger.error(`[ToolCallStreamer] Immediate sync failed: session=${state.sessionId}`, error);
|
|
185
|
+
});
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
state.timer = setTimeout(() => {
|
|
189
|
+
state.timer = null;
|
|
190
|
+
void this.enqueueTask(state, () => this.syncState(state, "throttle_elapsed")).catch((error) => {
|
|
191
|
+
logger.error(`[ToolCallStreamer] Throttled sync failed: session=${state.sessionId}`, error);
|
|
192
|
+
});
|
|
193
|
+
}, this.throttleMs);
|
|
194
|
+
}
|
|
195
|
+
enqueueTask(state, task) {
|
|
196
|
+
const nextTask = state.task.catch(() => false).then(task);
|
|
197
|
+
state.task = nextTask;
|
|
198
|
+
return nextTask;
|
|
199
|
+
}
|
|
200
|
+
cancelState(state) {
|
|
201
|
+
state.cancelled = true;
|
|
202
|
+
this.clearTimer(state);
|
|
203
|
+
}
|
|
204
|
+
removeState(state) {
|
|
205
|
+
if (this.states.get(state.sessionId) === state) {
|
|
206
|
+
this.states.delete(state.sessionId);
|
|
207
|
+
}
|
|
208
|
+
this.allStates.delete(state);
|
|
209
|
+
}
|
|
210
|
+
async syncState(state, reason) {
|
|
211
|
+
if (state.cancelled) {
|
|
212
|
+
return false;
|
|
213
|
+
}
|
|
214
|
+
if (state.isBroken) {
|
|
215
|
+
return false;
|
|
216
|
+
}
|
|
217
|
+
while (!state.isBroken && !state.cancelled) {
|
|
218
|
+
const parts = state.latestParts;
|
|
219
|
+
const unchanged = parts.length === state.lastSentParts.length &&
|
|
220
|
+
parts.every((part, index) => state.lastSentParts[index] === part);
|
|
221
|
+
if (unchanged) {
|
|
222
|
+
return state.telegramMessageIds.length > 0;
|
|
223
|
+
}
|
|
224
|
+
if (parts.length === 0) {
|
|
225
|
+
return state.telegramMessageIds.length > 0;
|
|
226
|
+
}
|
|
227
|
+
try {
|
|
228
|
+
await this.syncMessages(state, parts);
|
|
229
|
+
if (state.cancelled) {
|
|
230
|
+
return false;
|
|
231
|
+
}
|
|
232
|
+
logger.debug(`[ToolCallStreamer] Stream synced: session=${state.sessionId}, reason=${reason}, parts=${parts.length}`);
|
|
233
|
+
return true;
|
|
234
|
+
}
|
|
235
|
+
catch (error) {
|
|
236
|
+
const retryAfterMs = getRetryAfterMs(error);
|
|
237
|
+
if (retryAfterMs === null) {
|
|
238
|
+
this.markStreamBroken(state, error, reason);
|
|
239
|
+
return false;
|
|
240
|
+
}
|
|
241
|
+
const delayMs = Math.max(this.throttleMs, retryAfterMs);
|
|
242
|
+
logger.warn(`[ToolCallStreamer] Stream sync rate-limited, retrying in ${delayMs}ms: session=${state.sessionId}, reason=${reason}`, error);
|
|
243
|
+
await delay(delayMs);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
return false;
|
|
247
|
+
}
|
|
248
|
+
markStreamBroken(state, error, reason) {
|
|
249
|
+
state.isBroken = true;
|
|
250
|
+
state.fatalErrorMessage = getErrorMessage(error);
|
|
251
|
+
if (state.fatalErrorLogged) {
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
state.fatalErrorLogged = true;
|
|
255
|
+
logger.error(`[ToolCallStreamer] Stream marked as broken: session=${state.sessionId}, reason=${reason}, error=${state.fatalErrorMessage}`, error);
|
|
256
|
+
}
|
|
257
|
+
async syncMessages(state, parts) {
|
|
258
|
+
for (let index = 0; index < parts.length; index++) {
|
|
259
|
+
if (state.cancelled) {
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
const text = parts[index];
|
|
263
|
+
const currentMessageId = state.telegramMessageIds[index];
|
|
264
|
+
if (currentMessageId) {
|
|
265
|
+
await this.editText(state.sessionId, currentMessageId, text);
|
|
266
|
+
state.lastSentParts[index] = text;
|
|
267
|
+
continue;
|
|
268
|
+
}
|
|
269
|
+
const messageId = await this.sendText(state.sessionId, text);
|
|
270
|
+
state.telegramMessageIds[index] = messageId;
|
|
271
|
+
state.lastSentParts[index] = text;
|
|
272
|
+
}
|
|
273
|
+
for (let index = state.telegramMessageIds.length - 1; index >= parts.length; index--) {
|
|
274
|
+
if (state.cancelled) {
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
const messageId = state.telegramMessageIds[index];
|
|
278
|
+
if (messageId) {
|
|
279
|
+
await this.deleteText(state.sessionId, messageId);
|
|
280
|
+
}
|
|
281
|
+
state.telegramMessageIds.pop();
|
|
282
|
+
state.lastSentParts.pop();
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
@@ -4,9 +4,6 @@ export function deliverThinkingMessage(sessionId, batcher, options) {
|
|
|
4
4
|
return;
|
|
5
5
|
}
|
|
6
6
|
const message = t("bot.thinking");
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
return;
|
|
10
|
-
}
|
|
11
|
-
batcher.enqueue(sessionId, message);
|
|
7
|
+
void options.responseStreaming;
|
|
8
|
+
batcher.sendTextNow(sessionId, message, "thinking_started");
|
|
12
9
|
}
|
package/dist/i18n/de.js
CHANGED
|
@@ -273,7 +273,7 @@ export const de = {
|
|
|
273
273
|
"task.schedule_too_frequent": "Der wiederkehrende Zeitplan ist zu häufig. Das minimale erlaubte Intervall ist einmal alle 5 Minuten.",
|
|
274
274
|
"task.kind.cron": "wiederkehrend",
|
|
275
275
|
"task.kind.once": "einmalig",
|
|
276
|
-
"task.run.success": "⏰ Geplante Aufgabe abgeschlossen: {description}
|
|
276
|
+
"task.run.success": "⏰ Geplante Aufgabe abgeschlossen: {description}",
|
|
277
277
|
"task.run.error": "🔴 Geplante Aufgabe fehlgeschlagen: {description}\n\nFehler: {error}",
|
|
278
278
|
"tasklist.empty": "📭 Noch keine geplanten Aufgaben.",
|
|
279
279
|
"tasklist.select": "Wähle eine geplante Aufgabe:",
|
package/dist/i18n/en.js
CHANGED
|
@@ -273,7 +273,7 @@ export const en = {
|
|
|
273
273
|
"task.schedule_too_frequent": "Recurring schedule is too frequent. The minimum allowed interval is once every 5 minutes.",
|
|
274
274
|
"task.kind.cron": "recurring",
|
|
275
275
|
"task.kind.once": "one-time",
|
|
276
|
-
"task.run.success": "⏰ Scheduled task completed: {description}
|
|
276
|
+
"task.run.success": "⏰ Scheduled task completed: {description}",
|
|
277
277
|
"task.run.error": "🔴 Scheduled task failed: {description}\n\nError: {error}",
|
|
278
278
|
"tasklist.empty": "📭 No scheduled tasks yet.",
|
|
279
279
|
"tasklist.select": "Select a scheduled task:",
|
package/dist/i18n/es.js
CHANGED
|
@@ -273,7 +273,7 @@ export const es = {
|
|
|
273
273
|
"task.schedule_too_frequent": "El horario recurrente es demasiado frecuente. El intervalo mínimo permitido es una vez cada 5 minutos.",
|
|
274
274
|
"task.kind.cron": "recurrente",
|
|
275
275
|
"task.kind.once": "única",
|
|
276
|
-
"task.run.success": "⏰ Tarea programada completada: {description}
|
|
276
|
+
"task.run.success": "⏰ Tarea programada completada: {description}",
|
|
277
277
|
"task.run.error": "🔴 La tarea programada falló: {description}\n\nError: {error}",
|
|
278
278
|
"tasklist.empty": "📭 Aún no hay tareas programadas.",
|
|
279
279
|
"tasklist.select": "Elige una tarea programada:",
|
package/dist/i18n/fr.js
CHANGED
|
@@ -273,7 +273,7 @@ export const fr = {
|
|
|
273
273
|
"task.schedule_too_frequent": "Le planning récurrent est trop fréquent. L'intervalle minimum autorisé est d'une fois toutes les 5 minutes.",
|
|
274
274
|
"task.kind.cron": "récurrente",
|
|
275
275
|
"task.kind.once": "ponctuelle",
|
|
276
|
-
"task.run.success": "⏰ Tâche planifiée terminée : {description}
|
|
276
|
+
"task.run.success": "⏰ Tâche planifiée terminée : {description}",
|
|
277
277
|
"task.run.error": "🔴 Échec de la tâche planifiée : {description}\n\nErreur : {error}",
|
|
278
278
|
"tasklist.empty": "📭 Aucune tâche planifiée pour le moment.",
|
|
279
279
|
"tasklist.select": "Sélectionnez une tâche planifiée :",
|
package/dist/i18n/ru.js
CHANGED
|
@@ -273,7 +273,7 @@ export const ru = {
|
|
|
273
273
|
"task.schedule_too_frequent": "Повторяющееся расписание слишком частое. Минимально допустимый интервал - один запуск в 5 минут.",
|
|
274
274
|
"task.kind.cron": "повторяющаяся",
|
|
275
275
|
"task.kind.once": "однократная",
|
|
276
|
-
"task.run.success": "⏰ Задача по расписанию выполнена: {description}
|
|
276
|
+
"task.run.success": "⏰ Задача по расписанию выполнена: {description}",
|
|
277
277
|
"task.run.error": "🔴 Ошибка выполнения задачи по расписанию: {description}\n\nОшибка: {error}",
|
|
278
278
|
"tasklist.empty": "📭 Задач по расписанию пока нет.",
|
|
279
279
|
"tasklist.select": "Выберите задачу по расписанию:",
|
package/dist/i18n/zh.js
CHANGED
|
@@ -273,7 +273,7 @@ export const zh = {
|
|
|
273
273
|
"task.schedule_too_frequent": "重复任务过于频繁。最小允许间隔为每 5 分钟一次。",
|
|
274
274
|
"task.kind.cron": "重复",
|
|
275
275
|
"task.kind.once": "一次性",
|
|
276
|
-
"task.run.success": "⏰ 定时任务已完成: {description}
|
|
276
|
+
"task.run.success": "⏰ 定时任务已完成: {description}",
|
|
277
277
|
"task.run.error": "🔴 定时任务执行失败: {description}\n\n错误: {error}",
|
|
278
278
|
"tasklist.empty": "📭 还没有定时任务。",
|
|
279
279
|
"tasklist.select": "请选择一个定时任务:",
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { config } from "../config.js";
|
|
2
|
+
import { escapePlainTextForTelegramMarkdownV2, formatSummaryWithMode, } from "../summary/formatter.js";
|
|
2
3
|
import { t } from "../i18n/index.js";
|
|
3
4
|
import { logger } from "../utils/logger.js";
|
|
4
5
|
import { safeBackgroundTask } from "../utils/safe-background-task.js";
|
|
@@ -7,28 +8,30 @@ import { executeScheduledTask } from "./executor.js";
|
|
|
7
8
|
import { foregroundSessionState } from "./foreground-state.js";
|
|
8
9
|
import { computeNextRunAt, isTaskDue } from "./next-run.js";
|
|
9
10
|
import { getScheduledTask, listScheduledTasks, removeScheduledTask, replaceScheduledTasks, updateScheduledTask, } from "./store.js";
|
|
10
|
-
const TELEGRAM_MESSAGE_LIMIT = 4096;
|
|
11
11
|
const MAX_TIMER_DELAY_MS = 2_147_483_647;
|
|
12
|
+
const TELEGRAM_MESSAGE_LIMIT = 4096;
|
|
12
13
|
const TASK_DESCRIPTION_PREVIEW_LENGTH = 64;
|
|
13
14
|
const RESTART_INTERRUPTED_ERROR = "Interrupted by bot restart during scheduled task execution.";
|
|
14
|
-
function
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
function getScheduledTaskDeliveryFormat() {
|
|
16
|
+
return config.bot.messageFormatMode === "markdown" ? "markdown_v2" : "raw";
|
|
17
|
+
}
|
|
18
|
+
function buildScheduledTaskSuccessMessageParts(delivery) {
|
|
19
|
+
if (!delivery.resultText) {
|
|
20
|
+
return [delivery.notificationText];
|
|
17
21
|
}
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
parts.push(remaining.slice(0, splitIndex).trim());
|
|
26
|
-
remaining = remaining.slice(splitIndex).trimStart();
|
|
22
|
+
if (config.bot.messageFormatMode !== "markdown") {
|
|
23
|
+
return formatSummaryWithMode(`${delivery.notificationText}\n\n${delivery.resultText}`, config.bot.messageFormatMode);
|
|
24
|
+
}
|
|
25
|
+
const header = escapePlainTextForTelegramMarkdownV2(delivery.notificationText);
|
|
26
|
+
const resultParts = formatSummaryWithMode(delivery.resultText, config.bot.messageFormatMode);
|
|
27
|
+
if (resultParts.length === 0) {
|
|
28
|
+
return [header];
|
|
27
29
|
}
|
|
28
|
-
|
|
29
|
-
|
|
30
|
+
const firstPart = `${header}\n\n${resultParts[0]}`;
|
|
31
|
+
if (firstPart.length <= TELEGRAM_MESSAGE_LIMIT) {
|
|
32
|
+
return [firstPart, ...resultParts.slice(1)];
|
|
30
33
|
}
|
|
31
|
-
return
|
|
34
|
+
return [header, ...resultParts];
|
|
32
35
|
}
|
|
33
36
|
function normalizeTaskPrompt(prompt) {
|
|
34
37
|
const normalized = prompt.replace(/\s+/g, " ").trim();
|
|
@@ -44,10 +47,10 @@ function buildSuccessDelivery(task, runAt, resultText) {
|
|
|
44
47
|
prompt: task.prompt,
|
|
45
48
|
runAt,
|
|
46
49
|
status: "success",
|
|
47
|
-
|
|
50
|
+
notificationText: t("task.run.success", {
|
|
48
51
|
description: normalizeTaskPrompt(task.prompt),
|
|
49
|
-
result: resultText,
|
|
50
52
|
}),
|
|
53
|
+
resultText,
|
|
51
54
|
};
|
|
52
55
|
}
|
|
53
56
|
function buildErrorDelivery(task, runAt, errorMessage) {
|
|
@@ -57,7 +60,7 @@ function buildErrorDelivery(task, runAt, errorMessage) {
|
|
|
57
60
|
prompt: task.prompt,
|
|
58
61
|
runAt,
|
|
59
62
|
status: "error",
|
|
60
|
-
|
|
63
|
+
notificationText: t("task.run.error", {
|
|
61
64
|
description: normalizeTaskPrompt(task.prompt),
|
|
62
65
|
error: errorMessage,
|
|
63
66
|
}),
|
|
@@ -342,13 +345,16 @@ export class ScheduledTaskRuntime {
|
|
|
342
345
|
return false;
|
|
343
346
|
}
|
|
344
347
|
try {
|
|
345
|
-
const messageParts =
|
|
348
|
+
const messageParts = delivery.status === "success"
|
|
349
|
+
? buildScheduledTaskSuccessMessageParts(delivery)
|
|
350
|
+
: [delivery.notificationText];
|
|
351
|
+
const format = delivery.status === "success" ? getScheduledTaskDeliveryFormat() : "raw";
|
|
346
352
|
for (const part of messageParts) {
|
|
347
353
|
await sendBotText({
|
|
348
354
|
api: this.botApi,
|
|
349
355
|
chatId: this.chatId,
|
|
350
356
|
text: part,
|
|
351
|
-
format
|
|
357
|
+
format,
|
|
352
358
|
});
|
|
353
359
|
}
|
|
354
360
|
return true;
|
|
@@ -5,6 +5,7 @@ import { logger } from "../utils/logger.js";
|
|
|
5
5
|
import { t } from "../i18n/index.js";
|
|
6
6
|
import { getCurrentProject } from "../settings/manager.js";
|
|
7
7
|
const TELEGRAM_MESSAGE_LIMIT = 4096;
|
|
8
|
+
const MARKDOWN_V2_RESERVED_CHARS = /([_\*\[\]\(\)~`>#+\-=|{}.!\\])/g;
|
|
8
9
|
function splitText(text, maxLength) {
|
|
9
10
|
const parts = [];
|
|
10
11
|
let currentIndex = 0;
|
|
@@ -134,16 +135,48 @@ export function getAssistantParseMode() {
|
|
|
134
135
|
}
|
|
135
136
|
return undefined;
|
|
136
137
|
}
|
|
138
|
+
export function escapePlainTextForTelegramMarkdownV2(text) {
|
|
139
|
+
return text.replace(MARKDOWN_V2_RESERVED_CHARS, "\\$1");
|
|
140
|
+
}
|
|
137
141
|
function formatMarkdownForTelegram(text) {
|
|
138
142
|
try {
|
|
139
143
|
const preprocessed = preprocessMarkdownForTelegram(text);
|
|
140
|
-
return convert(preprocessed, "keep");
|
|
144
|
+
return escapeMarkdownV2PipesOutsideCode(convert(preprocessed, "keep"));
|
|
141
145
|
}
|
|
142
146
|
catch (error) {
|
|
143
147
|
logger.warn("[Formatter] Failed to convert markdown summary, falling back to raw text", error);
|
|
144
148
|
return text;
|
|
145
149
|
}
|
|
146
150
|
}
|
|
151
|
+
function escapeMarkdownV2PipesOutsideCode(text) {
|
|
152
|
+
let result = "";
|
|
153
|
+
let index = 0;
|
|
154
|
+
let inInlineCode = false;
|
|
155
|
+
let inCodeFence = false;
|
|
156
|
+
while (index < text.length) {
|
|
157
|
+
if (text.startsWith("```", index)) {
|
|
158
|
+
result += "```";
|
|
159
|
+
index += 3;
|
|
160
|
+
inCodeFence = !inCodeFence;
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
const char = text[index];
|
|
164
|
+
if (!inCodeFence && char === "`") {
|
|
165
|
+
inInlineCode = !inInlineCode;
|
|
166
|
+
result += char;
|
|
167
|
+
index += 1;
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
if (!inCodeFence && !inInlineCode && char === "|" && text[index - 1] !== "\\") {
|
|
171
|
+
result += "\\|";
|
|
172
|
+
index += 1;
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
result += char;
|
|
176
|
+
index += 1;
|
|
177
|
+
}
|
|
178
|
+
return result;
|
|
179
|
+
}
|
|
147
180
|
export function formatSummaryWithMode(text, mode, maxLength = TELEGRAM_MESSAGE_LIMIT) {
|
|
148
181
|
if (!text || text.trim().length === 0) {
|
|
149
182
|
return [];
|