opc-agent 4.0.8 → 4.0.9
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/dist/channels/telegram.d.ts +5 -0
- package/dist/channels/telegram.js +122 -12
- package/dist/core/runtime.js +13 -2
- package/package.json +1 -1
- package/src/channels/telegram.ts +123 -12
- package/src/core/runtime.ts +13 -2
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { Message } from '../core/types';
|
|
1
2
|
import { BaseChannel } from './index';
|
|
2
3
|
/**
|
|
3
4
|
* Telegram channel — supports both long-polling and webhook modes.
|
|
@@ -35,6 +36,10 @@ export declare class TelegramChannel extends BaseChannel {
|
|
|
35
36
|
private startWebhook;
|
|
36
37
|
private stopWebhook;
|
|
37
38
|
private processUpdate;
|
|
39
|
+
private streamResponse;
|
|
40
|
+
private streamHandler?;
|
|
41
|
+
setStreamHandler(handler: (msg: Message) => AsyncIterable<string>): void;
|
|
42
|
+
sendMarkdown(chatId: number | string, text: string, replyTo?: number): Promise<any>;
|
|
38
43
|
sendMessage(chatId: number | string, text: string): Promise<void>;
|
|
39
44
|
private apiCall;
|
|
40
45
|
private splitText;
|
|
@@ -152,12 +152,21 @@ class TelegramChannel extends index_1.BaseChannel {
|
|
|
152
152
|
// ─── Shared ──────────────────────────────────────────────
|
|
153
153
|
async processUpdate(update) {
|
|
154
154
|
const message = update.message || update.edited_message;
|
|
155
|
-
if (!message
|
|
155
|
+
if (!message || !this.handler)
|
|
156
|
+
return;
|
|
157
|
+
// Handle /start command
|
|
158
|
+
if (message.text === '/start') {
|
|
159
|
+
await this.sendMarkdown(message.chat.id, '👋 Hello! I\'m ready to help. Send me a message to get started.');
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
// Handle text, photo captions, document captions
|
|
163
|
+
const text = message.text || message.caption;
|
|
164
|
+
if (!text)
|
|
156
165
|
return;
|
|
157
166
|
const msg = {
|
|
158
167
|
id: `tg_${message.message_id}`,
|
|
159
168
|
role: 'user',
|
|
160
|
-
content:
|
|
169
|
+
content: text,
|
|
161
170
|
timestamp: message.date * 1000,
|
|
162
171
|
metadata: {
|
|
163
172
|
sessionId: `tg_${message.chat.id}`,
|
|
@@ -167,31 +176,132 @@ class TelegramChannel extends index_1.BaseChannel {
|
|
|
167
176
|
firstName: message.from?.first_name,
|
|
168
177
|
platform: 'telegram',
|
|
169
178
|
chatType: message.chat.type,
|
|
179
|
+
replyToMessageId: message.message_id,
|
|
170
180
|
},
|
|
171
181
|
};
|
|
172
|
-
// Show typing indicator
|
|
182
|
+
// Show typing indicator
|
|
173
183
|
await this.apiCall('sendChatAction', { chat_id: message.chat.id, action: 'typing' });
|
|
174
184
|
const typingInterval = setInterval(() => {
|
|
175
185
|
this.apiCall('sendChatAction', { chat_id: message.chat.id, action: 'typing' }).catch(() => { });
|
|
176
186
|
}, 4000);
|
|
177
187
|
try {
|
|
178
|
-
|
|
179
|
-
|
|
188
|
+
// Try streaming if provider supports it
|
|
189
|
+
if (this.streamHandler) {
|
|
190
|
+
await this.streamResponse(message.chat.id, msg, message.message_id);
|
|
191
|
+
}
|
|
192
|
+
else {
|
|
193
|
+
const response = await this.handler(msg);
|
|
194
|
+
await this.sendMarkdown(message.chat.id, response.content, message.message_id);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
catch (err) {
|
|
198
|
+
console.error('[TelegramChannel] Error processing message:', err);
|
|
199
|
+
await this.sendMarkdown(message.chat.id, '⚠️ Sorry, something went wrong. Please try again.');
|
|
180
200
|
}
|
|
181
201
|
finally {
|
|
182
202
|
clearInterval(typingInterval);
|
|
183
203
|
}
|
|
184
204
|
}
|
|
185
|
-
async
|
|
186
|
-
|
|
205
|
+
async streamResponse(chatId, msg, replyTo) {
|
|
206
|
+
if (!this.streamHandler)
|
|
207
|
+
return;
|
|
208
|
+
let sentMessageId = null;
|
|
209
|
+
let fullText = '';
|
|
210
|
+
let lastEditTime = 0;
|
|
211
|
+
const EDIT_INTERVAL = 1000; // Edit at most once per second
|
|
212
|
+
let pendingEdit = false;
|
|
213
|
+
const doEdit = async () => {
|
|
214
|
+
if (!sentMessageId || !fullText)
|
|
215
|
+
return;
|
|
216
|
+
pendingEdit = false;
|
|
217
|
+
try {
|
|
218
|
+
await this.apiCall('editMessageText', {
|
|
219
|
+
chat_id: chatId,
|
|
220
|
+
message_id: sentMessageId,
|
|
221
|
+
text: fullText,
|
|
222
|
+
parse_mode: 'Markdown',
|
|
223
|
+
});
|
|
224
|
+
lastEditTime = Date.now();
|
|
225
|
+
}
|
|
226
|
+
catch (err) {
|
|
227
|
+
// If Markdown fails, try plain text
|
|
228
|
+
if (err?.message?.includes('parse') || err?.message?.includes('entities')) {
|
|
229
|
+
try {
|
|
230
|
+
await this.apiCall('editMessageText', {
|
|
231
|
+
chat_id: chatId,
|
|
232
|
+
message_id: sentMessageId,
|
|
233
|
+
text: fullText,
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
catch { }
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
};
|
|
240
|
+
try {
|
|
241
|
+
for await (const chunk of this.streamHandler(msg)) {
|
|
242
|
+
fullText += chunk;
|
|
243
|
+
if (!sentMessageId) {
|
|
244
|
+
// Send first message
|
|
245
|
+
const result = await this.sendMarkdown(chatId, fullText, replyTo);
|
|
246
|
+
sentMessageId = result?.message_id;
|
|
247
|
+
lastEditTime = Date.now();
|
|
248
|
+
}
|
|
249
|
+
else {
|
|
250
|
+
const now = Date.now();
|
|
251
|
+
if (now - lastEditTime >= EDIT_INTERVAL) {
|
|
252
|
+
await doEdit();
|
|
253
|
+
}
|
|
254
|
+
else if (!pendingEdit) {
|
|
255
|
+
pendingEdit = true;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
// Final edit with complete text
|
|
260
|
+
if (sentMessageId && fullText) {
|
|
261
|
+
await doEdit();
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
catch (err) {
|
|
265
|
+
// If streaming fails, send what we have
|
|
266
|
+
if (!sentMessageId && fullText) {
|
|
267
|
+
await this.sendMarkdown(chatId, fullText, replyTo);
|
|
268
|
+
}
|
|
269
|
+
throw err;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
// Stream handler — set by runtime when provider supports streaming
|
|
273
|
+
streamHandler;
|
|
274
|
+
setStreamHandler(handler) {
|
|
275
|
+
this.streamHandler = handler;
|
|
276
|
+
}
|
|
277
|
+
async sendMarkdown(chatId, text, replyTo) {
|
|
187
278
|
const chunks = this.splitText(text, 4096);
|
|
279
|
+
let lastResult = null;
|
|
188
280
|
for (const chunk of chunks) {
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
281
|
+
try {
|
|
282
|
+
lastResult = await this.apiCall('sendMessage', {
|
|
283
|
+
chat_id: chatId,
|
|
284
|
+
text: chunk,
|
|
285
|
+
parse_mode: 'Markdown',
|
|
286
|
+
...(replyTo ? { reply_to_message_id: replyTo } : {}),
|
|
287
|
+
});
|
|
288
|
+
// Only reply to first chunk
|
|
289
|
+
replyTo = undefined;
|
|
290
|
+
}
|
|
291
|
+
catch (err) {
|
|
292
|
+
// Markdown parse failed — send as plain text
|
|
293
|
+
lastResult = await this.apiCall('sendMessage', {
|
|
294
|
+
chat_id: chatId,
|
|
295
|
+
text: chunk,
|
|
296
|
+
...(replyTo ? { reply_to_message_id: replyTo } : {}),
|
|
297
|
+
});
|
|
298
|
+
replyTo = undefined;
|
|
299
|
+
}
|
|
194
300
|
}
|
|
301
|
+
return lastResult?.result;
|
|
302
|
+
}
|
|
303
|
+
async sendMessage(chatId, text) {
|
|
304
|
+
await this.sendMarkdown(chatId, text);
|
|
195
305
|
}
|
|
196
306
|
async apiCall(method, body) {
|
|
197
307
|
const url = `https://api.telegram.org/bot${this.token}/${method}`;
|
package/dist/core/runtime.js
CHANGED
|
@@ -165,10 +165,21 @@ class AgentRuntime {
|
|
|
165
165
|
this.logger.info('Bound web channel', { port });
|
|
166
166
|
}
|
|
167
167
|
else if (ch.type === 'telegram') {
|
|
168
|
-
|
|
168
|
+
const tgChannel = new telegram_1.TelegramChannel({
|
|
169
169
|
token: ch.config?.token,
|
|
170
170
|
port: ch.port,
|
|
171
|
-
})
|
|
171
|
+
});
|
|
172
|
+
// Wire up streaming for real-time message editing
|
|
173
|
+
const agentRef = this.agent;
|
|
174
|
+
tgChannel.setStreamHandler(async function* (msg) {
|
|
175
|
+
const history = [msg];
|
|
176
|
+
const provider = agentRef.provider;
|
|
177
|
+
const systemPrompt = agentRef.getSystemPrompt();
|
|
178
|
+
for await (const chunk of provider.chatStream(history, systemPrompt)) {
|
|
179
|
+
yield chunk;
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
this.agent.bindChannel(tgChannel);
|
|
172
183
|
this.logger.info('Bound telegram channel');
|
|
173
184
|
}
|
|
174
185
|
else if (ch.type === 'websocket') {
|
package/package.json
CHANGED
package/src/channels/telegram.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Message } from '../core/types';
|
|
2
|
+
import type { LLMProvider } from '../providers/index';
|
|
2
3
|
import { BaseChannel } from './index';
|
|
3
4
|
|
|
4
5
|
/**
|
|
@@ -152,12 +153,22 @@ export class TelegramChannel extends BaseChannel {
|
|
|
152
153
|
|
|
153
154
|
private async processUpdate(update: any): Promise<void> {
|
|
154
155
|
const message = update.message || update.edited_message;
|
|
155
|
-
if (!message
|
|
156
|
+
if (!message || !this.handler) return;
|
|
157
|
+
|
|
158
|
+
// Handle /start command
|
|
159
|
+
if (message.text === '/start') {
|
|
160
|
+
await this.sendMarkdown(message.chat.id, '👋 Hello! I\'m ready to help. Send me a message to get started.');
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Handle text, photo captions, document captions
|
|
165
|
+
const text = message.text || message.caption;
|
|
166
|
+
if (!text) return;
|
|
156
167
|
|
|
157
168
|
const msg: Message = {
|
|
158
169
|
id: `tg_${message.message_id}`,
|
|
159
170
|
role: 'user',
|
|
160
|
-
content:
|
|
171
|
+
content: text,
|
|
161
172
|
timestamp: message.date * 1000,
|
|
162
173
|
metadata: {
|
|
163
174
|
sessionId: `tg_${message.chat.id}`,
|
|
@@ -167,33 +178,133 @@ export class TelegramChannel extends BaseChannel {
|
|
|
167
178
|
firstName: message.from?.first_name,
|
|
168
179
|
platform: 'telegram',
|
|
169
180
|
chatType: message.chat.type,
|
|
181
|
+
replyToMessageId: message.message_id,
|
|
170
182
|
},
|
|
171
183
|
};
|
|
172
184
|
|
|
173
|
-
// Show typing indicator
|
|
185
|
+
// Show typing indicator
|
|
174
186
|
await this.apiCall('sendChatAction', { chat_id: message.chat.id, action: 'typing' });
|
|
175
187
|
const typingInterval = setInterval(() => {
|
|
176
188
|
this.apiCall('sendChatAction', { chat_id: message.chat.id, action: 'typing' }).catch(() => {});
|
|
177
189
|
}, 4000);
|
|
178
190
|
|
|
179
191
|
try {
|
|
180
|
-
|
|
181
|
-
|
|
192
|
+
// Try streaming if provider supports it
|
|
193
|
+
if (this.streamHandler) {
|
|
194
|
+
await this.streamResponse(message.chat.id, msg, message.message_id);
|
|
195
|
+
} else {
|
|
196
|
+
const response = await this.handler(msg);
|
|
197
|
+
await this.sendMarkdown(message.chat.id, response.content, message.message_id);
|
|
198
|
+
}
|
|
199
|
+
} catch (err) {
|
|
200
|
+
console.error('[TelegramChannel] Error processing message:', err);
|
|
201
|
+
await this.sendMarkdown(message.chat.id, '⚠️ Sorry, something went wrong. Please try again.');
|
|
182
202
|
} finally {
|
|
183
203
|
clearInterval(typingInterval);
|
|
184
204
|
}
|
|
185
205
|
}
|
|
186
206
|
|
|
187
|
-
async
|
|
188
|
-
|
|
207
|
+
private async streamResponse(chatId: number | string, msg: Message, replyTo?: number): Promise<void> {
|
|
208
|
+
if (!this.streamHandler) return;
|
|
209
|
+
|
|
210
|
+
let sentMessageId: number | null = null;
|
|
211
|
+
let fullText = '';
|
|
212
|
+
let lastEditTime = 0;
|
|
213
|
+
const EDIT_INTERVAL = 1000; // Edit at most once per second
|
|
214
|
+
let pendingEdit = false;
|
|
215
|
+
|
|
216
|
+
const doEdit = async () => {
|
|
217
|
+
if (!sentMessageId || !fullText) return;
|
|
218
|
+
pendingEdit = false;
|
|
219
|
+
try {
|
|
220
|
+
await this.apiCall('editMessageText', {
|
|
221
|
+
chat_id: chatId,
|
|
222
|
+
message_id: sentMessageId,
|
|
223
|
+
text: fullText,
|
|
224
|
+
parse_mode: 'Markdown',
|
|
225
|
+
});
|
|
226
|
+
lastEditTime = Date.now();
|
|
227
|
+
} catch (err: any) {
|
|
228
|
+
// If Markdown fails, try plain text
|
|
229
|
+
if (err?.message?.includes('parse') || err?.message?.includes('entities')) {
|
|
230
|
+
try {
|
|
231
|
+
await this.apiCall('editMessageText', {
|
|
232
|
+
chat_id: chatId,
|
|
233
|
+
message_id: sentMessageId,
|
|
234
|
+
text: fullText,
|
|
235
|
+
});
|
|
236
|
+
} catch {}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
try {
|
|
242
|
+
for await (const chunk of this.streamHandler(msg)) {
|
|
243
|
+
fullText += chunk;
|
|
244
|
+
|
|
245
|
+
if (!sentMessageId) {
|
|
246
|
+
// Send first message
|
|
247
|
+
const result = await this.sendMarkdown(chatId, fullText, replyTo);
|
|
248
|
+
sentMessageId = result?.message_id;
|
|
249
|
+
lastEditTime = Date.now();
|
|
250
|
+
} else {
|
|
251
|
+
const now = Date.now();
|
|
252
|
+
if (now - lastEditTime >= EDIT_INTERVAL) {
|
|
253
|
+
await doEdit();
|
|
254
|
+
} else if (!pendingEdit) {
|
|
255
|
+
pendingEdit = true;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Final edit with complete text
|
|
261
|
+
if (sentMessageId && fullText) {
|
|
262
|
+
await doEdit();
|
|
263
|
+
}
|
|
264
|
+
} catch (err) {
|
|
265
|
+
// If streaming fails, send what we have
|
|
266
|
+
if (!sentMessageId && fullText) {
|
|
267
|
+
await this.sendMarkdown(chatId, fullText, replyTo);
|
|
268
|
+
}
|
|
269
|
+
throw err;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Stream handler — set by runtime when provider supports streaming
|
|
274
|
+
private streamHandler?: (msg: Message) => AsyncIterable<string>;
|
|
275
|
+
|
|
276
|
+
setStreamHandler(handler: (msg: Message) => AsyncIterable<string>): void {
|
|
277
|
+
this.streamHandler = handler;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
async sendMarkdown(chatId: number | string, text: string, replyTo?: number): Promise<any> {
|
|
189
281
|
const chunks = this.splitText(text, 4096);
|
|
282
|
+
let lastResult: any = null;
|
|
190
283
|
for (const chunk of chunks) {
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
284
|
+
try {
|
|
285
|
+
lastResult = await this.apiCall('sendMessage', {
|
|
286
|
+
chat_id: chatId,
|
|
287
|
+
text: chunk,
|
|
288
|
+
parse_mode: 'Markdown',
|
|
289
|
+
...(replyTo ? { reply_to_message_id: replyTo } : {}),
|
|
290
|
+
});
|
|
291
|
+
// Only reply to first chunk
|
|
292
|
+
replyTo = undefined;
|
|
293
|
+
} catch (err: any) {
|
|
294
|
+
// Markdown parse failed — send as plain text
|
|
295
|
+
lastResult = await this.apiCall('sendMessage', {
|
|
296
|
+
chat_id: chatId,
|
|
297
|
+
text: chunk,
|
|
298
|
+
...(replyTo ? { reply_to_message_id: replyTo } : {}),
|
|
299
|
+
});
|
|
300
|
+
replyTo = undefined;
|
|
301
|
+
}
|
|
196
302
|
}
|
|
303
|
+
return lastResult?.result;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
async sendMessage(chatId: number | string, text: string): Promise<void> {
|
|
307
|
+
await this.sendMarkdown(chatId, text);
|
|
197
308
|
}
|
|
198
309
|
|
|
199
310
|
private async apiCall(method: string, body?: Record<string, unknown>): Promise<any> {
|
package/src/core/runtime.ts
CHANGED
|
@@ -136,10 +136,21 @@ export class AgentRuntime {
|
|
|
136
136
|
this.agent.bindChannel(webChannel);
|
|
137
137
|
this.logger.info('Bound web channel', { port });
|
|
138
138
|
} else if (ch.type === 'telegram') {
|
|
139
|
-
|
|
139
|
+
const tgChannel = new TelegramChannel({
|
|
140
140
|
token: ch.config?.token as string,
|
|
141
141
|
port: ch.port,
|
|
142
|
-
})
|
|
142
|
+
});
|
|
143
|
+
// Wire up streaming for real-time message editing
|
|
144
|
+
const agentRef = this.agent;
|
|
145
|
+
tgChannel.setStreamHandler(async function* (msg) {
|
|
146
|
+
const history = [msg];
|
|
147
|
+
const provider = agentRef.provider;
|
|
148
|
+
const systemPrompt = agentRef.getSystemPrompt();
|
|
149
|
+
for await (const chunk of provider.chatStream(history, systemPrompt)) {
|
|
150
|
+
yield chunk;
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
this.agent.bindChannel(tgChannel);
|
|
143
154
|
this.logger.info('Bound telegram channel');
|
|
144
155
|
} else if (ch.type === 'websocket') {
|
|
145
156
|
this.agent.bindChannel(new WebSocketChannel(ch.port ?? 3002));
|