@codebam/cf-workers-telegram-bot 12.6.2 → 12.6.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 +44 -0
- package/dist/ai.d.ts +23 -0
- package/dist/ai.js +367 -0
- package/dist/history_manager.js +1 -1
- package/dist/main.d.ts +1 -0
- package/dist/main.js +1 -0
- package/dist/telegram_api.d.ts +1 -1
- package/dist/telegram_execution_context.d.ts +6 -6
- package/dist/telegram_execution_context.js +47 -13
- package/dist/utils.js +21 -8
- package/package.json +22 -6
package/README.md
CHANGED
|
@@ -119,6 +119,50 @@ npx wrangler deploy
|
|
|
119
119
|
|
|
120
120
|
To automate deployments, use the [Wrangler Action](https://github.com/cloudflare/wrangler-action) or Cloudflare's built-in [GitHub integration](https://developers.cloudflare.com/workers/ci-cd/github-actions/).
|
|
121
121
|
|
|
122
|
+
## Structure
|
|
123
|
+
|
|
124
|
+
This is a monorepo containing:
|
|
125
|
+
- Root: Core library `@codebam/cf-workers-telegram-bot`
|
|
126
|
+
- `ai-workflow`: A Cloudflare Workflow for handling long-running AI tasks
|
|
127
|
+
- `webapp`: A Svelte 5 web application for interacting with the bot
|
|
128
|
+
- `consumer`: A minimal consumer of the library
|
|
129
|
+
|
|
130
|
+
## Development
|
|
131
|
+
|
|
132
|
+
You can use the root `Makefile` to run common tasks across all projects:
|
|
133
|
+
|
|
134
|
+
```sh
|
|
135
|
+
make build # Build all projects
|
|
136
|
+
make test # Run tests for all projects
|
|
137
|
+
make lint # Lint all projects
|
|
138
|
+
make format # Format all projects
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
### Setup
|
|
142
|
+
|
|
143
|
+
1. **Install dependencies**:
|
|
144
|
+
```sh
|
|
145
|
+
npm install
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
2. **Set up Git hooks**:
|
|
149
|
+
This project uses custom Git hooks for quality control. Run the following script to enable them:
|
|
150
|
+
```sh
|
|
151
|
+
./setup_hooks.sh
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### Scripts
|
|
155
|
+
|
|
156
|
+
- `npm run lint`: Run ESLint on the source code.
|
|
157
|
+
- `npm run format`: Format the code using Prettier.
|
|
158
|
+
- `npm run build`: Compile TypeScript and run type checks.
|
|
159
|
+
- `npm run test`: Run unit tests with Vitest.
|
|
160
|
+
- `npm run lint:all`: Run linting for the root project and all subprojects.
|
|
161
|
+
- `npm run build:all`: Run build for the root project and all subprojects.
|
|
162
|
+
- `npm run test:all`: Run tests for the root project and all subprojects.
|
|
163
|
+
|
|
164
|
+
The pre-commit hook automatically runs formatting and linting on staged files (via `lint-staged`), followed by a full project type check and tests before every commit.
|
|
165
|
+
|
|
122
166
|
## API Documentation
|
|
123
167
|
|
|
124
168
|
Detailed API documentation is available at [cf-workers-telegram-bot.codebam.ca](https://cf-workers-telegram-bot.codebam.ca).
|
package/dist/ai.d.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import TelegramExecutionContext from './telegram_execution_context';
|
|
2
|
+
/**
|
|
3
|
+
* Robustly extract text from various AI response formats.
|
|
4
|
+
* Handles OpenAI, Cloudflare, and Google Gemini structures.
|
|
5
|
+
*/
|
|
6
|
+
export declare function extractText(obj: any): string;
|
|
7
|
+
/**
|
|
8
|
+
* Custom runner that supports tool calls across different AI models.
|
|
9
|
+
*/
|
|
10
|
+
export declare function customRunWithTools(ai: any, model: string, input: {
|
|
11
|
+
messages: any[];
|
|
12
|
+
tools?: any[];
|
|
13
|
+
}, config: {
|
|
14
|
+
streamFinalResponse: boolean;
|
|
15
|
+
}): Promise<any>;
|
|
16
|
+
/**
|
|
17
|
+
* Stream AI response to Telegram, with periodic updates to avoid rate limits.
|
|
18
|
+
*/
|
|
19
|
+
export declare function streamAiResponseToTelegram(bot: TelegramExecutionContext, ai: any, modelId: string, messages: any[], task: any, tools?: any[]): Promise<string>;
|
|
20
|
+
/**
|
|
21
|
+
* Creates a mock TelegramExecutionContext for use in environments where the full context isn't available (e.g., Workflows).
|
|
22
|
+
*/
|
|
23
|
+
export declare function createMockTelegramExecutionContext(task: Record<string, unknown>): TelegramExecutionContext;
|
package/dist/ai.js
ADDED
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
import TelegramApi from './telegram_api';
|
|
2
|
+
import { markdownToHtml } from './utils';
|
|
3
|
+
/**
|
|
4
|
+
* Robustly extract text from various AI response formats.
|
|
5
|
+
* Handles OpenAI, Cloudflare, and Google Gemini structures.
|
|
6
|
+
*/
|
|
7
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
8
|
+
export function extractText(obj) {
|
|
9
|
+
if (typeof obj === 'string') {
|
|
10
|
+
return obj;
|
|
11
|
+
}
|
|
12
|
+
if (typeof obj !== 'object' || obj === null) {
|
|
13
|
+
return '';
|
|
14
|
+
}
|
|
15
|
+
// Direct fields
|
|
16
|
+
if (typeof obj.response === 'string') {
|
|
17
|
+
return obj.response;
|
|
18
|
+
}
|
|
19
|
+
if (typeof obj.text === 'string') {
|
|
20
|
+
return obj.text;
|
|
21
|
+
}
|
|
22
|
+
if (typeof obj.content === 'string') {
|
|
23
|
+
return obj.content;
|
|
24
|
+
}
|
|
25
|
+
if (typeof obj.delta === 'string') {
|
|
26
|
+
return obj.delta;
|
|
27
|
+
}
|
|
28
|
+
// Nested fields
|
|
29
|
+
if (obj.choices && Array.isArray(obj.choices) && obj.choices.length > 0) {
|
|
30
|
+
return extractText(obj.choices[0]);
|
|
31
|
+
}
|
|
32
|
+
if (obj.message) {
|
|
33
|
+
return extractText(obj.message);
|
|
34
|
+
}
|
|
35
|
+
if (obj.delta) {
|
|
36
|
+
return extractText(obj.delta);
|
|
37
|
+
}
|
|
38
|
+
if (obj.candidates && Array.isArray(obj.candidates) && obj.candidates.length > 0) {
|
|
39
|
+
return extractText(obj.candidates[0]);
|
|
40
|
+
}
|
|
41
|
+
if (obj.content) {
|
|
42
|
+
return extractText(obj.content);
|
|
43
|
+
}
|
|
44
|
+
if (obj.parts && Array.isArray(obj.parts) && obj.parts.length > 0) {
|
|
45
|
+
return extractText(obj.parts[0]);
|
|
46
|
+
}
|
|
47
|
+
return '';
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Custom runner that supports tool calls across different AI models.
|
|
51
|
+
*/
|
|
52
|
+
export async function customRunWithTools(
|
|
53
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
54
|
+
ai, model,
|
|
55
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
56
|
+
input, config) {
|
|
57
|
+
const messages = [...input.messages];
|
|
58
|
+
const tools = input.tools || [];
|
|
59
|
+
const isGemini = model.includes('google/gemini');
|
|
60
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
61
|
+
const cfTools = tools.map((t) => ({
|
|
62
|
+
type: 'function',
|
|
63
|
+
function: {
|
|
64
|
+
name: t.name,
|
|
65
|
+
description: t.description,
|
|
66
|
+
parameters: t.parameters,
|
|
67
|
+
},
|
|
68
|
+
}));
|
|
69
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
70
|
+
const runModel = async (msgs, stream) => {
|
|
71
|
+
if (isGemini) {
|
|
72
|
+
const systemMessage = msgs.find((m) => m.role === 'system');
|
|
73
|
+
const otherMessages = msgs.filter((m) => m.role !== 'system');
|
|
74
|
+
const geminiInput = {
|
|
75
|
+
contents: otherMessages.map((m) => ({
|
|
76
|
+
role: m.role === 'assistant' ? 'model' : 'user',
|
|
77
|
+
parts: [{ text: m.content }],
|
|
78
|
+
})),
|
|
79
|
+
stream,
|
|
80
|
+
};
|
|
81
|
+
if (systemMessage) {
|
|
82
|
+
geminiInput.system_instruction = {
|
|
83
|
+
parts: [{ text: systemMessage.content }],
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
return await ai.run(model, geminiInput);
|
|
87
|
+
}
|
|
88
|
+
return await ai.run(model, { messages: msgs, tools: cfTools.length > 0 ? cfTools : undefined, stream });
|
|
89
|
+
};
|
|
90
|
+
if (cfTools.length === 0 || isGemini) {
|
|
91
|
+
return await runModel(messages, config.streamFinalResponse);
|
|
92
|
+
}
|
|
93
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
94
|
+
const response = (await runModel(messages, false));
|
|
95
|
+
// FIX: Robustly extract from BOTH Cloudflare formats (Standard and OpenAI-compatible)
|
|
96
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
97
|
+
let toolCalls = [];
|
|
98
|
+
if (response?.tool_calls) {
|
|
99
|
+
toolCalls = [...response.tool_calls];
|
|
100
|
+
}
|
|
101
|
+
else if (response?.choices?.[0]?.message?.tool_calls) {
|
|
102
|
+
toolCalls = [...response.choices[0].message.tool_calls];
|
|
103
|
+
}
|
|
104
|
+
let responseText = response?.response || response?.choices?.[0]?.message?.content || '';
|
|
105
|
+
// GEMMA/LLAMA FALLBACK: Catch raw tokens if native interception fails
|
|
106
|
+
if (toolCalls.length === 0) {
|
|
107
|
+
const gemmaRegex = /<\|tool_call>\s*call:\s*([a-zA-Z0-9_]+)([\s\S]*?)<tool_call\|>/g;
|
|
108
|
+
const standardRegex = /<tool_call>\s*([\s\S]*?)\s*<\/tool_call>/g;
|
|
109
|
+
let match;
|
|
110
|
+
while ((match = gemmaRegex.exec(responseText)) !== null) {
|
|
111
|
+
let name = match[1].trim();
|
|
112
|
+
if (name === 'http_fetch' || name === 'api_fetch') {
|
|
113
|
+
name = 'fetch';
|
|
114
|
+
}
|
|
115
|
+
let argsString = match[2].trim();
|
|
116
|
+
// Sanitize malformed JSON syntax
|
|
117
|
+
argsString = argsString.replace(/([{,]\s*)([a-zA-Z0-9_]+)\s*:/g, '$1"$2":').replace(/:\s*'([^']*)'/g, ': "$1"');
|
|
118
|
+
toolCalls.push({
|
|
119
|
+
id: `call_${Math.random().toString(36).substring(2, 9)}`,
|
|
120
|
+
type: 'function',
|
|
121
|
+
function: { name, arguments: argsString },
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
while ((match = standardRegex.exec(responseText)) !== null) {
|
|
125
|
+
const content = match[1].trim();
|
|
126
|
+
try {
|
|
127
|
+
// Handle both raw JSON and name/args format
|
|
128
|
+
const parsed = JSON.parse(content.replace(/'/g, '"'));
|
|
129
|
+
const name = parsed.name || 'fetch';
|
|
130
|
+
const args = parsed.arguments || parsed;
|
|
131
|
+
toolCalls.push({
|
|
132
|
+
id: `call_${Math.random().toString(36).substring(2, 9)}`,
|
|
133
|
+
type: 'function',
|
|
134
|
+
function: { name, arguments: typeof args === 'string' ? args : JSON.stringify(args) },
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
catch (e) {
|
|
138
|
+
console.error('Failed to parse tool call:', content, e);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
// Strip the raw tokens from the visible response
|
|
142
|
+
responseText = responseText
|
|
143
|
+
.replace(/<\|tool_call>[\s\S]*?<tool_call\|>/g, '')
|
|
144
|
+
.replace(/<tool_call>[\s\S]*?<\/tool_call>/g, '')
|
|
145
|
+
.trim();
|
|
146
|
+
}
|
|
147
|
+
if (toolCalls.length > 0) {
|
|
148
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
149
|
+
const normalizedToolCalls = toolCalls.map((call, index) => {
|
|
150
|
+
const name = call.name || (call.function && call.function.name);
|
|
151
|
+
let args = call.arguments || (call.function && call.function.arguments);
|
|
152
|
+
if (typeof args !== 'string') {
|
|
153
|
+
try {
|
|
154
|
+
args = JSON.stringify(args);
|
|
155
|
+
}
|
|
156
|
+
catch {
|
|
157
|
+
args = '{}';
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return {
|
|
161
|
+
id: call.id || `call_${Math.random().toString(36).substring(2, 9)}_${index}`,
|
|
162
|
+
type: 'function',
|
|
163
|
+
function: { name, arguments: args },
|
|
164
|
+
};
|
|
165
|
+
});
|
|
166
|
+
messages.push({
|
|
167
|
+
role: 'assistant',
|
|
168
|
+
content: responseText,
|
|
169
|
+
tool_calls: normalizedToolCalls,
|
|
170
|
+
});
|
|
171
|
+
for (const call of normalizedToolCalls) {
|
|
172
|
+
const toolName = call.function.name;
|
|
173
|
+
const toolId = call.id;
|
|
174
|
+
const toolArgsString = call.function.arguments;
|
|
175
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
176
|
+
const tool = tools.find((t) => t.name === toolName);
|
|
177
|
+
if (tool && tool.function) {
|
|
178
|
+
try {
|
|
179
|
+
let parsedArgs;
|
|
180
|
+
try {
|
|
181
|
+
parsedArgs = JSON.parse(toolArgsString);
|
|
182
|
+
}
|
|
183
|
+
catch {
|
|
184
|
+
parsedArgs = toolArgsString;
|
|
185
|
+
}
|
|
186
|
+
const result = await tool.function(parsedArgs);
|
|
187
|
+
messages.push({ role: 'tool', tool_call_id: toolId, name: toolName, content: String(result) });
|
|
188
|
+
}
|
|
189
|
+
catch (e) {
|
|
190
|
+
messages.push({ role: 'tool', tool_call_id: toolId, name: toolName, content: String(e) });
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
else {
|
|
194
|
+
messages.push({ role: 'tool', tool_call_id: toolId, name: toolName, content: 'Tool not found' });
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
return await runModel(messages, true);
|
|
198
|
+
}
|
|
199
|
+
return response;
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Stream AI response to Telegram, with periodic updates to avoid rate limits.
|
|
203
|
+
*/
|
|
204
|
+
export async function streamAiResponseToTelegram(bot,
|
|
205
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
206
|
+
ai, modelId,
|
|
207
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
208
|
+
messages,
|
|
209
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
210
|
+
task,
|
|
211
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
212
|
+
tools = []) {
|
|
213
|
+
const botApi = new TelegramApi();
|
|
214
|
+
// Use updateId as a stable draftId if available, otherwise generate one
|
|
215
|
+
const draftId = task.updateId || Date.now();
|
|
216
|
+
// Skip Thinking message for guest messages as they only support one response
|
|
217
|
+
if (task.updateType !== 'guest_message') {
|
|
218
|
+
await botApi.sendMessageDraft(`https://api.telegram.org/bot${task.telegramToken || task.token}`, {
|
|
219
|
+
chat_id: task.chatId,
|
|
220
|
+
text: 'Thinking...',
|
|
221
|
+
parse_mode: 'HTML',
|
|
222
|
+
message_thread_id: task.threadId,
|
|
223
|
+
business_connection_id: task.businessConnectionId,
|
|
224
|
+
draft_id: draftId,
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
let streamContent = '';
|
|
228
|
+
let lastUpdate = Date.now();
|
|
229
|
+
try {
|
|
230
|
+
const aiResponse = await customRunWithTools(ai, modelId, {
|
|
231
|
+
messages,
|
|
232
|
+
tools,
|
|
233
|
+
}, { streamFinalResponse: true });
|
|
234
|
+
if (typeof aiResponse === 'object' && aiResponse !== null && 'getReader' in aiResponse) {
|
|
235
|
+
const stream = aiResponse;
|
|
236
|
+
const reader = stream.getReader();
|
|
237
|
+
const decoder = new TextDecoder();
|
|
238
|
+
while (true) {
|
|
239
|
+
const { done, value } = await reader.read();
|
|
240
|
+
if (done) {
|
|
241
|
+
break;
|
|
242
|
+
}
|
|
243
|
+
const chunk = decoder.decode(value, { stream: true });
|
|
244
|
+
const lines = chunk.split('\n');
|
|
245
|
+
for (const line of lines) {
|
|
246
|
+
if (line.startsWith('data: ')) {
|
|
247
|
+
const data = line.slice(6);
|
|
248
|
+
if (data === '[DONE]') {
|
|
249
|
+
break;
|
|
250
|
+
}
|
|
251
|
+
try {
|
|
252
|
+
const parsed = JSON.parse(data);
|
|
253
|
+
const text = extractText(parsed);
|
|
254
|
+
streamContent += text;
|
|
255
|
+
}
|
|
256
|
+
catch {
|
|
257
|
+
// Ignore malformed JSON chunks
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
// Update Telegram every 2 seconds to avoid rate limits
|
|
262
|
+
if (task.updateType !== 'guest_message' && Date.now() - lastUpdate > 2000 && streamContent.trim()) {
|
|
263
|
+
const currentContent = streamContent;
|
|
264
|
+
bot
|
|
265
|
+
.streamReply(await markdownToHtml(currentContent + '...'), draftId, 'HTML')
|
|
266
|
+
.catch((e) => console.error('Streaming error:', e));
|
|
267
|
+
lastUpdate = Date.now();
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
else {
|
|
272
|
+
// Handle static response
|
|
273
|
+
streamContent = extractText(aiResponse);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
catch (e) {
|
|
277
|
+
console.error('Error reading AI stream:', e);
|
|
278
|
+
}
|
|
279
|
+
// Send final response (blocking)
|
|
280
|
+
if (streamContent.trim()) {
|
|
281
|
+
await bot.streamReply(await markdownToHtml(streamContent), draftId, 'HTML', {}, true);
|
|
282
|
+
}
|
|
283
|
+
return streamContent;
|
|
284
|
+
}
|
|
285
|
+
/**
|
|
286
|
+
* Creates a mock TelegramExecutionContext for use in environments where the full context isn't available (e.g., Workflows).
|
|
287
|
+
*/
|
|
288
|
+
export function createMockTelegramExecutionContext(task) {
|
|
289
|
+
return {
|
|
290
|
+
chat: { id: task.chatId },
|
|
291
|
+
from: { id: task.userId },
|
|
292
|
+
update_type: task.updateType,
|
|
293
|
+
reply: async (text, options = {}) => {
|
|
294
|
+
const api = new TelegramApi();
|
|
295
|
+
if (task.updateType === 'guest_message' && task.guestQueryId) {
|
|
296
|
+
return await api.answerGuestQuery(`https://api.telegram.org/bot${task.telegramToken || task.token}`, {
|
|
297
|
+
guest_query_id: task.guestQueryId,
|
|
298
|
+
result: {
|
|
299
|
+
type: 'article',
|
|
300
|
+
id: crypto.randomUUID(),
|
|
301
|
+
title: 'Response',
|
|
302
|
+
input_message_content: { message_text: text, parse_mode: (options.parse_mode || 'HTML') },
|
|
303
|
+
},
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
return await api.sendMessage(`https://api.telegram.org/bot${task.telegramToken || task.token}`, {
|
|
307
|
+
chat_id: task.chatId,
|
|
308
|
+
text,
|
|
309
|
+
parse_mode: (options.parse_mode || 'HTML'),
|
|
310
|
+
reply_markup: options.reply_markup,
|
|
311
|
+
message_thread_id: task.threadId,
|
|
312
|
+
business_connection_id: task.businessConnectionId,
|
|
313
|
+
reply_to_message_id: task.messageId,
|
|
314
|
+
});
|
|
315
|
+
},
|
|
316
|
+
streamReply: async (text, draft_id, parse_mode = '', options = {}, finish = false) => {
|
|
317
|
+
const api = new TelegramApi();
|
|
318
|
+
if (task.updateType === 'guest_message' && task.guestQueryId) {
|
|
319
|
+
if (finish) {
|
|
320
|
+
return await api.answerGuestQuery(`https://api.telegram.org/bot${task.telegramToken || task.token}`, {
|
|
321
|
+
guest_query_id: task.guestQueryId,
|
|
322
|
+
result: {
|
|
323
|
+
type: 'article',
|
|
324
|
+
id: crypto.randomUUID(),
|
|
325
|
+
title: 'Response',
|
|
326
|
+
input_message_content: { message_text: text, parse_mode: (parse_mode || 'HTML') },
|
|
327
|
+
},
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
return null;
|
|
331
|
+
}
|
|
332
|
+
if (finish) {
|
|
333
|
+
// Send a final message draft to signal the end of animation
|
|
334
|
+
await api.sendMessageDraft(`https://api.telegram.org/bot${task.telegramToken || task.token}`, {
|
|
335
|
+
chat_id: task.chatId,
|
|
336
|
+
text,
|
|
337
|
+
parse_mode: (parse_mode || 'HTML'),
|
|
338
|
+
draft_id,
|
|
339
|
+
message_thread_id: task.threadId,
|
|
340
|
+
business_connection_id: task.businessConnectionId,
|
|
341
|
+
finish: true,
|
|
342
|
+
...options,
|
|
343
|
+
});
|
|
344
|
+
// Then send the actual final message as a reply
|
|
345
|
+
return await api.sendMessage(`https://api.telegram.org/bot${task.telegramToken || task.token}`, {
|
|
346
|
+
chat_id: task.chatId,
|
|
347
|
+
text,
|
|
348
|
+
parse_mode: (parse_mode || 'HTML'),
|
|
349
|
+
reply_markup: options.reply_markup,
|
|
350
|
+
message_thread_id: task.threadId,
|
|
351
|
+
business_connection_id: task.businessConnectionId,
|
|
352
|
+
reply_to_message_id: task.messageId,
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
return await api.sendMessageDraft(`https://api.telegram.org/bot${task.telegramToken || task.token}`, {
|
|
356
|
+
chat_id: task.chatId,
|
|
357
|
+
text,
|
|
358
|
+
parse_mode: (parse_mode || 'HTML'),
|
|
359
|
+
draft_id,
|
|
360
|
+
message_thread_id: task.threadId,
|
|
361
|
+
business_connection_id: task.businessConnectionId,
|
|
362
|
+
finish: false,
|
|
363
|
+
...options,
|
|
364
|
+
});
|
|
365
|
+
},
|
|
366
|
+
};
|
|
367
|
+
}
|
package/dist/history_manager.js
CHANGED
|
@@ -36,7 +36,7 @@ export class HistoryManager {
|
|
|
36
36
|
history.push({ role: 'assistant', content: response });
|
|
37
37
|
const trimmedHistory = history.slice(-20);
|
|
38
38
|
await this.kv.put(this.getKey(userId, threadId), JSON.stringify(trimmedHistory), {
|
|
39
|
-
expirationTtl: 86400
|
|
39
|
+
expirationTtl: 86400,
|
|
40
40
|
});
|
|
41
41
|
}
|
|
42
42
|
/**
|
package/dist/main.d.ts
CHANGED
|
@@ -9,5 +9,6 @@ import PartialTelegramUpdate from './types/PartialTelegramUpdate.js';
|
|
|
9
9
|
import TelegramInlineQueryType from './types/TelegramInlineQueryType.js';
|
|
10
10
|
import { markdownToHtml, fetchTool } from './utils.js';
|
|
11
11
|
import { HistoryManager, getBalance } from './history_manager.js';
|
|
12
|
+
export { extractText, customRunWithTools, streamAiResponseToTelegram, createMockTelegramExecutionContext } from './ai.js';
|
|
12
13
|
export default TelegramBot;
|
|
13
14
|
export { TelegramBot, TelegramExecutionContext, Webhook, TelegramApi, TelegramApiBaseParams, SendMessageParams, SendMessageDraftParams, SendPhotoParams, SendVideoParams, SendVoiceParams, SendChatActionParams, AnswerCallbackParams, AnswerInlineParams, AnswerGuestParams, SendInvoiceParams, AnswerPreCheckoutParams, TelegramApiParams, TelegramCommand, TelegramFrom, TelegramChat, TelegramUser, TelegramMessageEntity, TelegramPhotoSize, TelegramMessage, TelegramVoice, TelegramGuestMessage, TelegramInputMessageContent, TelegramInlineQuery, TelegramUpdate, PartialTelegramUpdate, TelegramInlineQueryType, TelegramInlineQueryResult, TelegramInlineQueryResultPhoto, TelegramInlineQueryResultArticle, TelegramInlineQueryResultVideo, TelegramInlineQueryResultVoice, ChatPermissions, TelegramBusinessMessage, TelegramCallbackQuery, TelegramPreCheckoutQuery, TelegramDocument, TelegramSuccessfulPayment, markdownToHtml, fetchTool, HistoryManager, getBalance, };
|
package/dist/main.js
CHANGED
|
@@ -4,5 +4,6 @@ import Webhook from './webhook.js';
|
|
|
4
4
|
import TelegramApi from './telegram_api.js';
|
|
5
5
|
import { markdownToHtml, fetchTool } from './utils.js';
|
|
6
6
|
import { HistoryManager, getBalance } from './history_manager.js';
|
|
7
|
+
export { extractText, customRunWithTools, streamAiResponseToTelegram, createMockTelegramExecutionContext } from './ai.js';
|
|
7
8
|
export default TelegramBot;
|
|
8
9
|
export { TelegramBot, TelegramExecutionContext, Webhook, TelegramApi, markdownToHtml, fetchTool, HistoryManager, getBalance, };
|
package/dist/telegram_api.d.ts
CHANGED
|
@@ -9,7 +9,7 @@ export interface TelegramApiBaseParams {
|
|
|
9
9
|
/** Interface for message parameters */
|
|
10
10
|
export interface SendMessageParams extends TelegramApiBaseParams {
|
|
11
11
|
text: string;
|
|
12
|
-
parse_mode
|
|
12
|
+
parse_mode?: string;
|
|
13
13
|
reply_to_message_id?: number | string;
|
|
14
14
|
disable_web_page_preview?: boolean;
|
|
15
15
|
disable_notification?: boolean;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Update as TelegramUpdate, InlineQueryResult as TelegramInlineQueryResult } from '@grammyjs/types';
|
|
2
|
-
import TelegramApi from './telegram_api.js';
|
|
2
|
+
import TelegramApi, { SendMessageParams, SendPhotoParams, SendVideoParams, SendVoiceParams, SendMessageDraftParams } from './telegram_api.js';
|
|
3
3
|
import TelegramBot from './telegram_bot.js';
|
|
4
4
|
/** Class representing the context of execution */
|
|
5
5
|
export default class TelegramExecutionContext {
|
|
@@ -83,7 +83,7 @@ export default class TelegramExecutionContext {
|
|
|
83
83
|
* Helper to handle business connection fallbacks
|
|
84
84
|
*/
|
|
85
85
|
private withBusinessFallback;
|
|
86
|
-
replyVideo(video: string, options?:
|
|
86
|
+
replyVideo(video: string, options?: Partial<SendVideoParams>): Promise<Response | null>;
|
|
87
87
|
/**
|
|
88
88
|
* Get File from telegram file_id
|
|
89
89
|
* @param file_id - telegram file_id
|
|
@@ -97,7 +97,7 @@ export default class TelegramExecutionContext {
|
|
|
97
97
|
* @param options - any additional options to pass to sendPhoto
|
|
98
98
|
* @returns Promise with the API response
|
|
99
99
|
*/
|
|
100
|
-
replyPhoto(photo: string, caption?: string, options?:
|
|
100
|
+
replyPhoto(photo: string, caption?: string, options?: Partial<SendPhotoParams>): Promise<Response | null>;
|
|
101
101
|
/**
|
|
102
102
|
* Reply to the last message with a voice message
|
|
103
103
|
* @param voice - url or file_id to voice
|
|
@@ -105,7 +105,7 @@ export default class TelegramExecutionContext {
|
|
|
105
105
|
* @param options - any additional options to pass to sendVoice
|
|
106
106
|
* @returns Promise with the API response
|
|
107
107
|
*/
|
|
108
|
-
replyVoice(voice: string, caption?: string, options?:
|
|
108
|
+
replyVoice(voice: string, caption?: string, options?: Partial<SendVoiceParams>): Promise<Response | null>;
|
|
109
109
|
/**
|
|
110
110
|
* Send typing in a chat
|
|
111
111
|
* @returns Promise with the API response
|
|
@@ -166,8 +166,8 @@ export default class TelegramExecutionContext {
|
|
|
166
166
|
* @param options - any additional options to pass to sendMessage/editMessageText
|
|
167
167
|
* @returns Promise with the API response
|
|
168
168
|
*/
|
|
169
|
-
streamReply(message: string, draft_id: number, parse_mode?: string, options?:
|
|
170
|
-
reply(message: string, parse_mode?: string, reply?: boolean, options?:
|
|
169
|
+
streamReply(message: string, draft_id: number, parse_mode?: string, options?: Partial<SendMessageDraftParams>, finish?: boolean): Promise<Response | null>;
|
|
170
|
+
reply(message: string, parse_mode?: string, reply?: boolean, options?: Partial<SendMessageParams>): Promise<Response | null>;
|
|
171
171
|
/**
|
|
172
172
|
* Send an invoice for Telegram Stars
|
|
173
173
|
* @param title - product name
|
|
@@ -52,10 +52,7 @@ export default class TelegramExecutionContext {
|
|
|
52
52
|
* @returns True if the sender is a bot
|
|
53
53
|
*/
|
|
54
54
|
get isBot() {
|
|
55
|
-
return (this.update.message?.from?.is_bot ??
|
|
56
|
-
this.update.business_message?.from?.is_bot ??
|
|
57
|
-
this.update.guest_message?.from?.is_bot ??
|
|
58
|
-
false);
|
|
55
|
+
return (this.update.message?.from?.is_bot ?? this.update.business_message?.from?.is_bot ?? this.update.guest_message?.from?.is_bot ?? false);
|
|
59
56
|
}
|
|
60
57
|
/**
|
|
61
58
|
* Parse arguments from the update
|
|
@@ -66,7 +63,7 @@ export default class TelegramExecutionContext {
|
|
|
66
63
|
case 'message':
|
|
67
64
|
case 'business_message':
|
|
68
65
|
case 'guest_message':
|
|
69
|
-
return (this.update.message?.text ?? this.update.business_message?.text ?? this.update.guest_message?.text)?.toString().split(' ') ?? [];
|
|
66
|
+
return ((this.update.message?.text ?? this.update.business_message?.text ?? this.update.guest_message?.text)?.toString().split(' ') ?? []);
|
|
70
67
|
case 'inline':
|
|
71
68
|
return this.update.inline_query?.query.split(' ') ?? [];
|
|
72
69
|
default:
|
|
@@ -97,7 +94,7 @@ export default class TelegramExecutionContext {
|
|
|
97
94
|
try {
|
|
98
95
|
const response = await this.api.getBusinessConnection(this.bot.api.toString(), connectionId);
|
|
99
96
|
if (response.status === 200) {
|
|
100
|
-
const json = await response.json();
|
|
97
|
+
const json = (await response.json());
|
|
101
98
|
if (json.ok && json.result) {
|
|
102
99
|
ownerId = json.result.user?.id || json.result.user_chat_id;
|
|
103
100
|
if (ownerId) {
|
|
@@ -217,7 +214,7 @@ export default class TelegramExecutionContext {
|
|
|
217
214
|
try {
|
|
218
215
|
const response = await this.api.getBusinessConnection(this.bot.api.toString(), connectionId);
|
|
219
216
|
if (response.status === 200) {
|
|
220
|
-
const json = await response.json();
|
|
217
|
+
const json = (await response.json());
|
|
221
218
|
if (json.ok && json.result) {
|
|
222
219
|
ownerId = json.result.user?.id || json.result.user_chat_id;
|
|
223
220
|
if (ownerId) {
|
|
@@ -278,7 +275,9 @@ export default class TelegramExecutionContext {
|
|
|
278
275
|
return await this.api.answerInline(this.bot.api.toString(), {
|
|
279
276
|
...options,
|
|
280
277
|
inline_query_id: this.update.inline_query?.id.toString() ?? '',
|
|
281
|
-
results: [
|
|
278
|
+
results: [
|
|
279
|
+
{ type: 'video', id: crypto.randomUUID(), video_url: video, mime_type: 'video/mp4', thumbnail_url: video, title: 'Video' },
|
|
280
|
+
],
|
|
282
281
|
});
|
|
283
282
|
}
|
|
284
283
|
return await this.api.sendVideo(this.bot.api.toString(), params);
|
|
@@ -377,7 +376,14 @@ export default class TelegramExecutionContext {
|
|
|
377
376
|
if (this.update_type === 'inline') {
|
|
378
377
|
return await this.api.answerInline(this.bot.api.toString(), {
|
|
379
378
|
inline_query_id: this.update.inline_query?.id.toString() ?? '',
|
|
380
|
-
results: [
|
|
379
|
+
results: [
|
|
380
|
+
{
|
|
381
|
+
type: 'article',
|
|
382
|
+
id: crypto.randomUUID(),
|
|
383
|
+
title: title ?? '',
|
|
384
|
+
input_message_content: { message_text: message, parse_mode: parse_mode },
|
|
385
|
+
},
|
|
386
|
+
],
|
|
381
387
|
});
|
|
382
388
|
}
|
|
383
389
|
return null;
|
|
@@ -400,7 +406,12 @@ export default class TelegramExecutionContext {
|
|
|
400
406
|
* @returns Promise with the API response
|
|
401
407
|
*/
|
|
402
408
|
async answerGuestQueryText(message, parse_mode = '') {
|
|
403
|
-
return await this.answerGuestQuery({
|
|
409
|
+
return await this.answerGuestQuery({
|
|
410
|
+
type: 'article',
|
|
411
|
+
id: crypto.randomUUID(),
|
|
412
|
+
title: 'Response',
|
|
413
|
+
input_message_content: { message_text: message, parse_mode: parse_mode },
|
|
414
|
+
});
|
|
404
415
|
}
|
|
405
416
|
/**
|
|
406
417
|
* Answer a guest query with a photo
|
|
@@ -410,7 +421,14 @@ export default class TelegramExecutionContext {
|
|
|
410
421
|
* @returns Promise with the API response
|
|
411
422
|
*/
|
|
412
423
|
async answerGuestQueryPhoto(photo, caption = '', parse_mode = '') {
|
|
413
|
-
return await this.answerGuestQuery({
|
|
424
|
+
return await this.answerGuestQuery({
|
|
425
|
+
type: 'photo',
|
|
426
|
+
id: crypto.randomUUID(),
|
|
427
|
+
photo_url: photo,
|
|
428
|
+
thumbnail_url: photo,
|
|
429
|
+
caption,
|
|
430
|
+
parse_mode: parse_mode,
|
|
431
|
+
});
|
|
414
432
|
}
|
|
415
433
|
/**
|
|
416
434
|
* Answer a guest query with a video
|
|
@@ -420,7 +438,16 @@ export default class TelegramExecutionContext {
|
|
|
420
438
|
* @returns Promise with the API response
|
|
421
439
|
*/
|
|
422
440
|
async answerGuestQueryVideo(video, caption = '', parse_mode = '') {
|
|
423
|
-
return await this.answerGuestQuery({
|
|
441
|
+
return await this.answerGuestQuery({
|
|
442
|
+
type: 'video',
|
|
443
|
+
id: crypto.randomUUID(),
|
|
444
|
+
video_url: video,
|
|
445
|
+
mime_type: 'video/mp4',
|
|
446
|
+
thumbnail_url: video,
|
|
447
|
+
title: 'Video',
|
|
448
|
+
caption,
|
|
449
|
+
parse_mode: parse_mode,
|
|
450
|
+
});
|
|
424
451
|
}
|
|
425
452
|
/**
|
|
426
453
|
* Answer a guest query with a voice message
|
|
@@ -430,7 +457,14 @@ export default class TelegramExecutionContext {
|
|
|
430
457
|
* @returns Promise with the API response
|
|
431
458
|
*/
|
|
432
459
|
async answerGuestQueryVoice(voice, caption = '', parse_mode = '') {
|
|
433
|
-
return await this.answerGuestQuery({
|
|
460
|
+
return await this.answerGuestQuery({
|
|
461
|
+
type: 'voice',
|
|
462
|
+
id: crypto.randomUUID(),
|
|
463
|
+
voice_url: voice,
|
|
464
|
+
title: 'Voice',
|
|
465
|
+
caption,
|
|
466
|
+
parse_mode: parse_mode,
|
|
467
|
+
});
|
|
434
468
|
}
|
|
435
469
|
/** Map of draft IDs to message IDs for streaming */
|
|
436
470
|
drafts = new Map();
|
package/dist/utils.js
CHANGED
|
@@ -21,7 +21,7 @@ export async function markdownToHtml(s) {
|
|
|
21
21
|
let result = '';
|
|
22
22
|
for (let i = 0; i < items.length; i++) {
|
|
23
23
|
const item = items[i];
|
|
24
|
-
const prefix = ordered ? `${
|
|
24
|
+
const prefix = ordered ? `${start !== '' && start !== undefined ? Number(start) + i : i + 1}. ` : '• ';
|
|
25
25
|
result += `${prefix}${renderer.listitem(item)}\n`;
|
|
26
26
|
}
|
|
27
27
|
return result;
|
|
@@ -50,8 +50,21 @@ export async function markdownToHtml(s) {
|
|
|
50
50
|
// html tag pass-through for supported tags or escaping
|
|
51
51
|
renderer.html = ({ text }) => {
|
|
52
52
|
const allowedTags = [
|
|
53
|
-
'b',
|
|
54
|
-
'
|
|
53
|
+
'b',
|
|
54
|
+
'strong',
|
|
55
|
+
'i',
|
|
56
|
+
'em',
|
|
57
|
+
'u',
|
|
58
|
+
'ins',
|
|
59
|
+
's',
|
|
60
|
+
'strike',
|
|
61
|
+
'del',
|
|
62
|
+
'span',
|
|
63
|
+
'tg-spoiler',
|
|
64
|
+
'a',
|
|
65
|
+
'code',
|
|
66
|
+
'pre',
|
|
67
|
+
'blockquote',
|
|
55
68
|
];
|
|
56
69
|
const match = /^<\/?([a-z0-9-]+)(?:\s+[^>]*)?>/i.exec(text);
|
|
57
70
|
if (match) {
|
|
@@ -87,9 +100,9 @@ export const fetchTool = {
|
|
|
87
100
|
url: { type: 'string', description: 'The URL to fetch' },
|
|
88
101
|
method: { type: 'string', enum: ['GET', 'POST', 'PUT', 'DELETE'], default: 'GET' },
|
|
89
102
|
headers: { type: 'object', description: 'HTTP headers to include in the request' },
|
|
90
|
-
body: { type: 'string', description: 'The request body' }
|
|
103
|
+
body: { type: 'string', description: 'The request body' },
|
|
91
104
|
},
|
|
92
|
-
required: ['url']
|
|
105
|
+
required: ['url'],
|
|
93
106
|
},
|
|
94
107
|
function: async ({ url, method, headers, body }) => {
|
|
95
108
|
try {
|
|
@@ -97,9 +110,9 @@ export const fetchTool = {
|
|
|
97
110
|
method: method || 'GET',
|
|
98
111
|
headers: {
|
|
99
112
|
'User-Agent': 'Mozilla/5.0 (Cloudflare Worker Telegram Bot)',
|
|
100
|
-
...headers
|
|
113
|
+
...headers,
|
|
101
114
|
},
|
|
102
|
-
body: body ? (typeof body === 'string' ? body : JSON.stringify(body)) : undefined
|
|
115
|
+
body: body ? (typeof body === 'string' ? body : JSON.stringify(body)) : undefined,
|
|
103
116
|
});
|
|
104
117
|
const text = await res.text();
|
|
105
118
|
return text.slice(0, 10000);
|
|
@@ -107,5 +120,5 @@ export const fetchTool = {
|
|
|
107
120
|
catch (e) {
|
|
108
121
|
return `Error executing fetch: ${String(e)}`;
|
|
109
122
|
}
|
|
110
|
-
}
|
|
123
|
+
},
|
|
111
124
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@codebam/cf-workers-telegram-bot",
|
|
3
|
-
"version": "12.6.
|
|
3
|
+
"version": "12.6.5",
|
|
4
4
|
"description": "serverless telegram bot on cf workers",
|
|
5
5
|
"main": "./dist/main.js",
|
|
6
6
|
"module": "./dist/main.js",
|
|
@@ -22,11 +22,18 @@
|
|
|
22
22
|
},
|
|
23
23
|
"scripts": {
|
|
24
24
|
"build": "tsc --project tsconfig.json",
|
|
25
|
+
"build:all": "npm run build && npm run build --prefix ai-workflow && npm run build --prefix consumer && npm run build --prefix webapp",
|
|
25
26
|
"lint": "eslint src",
|
|
27
|
+
"lint:all": "npm run lint && npm run lint --prefix ai-workflow && npm run lint --prefix consumer && npm run lint --prefix webapp",
|
|
26
28
|
"test": "vitest --config vitest.config.js",
|
|
29
|
+
"test:all": "npm test && npm test --prefix ai-workflow && npm test --prefix consumer && npm test --prefix webapp",
|
|
30
|
+
"format": "prettier --write src test *.json *.js *.mjs",
|
|
31
|
+
"format:check": "prettier --check src test *.json *.js *.mjs",
|
|
27
32
|
"docs": "typedoc --options typedoc.json",
|
|
28
33
|
"deploy:docs": "npm run docs && wrangler pages deploy docs",
|
|
29
|
-
"ncu": "npx npm-check-updates -
|
|
34
|
+
"ncu": "npx npm-check-updates -u --root",
|
|
35
|
+
"ncu:interactive": "npx npm-check-updates -i --root",
|
|
36
|
+
"prepare": "./setup_hooks.sh"
|
|
30
37
|
},
|
|
31
38
|
"author": "codebam",
|
|
32
39
|
"license": "Apache-2.0",
|
|
@@ -35,13 +42,16 @@
|
|
|
35
42
|
"url": "git+https://github.com/codebam/cf-workers-telegram-bot.git"
|
|
36
43
|
},
|
|
37
44
|
"devDependencies": {
|
|
38
|
-
"@cloudflare/workers
|
|
45
|
+
"@cloudflare/vitest-pool-workers": "^0.16.6",
|
|
46
|
+
"@eslint/compat": "^2.1.0",
|
|
39
47
|
"@eslint/js": "^10.0.1",
|
|
48
|
+
"@types/node": "^25.8.0",
|
|
40
49
|
"@typescript-eslint/eslint-plugin": "^8.59.3",
|
|
41
50
|
"@typescript-eslint/parser": "^8.59.3",
|
|
42
|
-
"eslint": "^10.
|
|
51
|
+
"eslint": "^10.4.0",
|
|
43
52
|
"eslint-config-prettier": "^10.1.8",
|
|
44
53
|
"globals": "^17.6.0",
|
|
54
|
+
"lint-staged": "^17.0.5",
|
|
45
55
|
"npm-check-updates": "^22.2.0",
|
|
46
56
|
"prettier": "^3.8.3",
|
|
47
57
|
"typedoc": "^0.28.19",
|
|
@@ -49,16 +59,22 @@
|
|
|
49
59
|
"typescript": "^6.0.3",
|
|
50
60
|
"typescript-eslint": "^8.59.3",
|
|
51
61
|
"vitest": "^4.1.6",
|
|
52
|
-
"wrangler": "^4.
|
|
62
|
+
"wrangler": "^4.92.0"
|
|
53
63
|
},
|
|
54
64
|
"dependencies": {
|
|
55
65
|
"@eslint/eslintrc": "^3.3.5",
|
|
56
|
-
"@grammyjs/types": "^3.27.
|
|
66
|
+
"@grammyjs/types": "^3.27.3",
|
|
57
67
|
"marked": "^18.0.3"
|
|
58
68
|
},
|
|
59
69
|
"typedocOptions": {
|
|
60
70
|
"entryPoints": [
|
|
61
71
|
"./src/main.ts"
|
|
62
72
|
]
|
|
73
|
+
},
|
|
74
|
+
"lint-staged": {
|
|
75
|
+
"*.{ts,js,mjs,json}": [
|
|
76
|
+
"prettier --write",
|
|
77
|
+
"eslint --fix"
|
|
78
|
+
]
|
|
63
79
|
}
|
|
64
80
|
}
|