搞懂 Listener 系统——怎么让 AI 在后台"盯着"你的邮箱,自动处理新邮件。这是整个 demo 最核心的自动化能力。

第9课:自动化 —— Listeners 监听器详解

本课目标

搞懂 Listener 系统——怎么让 AI 在后台"盯着"你的邮箱,自动处理新邮件。这是整个 demo 最核心的自动化能力。


Listener 是什么?

如果 Action 是"你按按钮,它干活",那 Listener 就是"你啥都不用做,它自己盯着、自己干"。

code
Action:    用户触发 → 执行
Listener:  事件触发 → 自动执行(你可能都不知道它干了)

支持哪些事件?

事件 什么时候触发 典型用途
email_received 收到新邮件 自动分类、提取待办、记账
email_sent 发送邮件后 记录发送历史
email_starred 邮件被加星标 触发特殊处理
email_archived 邮件被归档 记录日志
email_labeled 邮件被贴标签 根据标签做后续处理
scheduled_time 定时触发 每天早上生成邮件摘要

最常用的是 email_received——每封新邮件到达时,所有监听这个事件的 Listener 都会被触发。


Listener 模板长什么样?

位置:agent/custom_scripts/listeners/

示例1:财务邮件自动记账

code
// 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 的精妙之处:

  1. 先用关键词快速过滤——90%的邮件在第一步就被过滤掉,不会调 AI(省钱!)
  2. 只有疑似财务邮件才调 AI——用 callAgent() 做精确判断
  3. AI 返回结构化数据——通过 JSON Schema 确保格式正确
  4. 自动更新看板——财务数据实时出现在前端
  5. 贴标签+通知——让你知道发生了什么

示例2:邮件待办提取器

code
// 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

这个管理器负责:

code
1. 启动时扫描 custom_scripts/listeners/ 目录,加载所有 Listener
2. 监听文件变化 → 热重载(改了代码不用重启)
3. 当事件发生时,找到所有监听该事件的 Listener,逐一执行
4. 记录日志到 .logs/listeners/
5. 处理错误(某个 Listener 出错不影响其他的)

事件触发流程

code
// 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 出错不影响其他的
            }
        }
    }
}

热重载:改代码不用重启

code
// 监听文件变化
watch('agent/custom_scripts/listeners/', (event, filename) => {
    if (filename.endsWith('.ts')) {
        console.log(`🔄 Listener 文件变化: ${filename},重新加载...`);
        this.reloadListeners();
    }
});

你改了 Listener 的代码,保存后自动生效,不需要重启服务器。开发体验很好。


前端的监听器面板

在浏览器界面里,你可以看到哪些 Listener 正在运行:

code
┌─────────────────────────────────────┐
│  活跃的监听器                        │
│                                     │
│  ✅ 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 自动分类、记账、提取待办

本课小结

  1. Listener = 后台自动化规则,事件触发即执行
  2. config 定义监听什么事件,handler 定义做什么
  3. 先用关键词过滤,再调 AI 判断——省钱的关键技巧
  4. callAgent() + JSON Schema 实现结构化的 AI 判断
  5. 热重载让开发迭代更快
  6. JSONL 日志记录所有执行历史

沿着当前专题继续,或返回课程目录重新整理阅读顺序。

返回课程目录