@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,96 @@
|
|
|
1
|
+
import { InlineKeyboard } from "grammy";
|
|
2
|
+
import { selectAgent, getAvailableAgents, fetchCurrentAgent } from "../../agent/manager.js";
|
|
3
|
+
import { getAgentDisplayName, getAgentEmoji } from "../../agent/types.js";
|
|
4
|
+
import { getStoredModel } from "../../model/manager.js";
|
|
5
|
+
import { formatVariantForButton } from "../../variant/manager.js";
|
|
6
|
+
import { logger } from "../../utils/logger.js";
|
|
7
|
+
import { createMainKeyboard } from "../utils/keyboard.js";
|
|
8
|
+
import { pinnedMessageManager } from "../../pinned/manager.js";
|
|
9
|
+
import { keyboardManager } from "../../keyboard/manager.js";
|
|
10
|
+
/**
|
|
11
|
+
* Handle agent selection callback
|
|
12
|
+
* @param ctx grammY context
|
|
13
|
+
* @returns true if handled, false otherwise
|
|
14
|
+
*/
|
|
15
|
+
export async function handleAgentSelect(ctx) {
|
|
16
|
+
const callbackQuery = ctx.callbackQuery;
|
|
17
|
+
if (!callbackQuery?.data || !callbackQuery.data.startsWith("agent:")) {
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
logger.debug(`[AgentHandler] Received callback: ${callbackQuery.data}`);
|
|
21
|
+
try {
|
|
22
|
+
if (ctx.chat) {
|
|
23
|
+
keyboardManager.initialize(ctx.api, ctx.chat.id);
|
|
24
|
+
}
|
|
25
|
+
if (pinnedMessageManager.getContextLimit() === 0) {
|
|
26
|
+
await pinnedMessageManager.refreshContextLimit();
|
|
27
|
+
}
|
|
28
|
+
const agentName = callbackQuery.data.replace("agent:", "");
|
|
29
|
+
// Select agent and persist
|
|
30
|
+
selectAgent(agentName);
|
|
31
|
+
// Update keyboard manager state
|
|
32
|
+
keyboardManager.updateAgent(agentName);
|
|
33
|
+
// Update Reply Keyboard with new agent, current model, and context
|
|
34
|
+
const currentModel = getStoredModel();
|
|
35
|
+
const contextInfo = pinnedMessageManager.getContextInfo() ??
|
|
36
|
+
(pinnedMessageManager.getContextLimit() > 0
|
|
37
|
+
? { tokensUsed: 0, tokensLimit: pinnedMessageManager.getContextLimit() }
|
|
38
|
+
: null);
|
|
39
|
+
keyboardManager.updateModel(currentModel);
|
|
40
|
+
if (contextInfo) {
|
|
41
|
+
keyboardManager.updateContext(contextInfo.tokensUsed, contextInfo.tokensLimit);
|
|
42
|
+
}
|
|
43
|
+
const state = keyboardManager.getState();
|
|
44
|
+
const variantName = state?.variantName ?? formatVariantForButton(currentModel.variant || "default");
|
|
45
|
+
const keyboard = createMainKeyboard(agentName, currentModel, contextInfo ?? undefined, variantName);
|
|
46
|
+
const displayName = getAgentDisplayName(agentName);
|
|
47
|
+
// Send confirmation message with updated keyboard
|
|
48
|
+
await ctx.answerCallbackQuery({ text: `Режим изменен: ${displayName}` });
|
|
49
|
+
await ctx.reply(`✅ Режим изменен на: ${displayName}`, {
|
|
50
|
+
reply_markup: keyboard,
|
|
51
|
+
});
|
|
52
|
+
// Delete the inline menu message
|
|
53
|
+
await ctx.deleteMessage().catch(() => { });
|
|
54
|
+
return true;
|
|
55
|
+
}
|
|
56
|
+
catch (err) {
|
|
57
|
+
logger.error("[AgentHandler] Error handling agent select:", err);
|
|
58
|
+
await ctx.answerCallbackQuery({ text: "Ошибка при смене режима" }).catch(() => { });
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Build inline keyboard with available agents
|
|
64
|
+
* @param currentAgent Current agent name for highlighting
|
|
65
|
+
* @returns InlineKeyboard with agent selection buttons
|
|
66
|
+
*/
|
|
67
|
+
export async function buildAgentSelectionMenu(currentAgent) {
|
|
68
|
+
const keyboard = new InlineKeyboard();
|
|
69
|
+
const agents = await getAvailableAgents();
|
|
70
|
+
if (agents.length === 0) {
|
|
71
|
+
logger.warn("[AgentHandler] No available agents found");
|
|
72
|
+
return keyboard;
|
|
73
|
+
}
|
|
74
|
+
// Add button for each agent
|
|
75
|
+
agents.forEach((agent) => {
|
|
76
|
+
const emoji = getAgentEmoji(agent.name);
|
|
77
|
+
const isActive = agent.name === currentAgent;
|
|
78
|
+
const label = isActive
|
|
79
|
+
? `✅ ${emoji} ${agent.name.toUpperCase()}`
|
|
80
|
+
: `${emoji} ${agent.name.charAt(0).toUpperCase() + agent.name.slice(1)}`;
|
|
81
|
+
keyboard.text(label, `agent:${agent.name}`).row();
|
|
82
|
+
});
|
|
83
|
+
return keyboard;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Show agent selection menu
|
|
87
|
+
* @param ctx grammY context
|
|
88
|
+
*/
|
|
89
|
+
export async function showAgentSelectionMenu(ctx) {
|
|
90
|
+
const currentAgent = await fetchCurrentAgent();
|
|
91
|
+
const keyboard = await buildAgentSelectionMenu(currentAgent);
|
|
92
|
+
const text = currentAgent
|
|
93
|
+
? `Текущий режим: ${getAgentDisplayName(currentAgent)}\n\nВыберите режим:`
|
|
94
|
+
: "Выберите режим работы:";
|
|
95
|
+
await ctx.reply(text, { reply_markup: keyboard });
|
|
96
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { InlineKeyboard } from "grammy";
|
|
2
|
+
import { getCurrentSession } from "../../session/manager.js";
|
|
3
|
+
import { opencodeClient } from "../../opencode/client.js";
|
|
4
|
+
import { getStoredModel } from "../../model/manager.js";
|
|
5
|
+
import { logger } from "../../utils/logger.js";
|
|
6
|
+
/**
|
|
7
|
+
* Build inline keyboard with compact confirmation menu
|
|
8
|
+
* @returns InlineKeyboard with "Yes" and "Cancel" buttons
|
|
9
|
+
*/
|
|
10
|
+
export function buildCompactConfirmationMenu() {
|
|
11
|
+
const keyboard = new InlineKeyboard();
|
|
12
|
+
keyboard.text("✅ Да, сжать контекст", "compact:confirm").row();
|
|
13
|
+
keyboard.text("❌ Отмена", "compact:cancel").row();
|
|
14
|
+
return keyboard;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Handle context button press (text message from Reply Keyboard)
|
|
18
|
+
* Shows inline menu with compact confirmation
|
|
19
|
+
* @param ctx grammY context
|
|
20
|
+
*/
|
|
21
|
+
export async function handleContextButtonPress(ctx) {
|
|
22
|
+
logger.debug("[ContextHandler] Context button pressed");
|
|
23
|
+
const session = getCurrentSession();
|
|
24
|
+
if (!session) {
|
|
25
|
+
await ctx.reply("⚠️ Нет активной сессии. Создайте сессию командой /new");
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
const keyboard = buildCompactConfirmationMenu();
|
|
29
|
+
await ctx.reply(`📊 Сжатие контекста для сессии "${session.title}"\n\n` +
|
|
30
|
+
`Это уменьшит использование контекста, удалив старые сообщения из истории. ` +
|
|
31
|
+
`Текущая задача не будет прервана.\n\n` +
|
|
32
|
+
`Продолжить?`, { reply_markup: keyboard });
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Handle compact confirmation callback
|
|
36
|
+
* Calls OpenCode API to compact the session
|
|
37
|
+
* @param ctx grammY context
|
|
38
|
+
*/
|
|
39
|
+
export async function handleCompactConfirm(ctx) {
|
|
40
|
+
const callbackQuery = ctx.callbackQuery;
|
|
41
|
+
if (!callbackQuery?.data || callbackQuery.data !== "compact:confirm") {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
logger.debug("[ContextHandler] Compact confirmed");
|
|
45
|
+
try {
|
|
46
|
+
const session = getCurrentSession();
|
|
47
|
+
if (!session) {
|
|
48
|
+
await ctx.answerCallbackQuery({ text: "Сессия не найдена" });
|
|
49
|
+
await ctx.reply("⚠️ Нет активной сессии");
|
|
50
|
+
await ctx.deleteMessage().catch(() => { });
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
53
|
+
// Answer callback query and delete menu immediately
|
|
54
|
+
await ctx.answerCallbackQuery({ text: "Сжатие контекста..." });
|
|
55
|
+
await ctx.deleteMessage().catch(() => { });
|
|
56
|
+
// Send progress message
|
|
57
|
+
const progressMessage = await ctx.reply("⏳ Сжимаю контекст...");
|
|
58
|
+
// Show typing indicator
|
|
59
|
+
await ctx.api.sendChatAction(ctx.chat.id, "typing");
|
|
60
|
+
const storedModel = getStoredModel();
|
|
61
|
+
logger.debug(`[ContextHandler] Calling summarize with sessionID=${session.id}, directory=${session.directory}, model=${storedModel.providerID}/${storedModel.modelID}`);
|
|
62
|
+
// Call summarize API (AI compaction)
|
|
63
|
+
const { error } = await opencodeClient.session.summarize({
|
|
64
|
+
sessionID: session.id,
|
|
65
|
+
directory: session.directory,
|
|
66
|
+
providerID: storedModel.providerID,
|
|
67
|
+
modelID: storedModel.modelID,
|
|
68
|
+
});
|
|
69
|
+
if (error) {
|
|
70
|
+
logger.error("[ContextHandler] Compact failed:", error);
|
|
71
|
+
// Update progress message to show error
|
|
72
|
+
await ctx.api
|
|
73
|
+
.editMessageText(ctx.chat.id, progressMessage.message_id, "❌ Ошибка при сжатии контекста")
|
|
74
|
+
.catch(() => { });
|
|
75
|
+
return true;
|
|
76
|
+
}
|
|
77
|
+
logger.info(`[ContextHandler] Session compacted: ${session.id}`);
|
|
78
|
+
// Update progress message to show success
|
|
79
|
+
await ctx.api
|
|
80
|
+
.editMessageText(ctx.chat.id, progressMessage.message_id, "✅ Контекст успешно сжат")
|
|
81
|
+
.catch(() => { });
|
|
82
|
+
return true;
|
|
83
|
+
}
|
|
84
|
+
catch (err) {
|
|
85
|
+
logger.error("[ContextHandler] Compact exception:", err);
|
|
86
|
+
await ctx.answerCallbackQuery({ text: "Ошибка" }).catch(() => { });
|
|
87
|
+
await ctx.reply("❌ Ошибка при сжатии контекста");
|
|
88
|
+
await ctx.deleteMessage().catch(() => { });
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Handle compact cancel callback
|
|
94
|
+
* @param ctx grammY context
|
|
95
|
+
*/
|
|
96
|
+
export async function handleCompactCancel(ctx) {
|
|
97
|
+
const callbackQuery = ctx.callbackQuery;
|
|
98
|
+
if (!callbackQuery?.data || callbackQuery.data !== "compact:cancel") {
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
logger.debug("[ContextHandler] Compact cancelled");
|
|
102
|
+
try {
|
|
103
|
+
await ctx.answerCallbackQuery({ text: "Отменено" });
|
|
104
|
+
// Delete the inline menu message
|
|
105
|
+
await ctx.deleteMessage().catch(() => { });
|
|
106
|
+
return true;
|
|
107
|
+
}
|
|
108
|
+
catch (err) {
|
|
109
|
+
logger.error("[ContextHandler] Cancel error:", err);
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { InlineKeyboard } from "grammy";
|
|
2
|
+
import { selectModel, getFavoriteModels, fetchCurrentModel } from "../../model/manager.js";
|
|
3
|
+
import { formatModelForDisplay } from "../../model/types.js";
|
|
4
|
+
import { formatVariantForButton } from "../../variant/manager.js";
|
|
5
|
+
import { logger } from "../../utils/logger.js";
|
|
6
|
+
import { createMainKeyboard } from "../utils/keyboard.js";
|
|
7
|
+
import { getStoredAgent } from "../../agent/manager.js";
|
|
8
|
+
import { pinnedMessageManager } from "../../pinned/manager.js";
|
|
9
|
+
import { keyboardManager } from "../../keyboard/manager.js";
|
|
10
|
+
/**
|
|
11
|
+
* Handle model selection callback
|
|
12
|
+
* @param ctx grammY context
|
|
13
|
+
* @returns true if handled, false otherwise
|
|
14
|
+
*/
|
|
15
|
+
export async function handleModelSelect(ctx) {
|
|
16
|
+
const callbackQuery = ctx.callbackQuery;
|
|
17
|
+
if (!callbackQuery?.data || !callbackQuery.data.startsWith("model:")) {
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
logger.debug(`[ModelHandler] Received callback: ${callbackQuery.data}`);
|
|
21
|
+
try {
|
|
22
|
+
if (ctx.chat) {
|
|
23
|
+
keyboardManager.initialize(ctx.api, ctx.chat.id);
|
|
24
|
+
}
|
|
25
|
+
// Parse callback data: "model:providerID:modelID"
|
|
26
|
+
const parts = callbackQuery.data.split(":");
|
|
27
|
+
if (parts.length < 3) {
|
|
28
|
+
logger.error(`[ModelHandler] Invalid callback data format: ${callbackQuery.data}`);
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
const providerID = parts[1];
|
|
32
|
+
const modelID = parts.slice(2).join(":"); // Handle model IDs that may contain ":"
|
|
33
|
+
const modelInfo = {
|
|
34
|
+
providerID,
|
|
35
|
+
modelID,
|
|
36
|
+
variant: "default", // Reset to default when switching models
|
|
37
|
+
};
|
|
38
|
+
// Select model and persist
|
|
39
|
+
selectModel(modelInfo);
|
|
40
|
+
// Update keyboard manager state (may not be initialized if no session selected)
|
|
41
|
+
keyboardManager.updateModel(modelInfo);
|
|
42
|
+
// Refresh context limit for new model
|
|
43
|
+
await pinnedMessageManager.refreshContextLimit();
|
|
44
|
+
// Update Reply Keyboard with new model and context
|
|
45
|
+
const currentAgent = getStoredAgent();
|
|
46
|
+
const contextInfo = pinnedMessageManager.getContextInfo() ??
|
|
47
|
+
(pinnedMessageManager.getContextLimit() > 0
|
|
48
|
+
? { tokensUsed: 0, tokensLimit: pinnedMessageManager.getContextLimit() }
|
|
49
|
+
: null);
|
|
50
|
+
if (contextInfo) {
|
|
51
|
+
keyboardManager.updateContext(contextInfo.tokensUsed, contextInfo.tokensLimit);
|
|
52
|
+
}
|
|
53
|
+
const variantName = formatVariantForButton(modelInfo.variant || "default");
|
|
54
|
+
const keyboard = createMainKeyboard(currentAgent, modelInfo, contextInfo ?? undefined, variantName);
|
|
55
|
+
const displayName = formatModelForDisplay(modelInfo.providerID, modelInfo.modelID);
|
|
56
|
+
// Send confirmation message with updated keyboard
|
|
57
|
+
await ctx.answerCallbackQuery({ text: `Модель изменена: ${displayName}` });
|
|
58
|
+
await ctx.reply(`✅ Модель изменена на: ${displayName}`, {
|
|
59
|
+
reply_markup: keyboard,
|
|
60
|
+
});
|
|
61
|
+
// Delete the inline menu message
|
|
62
|
+
await ctx.deleteMessage().catch(() => { });
|
|
63
|
+
return true;
|
|
64
|
+
}
|
|
65
|
+
catch (err) {
|
|
66
|
+
logger.error("[ModelHandler] Error handling model select:", err);
|
|
67
|
+
await ctx.answerCallbackQuery({ text: "Ошибка при смене модели" }).catch(() => { });
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Build inline keyboard with favorite models
|
|
73
|
+
* @param currentModel Current model for highlighting
|
|
74
|
+
* @returns InlineKeyboard with model selection buttons
|
|
75
|
+
*/
|
|
76
|
+
export async function buildModelSelectionMenu(currentModel) {
|
|
77
|
+
const keyboard = new InlineKeyboard();
|
|
78
|
+
const favorites = await getFavoriteModels();
|
|
79
|
+
if (favorites.length === 0) {
|
|
80
|
+
logger.warn("[ModelHandler] No favorite models found");
|
|
81
|
+
return keyboard;
|
|
82
|
+
}
|
|
83
|
+
// Add button for each favorite model
|
|
84
|
+
favorites.forEach((model) => {
|
|
85
|
+
const isActive = currentModel &&
|
|
86
|
+
model.providerID === currentModel.providerID &&
|
|
87
|
+
model.modelID === currentModel.modelID;
|
|
88
|
+
// Inline buttons use full model ID without truncation
|
|
89
|
+
const label = `${model.providerID}/${model.modelID}`;
|
|
90
|
+
const labelWithCheck = isActive ? `✅ ${label}` : label;
|
|
91
|
+
keyboard.text(labelWithCheck, `model:${model.providerID}:${model.modelID}`).row();
|
|
92
|
+
});
|
|
93
|
+
return keyboard;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Show model selection menu
|
|
97
|
+
* @param ctx grammY context
|
|
98
|
+
*/
|
|
99
|
+
export async function showModelSelectionMenu(ctx) {
|
|
100
|
+
try {
|
|
101
|
+
const currentModel = fetchCurrentModel();
|
|
102
|
+
const keyboard = await buildModelSelectionMenu(currentModel);
|
|
103
|
+
if (keyboard.inline_keyboard.length === 0) {
|
|
104
|
+
await ctx.reply("⚠️ Нет доступных моделей");
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
const displayName = formatModelForDisplay(currentModel.providerID, currentModel.modelID);
|
|
108
|
+
const text = `Текущая модель: ${displayName}\n\nВыберите модель:`;
|
|
109
|
+
await ctx.reply(text, { reply_markup: keyboard });
|
|
110
|
+
}
|
|
111
|
+
catch (err) {
|
|
112
|
+
logger.error("[ModelHandler] Error showing model menu:", err);
|
|
113
|
+
await ctx.reply("🔴 Не удалось получить список моделей");
|
|
114
|
+
}
|
|
115
|
+
}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { InlineKeyboard } from "grammy";
|
|
2
|
+
import { permissionManager } from "../../permission/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
|
+
// Permission type display names
|
|
9
|
+
const PERMISSION_NAMES = {
|
|
10
|
+
bash: "Bash",
|
|
11
|
+
edit: "Edit",
|
|
12
|
+
write: "Write",
|
|
13
|
+
read: "Read",
|
|
14
|
+
webfetch: "Web Fetch",
|
|
15
|
+
websearch: "Web Search",
|
|
16
|
+
glob: "File Search",
|
|
17
|
+
grep: "Content Search",
|
|
18
|
+
list: "List Directory",
|
|
19
|
+
task: "Task",
|
|
20
|
+
lsp: "LSP",
|
|
21
|
+
};
|
|
22
|
+
// Permission type emojis
|
|
23
|
+
const PERMISSION_EMOJIS = {
|
|
24
|
+
bash: "⚡",
|
|
25
|
+
edit: "✏️",
|
|
26
|
+
write: "📝",
|
|
27
|
+
read: "📖",
|
|
28
|
+
webfetch: "🌐",
|
|
29
|
+
websearch: "🔍",
|
|
30
|
+
glob: "📁",
|
|
31
|
+
grep: "🔎",
|
|
32
|
+
list: "📂",
|
|
33
|
+
task: "⚙️",
|
|
34
|
+
lsp: "🔧",
|
|
35
|
+
};
|
|
36
|
+
/**
|
|
37
|
+
* Handle permission callback from inline buttons
|
|
38
|
+
*/
|
|
39
|
+
export async function handlePermissionCallback(ctx) {
|
|
40
|
+
const data = ctx.callbackQuery?.data;
|
|
41
|
+
if (!data)
|
|
42
|
+
return false;
|
|
43
|
+
if (!data.startsWith("permission:")) {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
logger.debug(`[PermissionHandler] Received callback: ${data}`);
|
|
47
|
+
if (!permissionManager.isActive()) {
|
|
48
|
+
await ctx.answerCallbackQuery({ text: "Запрос разрешения неактивен", show_alert: true });
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
const parts = data.split(":");
|
|
52
|
+
const action = parts[1];
|
|
53
|
+
try {
|
|
54
|
+
await handlePermissionReply(ctx, action);
|
|
55
|
+
}
|
|
56
|
+
catch (err) {
|
|
57
|
+
logger.error("[PermissionHandler] Error handling callback:", err);
|
|
58
|
+
await ctx.answerCallbackQuery({ text: "Ошибка при обработке", show_alert: true });
|
|
59
|
+
}
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Handle permission reply (once/always/reject)
|
|
64
|
+
*/
|
|
65
|
+
async function handlePermissionReply(ctx, reply) {
|
|
66
|
+
const requestID = permissionManager.getRequestID();
|
|
67
|
+
const currentProject = getCurrentProject();
|
|
68
|
+
const chatId = ctx.chat?.id;
|
|
69
|
+
if (!requestID || !currentProject || !chatId) {
|
|
70
|
+
await ctx.answerCallbackQuery({ text: "Ошибка: нет активного запроса", show_alert: true });
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
// Reply labels for user feedback
|
|
74
|
+
const replyLabels = {
|
|
75
|
+
once: "Разрешено однократно",
|
|
76
|
+
always: "Разрешено всегда",
|
|
77
|
+
reject: "Отклонено",
|
|
78
|
+
};
|
|
79
|
+
await ctx.answerCallbackQuery({ text: replyLabels[reply] });
|
|
80
|
+
// Delete the permission message
|
|
81
|
+
await ctx.deleteMessage().catch(() => { });
|
|
82
|
+
// Stop typing indicator since we're responding
|
|
83
|
+
summaryAggregator.stopTypingIndicator();
|
|
84
|
+
logger.info(`[PermissionHandler] Sending permission reply: ${reply}, requestID=${requestID}`);
|
|
85
|
+
// CRITICAL: Fire-and-forget! Do not block the handler
|
|
86
|
+
safeBackgroundTask({
|
|
87
|
+
taskName: "permission.reply",
|
|
88
|
+
task: () => opencodeClient.permission.reply({
|
|
89
|
+
requestID,
|
|
90
|
+
directory: currentProject.worktree,
|
|
91
|
+
reply,
|
|
92
|
+
}),
|
|
93
|
+
onSuccess: ({ error }) => {
|
|
94
|
+
if (error) {
|
|
95
|
+
logger.error("[PermissionHandler] Failed to send permission reply:", error);
|
|
96
|
+
if (ctx.api && chatId) {
|
|
97
|
+
void ctx.api
|
|
98
|
+
.sendMessage(chatId, "❌ Не удалось отправить ответ на запрос разрешения")
|
|
99
|
+
.catch(() => { });
|
|
100
|
+
}
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
logger.info("[PermissionHandler] Permission reply sent successfully");
|
|
104
|
+
},
|
|
105
|
+
});
|
|
106
|
+
permissionManager.clear();
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Show permission request message with inline buttons
|
|
110
|
+
*/
|
|
111
|
+
export async function showPermissionRequest(bot, chatId, request) {
|
|
112
|
+
logger.debug(`[PermissionHandler] Showing permission request: ${request.permission}`);
|
|
113
|
+
permissionManager.startPermission(request);
|
|
114
|
+
const text = formatPermissionText(request);
|
|
115
|
+
const keyboard = buildPermissionKeyboard();
|
|
116
|
+
try {
|
|
117
|
+
const message = await bot.sendMessage(chatId, text, {
|
|
118
|
+
reply_markup: keyboard,
|
|
119
|
+
parse_mode: "Markdown",
|
|
120
|
+
});
|
|
121
|
+
logger.debug(`[PermissionHandler] Message sent, messageId=${message.message_id}`);
|
|
122
|
+
permissionManager.setMessageId(message.message_id);
|
|
123
|
+
summaryAggregator.stopTypingIndicator();
|
|
124
|
+
}
|
|
125
|
+
catch (err) {
|
|
126
|
+
logger.error("[PermissionHandler] Failed to send permission message:", err);
|
|
127
|
+
throw err;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Format permission request text
|
|
132
|
+
*/
|
|
133
|
+
function formatPermissionText(request) {
|
|
134
|
+
const emoji = PERMISSION_EMOJIS[request.permission] || "🔐";
|
|
135
|
+
const name = PERMISSION_NAMES[request.permission] || request.permission;
|
|
136
|
+
let text = `${emoji} **Запрос разрешения: ${name}**\n\n`;
|
|
137
|
+
// Show patterns (commands/files)
|
|
138
|
+
if (request.patterns.length > 0) {
|
|
139
|
+
request.patterns.forEach((pattern) => {
|
|
140
|
+
// Escape backticks for Markdown code
|
|
141
|
+
const escapedPattern = pattern.replace(/`/g, "\\`");
|
|
142
|
+
text += `\`${escapedPattern}\`\n`;
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
return text;
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Build inline keyboard with permission buttons
|
|
149
|
+
*/
|
|
150
|
+
function buildPermissionKeyboard() {
|
|
151
|
+
const keyboard = new InlineKeyboard();
|
|
152
|
+
// Single row with all 3 buttons
|
|
153
|
+
keyboard
|
|
154
|
+
.text("✅ Разрешить", "permission:once")
|
|
155
|
+
.text("🔓 Всегда", "permission:always")
|
|
156
|
+
.text("❌ Отклонить", "permission:reject");
|
|
157
|
+
return keyboard;
|
|
158
|
+
}
|