@grinev/opencode-telegram-bot 0.1.0-rc.1
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 +34 -0
- package/LICENSE +21 -0
- package/README.md +72 -0
- package/dist/agent/manager.js +92 -0
- package/dist/agent/types.js +26 -0
- package/dist/app/start-bot-app.js +26 -0
- package/dist/bot/commands/agent.js +16 -0
- package/dist/bot/commands/definitions.js +20 -0
- package/dist/bot/commands/help.js +7 -0
- package/dist/bot/commands/model.js +16 -0
- package/dist/bot/commands/models.js +37 -0
- package/dist/bot/commands/new.js +58 -0
- package/dist/bot/commands/opencode-start.js +87 -0
- package/dist/bot/commands/opencode-stop.js +46 -0
- package/dist/bot/commands/projects.js +104 -0
- package/dist/bot/commands/server-restart.js +23 -0
- package/dist/bot/commands/server-start.js +23 -0
- package/dist/bot/commands/sessions.js +240 -0
- package/dist/bot/commands/start.js +40 -0
- package/dist/bot/commands/status.js +63 -0
- package/dist/bot/commands/stop.js +92 -0
- package/dist/bot/handlers/agent.js +96 -0
- package/dist/bot/handlers/context.js +112 -0
- package/dist/bot/handlers/model.js +115 -0
- package/dist/bot/handlers/permission.js +158 -0
- package/dist/bot/handlers/question.js +294 -0
- package/dist/bot/handlers/variant.js +126 -0
- package/dist/bot/index.js +573 -0
- package/dist/bot/middleware/auth.js +30 -0
- package/dist/bot/utils/keyboard.js +66 -0
- package/dist/cli/args.js +97 -0
- package/dist/cli.js +90 -0
- package/dist/config.js +46 -0
- package/dist/index.js +26 -0
- package/dist/keyboard/manager.js +171 -0
- package/dist/keyboard/types.js +1 -0
- package/dist/model/manager.js +123 -0
- package/dist/model/types.js +26 -0
- package/dist/opencode/client.js +13 -0
- package/dist/opencode/events.js +79 -0
- package/dist/opencode/server.js +104 -0
- package/dist/permission/manager.js +78 -0
- package/dist/permission/types.js +1 -0
- package/dist/pinned/manager.js +610 -0
- package/dist/pinned/types.js +1 -0
- package/dist/pinned-message/service.js +54 -0
- package/dist/process/manager.js +273 -0
- package/dist/process/types.js +1 -0
- package/dist/project/manager.js +28 -0
- package/dist/question/manager.js +143 -0
- package/dist/question/types.js +1 -0
- package/dist/runtime/bootstrap.js +278 -0
- package/dist/runtime/mode.js +74 -0
- package/dist/runtime/paths.js +37 -0
- package/dist/session/manager.js +10 -0
- package/dist/session/state.js +24 -0
- package/dist/settings/manager.js +99 -0
- package/dist/status/formatter.js +44 -0
- package/dist/summary/aggregator.js +427 -0
- package/dist/summary/formatter.js +226 -0
- package/dist/utils/formatting.js +237 -0
- package/dist/utils/logger.js +59 -0
- package/dist/utils/safe-background-task.js +33 -0
- package/dist/variant/manager.js +103 -0
- package/dist/variant/types.js +1 -0
- package/package.json +63 -0
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
import { InlineKeyboard } from "grammy";
|
|
2
|
+
import { questionManager } from "../../question/manager.js";
|
|
3
|
+
import { opencodeClient } from "../../opencode/client.js";
|
|
4
|
+
import { getCurrentProject } from "../../settings/manager.js";
|
|
5
|
+
import { summaryAggregator } from "../../summary/aggregator.js";
|
|
6
|
+
import { logger } from "../../utils/logger.js";
|
|
7
|
+
import { safeBackgroundTask } from "../../utils/safe-background-task.js";
|
|
8
|
+
const MAX_BUTTON_LENGTH = 60;
|
|
9
|
+
export async function handleQuestionCallback(ctx) {
|
|
10
|
+
const data = ctx.callbackQuery?.data;
|
|
11
|
+
if (!data)
|
|
12
|
+
return false;
|
|
13
|
+
if (!data.startsWith("question:")) {
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
logger.debug(`[QuestionHandler] Received callback: ${data}`);
|
|
17
|
+
if (!questionManager.isActive()) {
|
|
18
|
+
await ctx.answerCallbackQuery({ text: "Опрос неактивен", show_alert: true });
|
|
19
|
+
return true;
|
|
20
|
+
}
|
|
21
|
+
const parts = data.split(":");
|
|
22
|
+
const action = parts[1];
|
|
23
|
+
const questionIndex = parseInt(parts[2], 10);
|
|
24
|
+
try {
|
|
25
|
+
switch (action) {
|
|
26
|
+
case "select":
|
|
27
|
+
await handleSelectOption(ctx, questionIndex, parseInt(parts[3], 10));
|
|
28
|
+
break;
|
|
29
|
+
case "submit":
|
|
30
|
+
await handleSubmitAnswer(ctx, questionIndex);
|
|
31
|
+
break;
|
|
32
|
+
case "custom":
|
|
33
|
+
await handleCustomAnswer(ctx, questionIndex);
|
|
34
|
+
break;
|
|
35
|
+
case "cancel":
|
|
36
|
+
await handleCancelPoll(ctx);
|
|
37
|
+
break;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
catch (err) {
|
|
41
|
+
logger.error("[QuestionHandler] Error handling callback:", err);
|
|
42
|
+
await ctx.answerCallbackQuery({ text: "Ошибка при обработке", show_alert: true });
|
|
43
|
+
}
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
async function handleSelectOption(ctx, questionIndex, optionIndex) {
|
|
47
|
+
logger.debug(`[QuestionHandler] handleSelectOption: qIndex=${questionIndex}, oIndex=${optionIndex}`);
|
|
48
|
+
const question = questionManager.getCurrentQuestion();
|
|
49
|
+
if (!question) {
|
|
50
|
+
logger.debug("[QuestionHandler] No current question");
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
questionManager.selectOption(questionIndex, optionIndex);
|
|
54
|
+
if (question.multiple) {
|
|
55
|
+
logger.debug("[QuestionHandler] Multiple choice mode, updating message");
|
|
56
|
+
await updateQuestionMessage(ctx);
|
|
57
|
+
await ctx.answerCallbackQuery();
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
logger.debug("[QuestionHandler] Single choice mode, moving to next question");
|
|
61
|
+
await ctx.answerCallbackQuery();
|
|
62
|
+
const answer = questionManager.getSelectedAnswer(questionIndex);
|
|
63
|
+
logger.debug(`[QuestionHandler] Selected answer for question ${questionIndex}: ${answer}`);
|
|
64
|
+
// Удаляем сообщение с вопросом перед показом следующего
|
|
65
|
+
await ctx.deleteMessage().catch(() => { });
|
|
66
|
+
// НЕ отправляем ответ сразу - переходим к следующему вопросу
|
|
67
|
+
// Все ответы будут отправлены вместе когда пользователь ответит на все вопросы
|
|
68
|
+
await showNextQuestion(ctx);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
async function handleSubmitAnswer(ctx, questionIndex) {
|
|
72
|
+
const answer = questionManager.getSelectedAnswer(questionIndex);
|
|
73
|
+
if (!answer) {
|
|
74
|
+
await ctx.answerCallbackQuery({
|
|
75
|
+
text: "Выберите хотя бы один вариант",
|
|
76
|
+
show_alert: true,
|
|
77
|
+
});
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
logger.debug(`[QuestionHandler] Submit answer for question ${questionIndex}: ${answer}`);
|
|
81
|
+
await ctx.answerCallbackQuery();
|
|
82
|
+
// Удаляем сообщение с вопросом перед показом следующего
|
|
83
|
+
await ctx.deleteMessage().catch(() => { });
|
|
84
|
+
// НЕ отправляем ответ сразу - переходим к следующему вопросу
|
|
85
|
+
// Все ответы будут отправлены вместе когда пользователь ответит на все вопросы
|
|
86
|
+
await showNextQuestion(ctx);
|
|
87
|
+
}
|
|
88
|
+
async function handleCustomAnswer(ctx, _questionIndex) {
|
|
89
|
+
await ctx.answerCallbackQuery({
|
|
90
|
+
text: "Введите свой ответ сообщением",
|
|
91
|
+
show_alert: true,
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
async function handleCancelPoll(ctx) {
|
|
95
|
+
questionManager.cancel();
|
|
96
|
+
await ctx.editMessageText("❌ Опрос отменен");
|
|
97
|
+
await ctx.answerCallbackQuery();
|
|
98
|
+
}
|
|
99
|
+
async function updateQuestionMessage(ctx) {
|
|
100
|
+
const question = questionManager.getCurrentQuestion();
|
|
101
|
+
if (!question) {
|
|
102
|
+
logger.debug("[QuestionHandler] updateQuestionMessage: no current question");
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
const text = formatQuestionText(question);
|
|
106
|
+
const keyboard = buildQuestionKeyboard(question, questionManager.getSelectedOptions(questionManager.getCurrentIndex()));
|
|
107
|
+
logger.debug("[QuestionHandler] Updating question message");
|
|
108
|
+
try {
|
|
109
|
+
await ctx.editMessageText(text, {
|
|
110
|
+
reply_markup: keyboard,
|
|
111
|
+
parse_mode: "Markdown",
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
catch (err) {
|
|
115
|
+
logger.error("[QuestionHandler] Failed to update message:", err);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
export async function showCurrentQuestion(bot, chatId) {
|
|
119
|
+
const question = questionManager.getCurrentQuestion();
|
|
120
|
+
if (!question) {
|
|
121
|
+
await showPollSummary(bot, chatId);
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
logger.debug(`[QuestionHandler] Showing question: ${question.header} - ${question.question}`);
|
|
125
|
+
const text = formatQuestionText(question);
|
|
126
|
+
const keyboard = buildQuestionKeyboard(question, questionManager.getSelectedOptions(questionManager.getCurrentIndex()));
|
|
127
|
+
logger.debug(`[QuestionHandler] Sending message with keyboard, chatId=${chatId}`);
|
|
128
|
+
try {
|
|
129
|
+
const message = await bot.sendMessage(chatId, text, {
|
|
130
|
+
reply_markup: keyboard,
|
|
131
|
+
parse_mode: "Markdown",
|
|
132
|
+
});
|
|
133
|
+
logger.debug(`[QuestionHandler] Message sent, messageId=${message.message_id}`);
|
|
134
|
+
questionManager.addMessageId(message.message_id);
|
|
135
|
+
summaryAggregator.stopTypingIndicator();
|
|
136
|
+
}
|
|
137
|
+
catch (err) {
|
|
138
|
+
logger.error("[QuestionHandler] Failed to send question message:", err);
|
|
139
|
+
throw err;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
export async function handleQuestionTextAnswer(ctx) {
|
|
143
|
+
const text = ctx.message?.text;
|
|
144
|
+
if (!text)
|
|
145
|
+
return;
|
|
146
|
+
const currentIndex = questionManager.getCurrentIndex();
|
|
147
|
+
if (questionManager.hasCustomAnswer(currentIndex)) {
|
|
148
|
+
await ctx.reply("Ответ уже получен, подождите...");
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
logger.debug(`[QuestionHandler] Custom text answer for question ${currentIndex}: ${text}`);
|
|
152
|
+
questionManager.setCustomAnswer(currentIndex, text);
|
|
153
|
+
// Удаляем предыдущее сообщение с вопросом
|
|
154
|
+
const messageIds = questionManager.getMessageIds();
|
|
155
|
+
if (messageIds.length > 0 && ctx.chat) {
|
|
156
|
+
const lastMessageId = messageIds[messageIds.length - 1];
|
|
157
|
+
await ctx.api.deleteMessage(ctx.chat.id, lastMessageId).catch(() => { });
|
|
158
|
+
}
|
|
159
|
+
// НЕ отправляем ответ сразу - переходим к следующему вопросу
|
|
160
|
+
// Все ответы будут отправлены вместе когда пользователь ответит на все вопросы
|
|
161
|
+
await showNextQuestion(ctx);
|
|
162
|
+
}
|
|
163
|
+
async function showNextQuestion(ctx) {
|
|
164
|
+
questionManager.nextQuestion();
|
|
165
|
+
if (!ctx.chat) {
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
if (questionManager.hasNextQuestion()) {
|
|
169
|
+
await showCurrentQuestion(ctx.api, ctx.chat.id);
|
|
170
|
+
}
|
|
171
|
+
else {
|
|
172
|
+
await showPollSummary(ctx.api, ctx.chat.id);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
async function showPollSummary(bot, chatId) {
|
|
176
|
+
const answers = questionManager.getAllAnswers();
|
|
177
|
+
const totalQuestions = questionManager.getTotalQuestions();
|
|
178
|
+
logger.info(`[QuestionHandler] Poll completed: ${answers.length}/${totalQuestions} questions answered`);
|
|
179
|
+
// Отправляем все ответы в OpenCode API
|
|
180
|
+
await sendAllAnswersToAgent(bot, chatId);
|
|
181
|
+
if (answers.length === 0) {
|
|
182
|
+
await bot.sendMessage(chatId, "✅ Опрос завершен (без ответов)");
|
|
183
|
+
}
|
|
184
|
+
else {
|
|
185
|
+
const summary = formatAnswersSummary(answers);
|
|
186
|
+
await bot.sendMessage(chatId, summary);
|
|
187
|
+
}
|
|
188
|
+
questionManager.clear();
|
|
189
|
+
logger.debug("[QuestionHandler] Poll completed and cleared");
|
|
190
|
+
}
|
|
191
|
+
async function sendAllAnswersToAgent(bot, chatId) {
|
|
192
|
+
const currentProject = getCurrentProject();
|
|
193
|
+
const requestID = questionManager.getRequestID();
|
|
194
|
+
const totalQuestions = questionManager.getTotalQuestions();
|
|
195
|
+
if (!currentProject) {
|
|
196
|
+
logger.error("[QuestionHandler] No project for sending answers");
|
|
197
|
+
await bot.sendMessage(chatId, "❌ Нет активного проекта");
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
if (!requestID) {
|
|
201
|
+
logger.error("[QuestionHandler] No requestID for sending answers");
|
|
202
|
+
await bot.sendMessage(chatId, "❌ Нет активного запроса");
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
// Собираем ответы на все вопросы
|
|
206
|
+
// Формат: Array<Array<string>> - для каждого вопроса массив строк (выбранные опции)
|
|
207
|
+
const allAnswers = [];
|
|
208
|
+
for (let i = 0; i < totalQuestions; i++) {
|
|
209
|
+
const customAnswer = questionManager.getCustomAnswer(i);
|
|
210
|
+
const selectedAnswer = questionManager.getSelectedAnswer(i);
|
|
211
|
+
// Приоритет: custom answer > selected options
|
|
212
|
+
const answer = customAnswer || selectedAnswer || "";
|
|
213
|
+
if (answer) {
|
|
214
|
+
// Split by newlines if multiple options were selected (in multiple choice mode)
|
|
215
|
+
// Each option is formatted as "* Label: Description"
|
|
216
|
+
const answerParts = answer.split("\n").filter((part) => part.trim());
|
|
217
|
+
allAnswers.push(answerParts);
|
|
218
|
+
}
|
|
219
|
+
else {
|
|
220
|
+
// Empty answer for unanswered questions
|
|
221
|
+
allAnswers.push([]);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
logger.info(`[QuestionHandler] Sending all ${totalQuestions} answers to agent via question.reply: requestID=${requestID}`);
|
|
225
|
+
logger.debug(`[QuestionHandler] Answers payload:`, JSON.stringify(allAnswers, null, 2));
|
|
226
|
+
// КРИТИЧНО: Fire-and-forget! Не ждём завершения question.reply,
|
|
227
|
+
// иначе может заблокировать следующие updates
|
|
228
|
+
safeBackgroundTask({
|
|
229
|
+
taskName: "question.reply",
|
|
230
|
+
task: () => opencodeClient.question.reply({
|
|
231
|
+
requestID,
|
|
232
|
+
directory: currentProject.worktree,
|
|
233
|
+
answers: allAnswers,
|
|
234
|
+
}),
|
|
235
|
+
onSuccess: ({ error }) => {
|
|
236
|
+
if (error) {
|
|
237
|
+
logger.error("[QuestionHandler] Failed to send answers via question.reply:", error);
|
|
238
|
+
void bot.sendMessage(chatId, "❌ Не удалось отправить ответы агенту").catch(() => { });
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
logger.info("[QuestionHandler] All answers sent to agent successfully via question.reply");
|
|
242
|
+
},
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
function formatQuestionText(question) {
|
|
246
|
+
const currentIndex = questionManager.getCurrentIndex();
|
|
247
|
+
const totalQuestions = questionManager.getTotalQuestions();
|
|
248
|
+
const progressText = totalQuestions > 0 ? `${currentIndex + 1}/${totalQuestions}` : "";
|
|
249
|
+
const headerTitle = [progressText, question.header].filter(Boolean).join(" ");
|
|
250
|
+
const header = headerTitle ? `**${headerTitle}**\n\n` : "";
|
|
251
|
+
const multiple = question.multiple ? "\n*Можно выбрать несколько вариантов*" : "";
|
|
252
|
+
return `${header}${question.question}${multiple}`;
|
|
253
|
+
}
|
|
254
|
+
function buildQuestionKeyboard(question, selectedOptions) {
|
|
255
|
+
const keyboard = new InlineKeyboard();
|
|
256
|
+
const questionIndex = questionManager.getCurrentIndex();
|
|
257
|
+
logger.debug(`[QuestionHandler] Building keyboard for question ${questionIndex}`);
|
|
258
|
+
question.options.forEach((option, index) => {
|
|
259
|
+
const isSelected = selectedOptions.has(index);
|
|
260
|
+
const icon = isSelected ? "✅ " : "";
|
|
261
|
+
const buttonText = formatButtonText(option.label, option.description, icon);
|
|
262
|
+
const callbackData = `question:select:${questionIndex}:${index}`;
|
|
263
|
+
logger.debug(`[QuestionHandler] Button ${index}: "${buttonText}" -> "${callbackData}"`);
|
|
264
|
+
keyboard.text(buttonText, callbackData).row();
|
|
265
|
+
});
|
|
266
|
+
if (question.multiple) {
|
|
267
|
+
keyboard.text("✅ Готово", `question:submit:${questionIndex}`);
|
|
268
|
+
logger.debug(`[QuestionHandler] Added submit button`);
|
|
269
|
+
}
|
|
270
|
+
keyboard.text("🔤 Свой ответ", `question:custom:${questionIndex}`);
|
|
271
|
+
logger.debug(`[QuestionHandler] Added custom answer button`);
|
|
272
|
+
keyboard.text("❌ Отмена", `question:cancel:${questionIndex}`);
|
|
273
|
+
logger.debug(`[QuestionHandler] Added cancel button`);
|
|
274
|
+
logger.debug(`[QuestionHandler] Final keyboard: ${JSON.stringify(keyboard.inline_keyboard)}`);
|
|
275
|
+
return keyboard;
|
|
276
|
+
}
|
|
277
|
+
function formatButtonText(label, description, icon) {
|
|
278
|
+
let text = `${icon}${label}`;
|
|
279
|
+
if (description && icon === "") {
|
|
280
|
+
text += ` - ${description}`;
|
|
281
|
+
}
|
|
282
|
+
if (text.length > MAX_BUTTON_LENGTH) {
|
|
283
|
+
text = text.substring(0, MAX_BUTTON_LENGTH - 3) + "...";
|
|
284
|
+
}
|
|
285
|
+
return text;
|
|
286
|
+
}
|
|
287
|
+
function formatAnswersSummary(answers) {
|
|
288
|
+
let summary = "✅ Опрос завершен!\n\n";
|
|
289
|
+
answers.forEach((item, index) => {
|
|
290
|
+
summary += `Вопрос ${index + 1}:\n${item.question}\n\n`;
|
|
291
|
+
summary += `Ответ:\n${item.answer}\n\n`;
|
|
292
|
+
});
|
|
293
|
+
return summary;
|
|
294
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { InlineKeyboard } from "grammy";
|
|
2
|
+
import { getAvailableVariants, getCurrentVariant, setCurrentVariant, formatVariantForDisplay, formatVariantForButton, } from "../../variant/manager.js";
|
|
3
|
+
import { getStoredModel } from "../../model/manager.js";
|
|
4
|
+
import { getStoredAgent } from "../../agent/manager.js";
|
|
5
|
+
import { logger } from "../../utils/logger.js";
|
|
6
|
+
import { keyboardManager } from "../../keyboard/manager.js";
|
|
7
|
+
import { pinnedMessageManager } from "../../pinned/manager.js";
|
|
8
|
+
import { createMainKeyboard } from "../utils/keyboard.js";
|
|
9
|
+
/**
|
|
10
|
+
* Handle variant selection callback
|
|
11
|
+
* @param ctx grammY context
|
|
12
|
+
* @returns true if handled, false otherwise
|
|
13
|
+
*/
|
|
14
|
+
export async function handleVariantSelect(ctx) {
|
|
15
|
+
const callbackQuery = ctx.callbackQuery;
|
|
16
|
+
if (!callbackQuery?.data || !callbackQuery.data.startsWith("variant:")) {
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
logger.debug(`[VariantHandler] Received callback: ${callbackQuery.data}`);
|
|
20
|
+
try {
|
|
21
|
+
if (ctx.chat) {
|
|
22
|
+
keyboardManager.initialize(ctx.api, ctx.chat.id);
|
|
23
|
+
}
|
|
24
|
+
if (pinnedMessageManager.getContextLimit() === 0) {
|
|
25
|
+
await pinnedMessageManager.refreshContextLimit();
|
|
26
|
+
}
|
|
27
|
+
// Parse callback data: "variant:variantId"
|
|
28
|
+
const variantId = callbackQuery.data.replace("variant:", "");
|
|
29
|
+
// Get current model
|
|
30
|
+
const currentModel = getStoredModel();
|
|
31
|
+
if (!currentModel.providerID || !currentModel.modelID) {
|
|
32
|
+
logger.error("[VariantHandler] No model selected");
|
|
33
|
+
await ctx.answerCallbackQuery({ text: "Ошибка: модель не выбрана" });
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
// Set variant
|
|
37
|
+
setCurrentVariant(variantId);
|
|
38
|
+
// Re-read model after variant update
|
|
39
|
+
const updatedModel = getStoredModel();
|
|
40
|
+
// Update keyboard manager state
|
|
41
|
+
keyboardManager.updateModel(updatedModel);
|
|
42
|
+
keyboardManager.updateVariant(variantId);
|
|
43
|
+
// Build keyboard with correct context info
|
|
44
|
+
const currentAgent = getStoredAgent();
|
|
45
|
+
const contextInfo = pinnedMessageManager.getContextInfo() ??
|
|
46
|
+
(pinnedMessageManager.getContextLimit() > 0
|
|
47
|
+
? { tokensUsed: 0, tokensLimit: pinnedMessageManager.getContextLimit() }
|
|
48
|
+
: null);
|
|
49
|
+
if (contextInfo) {
|
|
50
|
+
keyboardManager.updateContext(contextInfo.tokensUsed, contextInfo.tokensLimit);
|
|
51
|
+
}
|
|
52
|
+
const variantName = formatVariantForButton(variantId);
|
|
53
|
+
const keyboard = createMainKeyboard(currentAgent, updatedModel, contextInfo ?? undefined, variantName);
|
|
54
|
+
// Send confirmation message with updated keyboard
|
|
55
|
+
const displayName = formatVariantForDisplay(variantId);
|
|
56
|
+
await ctx.answerCallbackQuery({ text: `Variant изменен: ${displayName}` });
|
|
57
|
+
await ctx.reply(`✅ Variant изменен на: ${displayName}`, {
|
|
58
|
+
reply_markup: keyboard,
|
|
59
|
+
});
|
|
60
|
+
// Delete the inline menu message
|
|
61
|
+
await ctx.deleteMessage().catch(() => { });
|
|
62
|
+
return true;
|
|
63
|
+
}
|
|
64
|
+
catch (err) {
|
|
65
|
+
logger.error("[VariantHandler] Error handling variant select:", err);
|
|
66
|
+
await ctx.answerCallbackQuery({ text: "Ошибка при смене variant" }).catch(() => { });
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Build inline keyboard with available variants
|
|
72
|
+
* @param currentVariant Current variant for highlighting
|
|
73
|
+
* @param providerID Provider ID
|
|
74
|
+
* @param modelID Model ID
|
|
75
|
+
* @returns InlineKeyboard with variant selection buttons
|
|
76
|
+
*/
|
|
77
|
+
export async function buildVariantSelectionMenu(currentVariant, providerID, modelID) {
|
|
78
|
+
const keyboard = new InlineKeyboard();
|
|
79
|
+
const variants = await getAvailableVariants(providerID, modelID);
|
|
80
|
+
if (variants.length === 0) {
|
|
81
|
+
logger.warn("[VariantHandler] No variants found");
|
|
82
|
+
return keyboard;
|
|
83
|
+
}
|
|
84
|
+
// Filter only active variants (not disabled)
|
|
85
|
+
const activeVariants = variants.filter((v) => !v.disabled);
|
|
86
|
+
if (activeVariants.length === 0) {
|
|
87
|
+
logger.warn("[VariantHandler] No active variants found");
|
|
88
|
+
// If no active variants, show default at least
|
|
89
|
+
keyboard.text("✅ Default", "variant:default").row();
|
|
90
|
+
return keyboard;
|
|
91
|
+
}
|
|
92
|
+
// Add button for each variant (one per row)
|
|
93
|
+
activeVariants.forEach((variant) => {
|
|
94
|
+
const isActive = variant.id === currentVariant;
|
|
95
|
+
const label = formatVariantForDisplay(variant.id);
|
|
96
|
+
const labelWithCheck = isActive ? `✅ ${label}` : label;
|
|
97
|
+
keyboard.text(labelWithCheck, `variant:${variant.id}`).row();
|
|
98
|
+
});
|
|
99
|
+
return keyboard;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Show variant selection menu
|
|
103
|
+
* @param ctx grammY context
|
|
104
|
+
*/
|
|
105
|
+
export async function showVariantSelectionMenu(ctx) {
|
|
106
|
+
try {
|
|
107
|
+
const currentModel = getStoredModel();
|
|
108
|
+
if (!currentModel.providerID || !currentModel.modelID) {
|
|
109
|
+
await ctx.reply("⚠️ Сначала выберите модель");
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
const currentVariant = getCurrentVariant();
|
|
113
|
+
const keyboard = await buildVariantSelectionMenu(currentVariant, currentModel.providerID, currentModel.modelID);
|
|
114
|
+
if (keyboard.inline_keyboard.length === 0) {
|
|
115
|
+
await ctx.reply("⚠️ Нет доступных вариантов");
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
const displayName = formatVariantForDisplay(currentVariant);
|
|
119
|
+
const text = `Текущий variant: ${displayName}\n\nВыберите variant:`;
|
|
120
|
+
await ctx.reply(text, { reply_markup: keyboard });
|
|
121
|
+
}
|
|
122
|
+
catch (err) {
|
|
123
|
+
logger.error("[VariantHandler] Error showing variant menu:", err);
|
|
124
|
+
await ctx.reply("🔴 Не удалось получить список вариантов");
|
|
125
|
+
}
|
|
126
|
+
}
|