@grinev/opencode-telegram-bot 0.9.0 → 0.9.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/README.md +4 -6
- package/dist/bot/commands/definitions.js +0 -2
- package/dist/bot/commands/help.js +1 -1
- package/dist/bot/commands/projects.js +13 -3
- package/dist/bot/commands/status.js +12 -1
- package/dist/bot/index.js +16 -4
- package/dist/bot/utils/send-with-markdown-fallback.js +1 -1
- package/dist/i18n/de.js +3 -3
- package/dist/i18n/en.js +3 -3
- package/dist/i18n/es.js +3 -3
- package/dist/i18n/ru.js +7 -7
- package/dist/i18n/zh.js +3 -3
- package/dist/summary/aggregator.js +19 -1
- package/dist/summary/tool-message-batcher.js +32 -15
- package/package.json +1 -1
- package/dist/bot/commands/agent.js +0 -17
- package/dist/bot/commands/model.js +0 -17
package/README.md
CHANGED
|
@@ -32,7 +32,7 @@ Quick start: `npx @grinev/opencode-telegram-bot`
|
|
|
32
32
|
- **Input flow control** — when an interactive flow is active, the bot accepts only relevant input to keep context consistent and avoid accidental actions
|
|
33
33
|
- **Configurable reply formatting** — assistant replies use Telegram MarkdownV2 by default, with optional raw mode (`MESSAGE_FORMAT_MODE=markdown|raw`)
|
|
34
34
|
- **Security** — strict user ID whitelist; no one else can access your bot, even if they find it
|
|
35
|
-
- **Localization** —
|
|
35
|
+
- **Localization** — UI localization is supported for multiple languages (`BOT_LOCALE`)
|
|
36
36
|
|
|
37
37
|
## Prerequisites
|
|
38
38
|
|
|
@@ -52,7 +52,7 @@ You'll also need your **Telegram User ID** — send any message to [@userinfobot
|
|
|
52
52
|
|
|
53
53
|
### 2. Start OpenCode Server
|
|
54
54
|
|
|
55
|
-
|
|
55
|
+
Start the OpenCode server:
|
|
56
56
|
|
|
57
57
|
```bash
|
|
58
58
|
opencode serve
|
|
@@ -100,8 +100,6 @@ opencode-telegram config
|
|
|
100
100
|
| `/stop` | Abort the current task |
|
|
101
101
|
| `/sessions` | Browse and switch between recent sessions |
|
|
102
102
|
| `/projects` | Switch between OpenCode projects |
|
|
103
|
-
| `/model` | Choose a model from your favorites |
|
|
104
|
-
| `/agent` | Switch agent mode (Plan / Build) |
|
|
105
103
|
| `/rename` | Rename the current session |
|
|
106
104
|
| `/opencode_start` | Start the OpenCode server remotely |
|
|
107
105
|
| `/opencode_stop` | Stop the OpenCode server remotely |
|
|
@@ -184,7 +182,7 @@ The bot picks up your **favorite models** from OpenCode. To add a model to favor
|
|
|
184
182
|
2. Go to model selection
|
|
185
183
|
3. Hover over the model you want and press **Ctrl+F** to add it to favorites
|
|
186
184
|
|
|
187
|
-
These favorites will appear in the
|
|
185
|
+
These favorites will appear in the model picker opened from the bottom Telegram keyboard.
|
|
188
186
|
|
|
189
187
|
A free model (`opencode/big-pickle`) is configured as the default fallback — if you haven't set up any favorites yet, the bot will use it automatically.
|
|
190
188
|
|
|
@@ -239,7 +237,7 @@ npm run dev
|
|
|
239
237
|
- Ensure `opencode serve` is running in your project directory
|
|
240
238
|
- Check that `OPENCODE_API_URL` points to the correct address (default: `http://localhost:4096`)
|
|
241
239
|
|
|
242
|
-
**No models in
|
|
240
|
+
**No models in model picker**
|
|
243
241
|
|
|
244
242
|
- Add models to your OpenCode favorites: open OpenCode TUI, go to model selection, press **Ctrl+F** on desired models
|
|
245
243
|
|
|
@@ -9,8 +9,6 @@ const COMMAND_DEFINITIONS = [
|
|
|
9
9
|
{ command: "stop", descriptionKey: "cmd.description.stop" },
|
|
10
10
|
{ command: "sessions", descriptionKey: "cmd.description.sessions" },
|
|
11
11
|
{ command: "projects", descriptionKey: "cmd.description.projects" },
|
|
12
|
-
{ command: "model", descriptionKey: "cmd.description.model" },
|
|
13
|
-
{ command: "agent", descriptionKey: "cmd.description.agent" },
|
|
14
12
|
{ command: "rename", descriptionKey: "cmd.description.rename" },
|
|
15
13
|
{ command: "opencode_start", descriptionKey: "cmd.description.opencode_start" },
|
|
16
14
|
{ command: "opencode_stop", descriptionKey: "cmd.description.opencode_stop" },
|
|
@@ -3,7 +3,7 @@ import { getLocalizedBotCommands } from "./definitions.js";
|
|
|
3
3
|
function formatHelpText() {
|
|
4
4
|
const commands = getLocalizedBotCommands();
|
|
5
5
|
const lines = commands.map((item) => `/${item.command} - ${item.description}`);
|
|
6
|
-
return `📖 ${t("cmd.description.help")}\n\n${lines.join("\n")}`;
|
|
6
|
+
return `📖 ${t("cmd.description.help")}\n\n${lines.join("\n")}\n\n${t("help.keyboard_hint")}`;
|
|
7
7
|
}
|
|
8
8
|
export async function helpCommand(ctx) {
|
|
9
9
|
await ctx.reply(formatHelpText());
|
|
@@ -24,6 +24,18 @@ function formatProjectButtonLabel(label, isActive) {
|
|
|
24
24
|
}
|
|
25
25
|
return `${prefix}${label.slice(0, Math.max(0, availableLength - 3))}...`;
|
|
26
26
|
}
|
|
27
|
+
export function getProjectFolderName(worktree) {
|
|
28
|
+
const normalized = worktree.replace(/[\\/]+$/g, "");
|
|
29
|
+
if (!normalized) {
|
|
30
|
+
return worktree;
|
|
31
|
+
}
|
|
32
|
+
const segments = normalized.split(/[\\/]/).filter(Boolean);
|
|
33
|
+
return segments.at(-1) ?? normalized;
|
|
34
|
+
}
|
|
35
|
+
export function buildProjectButtonLabel(index, worktree) {
|
|
36
|
+
const folderName = getProjectFolderName(worktree);
|
|
37
|
+
return `${index + 1}. ${folderName} [${worktree}]`;
|
|
38
|
+
}
|
|
27
39
|
export async function projectsCommand(ctx) {
|
|
28
40
|
try {
|
|
29
41
|
await syncSessionDirectoryCache();
|
|
@@ -38,9 +50,7 @@ export async function projectsCommand(ctx) {
|
|
|
38
50
|
projectsToShow.forEach((project, index) => {
|
|
39
51
|
const isActive = currentProject &&
|
|
40
52
|
(project.id === currentProject.id || project.worktree === currentProject.worktree);
|
|
41
|
-
const label = project.
|
|
42
|
-
? `${index + 1}. ${project.name}`
|
|
43
|
-
: `${index + 1}. ${project.worktree}`;
|
|
53
|
+
const label = buildProjectButtonLabel(index, project.worktree);
|
|
44
54
|
const labelWithCheck = formatProjectButtonLabel(label, Boolean(isActive));
|
|
45
55
|
keyboard.text(labelWithCheck, `project:${project.id}`).row();
|
|
46
56
|
});
|
|
@@ -8,6 +8,7 @@ import { formatModelForDisplay } from "../../model/types.js";
|
|
|
8
8
|
import { processManager } from "../../process/manager.js";
|
|
9
9
|
import { logger } from "../../utils/logger.js";
|
|
10
10
|
import { t } from "../../i18n/index.js";
|
|
11
|
+
import { sendMessageWithMarkdownFallback } from "../utils/send-with-markdown-fallback.js";
|
|
11
12
|
export async function statusCommand(ctx) {
|
|
12
13
|
try {
|
|
13
14
|
const { data, error } = await opencodeClient.global.health();
|
|
@@ -58,7 +59,17 @@ export async function statusCommand(ctx) {
|
|
|
58
59
|
message += `\n${t("status.session_not_selected")}\n`;
|
|
59
60
|
message += t("status.session_hint");
|
|
60
61
|
}
|
|
61
|
-
|
|
62
|
+
if (ctx.chat) {
|
|
63
|
+
await sendMessageWithMarkdownFallback({
|
|
64
|
+
api: ctx.api,
|
|
65
|
+
chatId: ctx.chat.id,
|
|
66
|
+
text: message,
|
|
67
|
+
parseMode: "Markdown",
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
await ctx.reply(message);
|
|
72
|
+
}
|
|
62
73
|
}
|
|
63
74
|
catch (error) {
|
|
64
75
|
logger.error("[Bot] Error checking server status:", error);
|
package/dist/bot/index.js
CHANGED
|
@@ -19,8 +19,6 @@ import { projectsCommand, handleProjectSelect } from "./commands/projects.js";
|
|
|
19
19
|
import { stopCommand } from "./commands/stop.js";
|
|
20
20
|
import { opencodeStartCommand } from "./commands/opencode-start.js";
|
|
21
21
|
import { opencodeStopCommand } from "./commands/opencode-stop.js";
|
|
22
|
-
import { handleAgentCommand } from "./commands/agent.js";
|
|
23
|
-
import { handleModelCommand } from "./commands/model.js";
|
|
24
22
|
import { renameCommand, handleRenameCancel, handleRenameTextAnswer } from "./commands/rename.js";
|
|
25
23
|
import { handleQuestionCallback, showCurrentQuestion, handleQuestionTextAnswer, } from "./handlers/question.js";
|
|
26
24
|
import { handlePermissionCallback, showPermissionRequest } from "./handlers/permission.js";
|
|
@@ -53,6 +51,7 @@ let botInstance = null;
|
|
|
53
51
|
let chatIdInstance = null;
|
|
54
52
|
let commandsInitialized = false;
|
|
55
53
|
const TELEGRAM_DOCUMENT_CAPTION_MAX_LENGTH = 1024;
|
|
54
|
+
const SESSION_RETRY_PREFIX = "🔁";
|
|
56
55
|
const __filename = fileURLToPath(import.meta.url);
|
|
57
56
|
const __dirname = path.dirname(__filename);
|
|
58
57
|
const TEMP_DIR = path.join(__dirname, "..", ".tmp");
|
|
@@ -323,6 +322,21 @@ async function ensureEventSubscription(directory) {
|
|
|
323
322
|
logger.error("[Bot] Failed to send session.error message:", err);
|
|
324
323
|
});
|
|
325
324
|
});
|
|
325
|
+
summaryAggregator.setOnSessionRetry(async ({ sessionId, message }) => {
|
|
326
|
+
if (!botInstance || !chatIdInstance) {
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
const currentSession = getCurrentSession();
|
|
330
|
+
if (!currentSession || currentSession.id !== sessionId) {
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
const normalizedMessage = message.trim() || t("common.unknown_error");
|
|
334
|
+
const truncatedMessage = normalizedMessage.length > 3500
|
|
335
|
+
? `${normalizedMessage.slice(0, 3497)}...`
|
|
336
|
+
: normalizedMessage;
|
|
337
|
+
const retryMessage = t("bot.session_retry", { message: truncatedMessage });
|
|
338
|
+
toolMessageBatcher.enqueueUniqueByPrefix(sessionId, retryMessage, SESSION_RETRY_PREFIX);
|
|
339
|
+
});
|
|
326
340
|
summaryAggregator.setOnSessionDiff(async (_sessionId, diffs) => {
|
|
327
341
|
if (!pinnedMessageManager.isInitialized()) {
|
|
328
342
|
return;
|
|
@@ -440,8 +454,6 @@ export function createBot() {
|
|
|
440
454
|
bot.command("projects", projectsCommand);
|
|
441
455
|
bot.command("sessions", sessionsCommand);
|
|
442
456
|
bot.command("new", newCommand);
|
|
443
|
-
bot.command("agent", handleAgentCommand);
|
|
444
|
-
bot.command("model", handleModelCommand);
|
|
445
457
|
bot.command("stop", stopCommand);
|
|
446
458
|
bot.command("rename", renameCommand);
|
|
447
459
|
bot.on("message:text", unknownCommandMiddleware);
|
|
@@ -52,7 +52,7 @@ export async function sendMessageWithMarkdownFallback({ api, chatId, text, optio
|
|
|
52
52
|
if (!isTelegramMarkdownParseError(error)) {
|
|
53
53
|
throw error;
|
|
54
54
|
}
|
|
55
|
-
logger.warn("[Bot]
|
|
55
|
+
logger.warn("[Bot] Markdown parse failed, retrying assistant message in raw mode", error);
|
|
56
56
|
await api.sendMessage(chatId, text, options);
|
|
57
57
|
}
|
|
58
58
|
}
|
package/dist/i18n/de.js
CHANGED
|
@@ -4,8 +4,6 @@ export const de = {
|
|
|
4
4
|
"cmd.description.stop": "Aktuelle Aktion stoppen",
|
|
5
5
|
"cmd.description.sessions": "Sitzungen auflisten",
|
|
6
6
|
"cmd.description.projects": "Projekte auflisten",
|
|
7
|
-
"cmd.description.model": "Modell auswählen",
|
|
8
|
-
"cmd.description.agent": "Modus auswählen",
|
|
9
7
|
"cmd.description.opencode_start": "OpenCode-Server starten",
|
|
10
8
|
"cmd.description.opencode_stop": "OpenCode-Server stoppen",
|
|
11
9
|
"cmd.description.help": "Hilfe",
|
|
@@ -31,7 +29,8 @@ export const de = {
|
|
|
31
29
|
"inline.cancelled_callback": "Abgebrochen",
|
|
32
30
|
"common.unknown": "unbekannt",
|
|
33
31
|
"common.unknown_error": "unbekannter Fehler",
|
|
34
|
-
"start.welcome": "👋 Willkommen beim OpenCode Telegram Bot!\n\nNutze Befehle:\n/projects — Projekt auswählen\n/sessions — Sitzungsliste\n/new — neue Sitzung\n/
|
|
32
|
+
"start.welcome": "👋 Willkommen beim OpenCode Telegram Bot!\n\nNutze Befehle:\n/projects — Projekt auswählen\n/sessions — Sitzungsliste\n/new — neue Sitzung\n/status — Status\n/help — Hilfe\n\nNutze die unteren Buttons, um Modus, Modell und Variante zu wählen.",
|
|
33
|
+
"help.keyboard_hint": "💡 Nutze die unteren Buttons für Modus, Modell, Variante und Kontextaktionen.",
|
|
35
34
|
"help.text": "📖 **Hilfe**\n\n/status - Serverstatus prüfen\n/sessions - Sitzungsliste\n/new - Neue Sitzung erstellen\n/help - Hilfe",
|
|
36
35
|
"bot.thinking": "💭 Denke...",
|
|
37
36
|
"bot.project_not_selected": "🏗 Projekt ist nicht ausgewählt.\n\nWähle zuerst ein Projekt mit /projects.",
|
|
@@ -42,6 +41,7 @@ export const de = {
|
|
|
42
41
|
"bot.session_reset_project_mismatch": "⚠️ Die aktive Sitzung passt nicht zum ausgewählten Projekt und wurde daher zurückgesetzt. Nutze /sessions zur Auswahl oder /new, um eine neue Sitzung zu erstellen.",
|
|
43
42
|
"bot.prompt_send_error": "Anfrage konnte nicht an OpenCode gesendet werden.",
|
|
44
43
|
"bot.session_error": "🔴 OpenCode meldete einen Fehler: {message}",
|
|
44
|
+
"bot.session_retry": "🔁 {message}\n\nDer Provider liefert bei wiederholten Versuchen immer wieder denselben Fehler. Mit /stop abbrechen.",
|
|
45
45
|
"bot.unknown_command": "⚠️ Unbekannter Befehl: {command}. Nutze /help, um verfügbare Befehle zu sehen.",
|
|
46
46
|
"bot.photo_downloading": "⏳ Lade Foto herunter...",
|
|
47
47
|
"bot.photo_too_large": "⚠️ Foto ist zu groß (max. {maxSizeMb}MB)",
|
package/dist/i18n/en.js
CHANGED
|
@@ -4,8 +4,6 @@ export const en = {
|
|
|
4
4
|
"cmd.description.stop": "Stop current action",
|
|
5
5
|
"cmd.description.sessions": "List sessions",
|
|
6
6
|
"cmd.description.projects": "List projects",
|
|
7
|
-
"cmd.description.model": "Select model",
|
|
8
|
-
"cmd.description.agent": "Select mode",
|
|
9
7
|
"cmd.description.opencode_start": "Start OpenCode server",
|
|
10
8
|
"cmd.description.opencode_stop": "Stop OpenCode server",
|
|
11
9
|
"cmd.description.help": "Help",
|
|
@@ -31,7 +29,8 @@ export const en = {
|
|
|
31
29
|
"inline.cancelled_callback": "Cancelled",
|
|
32
30
|
"common.unknown": "unknown",
|
|
33
31
|
"common.unknown_error": "unknown error",
|
|
34
|
-
"start.welcome": "👋 Welcome to OpenCode Telegram Bot!\n\nUse commands:\n/projects — select project\n/sessions — session list\n/new — new session\n/
|
|
32
|
+
"start.welcome": "👋 Welcome to OpenCode Telegram Bot!\n\nUse commands:\n/projects — select project\n/sessions — session list\n/new — new session\n/status — status\n/help — help\n\nUse the bottom buttons to select agent mode, model, and variant.",
|
|
33
|
+
"help.keyboard_hint": "💡 Use the bottom keyboard buttons for agent mode, model, variant, and context actions.",
|
|
35
34
|
"help.text": "📖 **Help**\n\n/status - Check server status\n/sessions - Session list\n/new - Create new session\n/help - Help",
|
|
36
35
|
"bot.thinking": "💭 Thinking...",
|
|
37
36
|
"bot.project_not_selected": "🏗 Project is not selected.\n\nFirst select a project with /projects.",
|
|
@@ -42,6 +41,7 @@ export const en = {
|
|
|
42
41
|
"bot.session_reset_project_mismatch": "⚠️ Active session does not match the selected project, so it was reset. Use /sessions to pick one or /new to create a new session.",
|
|
43
42
|
"bot.prompt_send_error": "Failed to send request to OpenCode.",
|
|
44
43
|
"bot.session_error": "🔴 OpenCode returned an error: {message}",
|
|
44
|
+
"bot.session_retry": "🔁 {message}\n\nProvider keeps returning the same error on repeated retries. Use /stop to abort.",
|
|
45
45
|
"bot.unknown_command": "⚠️ Unknown command: {command}. Use /help to see available commands.",
|
|
46
46
|
"bot.photo_downloading": "⏳ Downloading photo...",
|
|
47
47
|
"bot.photo_too_large": "⚠️ Photo is too large (max {maxSizeMb}MB)",
|
package/dist/i18n/es.js
CHANGED
|
@@ -4,8 +4,6 @@ export const es = {
|
|
|
4
4
|
"cmd.description.stop": "Detener la acción actual",
|
|
5
5
|
"cmd.description.sessions": "Listar sesiones",
|
|
6
6
|
"cmd.description.projects": "Listar proyectos",
|
|
7
|
-
"cmd.description.model": "Seleccionar modelo",
|
|
8
|
-
"cmd.description.agent": "Seleccionar modo",
|
|
9
7
|
"cmd.description.opencode_start": "Iniciar servidor OpenCode",
|
|
10
8
|
"cmd.description.opencode_stop": "Detener servidor OpenCode",
|
|
11
9
|
"cmd.description.help": "Ayuda",
|
|
@@ -31,7 +29,8 @@ export const es = {
|
|
|
31
29
|
"inline.cancelled_callback": "Cancelado",
|
|
32
30
|
"common.unknown": "desconocido",
|
|
33
31
|
"common.unknown_error": "error desconocido",
|
|
34
|
-
"start.welcome": "👋 ¡Bienvenido a OpenCode Telegram Bot!\n\nUsa los comandos:\n/projects — seleccionar proyecto\n/sessions — lista de sesiones\n/new — sesión nueva\n/
|
|
32
|
+
"start.welcome": "👋 ¡Bienvenido a OpenCode Telegram Bot!\n\nUsa los comandos:\n/projects — seleccionar proyecto\n/sessions — lista de sesiones\n/new — sesión nueva\n/status — estado\n/help — ayuda\n\nUsa los botones inferiores para elegir modo, modelo y variante.",
|
|
33
|
+
"help.keyboard_hint": "💡 Usa los botones inferiores para modo del agente, modelo, variante y acciones de contexto.",
|
|
35
34
|
"help.text": "📖 **Ayuda**\n\n/status - Ver estado del servidor\n/sessions - Lista de sesiones\n/new - Crear una sesión nueva\n/help - Ayuda",
|
|
36
35
|
"bot.thinking": "💭 Pensando...",
|
|
37
36
|
"bot.project_not_selected": "🏗 No hay un proyecto seleccionado.\n\nPrimero selecciona un proyecto con /projects.",
|
|
@@ -42,6 +41,7 @@ export const es = {
|
|
|
42
41
|
"bot.session_reset_project_mismatch": "⚠️ La sesión activa no coincide con el proyecto seleccionado, así que se reinició. Usa /sessions para elegir una o /new para crear una nueva.",
|
|
43
42
|
"bot.prompt_send_error": "No se pudo enviar la solicitud a OpenCode.",
|
|
44
43
|
"bot.session_error": "🔴 OpenCode devolvió un error: {message}",
|
|
44
|
+
"bot.session_retry": "🔁 {message}\n\nEl proveedor devuelve el mismo error en intentos repetidos. Usa /stop para detenerlo.",
|
|
45
45
|
"bot.unknown_command": "⚠️ Comando desconocido: {command}. Usa /help para ver los comandos disponibles.",
|
|
46
46
|
"bot.photo_downloading": "⏳ Descargando foto...",
|
|
47
47
|
"bot.photo_too_large": "⚠️ La foto es demasiado grande (max {maxSizeMb}MB)",
|
package/dist/i18n/ru.js
CHANGED
|
@@ -4,8 +4,6 @@ export const ru = {
|
|
|
4
4
|
"cmd.description.stop": "Прервать текущее действие",
|
|
5
5
|
"cmd.description.sessions": "Список сессий",
|
|
6
6
|
"cmd.description.projects": "Список проектов",
|
|
7
|
-
"cmd.description.model": "Выбрать модель",
|
|
8
|
-
"cmd.description.agent": "Выбрать режим работы",
|
|
9
7
|
"cmd.description.opencode_start": "Запустить OpenCode сервер",
|
|
10
8
|
"cmd.description.opencode_stop": "Остановить OpenCode сервер",
|
|
11
9
|
"cmd.description.help": "Справка",
|
|
@@ -31,7 +29,8 @@ export const ru = {
|
|
|
31
29
|
"inline.cancelled_callback": "Отменено",
|
|
32
30
|
"common.unknown": "неизвестна",
|
|
33
31
|
"common.unknown_error": "неизвестная ошибка",
|
|
34
|
-
"start.welcome": "👋 Добро пожаловать в OpenCode Telegram Bot!\n\nИспользуйте команды:\n/projects — выбрать проект\n/sessions — список сессий\n/new — новая сессия\n/
|
|
32
|
+
"start.welcome": "👋 Добро пожаловать в OpenCode Telegram Bot!\n\nИспользуйте команды:\n/projects — выбрать проект\n/sessions — список сессий\n/new — новая сессия\n/status — статус\n/help — справка\n\nРежим, модель и вариант выбираются кнопками внизу.",
|
|
33
|
+
"help.keyboard_hint": "💡 Режим, модель, вариант и действия с контекстом доступны через нижние кнопки клавиатуры.",
|
|
35
34
|
"help.text": "📖 **Справка**\n\n/status - Проверить статус сервера\n/sessions - Список сессий\n/new - Создать новую сессию\n/help - Справка",
|
|
36
35
|
"bot.thinking": "💭 Думаю...",
|
|
37
36
|
"bot.project_not_selected": "🏗 Проект не выбран.\n\nСначала выберите проект командой /projects.",
|
|
@@ -42,6 +41,7 @@ export const ru = {
|
|
|
42
41
|
"bot.session_reset_project_mismatch": "⚠️ Активная сессия не соответствует выбранному проекту, поэтому была сброшена. Используйте /sessions для выбора или /new для создания новой сессии.",
|
|
43
42
|
"bot.prompt_send_error": "Не удалось отправить запрос в OpenCode.",
|
|
44
43
|
"bot.session_error": "🔴 OpenCode вернул ошибку: {message}",
|
|
44
|
+
"bot.session_retry": "🔁 {message}\n\nПровайдер возвращает одну и ту же ошибку при повторных запросах. Используйте /stop для остановки.",
|
|
45
45
|
"bot.unknown_command": "⚠️ Неизвестная команда: {command}. Используйте /help для списка команд.",
|
|
46
46
|
"bot.photo_downloading": "⏳ Скачиваю фото...",
|
|
47
47
|
"bot.photo_too_large": "⚠️ Фото слишком большое (макс. {maxSizeMb}МБ)",
|
|
@@ -123,12 +123,12 @@ export const ru = {
|
|
|
123
123
|
"model.menu.favorites_hint": "ℹ️ Список моделей формируется из favorites в OpenCode CLI.",
|
|
124
124
|
"model.menu.error": "🔴 Не удалось получить список моделей",
|
|
125
125
|
"variant.model_not_selected_callback": "Ошибка: модель не выбрана",
|
|
126
|
-
"variant.changed_callback": "
|
|
127
|
-
"variant.changed_message": "✅
|
|
128
|
-
"variant.change_error_callback": "Ошибка при смене
|
|
126
|
+
"variant.changed_callback": "Вариант изменен: {name}",
|
|
127
|
+
"variant.changed_message": "✅ Вариант изменен на: {name}",
|
|
128
|
+
"variant.change_error_callback": "Ошибка при смене варианта",
|
|
129
129
|
"variant.select_model_first": "⚠️ Сначала выберите модель",
|
|
130
130
|
"variant.menu.empty": "⚠️ Нет доступных вариантов",
|
|
131
|
-
"variant.menu.current": "Текущий
|
|
131
|
+
"variant.menu.current": "Текущий вариант: {name}\n\nВыберите вариант:",
|
|
132
132
|
"variant.menu.error": "🔴 Не удалось получить список вариантов",
|
|
133
133
|
"context.button.confirm": "✅ Да, сжать контекст",
|
|
134
134
|
"context.no_active_session": "⚠️ Нет активной сессии. Создайте сессию командой /new",
|
package/dist/i18n/zh.js
CHANGED
|
@@ -4,8 +4,6 @@ export const zh = {
|
|
|
4
4
|
"cmd.description.stop": "停止当前操作",
|
|
5
5
|
"cmd.description.sessions": "列出会话",
|
|
6
6
|
"cmd.description.projects": "列出项目",
|
|
7
|
-
"cmd.description.model": "选择模型",
|
|
8
|
-
"cmd.description.agent": "选择模式",
|
|
9
7
|
"cmd.description.opencode_start": "启动 OpenCode 服务器",
|
|
10
8
|
"cmd.description.opencode_stop": "停止 OpenCode 服务器",
|
|
11
9
|
"cmd.description.help": "帮助",
|
|
@@ -31,7 +29,8 @@ export const zh = {
|
|
|
31
29
|
"inline.cancelled_callback": "已取消",
|
|
32
30
|
"common.unknown": "未知",
|
|
33
31
|
"common.unknown_error": "未知错误",
|
|
34
|
-
"start.welcome": "👋 欢迎使用 OpenCode Telegram Bot!\n\n可用命令:\n/projects — 选择项目\n/sessions — 会话列表\n/new — 新建会话\n/
|
|
32
|
+
"start.welcome": "👋 欢迎使用 OpenCode Telegram Bot!\n\n可用命令:\n/projects — 选择项目\n/sessions — 会话列表\n/new — 新建会话\n/status — 状态\n/help — 帮助\n\n请使用底部按钮选择模式、模型和变体。",
|
|
33
|
+
"help.keyboard_hint": "💡 代理模式、模型、变体和上下文操作请使用底部键盘按钮。",
|
|
35
34
|
"help.text": "📖 **帮助**\n\n/status - 查看服务器状态\n/sessions - 会话列表\n/new - 创建新会话\n/help - 帮助",
|
|
36
35
|
"bot.thinking": "💭 思考中...",
|
|
37
36
|
"bot.project_not_selected": "🏗 未选择项目。\n\n请先使用 /projects 选择一个项目。",
|
|
@@ -42,6 +41,7 @@ export const zh = {
|
|
|
42
41
|
"bot.session_reset_project_mismatch": "⚠️ 活动会话与所选项目不匹配,因此已重置。使用 /sessions 选择一个会话,或 /new 创建新会话。",
|
|
43
42
|
"bot.prompt_send_error": "向 OpenCode 发送请求失败。",
|
|
44
43
|
"bot.session_error": "🔴 OpenCode 返回错误:{message}",
|
|
44
|
+
"bot.session_retry": "🔁 {message}\n\n提供方在重复重试时持续返回同一错误。使用 /stop 可停止。",
|
|
45
45
|
"bot.unknown_command": "⚠️ 未知命令:{command}。使用 /help 查看可用命令。",
|
|
46
46
|
"bot.photo_downloading": "⏳ 正在下载照片...",
|
|
47
47
|
"bot.photo_too_large": "⚠️ 照片过大(最大 {maxSizeMb}MB)",
|
|
@@ -40,6 +40,7 @@ class SummaryAggregator {
|
|
|
40
40
|
onTokensCallback = null;
|
|
41
41
|
onSessionCompactedCallback = null;
|
|
42
42
|
onSessionErrorCallback = null;
|
|
43
|
+
onSessionRetryCallback = null;
|
|
43
44
|
onPermissionCallback = null;
|
|
44
45
|
onSessionDiffCallback = null;
|
|
45
46
|
onFileChangeCallback = null;
|
|
@@ -81,6 +82,9 @@ class SummaryAggregator {
|
|
|
81
82
|
setOnSessionError(callback) {
|
|
82
83
|
this.onSessionErrorCallback = callback;
|
|
83
84
|
}
|
|
85
|
+
setOnSessionRetry(callback) {
|
|
86
|
+
this.onSessionRetryCallback = callback;
|
|
87
|
+
}
|
|
84
88
|
setOnPermission(callback) {
|
|
85
89
|
this.onPermissionCallback = callback;
|
|
86
90
|
}
|
|
@@ -434,10 +438,24 @@ class SummaryAggregator {
|
|
|
434
438
|
return hash.toString(36);
|
|
435
439
|
}
|
|
436
440
|
handleSessionStatus(event) {
|
|
437
|
-
const { sessionID } = event.properties;
|
|
441
|
+
const { sessionID, status } = event.properties;
|
|
438
442
|
if (sessionID !== this.currentSessionId) {
|
|
439
443
|
return;
|
|
440
444
|
}
|
|
445
|
+
if (status?.type !== "retry" || !this.onSessionRetryCallback) {
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
448
|
+
const callback = this.onSessionRetryCallback;
|
|
449
|
+
const message = status.message?.trim() || "Unknown retry error";
|
|
450
|
+
logger.warn(`[Aggregator] Session retry: session=${sessionID}, attempt=${status.attempt ?? "n/a"}, message=${message}`);
|
|
451
|
+
setImmediate(() => {
|
|
452
|
+
callback({
|
|
453
|
+
sessionId: sessionID,
|
|
454
|
+
attempt: status.attempt,
|
|
455
|
+
message,
|
|
456
|
+
next: status.next,
|
|
457
|
+
});
|
|
458
|
+
});
|
|
441
459
|
}
|
|
442
460
|
handleSessionIdle(event) {
|
|
443
461
|
const { sessionID } = event.properties;
|
|
@@ -44,21 +44,10 @@ export class ToolMessageBatcher {
|
|
|
44
44
|
return this.intervalSeconds;
|
|
45
45
|
}
|
|
46
46
|
enqueue(sessionId, message) {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
if (this.intervalSeconds === 0) {
|
|
52
|
-
const expectedGeneration = this.generation;
|
|
53
|
-
logger.debug(`[ToolBatcher] Sending immediate text message: session=${sessionId}`);
|
|
54
|
-
void this.enqueueTask(sessionId, () => this.sendTextSafe(sessionId, normalizedMessage, "immediate", expectedGeneration));
|
|
55
|
-
return;
|
|
56
|
-
}
|
|
57
|
-
const queue = this.queues.get(sessionId) ?? [];
|
|
58
|
-
queue.push({ kind: "text", text: normalizedMessage });
|
|
59
|
-
this.queues.set(sessionId, queue);
|
|
60
|
-
logger.debug(`[ToolBatcher] Queued text message: session=${sessionId}, queueSize=${queue.length}, interval=${this.intervalSeconds}s`);
|
|
61
|
-
this.ensureTimer(sessionId);
|
|
47
|
+
this.enqueueTextInternal(sessionId, message);
|
|
48
|
+
}
|
|
49
|
+
enqueueUniqueByPrefix(sessionId, message, prefix) {
|
|
50
|
+
this.enqueueTextInternal(sessionId, message, prefix);
|
|
62
51
|
}
|
|
63
52
|
enqueueFile(sessionId, fileData) {
|
|
64
53
|
if (!sessionId) {
|
|
@@ -142,6 +131,34 @@ export class ToolMessageBatcher {
|
|
|
142
131
|
this.sessionTasks.set(sessionId, nextTask);
|
|
143
132
|
return nextTask;
|
|
144
133
|
}
|
|
134
|
+
enqueueTextInternal(sessionId, message, uniquePrefix) {
|
|
135
|
+
const normalizedMessage = message.trim();
|
|
136
|
+
if (!sessionId || normalizedMessage.length === 0) {
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
if (this.intervalSeconds === 0) {
|
|
140
|
+
const expectedGeneration = this.generation;
|
|
141
|
+
logger.debug(`[ToolBatcher] Sending immediate text message: session=${sessionId}`);
|
|
142
|
+
void this.enqueueTask(sessionId, () => this.sendTextSafe(sessionId, normalizedMessage, "immediate", expectedGeneration));
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
const normalizedPrefix = uniquePrefix?.trim();
|
|
146
|
+
const queue = this.queues.get(sessionId) ?? [];
|
|
147
|
+
if (normalizedPrefix) {
|
|
148
|
+
const existingUniqueMessage = queue.find((item) => item.kind === "text" && item.text.startsWith(normalizedPrefix));
|
|
149
|
+
if (existingUniqueMessage) {
|
|
150
|
+
existingUniqueMessage.text = normalizedMessage;
|
|
151
|
+
this.queues.set(sessionId, queue);
|
|
152
|
+
logger.debug(`[ToolBatcher] Updated queued unique text message: session=${sessionId}, prefix=${normalizedPrefix}, interval=${this.intervalSeconds}s`);
|
|
153
|
+
this.ensureTimer(sessionId);
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
queue.push({ kind: "text", text: normalizedMessage });
|
|
158
|
+
this.queues.set(sessionId, queue);
|
|
159
|
+
logger.debug(`[ToolBatcher] Queued text message: session=${sessionId}, queueSize=${queue.length}, interval=${this.intervalSeconds}s`);
|
|
160
|
+
this.ensureTimer(sessionId);
|
|
161
|
+
}
|
|
145
162
|
async flushSessionInternal(sessionId, reason) {
|
|
146
163
|
const expectedGeneration = this.generation;
|
|
147
164
|
this.clearTimer(sessionId);
|
package/package.json
CHANGED
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
import { showAgentSelectionMenu } from "../handlers/agent.js";
|
|
2
|
-
import { logger } from "../../utils/logger.js";
|
|
3
|
-
import { t } from "../../i18n/index.js";
|
|
4
|
-
/**
|
|
5
|
-
* Handler for /agent command
|
|
6
|
-
* Shows inline menu to select agent mode
|
|
7
|
-
*/
|
|
8
|
-
export async function handleAgentCommand(ctx) {
|
|
9
|
-
logger.debug("[AgentCommand] /agent command received");
|
|
10
|
-
try {
|
|
11
|
-
await showAgentSelectionMenu(ctx);
|
|
12
|
-
}
|
|
13
|
-
catch (err) {
|
|
14
|
-
logger.error("[AgentCommand] Error showing agent menu:", err);
|
|
15
|
-
await ctx.reply(t("error.load_agents"));
|
|
16
|
-
}
|
|
17
|
-
}
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
import { showModelSelectionMenu } from "../handlers/model.js";
|
|
2
|
-
import { logger } from "../../utils/logger.js";
|
|
3
|
-
import { t } from "../../i18n/index.js";
|
|
4
|
-
/**
|
|
5
|
-
* Handler for /model command
|
|
6
|
-
* Shows inline menu to select model from favorites
|
|
7
|
-
*/
|
|
8
|
-
export async function handleModelCommand(ctx) {
|
|
9
|
-
logger.debug("[ModelCommand] /model command received");
|
|
10
|
-
try {
|
|
11
|
-
await showModelSelectionMenu(ctx);
|
|
12
|
-
}
|
|
13
|
-
catch (err) {
|
|
14
|
-
logger.error("[ModelCommand] Error showing model menu:", err);
|
|
15
|
-
await ctx.reply(t("error.load_models"));
|
|
16
|
-
}
|
|
17
|
-
}
|