opencode-telegram-bot 1.0.4 → 1.0.6
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 +14 -13
- package/dist/app.d.ts +4 -0
- package/dist/index.js +247 -76
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -99,17 +99,14 @@ These are handled directly by the Telegram bot:
|
|
|
99
99
|
|---|---|
|
|
100
100
|
| `/start` | Welcome message |
|
|
101
101
|
| `/new` | Start a new conversation |
|
|
102
|
-
| `/sessions` | List
|
|
103
|
-
| `/switch <id>` | Switch to a different session (prefix match supported) |
|
|
102
|
+
| `/sessions` | List sessions with inline buttons |
|
|
104
103
|
| `/title <text>` | Rename the current session |
|
|
105
|
-
| `/delete <id>` | Delete a session from the server |
|
|
106
104
|
| `/export` | Export the current session as a markdown file |
|
|
107
105
|
| `/export full` | Export with all details (thinking, costs, steps) |
|
|
108
106
|
| `/verbose` | Toggle verbose mode (show thinking and tool calls in chat) |
|
|
109
|
-
| `/verbose on
|
|
107
|
+
| `/verbose on\|off` | Explicitly enable/disable verbose mode |
|
|
110
108
|
| `/model` | Show current model and usage hints |
|
|
111
109
|
| `/model <keyword>` | Search models by keyword |
|
|
112
|
-
| `/model <number>` | Switch to a model from the last search |
|
|
113
110
|
| `/model default` | Reset to the default model |
|
|
114
111
|
| `/usage` | Show token and cost usage for this session |
|
|
115
112
|
| `/help` | Show available commands |
|
|
@@ -141,13 +138,11 @@ Use `/model` to search and switch models without typing long names:
|
|
|
141
138
|
```
|
|
142
139
|
You: /model sonnet
|
|
143
140
|
Bot: Models matching "sonnet":
|
|
144
|
-
|
|
145
|
-
2. claude-sonnet-4 (anthropic)
|
|
141
|
+
Tap a model to select.
|
|
146
142
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
You: /model 1
|
|
143
|
+
You: [tap "claude-sonnet-4-5 (google-vertex-anthropic)"]
|
|
150
144
|
Bot: Switched to claude-sonnet-4-5 (google-vertex-anthropic)
|
|
145
|
+
|
|
151
146
|
```
|
|
152
147
|
|
|
153
148
|
Other commands:
|
|
@@ -190,6 +185,13 @@ If you type an unknown command, the bot will reply with the list of available co
|
|
|
190
185
|
|
|
191
186
|
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
187
|
|
|
188
|
+
### Files and Images
|
|
189
|
+
|
|
190
|
+
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:
|
|
191
|
+
|
|
192
|
+
- Photos sent via Telegram
|
|
193
|
+
- Documents (PDFs, images, etc.)
|
|
194
|
+
|
|
193
195
|
## Session Management
|
|
194
196
|
|
|
195
197
|
Sessions are never deleted automatically. You can have multiple sessions and switch between them.
|
|
@@ -212,9 +214,8 @@ Bot: [agent response about tests - new session auto-titled "Fix the broken test
|
|
|
212
214
|
|
|
213
215
|
You: /sessions
|
|
214
216
|
Bot: Your sessions:
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
Use /switch <id> to switch sessions.
|
|
217
|
+
Current session: Fix the broken tests
|
|
218
|
+
Tap a session to switch or delete.
|
|
218
219
|
|
|
219
220
|
You: /switch def6
|
|
220
221
|
Bot: Switched to session: Auth refactoring (def67890)
|
package/dist/app.d.ts
CHANGED
|
@@ -36,6 +36,7 @@ interface TelegramBot {
|
|
|
36
36
|
help: (fn: (ctx: any) => void | Promise<void>) => void;
|
|
37
37
|
command: (command: string, fn: (ctx: any) => void | Promise<void>) => void;
|
|
38
38
|
on: (event: string, fn: (ctx: any) => void | Promise<void>) => void;
|
|
39
|
+
action: (trigger: string | RegExp, fn: (ctx: any) => void | Promise<void>) => void;
|
|
39
40
|
launch: () => Promise<void>;
|
|
40
41
|
stop: (reason?: string) => void;
|
|
41
42
|
telegram: {
|
|
@@ -45,6 +46,9 @@ interface TelegramBot {
|
|
|
45
46
|
source: Buffer;
|
|
46
47
|
filename: string;
|
|
47
48
|
}) => Promise<void>;
|
|
49
|
+
getFileLink: (fileId: string) => Promise<{
|
|
50
|
+
toString(): string;
|
|
51
|
+
}>;
|
|
48
52
|
};
|
|
49
53
|
}
|
|
50
54
|
export declare function startTelegram(options: StartOptions): Promise<TelegramBot>;
|
package/dist/index.js
CHANGED
|
@@ -18791,6 +18791,64 @@ 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
|
+
function getChatIdFromContext(ctx) {
|
|
18805
|
+
const direct = ctx.chat?.id;
|
|
18806
|
+
if (direct)
|
|
18807
|
+
return direct.toString();
|
|
18808
|
+
const fromCallback = ctx.callbackQuery?.message?.chat?.id;
|
|
18809
|
+
if (fromCallback)
|
|
18810
|
+
return fromCallback.toString();
|
|
18811
|
+
return null;
|
|
18812
|
+
}
|
|
18813
|
+
async function answerAndEdit(ctx, text) {
|
|
18814
|
+
if (typeof ctx.answerCbQuery === "function") {
|
|
18815
|
+
await ctx.answerCbQuery();
|
|
18816
|
+
}
|
|
18817
|
+
if (typeof ctx.editMessageText === "function") {
|
|
18818
|
+
await ctx.editMessageText(text);
|
|
18819
|
+
return;
|
|
18820
|
+
}
|
|
18821
|
+
await ctx.reply(text);
|
|
18822
|
+
}
|
|
18823
|
+
async function getTelegramFileUrl(fileId) {
|
|
18824
|
+
const link = await bot.telegram.getFileLink(fileId);
|
|
18825
|
+
return link.toString();
|
|
18826
|
+
}
|
|
18827
|
+
function isTextMime(mime) {
|
|
18828
|
+
const normalized = mime.toLowerCase();
|
|
18829
|
+
return (normalized.startsWith("text/") ||
|
|
18830
|
+
normalized === "application/json" ||
|
|
18831
|
+
normalized === "application/xml" ||
|
|
18832
|
+
normalized === "application/yaml" ||
|
|
18833
|
+
normalized === "application/x-yaml" ||
|
|
18834
|
+
normalized === "application/markdown");
|
|
18835
|
+
}
|
|
18836
|
+
async function fetchTelegramFileText(fileId, maxBytes = 200_000) {
|
|
18837
|
+
const url = await getTelegramFileUrl(fileId);
|
|
18838
|
+
const response = await fetch(url);
|
|
18839
|
+
if (!response.ok) {
|
|
18840
|
+
throw new Error(`Failed to fetch file: ${response.status}`);
|
|
18841
|
+
}
|
|
18842
|
+
const contentLength = response.headers.get("content-length");
|
|
18843
|
+
if (contentLength && Number(contentLength) > maxBytes) {
|
|
18844
|
+
throw new Error("File is too large to send as text.");
|
|
18845
|
+
}
|
|
18846
|
+
const buffer = await response.arrayBuffer();
|
|
18847
|
+
if (buffer.byteLength > maxBytes) {
|
|
18848
|
+
throw new Error("File is too large to send as text.");
|
|
18849
|
+
}
|
|
18850
|
+
return new TextDecoder("utf-8").decode(buffer);
|
|
18851
|
+
}
|
|
18794
18852
|
/**
|
|
18795
18853
|
* Build a one-line summary for a tool call, picking the most meaningful input field.
|
|
18796
18854
|
*/
|
|
@@ -19000,6 +19058,40 @@ async function startTelegram(options) {
|
|
|
19000
19058
|
await sendTelegramMessage(chatId, chunk);
|
|
19001
19059
|
}
|
|
19002
19060
|
}
|
|
19061
|
+
async function handleFileMessage(ctx, fileId, mime, filename, caption) {
|
|
19062
|
+
const chatId = ctx.chat.id.toString();
|
|
19063
|
+
try {
|
|
19064
|
+
const processingMsg = await ctx.reply("Processing your request...");
|
|
19065
|
+
const sessionId = await getOrCreateSession(chatId);
|
|
19066
|
+
const titleText = caption || filename || "File upload";
|
|
19067
|
+
await autoTitleSession(sessionId, titleText);
|
|
19068
|
+
const parts = [];
|
|
19069
|
+
if (caption) {
|
|
19070
|
+
parts.push({ type: "text", text: caption });
|
|
19071
|
+
}
|
|
19072
|
+
const normalizedMime = mime.toLowerCase();
|
|
19073
|
+
if (isTextMime(normalizedMime)) {
|
|
19074
|
+
const textContent = await fetchTelegramFileText(fileId);
|
|
19075
|
+
const header = filename ? `File: ${filename}` : "File";
|
|
19076
|
+
parts.push({
|
|
19077
|
+
type: "text",
|
|
19078
|
+
text: `${header}\n\n${textContent}`,
|
|
19079
|
+
});
|
|
19080
|
+
}
|
|
19081
|
+
else {
|
|
19082
|
+
const url = await getTelegramFileUrl(fileId);
|
|
19083
|
+
parts.push({ type: "file", mime: normalizedMime, filename, url });
|
|
19084
|
+
}
|
|
19085
|
+
const promptBody = buildPromptBody(chatId, parts);
|
|
19086
|
+
const verbose = chatVerboseMode.has(chatId);
|
|
19087
|
+
await sendPromptStreaming(ctx.chat.id, sessionId, promptBody, processingMsg.message_id, verbose);
|
|
19088
|
+
}
|
|
19089
|
+
catch (err) {
|
|
19090
|
+
console.error("[Telegram] Error processing file message:", err);
|
|
19091
|
+
await handleSessionError(chatId);
|
|
19092
|
+
await ctx.reply("Sorry, there was an error processing your request. Try again or use /new to start a fresh session.");
|
|
19093
|
+
}
|
|
19094
|
+
}
|
|
19003
19095
|
/**
|
|
19004
19096
|
* Handle session errors - clear invalid sessions.
|
|
19005
19097
|
*/
|
|
@@ -19048,9 +19140,7 @@ async function startTelegram(options) {
|
|
|
19048
19140
|
"Bot commands:\n" +
|
|
19049
19141
|
"/new - Start a new conversation\n" +
|
|
19050
19142
|
"/sessions - List your sessions\n" +
|
|
19051
|
-
"/switch <number> - Switch to a different session\n" +
|
|
19052
19143
|
"/title <text> - Rename the current session\n" +
|
|
19053
|
-
"/delete <number> - Delete a session\n" +
|
|
19054
19144
|
"/export - Export the current session as a markdown file\n" +
|
|
19055
19145
|
"/export full - Export with all details (thinking, costs, steps)\n" +
|
|
19056
19146
|
"/verbose - Toggle verbose mode (show thinking and tool calls)\n" +
|
|
@@ -19071,9 +19161,7 @@ async function startTelegram(options) {
|
|
|
19071
19161
|
"Bot commands:\n" +
|
|
19072
19162
|
"/new - Start a new conversation\n" +
|
|
19073
19163
|
"/sessions - List your sessions\n" +
|
|
19074
|
-
"/switch <number> - Switch to a different session\n" +
|
|
19075
19164
|
"/title <text> - Rename the current session\n" +
|
|
19076
|
-
"/delete <number> - Delete a session\n" +
|
|
19077
19165
|
"/export - Export the current session as a markdown file\n" +
|
|
19078
19166
|
"/export full - Export with all details (thinking, costs, steps)\n" +
|
|
19079
19167
|
"/verbose - Toggle verbose mode (show thinking and tool calls)\n" +
|
|
@@ -19109,16 +19197,9 @@ async function startTelegram(options) {
|
|
|
19109
19197
|
.sort((a, b) => b.time.updated - a.time.updated);
|
|
19110
19198
|
}
|
|
19111
19199
|
/**
|
|
19112
|
-
* Resolve a user argument to a session
|
|
19113
|
-
* (1-based, as shown by /sessions) or a session ID / prefix.
|
|
19200
|
+
* Resolve a user argument to a session ID / prefix.
|
|
19114
19201
|
*/
|
|
19115
19202
|
function resolveSession(sessions, arg) {
|
|
19116
|
-
// Try numeric index first
|
|
19117
|
-
const num = parseInt(arg, 10);
|
|
19118
|
-
if (!isNaN(num) && String(num) === arg && num >= 1 && num <= sessions.length) {
|
|
19119
|
-
return sessions[num - 1];
|
|
19120
|
-
}
|
|
19121
|
-
// Fall back to ID / prefix match
|
|
19122
19203
|
return sessions.find((s) => s.id === arg || s.id.startsWith(arg));
|
|
19123
19204
|
}
|
|
19124
19205
|
// Handle /sessions command - list Telegram bot sessions only
|
|
@@ -19131,28 +19212,48 @@ async function startTelegram(options) {
|
|
|
19131
19212
|
await ctx.reply("No sessions found.");
|
|
19132
19213
|
return;
|
|
19133
19214
|
}
|
|
19134
|
-
|
|
19135
|
-
|
|
19136
|
-
|
|
19137
|
-
|
|
19138
|
-
|
|
19139
|
-
|
|
19215
|
+
const activeSession = sessions.find((session) => session.id === activeSessionId);
|
|
19216
|
+
const msgLines = ["Your sessions:"];
|
|
19217
|
+
if (activeSession) {
|
|
19218
|
+
msgLines.push(`Current session: ${activeSession.title}`);
|
|
19219
|
+
}
|
|
19220
|
+
const otherSessions = sessions.filter((session) => session.id !== activeSessionId);
|
|
19221
|
+
if (otherSessions.length === 0) {
|
|
19222
|
+
msgLines.push("This is your only session.");
|
|
19223
|
+
await ctx.reply(msgLines.join("\n"));
|
|
19224
|
+
return;
|
|
19225
|
+
}
|
|
19226
|
+
msgLines.push("Tap a session to switch or delete.");
|
|
19227
|
+
const keyboard = [];
|
|
19228
|
+
otherSessions.forEach((session) => {
|
|
19229
|
+
keyboard.push([
|
|
19230
|
+
{
|
|
19231
|
+
text: session.title,
|
|
19232
|
+
callback_data: `switch:${session.id}`,
|
|
19233
|
+
},
|
|
19234
|
+
{
|
|
19235
|
+
text: "Delete",
|
|
19236
|
+
callback_data: `delete:${session.id}`,
|
|
19237
|
+
},
|
|
19238
|
+
]);
|
|
19239
|
+
});
|
|
19240
|
+
await ctx.reply(msgLines.join("\n"), {
|
|
19241
|
+
reply_markup: {
|
|
19242
|
+
inline_keyboard: keyboard,
|
|
19243
|
+
},
|
|
19140
19244
|
});
|
|
19141
|
-
msg += `\nUse /switch <number> to switch sessions.`;
|
|
19142
|
-
msg += `\nUse /delete <number> to delete a session.`;
|
|
19143
|
-
await ctx.reply(msg);
|
|
19144
19245
|
}
|
|
19145
19246
|
catch (err) {
|
|
19146
19247
|
console.error("[Telegram] Error listing sessions:", err);
|
|
19147
19248
|
await ctx.reply("Failed to list sessions.");
|
|
19148
19249
|
}
|
|
19149
19250
|
});
|
|
19150
|
-
// Handle /switch <
|
|
19251
|
+
// Handle /switch <id> command - switch to a different session
|
|
19151
19252
|
bot.command("switch", async (ctx) => {
|
|
19152
19253
|
const chatId = ctx.chat.id.toString();
|
|
19153
19254
|
const args = ctx.message.text.replace(/^\/switch\s*/, "").trim();
|
|
19154
19255
|
if (!args) {
|
|
19155
|
-
await ctx.reply("Usage: /switch <
|
|
19256
|
+
await ctx.reply("Usage: /switch <session id>\n\nUse /sessions to see available sessions.");
|
|
19156
19257
|
return;
|
|
19157
19258
|
}
|
|
19158
19259
|
try {
|
|
@@ -19201,12 +19302,12 @@ async function startTelegram(options) {
|
|
|
19201
19302
|
await ctx.reply("Failed to rename session.");
|
|
19202
19303
|
}
|
|
19203
19304
|
});
|
|
19204
|
-
// Handle /delete <
|
|
19305
|
+
// Handle /delete <id> command - delete a session
|
|
19205
19306
|
bot.command("delete", async (ctx) => {
|
|
19206
19307
|
const chatId = ctx.chat.id.toString();
|
|
19207
19308
|
const args = ctx.message.text.replace(/^\/delete\s*/, "").trim();
|
|
19208
19309
|
if (!args) {
|
|
19209
|
-
await ctx.reply("Usage: /delete <
|
|
19310
|
+
await ctx.reply("Usage: /delete <session id>\n\nUse /sessions to see available sessions.");
|
|
19210
19311
|
return;
|
|
19211
19312
|
}
|
|
19212
19313
|
try {
|
|
@@ -19240,6 +19341,61 @@ async function startTelegram(options) {
|
|
|
19240
19341
|
await ctx.reply("Failed to delete session.");
|
|
19241
19342
|
}
|
|
19242
19343
|
});
|
|
19344
|
+
bot.action(/^switch:(.+)$/, async (ctx) => {
|
|
19345
|
+
const chatId = getChatIdFromContext(ctx);
|
|
19346
|
+
const sessionId = ctx.match?.[1];
|
|
19347
|
+
if (!chatId || !sessionId)
|
|
19348
|
+
return;
|
|
19349
|
+
try {
|
|
19350
|
+
const sessions = await getKnownSessions();
|
|
19351
|
+
const match = sessions.find((session) => session.id === sessionId);
|
|
19352
|
+
if (!match) {
|
|
19353
|
+
await answerAndEdit(ctx, "Session not found. Use /sessions to refresh.");
|
|
19354
|
+
return;
|
|
19355
|
+
}
|
|
19356
|
+
chatSessions.set(chatId, match.id);
|
|
19357
|
+
saveSessions();
|
|
19358
|
+
console.log(`[Telegram] Switched chat ${chatId} to session ${match.id}`);
|
|
19359
|
+
await answerAndEdit(ctx, `Switched to session: ${match.title}`);
|
|
19360
|
+
}
|
|
19361
|
+
catch (err) {
|
|
19362
|
+
console.error("[Telegram] Error switching session:", err);
|
|
19363
|
+
await answerAndEdit(ctx, "Failed to switch session.");
|
|
19364
|
+
}
|
|
19365
|
+
});
|
|
19366
|
+
bot.action(/^delete:(.+)$/, async (ctx) => {
|
|
19367
|
+
const chatId = getChatIdFromContext(ctx);
|
|
19368
|
+
const sessionId = ctx.match?.[1];
|
|
19369
|
+
if (!chatId || !sessionId)
|
|
19370
|
+
return;
|
|
19371
|
+
try {
|
|
19372
|
+
const sessions = await getKnownSessions();
|
|
19373
|
+
const match = sessions.find((session) => session.id === sessionId);
|
|
19374
|
+
if (!match) {
|
|
19375
|
+
await answerAndEdit(ctx, "Session not found. Use /sessions to refresh.");
|
|
19376
|
+
return;
|
|
19377
|
+
}
|
|
19378
|
+
const activeSessionId = chatSessions.get(chatId);
|
|
19379
|
+
if (match.id === activeSessionId) {
|
|
19380
|
+
await answerAndEdit(ctx, "Cannot delete the active session. Use /new or /switch first, then delete it.");
|
|
19381
|
+
return;
|
|
19382
|
+
}
|
|
19383
|
+
const result = await client.session.delete({
|
|
19384
|
+
path: { id: match.id },
|
|
19385
|
+
});
|
|
19386
|
+
if (result.error) {
|
|
19387
|
+
throw new Error(JSON.stringify(result.error));
|
|
19388
|
+
}
|
|
19389
|
+
knownSessionIds.delete(match.id);
|
|
19390
|
+
saveSessions();
|
|
19391
|
+
console.log(`[Telegram] Deleted session ${match.id}`);
|
|
19392
|
+
await answerAndEdit(ctx, `Deleted session: ${match.title}`);
|
|
19393
|
+
}
|
|
19394
|
+
catch (err) {
|
|
19395
|
+
console.error("[Telegram] Error deleting session:", err);
|
|
19396
|
+
await answerAndEdit(ctx, "Failed to delete session.");
|
|
19397
|
+
}
|
|
19398
|
+
});
|
|
19243
19399
|
// Handle /verbose command - toggle verbose mode for this chat
|
|
19244
19400
|
// Usage: /verbose, /verbose on, /verbose off
|
|
19245
19401
|
bot.command("verbose", (ctx) => {
|
|
@@ -19295,7 +19451,7 @@ async function startTelegram(options) {
|
|
|
19295
19451
|
return results;
|
|
19296
19452
|
}
|
|
19297
19453
|
// Handle /model command
|
|
19298
|
-
// Usage: /model, /model <keyword>, /model
|
|
19454
|
+
// Usage: /model, /model <keyword>, /model default
|
|
19299
19455
|
bot.command("model", async (ctx) => {
|
|
19300
19456
|
const chatId = ctx.chat.id.toString();
|
|
19301
19457
|
const args = ctx.message.text.replace(/^\/model\s*/, "").trim();
|
|
@@ -19312,24 +19468,6 @@ async function startTelegram(options) {
|
|
|
19312
19468
|
await ctx.reply("Model reset to the default model.");
|
|
19313
19469
|
return;
|
|
19314
19470
|
}
|
|
19315
|
-
const asNumber = Number.parseInt(args, 10);
|
|
19316
|
-
if (!Number.isNaN(asNumber) && String(asNumber) === args) {
|
|
19317
|
-
const results = chatModelSearchResults.get(chatId) || [];
|
|
19318
|
-
if (results.length === 0) {
|
|
19319
|
-
await ctx.reply("No recent search results. Use /model <keyword> first.");
|
|
19320
|
-
return;
|
|
19321
|
-
}
|
|
19322
|
-
if (asNumber < 1 || asNumber > results.length) {
|
|
19323
|
-
await ctx.reply("Invalid selection. Use /model <number> from the latest search results.");
|
|
19324
|
-
return;
|
|
19325
|
-
}
|
|
19326
|
-
const selection = results[asNumber - 1];
|
|
19327
|
-
const value = `${selection.providerID}/${selection.modelID}`;
|
|
19328
|
-
chatModelOverride.set(chatId, value);
|
|
19329
|
-
saveSessions();
|
|
19330
|
-
await ctx.reply(`Switched to ${selection.displayName}`);
|
|
19331
|
-
return;
|
|
19332
|
-
}
|
|
19333
19471
|
try {
|
|
19334
19472
|
const results = await searchModels(args);
|
|
19335
19473
|
if (results.length === 0) {
|
|
@@ -19338,21 +19476,60 @@ async function startTelegram(options) {
|
|
|
19338
19476
|
}
|
|
19339
19477
|
const limited = results.slice(0, 10);
|
|
19340
19478
|
chatModelSearchResults.set(chatId, limited);
|
|
19341
|
-
let msg = `Models matching "${args}"
|
|
19342
|
-
for (const [index, item] of limited.entries()) {
|
|
19343
|
-
msg += `${index + 1}. ${item.displayName}\n`;
|
|
19344
|
-
}
|
|
19479
|
+
let msg = `Models matching "${args}":`;
|
|
19345
19480
|
if (results.length > limited.length) {
|
|
19346
19481
|
msg += `\nFound ${results.length} models. Refine your search to narrow the list.`;
|
|
19347
19482
|
}
|
|
19348
|
-
msg += "\
|
|
19349
|
-
|
|
19483
|
+
msg += "\nTap a model to select.";
|
|
19484
|
+
const keyboard = limited.map((item, index) => [
|
|
19485
|
+
{
|
|
19486
|
+
text: item.displayName,
|
|
19487
|
+
callback_data: `model:${index + 1}`,
|
|
19488
|
+
},
|
|
19489
|
+
]);
|
|
19490
|
+
keyboard.push([
|
|
19491
|
+
{ text: "Reset to default", callback_data: "model_default" },
|
|
19492
|
+
]);
|
|
19493
|
+
await ctx.reply(msg, {
|
|
19494
|
+
reply_markup: {
|
|
19495
|
+
inline_keyboard: keyboard,
|
|
19496
|
+
},
|
|
19497
|
+
});
|
|
19350
19498
|
}
|
|
19351
19499
|
catch (err) {
|
|
19352
19500
|
console.error("[Telegram] Error searching models:", err);
|
|
19353
19501
|
await ctx.reply("Failed to list models. Try again later.");
|
|
19354
19502
|
}
|
|
19355
19503
|
});
|
|
19504
|
+
bot.action(/^model:(\d+)$/, async (ctx) => {
|
|
19505
|
+
const chatId = getChatIdFromContext(ctx);
|
|
19506
|
+
const indexText = ctx.match?.[1];
|
|
19507
|
+
if (!chatId || !indexText)
|
|
19508
|
+
return;
|
|
19509
|
+
const selectionIndex = Number.parseInt(indexText, 10);
|
|
19510
|
+
const results = chatModelSearchResults.get(chatId) || [];
|
|
19511
|
+
if (results.length === 0) {
|
|
19512
|
+
await answerAndEdit(ctx, "No recent search results. Use /model <keyword> first.");
|
|
19513
|
+
return;
|
|
19514
|
+
}
|
|
19515
|
+
if (selectionIndex < 1 || selectionIndex > results.length) {
|
|
19516
|
+
await answerAndEdit(ctx, "Invalid selection. Use the latest model search results.");
|
|
19517
|
+
return;
|
|
19518
|
+
}
|
|
19519
|
+
const selection = results[selectionIndex - 1];
|
|
19520
|
+
const value = `${selection.providerID}/${selection.modelID}`;
|
|
19521
|
+
chatModelOverride.set(chatId, value);
|
|
19522
|
+
saveSessions();
|
|
19523
|
+
await answerAndEdit(ctx, `Switched to ${selection.displayName}`);
|
|
19524
|
+
});
|
|
19525
|
+
bot.action("model_default", async (ctx) => {
|
|
19526
|
+
const chatId = getChatIdFromContext(ctx);
|
|
19527
|
+
if (!chatId)
|
|
19528
|
+
return;
|
|
19529
|
+
chatModelOverride.delete(chatId);
|
|
19530
|
+
saveSessions();
|
|
19531
|
+
await answerAndEdit(ctx, "Model reset to the default model.");
|
|
19532
|
+
});
|
|
19356
19533
|
// Handle /usage command - show token and cost usage for current session
|
|
19357
19534
|
bot.command("usage", async (ctx) => {
|
|
19358
19535
|
const chatId = ctx.chat.id.toString();
|
|
@@ -19404,6 +19581,23 @@ async function startTelegram(options) {
|
|
|
19404
19581
|
await ctx.reply("Failed to fetch usage. Try again later.");
|
|
19405
19582
|
}
|
|
19406
19583
|
});
|
|
19584
|
+
// Handle photo messages (images)
|
|
19585
|
+
bot.on("photo", async (ctx) => {
|
|
19586
|
+
const photos = ctx.message.photo;
|
|
19587
|
+
if (!photos || photos.length === 0)
|
|
19588
|
+
return;
|
|
19589
|
+
const largest = photos[photos.length - 1];
|
|
19590
|
+
const caption = ctx.message.caption;
|
|
19591
|
+
await handleFileMessage(ctx, largest.file_id, "image/jpeg", "photo.jpg", caption);
|
|
19592
|
+
});
|
|
19593
|
+
// Handle document messages (files)
|
|
19594
|
+
bot.on("document", async (ctx) => {
|
|
19595
|
+
const doc = ctx.message.document;
|
|
19596
|
+
if (!doc)
|
|
19597
|
+
return;
|
|
19598
|
+
const caption = ctx.message.caption;
|
|
19599
|
+
await handleFileMessage(ctx, doc.file_id, doc.mime_type || "application/octet-stream", doc.file_name, caption);
|
|
19600
|
+
});
|
|
19407
19601
|
/**
|
|
19408
19602
|
* Render a message part to markdown.
|
|
19409
19603
|
* In default mode: only text and tool calls (name + input/output).
|
|
@@ -19686,16 +19880,9 @@ async function startTelegram(options) {
|
|
|
19686
19880
|
const sessionId = await getOrCreateSession(chatId);
|
|
19687
19881
|
// Auto-title if this is the first message in a new session
|
|
19688
19882
|
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
|
-
}
|
|
19883
|
+
const promptBody = buildPromptBody(chatId, [
|
|
19884
|
+
{ type: "text", text: userText },
|
|
19885
|
+
]);
|
|
19699
19886
|
const verbose = chatVerboseMode.has(chatId);
|
|
19700
19887
|
await sendPromptStreaming(ctx.chat.id, sessionId, promptBody, processingMsg.message_id, verbose);
|
|
19701
19888
|
}
|
|
@@ -19721,22 +19908,6 @@ async function startTelegram(options) {
|
|
|
19721
19908
|
}
|
|
19722
19909
|
return bot;
|
|
19723
19910
|
}
|
|
19724
|
-
/**
|
|
19725
|
-
* Format a Unix timestamp (seconds) into a human-readable relative time.
|
|
19726
|
-
*/
|
|
19727
|
-
function formatAge(timestamp) {
|
|
19728
|
-
const now = Date.now() / 1000;
|
|
19729
|
-
const diff = now - timestamp;
|
|
19730
|
-
if (diff < 60)
|
|
19731
|
-
return "just now";
|
|
19732
|
-
if (diff < 3600)
|
|
19733
|
-
return `${Math.floor(diff / 60)} min ago`;
|
|
19734
|
-
if (diff < 86400)
|
|
19735
|
-
return `${Math.floor(diff / 3600)} hours ago`;
|
|
19736
|
-
if (diff < 604800)
|
|
19737
|
-
return `${Math.floor(diff / 86400)} days ago`;
|
|
19738
|
-
return `${Math.floor(diff / 604800)} weeks ago`;
|
|
19739
|
-
}
|
|
19740
19911
|
/**
|
|
19741
19912
|
* Truncate text to a maximum length, appending "..." if truncated.
|
|
19742
19913
|
*/
|