学会自己给系统添加新功能——新的自动化规则、新的操作、新的看板。

第12课:进阶扩展 —— 自己写 Action / Listener / 组件

本课目标

学会自己给系统添加新功能——新的自动化规则、新的操作、新的看板。


一、写一个新 Listener

需求:紧急邮件自动置顶 + 通知

当收到来自特定发件人的邮件、或主题包含紧急关键词时,自动标星、贴标签、发通知。

code
// agent/custom_scripts/listeners/urgent-watcher.ts

export const config = {
    listenerId: "urgent-watcher",
    name: "紧急邮件监控",
    description: "自动识别紧急邮件并置顶提醒",
    event: "email_received",
    enabled: true,
};

export async function handler(email, context) {
    // 规则1:关键词检测(不花钱)
    const urgentKeywords = ['urgent', '紧急', 'ASAP', 'critical', 'emergency', 'production down'];
    const text = `${email.subject} ${email.body_text}`.toLowerCase();
    const hasUrgentKeyword = urgentKeywords.some(kw => text.includes(kw.toLowerCase()));

    // 规则2:VIP 发件人
    const vipSenders = ['boss@company.com', 'cto@company.com', 'client@bigcorp.com'];
    const isVIP = vipSenders.some(vip => email.from.includes(vip));

    if (!hasUrgentKeyword && !isVIP) {
        return; // 不紧急,放行
    }

    // 只有疑似紧急的才调 AI 确认(省钱)
    const analysis = await context.callAgent({
        prompt: `判断这封邮件是否真的紧急,需要立即处理:
            发件人: ${email.from}
            主题: ${email.subject}
            内容: ${email.body_text.slice(0, 500)}`,
        schema: {
            type: "object",
            properties: {
                is_truly_urgent: { type: "boolean" },
                reason: { type: "string" },
                suggested_action: { type: "string" }
            }
        }
    });

    if (!analysis.is_truly_urgent) return;

    // 标星 + 贴标签
    await context.star(email.messageId);
    await context.label(email.messageId, 'URGENT');

    // 发通知
    context.notify(
        `🚨 紧急邮件!\n` +
        `来自: ${email.from}\n` +
        `主题: ${email.subject}\n` +
        `原因: ${analysis.reason}\n` +
        `建议: ${analysis.suggested_action}`
    );
}

保存文件后,热重载自动生效——不需要重启服务器。


二、写一个新 Action

需求:一键生成邮件摘要并回复

code
// agent/custom_scripts/actions/summarize-and-reply.ts

export const config = {
    templateId: "summarize-and-reply",
    name: "摘要回复",
    description: "AI 生成邮件摘要,作为回复发送",
    parameters: {
        type: "object",
        properties: {
            emailId: { type: "string", description: "原始邮件ID" },
            tone: {
                type: "string",
                enum: ["formal", "casual", "brief"],
                description: "回复语气",
                default: "formal"
            }
        },
        required: ["emailId"]
    }
};

export async function handler(params, context) {
    const { emailId, tone } = params;

    // 读取原始邮件
    const email = await context.emailApi.read(emailId);

    // 让 AI 生成回复
    const reply = await context.callAgent({
        prompt: `为这封邮件写一个${tone === 'formal' ? '正式' : tone === 'casual' ? '随意' : '简短'}的回复:
            发件人: ${email.from}
            主题: ${email.subject}
            内容: ${email.body_text}

            回复要求:
            - 确认收到
            - 总结邮件关键点
            - 给出下一步建议`,
        schema: {
            type: "object",
            properties: {
                subject: { type: "string" },
                body: { type: "string" }
            }
        }
    });

    // 发送回复
    await context.emailApi.send({
        to: email.from,
        subject: `Re: ${email.subject}`,
        body: reply.body,
        inReplyTo: email.messageId
    });

    context.notify(`已回复 ${email.from}`);
    return { success: true };
}

三、写一个新 UI State + 组件

需求:邮件统计看板

第一步:定义数据结构

code
// agent/custom_scripts/ui-states/email-analytics.ts

export const config = {
    stateId: "email-analytics",
    name: "邮件统计",
    description: "统计邮件发送和接收情况",
    initialData: {
        daily_counts: [],         // 每天收发数量
        top_senders: [],          // 最频繁的发件人
        top_subjects: [],         // 最常见的主题关键词
        response_times: [],       // 平均回复时间
        last_updated: null
    }
};

第二步:写一个 Listener 来更新数据

code
// agent/custom_scripts/listeners/email-analytics-updater.ts

export const config = {
    listenerId: "email-analytics-updater",
    name: "邮件统计更新器",
    event: "email_received",
    enabled: true,
};

export async function handler(email, context) {
    const analytics = await context.uiState.get('email-analytics');

    // 更新每日计数
    const today = new Date().toISOString().split('T')[0];
    let todayEntry = analytics.daily_counts.find(d => d.date === today);
    if (!todayEntry) {
        todayEntry = { date: today, received: 0, sent: 0 };
        analytics.daily_counts.push(todayEntry);
    }
    todayEntry.received++;

    // 更新 Top 发件人
    const senderEntry = analytics.top_senders.find(s => s.email === email.from);
    if (senderEntry) {
        senderEntry.count++;
    } else {
        analytics.top_senders.push({ email: email.from, count: 1 });
    }
    // 排序取前10
    analytics.top_senders.sort((a, b) => b.count - a.count);
    analytics.top_senders = analytics.top_senders.slice(0, 10);

    analytics.last_updated = new Date().toISOString();
    await context.uiState.set('email-analytics', analytics);
}

第三步:写 React 组件

code
// client/components/custom/EmailAnalytics.tsx

import React from 'react';

