第9课:自动化 —— Listeners 监听器详解
本课目标
搞懂 Listener 系统——怎么让 AI 在后台"盯着"你的邮箱,自动处理新邮件。这是整个 demo 最核心的自动化能力。
Listener 是什么?
如果 Action 是"你按按钮,它干活",那 Listener 就是"你啥都不用做,它自己盯着、自己干"。
Action: 用户触发 → 执行
Listener: 事件触发 → 自动执行(你可能都不知道它干了)
支持哪些事件?
| 事件 | 什么时候触发 | 典型用途 |
|---|---|---|
email_received |
收到新邮件 | 自动分类、提取待办、记账 |
email_sent |
发送邮件后 | 记录发送历史 |
email_starred |
邮件被加星标 | 触发特殊处理 |
email_archived |
邮件被归档 | 记录日志 |
email_labeled |
邮件被贴标签 | 根据标签做后续处理 |
scheduled_time |
定时触发 | 每天早上生成邮件摘要 |
最常用的是 email_received——每封新邮件到达时,所有监听这个事件的 Listener 都会被触发。
Listener 模板长什么样?
位置:agent/custom_scripts/listeners/
示例1:财务邮件自动记账
// agent/custom_scripts/listeners/finance-email-tracker.ts
export const config = {
listenerId: "finance-email-tracker",
name: "财务邮件自动追踪",
description: "自动识别发票和收据邮件,提取金额记录到财务看板",
event: "email_received", // 监听 "收到新邮件" 事件
enabled: true, // 默认启用
};
export async function handler(email, context) {
// 第一步:用简单规则快速过滤(省钱!不是每封邮件都调AI)
const financeKeywords = ['invoice', 'receipt', 'payment', 'charge', 'billing'];
const text = `${email.subject} ${email.body_text}`.toLowerCase();
const mightBeFinance = financeKeywords.some(kw => text.includes(kw));
if (!mightBeFinance) {
return; // 不像财务邮件,跳过,不调 AI(省钱)
}
// 第二步:调 AI 做精确判断(递归 AI 核心!)
const analysis = await context.callAgent({
prompt: `分析这封邮件是否包含财务信息:
发件人: ${email.from}
主题: ${email.subject}
内容: ${email.body_text.slice(0, 1000)}
如果包含财务信息,提取金额、类型和供应商。`,
schema: {
type: "object",
properties: {
is_financial: { type: "boolean" },
transaction_type: {
type: "string",
enum: ["expense", "income", "refund", "subscription"]
},
amount: { type: "number" },
currency: { type: "string" },
vendor: { type: "string" },
category: { type: "string" }
}
}
});
if (!analysis.is_financial) {
return; // AI 说不是财务邮件,放行
}
// 第三步:记录到财务看板
const dashboard = await context.uiState.get('financial-dashboard');
dashboard.transactions.push({
date: email.date_sent,
type: analysis.transaction_type,
amount: analysis.amount,
vendor: analysis.vendor,
category: analysis.category,
emailId: email.messageId,
});
await context.uiState.set('financial-dashboard', dashboard);
// 第四步:给邮件贴标签
await context.label(email.messageId, 'Finance');
// 第五步:通知用户
context.notify(
`💰 检测到${analysis.transaction_type === 'expense' ? '支出' : '收入'}: ` +
`${analysis.currency}${analysis.amount} - ${analysis.vendor}`
);
}
这个 Listener 的精妙之处:
- 先用关键词快速过滤——90%的邮件在第一步就被过滤掉,不会调 AI(省钱!)
- 只有疑似财务邮件才调 AI——用
callAgent()做精确判断 - AI 返回结构化数据——通过 JSON Schema 确保格式正确
- 自动更新看板——财务数据实时出现在前端
- 贴标签+通知——让你知道发生了什么
示例2:邮件待办提取器
// agent/custom_scripts/listeners/todo-extractor.ts
export const config = {
listenerId: "todo-extractor",
name: "待办事项提取器",
description: "从邮件中自动提取行动项,添加到任务看板",
event: "email_received",
enabled: true,
};
export async function handler(email, context) {
// 只处理来自同事/上司的邮件(跳过营销邮件等)
const workDomains = ['company.com', 'partner.org'];
const isWorkEmail = workDomains.some(d => email.from.includes(d));
if (!isWorkEmail) return;
// 调 AI 提取待办事项
const result = await context.callAgent({
prompt: `从这封工作邮件中提取行动项(如果有的话):
发件人: ${email.from}
主题: ${email.subject}
内容: ${email.body_text.slice(0, 2000)}
只提取明确需要你做的事情,不要猜测。`,
schema: {
type: "object",
properties: {
has_todos: { type: "boolean" },
todos: {
type: "array",
items: {
type: "object",
properties: {
title: { type: "string" },
priority: { type: "string", enum: ["high", "medium", "low"] },
deadline: { type: "string" }
}
}
}
}
}
});
if (!result.has_todos || result.todos.length === 0) return;
// 添加到任务看板
const taskBoard = await context.uiState.get('task-board');
for (const todo of result.todos) {
taskBoard.tasks.push({
title: todo.title,
priority: todo.priority,
deadline: todo.deadline,
status: 'todo',
source: `邮件: ${email.subject}`,
emailId: email.messageId,
createdAt: new Date().toISOString(),
});
}
await context.uiState.set('task-board', taskBoard);
context.notify(`📋 从 "${email.subject}" 中提取了 ${result.todos.length} 个待办事项`);
}
ListenerContext 里有什么?
handler 的第二个参数 context:
| 方法 | 功能 |
|---|---|
context.callAgent() |
调用 AI 做判断(核心!) |
context.notify() |
给用户发通知 |
context.archive() |
归档邮件 |
context.star() |
标星邮件 |
context.label() |
贴标签 |
context.markRead() / markUnread() |
标记已读/未读 |
context.uiState.get() / set() |
读写看板数据 |
Listeners Manager 怎么管理?
位置:ccsdk/listeners-manager.ts
这个管理器负责:
1. 启动时扫描 custom_scripts/listeners/ 目录,加载所有 Listener
2. 监听文件变化 → 热重载(改了代码不用重启)
3. 当事件发生时,找到所有监听该事件的 Listener,逐一执行
4. 记录日志到 .logs/listeners/
5. 处理错误(某个 Listener 出错不影响其他的)
事件触发流程
// listeners-manager.ts(简化版)
class ListenersManager {
private listeners = []; // 所有已加载的 Listener
async checkEvent(eventType: string, data: any) {
// 找到所有监听这个事件的 Listener
const matching = this.listeners.filter(
l => l.config.event === eventType && l.config.enabled
);
// 逐一执行
for (const listener of matching) {
try {
await listener.handler(data, this.createContext());
this.log(listener.config.listenerId, 'success', data);
} catch (error) {
this.log(listener.config.listenerId, 'error', error);
// 不抛出异常 → 一个 Listener 出错不影响其他的
}
}
}
}
热重载:改代码不用重启
// 监听文件变化
watch('agent/custom_scripts/listeners/', (event, filename) => {
if (filename.endsWith('.ts')) {
console.log(`🔄 Listener 文件变化: ${filename},重新加载...`);
this.reloadListeners();
}
});
你改了 Listener 的代码,保存后自动生效,不需要重启服务器。开发体验很好。
前端的监听器面板
在浏览器界面里,你可以看到哪些 Listener 正在运行:
┌─────────────────────────────────────┐
│ 活跃的监听器 │
│ │
│ ✅ finance-email-tracker │
│ 财务邮件自动追踪 │
│ 事件: email_received │
│ │
│ ✅ todo-extractor │
│ 待办事项提取器 │
│ 事件: email_received │
│ │
│ ⏸️ urgent-watcher (已暂停) │
│ 紧急邮件监控 │
│ 事件: email_received │
└─────────────────────────────────────┘
可以通过 API 开关某个 Listener:POST /api/listeners/:id/toggle
Action vs Listener 对比
| Action | Listener | |
|---|---|---|
| 触发方式 | 用户点击按钮 | 事件自动触发 |
| 适用场景 | 需要用户确认的操作 | 全自动无需干预 |
| 执行频率 | 偶尔 | 每封新邮件都可能触发 |
| 成本考量 | 用户主动触发,可控 | 自动运行,注意控制 AI 调用 |
| 典型例子 | 归档邮件、转发Bug | 自动分类、记账、提取待办 |
本课小结
- Listener = 后台自动化规则,事件触发即执行
- config 定义监听什么事件,handler 定义做什么
- 先用关键词过滤,再调 AI 判断——省钱的关键技巧
- callAgent() + JSON Schema 实现结构化的 AI 判断
- 热重载让开发迭代更快
- JSONL 日志记录所有执行历史