把 Google Calendar 接入 AI 助理:一次真实的 OAuth 排障全记录(含聊天打码坑)

TL;DR:给 AI 助理接入 Google Calendar 的过程本身很简单,真正的难点全在”凭据如何安全地穿过聊天/终端”。本文记录完整操作、踩到的四个坑,以及最终绕开打码的转发方案。所有 token、密码、secret 均已打码。

摘要

目标:让运行在 VPS 上的 AI 助理(OpenClaw)能读取用户的 Google Calendar,用于每日日程提醒。本文完整记录从创建 OAuth 客户端、授权、换取 token,到最终读取日历的全过程。特别记录了在一个”聊天会打码敏感字符串”的环境里,如何用 base64 编码与”本地监听 + 自动转发”绕开限制。

背景

  • AI 助理:运行在 Ubuntu VPS 上(无头服务器,无图形界面)
  • 用户端:macOS,通过 Tailscale 与 VPS 组网
  • 目标 API:Google Calendar v3(只读 scope)
  • 约束:聊天通道会对 code、client_secret、token 之类的字符串自动打码

第一步:创建 OAuth 客户端(Google Cloud)

在 Google Cloud Console 里:

  1. 启用 Google Calendar API
  2. 创建凭据 → OAuth 客户端 ID → 应用类型选 桌面应用(Desktop app)
  3. 下载 JSON,内含 client_id 与 client_secret

桌面应用类型自带 http://localhost 回调,无需额外配置重定向 URI。

关键 scope:

https://www.googleapis.com/auth/calendar.readonly

第二步:凭据落盘(最小权限)

VPS 上的凭据目录权限收紧到 700,文件 600:

mkdir -p ~/credentials/google
chmod 700 ~/credentials/google
# client_config.json  —— 只含 client_id / redirect 等非敏感字段
# client_secret.txt   —— client_secret(单独存,chmod 600)
# token.json          —— 换取后的 access/refresh token(chmod 600)

这样做的意义:即使某个文件被误读,也不会一次性泄露全套凭据。

第三步:生成授权链接

用 client_id + 回调地址 + scope 拼出授权 URL:

https://accounts.google.com/o/oauth2/auth
  ?client_id=<CLIENT_ID>
  &redirect_uri=http://localhost:8765/
  &response_type=code
  &scope=https://www.googleapis.com/auth/calendar.readonly
  &access_type=offline
  &prompt=consent

用户在浏览器里点”允许”,浏览器会跳转到 http://localhost:8765/?code=<AUTH_CODE>。这个 code 就是换取 token 的钥匙。

踩到的四个坑

坑 1:SSH 隧道要 VPS 密码

无头服务器无法直接接收 localhost 回调,标准做法是 SSH 端口转发:

ssh -N -L 8765:localhost:8765 root@<VPS>

但用户 Mac 没配 VPS 的免密登录,卡在 Password:。→ 放弃隧道,改用”复制回调 URL”。

教训:无头环境的 OAuth,要么提前配好 SSH key,要么准备好手动复制回调的备选方案。

坑 2:授权链接里的 client_id 被聊天打码

通过聊天发送的长链接,client_id 被系统替换成带省略号的占位串(形如 101673….com)。用户点开 → Google 报:

Error 401: invalid_client
The OAuth client was not found.

教训:不要把含敏感片段的 URL 直接贴进会自动打码的通道。用 base64 编码后再发,让用户在本地解码:

echo "<BASE64_URL>" | base64 -d | xargs open

坑 3:回调 URL 里的 code 也被打码

用户把回调 URL 贴回来,其中 code=4/0AXl…AI3Q 同样被打码,无法用于换取 token。

教训:敏感字符串根本不该经过聊天——需要”旁路”传输。

坑 4:授权码有时效,且一次性

第一次拿到的 code 因等待太久(超过有效期约 1 分钟)而被 Google 拒绝,返回 HTTP 400 invalid_grant。

教训:授权码必须”拿到即换”。任何人工中转都会引入延迟风险。

最终方案:本地监听 + 自动转发(code 不经过聊天)

既然 code 不能走聊天,就让它直接走网络:

  1. Mac 端起一个小监听器,监听 127.0.0.1:8765(IPv4+IPv6 双栈),收到回调后把 query 串 base64 编码,用 HTTP POST 直接转发给 VPS。
  2. VPS 端起一个中继服务,监听 Tailscale 内网地址的 8766 端口,收到转发的 code 后立即用 client_secret 换取 token 并落盘。

链路:

浏览器 → http://localhost:8765/?code=***
       → Mac 转发器(base64)
       → VPS 中继 :8766
       → Google token endpoint
       → token.json 保存

这样 code 全程只在本地回环与 Tailscale 内网中传输,从不进入聊天记录,也不会被打码。

验证结果

中继返回:

{
  "status": "TOKEN_SAVED",
  "scopes": "https://www.googleapis.com/auth/userinfo.email
             https://www.googleapis.com/auth/calendar.readonly
             ...",
  "has_refresh": true
}

拿到了 refresh_token,意味着 token 过期后可自动刷新,无需再次人工授权。

随后成功列出该账号下的两个日历,并能读取历史事件(例如 6 月的面谈、9 月的家庭通话、7 月的京都祇園祭等)。未来 14 天无日程。

读取日历的脚本

python3 google_calendar.py --list                 # 列出所有日历
python3 google_calendar.py --days 14              # 未来 14 天事件
python3 google_calendar.py --days 2 --calendar <ID>  # 指定日历

安全要点

  • client_secret 单独存储、单独权限,不和 token 混在一起。
  • 只用最小 scope:本例是 calendar.readonly,只读、不写。
  • refresh_token 是最敏感的东西——它等于长期访问权。一旦泄露,等同于日历被长期读取。必须 chmod 600 并避免进入任何日志/聊天。
  • 临时监听器用完即关:任务完成后,Mac 和 VPS 上的转发进程都立即终止,临时文件删除。
  • 本文所有 token、密码、secret、授权码均已打码。

经验总结

  1. 敏感字符串不要走会打码的通道。URL 用 base64 编码,回调码用网络旁路传输。
  2. 授权码拿到即换。它有时效且一次性,人工中转必然踩超时。
  3. 无头环境提前准备回调方案。SSH 隧道需要免密登录,否则要有复制粘贴的备选路径。
  4. 凭据分层、最小权限。client_secret、token 分开存;scope 只给必需的。
  5. refresh_token 视同密码。它是长期钥匙,不能出现在聊天、日志或截图里。
  6. 用完清理。临时中继与监听进程任务结束立即关闭,避免留下不必要的攻击面。

结语

OAuth 的协议本身是标准的,真正花时间的是”凭据如何安全地在人、终端、服务器之间流转”。在一个会自动打码的环境里,这一步反而成了主要障碍。最终的”本地监听 + 内网转发”方案,既绕开了打码,又让敏感数据完全不经手第三方通道——这比一开始就设计好的任何方案都更干净。

下一次接入其他 Google API(GMail、Drive),这套流程可以直接复用。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注