sandbox connected and streaming
This commit is contained in:
@@ -1,15 +1,15 @@
|
||||
import type { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify';
|
||||
import type { Authenticator } from '../auth/authenticator.js';
|
||||
import { AgentHarness } from '../harness/agent-harness.js';
|
||||
import type { AgentHarness, HarnessFactory } from '../harness/agent-harness.js';
|
||||
import type { InboundMessage } from '../types/messages.js';
|
||||
import { randomUUID } from 'crypto';
|
||||
|
||||
import type { ProviderConfig } from '../llm/provider.js';
|
||||
import type { ChannelAdapter, ChannelCapabilities } from '../workspace/index.js';
|
||||
|
||||
export interface TelegramHandlerConfig {
|
||||
authenticator: Authenticator;
|
||||
providerConfig: ProviderConfig;
|
||||
telegramBotToken: string;
|
||||
createHarness: HarnessFactory;
|
||||
}
|
||||
|
||||
interface TelegramUpdate {
|
||||
@@ -33,12 +33,18 @@ interface TelegramUpdate {
|
||||
};
|
||||
}
|
||||
|
||||
interface TelegramSession {
|
||||
harness: AgentHarness;
|
||||
lastActivity: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Telegram webhook handler
|
||||
*/
|
||||
export class TelegramHandler {
|
||||
private config: TelegramHandlerConfig;
|
||||
private sessions = new Map<string, AgentHarness>();
|
||||
private sessions = new Map<string, TelegramSession>();
|
||||
private chatIds = new Map<string, number>(); // sessionId -> chatId
|
||||
|
||||
constructor(config: TelegramHandlerConfig) {
|
||||
this.config = config;
|
||||
@@ -90,18 +96,59 @@ export class TelegramHandler {
|
||||
return;
|
||||
}
|
||||
|
||||
// Store chatId for this session
|
||||
this.chatIds.set(authContext.sessionId, chatId);
|
||||
|
||||
// Create Telegram channel adapter
|
||||
const telegramAdapter: ChannelAdapter = {
|
||||
sendSnapshot: () => {
|
||||
// Telegram doesn't support sync protocol
|
||||
},
|
||||
sendPatch: () => {
|
||||
// Telegram doesn't support sync protocol
|
||||
},
|
||||
sendText: (msg) => {
|
||||
this.sendTelegramMessage(chatId, msg.text).catch((err) => {
|
||||
logger.error({ error: err }, 'Failed to send Telegram text');
|
||||
});
|
||||
},
|
||||
sendChunk: () => {
|
||||
// Telegram doesn't support streaming; full response sent after handleMessage resolves
|
||||
},
|
||||
sendImage: (msg) => {
|
||||
this.sendTelegramPhoto(chatId, msg.data, msg.mimeType, msg.caption).catch((err) => {
|
||||
logger.error({ error: err }, 'Failed to send Telegram image');
|
||||
});
|
||||
},
|
||||
getCapabilities: (): ChannelCapabilities => ({
|
||||
supportsSync: false,
|
||||
supportsImages: true,
|
||||
supportsMarkdown: true,
|
||||
supportsStreaming: false,
|
||||
supportsTradingViewEmbed: false,
|
||||
}),
|
||||
};
|
||||
|
||||
// Get or create harness
|
||||
let harness = this.sessions.get(authContext.sessionId);
|
||||
if (!harness) {
|
||||
harness = new AgentHarness({
|
||||
let session = this.sessions.get(authContext.sessionId);
|
||||
if (!session) {
|
||||
const harness = this.config.createHarness({
|
||||
userId: authContext.userId,
|
||||
sessionId: authContext.sessionId,
|
||||
license: authContext.license,
|
||||
providerConfig: this.config.providerConfig,
|
||||
mcpServerUrl: authContext.mcpServerUrl,
|
||||
logger,
|
||||
channelAdapter: telegramAdapter,
|
||||
channelType: authContext.channelType,
|
||||
channelUserId: authContext.channelUserId,
|
||||
});
|
||||
await harness.initialize();
|
||||
this.sessions.set(authContext.sessionId, harness);
|
||||
session = { harness, lastActivity: Date.now() };
|
||||
this.sessions.set(authContext.sessionId, session);
|
||||
} else {
|
||||
// Update channel adapter and activity timestamp for existing session
|
||||
session.harness.setChannelAdapter(telegramAdapter);
|
||||
session.lastActivity = Date.now();
|
||||
}
|
||||
|
||||
// Process message
|
||||
@@ -114,7 +161,7 @@ export class TelegramHandler {
|
||||
timestamp: new Date(),
|
||||
};
|
||||
|
||||
const response = await harness.handleMessage(inboundMessage);
|
||||
const response = await session.harness.handleMessage(inboundMessage);
|
||||
|
||||
// Send response back to Telegram
|
||||
await this.sendTelegramMessage(chatId, response.content);
|
||||
@@ -127,7 +174,7 @@ export class TelegramHandler {
|
||||
}
|
||||
|
||||
/**
|
||||
* Send message to Telegram chat
|
||||
* Send text message to Telegram chat
|
||||
*/
|
||||
private async sendTelegramMessage(chatId: number, text: string): Promise<void> {
|
||||
const url = `https://api.telegram.org/bot${this.config.telegramBotToken}/sendMessage`;
|
||||
@@ -155,10 +202,80 @@ export class TelegramHandler {
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleanup old sessions (call periodically)
|
||||
* Send photo to Telegram chat
|
||||
* Converts base64 image data to a buffer and sends via sendPhoto API
|
||||
*/
|
||||
async cleanupSessions(_maxAgeMs = 30 * 60 * 1000): Promise<void> {
|
||||
// TODO: Track session last activity and cleanup
|
||||
// For now, sessions persist until server restart
|
||||
private async sendTelegramPhoto(
|
||||
chatId: number,
|
||||
base64Data: string,
|
||||
mimeType: string,
|
||||
caption?: string
|
||||
): Promise<void> {
|
||||
const url = `https://api.telegram.org/bot${this.config.telegramBotToken}/sendPhoto`;
|
||||
|
||||
try {
|
||||
// Convert base64 to buffer
|
||||
const imageBuffer = Buffer.from(base64Data, 'base64');
|
||||
|
||||
// Determine filename from mimeType
|
||||
const extension = mimeType.split('/')[1] || 'png';
|
||||
const filename = `image.${extension}`;
|
||||
|
||||
// Create FormData for multipart upload
|
||||
const formData = new FormData();
|
||||
formData.append('chat_id', chatId.toString());
|
||||
formData.append('photo', new Blob([imageBuffer], { type: mimeType }), filename);
|
||||
if (caption) {
|
||||
formData.append('caption', caption);
|
||||
}
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
throw new Error(`Telegram API error: ${response.statusText} - ${errorText}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to send Telegram photo:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up sessions that have been idle longer than maxAgeMs.
|
||||
* Triggers Iceberg flush for each expired session via harness.cleanup().
|
||||
*/
|
||||
async cleanupSessions(maxAgeMs = 30 * 60 * 1000): Promise<void> {
|
||||
const now = Date.now();
|
||||
const expired: string[] = [];
|
||||
|
||||
for (const [sessionId, session] of this.sessions) {
|
||||
if (now - session.lastActivity > maxAgeMs) {
|
||||
expired.push(sessionId);
|
||||
}
|
||||
}
|
||||
|
||||
for (const sessionId of expired) {
|
||||
const session = this.sessions.get(sessionId);
|
||||
if (session) {
|
||||
await session.harness.cleanup().catch(() => {});
|
||||
this.sessions.delete(sessionId);
|
||||
this.chatIds.delete(sessionId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Flush and clean up all active sessions.
|
||||
* Called during graceful shutdown.
|
||||
*/
|
||||
async endAllSessions(): Promise<void> {
|
||||
const cleanups = Array.from(this.sessions.values()).map(s => s.harness.cleanup());
|
||||
await Promise.allSettled(cleanups);
|
||||
this.sessions.clear();
|
||||
this.chatIds.clear();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user