第 10 章 · Channels 事件通道
本章目标:理解 Flue 的 Channels 入站事件模型,学会接收 Slack/GitHub 等外部平台的验证事件,并用
dispatch(...)把事件路由进 agent 会话。
10.1 Channel 是什么
一个 channel 把外部服务(Slack、GitHub、Stripe……)连接到你的 agents。它本质上是一段经过验证的 HTTP 入口:每个到达的投递(delivery)先被验证签名与身份,然后把平台原生 payload 交给你的代码,由你决定路由给哪个 agent 会话。
关键设计:入站专用
Channel 只负责"进来"。对外调用(发 Slack 消息、建 GitHub issue)不属于 channel——它们留在你的应用里,直接使用对应平台自己的 SDK 编写。这个边界让 Flue 不必封装所有第三方 API。
// src/channels/github.ts
// GitHub channel:接收 webhook 投递并验证来源
import { defineChannel } from '@flue/runtime';
export const githubChannel = defineChannel({
name: 'github',
// 平台标识:Flue 据此选择对应的签名验证逻辑
provider: 'github',
});10.2 验证与信任
"Verified events" 是 channels 的核心承诺:不是任何人 POST 一个 JSON 就能触发你的 agent。以 GitHub 为例,标准做法是校验 X-Hub-Signature-256:
// 手动验证 webhook 签名(理解 channel 内部做了什么)
import { createHmac } from 'node:crypto';
export function verifyGithubSignature(
rawBody: string,
signature: string | undefined,
secret: string,
): boolean {
if (!signature) return false;
// HMAC-SHA256 计算摘要
const expected =
'sha256=' +
createHmac('sha256', secret).update(rawBody).digest('hex');
// 常量时间比较,避免时序攻击
return signature === expected;
}Flue 的 channel 层把这类验证标准化了:你配置好密钥,投递未通过验证时根本不会进入你的处理函数。
10.3 dispatch(...):把事件路由给 agent
拿到已验证的原生 payload 后,用 dispatch(...) 把它送进某个 agent 的会话:
// src/agents/triage.ts(节选)
'use agent';
import { useModel, useTool } from '@flue/runtime';
import { searchCode } from '../tools/github.ts';
export function Triage() {
useModel('anthropic/claude-sonnet-4-6');
useTool(searchCode);
return '你是 issue 分诊助手,收到新 issue 后判断优先级并给出结论。';
}// src/channels/route-github.ts
// 把 GitHub webhook payload 路由给 Triage agent
import { dispatch } from '@flue/runtime';
import type { AgentHandle } from '@flue/runtime';
export async function onIssueOpened(
triage: AgentHandle,
event: { action: string; issue: { number: number; title: string; body?: string } },
) {
// 只关心"打开 issue"这一种动作
if (event.action !== 'opened') return;
await dispatch(triage, [
{
role: 'user',
content: `新 GitHub issue #${event.issue.number}:
标题:${event.issue.title}
内容:${event.issue.body ?? '(无)'}
请分诊:判断类型、严重程度、是否需要立即修复。`,
},
]);
}dispatch 的返回是"受理"而非"完成结果"——真正的执行由 runtime 接管,这也是第 12 章 Durability 合约的基础。
10.4 事件驱动的完整链路
一条 Slack 消息从用户发出到 agent 回应,经过四个阶段:
① Slack 发送事件 → ② Flue HTTP 入口验签 → ③ 你的路由代码 dispatch() → ④ agent 会话执行// src/server.ts
// 把多个 channel 注册到同一个 HTTP 服务上
import { createApp } from '@flue/runtime/node';
import { githubChannel } from './channels/github.ts';
const app = createApp();
// 注册 channel:Flue 自动挂载验证过的入口路由
app.channel(githubChannel, {
secret: process.env.GITHUB_WEBHOOK_SECRET!, // 从环境变量读密钥
});
app.listen(3000, () => console.log('channels ready on :3000'));密钥管理
Webhook 密钥永远不要硬编码。本地开发放 .env,Cloudflare Workers 用 Secrets,CI 用仓库 Secrets——三种环境统一通过 process.env / env 读取。
10.5 常见模式:去重与过滤
外部平台会重试投递(超时即重发),你的路由层应当幂等。两个实用技巧:
// 幂等去重:用 delivery id 做短期缓存
const seen = new Set<string>();
export async function handleDelivery(id: string, fn: () => Promise<void>) {
if (seen.has(id)) return; // 重复投递直接忽略
seen.add(id);
try {
await fn();
} finally {
// 生产环境建议换成 Redis/数据库 + TTL,这里仅演示
setTimeout(() => seen.delete(id), 10 * 60 * 1000);
}
}// 事件过滤:只让值得处理的负载进入 agent(省钱省时间)
export function isWorthTriage(issue: { labels: string[]; author_association: string }) {
// 已有标签或机器人创建的 issue 不再分诊
if (issue.labels.length > 0) return false;
if (issue.author_association === 'BOT') return false;
return true;
}10.6 本章小结
- Channel = 经过验证的 HTTP 入口 + 平台原生 payload +
dispatch(...)路由; - Channel 只做入站;出站调用留在应用层,用各平台自己的 SDK;
- 每个 delivery 先验签后处理,未通过验证不会触发 agent;
- 生产必备两件事:幂等去重(应对重试)+ 密钥走环境变量。
🧪 随堂测验
点击你认为正确的选项。答错时会展示正确答案与原因解析。
1. Flue 中 Channel 的职责边界是什么?
2. 把一个已验证的外部事件送进 agent 会话,应该使用哪个 API?
3. GitHub 对同一次 webhook 因超时会重复投递,你的路由代码应该如何应对?
4. 关于 webhook 密钥的管理,下列做法正确的是?
🛠️ 动手实践
- 为 GitHub channel 写一个只响应
issues.opened与pull_request.synchronize两种事件的过滤器,其余事件打印日志后忽略。 - 给 10.5 的去重函数换上持久化实现(SQLite 或 Redis),并在 README 里说明 TTL 选择依据。
- 用
curl -X POST模拟一次不带签名的投递,确认 Flue 入口拒绝它;再带上正确签名重试。