@grinev/opencode-telegram-bot 0.18.0 → 0.19.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 +22 -1
- package/README.md +20 -5
- package/dist/app/start-bot-app.js +4 -0
- package/dist/attach/service.js +6 -1
- package/dist/bot/commands/definitions.js +1 -0
- package/dist/bot/commands/mcps.js +394 -0
- package/dist/bot/commands/opencode-start.js +2 -10
- package/dist/bot/commands/projects.js +5 -4
- package/dist/bot/commands/tasklist.js +3 -1
- package/dist/bot/handlers/prompt.js +15 -14
- package/dist/bot/index.js +6 -2
- package/dist/config.js +25 -6
- package/dist/i18n/de.js +21 -0
- package/dist/i18n/en.js +21 -0
- package/dist/i18n/es.js +21 -0
- package/dist/i18n/fr.js +21 -0
- package/dist/i18n/ru.js +21 -0
- package/dist/i18n/zh.js +21 -0
- package/dist/opencode/auto-restart.js +92 -0
- package/dist/opencode/process.js +18 -1
- package/dist/pinned/manager.js +21 -0
- package/dist/scheduled-task/executor.js +155 -8
- package/dist/scheduled-task/runtime.js +30 -5
- package/dist/session/cache-manager.js +19 -6
- package/dist/summary/aggregator.js +35 -0
- package/dist/summary/formatter.js +2 -2
- package/dist/summary/markdown-to-telegram-v2.js +99 -0
- package/dist/summary/subagent-formatter.js +23 -2
- package/dist/telegram/render/inline-renderer.js +18 -0
- package/dist/tts/client.js +94 -19
- package/dist/utils/logger.js +47 -1
- package/package.json +2 -2
|
@@ -197,7 +197,7 @@ export async function processUserPrompt(ctx, text, deps, fileParts = [], options
|
|
|
197
197
|
promptLength: text.length,
|
|
198
198
|
fileCount: fileParts.length,
|
|
199
199
|
};
|
|
200
|
-
logger.info(`[Bot] Calling session.
|
|
200
|
+
logger.info(`[Bot] Calling session.promptAsync (start-only) with agent=${currentAgent}, fileCount=${fileParts.length}...`);
|
|
201
201
|
foregroundSessionState.markBusy(currentSession.id);
|
|
202
202
|
await markAttachedSessionBusy(currentSession.id);
|
|
203
203
|
assistantRunState.startRun(currentSession.id, {
|
|
@@ -210,13 +210,14 @@ export async function processUserPrompt(ctx, text, deps, fileParts = [], options
|
|
|
210
210
|
if (text.trim().length > 0) {
|
|
211
211
|
externalUserInputSuppressionManager.register(currentSession.id, text);
|
|
212
212
|
}
|
|
213
|
-
// CRITICAL:
|
|
214
|
-
//
|
|
215
|
-
// which
|
|
216
|
-
//
|
|
213
|
+
// CRITICAL: Use the async prompt start endpoint here.
|
|
214
|
+
// session.prompt streams the full assistant response and can outlive the original
|
|
215
|
+
// Telegram message handler, which turns late transport failures into misleading
|
|
216
|
+
// "failed to send" messages even after the run has already started.
|
|
217
|
+
// The actual assistant result still arrives via the SSE event subscription.
|
|
217
218
|
safeBackgroundTask({
|
|
218
|
-
taskName: "session.
|
|
219
|
-
task: () => opencodeClient.session.
|
|
219
|
+
taskName: "session.promptAsync",
|
|
220
|
+
task: () => opencodeClient.session.promptAsync(promptOptions),
|
|
220
221
|
onSuccess: ({ error }) => {
|
|
221
222
|
if (error) {
|
|
222
223
|
foregroundSessionState.markIdle(currentSession.id);
|
|
@@ -224,14 +225,14 @@ export async function processUserPrompt(ctx, text, deps, fileParts = [], options
|
|
|
224
225
|
assistantRunState.clearRun(currentSession.id, "session_prompt_api_error");
|
|
225
226
|
clearPromptResponseMode(currentSession.id);
|
|
226
227
|
const details = formatErrorDetails(error, 6000);
|
|
227
|
-
logger.error("[Bot] OpenCode API returned an error for session.
|
|
228
|
-
logger.error("[Bot] session.
|
|
229
|
-
logger.error("[Bot] session.
|
|
228
|
+
logger.error("[Bot] OpenCode API returned an error for session.promptAsync", promptErrorLogContext);
|
|
229
|
+
logger.error("[Bot] session.promptAsync error details:", details);
|
|
230
|
+
logger.error("[Bot] session.promptAsync raw API error object:", error);
|
|
230
231
|
// Send user-friendly error via API directly because ctx is no longer available
|
|
231
232
|
void bot.api.sendMessage(ctx.chat.id, t("bot.prompt_send_error")).catch(() => { });
|
|
232
233
|
return;
|
|
233
234
|
}
|
|
234
|
-
logger.info("[Bot] session.
|
|
235
|
+
logger.info("[Bot] session.promptAsync accepted");
|
|
235
236
|
},
|
|
236
237
|
onError: (error) => {
|
|
237
238
|
foregroundSessionState.markIdle(currentSession.id);
|
|
@@ -239,9 +240,9 @@ export async function processUserPrompt(ctx, text, deps, fileParts = [], options
|
|
|
239
240
|
assistantRunState.clearRun(currentSession.id, "session_prompt_background_error");
|
|
240
241
|
clearPromptResponseMode(currentSession.id);
|
|
241
242
|
const details = formatErrorDetails(error, 6000);
|
|
242
|
-
logger.error("[Bot] session.
|
|
243
|
-
logger.error("[Bot] session.
|
|
244
|
-
logger.error("[Bot] session.
|
|
243
|
+
logger.error("[Bot] session.promptAsync background task failed", promptErrorLogContext);
|
|
244
|
+
logger.error("[Bot] session.promptAsync background failure details:", details);
|
|
245
|
+
logger.error("[Bot] session.promptAsync raw background error object:", error);
|
|
245
246
|
void bot.api.sendMessage(ctx.chat.id, t("bot.prompt_send_error")).catch(() => { });
|
|
246
247
|
},
|
|
247
248
|
});
|
package/dist/bot/index.js
CHANGED
|
@@ -26,6 +26,7 @@ import { handleTaskCallback, handleTaskTextInput, taskCommand } from "./commands
|
|
|
26
26
|
import { handleTaskListCallback, taskListCommand } from "./commands/tasklist.js";
|
|
27
27
|
import { commandsCommand, handleCommandsCallback, handleCommandTextArguments, } from "./commands/commands.js";
|
|
28
28
|
import { skillsCommand, handleSkillsCallback, handleSkillTextArguments, } from "./commands/skills.js";
|
|
29
|
+
import { mcpsCommand, handleMcpsCallback } from "./commands/mcps.js";
|
|
29
30
|
import { ttsCommand } from "./commands/tts.js";
|
|
30
31
|
import { handleQuestionCallback, showCurrentQuestion, handleQuestionTextAnswer, } from "./handlers/question.js";
|
|
31
32
|
import { handlePermissionCallback, showPermissionRequest } from "./handlers/permission.js";
|
|
@@ -873,6 +874,7 @@ export function createBot() {
|
|
|
873
874
|
bot.command("rename", renameCommand);
|
|
874
875
|
bot.command("commands", commandsCommand);
|
|
875
876
|
bot.command("skills", skillsCommand);
|
|
877
|
+
bot.command("mcps", mcpsCommand);
|
|
876
878
|
bot.on("message:text", unknownCommandMiddleware);
|
|
877
879
|
bot.on("callback_query:data", async (ctx) => {
|
|
878
880
|
logger.debug(`[Bot] Received callback_query:data: ${ctx.callbackQuery?.data}`);
|
|
@@ -902,7 +904,8 @@ export function createBot() {
|
|
|
902
904
|
const handledRenameCancel = await handleRenameCancel(ctx);
|
|
903
905
|
const handledCommands = await handleCommandsCallback(ctx, { bot, ensureEventSubscription });
|
|
904
906
|
const handledSkills = await handleSkillsCallback(ctx, { bot, ensureEventSubscription });
|
|
905
|
-
|
|
907
|
+
const handledMcps = await handleMcpsCallback(ctx);
|
|
908
|
+
logger.debug(`[Bot] Callback handled: inlineCancel=${handledInlineCancel}, session=${handledSession}, project=${handledProject}, worktree=${handledWorktree}, open=${handledOpen}, question=${handledQuestion}, permission=${handledPermission}, agent=${handledAgent}, model=${handledModel}, variant=${handledVariant}, compactConfirm=${handledCompactConfirm}, task=${handledTask}, taskList=${handledTaskList}, rename=${handledRenameCancel}, commands=${handledCommands}, skills=${handledSkills}, mcps=${handledMcps}`);
|
|
906
909
|
if (!handledInlineCancel &&
|
|
907
910
|
!handledSession &&
|
|
908
911
|
!handledProject &&
|
|
@@ -918,7 +921,8 @@ export function createBot() {
|
|
|
918
921
|
!handledTaskList &&
|
|
919
922
|
!handledRenameCancel &&
|
|
920
923
|
!handledCommands &&
|
|
921
|
-
!handledSkills
|
|
924
|
+
!handledSkills &&
|
|
925
|
+
!handledMcps) {
|
|
922
926
|
logger.debug("Unknown callback query:", ctx.callbackQuery?.data);
|
|
923
927
|
await ctx.answerCallbackQuery({ text: t("callback.unknown_command") });
|
|
924
928
|
}
|
package/dist/config.js
CHANGED
|
@@ -50,6 +50,18 @@ function getOptionalMessageFormatModeEnvVar(key, defaultValue) {
|
|
|
50
50
|
}
|
|
51
51
|
return defaultValue;
|
|
52
52
|
}
|
|
53
|
+
const VALID_TTS_PROVIDERS = ["openai", "google"];
|
|
54
|
+
function getOptionalTtsProviderEnvVar(key, defaultValue) {
|
|
55
|
+
const value = getEnvVar(key, false);
|
|
56
|
+
if (!value) {
|
|
57
|
+
return defaultValue;
|
|
58
|
+
}
|
|
59
|
+
const normalized = value.trim().toLowerCase();
|
|
60
|
+
if (VALID_TTS_PROVIDERS.includes(normalized)) {
|
|
61
|
+
return normalized;
|
|
62
|
+
}
|
|
63
|
+
return defaultValue;
|
|
64
|
+
}
|
|
53
65
|
export const config = {
|
|
54
66
|
telegram: {
|
|
55
67
|
token: getEnvVar("TELEGRAM_BOT_TOKEN"),
|
|
@@ -60,6 +72,8 @@ export const config = {
|
|
|
60
72
|
apiUrl: getEnvVar("OPENCODE_API_URL", false) || "http://localhost:4096",
|
|
61
73
|
username: getEnvVar("OPENCODE_SERVER_USERNAME", false) || "opencode",
|
|
62
74
|
password: getEnvVar("OPENCODE_SERVER_PASSWORD", false),
|
|
75
|
+
autoRestartEnabled: getOptionalBooleanEnvVar("OPENCODE_AUTO_RESTART_ENABLED", false),
|
|
76
|
+
monitorIntervalSec: getOptionalPositiveIntEnvVar("OPENCODE_MONITOR_INTERVAL_SEC", 300),
|
|
63
77
|
model: {
|
|
64
78
|
provider: getEnvVar("OPENCODE_MODEL_PROVIDER", true), // Required
|
|
65
79
|
modelId: getEnvVar("OPENCODE_MODEL_ID", true), // Required
|
|
@@ -95,10 +109,15 @@ export const config = {
|
|
|
95
109
|
language: getEnvVar("STT_LANGUAGE", false),
|
|
96
110
|
notePrompt: getEnvVar("STT_NOTE_PROMPT", false),
|
|
97
111
|
},
|
|
98
|
-
tts: {
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
112
|
+
tts: (() => {
|
|
113
|
+
const provider = getOptionalTtsProviderEnvVar("TTS_PROVIDER", "openai");
|
|
114
|
+
const defaultVoice = provider === "google" ? "en-US-Studio-O" : "alloy";
|
|
115
|
+
return {
|
|
116
|
+
apiUrl: getEnvVar("TTS_API_URL", false),
|
|
117
|
+
apiKey: getEnvVar("TTS_API_KEY", false),
|
|
118
|
+
provider,
|
|
119
|
+
model: getEnvVar("TTS_MODEL", false) || "gpt-4o-mini-tts",
|
|
120
|
+
voice: getEnvVar("TTS_VOICE", false) || defaultVoice,
|
|
121
|
+
};
|
|
122
|
+
})(),
|
|
104
123
|
};
|
package/dist/i18n/de.js
CHANGED
|
@@ -10,6 +10,7 @@ export const de = {
|
|
|
10
10
|
"cmd.description.tasklist": "Geplante Aufgaben anzeigen",
|
|
11
11
|
"cmd.description.commands": "Benutzerdefinierte Befehle",
|
|
12
12
|
"cmd.description.skills": "Skill-Katalog",
|
|
13
|
+
"cmd.description.mcps": "MCP servers",
|
|
13
14
|
"cmd.description.opencode_start": "OpenCode-Server starten",
|
|
14
15
|
"cmd.description.opencode_stop": "OpenCode-Server stoppen",
|
|
15
16
|
"cmd.description.help": "Hilfe",
|
|
@@ -319,6 +320,8 @@ export const de = {
|
|
|
319
320
|
"task.kind.once": "einmalig",
|
|
320
321
|
"task.run.success": "⏰ Geplante Aufgabe abgeschlossen: {description}",
|
|
321
322
|
"task.run.error": "🔴 Geplante Aufgabe fehlgeschlagen: {description}\n\nFehler: {error}",
|
|
323
|
+
"task.run.error.interactive_question": "Die geplante Aufgabe hat eine interaktive Frage gestellt und kann unbeaufsichtigt nicht fortfahren.",
|
|
324
|
+
"task.run.error.interactive_permission": "Die geplante Aufgabe hat eine interaktive Berechtigung angefordert und kann unbeaufsichtigt nicht fortfahren.",
|
|
322
325
|
"tasklist.empty": "📭 Noch keine geplanten Aufgaben.",
|
|
323
326
|
"tasklist.select": "Wähle eine geplante Aufgabe:",
|
|
324
327
|
"tasklist.details": "⏰ Geplante Aufgabe\n\nAufgabe: {prompt}\nProjekt: {project}\nZeitplan: {schedule}\n{cronLine}Zeitzone: {timezone}\nNächster Lauf: {nextRunAt}\nLetzter Lauf: {lastRunAt}\nAnzahl Läufe: {runCount}",
|
|
@@ -364,6 +367,24 @@ export const de = {
|
|
|
364
367
|
"skills.button.next_page": "Weiter ➡️",
|
|
365
368
|
"skills.page_empty_callback": "Keine Skills auf dieser Seite",
|
|
366
369
|
"skills.page_load_error_callback": "Diese Seite konnte nicht geladen werden. Bitte versuche es erneut.",
|
|
370
|
+
"mcps.select": "MCP servers:",
|
|
371
|
+
"mcps.empty": "📭 No MCP servers configured.",
|
|
372
|
+
"mcps.fetch_error": "🔴 Failed to load MCP servers.",
|
|
373
|
+
"mcps.toggle_error": "🔴 Failed to toggle MCP server.",
|
|
374
|
+
"mcps.enabling": "Enabling...",
|
|
375
|
+
"mcps.disabling": "Disabling...",
|
|
376
|
+
"mcps.status.connected": "🟢 Connected",
|
|
377
|
+
"mcps.status.disabled": "🔴 Disabled",
|
|
378
|
+
"mcps.status.failed": "⚠️ Failed",
|
|
379
|
+
"mcps.status.needs_auth": "🔒 Needs auth",
|
|
380
|
+
"mcps.status.needs_client_registration": "🔒 Needs registration",
|
|
381
|
+
"mcps.detail.title": "Server: {name}",
|
|
382
|
+
"mcps.detail.status": "Status: {status}",
|
|
383
|
+
"mcps.detail.error": "Error: {error}",
|
|
384
|
+
"mcps.button.enable": "🟢 Enable",
|
|
385
|
+
"mcps.button.disable": "🔴 Disable",
|
|
386
|
+
"mcps.button.back": "⬅️ Back",
|
|
387
|
+
"mcps.auth_required": "This server requires authorization and cannot be enabled from the bot.",
|
|
367
388
|
"cmd.description.rename": "Aktuelle Sitzung umbenennen",
|
|
368
389
|
"legacy.models.fetch_error": "🔴 Modellliste konnte nicht geladen werden. Prüfe den Serverstatus mit /status.",
|
|
369
390
|
"legacy.models.empty": "📋 Keine verfügbaren Modelle. Konfiguriere Provider in OpenCode.",
|
package/dist/i18n/en.js
CHANGED
|
@@ -10,6 +10,7 @@ export const en = {
|
|
|
10
10
|
"cmd.description.tasklist": "List scheduled tasks",
|
|
11
11
|
"cmd.description.commands": "Custom commands",
|
|
12
12
|
"cmd.description.skills": "Skills catalog",
|
|
13
|
+
"cmd.description.mcps": "MCP servers",
|
|
13
14
|
"cmd.description.opencode_start": "Start OpenCode server",
|
|
14
15
|
"cmd.description.opencode_stop": "Stop OpenCode server",
|
|
15
16
|
"cmd.description.help": "Help",
|
|
@@ -319,6 +320,8 @@ export const en = {
|
|
|
319
320
|
"task.kind.once": "one-time",
|
|
320
321
|
"task.run.success": "⏰ Scheduled task completed: {description}",
|
|
321
322
|
"task.run.error": "🔴 Scheduled task failed: {description}\n\nError: {error}",
|
|
323
|
+
"task.run.error.interactive_question": "Scheduled task requested an interactive question and cannot continue unattended.",
|
|
324
|
+
"task.run.error.interactive_permission": "Scheduled task requested interactive permission and cannot continue unattended.",
|
|
322
325
|
"tasklist.empty": "📭 No scheduled tasks yet.",
|
|
323
326
|
"tasklist.select": "Select a scheduled task:",
|
|
324
327
|
"tasklist.details": "⏰ Scheduled task\n\nTask: {prompt}\nProject: {project}\nSchedule: {schedule}\n{cronLine}Timezone: {timezone}\nNext run: {nextRunAt}\nLast run: {lastRunAt}\nRun count: {runCount}",
|
|
@@ -364,6 +367,24 @@ export const en = {
|
|
|
364
367
|
"skills.button.next_page": "Next ➡️",
|
|
365
368
|
"skills.page_empty_callback": "No skills on this page",
|
|
366
369
|
"skills.page_load_error_callback": "Cannot load this page. Please try again.",
|
|
370
|
+
"mcps.select": "MCP servers:",
|
|
371
|
+
"mcps.empty": "📭 No MCP servers configured.",
|
|
372
|
+
"mcps.fetch_error": "🔴 Failed to load MCP servers.",
|
|
373
|
+
"mcps.toggle_error": "🔴 Failed to toggle MCP server.",
|
|
374
|
+
"mcps.enabling": "Enabling...",
|
|
375
|
+
"mcps.disabling": "Disabling...",
|
|
376
|
+
"mcps.status.connected": "🟢 Connected",
|
|
377
|
+
"mcps.status.disabled": "🔴 Disabled",
|
|
378
|
+
"mcps.status.failed": "⚠️ Failed",
|
|
379
|
+
"mcps.status.needs_auth": "🔒 Needs auth",
|
|
380
|
+
"mcps.status.needs_client_registration": "🔒 Needs registration",
|
|
381
|
+
"mcps.detail.title": "Server: {name}",
|
|
382
|
+
"mcps.detail.status": "Status: {status}",
|
|
383
|
+
"mcps.detail.error": "Error: {error}",
|
|
384
|
+
"mcps.button.enable": "🟢 Enable",
|
|
385
|
+
"mcps.button.disable": "🔴 Disable",
|
|
386
|
+
"mcps.button.back": "⬅️ Back",
|
|
387
|
+
"mcps.auth_required": "This server requires authorization and cannot be enabled from the bot.",
|
|
367
388
|
"cmd.description.rename": "Rename current session",
|
|
368
389
|
"legacy.models.fetch_error": "🔴 Failed to get models list. Check server status with /status.",
|
|
369
390
|
"legacy.models.empty": "📋 No available models. Configure providers in OpenCode.",
|
package/dist/i18n/es.js
CHANGED
|
@@ -10,6 +10,7 @@ export const es = {
|
|
|
10
10
|
"cmd.description.tasklist": "Ver tareas programadas",
|
|
11
11
|
"cmd.description.commands": "Comandos personalizados",
|
|
12
12
|
"cmd.description.skills": "Catálogo de skills",
|
|
13
|
+
"cmd.description.mcps": "MCP servers",
|
|
13
14
|
"cmd.description.opencode_start": "Iniciar servidor OpenCode",
|
|
14
15
|
"cmd.description.opencode_stop": "Detener servidor OpenCode",
|
|
15
16
|
"cmd.description.help": "Ayuda",
|
|
@@ -319,6 +320,8 @@ export const es = {
|
|
|
319
320
|
"task.kind.once": "única",
|
|
320
321
|
"task.run.success": "⏰ Tarea programada completada: {description}",
|
|
321
322
|
"task.run.error": "🔴 La tarea programada falló: {description}\n\nError: {error}",
|
|
323
|
+
"task.run.error.interactive_question": "La tarea programada solicitó una pregunta interactiva y no puede continuar sin supervisión.",
|
|
324
|
+
"task.run.error.interactive_permission": "La tarea programada solicitó un permiso interactivo y no puede continuar sin supervisión.",
|
|
322
325
|
"tasklist.empty": "📭 Aún no hay tareas programadas.",
|
|
323
326
|
"tasklist.select": "Elige una tarea programada:",
|
|
324
327
|
"tasklist.details": "⏰ Tarea programada\n\nTarea: {prompt}\nProyecto: {project}\nHorario: {schedule}\n{cronLine}Zona horaria: {timezone}\nPróxima ejecución: {nextRunAt}\nÚltima ejecución: {lastRunAt}\nNúmero de ejecuciones: {runCount}",
|
|
@@ -364,6 +367,24 @@ export const es = {
|
|
|
364
367
|
"skills.button.next_page": "Siguiente ➡️",
|
|
365
368
|
"skills.page_empty_callback": "No hay skills en esta página",
|
|
366
369
|
"skills.page_load_error_callback": "No se pudo cargar esta página. Por favor, inténtalo de nuevo.",
|
|
370
|
+
"mcps.select": "MCP servers:",
|
|
371
|
+
"mcps.empty": "📭 No MCP servers configured.",
|
|
372
|
+
"mcps.fetch_error": "🔴 Failed to load MCP servers.",
|
|
373
|
+
"mcps.toggle_error": "🔴 Failed to toggle MCP server.",
|
|
374
|
+
"mcps.enabling": "Enabling...",
|
|
375
|
+
"mcps.disabling": "Disabling...",
|
|
376
|
+
"mcps.status.connected": "🟢 Connected",
|
|
377
|
+
"mcps.status.disabled": "🔴 Disabled",
|
|
378
|
+
"mcps.status.failed": "⚠️ Failed",
|
|
379
|
+
"mcps.status.needs_auth": "🔒 Needs auth",
|
|
380
|
+
"mcps.status.needs_client_registration": "🔒 Needs registration",
|
|
381
|
+
"mcps.detail.title": "Server: {name}",
|
|
382
|
+
"mcps.detail.status": "Status: {status}",
|
|
383
|
+
"mcps.detail.error": "Error: {error}",
|
|
384
|
+
"mcps.button.enable": "🟢 Enable",
|
|
385
|
+
"mcps.button.disable": "🔴 Disable",
|
|
386
|
+
"mcps.button.back": "⬅️ Back",
|
|
387
|
+
"mcps.auth_required": "This server requires authorization and cannot be enabled from the bot.",
|
|
367
388
|
"cmd.description.rename": "Renombrar la sesión actual",
|
|
368
389
|
"legacy.models.fetch_error": "🔴 No se pudo obtener la lista de modelos. Revisa el estado del servidor con /status.",
|
|
369
390
|
"legacy.models.empty": "📋 No hay modelos disponibles. Configura los proveedores en OpenCode.",
|
package/dist/i18n/fr.js
CHANGED
|
@@ -10,6 +10,7 @@ export const fr = {
|
|
|
10
10
|
"cmd.description.tasklist": "Afficher les tâches planifiées",
|
|
11
11
|
"cmd.description.commands": "Commandes personnalisées",
|
|
12
12
|
"cmd.description.skills": "Catalogue de skills",
|
|
13
|
+
"cmd.description.mcps": "MCP servers",
|
|
13
14
|
"cmd.description.opencode_start": "Démarrer le serveur OpenCode",
|
|
14
15
|
"cmd.description.opencode_stop": "Arrêter le serveur OpenCode",
|
|
15
16
|
"cmd.description.help": "Aide",
|
|
@@ -319,6 +320,8 @@ export const fr = {
|
|
|
319
320
|
"task.kind.once": "ponctuelle",
|
|
320
321
|
"task.run.success": "⏰ Tâche planifiée terminée : {description}",
|
|
321
322
|
"task.run.error": "🔴 Échec de la tâche planifiée : {description}\n\nErreur : {error}",
|
|
323
|
+
"task.run.error.interactive_question": "La tâche planifiée a demandé une question interactive et ne peut pas continuer sans intervention.",
|
|
324
|
+
"task.run.error.interactive_permission": "La tâche planifiée a demandé une autorisation interactive et ne peut pas continuer sans intervention.",
|
|
322
325
|
"tasklist.empty": "📭 Aucune tâche planifiée pour le moment.",
|
|
323
326
|
"tasklist.select": "Sélectionnez une tâche planifiée :",
|
|
324
327
|
"tasklist.details": "⏰ Tâche planifiée\n\nTâche : {prompt}\nProjet : {project}\nPlanning : {schedule}\n{cronLine}Fuseau horaire : {timezone}\nProchaine exécution : {nextRunAt}\nDernière exécution : {lastRunAt}\nNombre d'exécutions : {runCount}",
|
|
@@ -364,6 +367,24 @@ export const fr = {
|
|
|
364
367
|
"skills.button.next_page": "Suivant ➡️",
|
|
365
368
|
"skills.page_empty_callback": "Aucun skill sur cette page",
|
|
366
369
|
"skills.page_load_error_callback": "Impossible de charger cette page. Veuillez réessayer.",
|
|
370
|
+
"mcps.select": "MCP servers:",
|
|
371
|
+
"mcps.empty": "📭 No MCP servers configured.",
|
|
372
|
+
"mcps.fetch_error": "🔴 Failed to load MCP servers.",
|
|
373
|
+
"mcps.toggle_error": "🔴 Failed to toggle MCP server.",
|
|
374
|
+
"mcps.enabling": "Enabling...",
|
|
375
|
+
"mcps.disabling": "Disabling...",
|
|
376
|
+
"mcps.status.connected": "🟢 Connected",
|
|
377
|
+
"mcps.status.disabled": "🔴 Disabled",
|
|
378
|
+
"mcps.status.failed": "⚠️ Failed",
|
|
379
|
+
"mcps.status.needs_auth": "🔒 Needs auth",
|
|
380
|
+
"mcps.status.needs_client_registration": "🔒 Needs registration",
|
|
381
|
+
"mcps.detail.title": "Server: {name}",
|
|
382
|
+
"mcps.detail.status": "Status: {status}",
|
|
383
|
+
"mcps.detail.error": "Error: {error}",
|
|
384
|
+
"mcps.button.enable": "🟢 Enable",
|
|
385
|
+
"mcps.button.disable": "🔴 Disable",
|
|
386
|
+
"mcps.button.back": "⬅️ Back",
|
|
387
|
+
"mcps.auth_required": "This server requires authorization and cannot be enabled from the bot.",
|
|
367
388
|
"cmd.description.rename": "Renommer la session actuelle",
|
|
368
389
|
"legacy.models.fetch_error": "🔴 Impossible de récupérer la liste des modèles. Vérifiez l'état du serveur avec /status.",
|
|
369
390
|
"legacy.models.empty": "📋 Aucun modèle disponible. Configurez les fournisseurs dans OpenCode.",
|
package/dist/i18n/ru.js
CHANGED
|
@@ -10,6 +10,7 @@ export const ru = {
|
|
|
10
10
|
"cmd.description.tasklist": "Список задач по расписанию",
|
|
11
11
|
"cmd.description.commands": "Пользовательские команды",
|
|
12
12
|
"cmd.description.skills": "Каталог скиллов",
|
|
13
|
+
"cmd.description.mcps": "MCP серверы",
|
|
13
14
|
"cmd.description.opencode_start": "Запустить OpenCode сервер",
|
|
14
15
|
"cmd.description.opencode_stop": "Остановить OpenCode сервер",
|
|
15
16
|
"cmd.description.help": "Справка",
|
|
@@ -319,6 +320,8 @@ export const ru = {
|
|
|
319
320
|
"task.kind.once": "однократная",
|
|
320
321
|
"task.run.success": "⏰ Задача по расписанию выполнена: {description}",
|
|
321
322
|
"task.run.error": "🔴 Ошибка выполнения задачи по расписанию: {description}\n\nОшибка: {error}",
|
|
323
|
+
"task.run.error.interactive_question": "Задача по расписанию задала интерактивный вопрос и не может продолжить выполнение без участия пользователя.",
|
|
324
|
+
"task.run.error.interactive_permission": "Задача по расписанию запросила интерактивное разрешение и не может продолжить выполнение без участия пользователя.",
|
|
322
325
|
"tasklist.empty": "📭 Задач по расписанию пока нет.",
|
|
323
326
|
"tasklist.select": "Выберите задачу по расписанию:",
|
|
324
327
|
"tasklist.details": "⏰ Задача по расписанию\n\nЗадача: {prompt}\nПроект: {project}\nРасписание: {schedule}\n{cronLine}Часовой пояс: {timezone}\nСледующий запуск: {nextRunAt}\nПоследний запуск: {lastRunAt}\nКоличество запусков: {runCount}",
|
|
@@ -364,6 +367,24 @@ export const ru = {
|
|
|
364
367
|
"skills.button.next_page": "Вперёд ➡️",
|
|
365
368
|
"skills.page_empty_callback": "На этой странице нет скиллов",
|
|
366
369
|
"skills.page_load_error_callback": "Не удалось загрузить эту страницу. Пожалуйста, попробуйте снова.",
|
|
370
|
+
"mcps.select": "MCP серверы:",
|
|
371
|
+
"mcps.empty": "📭 MCP серверы не настроены.",
|
|
372
|
+
"mcps.fetch_error": "🔴 Не удалось загрузить MCP серверы.",
|
|
373
|
+
"mcps.toggle_error": "🔴 Не удалось переключить MCP сервер.",
|
|
374
|
+
"mcps.enabling": "Включаю...",
|
|
375
|
+
"mcps.disabling": "Выключаю...",
|
|
376
|
+
"mcps.status.connected": "🟢 Подключен",
|
|
377
|
+
"mcps.status.disabled": "🔴 Отключен",
|
|
378
|
+
"mcps.status.failed": "⚠️ Ошибка",
|
|
379
|
+
"mcps.status.needs_auth": "🔒 Требуется авторизация",
|
|
380
|
+
"mcps.status.needs_client_registration": "🔒 Требуется регистрация",
|
|
381
|
+
"mcps.detail.title": "Сервер: {name}",
|
|
382
|
+
"mcps.detail.status": "Статус: {status}",
|
|
383
|
+
"mcps.detail.error": "Ошибка: {error}",
|
|
384
|
+
"mcps.button.enable": "🟢 Включить",
|
|
385
|
+
"mcps.button.disable": "🔴 Выключить",
|
|
386
|
+
"mcps.button.back": "⬅️ Назад",
|
|
387
|
+
"mcps.auth_required": "Этот сервер требует авторизации и не может быть включен из бота.",
|
|
367
388
|
"cmd.description.rename": "Переименовать текущую сессию",
|
|
368
389
|
"legacy.models.fetch_error": "🔴 Не удалось получить список моделей. Проверьте статус сервера /status.",
|
|
369
390
|
"legacy.models.empty": "📋 Нет доступных моделей. Настройте провайдеры через OpenCode.",
|
package/dist/i18n/zh.js
CHANGED
|
@@ -10,6 +10,7 @@ export const zh = {
|
|
|
10
10
|
"cmd.description.tasklist": "查看定时任务",
|
|
11
11
|
"cmd.description.commands": "自定义命令",
|
|
12
12
|
"cmd.description.skills": "技能目录",
|
|
13
|
+
"cmd.description.mcps": "MCP servers",
|
|
13
14
|
"cmd.description.opencode_start": "启动 OpenCode 服务器",
|
|
14
15
|
"cmd.description.opencode_stop": "停止 OpenCode 服务器",
|
|
15
16
|
"cmd.description.help": "帮助",
|
|
@@ -319,6 +320,8 @@ export const zh = {
|
|
|
319
320
|
"task.kind.once": "一次性",
|
|
320
321
|
"task.run.success": "⏰ 定时任务已完成: {description}",
|
|
321
322
|
"task.run.error": "🔴 定时任务执行失败: {description}\n\n错误: {error}",
|
|
323
|
+
"task.run.error.interactive_question": "定时任务请求了交互式问题,无法在无人值守时继续。",
|
|
324
|
+
"task.run.error.interactive_permission": "定时任务请求了交互式权限,无法在无人值守时继续。",
|
|
322
325
|
"tasklist.empty": "📭 还没有定时任务。",
|
|
323
326
|
"tasklist.select": "请选择一个定时任务:",
|
|
324
327
|
"tasklist.details": "⏰ 定时任务\n\n任务:{prompt}\n项目:{project}\n计划:{schedule}\n{cronLine}时区:{timezone}\n下次运行:{nextRunAt}\n上次运行:{lastRunAt}\n运行次数:{runCount}",
|
|
@@ -364,6 +367,24 @@ export const zh = {
|
|
|
364
367
|
"skills.button.next_page": "下一页 ➡️",
|
|
365
368
|
"skills.page_empty_callback": "这一页没有技能",
|
|
366
369
|
"skills.page_load_error_callback": "无法加载此页面。请重试。",
|
|
370
|
+
"mcps.select": "MCP servers:",
|
|
371
|
+
"mcps.empty": "📭 No MCP servers configured.",
|
|
372
|
+
"mcps.fetch_error": "🔴 Failed to load MCP servers.",
|
|
373
|
+
"mcps.toggle_error": "🔴 Failed to toggle MCP server.",
|
|
374
|
+
"mcps.enabling": "Enabling...",
|
|
375
|
+
"mcps.disabling": "Disabling...",
|
|
376
|
+
"mcps.status.connected": "🟢 Connected",
|
|
377
|
+
"mcps.status.disabled": "🔴 Disabled",
|
|
378
|
+
"mcps.status.failed": "⚠️ Failed",
|
|
379
|
+
"mcps.status.needs_auth": "🔒 Needs auth",
|
|
380
|
+
"mcps.status.needs_client_registration": "🔒 Needs registration",
|
|
381
|
+
"mcps.detail.title": "Server: {name}",
|
|
382
|
+
"mcps.detail.status": "Status: {status}",
|
|
383
|
+
"mcps.detail.error": "Error: {error}",
|
|
384
|
+
"mcps.button.enable": "🟢 Enable",
|
|
385
|
+
"mcps.button.disable": "🔴 Disable",
|
|
386
|
+
"mcps.button.back": "⬅️ Back",
|
|
387
|
+
"mcps.auth_required": "This server requires authorization and cannot be enabled from the bot.",
|
|
367
388
|
"cmd.description.rename": "重命名当前会话",
|
|
368
389
|
"legacy.models.fetch_error": "🔴 获取模型列表失败。请使用 /status 检查服务器状态。",
|
|
369
390
|
"legacy.models.empty": "📋 没有可用模型。请在 OpenCode 中配置 providers。",
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { config } from "../config.js";
|
|
2
|
+
import { logger } from "../utils/logger.js";
|
|
3
|
+
import { opencodeClient } from "./client.js";
|
|
4
|
+
import { resolveLocalOpencodeTarget, startLocalOpencodeServer, } from "./process.js";
|
|
5
|
+
const SERVER_READY_TIMEOUT_MS = 10000;
|
|
6
|
+
const SERVER_READY_POLL_INTERVAL_MS = 500;
|
|
7
|
+
function sleep(ms) {
|
|
8
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
9
|
+
}
|
|
10
|
+
async function isOpencodeServerHealthy() {
|
|
11
|
+
try {
|
|
12
|
+
const { data, error } = await opencodeClient.global.health();
|
|
13
|
+
return !error && data?.healthy === true;
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
async function waitForOpencodeServerReady(timeoutMs) {
|
|
20
|
+
const startedAt = Date.now();
|
|
21
|
+
while (Date.now() - startedAt < timeoutMs) {
|
|
22
|
+
if (await isOpencodeServerHealthy()) {
|
|
23
|
+
return true;
|
|
24
|
+
}
|
|
25
|
+
await sleep(SERVER_READY_POLL_INTERVAL_MS);
|
|
26
|
+
}
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
export class OpencodeAutoRestartService {
|
|
30
|
+
timer = null;
|
|
31
|
+
localTarget = null;
|
|
32
|
+
started = false;
|
|
33
|
+
checkInProgress = false;
|
|
34
|
+
async start() {
|
|
35
|
+
if (this.started || !config.opencode.autoRestartEnabled) {
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
const localTarget = resolveLocalOpencodeTarget(config.opencode.apiUrl);
|
|
39
|
+
if (!localTarget) {
|
|
40
|
+
logger.warn(`[OpenCodeAutoRestart] Disabled because OPENCODE_API_URL is not local: ${config.opencode.apiUrl}`);
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
this.started = true;
|
|
44
|
+
this.localTarget = localTarget;
|
|
45
|
+
logger.info(`[OpenCodeAutoRestart] Enabled: port=${localTarget.port}, intervalSec=${config.opencode.monitorIntervalSec}`);
|
|
46
|
+
await this.checkAndRestart("startup");
|
|
47
|
+
this.timer = setInterval(() => {
|
|
48
|
+
void this.checkAndRestart("interval");
|
|
49
|
+
}, config.opencode.monitorIntervalSec * 1000);
|
|
50
|
+
this.timer.unref?.();
|
|
51
|
+
}
|
|
52
|
+
stop() {
|
|
53
|
+
if (this.timer) {
|
|
54
|
+
clearInterval(this.timer);
|
|
55
|
+
this.timer = null;
|
|
56
|
+
}
|
|
57
|
+
this.started = false;
|
|
58
|
+
this.localTarget = null;
|
|
59
|
+
}
|
|
60
|
+
async checkAndRestart(reason) {
|
|
61
|
+
if (this.checkInProgress || !this.localTarget) {
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
this.checkInProgress = true;
|
|
65
|
+
try {
|
|
66
|
+
if (await isOpencodeServerHealthy()) {
|
|
67
|
+
logger.debug(`[OpenCodeAutoRestart] Health-check succeeded: reason=${reason}`);
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
logger.warn(`[OpenCodeAutoRestart] OpenCode server is unavailable, starting local server: reason=${reason}, port=${this.localTarget.port}`);
|
|
71
|
+
const childProcess = startLocalOpencodeServer(this.localTarget);
|
|
72
|
+
childProcess.once("error", (error) => {
|
|
73
|
+
logger.error("[OpenCodeAutoRestart] OpenCode server process failed to start", error);
|
|
74
|
+
});
|
|
75
|
+
const pid = childProcess.pid;
|
|
76
|
+
childProcess.unref();
|
|
77
|
+
const ready = await waitForOpencodeServerReady(SERVER_READY_TIMEOUT_MS);
|
|
78
|
+
if (!ready) {
|
|
79
|
+
logger.warn(`[OpenCodeAutoRestart] OpenCode server was started but did not become ready: pid=${pid ?? "unknown"}, port=${this.localTarget.port}`);
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
logger.info(`[OpenCodeAutoRestart] OpenCode server recovered: pid=${pid ?? "unknown"}, port=${this.localTarget.port}`);
|
|
83
|
+
}
|
|
84
|
+
catch (error) {
|
|
85
|
+
logger.error("[OpenCodeAutoRestart] Failed to check or restart OpenCode server", error);
|
|
86
|
+
}
|
|
87
|
+
finally {
|
|
88
|
+
this.checkInProgress = false;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
export const opencodeAutoRestartService = new OpencodeAutoRestartService();
|
package/dist/opencode/process.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { exec } from "node:child_process";
|
|
1
|
+
import { exec, spawn } from "node:child_process";
|
|
2
2
|
import { promisify } from "node:util";
|
|
3
3
|
const execAsync = promisify(exec);
|
|
4
4
|
const DEFAULT_OPENCODE_PORT = 4096;
|
|
@@ -25,6 +25,23 @@ export function resolveLocalOpencodeTarget(apiUrl) {
|
|
|
25
25
|
return null;
|
|
26
26
|
}
|
|
27
27
|
}
|
|
28
|
+
export function createOpencodeServeSpawnCommand(target) {
|
|
29
|
+
const isWindows = process.platform === "win32";
|
|
30
|
+
const port = target.port.toString();
|
|
31
|
+
return {
|
|
32
|
+
command: isWindows ? "cmd.exe" : "opencode",
|
|
33
|
+
args: isWindows ? ["/c", "opencode", "serve", "--port", port] : ["serve", "--port", port],
|
|
34
|
+
windowsHide: isWindows,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
export function startLocalOpencodeServer(target) {
|
|
38
|
+
const spawnCommand = createOpencodeServeSpawnCommand(target);
|
|
39
|
+
return spawn(spawnCommand.command, spawnCommand.args, {
|
|
40
|
+
detached: true,
|
|
41
|
+
stdio: "ignore",
|
|
42
|
+
windowsHide: spawnCommand.windowsHide,
|
|
43
|
+
});
|
|
44
|
+
}
|
|
28
45
|
function parsePid(value) {
|
|
29
46
|
const pid = Number.parseInt(value.trim(), 10);
|
|
30
47
|
return Number.isInteger(pid) && pid > 0 ? pid : null;
|
package/dist/pinned/manager.js
CHANGED
|
@@ -77,6 +77,27 @@ class PinnedMessageManager {
|
|
|
77
77
|
// Load existing diffs from API (for session restoration)
|
|
78
78
|
await this.loadDiffsFromApi(sessionId);
|
|
79
79
|
}
|
|
80
|
+
/**
|
|
81
|
+
* Restore in-memory state for a persisted pinned message without creating a new Telegram message.
|
|
82
|
+
*/
|
|
83
|
+
async restoreExistingSession(sessionId, sessionTitle) {
|
|
84
|
+
logger.info(`[PinnedManager] Restoring existing pinned message for session: ${sessionId}`);
|
|
85
|
+
this.state.sessionId = sessionId;
|
|
86
|
+
this.state.sessionTitle = sessionTitle || t("pinned.default_session_title");
|
|
87
|
+
this.state.attachActive = false;
|
|
88
|
+
this.state.attachBusy = false;
|
|
89
|
+
this.state.changedFiles = [];
|
|
90
|
+
this.lastRenderedMessageText = null;
|
|
91
|
+
this.pendingUpdate = false;
|
|
92
|
+
this.pendingForceUpdate = false;
|
|
93
|
+
await this.refreshProjectMetadata();
|
|
94
|
+
await this.fetchContextLimit();
|
|
95
|
+
if (this.onKeyboardUpdateCallback && this.state.tokensLimit > 0) {
|
|
96
|
+
this.onKeyboardUpdateCallback(this.state.tokensUsed, this.state.tokensLimit);
|
|
97
|
+
}
|
|
98
|
+
await this.updatePinnedMessage(true);
|
|
99
|
+
await this.loadDiffsFromApi(sessionId);
|
|
100
|
+
}
|
|
80
101
|
/**
|
|
81
102
|
* Called when session title is updated (after first message)
|
|
82
103
|
*/
|