flare-agent Bài 10: Channels — Kết nối Agent với Telegram & Web Chat
Xây dựng @flare-agent/channels — kết nối agent với Telegram và Web Chat qua SSE. Một interface thống nhất, thêm channel mới chỉ cần implement vài method.
Nghe bài viết này dưới dạng podcastflare-agent Bài 10: Channels — Telegram & Web Chat
Series: Build Your Own AI Agent Framework trên Cloudflare Bài: 10 / 10 —
@flare-agent/channels
Channel là gì trong framework?
Agent xử lý logic — Channel xử lý I/O với thế giới bên ngoài.
Telegram message
↓
TelegramChannel ← normalize về IncomingMessage
↓
Agent.run() ← xử lý
↓
TelegramChannel ← gửi response về đúng nơi
↓
Telegram reply
Mỗi channel implement cùng 1 interface — agent không biết và không cần biết message đến từ Telegram hay Web Chat.
Cấu trúc package
packages/channels/
src/
types.ts # IncomingMessage, OutgoingMessage, Channel interface
telegram/
TelegramChannel.ts # Webhook handler + reply
types.ts # Telegram-specific types
webchat/
WebChatChannel.ts # SSE + WebSocket handler
types.ts
index.ts
Channel Interface
// src/types.ts
export interface IncomingMessage {
channelType: 'telegram' | 'webchat';
messageId: string;
sessionId: string; // dùng làm agent sessionId
userId: string; // unique per user per channel
text: string;
raw: unknown; // raw payload từ channel
metadata?: {
firstName?: string;
username?: string;
chatId?: string | number;
};
}
export interface OutgoingMessage {
text: string;
sessionId: string;
metadata?: Record<string, unknown>;
}
export interface Channel {
// Parse raw webhook payload thành IncomingMessage
parse(request: Request): Promise<IncomingMessage | null>;
// Gửi response về channel
send(message: OutgoingMessage, incoming: IncomingMessage): Promise<void>;
// Verify webhook signature (bảo mật)
verify(request: Request): Promise<boolean>;
}
Telegram Channel
Setup
Trước tiên cần tạo bot qua @BotFather trên Telegram, lấy BOT_TOKEN, rồi set webhook:
curl "https://api.telegram.org/bot<TOKEN>/setWebhook" \
-d "url=https://your-worker.workers.dev/channels/telegram"
Implementation
// src/telegram/TelegramChannel.ts
import type { Channel, IncomingMessage, OutgoingMessage } from '../types';
interface TelegramConfig {
botToken: string;
secretToken?: string; // webhook secret để verify
}
interface TelegramUpdate {
update_id: number;
message?: {
message_id: number;
from: { id: number; first_name: string; username?: string };
chat: { id: number };
text?: string;
};
}
export class TelegramChannel implements Channel {
private apiBase: string;
constructor(private config: TelegramConfig) {
this.apiBase = `https://api.telegram.org/bot${config.botToken}`;
}
async verify(request: Request): Promise<boolean> {
if (!this.config.secretToken) return true;
const token = request.headers.get('X-Telegram-Bot-Api-Secret-Token');
return token === this.config.secretToken;
}
async parse(request: Request): Promise<IncomingMessage | null> {
const update = await request.json<TelegramUpdate>();
const msg = update.message;
// Bỏ qua nếu không có text (sticker, photo, ...)
if (!msg?.text) return null;
return {
channelType: 'telegram',
messageId: `tg_${update.update_id}`,
// sessionId = chatId — mỗi conversation là 1 session
sessionId: `tg_${msg.chat.id}`,
userId: `tg_user_${msg.from.id}`,
text: msg.text,
raw: update,
metadata: {
firstName: msg.from.first_name,
username: msg.from.username,
chatId: msg.chat.id,
},
};
}
async send(message: OutgoingMessage, incoming: IncomingMessage): Promise<void> {
const chatId = incoming.metadata?.chatId;
if (!chatId) return;
// Telegram giới hạn 4096 chars/message — tự động split
const chunks = this.splitMessage(message.text);
for (const chunk of chunks) {
await fetch(`${this.apiBase}/sendMessage`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
chat_id: chatId,
text: chunk,
parse_mode: 'Markdown', // hỗ trợ **bold**, `code`
}),
});
}
}
// Typing indicator — hiện "đang nhập..."
async sendTyping(chatId: number): Promise<void> {
await fetch(`${this.apiBase}/sendChatAction`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
chat_id: chatId,
action: 'typing',
}),
});
}
private splitMessage(text: string, maxLen = 4096): string[] {
if (text.length <= maxLen) return [text];
const chunks: string[] = [];
for (let i = 0; i < text.length; i += maxLen) {
chunks.push(text.slice(i, i + maxLen));
}
return chunks;
}
}
Web Chat Channel
SSE cho streaming, fallback REST cho non-streaming:
// src/webchat/WebChatChannel.ts
import type { Channel, IncomingMessage, OutgoingMessage } from '../types';
interface WebChatConfig {
// Optional: verify requests bằng API key
apiKey?: string;
}
interface WebChatPayload {
sessionId: string;
userId?: string;
text: string;
stream?: boolean;
}
export class WebChatChannel implements Channel {
// Map sessionId → SSE controller (cho streaming)
private streams = new Map<string, ReadableStreamDefaultController<string>>();
constructor(private config: WebChatConfig = {}) {}
async verify(request: Request): Promise<boolean> {
if (!this.config.apiKey) return true;
const key = request.headers.get('X-Api-Key');
return key === this.config.apiKey;
}
async parse(request: Request): Promise<IncomingMessage | null> {
const body = await request.json<WebChatPayload>();
if (!body.text) return null;
return {
channelType: 'webchat',
messageId: `wc_${crypto.randomUUID()}`,
sessionId: body.sessionId,
userId: body.userId ?? `anon_${body.sessionId}`,
text: body.text,
raw: body,
metadata: { stream: body.stream },
};
}
async send(message: OutgoingMessage, incoming: IncomingMessage): Promise<void> {
// Web Chat dùng SSE — response được handle ở createSSEHandler
// Channel này chỉ cần implement interface
}
// Tạo SSE response cho 1 agent run
createSSEResponse(
agentStream: ReadableStream<string>
): Response {
const encoder = new TextEncoder();
const sseStream = agentStream.pipeThrough(
new TransformStream<string, Uint8Array>({
transform(chunk, controller) {
controller.enqueue(
encoder.encode(`data: ${JSON.stringify({ text: chunk })}\n\n`)
);
},
flush(controller) {
controller.enqueue(encoder.encode('data: [DONE]\n\n'));
},
})
);
return new Response(sseStream, {
headers: {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
'Access-Control-Allow-Origin': '*',
},
});
}
}
Channel Runner — wire channel + agent
// src/ChannelRunner.ts
import type { Channel, IncomingMessage } from './types';
import type { Agent } from '@flare-agent/core';
import type { AgentContext } from '@flare-agent/types';
export class ChannelRunner {
constructor(
private channel: Channel,
private agent: Agent,
private buildContext?: (msg: IncomingMessage) => Partial<AgentContext>
) {}
// Handle 1 incoming request — dùng trong Hono route
async handle(request: Request, env: Record<string, unknown>): Promise<Response> {
// 1. Verify
const valid = await this.channel.verify(request);
if (!valid) return new Response('Unauthorized', { status: 401 });
// 2. Parse
const incoming = await this.channel.parse(request.clone());
if (!incoming) return new Response('OK'); // ignored message type
// 3. Build context
const extraCtx = this.buildContext?.(incoming) ?? {};
const ctx: AgentContext = {
sessionId: incoming.sessionId,
userId: incoming.userId,
env,
...extraCtx,
};
// 4. Run agent
const result = await this.agent.run(incoming.text, ctx);
// 5. Send response
await this.channel.send(
{ text: result.output, sessionId: incoming.sessionId },
incoming
);
// Telegram cần trả về 200 OK ngay
return new Response('OK');
}
}
Tích hợp vào Hono Worker
// apps/worker/src/index.ts
import { Hono } from 'hono';
import { TelegramChannel } from '@flare-agent/channels';
import { WebChatChannel } from '@flare-agent/channels';
import { ChannelRunner } from '@flare-agent/channels';
import { vocabularyAgent } from './agents/vocabulary';
const app = new Hono<{ Bindings: Env }>();
// --- Telegram ---
app.post('/channels/telegram', async (c) => {
const telegram = new TelegramChannel({
botToken: c.env.TELEGRAM_BOT_TOKEN,
secretToken: c.env.TELEGRAM_SECRET,
});
// Hiện typing indicator trước khi run agent
const update = await c.req.json<any>();
if (update.message?.chat?.id) {
await telegram.sendTyping(update.message.chat.id);
}
const runner = new ChannelRunner(
telegram,
vocabularyAgent,
(msg) => ({
metadata: {
platform: 'telegram',
username: msg.metadata?.username,
},
})
);
return runner.handle(
new Request(c.req.url, {
method: 'POST',
body: JSON.stringify(update),
headers: c.req.raw.headers,
}),
c.env as any
);
});
// --- Web Chat: REST ---
app.post('/channels/webchat', async (c) => {
const webchat = new WebChatChannel({
apiKey: c.env.WEBCHAT_API_KEY,
});
const runner = new ChannelRunner(webchat, vocabularyAgent);
return runner.handle(c.req.raw, c.env as any);
});
// --- Web Chat: Streaming ---
app.post('/channels/webchat/stream', async (c) => {
const webchat = new WebChatChannel();
const body = await c.req.json<{ text: string; sessionId: string; userId?: string }>();
const ctx: AgentContext = {
sessionId: body.sessionId,
userId: body.userId,
env: c.env as any,
};
// Lấy provider stream trực tiếp
const messages = [{ role: 'user' as const, content: body.text }];
const stream = vocabularyAgent.stream(messages, ctx);
return webchat.createSSEResponse(stream);
});
export default app;
wrangler.toml — thêm secrets
[secrets]
TELEGRAM_BOT_TOKEN = ""
TELEGRAM_SECRET = ""
WEBCHAT_API_KEY = ""
React Hook — Web Chat client
// apps/web/src/hooks/useTelegramStyle.ts
// Dùng cho web chat có giao diện giống Telegram
export function useWebChat(sessionId: string) {
const [messages, setMessages] = useState<Message[]>([]);
const [streaming, setStreaming] = useState('');
const send = async (text: string) => {
// Thêm user message ngay lập tức
setMessages((prev) => [...prev, { role: 'user', text }]);
const res = await fetch('/channels/webchat/stream', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ text, sessionId }),
});
// Đọc SSE stream
const reader = res.body!.getReader();
const decoder = new TextDecoder();
let full = '';
while (true) {
const { done, value } = await reader.read();
if (done) break;
for (const line of decoder.decode(value).split('\n')) {
if (!line.startsWith('data: ')) continue;
const data = line.slice(6);
if (data === '[DONE]') break;
try {
full += JSON.parse(data).text;
setStreaming(full);
} catch { /* skip */ }
}
}
setMessages((prev) => [...prev, { role: 'assistant', text: full }]);
setStreaming('');
};
return { messages, streaming, send };
}
Thêm channel mới sau này
Chỉ cần implement Channel interface:
// Ví dụ: Slack channel
export class SlackChannel implements Channel {
async verify(request) { /* verify Slack signature */ }
async parse(request) { /* parse Slack event */ }
async send(message, incoming) { /* gửi về Slack */ }
}
// Wire vào ngay
app.post('/channels/slack', async (c) => {
const runner = new ChannelRunner(
new SlackChannel({ signingSecret: c.env.SLACK_SIGNING_SECRET }),
vocabularyAgent
);
return runner.handle(c.req.raw, c.env as any);
});
Checklist
- Tạo
packages/channels/ - Set Telegram webhook:
https://your-worker.workers.dev/channels/telegram - Add secrets vào wrangler:
TELEGRAM_BOT_TOKEN,TELEGRAM_SECRET - Test gửi
/startcho bot - Test web chat stream từ React
Series hoàn tất — 10 bài, 10 packages
@flare-agent/types ← interfaces
@flare-agent/providers ← Groq, WorkersAI, Ollama
@flare-agent/memory ← KV, D1, Vectorize
@flare-agent/core ← Agent Loop, Tool Registry
@flare-agent/workflow ← Graph-based Workflow
@flare-agent/multi-agent ← Agent Network, Handoff
@flare-agent/observability ← Tracing, Debugging
@flare-agent/channels ← Telegram, Web Chat ← mới
Agent của bạn giờ có thể nhận message từ Telegram và trả lời real-time qua SSE — tất cả chạy trên Cloudflare Workers, không cần server.
Chưa có bình luận
Để lại bình luận
Bình luận sẽ được phê duyệt trước khi hiển thị.