第12课:进阶扩展 —— 自己写 Action / Listener / 组件
本课目标
学会自己给系统添加新功能——新的自动化规则、新的操作、新的看板。
一、写一个新 Listener
需求:紧急邮件自动置顶 + 通知
当收到来自特定发件人的邮件、或主题包含紧急关键词时,自动标星、贴标签、发通知。
// 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
需求:一键生成邮件摘要并回复
// 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 + 组件
需求:邮件统计看板
第一步:定义数据结构
// 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 来更新数据
// 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 组件
// 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 中添加:
import EmailAnalytics from './EmailAnalytics';
// 在 registry 对象中添加:
'email-analytics': {
component: EmailAnalytics,
name: '邮件统计',
icon: '📈',
},
现在标签栏会自动出现"📈 邮件统计"标签。
四、进阶技巧
1. Listener 的性能优化
// 坏例子:每封邮件都调 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 输出的最佳实践
// 好的 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. 错误处理
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
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 课!
基础设施层:
第3课 环境搭建 → 跑起来
第4课 IMAP → 连上邮箱
第5课 SQLite → 存好数据
第6课 WebSocket → 实时通信
智能层:
第7课 Agent SDK → AI 大脑
第8课 Actions → 一键操作
第9课 Listeners → 自动化
第10课 UI State → 数据看板
应用层:
第11课 串讲 → 完整流程
第12课 扩展 → 自己动手
去做点有意思的东西吧!