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,30 @@
|
|
|
1
|
+
import { config } from "../../config.js";
|
|
2
|
+
import { logger } from "../../utils/logger.js";
|
|
3
|
+
export async function authMiddleware(ctx, next) {
|
|
4
|
+
const userId = ctx.from?.id;
|
|
5
|
+
logger.debug(`[Auth] Checking access: userId=${userId}, allowedUserId=${config.telegram.allowedUserId}, hasCallbackQuery=${!!ctx.callbackQuery}, hasMessage=${!!ctx.message}`);
|
|
6
|
+
if (userId && userId === config.telegram.allowedUserId) {
|
|
7
|
+
logger.debug(`[Auth] Access granted for userId=${userId}`);
|
|
8
|
+
await next();
|
|
9
|
+
}
|
|
10
|
+
else {
|
|
11
|
+
// Silently ignore unauthorized users
|
|
12
|
+
logger.warn(`Unauthorized access attempt from user ID: ${userId}`);
|
|
13
|
+
// Actively hide commands for unauthorized users by setting empty command list
|
|
14
|
+
// Only do this if the chat is NOT the authorized user's chat
|
|
15
|
+
// (to avoid resetting commands when forwarded messages are received)
|
|
16
|
+
if (ctx.chat?.id && ctx.chat.id !== config.telegram.allowedUserId) {
|
|
17
|
+
try {
|
|
18
|
+
// Set empty commands for this specific chat (more reliable than deleteMyCommands)
|
|
19
|
+
await ctx.api.setMyCommands([], {
|
|
20
|
+
scope: { type: "chat", chat_id: ctx.chat.id },
|
|
21
|
+
});
|
|
22
|
+
logger.debug(`[Auth] Set empty commands for unauthorized chat_id=${ctx.chat.id}`);
|
|
23
|
+
}
|
|
24
|
+
catch (err) {
|
|
25
|
+
// Ignore errors
|
|
26
|
+
logger.debug(`[Auth] Could not set empty commands: ${err}`);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { resolveInteractionGuardDecision } from "../../interaction/guard.js";
|
|
2
|
+
import { logger } from "../../utils/logger.js";
|
|
3
|
+
import { t } from "../../i18n/index.js";
|
|
4
|
+
function getInteractionBlockedMessage(reason, interactionKind) {
|
|
5
|
+
if (interactionKind === "permission") {
|
|
6
|
+
switch (reason) {
|
|
7
|
+
case "command_not_allowed":
|
|
8
|
+
return t("permission.blocked.command_not_allowed");
|
|
9
|
+
case "expected_callback":
|
|
10
|
+
case "expected_command":
|
|
11
|
+
case "expected_text":
|
|
12
|
+
default:
|
|
13
|
+
return t("permission.blocked.expected_reply");
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
if (interactionKind === "inline") {
|
|
17
|
+
switch (reason) {
|
|
18
|
+
case "command_not_allowed":
|
|
19
|
+
return t("inline.blocked.command_not_allowed");
|
|
20
|
+
case "expected_callback":
|
|
21
|
+
case "expected_command":
|
|
22
|
+
case "expected_text":
|
|
23
|
+
default:
|
|
24
|
+
return t("inline.blocked.expected_choice");
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
if (interactionKind === "question") {
|
|
28
|
+
switch (reason) {
|
|
29
|
+
case "command_not_allowed":
|
|
30
|
+
return t("question.blocked.command_not_allowed");
|
|
31
|
+
case "expected_callback":
|
|
32
|
+
case "expected_command":
|
|
33
|
+
case "expected_text":
|
|
34
|
+
default:
|
|
35
|
+
return t("question.blocked.expected_answer");
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
if (interactionKind === "rename") {
|
|
39
|
+
switch (reason) {
|
|
40
|
+
case "command_not_allowed":
|
|
41
|
+
return t("rename.blocked.command_not_allowed");
|
|
42
|
+
case "expected_callback":
|
|
43
|
+
case "expected_command":
|
|
44
|
+
case "expected_text":
|
|
45
|
+
default:
|
|
46
|
+
return t("rename.blocked.expected_name");
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
if (interactionKind === "task") {
|
|
50
|
+
switch (reason) {
|
|
51
|
+
case "command_not_allowed":
|
|
52
|
+
return t("task.blocked.command_not_allowed");
|
|
53
|
+
case "expected_callback":
|
|
54
|
+
case "expected_command":
|
|
55
|
+
case "expected_text":
|
|
56
|
+
default:
|
|
57
|
+
return t("task.blocked.expected_input");
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
switch (reason) {
|
|
61
|
+
case "expired":
|
|
62
|
+
return t("interaction.blocked.expired");
|
|
63
|
+
case "expected_callback":
|
|
64
|
+
return t("interaction.blocked.expected_callback");
|
|
65
|
+
case "expected_command":
|
|
66
|
+
return t("interaction.blocked.expected_command");
|
|
67
|
+
case "command_not_allowed":
|
|
68
|
+
return t("interaction.blocked.command_not_allowed");
|
|
69
|
+
case "expected_text":
|
|
70
|
+
default:
|
|
71
|
+
return t("interaction.blocked.expected_text");
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
export async function interactionGuardMiddleware(ctx, next) {
|
|
75
|
+
const decision = resolveInteractionGuardDecision(ctx);
|
|
76
|
+
if (decision.allow) {
|
|
77
|
+
await next();
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
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"}`);
|
|
86
|
+
if (ctx.callbackQuery) {
|
|
87
|
+
await ctx.answerCallbackQuery({ text: message }).catch(() => { });
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
if (ctx.chat) {
|
|
91
|
+
await ctx.reply(message).catch((err) => {
|
|
92
|
+
logger.error("[InteractionGuard] Failed to send blocked input message:", err);
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { extractCommandName, isKnownCommand } from "../utils/commands.js";
|
|
2
|
+
import { logger } from "../../utils/logger.js";
|
|
3
|
+
import { t } from "../../i18n/index.js";
|
|
4
|
+
export async function unknownCommandMiddleware(ctx, next) {
|
|
5
|
+
const text = ctx.message?.text;
|
|
6
|
+
if (!text) {
|
|
7
|
+
await next();
|
|
8
|
+
return;
|
|
9
|
+
}
|
|
10
|
+
const commandName = extractCommandName(text);
|
|
11
|
+
if (!commandName) {
|
|
12
|
+
await next();
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
if (isKnownCommand(commandName)) {
|
|
16
|
+
await next();
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
const commandToken = text.trim().split(/\s+/)[0];
|
|
20
|
+
logger.debug(`[Bot] Unknown slash command received: ${commandToken}`);
|
|
21
|
+
await ctx.reply(t("bot.unknown_command", { command: commandToken }));
|
|
22
|
+
}
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
import { logger } from "../../utils/logger.js";
|
|
2
|
+
function buildStateKey(sessionId, messageId) {
|
|
3
|
+
return `${sessionId}:${messageId}`;
|
|
4
|
+
}
|
|
5
|
+
function normalizePayload(payload) {
|
|
6
|
+
const normalizedParts = payload.parts
|
|
7
|
+
.map((part) => part.trim())
|
|
8
|
+
.filter((part) => part.length > 0);
|
|
9
|
+
if (normalizedParts.length === 0) {
|
|
10
|
+
return null;
|
|
11
|
+
}
|
|
12
|
+
return {
|
|
13
|
+
parts: normalizedParts,
|
|
14
|
+
format: payload.format,
|
|
15
|
+
sendOptions: payload.sendOptions,
|
|
16
|
+
editOptions: payload.editOptions,
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
function getErrorMessage(error) {
|
|
20
|
+
if (error instanceof Error) {
|
|
21
|
+
return error.message;
|
|
22
|
+
}
|
|
23
|
+
return String(error);
|
|
24
|
+
}
|
|
25
|
+
function getRetryAfterMs(error) {
|
|
26
|
+
const message = getErrorMessage(error);
|
|
27
|
+
if (!/\b429\b/.test(message)) {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
const retryMatch = message.match(/retry after\s+(\d+)/i);
|
|
31
|
+
if (!retryMatch) {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
const seconds = Number.parseInt(retryMatch[1], 10);
|
|
35
|
+
if (!Number.isFinite(seconds) || seconds <= 0) {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
return seconds * 1000;
|
|
39
|
+
}
|
|
40
|
+
function createSignature(text, format) {
|
|
41
|
+
return `${format}\n${text}`;
|
|
42
|
+
}
|
|
43
|
+
function delay(ms) {
|
|
44
|
+
return new Promise((resolve) => {
|
|
45
|
+
setTimeout(resolve, ms);
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
export class ResponseStreamer {
|
|
49
|
+
throttleMs;
|
|
50
|
+
sendText;
|
|
51
|
+
editText;
|
|
52
|
+
deleteText;
|
|
53
|
+
states = new Map();
|
|
54
|
+
constructor(options) {
|
|
55
|
+
this.throttleMs = Math.max(0, Math.floor(options.throttleMs));
|
|
56
|
+
this.sendText = options.sendText;
|
|
57
|
+
this.editText = options.editText;
|
|
58
|
+
this.deleteText = options.deleteText;
|
|
59
|
+
}
|
|
60
|
+
enqueue(sessionId, messageId, payload) {
|
|
61
|
+
const normalizedPayload = normalizePayload(payload);
|
|
62
|
+
if (!normalizedPayload) {
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
const state = this.getOrCreateState(sessionId, messageId);
|
|
66
|
+
state.latestPayload = normalizedPayload;
|
|
67
|
+
if (state.isBroken) {
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
this.ensureTimer(state);
|
|
71
|
+
}
|
|
72
|
+
async complete(sessionId, messageId, payload, options) {
|
|
73
|
+
const notStreamed = { streamed: false, telegramMessageIds: [] };
|
|
74
|
+
const state = this.states.get(buildStateKey(sessionId, messageId));
|
|
75
|
+
if (!state) {
|
|
76
|
+
return notStreamed;
|
|
77
|
+
}
|
|
78
|
+
if (payload) {
|
|
79
|
+
const normalizedPayload = normalizePayload(payload);
|
|
80
|
+
if (normalizedPayload) {
|
|
81
|
+
state.latestPayload = normalizedPayload;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
this.clearTimer(state);
|
|
85
|
+
await state.task.catch(() => false);
|
|
86
|
+
if (state.isBroken) {
|
|
87
|
+
await this.cleanupBrokenStream(state, "complete_broken_stream");
|
|
88
|
+
this.cancelState(state);
|
|
89
|
+
this.states.delete(state.key);
|
|
90
|
+
return notStreamed;
|
|
91
|
+
}
|
|
92
|
+
if (state.telegramMessageIds.length === 0) {
|
|
93
|
+
this.cancelState(state);
|
|
94
|
+
this.states.delete(state.key);
|
|
95
|
+
return notStreamed;
|
|
96
|
+
}
|
|
97
|
+
let synced = true;
|
|
98
|
+
if (options?.flushFinal !== false) {
|
|
99
|
+
synced = await this.enqueueTask(state, () => this.flushState(state, "complete"));
|
|
100
|
+
if (!synced && state.isBroken) {
|
|
101
|
+
await this.cleanupBrokenStream(state, "final_sync_failed_cleanup");
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
const messageIds = [...state.telegramMessageIds];
|
|
105
|
+
this.cancelState(state);
|
|
106
|
+
this.states.delete(state.key);
|
|
107
|
+
return { streamed: synced, telegramMessageIds: messageIds };
|
|
108
|
+
}
|
|
109
|
+
clearMessage(sessionId, messageId, reason) {
|
|
110
|
+
const key = buildStateKey(sessionId, messageId);
|
|
111
|
+
const state = this.states.get(key);
|
|
112
|
+
if (!state) {
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
this.cancelState(state);
|
|
116
|
+
this.states.delete(key);
|
|
117
|
+
logger.debug(`[ResponseStreamer] Cleared message stream: session=${sessionId}, message=${messageId}, reason=${reason}`);
|
|
118
|
+
}
|
|
119
|
+
clearSession(sessionId, reason) {
|
|
120
|
+
for (const state of Array.from(this.states.values())) {
|
|
121
|
+
if (state.sessionId !== sessionId) {
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
this.cancelState(state);
|
|
125
|
+
this.states.delete(state.key);
|
|
126
|
+
}
|
|
127
|
+
logger.debug(`[ResponseStreamer] Cleared session streams: session=${sessionId}, reason=${reason}`);
|
|
128
|
+
}
|
|
129
|
+
clearAll(reason) {
|
|
130
|
+
for (const state of this.states.values()) {
|
|
131
|
+
this.cancelState(state);
|
|
132
|
+
}
|
|
133
|
+
const count = this.states.size;
|
|
134
|
+
this.states.clear();
|
|
135
|
+
if (count > 0) {
|
|
136
|
+
logger.debug(`[ResponseStreamer] Cleared all streams: count=${count}, reason=${reason}`);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
getOrCreateState(sessionId, messageId) {
|
|
140
|
+
const key = buildStateKey(sessionId, messageId);
|
|
141
|
+
const existing = this.states.get(key);
|
|
142
|
+
if (existing) {
|
|
143
|
+
return existing;
|
|
144
|
+
}
|
|
145
|
+
const state = {
|
|
146
|
+
key,
|
|
147
|
+
sessionId,
|
|
148
|
+
messageId,
|
|
149
|
+
latestPayload: null,
|
|
150
|
+
lastSentSignatures: [],
|
|
151
|
+
telegramMessageIds: [],
|
|
152
|
+
timer: null,
|
|
153
|
+
task: Promise.resolve(true),
|
|
154
|
+
cancelled: false,
|
|
155
|
+
isBroken: false,
|
|
156
|
+
fatalErrorMessage: null,
|
|
157
|
+
fatalErrorLogged: false,
|
|
158
|
+
};
|
|
159
|
+
this.states.set(key, state);
|
|
160
|
+
return state;
|
|
161
|
+
}
|
|
162
|
+
ensureTimer(state) {
|
|
163
|
+
if (state.timer || state.cancelled) {
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
if (this.throttleMs === 0) {
|
|
167
|
+
void this.enqueueTask(state, () => this.flushState(state, "immediate")).catch((error) => {
|
|
168
|
+
logger.error(`[ResponseStreamer] Immediate stream sync failed: session=${state.sessionId}, message=${state.messageId}`, error);
|
|
169
|
+
});
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
state.timer = setTimeout(() => {
|
|
173
|
+
state.timer = null;
|
|
174
|
+
void this.enqueueTask(state, () => this.flushState(state, "throttle_elapsed")).catch((error) => {
|
|
175
|
+
logger.error(`[ResponseStreamer] Throttled stream sync failed: session=${state.sessionId}, message=${state.messageId}`, error);
|
|
176
|
+
});
|
|
177
|
+
}, this.throttleMs);
|
|
178
|
+
}
|
|
179
|
+
clearTimer(state) {
|
|
180
|
+
if (!state.timer) {
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
clearTimeout(state.timer);
|
|
184
|
+
state.timer = null;
|
|
185
|
+
}
|
|
186
|
+
cancelState(state) {
|
|
187
|
+
state.cancelled = true;
|
|
188
|
+
this.clearTimer(state);
|
|
189
|
+
}
|
|
190
|
+
enqueueTask(state, task) {
|
|
191
|
+
const nextTask = state.task.catch(() => false).then(task);
|
|
192
|
+
state.task = nextTask;
|
|
193
|
+
return nextTask;
|
|
194
|
+
}
|
|
195
|
+
async flushState(state, reason) {
|
|
196
|
+
if (state.cancelled) {
|
|
197
|
+
return false;
|
|
198
|
+
}
|
|
199
|
+
if (state.isBroken) {
|
|
200
|
+
return false;
|
|
201
|
+
}
|
|
202
|
+
while (!state.cancelled) {
|
|
203
|
+
const payload = state.latestPayload;
|
|
204
|
+
if (!payload) {
|
|
205
|
+
return state.telegramMessageIds.length > 0;
|
|
206
|
+
}
|
|
207
|
+
const targetSignatures = payload.parts.map((part) => createSignature(part, payload.format));
|
|
208
|
+
const unchanged = targetSignatures.length === state.lastSentSignatures.length &&
|
|
209
|
+
targetSignatures.every((signature, index) => signature === state.lastSentSignatures[index]);
|
|
210
|
+
if (unchanged) {
|
|
211
|
+
return state.telegramMessageIds.length > 0;
|
|
212
|
+
}
|
|
213
|
+
try {
|
|
214
|
+
await this.syncMessages(state, payload, targetSignatures);
|
|
215
|
+
logger.debug(`[ResponseStreamer] Stream synced: session=${state.sessionId}, message=${state.messageId}, reason=${reason}, parts=${payload.parts.length}`);
|
|
216
|
+
return true;
|
|
217
|
+
}
|
|
218
|
+
catch (error) {
|
|
219
|
+
const retryAfterMs = getRetryAfterMs(error);
|
|
220
|
+
if (retryAfterMs === null) {
|
|
221
|
+
this.markStreamBroken(state, error, reason);
|
|
222
|
+
return false;
|
|
223
|
+
}
|
|
224
|
+
const delayMs = Math.max(this.throttleMs, retryAfterMs);
|
|
225
|
+
logger.warn(`[ResponseStreamer] Stream sync rate-limited, retrying in ${delayMs}ms: session=${state.sessionId}, message=${state.messageId}, reason=${reason}`, error);
|
|
226
|
+
await delay(delayMs);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
return false;
|
|
230
|
+
}
|
|
231
|
+
markStreamBroken(state, error, reason) {
|
|
232
|
+
state.isBroken = true;
|
|
233
|
+
state.fatalErrorMessage = getErrorMessage(error);
|
|
234
|
+
if (state.fatalErrorLogged) {
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
state.fatalErrorLogged = true;
|
|
238
|
+
logger.error(`[ResponseStreamer] Stream marked as broken: session=${state.sessionId}, message=${state.messageId}, reason=${reason}, error=${state.fatalErrorMessage}`, error);
|
|
239
|
+
}
|
|
240
|
+
async cleanupBrokenStream(state, reason) {
|
|
241
|
+
if (state.telegramMessageIds.length === 0) {
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
for (let index = state.telegramMessageIds.length - 1; index >= 0; index--) {
|
|
245
|
+
const messageId = state.telegramMessageIds[index];
|
|
246
|
+
if (!messageId) {
|
|
247
|
+
continue;
|
|
248
|
+
}
|
|
249
|
+
try {
|
|
250
|
+
await this.deleteText(messageId);
|
|
251
|
+
}
|
|
252
|
+
catch (error) {
|
|
253
|
+
logger.warn(`[ResponseStreamer] Failed to delete broken stream message: session=${state.sessionId}, message=${state.messageId}, telegramMessageId=${messageId}, reason=${reason}`, error);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
state.telegramMessageIds = [];
|
|
257
|
+
state.lastSentSignatures = [];
|
|
258
|
+
logger.debug(`[ResponseStreamer] Cleaned up broken stream messages: session=${state.sessionId}, message=${state.messageId}, reason=${reason}`);
|
|
259
|
+
}
|
|
260
|
+
async syncMessages(state, payload, targetSignatures) {
|
|
261
|
+
for (let index = 0; index < payload.parts.length; index++) {
|
|
262
|
+
const text = payload.parts[index];
|
|
263
|
+
const nextSignature = targetSignatures[index];
|
|
264
|
+
const currentMessageId = state.telegramMessageIds[index];
|
|
265
|
+
if (currentMessageId) {
|
|
266
|
+
if (state.lastSentSignatures[index] === nextSignature) {
|
|
267
|
+
continue;
|
|
268
|
+
}
|
|
269
|
+
await this.editText(currentMessageId, text, payload.format, payload.editOptions);
|
|
270
|
+
state.lastSentSignatures[index] = nextSignature;
|
|
271
|
+
continue;
|
|
272
|
+
}
|
|
273
|
+
const messageId = await this.sendText(text, payload.format, payload.sendOptions);
|
|
274
|
+
state.telegramMessageIds[index] = messageId;
|
|
275
|
+
state.lastSentSignatures[index] = nextSignature;
|
|
276
|
+
}
|
|
277
|
+
for (let index = state.telegramMessageIds.length - 1; index >= payload.parts.length; index--) {
|
|
278
|
+
const messageId = state.telegramMessageIds[index];
|
|
279
|
+
if (messageId) {
|
|
280
|
+
await this.deleteText(messageId);
|
|
281
|
+
}
|
|
282
|
+
state.telegramMessageIds.pop();
|
|
283
|
+
state.lastSentSignatures.pop();
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|