背景与动机#

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 正式推出了一个新 OAuth 工具”,而是 Google Workspace 生态里出现了一个很实用的开源 CLI,它试图把大量样板代码和认证细节集中到一个工具里。

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

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

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

这也是我在自己的 skill 里使用它的原因。我的目标不是在 skill 里重新实现一套 Google OAuth 流程,而是尽快拿到一个可复用的用户凭据,然后去调用 Google Calendar API。用 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-authCredentials.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.readonlydrive.readonly

在 Google OAuth 里,calendarcalendar.readonly 是不同的 scope;drivedrive.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