背景与动机#

gws 是一个面向 Google Workspace API 的命令行工具。它的定位很直接:用一套统一的 CLI 去访问 Drive、Gmail、Calendar、Sheets、Docs、Chat 等 API,并且直接输出结构化 JSON。按项目 README 的说法,它是 “built for humans and AI agents”,命令面还是根据 Google 的 Discovery Service 动态生成的。

这里有个事实要先说清楚:它不是 Google 官方正式支持的产品。README 明确写了 “This is not an officially supported Google product.” 所以更准确的说法是:Google Workspace 生态里出现了一个很实用的开源 CLI,它试图把大量样板代码和认证细节集中到一个工具里,这和“Google 正式推出了一个新 OAuth 工具”不是一回事。

跟传统的 Google OAuth 接法相比,gws 的好处主要在工程层面。按 Google 官方的 OAuth 2.0 文档 和 Desktop App 流程,自己接一次用户授权通常要处理 OAuth client、浏览器跳转、回调、refresh token 持久化、scope 管理,以及后续 token 刷新。对一个只是想操作 Google Calendar 的脚本或 skill 来说,这些步骤并不复杂,但很分散,也很容易把注意力从“我要调用哪个 Calendar API”转移到“我又把 OAuth 配错在哪里”。

gws 解决的正是这部分重复劳动:

  • 它把 auth setup、auth login、auth export 这些步骤收成了统一命令,适合本机、CI 和脚本化场景
  • 它自带按服务筛 scope 的交互流程,不用自己手拼完整授权 URL
  • 它输出结构化 JSON,也能把凭据导出给别的程序继续用
  • 它本身就是围绕 Workspace API 设计的,所以拿来操作 Calendar 这类接口时更顺手

这也是我在自己的 skill 里使用它的原因。我的目标是尽快拿到一个可复用的用户凭据,然后去调用 Google Calendar API,不打算在 skill 里重新实现一套 Google OAuth 流程。用 gws 之后,skill 只需要关心两件事:

  • 申请对的 scope
  • 把导出的凭据交给 Python 代码或后续命令继续使用

文章下面的内容就只保留这一条主线:怎样正确用 gws 拿到 token,并把它稳定地接到 Python 的 Google Calendar 操作里。

一句话总结#

如果你的 Python 脚本只是读取 Google Calendar 和 Drive,我目前更倾向下面这种做法:

gws auth setup
gws auth login --readonly -s calendar,drive
gws auth export --unmasked > ~/.config/my-script/token.json

对应文档:

这里最重要的一点只有一条:-s calendar,drive 只是限制 scope picker 里显示哪些服务,真正决定授权范围的是 --readonly 或 --scopes。这一点以本地 gws auth login --help 为准。

第一步:先把 gws 登录好#

如果你本机已经装了 gcloud,按 gws auth setup 走一次即可。

如果你不想依赖 gcloud,直接按 gws 的 Manual OAuth setup 在 Google Cloud Console 里创建 Desktop app 类型的 OAuth client,然后再执行:

gws auth login --readonly -s calendar,drive

这条命令适合“只读 Calendar + Drive”场景:

  • --readonly 表示请求只读权限
  • -s calendar,drive 表示只在 picker 里挑 Calendar 和 Drive 相关 scope

如果你想完全显式地指定 scope,也可以直接使用 --scopes:

gws auth login --scopes \
https://www.googleapis.com/auth/calendar.readonly,\
https://www.googleapis.com/auth/drive.readonly

scope 含义参考 Google 官方文档:

第二步:把凭据导出给脚本#

登录完成后,用 gws 官方文档里的导出方式:

gws auth export --unmasked > ~/.config/my-script/token.json

参考:Headless / CI (export flow)

这一步会把 refresh token、client ID、client secret 等信息导出成 JSON。这个文件最好按敏感凭据来保存,不要提交到仓库。

第三步:Python 里显式传入 scopes#

在 Python 里,我目前更倾向直接使用 google-auth 的 Credentials.from_authorized_user_file 并显式传入 scopes=SCOPES:

from pathlib import Path

from google.auth.transport.requests import Request
from google.oauth2.credentials import Credentials

SCOPES = [
    "https://www.googleapis.com/auth/calendar.readonly",
    "https://www.googleapis.com/auth/drive.readonly",
]

TOKEN_FILE = Path.home() / ".config/my-script/token.json"

creds = Credentials.from_authorized_user_file(
    TOKEN_FILE,
    scopes=SCOPES,
)

if not creds.valid:
    creds.refresh(Request())

这样写有两个好处:

  • 你的脚本实际使用哪些 scope,一眼就能看清
  • 就算导出的 JSON 里没有 scopes 字段,google-auth 也能按你传入的 SCOPES 刷新 token

如果你读取的是 JSON 字典而不是文件,也可以使用 Credentials.from_authorized_user_info;原则一样,仍然显式传 scopes=SCOPES。

常见错误:invalid_scope#

如果刷新 token 时遇到:

invalid_scope: Bad Request

优先检查一件事:登录时申请的 scope,和脚本里实际使用的 scope,是不是同一组 URI。

最常见的错误是:

  • 登录时用了 gws auth login -s calendar,drive
  • 代码里却在用 calendar.readonly 和 drive.readonly

在 Google OAuth 里,calendar 和 calendar.readonly 是不同的 scope;drive 和 drive.readonly 也是不同的 scope。对应关系看官方 scope 文档:

所以对只读脚本来说,通常更省事的做法还是重新登录一次:

gws auth login --readonly -s calendar,drive
gws auth export --unmasked > ~/.config/my-script/token.json

最后保留一条经验#

如果你是给 Python 脚本接 gws,我觉得先记住这条就够用了:

登录时用 --readonly 或 --scopes 把权限说清楚;Python 里再把同一组 SCOPES 显式传给 google-auth。