
最近折腾了一个 Gmail 收件箱分析工具,踩了几个坑,这篇把设计思路说清楚。开源地址:apify-gmail-inbox-intel。需要强调的是,这不是爬虫,也不是批量发送工具,只是一个基于 gmail.readonly 权限范围的收件箱数据分析工具。本文不是教程,更像是设计复盘。
遇到过"哪个客户的邮件我忘了回复"或者"平均回复周期是多久"这类问题吗?这个工具就是来解决这类需求的。
我同时需要三样东西:无服务器运行时、按结果计费、以及规范的输入校验。Apify 全部搞定,不用写后端。托管端点、数据集存储、键值存储(用于状态管理)、以及已经付费的开发者用户群体,这些都现成的。
这个 Actor 通过单一入口暴露四个功能:
thread_search — 按 q 查询 Gmail 邮件串,支持分页,返回元数据 + 消息计数reply_metrics — 针对每个邮件串,计算我回复了多少、对方回复了多少、最后回复距今多久、是否超时summarizer — 可选的 OpenAI LLM(人工智能)摘要功能,需要自备 API(应用程序接口)密钥unread_digest — 列出最近 N 小时内未读邮件串,按标签分组早期最难的决定是 OAuth,有两条路:
{refresh_token, client_id, client_secret} 作为 Actor 输入传给我。我选了方案 2,理由:
Actor 里的流程是这样的:
# src/gmail_client.py — 核心逻辑
async def get_access_token(oauth_token: dict) -> str: resp = await httpx_client.post( "https://oauth2.googleapis.com/token", data={ "grant_type": "refresh_token", "refresh_token": oauth_token["refresh_token"], "client_id": oauth_token["client_id"], "client_secret": oauth_token["client_secret"], }, ) return resp.json()["access_token"]
Access token 只存在内存里。Job 结束 → 进程销毁 → token 消失。尽力而为,但至少我的代码路径不会把 token 持久化到 Apify 存储里。
拆成四个 Actor 看起来很诱人。没拆,原因是:
feature 枚举值的 Actor 只有一个商店页、一个评分、一堆评论。拆成四个 Actor,流量全部分散。src/main.py 就是一个路由:
FEATURES = { "thread_search": thread_search.run, "reply_metrics": reply_metrics.run, "summarizer": summarizer.run, "unread_digest": digest.run,
} async def main(): actor_input = await Actor.get_input() or {} feature = actor_input.get("feature") if feature not in FEATURES: raise ValueError(f"Unknown feature: {feature}") await FEATURES[feature](actor_input)
每个 feature 模块通过同一个共享文件定义自己的 INPUT_SCHEMA.json 语义 — feature 枚举驱动下游各 handler 的校验逻辑。
免费额度每月 100 个邮件串。这个计数器需要跨多次运行持久化。Apify KeyValueStore 是天然的选择 — 不需要额外的数据库,持久化,作用域限定在 Actor 范围内。
# src/quota.py — 核心逻辑
async def check_and_increment(user_id: str, feature: str, n: int): kvs = await Actor.open_key_value_store() key = f"quota/{user_id}/{month_key()}/{feature}" used = (await kvs.get_value(key)) or 0 if used + n > FREE_LIMIT: raise QuotaExceeded(feature, used, FREE_LIMIT) await kvs.set_value(key, used + n)
月份切换靠 year-month 字符串 key 实现 — 不需要 cron,不需要迁移,不会有漂移。Pro 版加个 flag 直接跳过检查。
六个 pytest 测试,pytest.ini 里配了 asyncio_mode = auto。覆盖的场景:
dry_run=True 时干净利落地短路退出[pytest]
asyncio_mode = auto
就这一行配置,决定了"6 个测试通过"和"6 个测试全报错:缺少事件循环"的区别。踩过坑才记住的。
Apify 处理账单,我专注写代码。
unread_digest 是手动触发。定时触发 + 飞书/钉钉推送才是下一步该做的。reply_metrics 现在是全局统计。给销售团队看每个标签的 SLA 矩阵会更有价值。.actor/actor.json + INPUT_SCHEMA.json,想 fork 的话直接用如果你也在做收件箱相关的自动化工作,欢迎试玩。有没有我遗漏的理由必须用完整的 3-legged OAuth?评论区聊。