opencode-telegram-bot 1.0.4 → 1.0.5
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 +8 -1
- package/dist/app.d.ts +3 -0
- package/dist/index.js +93 -10
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -106,7 +106,7 @@ These are handled directly by the Telegram bot:
|
|
|
106
106
|
| `/export` | Export the current session as a markdown file |
|
|
107
107
|
| `/export full` | Export with all details (thinking, costs, steps) |
|
|
108
108
|
| `/verbose` | Toggle verbose mode (show thinking and tool calls in chat) |
|
|
109
|
-
| `/verbose on
|
|
109
|
+
| `/verbose on\|off` | Explicitly enable/disable verbose mode |
|
|
110
110
|
| `/model` | Show current model and usage hints |
|
|
111
111
|
| `/model <keyword>` | Search models by keyword |
|
|
112
112
|
| `/model <number>` | Switch to a model from the last search |
|
|
@@ -190,6 +190,13 @@ If you type an unknown command, the bot will reply with the list of available co
|
|
|
190
190
|
|
|
191
191
|
Any other text message is forwarded to the OpenCode agent as a prompt. Follow-up messages go into the same session, so the agent has full conversation context. Use `/new` when you want a fresh conversation.
|
|
192
192
|
|
|
193
|
+
### Files and Images
|
|
194
|
+
|
|
195
|
+
You can send files or images to the bot. They are forwarded to OpenCode as file parts (with optional caption text). Text-based files (including markdown) are sent as text instead of file parts for better model compatibility. Supported types:
|
|
196
|
+
|
|
197
|
+
- Photos sent via Telegram
|
|
198
|
+
- Documents (PDFs, images, etc.)
|
|
199
|
+
|
|
193
200
|
## Session Management
|
|
194
201
|
|
|
195
202
|
Sessions are never deleted automatically. You can have multiple sessions and switch between them.
|
package/dist/app.d.ts
CHANGED
|
@@ -45,6 +45,9 @@ interface TelegramBot {
|
|
|
45
45
|
source: Buffer;
|
|
46
46
|
filename: string;
|
|
47
47
|
}) => Promise<void>;
|
|
48
|
+
getFileLink: (fileId: string) => Promise<{
|
|
49
|
+
toString(): string;
|
|
50
|
+
}>;
|
|
48
51
|
};
|
|
49
52
|
}
|
|
50
53
|
export declare function startTelegram(options: StartOptions): Promise<TelegramBot>;
|
package/dist/index.js
CHANGED
|
@@ -18791,6 +18791,45 @@ async function startTelegram(options) {
|
|
|
18791
18791
|
console.warn("[Telegram] Failed to auto-title session:", err);
|
|
18792
18792
|
}
|
|
18793
18793
|
}
|
|
18794
|
+
function buildPromptBody(chatId, parts) {
|
|
18795
|
+
const promptBody = { parts };
|
|
18796
|
+
const modelOverride = chatModelOverride.get(chatId) || model;
|
|
18797
|
+
if (modelOverride) {
|
|
18798
|
+
const [providerID, ...modelParts] = modelOverride.split("/");
|
|
18799
|
+
const modelID = modelParts.join("/");
|
|
18800
|
+
promptBody.model = { providerID, modelID };
|
|
18801
|
+
}
|
|
18802
|
+
return promptBody;
|
|
18803
|
+
}
|
|
18804
|
+
async function getTelegramFileUrl(fileId) {
|
|
18805
|
+
const link = await bot.telegram.getFileLink(fileId);
|
|
18806
|
+
return link.toString();
|
|
18807
|
+
}
|
|
18808
|
+
function isTextMime(mime) {
|
|
18809
|
+
const normalized = mime.toLowerCase();
|
|
18810
|
+
return (normalized.startsWith("text/") ||
|
|
18811
|
+
normalized === "application/json" ||
|
|
18812
|
+
normalized === "application/xml" ||
|
|
18813
|
+
normalized === "application/yaml" ||
|
|
18814
|
+
normalized === "application/x-yaml" ||
|
|
18815
|
+
normalized === "application/markdown");
|
|
18816
|
+
}
|
|
18817
|
+
async function fetchTelegramFileText(fileId, maxBytes = 200_000) {
|
|
18818
|
+
const url = await getTelegramFileUrl(fileId);
|
|
18819
|
+
const response = await fetch(url);
|
|
18820
|
+
if (!response.ok) {
|
|
18821
|
+
throw new Error(`Failed to fetch file: ${response.status}`);
|
|
18822
|
+
}
|
|
18823
|
+
const contentLength = response.headers.get("content-length");
|
|
18824
|
+
if (contentLength && Number(contentLength) > maxBytes) {
|
|
18825
|
+
throw new Error("File is too large to send as text.");
|
|
18826
|
+
}
|
|
18827
|
+
const buffer = await response.arrayBuffer();
|
|
18828
|
+
if (buffer.byteLength > maxBytes) {
|
|
18829
|
+
throw new Error("File is too large to send as text.");
|
|
18830
|
+
}
|
|
18831
|
+
return new TextDecoder("utf-8").decode(buffer);
|
|
18832
|
+
}
|
|
18794
18833
|
/**
|
|
18795
18834
|
* Build a one-line summary for a tool call, picking the most meaningful input field.
|
|
18796
18835
|
*/
|
|
@@ -19000,6 +19039,40 @@ async function startTelegram(options) {
|
|
|
19000
19039
|
await sendTelegramMessage(chatId, chunk);
|
|
19001
19040
|
}
|
|
19002
19041
|
}
|
|
19042
|
+
async function handleFileMessage(ctx, fileId, mime, filename, caption) {
|
|
19043
|
+
const chatId = ctx.chat.id.toString();
|
|
19044
|
+
try {
|
|
19045
|
+
const processingMsg = await ctx.reply("Processing your request...");
|
|
19046
|
+
const sessionId = await getOrCreateSession(chatId);
|
|
19047
|
+
const titleText = caption || filename || "File upload";
|
|
19048
|
+
await autoTitleSession(sessionId, titleText);
|
|
19049
|
+
const parts = [];
|
|
19050
|
+
if (caption) {
|
|
19051
|
+
parts.push({ type: "text", text: caption });
|
|
19052
|
+
}
|
|
19053
|
+
const normalizedMime = mime.toLowerCase();
|
|
19054
|
+
if (isTextMime(normalizedMime)) {
|
|
19055
|
+
const textContent = await fetchTelegramFileText(fileId);
|
|
19056
|
+
const header = filename ? `File: ${filename}` : "File";
|
|
19057
|
+
parts.push({
|
|
19058
|
+
type: "text",
|
|
19059
|
+
text: `${header}\n\n${textContent}`,
|
|
19060
|
+
});
|
|
19061
|
+
}
|
|
19062
|
+
else {
|
|
19063
|
+
const url = await getTelegramFileUrl(fileId);
|
|
19064
|
+
parts.push({ type: "file", mime: normalizedMime, filename, url });
|
|
19065
|
+
}
|
|
19066
|
+
const promptBody = buildPromptBody(chatId, parts);
|
|
19067
|
+
const verbose = chatVerboseMode.has(chatId);
|
|
19068
|
+
await sendPromptStreaming(ctx.chat.id, sessionId, promptBody, processingMsg.message_id, verbose);
|
|
19069
|
+
}
|
|
19070
|
+
catch (err) {
|
|
19071
|
+
console.error("[Telegram] Error processing file message:", err);
|
|
19072
|
+
await handleSessionError(chatId);
|
|
19073
|
+
await ctx.reply("Sorry, there was an error processing your request. Try again or use /new to start a fresh session.");
|
|
19074
|
+
}
|
|
19075
|
+
}
|
|
19003
19076
|
/**
|
|
19004
19077
|
* Handle session errors - clear invalid sessions.
|
|
19005
19078
|
*/
|
|
@@ -19404,6 +19477,23 @@ async function startTelegram(options) {
|
|
|
19404
19477
|
await ctx.reply("Failed to fetch usage. Try again later.");
|
|
19405
19478
|
}
|
|
19406
19479
|
});
|
|
19480
|
+
// Handle photo messages (images)
|
|
19481
|
+
bot.on("photo", async (ctx) => {
|
|
19482
|
+
const photos = ctx.message.photo;
|
|
19483
|
+
if (!photos || photos.length === 0)
|
|
19484
|
+
return;
|
|
19485
|
+
const largest = photos[photos.length - 1];
|
|
19486
|
+
const caption = ctx.message.caption;
|
|
19487
|
+
await handleFileMessage(ctx, largest.file_id, "image/jpeg", "photo.jpg", caption);
|
|
19488
|
+
});
|
|
19489
|
+
// Handle document messages (files)
|
|
19490
|
+
bot.on("document", async (ctx) => {
|
|
19491
|
+
const doc = ctx.message.document;
|
|
19492
|
+
if (!doc)
|
|
19493
|
+
return;
|
|
19494
|
+
const caption = ctx.message.caption;
|
|
19495
|
+
await handleFileMessage(ctx, doc.file_id, doc.mime_type || "application/octet-stream", doc.file_name, caption);
|
|
19496
|
+
});
|
|
19407
19497
|
/**
|
|
19408
19498
|
* Render a message part to markdown.
|
|
19409
19499
|
* In default mode: only text and tool calls (name + input/output).
|
|
@@ -19686,16 +19776,9 @@ async function startTelegram(options) {
|
|
|
19686
19776
|
const sessionId = await getOrCreateSession(chatId);
|
|
19687
19777
|
// Auto-title if this is the first message in a new session
|
|
19688
19778
|
await autoTitleSession(sessionId, userText);
|
|
19689
|
-
|
|
19690
|
-
|
|
19691
|
-
|
|
19692
|
-
};
|
|
19693
|
-
const modelOverride = chatModelOverride.get(chatId) || model;
|
|
19694
|
-
if (modelOverride) {
|
|
19695
|
-
const [providerID, ...modelParts] = modelOverride.split("/");
|
|
19696
|
-
const modelID = modelParts.join("/");
|
|
19697
|
-
promptBody.model = { providerID, modelID };
|
|
19698
|
-
}
|
|
19779
|
+
const promptBody = buildPromptBody(chatId, [
|
|
19780
|
+
{ type: "text", text: userText },
|
|
19781
|
+
]);
|
|
19699
19782
|
const verbose = chatVerboseMode.has(chatId);
|
|
19700
19783
|
await sendPromptStreaming(ctx.chat.id, sessionId, promptBody, processingMsg.message_id, verbose);
|
|
19701
19784
|
}
|