@grinev/opencode-telegram-bot 0.10.1 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -1
- package/dist/bot/commands/commands.js +379 -0
- package/dist/bot/commands/definitions.js +1 -0
- package/dist/bot/handlers/document.js +65 -0
- package/dist/bot/index.js +27 -6
- package/dist/bot/message-patterns.js +2 -1
- package/dist/bot/utils/file-download.js +16 -0
- package/dist/i18n/de.js +19 -0
- package/dist/i18n/en.js +19 -0
- package/dist/i18n/es.js +19 -0
- package/dist/i18n/ru.js +19 -0
- package/dist/i18n/zh.js +19 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -26,11 +26,12 @@ Languages: English (`en`), Deutsch (`de`), Español (`es`), Русский (`ru`
|
|
|
26
26
|
- **Live status** — pinned message with current project, model, context usage, and changed files list, updated in real time
|
|
27
27
|
- **Model switching** — pick models from OpenCode favorites and recent history directly in the chat (favorites are shown first)
|
|
28
28
|
- **Agent modes** — switch between Plan and Build modes on the fly
|
|
29
|
+
- **Custom Commands** — run OpenCode custom commands (and built-ins like `init`/`review`) from an inline menu with confirmation
|
|
29
30
|
- **Interactive Q&A** — answer agent questions and approve permissions via inline buttons
|
|
30
31
|
- **Voice prompts** — send voice/audio messages, transcribe them via a Whisper-compatible API, then forward recognized text to OpenCode
|
|
32
|
+
- **File attachments** — send images, PDF documents, and any text-based files to OpenCode (code, logs, configs etc.)
|
|
31
33
|
- **Context control** — compact context when it gets too large, right from the chat
|
|
32
34
|
- **Input flow control** — when an interactive flow is active, the bot accepts only relevant input to keep context consistent and avoid accidental actions
|
|
33
|
-
- **Configurable reply formatting** — assistant replies use Telegram MarkdownV2 by default, with optional raw mode (`MESSAGE_FORMAT_MODE=markdown|raw`)
|
|
34
35
|
- **Security** — strict user ID whitelist; no one else can access your bot, even if they find it
|
|
35
36
|
- **Localization** — UI localization is supported for multiple languages (`BOT_LOCALE`)
|
|
36
37
|
|
|
@@ -103,6 +104,7 @@ opencode-telegram config
|
|
|
103
104
|
| `/sessions` | Browse and switch between recent sessions |
|
|
104
105
|
| `/projects` | Switch between OpenCode projects |
|
|
105
106
|
| `/rename` | Rename the current session |
|
|
107
|
+
| `/commands` | Browse and run custom commands |
|
|
106
108
|
| `/opencode_start` | Start the OpenCode server remotely |
|
|
107
109
|
| `/opencode_stop` | Stop the OpenCode server remotely |
|
|
108
110
|
| `/help` | Show available commands |
|
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
import { InlineKeyboard } from "grammy";
|
|
2
|
+
import { opencodeClient } from "../../opencode/client.js";
|
|
3
|
+
import { getCurrentProject } from "../../settings/manager.js";
|
|
4
|
+
import { clearSession, getCurrentSession, setCurrentSession, } from "../../session/manager.js";
|
|
5
|
+
import { ingestSessionInfoForCache } from "../../session/cache-manager.js";
|
|
6
|
+
import { interactionManager } from "../../interaction/manager.js";
|
|
7
|
+
import { summaryAggregator } from "../../summary/aggregator.js";
|
|
8
|
+
import { getStoredAgent } from "../../agent/manager.js";
|
|
9
|
+
import { getStoredModel } from "../../model/manager.js";
|
|
10
|
+
import { safeBackgroundTask } from "../../utils/safe-background-task.js";
|
|
11
|
+
import { logger } from "../../utils/logger.js";
|
|
12
|
+
import { t } from "../../i18n/index.js";
|
|
13
|
+
const COMMANDS_CALLBACK_PREFIX = "commands:";
|
|
14
|
+
const COMMANDS_CALLBACK_SELECT_PREFIX = `${COMMANDS_CALLBACK_PREFIX}select:`;
|
|
15
|
+
const COMMANDS_CALLBACK_CANCEL = `${COMMANDS_CALLBACK_PREFIX}cancel`;
|
|
16
|
+
const COMMANDS_CALLBACK_EXECUTE = `${COMMANDS_CALLBACK_PREFIX}execute`;
|
|
17
|
+
const MAX_INLINE_BUTTON_LABEL_LENGTH = 64;
|
|
18
|
+
function formatExecutingCommandMessage(commandName, args) {
|
|
19
|
+
const commandText = `/${commandName}`;
|
|
20
|
+
const argsSuffix = args ? ` ${args}` : "";
|
|
21
|
+
return `${t("commands.executing_prefix")}\n${commandText}${argsSuffix}`;
|
|
22
|
+
}
|
|
23
|
+
function normalizeDirectoryForCommandApi(directory) {
|
|
24
|
+
return directory.replace(/\\/g, "/");
|
|
25
|
+
}
|
|
26
|
+
function getCallbackMessageId(ctx) {
|
|
27
|
+
const message = ctx.callbackQuery?.message;
|
|
28
|
+
if (!message || !("message_id" in message)) {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
const messageId = message.message_id;
|
|
32
|
+
return typeof messageId === "number" ? messageId : null;
|
|
33
|
+
}
|
|
34
|
+
function formatCommandButtonLabel(command) {
|
|
35
|
+
const description = command.description?.trim() || t("commands.no_description");
|
|
36
|
+
const rawLabel = `/${command.name} - ${description}`;
|
|
37
|
+
if (rawLabel.length <= MAX_INLINE_BUTTON_LABEL_LENGTH) {
|
|
38
|
+
return rawLabel;
|
|
39
|
+
}
|
|
40
|
+
return `${rawLabel.slice(0, MAX_INLINE_BUTTON_LABEL_LENGTH - 3)}...`;
|
|
41
|
+
}
|
|
42
|
+
function buildCommandsListKeyboard(commands) {
|
|
43
|
+
const keyboard = new InlineKeyboard();
|
|
44
|
+
commands.forEach((command, index) => {
|
|
45
|
+
keyboard
|
|
46
|
+
.text(formatCommandButtonLabel(command), `${COMMANDS_CALLBACK_SELECT_PREFIX}${index}`)
|
|
47
|
+
.row();
|
|
48
|
+
});
|
|
49
|
+
keyboard.text(t("commands.button.cancel"), COMMANDS_CALLBACK_CANCEL);
|
|
50
|
+
return keyboard;
|
|
51
|
+
}
|
|
52
|
+
function buildCommandsConfirmKeyboard() {
|
|
53
|
+
return new InlineKeyboard()
|
|
54
|
+
.text(t("commands.button.execute"), COMMANDS_CALLBACK_EXECUTE)
|
|
55
|
+
.text(t("commands.button.cancel"), COMMANDS_CALLBACK_CANCEL);
|
|
56
|
+
}
|
|
57
|
+
function parseCommandItems(value) {
|
|
58
|
+
if (!Array.isArray(value)) {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
const commands = [];
|
|
62
|
+
for (const item of value) {
|
|
63
|
+
if (!item || typeof item !== "object") {
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
const commandName = item.name;
|
|
67
|
+
if (typeof commandName !== "string" || !commandName.trim()) {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
const description = item.description;
|
|
71
|
+
commands.push({
|
|
72
|
+
name: commandName,
|
|
73
|
+
description: typeof description === "string" ? description : undefined,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
return commands;
|
|
77
|
+
}
|
|
78
|
+
function parseCommandsMetadata(state) {
|
|
79
|
+
if (!state || state.kind !== "custom") {
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
const flow = state.metadata.flow;
|
|
83
|
+
const stage = state.metadata.stage;
|
|
84
|
+
const messageId = state.metadata.messageId;
|
|
85
|
+
const projectDirectory = state.metadata.projectDirectory;
|
|
86
|
+
if (flow !== "commands" ||
|
|
87
|
+
typeof messageId !== "number" ||
|
|
88
|
+
typeof projectDirectory !== "string") {
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
if (stage === "list") {
|
|
92
|
+
const commands = parseCommandItems(state.metadata.commands);
|
|
93
|
+
if (!commands) {
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
return {
|
|
97
|
+
flow,
|
|
98
|
+
stage,
|
|
99
|
+
messageId,
|
|
100
|
+
projectDirectory,
|
|
101
|
+
commands,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
if (stage === "confirm") {
|
|
105
|
+
const commandName = state.metadata.commandName;
|
|
106
|
+
if (typeof commandName !== "string" || !commandName.trim()) {
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
return {
|
|
110
|
+
flow,
|
|
111
|
+
stage,
|
|
112
|
+
messageId,
|
|
113
|
+
projectDirectory,
|
|
114
|
+
commandName,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
function clearCommandsInteraction(reason) {
|
|
120
|
+
const metadata = parseCommandsMetadata(interactionManager.getSnapshot());
|
|
121
|
+
if (metadata) {
|
|
122
|
+
interactionManager.clear(reason);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
async function getCommandList(projectDirectory) {
|
|
126
|
+
const { data, error } = await opencodeClient.command.list({
|
|
127
|
+
directory: normalizeDirectoryForCommandApi(projectDirectory),
|
|
128
|
+
});
|
|
129
|
+
if (error || !data) {
|
|
130
|
+
throw error || new Error("No command data received");
|
|
131
|
+
}
|
|
132
|
+
return data
|
|
133
|
+
.filter((command) => typeof command.name === "string" && command.name.trim().length > 0)
|
|
134
|
+
.map((command) => ({
|
|
135
|
+
name: command.name,
|
|
136
|
+
description: command.description,
|
|
137
|
+
}));
|
|
138
|
+
}
|
|
139
|
+
function parseSelectIndex(data) {
|
|
140
|
+
if (!data.startsWith(COMMANDS_CALLBACK_SELECT_PREFIX)) {
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
143
|
+
const rawIndex = data.slice(COMMANDS_CALLBACK_SELECT_PREFIX.length);
|
|
144
|
+
const index = Number(rawIndex);
|
|
145
|
+
if (!Number.isInteger(index) || index < 0) {
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
return index;
|
|
149
|
+
}
|
|
150
|
+
async function isSessionBusy(sessionId, directory) {
|
|
151
|
+
try {
|
|
152
|
+
const { data, error } = await opencodeClient.session.status({ directory });
|
|
153
|
+
if (error || !data) {
|
|
154
|
+
logger.warn("[Commands] Failed to check session status before command:", error);
|
|
155
|
+
return false;
|
|
156
|
+
}
|
|
157
|
+
const sessionStatus = data[sessionId];
|
|
158
|
+
if (!sessionStatus) {
|
|
159
|
+
return false;
|
|
160
|
+
}
|
|
161
|
+
return sessionStatus.type === "busy";
|
|
162
|
+
}
|
|
163
|
+
catch (err) {
|
|
164
|
+
logger.warn("[Commands] Error checking session status before command:", err);
|
|
165
|
+
return false;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
async function ensureSessionForProject(ctx, projectDirectory) {
|
|
169
|
+
let currentSession = getCurrentSession();
|
|
170
|
+
if (currentSession && currentSession.directory !== projectDirectory) {
|
|
171
|
+
logger.warn(`[Commands] Session/project mismatch detected. sessionDirectory=${currentSession.directory}, projectDirectory=${projectDirectory}. Resetting session context.`);
|
|
172
|
+
clearSession();
|
|
173
|
+
summaryAggregator.clear();
|
|
174
|
+
await ctx.reply(t("bot.session_reset_project_mismatch"));
|
|
175
|
+
currentSession = null;
|
|
176
|
+
}
|
|
177
|
+
if (currentSession) {
|
|
178
|
+
return currentSession;
|
|
179
|
+
}
|
|
180
|
+
await ctx.reply(t("bot.creating_session"));
|
|
181
|
+
const { data: session, error } = await opencodeClient.session.create({
|
|
182
|
+
directory: projectDirectory,
|
|
183
|
+
});
|
|
184
|
+
if (error || !session) {
|
|
185
|
+
await ctx.reply(t("bot.create_session_error"));
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
188
|
+
const sessionInfo = {
|
|
189
|
+
id: session.id,
|
|
190
|
+
title: session.title,
|
|
191
|
+
directory: projectDirectory,
|
|
192
|
+
};
|
|
193
|
+
setCurrentSession(sessionInfo);
|
|
194
|
+
await ingestSessionInfoForCache(session);
|
|
195
|
+
await ctx.reply(t("bot.session_created", { title: session.title }));
|
|
196
|
+
return sessionInfo;
|
|
197
|
+
}
|
|
198
|
+
async function executeCommand(ctx, deps, params) {
|
|
199
|
+
if (!ctx.chat) {
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
const args = params.argumentsText.trim();
|
|
203
|
+
await ctx.reply(formatExecutingCommandMessage(params.commandName, args));
|
|
204
|
+
const session = await ensureSessionForProject(ctx, params.projectDirectory);
|
|
205
|
+
if (!session) {
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
await deps.ensureEventSubscription(session.directory);
|
|
209
|
+
summaryAggregator.setSession(session.id);
|
|
210
|
+
summaryAggregator.setBotAndChatId(deps.bot, ctx.chat.id);
|
|
211
|
+
const sessionIsBusy = await isSessionBusy(session.id, session.directory);
|
|
212
|
+
if (sessionIsBusy) {
|
|
213
|
+
await ctx.reply(t("bot.session_busy"));
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
const currentAgent = getStoredAgent();
|
|
217
|
+
const storedModel = getStoredModel();
|
|
218
|
+
const model = storedModel.providerID && storedModel.modelID
|
|
219
|
+
? `${storedModel.providerID}/${storedModel.modelID}`
|
|
220
|
+
: undefined;
|
|
221
|
+
safeBackgroundTask({
|
|
222
|
+
taskName: "session.command",
|
|
223
|
+
task: () => opencodeClient.session.command({
|
|
224
|
+
sessionID: session.id,
|
|
225
|
+
directory: session.directory,
|
|
226
|
+
command: params.commandName,
|
|
227
|
+
arguments: args,
|
|
228
|
+
agent: currentAgent,
|
|
229
|
+
model,
|
|
230
|
+
variant: storedModel.variant,
|
|
231
|
+
}),
|
|
232
|
+
onSuccess: ({ error }) => {
|
|
233
|
+
if (error) {
|
|
234
|
+
logger.error("[Commands] OpenCode API returned an error for session.command", {
|
|
235
|
+
sessionId: session.id,
|
|
236
|
+
command: params.commandName,
|
|
237
|
+
args,
|
|
238
|
+
});
|
|
239
|
+
logger.error("[Commands] session.command error details:", error);
|
|
240
|
+
void ctx.api.sendMessage(ctx.chat.id, t("commands.execute_error")).catch(() => { });
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
logger.info(`[Commands] session.command completed: session=${session.id}, command=/${params.commandName}`);
|
|
244
|
+
},
|
|
245
|
+
onError: (error) => {
|
|
246
|
+
logger.error("[Commands] session.command background task failed", {
|
|
247
|
+
sessionId: session.id,
|
|
248
|
+
command: params.commandName,
|
|
249
|
+
args,
|
|
250
|
+
});
|
|
251
|
+
logger.error("[Commands] session.command background failure details:", error);
|
|
252
|
+
void ctx.api.sendMessage(ctx.chat.id, t("commands.execute_error")).catch(() => { });
|
|
253
|
+
},
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
export async function commandsCommand(ctx) {
|
|
257
|
+
try {
|
|
258
|
+
const currentProject = getCurrentProject();
|
|
259
|
+
if (!currentProject) {
|
|
260
|
+
await ctx.reply(t("bot.project_not_selected"));
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
const commands = await getCommandList(currentProject.worktree);
|
|
264
|
+
if (commands.length === 0) {
|
|
265
|
+
await ctx.reply(t("commands.empty"));
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
const keyboard = buildCommandsListKeyboard(commands);
|
|
269
|
+
const message = await ctx.reply(t("commands.select"), {
|
|
270
|
+
reply_markup: keyboard,
|
|
271
|
+
});
|
|
272
|
+
interactionManager.start({
|
|
273
|
+
kind: "custom",
|
|
274
|
+
expectedInput: "callback",
|
|
275
|
+
metadata: {
|
|
276
|
+
flow: "commands",
|
|
277
|
+
stage: "list",
|
|
278
|
+
messageId: message.message_id,
|
|
279
|
+
projectDirectory: currentProject.worktree,
|
|
280
|
+
commands,
|
|
281
|
+
},
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
catch (error) {
|
|
285
|
+
logger.error("[Commands] Error fetching commands list:", error);
|
|
286
|
+
await ctx.reply(t("commands.fetch_error"));
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
export async function handleCommandsCallback(ctx, deps) {
|
|
290
|
+
const data = ctx.callbackQuery?.data;
|
|
291
|
+
if (!data || !data.startsWith(COMMANDS_CALLBACK_PREFIX)) {
|
|
292
|
+
return false;
|
|
293
|
+
}
|
|
294
|
+
const metadata = parseCommandsMetadata(interactionManager.getSnapshot());
|
|
295
|
+
const callbackMessageId = getCallbackMessageId(ctx);
|
|
296
|
+
if (!metadata || callbackMessageId === null || metadata.messageId !== callbackMessageId) {
|
|
297
|
+
await ctx.answerCallbackQuery({ text: t("commands.inactive_callback"), show_alert: true });
|
|
298
|
+
return true;
|
|
299
|
+
}
|
|
300
|
+
try {
|
|
301
|
+
if (data === COMMANDS_CALLBACK_CANCEL) {
|
|
302
|
+
clearCommandsInteraction("commands_cancelled");
|
|
303
|
+
await ctx.answerCallbackQuery({ text: t("commands.cancelled_callback") });
|
|
304
|
+
await ctx.deleteMessage().catch(() => { });
|
|
305
|
+
return true;
|
|
306
|
+
}
|
|
307
|
+
if (data === COMMANDS_CALLBACK_EXECUTE) {
|
|
308
|
+
if (metadata.stage !== "confirm") {
|
|
309
|
+
await ctx.answerCallbackQuery({ text: t("commands.inactive_callback"), show_alert: true });
|
|
310
|
+
return true;
|
|
311
|
+
}
|
|
312
|
+
clearCommandsInteraction("commands_execute_clicked");
|
|
313
|
+
await ctx.answerCallbackQuery({ text: t("commands.execute_callback") });
|
|
314
|
+
await ctx.deleteMessage().catch(() => { });
|
|
315
|
+
await executeCommand(ctx, deps, {
|
|
316
|
+
projectDirectory: metadata.projectDirectory,
|
|
317
|
+
commandName: metadata.commandName,
|
|
318
|
+
argumentsText: "",
|
|
319
|
+
});
|
|
320
|
+
return true;
|
|
321
|
+
}
|
|
322
|
+
const commandIndex = parseSelectIndex(data);
|
|
323
|
+
if (commandIndex === null || metadata.stage !== "list") {
|
|
324
|
+
await ctx.answerCallbackQuery({ text: t("callback.processing_error"), show_alert: true });
|
|
325
|
+
return true;
|
|
326
|
+
}
|
|
327
|
+
const selectedCommand = metadata.commands[commandIndex];
|
|
328
|
+
if (!selectedCommand) {
|
|
329
|
+
await ctx.answerCallbackQuery({ text: t("commands.inactive_callback"), show_alert: true });
|
|
330
|
+
return true;
|
|
331
|
+
}
|
|
332
|
+
await ctx.answerCallbackQuery();
|
|
333
|
+
await ctx.editMessageText(t("commands.confirm", { command: `/${selectedCommand.name}` }), {
|
|
334
|
+
reply_markup: buildCommandsConfirmKeyboard(),
|
|
335
|
+
});
|
|
336
|
+
interactionManager.transition({
|
|
337
|
+
expectedInput: "mixed",
|
|
338
|
+
metadata: {
|
|
339
|
+
flow: "commands",
|
|
340
|
+
stage: "confirm",
|
|
341
|
+
messageId: metadata.messageId,
|
|
342
|
+
projectDirectory: metadata.projectDirectory,
|
|
343
|
+
commandName: selectedCommand.name,
|
|
344
|
+
},
|
|
345
|
+
});
|
|
346
|
+
return true;
|
|
347
|
+
}
|
|
348
|
+
catch (error) {
|
|
349
|
+
logger.error("[Commands] Error handling command callback:", error);
|
|
350
|
+
clearCommandsInteraction("commands_callback_error");
|
|
351
|
+
await ctx.answerCallbackQuery({ text: t("callback.processing_error") }).catch(() => { });
|
|
352
|
+
return true;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
export async function handleCommandTextArguments(ctx, deps) {
|
|
356
|
+
const text = ctx.message?.text;
|
|
357
|
+
if (!text || text.startsWith("/")) {
|
|
358
|
+
return false;
|
|
359
|
+
}
|
|
360
|
+
const metadata = parseCommandsMetadata(interactionManager.getSnapshot());
|
|
361
|
+
if (!metadata || metadata.stage !== "confirm") {
|
|
362
|
+
return false;
|
|
363
|
+
}
|
|
364
|
+
const argumentsText = text.trim();
|
|
365
|
+
if (!argumentsText) {
|
|
366
|
+
await ctx.reply(t("commands.arguments_empty"));
|
|
367
|
+
return true;
|
|
368
|
+
}
|
|
369
|
+
clearCommandsInteraction("commands_arguments_submitted");
|
|
370
|
+
if (ctx.chat) {
|
|
371
|
+
await ctx.api.deleteMessage(ctx.chat.id, metadata.messageId).catch(() => { });
|
|
372
|
+
}
|
|
373
|
+
await executeCommand(ctx, deps, {
|
|
374
|
+
projectDirectory: metadata.projectDirectory,
|
|
375
|
+
commandName: metadata.commandName,
|
|
376
|
+
argumentsText,
|
|
377
|
+
});
|
|
378
|
+
return true;
|
|
379
|
+
}
|
|
@@ -10,6 +10,7 @@ const COMMAND_DEFINITIONS = [
|
|
|
10
10
|
{ command: "sessions", descriptionKey: "cmd.description.sessions" },
|
|
11
11
|
{ command: "projects", descriptionKey: "cmd.description.projects" },
|
|
12
12
|
{ command: "rename", descriptionKey: "cmd.description.rename" },
|
|
13
|
+
{ command: "commands", descriptionKey: "cmd.description.commands" },
|
|
13
14
|
{ command: "opencode_start", descriptionKey: "cmd.description.opencode_start" },
|
|
14
15
|
{ command: "opencode_stop", descriptionKey: "cmd.description.opencode_stop" },
|
|
15
16
|
{ command: "help", descriptionKey: "cmd.description.help" },
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { config } from "../../config.js";
|
|
2
|
+
import { processUserPrompt } from "./prompt.js";
|
|
3
|
+
import { downloadTelegramFile, toDataUri, isTextMimeType, isFileSizeAllowed, } from "../utils/file-download.js";
|
|
4
|
+
import { getModelCapabilities, supportsInput } from "../../model/capabilities.js";
|
|
5
|
+
import { getStoredModel } from "../../model/manager.js";
|
|
6
|
+
import { logger } from "../../utils/logger.js";
|
|
7
|
+
import { t } from "../../i18n/index.js";
|
|
8
|
+
export async function handleDocumentMessage(ctx, deps) {
|
|
9
|
+
const downloadFile = deps.downloadFile ?? downloadTelegramFile;
|
|
10
|
+
const getCapabilities = deps.getModelCapabilities ?? getModelCapabilities;
|
|
11
|
+
const getStored = deps.getStoredModel ?? getStoredModel;
|
|
12
|
+
const processPrompt = deps.processPrompt ?? processUserPrompt;
|
|
13
|
+
const doc = ctx.message?.document;
|
|
14
|
+
if (!doc) {
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
const caption = ctx.message.caption || "";
|
|
18
|
+
const mimeType = doc.mime_type || "";
|
|
19
|
+
const filename = doc.file_name || "document";
|
|
20
|
+
try {
|
|
21
|
+
if (isTextMimeType(mimeType)) {
|
|
22
|
+
if (!isFileSizeAllowed(doc.file_size, config.files.maxFileSizeKb)) {
|
|
23
|
+
logger.warn(`[Document] Text file too large: ${filename} (${doc.file_size} bytes > ${config.files.maxFileSizeKb}KB)`);
|
|
24
|
+
await ctx.reply(t("bot.text_file_too_large", { maxSizeKb: String(config.files.maxFileSizeKb) }));
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
await ctx.reply(t("bot.file_downloading"));
|
|
28
|
+
const downloadedFile = await downloadFile(ctx.api, doc.file_id);
|
|
29
|
+
const textContent = downloadedFile.buffer.toString("utf-8");
|
|
30
|
+
const promptWithFile = `--- Content of ${filename} ---\n${textContent}\n--- End of file ---\n\n${caption}`;
|
|
31
|
+
logger.info(`[Document] Sending text file (${downloadedFile.buffer.length} bytes, ${filename}) as prompt`);
|
|
32
|
+
await processPrompt(ctx, promptWithFile, deps);
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
if (mimeType === "application/pdf") {
|
|
36
|
+
const storedModel = getStored();
|
|
37
|
+
const capabilities = await getCapabilities(storedModel.providerID, storedModel.modelID);
|
|
38
|
+
if (!supportsInput(capabilities, "pdf")) {
|
|
39
|
+
logger.warn(`[Document] Model ${storedModel.providerID}/${storedModel.modelID} doesn't support PDF input`);
|
|
40
|
+
await ctx.reply(t("bot.model_no_pdf"));
|
|
41
|
+
if (caption.trim().length > 0) {
|
|
42
|
+
await processPrompt(ctx, caption, deps);
|
|
43
|
+
}
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
await ctx.reply(t("bot.file_downloading"));
|
|
47
|
+
const downloadedFile = await downloadFile(ctx.api, doc.file_id);
|
|
48
|
+
const dataUri = toDataUri(downloadedFile.buffer, mimeType);
|
|
49
|
+
const filePart = {
|
|
50
|
+
type: "file",
|
|
51
|
+
mime: mimeType,
|
|
52
|
+
filename: filename,
|
|
53
|
+
url: dataUri,
|
|
54
|
+
};
|
|
55
|
+
logger.info(`[Document] Sending PDF (${downloadedFile.buffer.length} bytes, ${filename}) with prompt`);
|
|
56
|
+
await processPrompt(ctx, caption, deps, [filePart]);
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
logger.debug(`[Document] Unsupported document MIME type: ${mimeType}, ignoring`);
|
|
60
|
+
}
|
|
61
|
+
catch (err) {
|
|
62
|
+
logger.error("[Document] Error handling document message:", err);
|
|
63
|
+
await ctx.reply(t("bot.file_download_error"));
|
|
64
|
+
}
|
|
65
|
+
}
|
package/dist/bot/index.js
CHANGED
|
@@ -12,7 +12,7 @@ import { BOT_COMMANDS } from "./commands/definitions.js";
|
|
|
12
12
|
import { startCommand } from "./commands/start.js";
|
|
13
13
|
import { helpCommand } from "./commands/help.js";
|
|
14
14
|
import { statusCommand } from "./commands/status.js";
|
|
15
|
-
import { MODEL_BUTTON_TEXT_PATTERN, VARIANT_BUTTON_TEXT_PATTERN } from "./message-patterns.js";
|
|
15
|
+
import { AGENT_MODE_BUTTON_TEXT_PATTERN, MODEL_BUTTON_TEXT_PATTERN, VARIANT_BUTTON_TEXT_PATTERN, } from "./message-patterns.js";
|
|
16
16
|
import { sessionsCommand, handleSessionSelect } from "./commands/sessions.js";
|
|
17
17
|
import { newCommand } from "./commands/new.js";
|
|
18
18
|
import { projectsCommand, handleProjectSelect } from "./commands/projects.js";
|
|
@@ -20,6 +20,7 @@ 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
22
|
import { renameCommand, handleRenameCancel, handleRenameTextAnswer } from "./commands/rename.js";
|
|
23
|
+
import { commandsCommand, handleCommandsCallback, handleCommandTextArguments, } from "./commands/commands.js";
|
|
23
24
|
import { handleQuestionCallback, showCurrentQuestion, handleQuestionTextAnswer, } from "./handlers/question.js";
|
|
24
25
|
import { handlePermissionCallback, showPermissionRequest } from "./handlers/permission.js";
|
|
25
26
|
import { handleAgentSelect, showAgentSelectionMenu } from "./handlers/agent.js";
|
|
@@ -43,6 +44,7 @@ import { pinnedMessageManager } from "../pinned/manager.js";
|
|
|
43
44
|
import { t } from "../i18n/index.js";
|
|
44
45
|
import { processUserPrompt } from "./handlers/prompt.js";
|
|
45
46
|
import { handleVoiceMessage } from "./handlers/voice.js";
|
|
47
|
+
import { handleDocumentMessage } from "./handlers/document.js";
|
|
46
48
|
import { downloadTelegramFile, toDataUri } from "./utils/file-download.js";
|
|
47
49
|
import { sendMessageWithMarkdownFallback } from "./utils/send-with-markdown-fallback.js";
|
|
48
50
|
import { getModelCapabilities, supportsInput } from "../model/capabilities.js";
|
|
@@ -456,10 +458,15 @@ export function createBot() {
|
|
|
456
458
|
bot.command("new", newCommand);
|
|
457
459
|
bot.command("stop", stopCommand);
|
|
458
460
|
bot.command("rename", renameCommand);
|
|
461
|
+
bot.command("commands", commandsCommand);
|
|
459
462
|
bot.on("message:text", unknownCommandMiddleware);
|
|
460
463
|
bot.on("callback_query:data", async (ctx) => {
|
|
461
464
|
logger.debug(`[Bot] Received callback_query:data: ${ctx.callbackQuery?.data}`);
|
|
462
465
|
logger.debug(`[Bot] Callback context: from=${ctx.from?.id}, chat=${ctx.chat?.id}`);
|
|
466
|
+
if (ctx.chat) {
|
|
467
|
+
botInstance = bot;
|
|
468
|
+
chatIdInstance = ctx.chat.id;
|
|
469
|
+
}
|
|
463
470
|
try {
|
|
464
471
|
const handledInlineCancel = await handleInlineMenuCancel(ctx);
|
|
465
472
|
const handledSession = await handleSessionSelect(ctx);
|
|
@@ -471,7 +478,8 @@ export function createBot() {
|
|
|
471
478
|
const handledVariant = await handleVariantSelect(ctx);
|
|
472
479
|
const handledCompactConfirm = await handleCompactConfirm(ctx);
|
|
473
480
|
const handledRenameCancel = await handleRenameCancel(ctx);
|
|
474
|
-
|
|
481
|
+
const handledCommands = await handleCommandsCallback(ctx, { bot, ensureEventSubscription });
|
|
482
|
+
logger.debug(`[Bot] Callback handled: inlineCancel=${handledInlineCancel}, session=${handledSession}, project=${handledProject}, question=${handledQuestion}, permission=${handledPermission}, agent=${handledAgent}, model=${handledModel}, variant=${handledVariant}, compactConfirm=${handledCompactConfirm}, rename=${handledRenameCancel}, commands=${handledCommands}`);
|
|
475
483
|
if (!handledInlineCancel &&
|
|
476
484
|
!handledSession &&
|
|
477
485
|
!handledProject &&
|
|
@@ -481,7 +489,8 @@ export function createBot() {
|
|
|
481
489
|
!handledModel &&
|
|
482
490
|
!handledVariant &&
|
|
483
491
|
!handledCompactConfirm &&
|
|
484
|
-
!handledRenameCancel
|
|
492
|
+
!handledRenameCancel &&
|
|
493
|
+
!handledCommands) {
|
|
485
494
|
logger.debug("Unknown callback query:", ctx.callbackQuery?.data);
|
|
486
495
|
await ctx.answerCallbackQuery({ text: t("callback.unknown_command") });
|
|
487
496
|
}
|
|
@@ -493,7 +502,7 @@ export function createBot() {
|
|
|
493
502
|
}
|
|
494
503
|
});
|
|
495
504
|
// Handle Reply Keyboard button press (agent mode indicator)
|
|
496
|
-
bot.hears(
|
|
505
|
+
bot.hears(AGENT_MODE_BUTTON_TEXT_PATTERN, async (ctx) => {
|
|
497
506
|
logger.debug(`[Bot] Agent mode button pressed: ${ctx.message?.text}`);
|
|
498
507
|
try {
|
|
499
508
|
if (await blockMenuWhileInteractionActive(ctx)) {
|
|
@@ -645,11 +654,21 @@ export function createBot() {
|
|
|
645
654
|
await ctx.reply(t("bot.photo_download_error"));
|
|
646
655
|
}
|
|
647
656
|
});
|
|
657
|
+
// Document message handler (PDF and text files)
|
|
658
|
+
bot.on("message:document", async (ctx) => {
|
|
659
|
+
logger.debug(`[Bot] Received document message, chatId=${ctx.chat.id}`);
|
|
660
|
+
botInstance = bot;
|
|
661
|
+
chatIdInstance = ctx.chat.id;
|
|
662
|
+
const deps = { bot, ensureEventSubscription };
|
|
663
|
+
await handleDocumentMessage(ctx, deps);
|
|
664
|
+
});
|
|
648
665
|
bot.on("message:text", async (ctx) => {
|
|
649
666
|
const text = ctx.message?.text;
|
|
650
667
|
if (!text) {
|
|
651
668
|
return;
|
|
652
669
|
}
|
|
670
|
+
botInstance = bot;
|
|
671
|
+
chatIdInstance = ctx.chat.id;
|
|
653
672
|
if (text.startsWith("/")) {
|
|
654
673
|
return;
|
|
655
674
|
}
|
|
@@ -661,9 +680,11 @@ export function createBot() {
|
|
|
661
680
|
if (handledRename) {
|
|
662
681
|
return;
|
|
663
682
|
}
|
|
664
|
-
botInstance = bot;
|
|
665
|
-
chatIdInstance = ctx.chat.id;
|
|
666
683
|
const promptDeps = { bot, ensureEventSubscription };
|
|
684
|
+
const handledCommandArgs = await handleCommandTextArguments(ctx, promptDeps);
|
|
685
|
+
if (handledCommandArgs) {
|
|
686
|
+
return;
|
|
687
|
+
}
|
|
667
688
|
await processUserPrompt(ctx, text, promptDeps);
|
|
668
689
|
logger.debug("[Bot] message:text handler completed (prompt sent in background)");
|
|
669
690
|
});
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
-
export const
|
|
1
|
+
export const AGENT_MODE_BUTTON_TEXT_PATTERN = /^(📋|🛠️|💬|🔍|📝|📄|📦|🤖)\s.+\sMode$/;
|
|
2
|
+
export const MODEL_BUTTON_TEXT_PATTERN = /^🤖\s(?!.*\sMode$)[\s\S]+$/;
|
|
2
3
|
// Keep support for both legacy "💭" and current "💡" prefix.
|
|
3
4
|
export const VARIANT_BUTTON_TEXT_PATTERN = /^(💡|💭)\s.+$/;
|
|
@@ -73,3 +73,19 @@ export function formatFileSize(bytes) {
|
|
|
73
73
|
}
|
|
74
74
|
return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
|
|
75
75
|
}
|
|
76
|
+
const APPLICATION_TEXT_MIME_TYPES = new Set([
|
|
77
|
+
"application/json",
|
|
78
|
+
"application/xml",
|
|
79
|
+
"application/javascript",
|
|
80
|
+
"application/x-yaml",
|
|
81
|
+
"application/sql",
|
|
82
|
+
]);
|
|
83
|
+
export function isTextMimeType(mimeType) {
|
|
84
|
+
if (!mimeType) {
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
if (mimeType.startsWith("text/")) {
|
|
88
|
+
return true;
|
|
89
|
+
}
|
|
90
|
+
return APPLICATION_TEXT_MIME_TYPES.has(mimeType);
|
|
91
|
+
}
|
package/dist/i18n/de.js
CHANGED
|
@@ -4,6 +4,7 @@ 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.commands": "Benutzerdefinierte Befehle",
|
|
7
8
|
"cmd.description.opencode_start": "OpenCode-Server starten",
|
|
8
9
|
"cmd.description.opencode_stop": "OpenCode-Server stoppen",
|
|
9
10
|
"cmd.description.help": "Hilfe",
|
|
@@ -48,6 +49,11 @@ export const de = {
|
|
|
48
49
|
"bot.photo_model_no_image": "⚠️ Das aktuelle Modell unterstützt keine Bildeingabe. Sende nur Text.",
|
|
49
50
|
"bot.photo_download_error": "🔴 Foto konnte nicht heruntergeladen werden",
|
|
50
51
|
"bot.photo_no_caption": "💡 Tipp: Füge eine Bildunterschrift hinzu, um zu beschreiben, was du mit diesem Foto tun möchtest.",
|
|
52
|
+
"bot.file_downloading": "⏳ Lade Datei herunter...",
|
|
53
|
+
"bot.file_too_large": "⚠️ Datei ist zu groß (max. {maxSizeMb}MB)",
|
|
54
|
+
"bot.file_download_error": "🔴 Datei konnte nicht heruntergeladen werden",
|
|
55
|
+
"bot.model_no_pdf": "⚠️ Das aktuelle Modell unterstützt keine PDF-Eingabe. Sende nur Text.",
|
|
56
|
+
"bot.text_file_too_large": "⚠️ Textdatei ist zu groß (max. {maxSizeKb}KB)",
|
|
51
57
|
"status.header_running": "🟢 **OpenCode-Server läuft**",
|
|
52
58
|
"status.health.healthy": "OK",
|
|
53
59
|
"status.health.unhealthy": "Nicht OK",
|
|
@@ -238,6 +244,19 @@ export const de = {
|
|
|
238
244
|
"rename.blocked.expected_name": "⚠️ Sende den neuen Sitzungsnamen als Text oder tippe in der Umbenennen-Nachricht auf Abbrechen.",
|
|
239
245
|
"rename.blocked.command_not_allowed": "⚠️ Dieser Befehl ist nicht verfügbar, solange beim Umbenennen auf einen neuen Namen gewartet wird.",
|
|
240
246
|
"rename.button.cancel": "❌ Abbrechen",
|
|
247
|
+
"commands.select": "Wähle einen OpenCode-Befehl:",
|
|
248
|
+
"commands.empty": "📭 Für dieses Projekt sind keine OpenCode-Befehle verfügbar.",
|
|
249
|
+
"commands.fetch_error": "🔴 OpenCode-Befehle konnten nicht geladen werden.",
|
|
250
|
+
"commands.no_description": "Keine Beschreibung",
|
|
251
|
+
"commands.button.execute": "✅ Ausführen",
|
|
252
|
+
"commands.button.cancel": "❌ Abbrechen",
|
|
253
|
+
"commands.confirm": "Bestätige die Ausführung des Befehls {command}. Für die Ausführung mit Argumenten sende die Argumente als Nachricht.",
|
|
254
|
+
"commands.inactive_callback": "Dieses Befehlsmenü ist inaktiv",
|
|
255
|
+
"commands.cancelled_callback": "Abgebrochen",
|
|
256
|
+
"commands.execute_callback": "Befehl wird ausgeführt...",
|
|
257
|
+
"commands.executing_prefix": "⚡ Befehl wird ausgeführt:",
|
|
258
|
+
"commands.arguments_empty": "⚠️ Argumente dürfen nicht leer sein. Sende Text oder tippe auf Ausführen.",
|
|
259
|
+
"commands.execute_error": "🔴 OpenCode-Befehl konnte nicht ausgeführt werden.",
|
|
241
260
|
"cmd.description.rename": "Aktuelle Sitzung umbenennen",
|
|
242
261
|
"cli.usage": "Verwendung:\n opencode-telegram [start] [--mode sources|installed]\n opencode-telegram status\n opencode-telegram stop\n opencode-telegram config\n\nHinweise:\n - Ohne Befehl wird standardmäßig `start` verwendet\n - `--mode` wird derzeit nur für `start` unterstützt",
|
|
243
262
|
"cli.placeholder.status": "Befehl `status` ist derzeit ein Platzhalter. Echte Statusprüfungen werden in der Service-Schicht hinzugefügt (Phase 5).",
|
package/dist/i18n/en.js
CHANGED
|
@@ -4,6 +4,7 @@ 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.commands": "Custom commands",
|
|
7
8
|
"cmd.description.opencode_start": "Start OpenCode server",
|
|
8
9
|
"cmd.description.opencode_stop": "Stop OpenCode server",
|
|
9
10
|
"cmd.description.help": "Help",
|
|
@@ -48,6 +49,11 @@ export const en = {
|
|
|
48
49
|
"bot.photo_model_no_image": "⚠️ Current model doesn't support image input. Sending text only.",
|
|
49
50
|
"bot.photo_download_error": "🔴 Failed to download photo",
|
|
50
51
|
"bot.photo_no_caption": "💡 Tip: Add a caption to describe what you want to do with this photo.",
|
|
52
|
+
"bot.file_downloading": "⏳ Downloading file...",
|
|
53
|
+
"bot.file_too_large": "⚠️ File is too large (max {maxSizeMb}MB)",
|
|
54
|
+
"bot.file_download_error": "🔴 Failed to download file",
|
|
55
|
+
"bot.model_no_pdf": "⚠️ Current model doesn't support PDF input. Sending text only.",
|
|
56
|
+
"bot.text_file_too_large": "⚠️ Text file is too large (max {maxSizeKb}KB)",
|
|
51
57
|
"status.header_running": "🟢 **OpenCode Server is running**",
|
|
52
58
|
"status.health.healthy": "Healthy",
|
|
53
59
|
"status.health.unhealthy": "Unhealthy",
|
|
@@ -238,6 +244,19 @@ export const en = {
|
|
|
238
244
|
"rename.blocked.expected_name": "⚠️ Enter a new session name as text or tap Cancel in rename message.",
|
|
239
245
|
"rename.blocked.command_not_allowed": "⚠️ This command is not available while rename is waiting for a new name.",
|
|
240
246
|
"rename.button.cancel": "❌ Cancel",
|
|
247
|
+
"commands.select": "Choose an OpenCode command:",
|
|
248
|
+
"commands.empty": "📭 No OpenCode commands are available for this project.",
|
|
249
|
+
"commands.fetch_error": "🔴 Failed to load OpenCode commands.",
|
|
250
|
+
"commands.no_description": "No description",
|
|
251
|
+
"commands.button.execute": "✅ Execute",
|
|
252
|
+
"commands.button.cancel": "❌ Cancel",
|
|
253
|
+
"commands.confirm": "Confirm execution of command {command}. To run it with arguments, send the arguments as a message.",
|
|
254
|
+
"commands.inactive_callback": "This command menu is inactive",
|
|
255
|
+
"commands.cancelled_callback": "Cancelled",
|
|
256
|
+
"commands.execute_callback": "Executing command...",
|
|
257
|
+
"commands.executing_prefix": "⚡ Executing command:",
|
|
258
|
+
"commands.arguments_empty": "⚠️ Arguments cannot be empty. Send text or tap Execute.",
|
|
259
|
+
"commands.execute_error": "🔴 Failed to execute OpenCode command.",
|
|
241
260
|
"cmd.description.rename": "Rename current session",
|
|
242
261
|
"cli.usage": "Usage:\n opencode-telegram [start] [--mode sources|installed]\n opencode-telegram status\n opencode-telegram stop\n opencode-telegram config\n\nNotes:\n - No command defaults to `start`\n - `--mode` is currently supported for `start` only",
|
|
243
262
|
"cli.placeholder.status": "Command `status` is currently a placeholder. Real status checks will be added in service layer (Phase 5).",
|
package/dist/i18n/es.js
CHANGED
|
@@ -4,6 +4,7 @@ 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.commands": "Comandos personalizados",
|
|
7
8
|
"cmd.description.opencode_start": "Iniciar servidor OpenCode",
|
|
8
9
|
"cmd.description.opencode_stop": "Detener servidor OpenCode",
|
|
9
10
|
"cmd.description.help": "Ayuda",
|
|
@@ -48,6 +49,11 @@ export const es = {
|
|
|
48
49
|
"bot.photo_model_no_image": "⚠️ El modelo actual no admite entrada de imagen. Enviaré solo texto.",
|
|
49
50
|
"bot.photo_download_error": "🔴 No se pudo descargar la foto",
|
|
50
51
|
"bot.photo_no_caption": "💡 Consejo: agrega un pie de foto para describir que quieres hacer con esta foto.",
|
|
52
|
+
"bot.file_downloading": "⏳ Descargando archivo...",
|
|
53
|
+
"bot.file_too_large": "⚠️ El archivo es demasiado grande (max {maxSizeMb}MB)",
|
|
54
|
+
"bot.file_download_error": "🔴 No se pudo descargar el archivo",
|
|
55
|
+
"bot.model_no_pdf": "⚠️ El modelo actual no admite entrada PDF. Enviaré solo texto.",
|
|
56
|
+
"bot.text_file_too_large": "⚠️ El archivo de texto es demasiado grande (max {maxSizeKb}KB)",
|
|
51
57
|
"status.header_running": "🟢 **OpenCode Server está en ejecución**",
|
|
52
58
|
"status.health.healthy": "Saludable",
|
|
53
59
|
"status.health.unhealthy": "No saludable",
|
|
@@ -238,6 +244,19 @@ export const es = {
|
|
|
238
244
|
"rename.blocked.expected_name": "⚠️ Introduce el nuevo nombre de la sesión como texto o toca Cancelar en el mensaje de cambio de nombre.",
|
|
239
245
|
"rename.blocked.command_not_allowed": "⚠️ Este comando no está disponible mientras el cambio de nombre espera un nuevo nombre.",
|
|
240
246
|
"rename.button.cancel": "❌ Cancelar",
|
|
247
|
+
"commands.select": "Elige un comando de OpenCode:",
|
|
248
|
+
"commands.empty": "📭 No hay comandos de OpenCode disponibles para este proyecto.",
|
|
249
|
+
"commands.fetch_error": "🔴 No se pudieron cargar los comandos de OpenCode.",
|
|
250
|
+
"commands.no_description": "Sin descripción",
|
|
251
|
+
"commands.button.execute": "✅ Ejecutar",
|
|
252
|
+
"commands.button.cancel": "❌ Cancelar",
|
|
253
|
+
"commands.confirm": "Confirma la ejecución del comando {command}. Para ejecutarlo con argumentos, envía los argumentos como mensaje.",
|
|
254
|
+
"commands.inactive_callback": "Este menú de comandos está inactivo",
|
|
255
|
+
"commands.cancelled_callback": "Cancelado",
|
|
256
|
+
"commands.execute_callback": "Ejecutando comando...",
|
|
257
|
+
"commands.executing_prefix": "⚡ Ejecutando comando:",
|
|
258
|
+
"commands.arguments_empty": "⚠️ Los argumentos no pueden estar vacíos. Envía texto o toca Ejecutar.",
|
|
259
|
+
"commands.execute_error": "🔴 No se pudo ejecutar el comando de OpenCode.",
|
|
241
260
|
"cmd.description.rename": "Renombrar la sesión actual",
|
|
242
261
|
"cli.usage": "Uso:\n opencode-telegram [start] [--mode sources|installed]\n opencode-telegram status\n opencode-telegram stop\n opencode-telegram config\n\nNotas:\n - Sin comando, el valor por defecto es `start`\n - `--mode` actualmente solo se admite para `start`",
|
|
243
262
|
"cli.placeholder.status": "El comando `status` es actualmente un marcador de posición. Las comprobaciones reales de estado se agregarán en la capa de servicio (Fase 5).",
|
package/dist/i18n/ru.js
CHANGED
|
@@ -4,6 +4,7 @@ export const ru = {
|
|
|
4
4
|
"cmd.description.stop": "Прервать текущее действие",
|
|
5
5
|
"cmd.description.sessions": "Список сессий",
|
|
6
6
|
"cmd.description.projects": "Список проектов",
|
|
7
|
+
"cmd.description.commands": "Пользовательские команды",
|
|
7
8
|
"cmd.description.opencode_start": "Запустить OpenCode сервер",
|
|
8
9
|
"cmd.description.opencode_stop": "Остановить OpenCode сервер",
|
|
9
10
|
"cmd.description.help": "Справка",
|
|
@@ -48,6 +49,11 @@ export const ru = {
|
|
|
48
49
|
"bot.photo_model_no_image": "⚠️ Текущая модель не поддерживает изображения. Отправляю только текст.",
|
|
49
50
|
"bot.photo_download_error": "🔴 Не удалось скачать фото",
|
|
50
51
|
"bot.photo_no_caption": "💡 Совет: Добавьте подпись, чтобы описать, что делать с этим фото.",
|
|
52
|
+
"bot.file_downloading": "⏳ Скачиваю файл...",
|
|
53
|
+
"bot.file_too_large": "⚠️ Файл слишком большой (макс. {maxSizeMb}МБ)",
|
|
54
|
+
"bot.file_download_error": "🔴 Не удалось скачать файл",
|
|
55
|
+
"bot.model_no_pdf": "⚠️ Текущая модель не поддерживает PDF. Отправляю только текст.",
|
|
56
|
+
"bot.text_file_too_large": "⚠️ Текстовый файл слишком большой (макс. {maxSizeKb}КБ)",
|
|
51
57
|
"status.header_running": "🟢 **OpenCode Server запущен**",
|
|
52
58
|
"status.health.healthy": "Healthy",
|
|
53
59
|
"status.health.unhealthy": "Unhealthy",
|
|
@@ -238,6 +244,19 @@ export const ru = {
|
|
|
238
244
|
"rename.blocked.expected_name": "⚠️ Введите новое название текстом или нажмите Отмена в сообщении переименования.",
|
|
239
245
|
"rename.blocked.command_not_allowed": "⚠️ Эта команда недоступна, пока ожидается новое название сессии.",
|
|
240
246
|
"rename.button.cancel": "❌ Отмена",
|
|
247
|
+
"commands.select": "Выберите команду OpenCode:",
|
|
248
|
+
"commands.empty": "📭 Для этого проекта нет доступных команд OpenCode.",
|
|
249
|
+
"commands.fetch_error": "🔴 Не удалось загрузить список команд OpenCode.",
|
|
250
|
+
"commands.no_description": "Без описания",
|
|
251
|
+
"commands.button.execute": "✅ Выполнить",
|
|
252
|
+
"commands.button.cancel": "❌ Отмена",
|
|
253
|
+
"commands.confirm": "Подтвердите выполнение команды {command}. Для выполнения с аргументами отправьте аргументы отдельным сообщением.",
|
|
254
|
+
"commands.inactive_callback": "Это меню команд уже неактивно",
|
|
255
|
+
"commands.cancelled_callback": "Отменено",
|
|
256
|
+
"commands.execute_callback": "Запускаю команду...",
|
|
257
|
+
"commands.executing_prefix": "⚡ Выполнение команды:",
|
|
258
|
+
"commands.arguments_empty": "⚠️ Аргументы не могут быть пустыми. Отправьте текст или нажмите Выполнить.",
|
|
259
|
+
"commands.execute_error": "🔴 Не удалось выполнить команду OpenCode.",
|
|
241
260
|
"cmd.description.rename": "Переименовать текущую сессию",
|
|
242
261
|
"cli.usage": "Использование:\n opencode-telegram [start] [--mode sources|installed]\n opencode-telegram status\n opencode-telegram stop\n opencode-telegram config\n\nЗаметки:\n - Без команды по умолчанию используется `start`\n - `--mode` сейчас поддерживается только для `start`",
|
|
243
262
|
"cli.placeholder.status": "Команда `status` пока работает как заглушка. Реальная проверка статуса появится на этапе service-слоя (Этап 5).",
|
package/dist/i18n/zh.js
CHANGED
|
@@ -4,6 +4,7 @@ export const zh = {
|
|
|
4
4
|
"cmd.description.stop": "停止当前操作",
|
|
5
5
|
"cmd.description.sessions": "列出会话",
|
|
6
6
|
"cmd.description.projects": "列出项目",
|
|
7
|
+
"cmd.description.commands": "自定义命令",
|
|
7
8
|
"cmd.description.opencode_start": "启动 OpenCode 服务器",
|
|
8
9
|
"cmd.description.opencode_stop": "停止 OpenCode 服务器",
|
|
9
10
|
"cmd.description.help": "帮助",
|
|
@@ -48,6 +49,11 @@ export const zh = {
|
|
|
48
49
|
"bot.photo_model_no_image": "⚠️ 当前模型不支持图像输入。将仅发送文本。",
|
|
49
50
|
"bot.photo_download_error": "🔴 下载照片失败",
|
|
50
51
|
"bot.photo_no_caption": "💡 提示:添加说明文字以描述你希望对这张照片做什么。",
|
|
52
|
+
"bot.file_downloading": "⏳ 正在下载文件...",
|
|
53
|
+
"bot.file_too_large": "⚠️ 文件过大(最大 {maxSizeMb}MB)",
|
|
54
|
+
"bot.file_download_error": "🔴 下载文件失败",
|
|
55
|
+
"bot.model_no_pdf": "⚠️ 当前模型不支持PDF输入。将仅发送文本。",
|
|
56
|
+
"bot.text_file_too_large": "⚠️ 文本文件过大(最大 {maxSizeKb}KB)",
|
|
51
57
|
"status.header_running": "🟢 **OpenCode 服务器正在运行**",
|
|
52
58
|
"status.health.healthy": "健康",
|
|
53
59
|
"status.health.unhealthy": "不健康",
|
|
@@ -238,6 +244,19 @@ export const zh = {
|
|
|
238
244
|
"rename.blocked.expected_name": "⚠️ 请以文本输入新会话名称,或在重命名消息中点击取消。",
|
|
239
245
|
"rename.blocked.command_not_allowed": "⚠️ 重命名等待新名称期间不可用此命令。",
|
|
240
246
|
"rename.button.cancel": "❌ 取消",
|
|
247
|
+
"commands.select": "请选择一个 OpenCode 命令:",
|
|
248
|
+
"commands.empty": "📭 当前项目没有可用的 OpenCode 命令。",
|
|
249
|
+
"commands.fetch_error": "🔴 加载 OpenCode 命令失败。",
|
|
250
|
+
"commands.no_description": "无描述",
|
|
251
|
+
"commands.button.execute": "✅ 执行",
|
|
252
|
+
"commands.button.cancel": "❌ 取消",
|
|
253
|
+
"commands.confirm": "请确认执行命令 {command}。若需带参数执行,请发送一条包含参数的消息。",
|
|
254
|
+
"commands.inactive_callback": "该命令菜单已失效",
|
|
255
|
+
"commands.cancelled_callback": "已取消",
|
|
256
|
+
"commands.execute_callback": "正在执行命令...",
|
|
257
|
+
"commands.executing_prefix": "⚡ 执行命令:",
|
|
258
|
+
"commands.arguments_empty": "⚠️ 参数不能为空。请发送文本或点击执行。",
|
|
259
|
+
"commands.execute_error": "🔴 执行 OpenCode 命令失败。",
|
|
241
260
|
"cmd.description.rename": "重命名当前会话",
|
|
242
261
|
"cli.usage": "用法:\n opencode-telegram [start] [--mode sources|installed]\n opencode-telegram status\n opencode-telegram stop\n opencode-telegram config\n\n说明:\n - 不带命令时默认执行 `start`\n - `--mode` 目前仅支持 `start` 命令",
|
|
243
262
|
"cli.placeholder.status": "命令 `status` 目前是占位符。真实状态检查将会在 service 层(阶段 5)添加。",
|