function EmailAnalytics({ state }) {
    if (!state) return <div>加载中...</div>;

    const { daily_counts, top_senders } = state;

    return (
        <div className="p-4">
            <h2 className="text-xl font-bold mb-4">邮件统计</h2>

            {/* 每日收件趋势 */}
            <div className="mb-6">
                <h3 className="font-semibold">最近7天收件量</h3>
                <div className="flex items-end gap-1 h-32">
                    {daily_counts.slice(-7).map(day => (
                        <div key={day.date} className="flex flex-col items-center flex-1">
                            <div
                                className="bg-blue-500 w-full rounded-t"
                                style={{ height: `${day.received * 8}px` }}
                            />
                            <span className="text-xs mt-1">{day.date.slice(5)}</span>
                            <span className="text-xs">{day.received}</span>
                        </div>
                    ))}
                </div>
            </div>

            {/* Top 发件人 */}
            <div>
                <h3 className="font-semibold mb-2">最活跃的发件人</h3>
                {top_senders.slice(0, 5).map((sender, i) => (
                    <div key={sender.email} className="flex justify-between py-1">
                        <span>{i + 1}. {sender.email}</span>
                        <span className="font-mono">{sender.count} </span>
                    </div>
                ))}
            </div>
        </div>
    );
}

export default EmailAnalytics;

第四步:注册组件

ComponentRegistry.ts 中添加:

code
import EmailAnalytics from './EmailAnalytics';

// 在 registry 对象中添加:
'email-analytics': {
    component: EmailAnalytics,
    name: '邮件统计',
    icon: '📈',
},

现在标签栏会自动出现"📈 邮件统计"标签。


四、进阶技巧

1. Listener 的性能优化

code
// 坏例子:每封邮件都调 AI,费钱!
export async function handler(email, context) {
    const result = await context.callAgent({ ... }); // ❌ 每封都调
}

// 好例子:先过滤再调 AI
export async function handler(email, context) {
    // 第一关:关键词过滤(免费)
    if (!hasRelevantKeywords(email)) return;

    // 第二关:发件人过滤(免费)
    if (!isFromRelevantDomain(email)) return;

    // 第三关:AI 精确判断(花钱,但只对少量邮件调用)
    const result = await context.callAgent({ ... }); // ✅ 只对可能相关的调
}

2. 结构化 AI 输出的最佳实践

code
// 好的 schema:具体、有约束
const schema = {
    type: "object",
    properties: {
        category: {
            type: "string",
            enum: ["invoice", "receipt", "subscription", "refund", "other"]
            // ↑ 用 enum 限制选项,AI 不会乱写
        },
        amount: { type: "number" },  // 数字类型,不会返回字符串
        confidence: { type: "number", minimum: 0, maximum: 1 }
        // ↑ 加置信度,低于 0.7 可以不处理
    },
    required: ["category", "confidence"]  // 必填字段
};

3. 错误处理

code
export async function handler(email, context) {
    try {
        const result = await context.callAgent({ ... });
        // ...
    } catch (error) {
        // Listener 出错不能崩溃整个系统
        console.error(`Listener error for email ${email.messageId}:`, error);
        // 可以通知用户
        context.notify(`⚠️ 处理邮件 "${email.subject}" 时出错`);
    }
}

4. 定时任务 Listener

code
export const config = {
    listenerId: "daily-digest",
    name: "每日邮件摘要",
    event: "scheduled_time",          // 定时触发
    schedule: "0 9 * * 1-5",         // 工作日早上9点(Cron 语法)
    enabled: true,
};

export async function handler(triggerData, context) {
    // 查询昨天到现在的未读邮件
    const unread = await context.emailApi.search({
        query: 'is:unread after:yesterday'
    });

    if (unread.length === 0) {
        context.notify("☀️ 早安!没有未处理的邮件。");
        return;
    }

    // 让 AI 生成摘要
    const digest = await context.callAgent({
        prompt: `为以下 ${unread.length} 封未读邮件生成简短摘要:
            ${unread.map(e => `- ${e.from}: ${e.subject}`).join('\n')}`,
        schema: {
            type: "object",
            properties: {
                summary: { type: "string" },
                urgent_count: { type: "number" },
                needs_reply: { type: "array", items: { type: "string" } }
            }
        }
    });

    context.notify(
        `📬 早间邮件摘要\n` +
        `${unread.length} 封未读,${digest.urgent_count} 封紧急\n` +
        `${digest.summary}\n` +
        `需要回复: ${digest.needs_reply.join(', ')}`
    );
}

五、能扩展成什么产品?

掌握了这套架构,你可以把它改造成:

产品方向 修改什么
客服工单系统 Listener 自动分类工单,Action 分配给对应团队
销售 CRM Listener 追踪客户邮件,看板显示销售漏斗
项目管理助手 Listener 从邮件提取进度更新,看板显示甘特图
合规审查系统 Listener 检查邮件合规性,Action 标记违规
Slack/飞书集成 把 IMAP 换成 Slack API,其他架构不变

核心架构不变:事件监听 → AI 分析 → 数据更新 → 界面展示。只是数据源和业务逻辑不同。


教程总结

恭喜你完成了全部 12 课!

code
基础设施层:
  第3课  环境搭建 → 跑起来
  第4课  IMAP → 连上邮箱
  第5课  SQLite → 存好数据
  第6课  WebSocket → 实时通信

智能层:
  第7课  Agent SDK → AI 大脑
  第8课  Actions → 一键操作
  第9课  Listeners → 自动化
  第10课 UI State → 数据看板

应用层:
  第11课 串讲 → 完整流程
  第12课 扩展 → 自己动手

去做点有意思的东西吧!

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

返回课程目录