日历迁移这件事,听起来简单,做起来却很容易翻车。你以为是“把一个日历导出,再导入到另一个日历”,实际上一旦涉及企业邮箱、跨平台工作流、重复日程和团队协作,问题就变成:参会人要不要重新通知?时区要不要换算?重复事件要不要展开?目标系统允不允许直接插入?这些琐碎的“最后 10%”才是迁移自动化的真正成本。
用 AI 编码代理(AI Coding Agent)配合 Zapier 这类自动化平台的 SDK 来写日历迁移脚本,最近变成了一个性价比很高的方案。我的判断是:AI 编码代理降低的不是“调用日历 API”的难度,而是“编写和调试胶水代码”的日常成本。但如果你不了解日历数据本身的陷阱,让 AI 帮你写 500 行代码,它也会很自信地帮你写出 500 行有边界问题的代码。所以这篇文章不只给代码,还会把架构、幂等、时区和排错方法讲透。
读完这篇文章,你将得到一套可以直接落地的日历事件迁移方案:如何读取源日历,如何通过 Zapier 的自动化入口写入目标日历,如何保证重复执行不产生重复事件,以及实际项目中常见的坑和规避方式。
1. 日历事件自动迁移的真正难点
很多人第一次做日历迁移,会直接从“导出 CSV”或者“抓 API”开始,结果越做越乱。原因是日历数据有三个隐蔽特征。
第一,日历事件不是单纯的“标题 + 时间”。一个真实事件可能包含邀请人、参会人、附件、地点、重复规则、忙闲状态、会议链接、提醒设置。不同平台之间字段天然不对齐,Google Calendar 的conferenceData和 Microsoft 365 的onlineMeeting都不是对方能直接消费的字段。
第二,重复事件的展开和压缩是难点。一个“每周一上午 10 点”的重复会议,在 API 里通常是一个RRULE规则,而不是 52 个独立事件。如果你只是简单查询items,得到的是规则本体;如果你用singleEvents=true去展开,又会得到大量单次实例。迁移时一旦处理错了,要么只迁移了一条规则,要么生成几百条孤立事件,后续根本没法管理。
第三,幂等性不是天然存在的。如果你写一个脚本每天跑一次,就必须有办法知道“这个事件我是否已经迁移过”。没有幂等控制,第二次运行就会把所有事件再复制一遍。日历数据因为涉及通知、日程占用,重复创建的后果比重复写一条数据库记录更严重——它会向所有参会人发送邀请。
从工程角度看,日历迁移的架构可以抽象成三个环节:
- 读:从源日历读取事件,做字段映射。
- 转:统一数据模型,处理时区、重复规则、参会人。
- 写:通过目标系统 API 或 Zapier 自动化写入,并保存迁移记录。
AI 编码代理在这三个环节都能帮上忙:它可以帮你快速生成 OAuth 认证代码、处理分页、构造请求体,还能解释某个陌生 API 字段的用途。但你仍然需要自己把关架构,尤其是“写”这一侧的幂等和失败重试。
2. 核心概念:AI 编码代理、Zapier SDK 与自动化编排
2.1 AI 编码代理是什么
AI 编码代理不是普通的代码补全工具,而是能在指定工作区里自主完成“理解需求 → 编写代码 → 执行命令 → 查看报错 → 修改代码”循环的智能体。常见的形态包括 Cursor、Claude Code、Codex CLI、Gemini CLI 等,它们在命令行或编辑器里运行,可以读写文件、执行终端命令。
它的实际价值可以分为三个层次:
- 生成层:根据自然语言描述生成接口调用代码。
- 解释层:面对不熟悉的 API 文档,让它帮你总结参数含义和调用顺序。
- 调试层:让它根据报错信息自动修改代码,省去反复搜索 Stack Overflow 的时间。
在日历迁移场景中,AI 编码代理最擅长的是把“官方文档里的示例”改造成“符合你业务需求的模块”。比如你让它“用 Google Calendar API v3 的时间最小值和分页参数读取指定时间段的非取消事件”,它通常能直接给出可运行的片段。
但要注意边界:AI 编码代理并不理解你的业务最终状态。它不知道“重复执行脚本不能重复创建事件”这个需求,除非你在提示词里明确要求,并在代码评审时检查幂等逻辑。
2.2 Zapier SDK 在自动化里扮演什么角色
Zapier 是一个自动化平台,能把不同 SaaS 应用连接起来。它的核心产品叫 Zap,由触发器(Trigger)和动作(Action)组成。比如“当 Google Calendar 新增事件时,在另一个系统创建日程”。
围绕 Zapier,官方提供了 CLI 与编程入口,开发者可以基于 Zapier 平台构建自定义集成。在具体工程里,有两种使用方式:
- 平台集成开发:使用 Zapier 官方 CLI/SDK 构建一个 app,定义触发器、动作、字段,发布后在 Zap 画布里使用。
- 轻量 webhook 触发:预先在 Zapier 里创建一个“Webhook by Zapier” 的 Zap,拿到一个 HTTPS 接口。代码侧只需要向这个接口发送 JSON,Zap 就会继续执行后续的日历创建动作。
从实际项目角度看,日历自动迁移大多数时候不需要把“日历应用”整个接入 Zapier,只需要打通“数据源 → Zapier 入口 → 目标日历”这一段通路。用 webhook 方式最轻量,代码侧依赖最少,后续如果要换成官方 SDK,只需要替换调用层,业务逻辑不受影响。这也是本文示例代码采用的方案。
2.3 各组件职责划分
| 组件 | 职责 | 不适合做的事 |
|---|---|---|
| 源日历 API | 提供事件数据与增量同步凭证 | 不负责目标系统的数据清洗 |
| AI 编码代理 | 生成代码、定位问题、解释 API | 不负责业务决策与生产环境鉴权 |
| Zapier | 编排跨应用写入动作 | 不适合做复杂的状态去重 |
| 迁移服务(你的脚本) | 读取、转换、幂等登记、重试 | 不应把 OAuth 密钥硬编码在脚本里 |
这个表格背后的核心思想是:Zapier 负责“把事件写到目标日历”,你的迁移服务负责“保证该写的事件才写”。前者是执行,后者是决策。
3. 环境准备与前置条件
开始写代码之前,需要先准备好账号、权限和本地开发环境。这一步经常被省略,但权限问题导致的报错往往要花很长时间排查。
3.1 账号与权限
你需要准备以下内容:
- 一个 Google Cloud 项目,用于开启 Google Calendar API。
- OAuth 2.0 客户端凭据(Client ID 和 Client Secret),允许你读取源日历。
- 至少一个测试日历,不要在生产日历上直接做完整个实验。
- Zapier 账号,并创建一个 Webhook 触发的 Zap,拿到 Webhook URL。
需要强调的是:Google API 的安全原则是“最小权限”。如果只需要读取日历事件,OAuth scope 就应该用https://www.googleapis.com/auth/calendar.readonly,不要申请读写权限。如果你还需要写入,再按需扩大。
3.2 开发环境
本文示例基于 Node.js,使用以下环境:
- Node.js 18 或更高版本(内置全局
fetch,不需要额外安装请求库)。 - npm 或 yarn 作为包管理器。
- TypeScript 运行时,推荐
tsx来直接运行 TypeScript 文件。 - SQLite 数据库文件,用于保存迁移记录。
安装依赖的命令:
mkdir calendar-migration cd calendar-migration npm init -y npm install googleapis better-sqlite3 dotenv npm install -D typescript tsx @types/node @types/better-sqlite3这里先解释一下为什么需要这些依赖:
googleapis:官方 Google API 客户端库,用来读取 Google Calendar。better-sqlite3:轻量本地数据库,记录每次迁移的事件 ID,实现幂等。dotenv:加载.env文件中的环境变量,避免把密钥写进代码。tsx:直接运行 TypeScript,减少编译配置。
如果你要对接的不是 Google Calendar,过程完全类似:换成对应平台的 SDK,核心架构不变。
4. 架构设计与整体流程
在动手写代码前,最好先明确整体设计。日历迁移服务可以拆成四个模块。
4.1 模块划分
- 认证模块:负责获取 Google Calendar 的访问令牌。
- 读取模块:从源日历拉取事件,处理分页和过滤。
- 转换模块:把源事件转换为目标系统需要的数据结构。
- 写入模块:把事件发给 Zapier Webhook,并登记迁移记录。
4.2 完整流程
一次迁移执行的过程如下:
- 读取环境变量,初始化认证与数据库。
- 从源日历按时间范围拉取事件列表。
- 对每个事件,检查
sync_records表里是否已存在对应记录。 - 如果是新事件,转换数据并发送给 Zapier Webhook。
- 写入成功后,把源事件 ID 与目标事件 ID 保存到数据库。
- 如果写入失败,记录错误状态,供重试使用。
这个流程在第一次运行时,会迁移全部符合条件的事件;后续再运行,只会处理新增或变化的事件。
4.3 数据表设计
为了让迁移脚本具备幂等能力,需要一张记录表。SQLite 的建表语句如下:
-- 文件路径:migrations/001_create_sync_records.sql CREATE TABLE IF NOT EXISTS sync_records ( id INTEGER PRIMARY KEY AUTOINCREMENT, source_calendar_id TEXT NOT NULL, source_event_id TEXT NOT NULL, target_event_id TEXT, status TEXT NOT NULL DEFAULT 'pending', payload_hash TEXT NOT NULL, created_at TEXT NOT NULL DEFAULT (datetime('now')), updated_at TEXT NOT NULL DEFAULT (datetime('now')), error_message TEXT, UNIQUE(source_calendar_id, source_event_id) );这张表的核心是source_event_id的唯一约束。同一源事件无论脚本跑多少次,都只会有一条迁移记录。
表中还加了payload_hash字段,用来判断事件内容是否发生变化。如果源事件被修改了,我们需要决定是更新目标事件,还是忽略。一般情况下,日历迁移建议采用“只新建,不更新已迁移数据”的保守策略,避免在目标日历里产生编辑通知风暴。
5. 完整代码实现
下面进入核心部分。为了方便你跑通,我按“配置 → 读取 → 写入 → 幂等 → 运行”的顺序拆开。
5.1 环境变量配置
首先创建.env.example文件,把密钥对应的键名列出来:
# 文件路径:.env.example GOOGLE_CLIENT_ID=your-client-id GOOGLE_CLIENT_SECRET=your-client-secret GOOGLE_REFRESH_TOKEN=your-refresh-token SOURCE_CALENDAR_ID=primary TARGET_CALENDAR_SUMMARY_PREFIX=AutoMated ZAPIER_WEBHOOK_URL=https://hooks.zapier.com/hooks/catch/your-code/ SCOPE=https://www.googleapis.com/auth/calendar.readonly TIME_ZONE=Asia/Shanghai实际使用中,把.env.example复制成.env,填入真实凭据。这篇文章不会把密钥提交到 Git,.env必须加入.gitignore:
echo '.env' >> .gitignore5.2 从 Google Calendar 读取事件
下面是读取模块的完整代码。它使用 OAuth 2.0 刷新令牌获取访问令牌,然后调用 Calendar API 读取从 startDate 到 endDate 之间的事件。
// 文件路径:src/googleCalendar.ts import { google } from 'googleapis'; import dotenv from 'dotenv'; dotenv.config(); interface CalendarEvent { id: string; summary: string; description?: string; location?: string; start?: { dateTime?: string; date?: string; timeZone?: string }; end?: { dateTime?: string; date?: string; timeZone?: string }; attendees?: { email: string }[]; recurrence?: string[]; status?: string; } function createCalendarClient() { const auth = new google.auth.OAuth2( process.env.GOOGLE_CLIENT_ID, process.env.GOOGLE_CLIENT_SECRET ); auth.setCredentials({ refresh_token: process.env.GOOGLE_REFRESH_TOKEN, }); return google.calendar({ version: 'v3', auth }); } export async function listEvents(startDate: string, endDate: string): Promise<CalendarEvent[]> { const calendar = createCalendarClient(); const events: CalendarEvent[] = []; let pageToken: string | undefined = undefined; do { const response = await calendar.events.list({ calendarId: process.env.SOURCE_CALENDAR_ID || 'primary', timeMin: new Date(startDate).toISOString(), timeMax: new Date(endDate).toISOString(), singleEvents: true, maxResults: 250, pageToken, orderBy: 'startTime', }); const items = response.data.items as unknown as CalendarEvent[] | undefined; if (items) { events.push(...items); } pageToken = response.data.nextPageToken || undefined; } while (pageToken); return events; }这段代码的关键点是:
singleEvents: true会把重复事件展开成单次实例。好处是目标日历可以直接创建独立事件,坏处是会产生大量记录。如果源日历有大量重复会议,这一步会让数据量暴增。orderBy: 'startTime'要求singleEvents为 true,它让返回结果按开始时间排序,便于日志查看。- 分页通过
nextPageToken处理,避免一次接口返回不全。
如果你希望保留重复规则,而不是展开,可以把singleEvents改成false,并额外读取recurrence字段。两种方案没有绝对对错,取决于目标日历是否支持RRULE。
5.3 调用 Zapier Webhook 创建目标日历事件
在 Zapier 侧,你需要创建一个 “Webhook by Zapier” 的 Zap,触发器选择 Catch Hook,动作选择你要写入的日历应用。创建后得到一个 HTTPS URL。
对应的写入模块如下。这里把调用逻辑封装在一个抽象层里,后续要替换成官方 SDK,不会影响读取和幂等逻辑。
// 文件路径:src/zapierClient.ts import dotenv from 'dotenv'; dotenv.config(); interface TargetEventPayload { summary: string; description: string; location: string; startDateTime: string; endDateTime: string; attendees: string[]; } export async function createCalendarEventViaZapier( event: TargetEventPayload ): Promise<{ success: boolean; targetEventId?: string }> { const webhookUrl = process.env.ZAPIER_WEBHOOK_URL; if (!webhookUrl) { throw new Error('ZAPIER_WEBHOOK_URL is not configured'); } const response = await fetch(webhookUrl, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(event), }); if (!response.ok) { const errorText = await response.text(); throw new Error(`Zapier webhook failed: ${response.status} ${errorText}`); } // Zapier webhook 一般会在响应体里返回一个 id 或结果对象 const result = await response.json().catch(() => null); return { success: true, targetEventId: result?.id || undefined, }; }这里一个真实存在的问题是:Zapier Webhook 的响应体结构取决于你在 Zap 动作里返回的字段映射。因此,在写这个模块之前,先手动向 Webhook 发一个测试请求,打开 Zapier 的日志看响应结构,再决定怎么解析目标事件 ID。
5.4 幂等记录与增量同步
真正让脚本可以反复执行的关键,在下面的迁移服务里。它读取事件后,先查数据库,再决定是否需要写入。
// 文件路径:src/syncCalendar.ts import Database from 'better-sqlite3'; import { createHash } from 'crypto'; import { listEvents } from './googleCalendar'; import { createCalendarEventViaZapier } from './zapierClient'; const db = new Database('migration.db'); function initDb() { db.exec(` CREATE TABLE IF NOT EXISTS sync_records ( id INTEGER PRIMARY KEY AUTOINCREMENT, source_calendar_id TEXT NOT NULL, source_event_id TEXT NOT NULL, target_event_id TEXT, status TEXT NOT NULL DEFAULT 'pending', payload_hash TEXT NOT NULL, created_at TEXT NOT NULL DEFAULT (datetime('now')), updated_at TEXT NOT NULL DEFAULT (datetime('now')), error_message TEXT, UNIQUE(source_calendar_id, source_event_id) ); `); } function hashPayload(payload: object): string { return createHash('sha256').update(JSON.stringify(payload)).digest('hex'); } function isAlreadyMigrated(sourceCalendarId: string, sourceEventId: string): boolean { const row = db .prepare('SELECT status FROM sync_records WHERE source_calendar_id = ? AND source_event_id = ?') .get(sourceCalendarId, sourceEventId); return row?.status === 'success'; } function markMigrated( sourceCalendarId: string, sourceEventId: string, payloadHash: string, targetEventId?: string ) { db.prepare(` INSERT INTO sync_records (source_calendar_id, source_event_id, target_event_id, status, payload_hash) VALUES (?, ?, ?, 'success', ?) ON CONFLICT(source_calendar_id, source_event_id) DO UPDATE SET target_event_id = excluded.target_event_id, status = 'success', payload_hash = excluded.payload_hash, updated_at = datetime('now') `).run(sourceCalendarId, sourceEventId, targetEventId || null, payloadHash); } export async function runMigration(startDate: string, endDate: string) { initDb(); const sourceCalendarId = process.env.SOURCE_CALENDAR_ID || 'primary'; const events = await listEvents(startDate, endDate); console.log(`读取到 ${events.length} 个事件`); let successCount = 0; let skippedCount = 0; let failedCount = 0; for (const event of events) { if (event.status === 'cancelled') { continue; } const payload = { summary: event.summary || '(无标题)', description: event.description || '', location: event.location || '', startDateTime: event.start?.dateTime || event.start?.date || '', endDateTime: event.end?.dateTime || event.end?.date || '', attendees: (event.attendees || []).map((a) => a.email), }; const hash = hashPayload(payload); if (isAlreadyMigrated(sourceCalendarId, event.id)) { console.log(`跳过已迁移事件: ${event.id}`); skippedCount++; continue; } try { const result = await createCalendarEventViaZapier(payload); markMigrated(sourceCalendarId, event.id, hash, result.targetEventId); console.log(`迁移成功: ${event.id} -> ${result.targetEventId || '无ID'}`); successCount++; } catch (error) { failedCount++; db.prepare(` INSERT INTO sync_records (source_calendar_id, source_event_id, status, payload_hash, error_message) VALUES (?, ?, 'failed', ?, ?) ON CONFLICT(source_calendar_id, source_event_id) DO UPDATE SET status = 'failed', error_message = excluded.error_message, updated_at = datetime('now') `).run(sourceCalendarId, event.id, hash, error instanceof Error ? error.message : String(error)); console.error(`迁移失败: ${event.id}`, error); } } console.log(`完成:成功 ${successCount},跳过 ${skippedCount},失败 ${failedCount}`); }这段代码体现了几个工程决策:
isAlreadyMigrated只判断status = 'success',失败记录不会阻止重试。- 迁移失败时会更新
error_message,方便后续排查。 - 使用
ON CONFLICT DO UPDATE保证同一事件重复插入不会报唯一约束错误。
5.5 运行与命令
在根目录创建一个入口文件:
// 文件路径:src/index.ts import { runMigration } from './syncCalendar'; const startDate = process.argv[2] || new Date(Date.now() - 30 * 24 * 60 * 60 * 1000).toISOString(); const endDate = process.argv[3] || new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString(); runMigration(startDate, endDate).catch((error) => { console.error(error); process.exit(1); });运行命令:
npx tsx src/index.ts "2025-01-01T00:00:00+08:00" "2025-03-01T00:00:00+08:00"如果不想手动传参数,也可以直接用默认的最近 30 天到未来 30 天:
npx tsx src/index.ts6. 运行结果与效果验证
脚本启动后,预期会输出类似下面的日志:
读取到 23 个事件 迁移成功: abc123eventid1 -> 12345 迁移成功: abc123eventid2 -> 67890 跳过已迁移事件: abc123eventid1 完成:成功 2,跳过 1,失败 0验证是否成功,可以从三个角度检查。
第一,目标日历是否出现新事件。打开 Zapier 对应的目标日历,确认事件标题、时间和参会人是否正确。如果事件没有出现,优先查看 Zapier 的任务历史(Task History)里 Webhook 请求的状态。
第二,重复运行是否产生重复事件。这是验证幂等最直接的方式。把同一个startDate和endDate再执行一次,日志里应该出现“跳过已迁移事件”,目标日历里不出现重复日程。
第三,数据库记录是否完整。用 SQL 查看sync_records表:
sqlite3 migration.db "SELECT source_event_id, status, target_event_id, error_message FROM sync_records LIMIT 20;"如果失败,第一步应该看代码抛出的错误信息和 Zapier 的 Task History。常见的情况是:请求到了 Zapier,但 Zap 里的字段映射和你发送的 JSON 字段名不一致,导致日历事件创建失败。这种情况脚本不会报错,因为 HTTP 状态码是 200,但你会在 Zapier 任务历史里看到红叉。
7. 常见问题与排查思路
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
| 返回 401 或 Invalid Credentials | Google OAuth refresh token 过期或被撤销 | 检查.env中凭据;重新生成 refresh token | 确认 OAuth 用户仍有权访问日历;使用最小权限 scope |
| 读取事件为空 | 时间范围写错,或日历 ID 不是primary | 打印SOURCE_CALENDAR_ID;用 API Explorer 重放请求 | 确认日历 ID;扩大时间范围 |
| Zapier Webhook 返回 200 但目标日历无事件 | Zap 内部动作失败,字段映射不匹配 | 查看 Zapier Task History 和 Zap 动作的字段映射 | 对照 Zap 的必填字段调整 JSON payload |
| 重复运行产生重复事件 | 没有幂等表,或唯一约束失效 | 查询sync_records是否有记录;检查是否同一源事件 ID | 确保ON CONFLICT逻辑正确;不要删除迁移记录表 |
| 重复事件展开导致数据量巨大 | singleEvents=true把 RRULE 展开为大量实例 | 检查日志里“读取到多少个事件” | 根据需要改为读取 recurrence 规则,或加过滤条件 |
| 时区错乱 | 源事件没有timeZone字段,或 JSON 传入的是无时区字符串 | 查看源 API 返回的start字段 | 统一使用带偏移的 ISO 字符串,如2025-01-01T10:00:00+08:00 |
| better-sqlite3 安装失败 | Node 版本与原生模块不兼容 | node -v;查看安装日志 | 使用 Node 18+;删除node_modules后重新npm install |
这些坑每一个都可能让脚本从“看起来能跑”变成“实际不能用”。尤其是时区和重复事件,建议在正式迁移前先用一个小规模测试集跑通。
8. 最佳实践与工程建议
8.1 OAuth 凭据安全
不要把 client secret 和 refresh token 放在代码仓库里。.env文件在本地开发没问题,CI 或服务器环境应该使用密钥管理服务。如果怀疑凭据泄漏,立刻在 Google Cloud Console 里重置客户端密钥。
同时,生产环境尽量使用服务账号而不是用户 OAuth。服务账号适合内部日历集成,用户 OAuth 适合需要模拟用户权限的场景。选型的核心标准是:这个日历数据属于个人用户,还是属于服务账号可访问的共享日历。
8.2 幂等设计的取舍
本文用的是“按源事件 ID 去重,成功后标记”。这个方案的优点是简单可靠,缺点是如果源事件被修改,不会同步到目标日历。
如果业务要求修改也能同步,就需要增加一个“检测到 payload_hash 变化后更新目标事件”的分支。但在更新日历事件时,要考虑调用的是目标系统的更新接口,而不是再次创建。这会增加一倍的接口逻辑复杂度,因此建议在“追加型迁移”阶段先只做新建,确认整个链路稳定后再加更新逻辑。
8.3 限流与重试
日历 API 和 Zapier Webhook 都有速率限制。代码里应该加入退避重试机制。简单做法是在捕获网络错误或 429 状态时,等待2^n秒后重试,最多重试 3 次。如果重试后仍然失败,把事件标记为failed,并通过日志系统告警,而不是阻塞整批任务。
8.4 测试策略
日历迁移至少要准备三类测试数据:
- 普通单次事件。
- 重复事件(确认是展开还是保留 RRULE)。
- 跨时区事件。
- 被取消的事件。
用这些数据分别验证:迁移成功、跳过已迁移、失败重试、字段映射正确。建议把日历时间范围缩小到几天,数据量控制在 10 条以内,避免因为数据太多掩盖逻辑问题。
8.5 AI 编码代理的使用边界
AI 编码代理在生成代码时,能明显提升效率和准确度,但要注意几个原则:
- 让代码可审查:生成代码后,逐行看它访问了哪些资源,尤其是 OAuth、Webhook 和数据库写操作。
- 把需求写进提示词:不要只说“读取日历事件”,要说“读取指定时间范围内未取消的事件,并按源事件 ID 去重”。
- 让 AI 解释报错:遇到不熟悉的 API 错误,把完整报错贴给它,比自己去搜索文档快得多。
- 不要让它直接操作生产数据:AI 编码代理执行终端命令时,最好限定在测试目录,避免误跑删除或批处理命令。
9. 总结与后续学习方向
日历事件自动迁移看起来是个简单的脚本,真正做起来,涉及 OAuth 权限、数据建模、幂等控制、时区换算、跨平台字段映射和失败重试。用 AI 编码代理可以快速生成骨架代码,但架构决策仍然要由你来完成。
建议你按下面的路径继续实践:
- 先用本文的代码,在自己的测试日历上跑通一次最小迁移。
- 查看 Zapier Task History,理解请求进入 Zap 后的执行链路。
- 增加一个“按修改时间同步”的功能,在事件更新时同步到目标日历。
- 尝试把迁移能力抽象成可配置的规则,比如不同日历映射到不同前缀、不同标签。
- 研究 Google Calendar API 的
syncToken增量同步机制,替代按时间范围全量扫描,提高长期运行的效率。 - 在生产环境部署前,务必在隔离环境中测试并发、重试和权限回收场景。
日历数据的价值在于“时间承诺”。自动化的意义不是把所有事件一次性搬过去,而是建立一条可持续、可观测、可回滚的同步通道。先跑通最小闭环,再逐步扩展,是这类项目最稳妥的推进方式。