telegix 1.1.1 → 1.1.2
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 +539 -63
- package/index.cjs +6305 -0
- package/index.d.ts +120 -0
- package/index.js +11 -2
- package/lib/api.js +97 -19
- package/lib/context.js +111 -0
- package/lib/format.js +6 -0
- package/lib/link-preview.js +256 -0
- package/lib/markdownv2.js +3 -0
- package/lib/markup.js +116 -4
- package/lib/rich.js +54 -0
- package/lib/stream.js +251 -0
- package/lib/webapp.js +511 -2
- package/package.json +1 -1
package/lib/stream.js
ADDED
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Telegix - Text Streaming Engine for Telegram Bots
|
|
3
|
+
* Handles smooth real-time response streaming (LLM tokens, AI chats, live output)
|
|
4
|
+
* with rate-limit protection, edit throttling, and live draft mode.
|
|
5
|
+
* @module telegix/stream
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Sleeps for the specified number of milliseconds
|
|
10
|
+
* @param {number} ms
|
|
11
|
+
* @returns {Promise<void>}
|
|
12
|
+
*/
|
|
13
|
+
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Converts an arbitrary stream/iterable input into an AsyncIterable of string chunks
|
|
17
|
+
* @param {AsyncIterable<string>|Iterable<string>|ReadableStream|Array<string>} input
|
|
18
|
+
* @returns {AsyncIterable<string>}
|
|
19
|
+
*/
|
|
20
|
+
export async function* toTextStream(input) {
|
|
21
|
+
if (!input) return;
|
|
22
|
+
|
|
23
|
+
// Handle strings directly
|
|
24
|
+
if (typeof input === 'string') {
|
|
25
|
+
yield input;
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Handle web ReadableStream (e.g. fetch response.body, OpenAI / Gemini streams)
|
|
30
|
+
if (typeof input.getReader === 'function') {
|
|
31
|
+
const reader = input.getReader();
|
|
32
|
+
const decoder = new TextDecoder();
|
|
33
|
+
try {
|
|
34
|
+
while (true) {
|
|
35
|
+
const { done, value } = await reader.read();
|
|
36
|
+
if (done) break;
|
|
37
|
+
if (typeof value === 'string') {
|
|
38
|
+
yield value;
|
|
39
|
+
} else if (value) {
|
|
40
|
+
yield decoder.decode(value, { stream: true });
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
} finally {
|
|
44
|
+
reader.releaseLock();
|
|
45
|
+
}
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Handle AsyncIterable (e.g. ai sdk, openai stream, gemini stream)
|
|
50
|
+
if (typeof input[Symbol.asyncIterator] === 'function') {
|
|
51
|
+
for await (const chunk of input) {
|
|
52
|
+
if (chunk === null || chunk === undefined) continue;
|
|
53
|
+
if (typeof chunk === 'string') {
|
|
54
|
+
yield chunk;
|
|
55
|
+
} else if (typeof chunk?.text === 'string') {
|
|
56
|
+
yield chunk.text;
|
|
57
|
+
} else if (typeof chunk?.content === 'string') {
|
|
58
|
+
yield chunk.content;
|
|
59
|
+
} else if (chunk?.choices?.[0]?.delta?.content) {
|
|
60
|
+
yield chunk.choices[0].delta.content;
|
|
61
|
+
} else if (chunk?.candidates?.[0]?.content?.parts?.[0]?.text) {
|
|
62
|
+
yield chunk.candidates[0].content.parts[0].text;
|
|
63
|
+
} else {
|
|
64
|
+
yield String(chunk);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Handle standard synchronous Iterable (Array, Set, Generator)
|
|
71
|
+
if (typeof input[Symbol.iterator] === 'function') {
|
|
72
|
+
for (const chunk of input) {
|
|
73
|
+
if (chunk !== null && chunk !== undefined) {
|
|
74
|
+
yield String(chunk);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Fallback for single object
|
|
81
|
+
yield String(input);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Stream text to a Telegram chat with rate-limit throttling and edit buffering
|
|
86
|
+
* @param {object} telegram - Telegix Telegram API instance
|
|
87
|
+
* @param {number|string} chatId - Target chat ID
|
|
88
|
+
* @param {AsyncIterable<string>|Iterable<string>|ReadableStream|Array<string>} textStream - Text stream or token source
|
|
89
|
+
* @param {object} [options]
|
|
90
|
+
* @param {string} [options.initialPlaceholder='⏳ Thinking...'] - Initial placeholder text
|
|
91
|
+
* @param {number} [options.intervalMs=600] - Throttle interval between message edits in milliseconds (default: 600ms)
|
|
92
|
+
* @param {number} [options.minDeltaChars=1] - Minimum new characters before attempting an edit
|
|
93
|
+
* @param {string} [options.parse_mode] - Parse mode for final message ('HTML', 'MarkdownV2', etc.)
|
|
94
|
+
* @param {object} [options.reply_markup] - Inline keyboard to attach upon stream completion
|
|
95
|
+
* @param {boolean} [options.useDraft=false] - Use live draft action (sendMessageDraft) instead of message editing
|
|
96
|
+
* @param {AbortSignal} [options.signal] - AbortSignal to cancel streaming
|
|
97
|
+
* @param {function} [options.onChunk] - Hook called when a new chunk is received: (currentFullText, chunk)
|
|
98
|
+
* @param {function} [options.onEdit] - Hook called when Telegram message is edited: (messageId, currentText)
|
|
99
|
+
* @returns {Promise<{ message_id?: number, text: string, done: boolean }>}
|
|
100
|
+
*/
|
|
101
|
+
export async function streamText(telegram, chatId, textStream, options = {}) {
|
|
102
|
+
const {
|
|
103
|
+
initialPlaceholder = '⏳ Thinking...',
|
|
104
|
+
intervalMs = 600,
|
|
105
|
+
minDeltaChars = 1,
|
|
106
|
+
parse_mode,
|
|
107
|
+
reply_markup,
|
|
108
|
+
useDraft = false,
|
|
109
|
+
signal,
|
|
110
|
+
onChunk,
|
|
111
|
+
onEdit,
|
|
112
|
+
...extra
|
|
113
|
+
} = options;
|
|
114
|
+
|
|
115
|
+
if (!telegram || typeof telegram.call !== 'function') {
|
|
116
|
+
throw new Error('Telegix streamText: valid telegram API instance is required.');
|
|
117
|
+
}
|
|
118
|
+
if (!chatId) {
|
|
119
|
+
throw new Error('Telegix streamText: chatId is required.');
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
let accumulatedText = '';
|
|
123
|
+
let lastSentText = '';
|
|
124
|
+
let messageId = null;
|
|
125
|
+
let lastEditTime = 0;
|
|
126
|
+
let draftId = useDraft ? Math.floor(Math.random() * 2147483647) + 1 : null;
|
|
127
|
+
|
|
128
|
+
const sendMsg = (cid, txt, opt = {}) => {
|
|
129
|
+
if (typeof telegram.sendMessage === 'function') {
|
|
130
|
+
return telegram.sendMessage(cid, txt, opt);
|
|
131
|
+
}
|
|
132
|
+
return telegram.call('sendMessage', { chat_id: cid, text: txt, ...opt });
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
const editMsg = (cid, mid, txt, opt = {}) => {
|
|
136
|
+
if (typeof telegram.editMessageText === 'function') {
|
|
137
|
+
return telegram.editMessageText(cid, mid, null, txt, opt);
|
|
138
|
+
}
|
|
139
|
+
return telegram.call('editMessageText', { chat_id: cid, message_id: mid, text: txt, ...opt });
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
const sendDraftMsg = (cid, txt, opt = {}) => {
|
|
143
|
+
if (typeof telegram.sendMessageDraft === 'function') {
|
|
144
|
+
return telegram.sendMessageDraft(cid, txt, opt);
|
|
145
|
+
}
|
|
146
|
+
return telegram.call('sendMessageDraft', { chat_id: cid, text: txt, ...opt });
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
// Mode 1: Live Draft Streaming (Bot API live draft preview)
|
|
150
|
+
if (useDraft) {
|
|
151
|
+
for await (const chunk of toTextStream(textStream)) {
|
|
152
|
+
if (signal?.aborted) break;
|
|
153
|
+
accumulatedText += chunk;
|
|
154
|
+
if (typeof onChunk === 'function') onChunk(accumulatedText, chunk);
|
|
155
|
+
|
|
156
|
+
const now = Date.now();
|
|
157
|
+
if (now - lastEditTime >= intervalMs && accumulatedText.length - lastSentText.length >= minDeltaChars) {
|
|
158
|
+
lastEditTime = now;
|
|
159
|
+
lastSentText = accumulatedText;
|
|
160
|
+
try {
|
|
161
|
+
await sendDraftMsg(chatId, accumulatedText, { draft_id: draftId, ...extra });
|
|
162
|
+
} catch {
|
|
163
|
+
// Non-critical, ignore transient draft update errors
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Finalize: send complete message to chat
|
|
169
|
+
const finalMsg = await sendMsg(chatId, accumulatedText || '...', {
|
|
170
|
+
parse_mode,
|
|
171
|
+
reply_markup,
|
|
172
|
+
...extra,
|
|
173
|
+
});
|
|
174
|
+
return {
|
|
175
|
+
message_id: finalMsg?.message_id,
|
|
176
|
+
text: accumulatedText,
|
|
177
|
+
done: true,
|
|
178
|
+
message: finalMsg,
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Mode 2: Real-time Message Edit Streaming
|
|
183
|
+
// Step 1: Send placeholder message
|
|
184
|
+
const initialMsg = await sendMsg(chatId, initialPlaceholder, {
|
|
185
|
+
...extra,
|
|
186
|
+
});
|
|
187
|
+
messageId = initialMsg?.message_id;
|
|
188
|
+
|
|
189
|
+
try {
|
|
190
|
+
for await (const chunk of toTextStream(textStream)) {
|
|
191
|
+
if (signal?.aborted) break;
|
|
192
|
+
accumulatedText += chunk;
|
|
193
|
+
if (typeof onChunk === 'function') onChunk(accumulatedText, chunk);
|
|
194
|
+
|
|
195
|
+
const now = Date.now();
|
|
196
|
+
const timeSinceLastEdit = now - lastEditTime;
|
|
197
|
+
const charDelta = accumulatedText.length - lastSentText.length;
|
|
198
|
+
|
|
199
|
+
if (timeSinceLastEdit >= intervalMs && charDelta >= minDeltaChars) {
|
|
200
|
+
lastEditTime = now;
|
|
201
|
+
lastSentText = accumulatedText;
|
|
202
|
+
try {
|
|
203
|
+
await editMsg(chatId, messageId, accumulatedText);
|
|
204
|
+
if (typeof onEdit === 'function') onEdit(messageId, accumulatedText);
|
|
205
|
+
} catch (err) {
|
|
206
|
+
// Ignore "message is not modified" errors from Telegram
|
|
207
|
+
if (!err?.message?.includes('message is not modified')) {
|
|
208
|
+
// If rate limited, back off slightly
|
|
209
|
+
if (err?.parameters?.retry_after) {
|
|
210
|
+
await sleep(err.parameters.retry_after * 1000);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Step 2: Final message update with final text, parse_mode, and reply_markup
|
|
218
|
+
const finalText = accumulatedText.trim() || ' ';
|
|
219
|
+
if (finalText !== lastSentText || reply_markup || parse_mode) {
|
|
220
|
+
try {
|
|
221
|
+
await editMsg(chatId, messageId, finalText, {
|
|
222
|
+
parse_mode,
|
|
223
|
+
reply_markup,
|
|
224
|
+
...extra,
|
|
225
|
+
});
|
|
226
|
+
} catch (err) {
|
|
227
|
+
if (!err?.message?.includes('message is not modified')) {
|
|
228
|
+
// Try sending as fallback if edit failed
|
|
229
|
+
// e.g. text formatting error
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
return {
|
|
235
|
+
message_id: messageId,
|
|
236
|
+
text: accumulatedText,
|
|
237
|
+
done: true,
|
|
238
|
+
message: initialMsg,
|
|
239
|
+
};
|
|
240
|
+
} catch (err) {
|
|
241
|
+
// If stream fails, ensure we leave clean text if possible
|
|
242
|
+
if (messageId && accumulatedText) {
|
|
243
|
+
try {
|
|
244
|
+
await telegram.editMessageText(chatId, messageId, null, accumulatedText);
|
|
245
|
+
} catch {
|
|
246
|
+
// Ignore fallback error
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
throw err;
|
|
250
|
+
}
|
|
251
|
+
}
|