在 Google Workspace 上做自动化和 AI Agent 接入,我是从一个很痛苦的阶段过来的。两三年前我第一次正经把工作区的命令行工具当生产力主力来用,说实话体验很糟糕:要记的 URL 太多、要改的 JSON 太长、几分钟一过访问令牌就失效,稍不注意整个流程就断在半路。后来我换了个思路,不再为每一个 API 方法单独写死脚本,而是做了一层“动态构建命令行”的封装,让 AI_agent 可以根据用户需求在现场把命令拼出来,执行完再校验结果。这套玩法跑通之后,无论是查邮件、建表格、拉日历日程,还是把这些操作组合成一个自动化任务,都变得非常顺。这篇就把它讲透,适合正在做 AI 办公自动化、写智能体工具链、或者只是想把手边跟 Google Workspace 相关重复劳动消灭掉的朋友。
1. Google Workspace CLI 到底解决什么问题
1.1 没有命令行之前,办公自动化有多难受
很多人的第一反应是:Google 不是有 Gmail、Google 日历、Google 表格的网页界面吗?我在上面点点不就行了吗?在“人用”的场景里当然没问题,但在“程序用”和“AI 用”的场景里,网页界面反而是最大的障碍。AI 没法稳定地点击网页里的按钮,自动化脚本也没法精确判断某个弹窗是否出现、某个页面是否加载完成。你写一个 Selenium 或者 Playwright 的脚本去操作网页,只要 Google 改一次布局,脚本大概率就要重写。
网页界面的另一个问题是批量操作。你想给 50 个人单独发不同的会议邀请,网页上一个人一个人点过去,不仅慢,还容易漏。可是只要走命令行的方式,全部逻辑就是一个循环:读名单、拼参数、逐个调用 API。我实测下来,50 封个性化邮件从拼装到发送不到一分钟,而且每一封都可以留日志。更关键的是,命令行的输入和输出都是文本,天然适合被 Python、Shell 或者大模型读取和处理。
1.2 AI_agent 的“原生语言”就是命令行
现在做 AI_agent 的朋友应该都有同感:大模型擅长生成文本和代码,但它不擅长操纵图形界面。你让 GPT 去打开浏览器、点击某个按钮、读取弹窗内容,这一步的准确率和速度都远不如让它写一段代码、执行一个命令、再读取标准输出。所以我一直认为,AI_agent 想真正落地到办公场景,得给它的“手”是一套可靠的命令行接口,而不是一套模拟点击的界面脚本。
命令行天然具备三个对 AI 特别友好的特性。一是可观测性:命令执行后输出的内容就是结构化文本,AI 可以直接解析;二是可管控性:你能在命令执行前拦一道,校验参数、确认权限;三是可回滚性:命令结果错了,日志还在,你可以回查。网页界面这三点都很难做到。
1.3 “动态构建”和“静态脚本”的分界线在哪
很多人误解动态构建的含义,以为就是写一堆固定命令,然后通过 Shell 变量替换几个参数。比如“发送邮件到 $TO 邮件地址,正文是 $BODY”,这种只能叫参数化脚本,谈不上动态。真正的动态构建是指:把命令的路径、方法、请求体、查询条件这些元素全部拆开,让调用方(尤其是 AI)根据上下文现场组合出完整请求。
举个例子。固定脚本能做的任务是“读取收件箱前 10 封邮件”。动态构建能做的任务是“根据用户今天剩下的会议时间,判断是否有空处理某个紧急审批邮件,如果处理不了就自动起草一封延迟回复”。后者需要先查日历、再查邮件、再生成决策,最后拼出邮件请求,中间每一步都要临时决定调用哪个接口、传什么参数。这种场景靠固定脚本写是写不完的,只有动态构建的命令层才撑得住。
2. 动态构建工作区的命令层设计
2.1 先用组合工具搭底子:GAM、oauth2l、curl 和官方 SDK
首先要澄清一个概念,市面上没有一个官方命令统一叫workspace。实际操作中大家口中的 Google Workspace CLI,通常是一套组合拳。GAM(Google Admin Manager)是一个社区里非常流行的开源命令行工具,主要用于管理用户、群组、日历资源等域级配置;oauth2l是 Google 官方维护的命令行令牌获取工具;curl适合直接打 REST API 做调试;如果要产品化,就用google-api-python-client官方 Python 库。
我建议的搭配是:GAM 管组织架构和批量管理,oauth2l 快速拿令牌,curl 做接口验证,Python SDK 做最终封装。日常排查问题的时候,我会先用GAM看能不能用现成指令解决问题,再用curl打一次原始 API 看返回结构。这样的好处是每一层都很薄,出了问题都知道去哪查,不会出现“一个几千行的工具黑盒,报错都不知道从哪看起”的情况。
2.2 动态命令的第一原则:把参数外部化
动态构建最关键的设计原则,是不要让 URL、请求头、查询参数散落在脚本各处,而是要统一收敛成“调用方传参数 -> 工具拼请求 -> 执行器发请求 -> 校验结果”的管线。我最开始的做法是把命令直接写在 Bash 脚本里,用环境变量接收外部参数。
# scripts/fetch_gmail.sh export TOKEN=$(oauth2l fetch --scope "https://www.googleapis.com/auth/gmail.modify" --json) QUERY=${QUERY:-"in:inbox newer_than:1d"} LIMIT=${LIMIT:-10} curl -sS \ -H "Authorization: Bearer ${TOKEN}" \ "https://gmail.googleapis.com/gmail/v1/users/me/messages?q=${QUERY}&maxResults=${LIMIT}" | jq '{messages: [.messages[]?.id], nextPageToken}'这里QUERY和LIMIT都不是写死的,而是由外部传入。AI_agent 或者上层自动化脚本只要计算出这两个参数,就能复用同一个命令。这种做法最大的好处是:命令本身可以注册成 AI 的工具,AI 只需要按照参数协议生成值,不需要去背 Gmail API 的完整 URL。
2.3 用 Python 做更稳的动态路由
Bash 写起来快,但参数多了之后容易出问题,比如引号转义、数组处理。生产环境我建议用 Python 做一层统一路由。这里提供一个我一直在用的简化思路:只保留一个入口函数,接收method、path、query和body,由它负责拼 URL、携带 Token、序列化参数和返回 JSON。
# workspace_cli/runner.py import json import os import urllib.request BASE = "https://{service}.googleapis.com" def call(service: str, method: str, path: str, query: dict | None = None, body: dict | None = None) -> dict: token = os.environ["WORKSPACE_TOKEN"] url = BASE.format(service=service) + path if query: url += "?" + urllib.parse.urlencode(query) headers = { "Authorization": f"Bearer {token}", "Content-Type": "application/json", } data = json.dumps(body).encode() if body is not None else None req = urllib.request.Request(url, data=data, headers=headers, method=method) with urllib.request.urlopen(req, timeout=30) as resp: result = resp.read().decode() return json.loads(result) if result else {}别看这个函数短,它承载了整个动态构建的核心逻辑。AI 或者上层脚本只需要确认四件事:调哪个服务、用什么方法、请求路径是什么、请求体是什么。函数内部把鉴权、序列化、超时、异常统一处理掉,输出也是固定的 Python 字典,后面想接日志、接告警、接测试都非常方便。
2.4 把“动态”圈在允许列表里,而不是放任自流
动态构建最让人担心的是安全:如果 AI 什么接口都能调,那它猜错一个参数可能就把不该删的数据删了。我的处理方式是给命令层加一张“允许表”。允许表里写明服务名、允许的方法、必要的 scope,以及哪些操作需要二次确认。比如只给日历模块开放读取和创建事件的权限,删除事件一律先进入审核队列。
这一步刚做的时候看起来限制很多,但实际跑起来之后你会发现,正是这个白名单保住了整个系统的底线。AI 就算生成完全离谱的请求,到了路由层也会被拦下来,返回一条类似operation not allowed的错误,而不是真的对生产数据造成破坏。自动化越灵活,这个安全围栏越不能省。
3. 实际动手:5 分钟跑通 Gmail、日历、表格三条命令
3.1 先准备授权:两种方式按场景选
Google Workspace 命令行自动化有两大授权路线。第一种是开发者本机的 OAuth 授权,适合个人账号和日常调试;第二种是服务账号配合域管理员授权,适合公司内跑无人值守的定时任务。我在个人实验里通常走gcloud的 application default credentials,一条命令就能拿到可用的令牌。
# 选 A:本机快速调试 gcloud auth application-default login # 然后设置环境变量 export WORKSPACE_TOKEN=$(gcloud auth application-default print-access-token) # 选 B:服务账号方式,适合定时任务 gcloud auth activate-service-account --key-file=service-account.json \ --project=your-project-id服务账号方式需要你在 Google Cloud Console 里开通相应 API,并且给服务账号授权。公司域内的服务账号还常常要配合“全域委派”才能模拟员工身份去访问日历和 Gmail,这一步如果配置错了会得到403或者invalid_grant,后边排查小节我会专门说。
3.2 读 Gmail 邮件:一条命令返回结构化列表
授权拿到 Token 之后,我最常调的是 Gmail 的搜索接口。它的优点是支持非常灵活的搜索语法,比如“最近三天的未读邮件”“某个标签下的所有邮件”,然后通过maxResults控制返回量。
curl -sS \ -H "Authorization: Bearer ${WORKSPACE_TOKEN}" \ "https://gmail.googleapis.com/gmail/v1/users/me/messages?q=in:inbox%20is:unread&maxResults=5"我一般在本地会再加一个jq管道,只挑出 ID 列表,因为完整邮件内容还需要再用messages/{id}接口单独取。这一段是典型的“动态构建”场景:搜索词、标签、数量全部由调用方在下一次执行时传入,而不是每次改脚本。AI_agent 如果要做“邮件摘要”,通常也是先拿到 ID 列表,然后逐封拉取正文再丢给大模型。
3.3 日历日程和表格创建:两个高频样例
日历场景里最高频的操作是查询未来事件。下面这个命令会返回主日历未来一周的前 10 个日程,按开始时间排序。
NOW=$(date -u +"%Y-%m-%dT%H:%M:%SZ") WEEK=$(date -u -d "+7 days" +"%Y-%m-%dT%H:%M:%SZ") curl -sS \ -H "Authorization: Bearer ${WORKSPACE_TOKEN}" \ "https://www.googleapis.com/calendar/v3/calendars/primary/events?timeMin=${NOW}&timeMax=${WEEK}&maxResults=10&singleEvents=true&orderBy=startTime"表格场景更常用创建和写入。创建表格的请求只需指定标题,返回值里就会带上新的 spreadsheetId。拿到这个 ID 后再拼一条追加写入的命令,就能把自动收集到的数据塞进去。
curl -sS -X POST \ -H "Authorization: Bearer ${WORKSPACE_TOKEN}" \ -H "Content-Type: application/json" \ -d '{"properties":{"title":"AI 自动生成报表"}}' \ "https://sheets.googleapis.com/v4/spreadsheets"我在做自动化报表的时候,通常会用 Python 拿到 spreadsheetId 后,再用values.append把多行数据一次性写进去。活泼一点说,这就像是给了机器人一间办公室、一个日历本、一张表格纸,它接下去就可以自己去“上班了”。
3.4 安全细节:最小权限、不入仓库、留审计日志
授权这里有几个坑,值得单独拎出来说。第一,给你的令牌和凭据文件设置尽量低的权限。如果我只需要发邮件,就不会申请gmail.readonly和gmail.modify以外的作用域,更不会申请删除相关作用域。第二,服务账号 JSON 密钥文件打死不要提交进 Git 仓库,也不要放在无密码共享盘,我自己的做法是放在本地~/.config/workspace-cli/目录,然后赋予chmod 600权限,运行时从环境变量读取路径。
第三,每次命令执行要在日志里记录调用者、目标接口、关键参数和返回码。刚开始我用最简单的文本日志,后来换成每行一个 JSON 的日志格式。没有日志,你后续根本没法复盘 AI 为什么把某个会议时间写错了,也没法给运维排查提供依据。
4. 让 AI_agent 学会调用工作区
4.1 用函数调用协议把能力暴露给大模型
现在主流大模型都支持 function calling。你只要把一个工具的描述、参数说明以 JSON Schema 的形式告诉模型,模型在收到用户问题后会先输出一个待执行的函数名和参数,再由你本地的代码去真正执行。下面这段协议是我的常用模板。
{ "name": "calendar_list_events", "description": "查询用户的日历日程,返回时间段内的事件列表", "parameters": { "type": "object", "properties": { "time_min": {"type": "string", "description": "起始时间,ISO 8601 格式"}, "time_max": {"type": "string", "description": "结束时间,ISO 8601 格式"}, "max_results": {"type": "integer", "default": 10} }, "required": ["time_min"] } }我这里刻意不把path和method暴露给模型,而是暴露语义化参数。原因是模型容易“自由发挥”编造接口路径,如果让它填path它可能填一个不存在的 URL 出来报错;给它语义参数,它只需要理解“开始时间”“结束时间”,错误率会低很多。
4.2 让模型生成参数,不让模型直接拼 URL
踩过几次坑之后,我的结论非常明确:大模型负责决策,代码负责组装。模型的任务是判断“用户要查今天到明天有哪些日程”,然后把它翻译成time_min=2025-...+time_max=...这样的结构化参数,至于请求最终打到calendar/v3/...的哪个完整地址,应该是本地路由函数根据 allowlist 决定。如果接到time_min缺失、格式错误的参数,稳健的做法是直接返回参数校验错误,让模型自己修正重试。这样既利用了模型的自然语言理解能力,又避免了模型幻觉直接污染 API 调用。
为了达到这个效果,我还会在工具描述里写清楚格式示例和限制条件,比如“time_min 必须是 ISO 8601 格式,示例 2025-01-01T09:00:00Z”“只能查询主日历”。描述越具体,模型第一次生成正确参数的概率就越高。
4.3 一个完整的“日程摘要通知”Agent 案例
这里分享一个完整的小案例:用户说“帮我看看这周会议多不多,挑出最忙的一天,并把会议名单存到表格里”。整个流程拆成四步:
- 调用日历查询命令,拉取本周所有日程;
- 本地脚本统计每天的事件数量,算出最忙的那天;
- 调用 Sheets 命令,在工作表里追加一条汇总记录;
- 调用 Gmail 命令,把结果邮件发给自己。
每一步对应一个明确的工具调用,中间结果都是结构化 JSON。AI_agent 在这个流程里的作用是理解意图和拆解步骤,真正的数据读取和写入都发生在命令层。我试过把流程完全交给模型写代码跑,问题在于它容易自作主张调整格式;用函数调用约束住之后,整个流程的稳定性和可预测性高了一个档次。
5. 自动化落地的周边配套
5.1 定时任务:三种跑法按需选
CLI 折腾清楚了,接下来就是把任务定时跑起来。个人开发环境里我推荐最朴素的 cron,只要你的机器不关机就行。
# 每天早上 9 点发送昨日工作摘要 0 9 * * * /usr/local/bin/python3 /home/user/workspace_cli/daily_summary.py >> /var/log/workspace_cli.log 2>&1Windows 环境可以用计划任务程序,macOS 推荐launchd。如果不想依赖本地机器,GitHub Actions 的 schedule 定时任务也完全能跑,把服务账号凭据配置成 Repository Secret 后,在 workflow 里设置 cron 表达式即可。这里有个细节:定时任务跑出的错误更容易被忽略,所以一定要有退出码判断和通知机制,绝不能“静默失败”。
5.2 用 pytest 给命令层做回归测试
自动化任务最怕的是改一处逻辑,别的地方悄悄坏了。我在命令层旁边写了一套 pytest 测试,专门验证参数拼装和结果解析。这里的思路是:真实的 API 请求只对有限环境跑,日常测试用 mock 数据。
# tests/test_calendar.py import pytest from workspace_cli import runner def test_calendar_query_url_building(monkeypatch): captured = {} def fake_call(service, method, path, query=None, body=None): captured["service"] = service captured["path"] = path captured["query"] = query return {"items": []} monkeypatch.setattr(runner, "call", fake_call) result = runner.list_calendar_events( time_min="2025-01-01T00:00:00Z", time_max="2025-01-02T00:00:00Z" ) assert captured["service"] == "www.googleapis.com" assert captured["path"] == "/calendar/v3/calendars/primary/events" assert captured["query"]["maxResults"] == 50 assert "items" in result别小看这些回归测试。我踩过一个很痛的坑:某天为了改日志格式,顺手动了参数序列化的位置,结果所有日历查询都悄悄少了timeMin参数,任务是过了三天才被用户发现的。有了 pytest 卡着,这类低级错误在合并代码的时候就会被立刻拦住。
5.3 幂等设计和重试策略是保命关键
自动化任务重复执行是常态。比如定时任务跑了两次,如果每次执行都往表格里追加一遍当天数据,就会出现重复行。解决办法是在命令层支持幂等:写入前先查是否存在相同 ID,或者把任务 ID 写入表格作为唯一主键,追加前先做去重检查。
网络请求的失败重试也很重要。Google API 偶尔会返回 429 限流或者 5xx 服务错误,我的封装里做了简单的指数退避重试:第一次失败等 1 秒,第二次等 2 秒,最多重试 4 次。但如果遇到 401 表示令牌过期,不应该重试,而应该立即刷新令牌。
5.4 把结果推给 IM 和企业群
CLI 执行完任务,结果如何通知人是另一个常见需求。最简单的方式是 Webhook 推送,把摘要文本 POST 到企业 IM 群机器人地址。这里的关键是把摘要控制在几行以内,并且带上任务名和执行时间。
curl -sS -X POST \ -H "Content-Type: application/json" \ -d '{"msg_type":"text","content":{"text":"9月20日会议统计已生成,最忙时段 14:00-16:00,明细见表格链接。"}}' \ "${IM_WEBHOOK_URL}"我在真实项目里的习惯是:正常完成任务推送一条简短记录,失败任务推送一条带日志文件路径的告警,用户一看就知道要不要人工介入。
6. 我踩过的 9 个坑:排查速查表
以下这些问题是新手最容易碰到的,也是我逐一排查过无数遍才总结出来的。做个表格方便你直接对号入座。
| 现象 | 大概率原因 | 解决办法 |
|---|---|---|
| 调用返回 401 Unauthorized | 令牌过期,或环境变量没设置 | 重新执行 gcloud auth application-default login,确认 export WORKSPACE_TOKEN |
| 返回 403 权限不足 | 服务账号没有被授权对应作用域 | 检查全域委派或 OAuth consent scope,至少包含工作所需的最小权限 |
| invalid_grant 错误 | 系统时间不准,或 OAuth client 与凭据不匹配 | 校准本机时间,重新生成凭据,检查 client_id 对应关系 |
| 日历查询不到任何事件 | 时间参数用错时区,或日历权限不足 | 统一使用 ISO 8601 UTC 格式,确认调用身份对主日历有读权限 |
| 创建了表格但打开为空 | 只创建了 spreadsheet,没有调用 values.update/append 写入数据 | 创建完成后必须显式写入表头和数据范围 |
| 中文内容乱码 | 请求体编码不是 UTF-8,或 Content-Type 缺失 | 确保 json.dumps 时 ensure_ascii=False,请求头带 utf-8 |
| GAM 命令突然失效 | GAM 版本过旧,或 API 接口发生变更 | 定期更新 GAM,检查官方 changelog |
| AI 反复调用同一步骤 | 工具描述不够清晰,模型不知道已有前置结果 | 在 prompt 中显式说明步骤依赖,或让工具接受可选的缓存参数 |
| 任务重复发送通知 | 脚本重试逻辑导致重复执行 | 为每次任务生成 UUID,接收端按 UUID 去重,或者写入去重表 |
这几个坑单独看都很小,但组合在一起,足以让一个自动化的初版项目连续卡住两三天。排查的时候建议按“令牌 -> 权限 -> 参数格式 -> 返回结构”的顺序来,不要一上来就怀疑框架和网络。
7. 最后聊一点我的实际体会
做的自动化越多,我越觉得“动态构建命令行”的本质不是炫技,而是给整个系统一个干净的边界。命令行作为边界,上面接大模型做意图理解和拆解,下面接 Google Workspace 的稳定 API,中间所有的输入输出都是文本和结构化 JSON。这样即使以后换了别的办公套件,只要命令层保持同样协议,上层 AI 逻辑几乎不用重写。
还有一个小技巧想分享:动态构建的命令层不要追求一次性写全,先跑通两条命令、加好测试、配上日志和告警,再慢慢扩展能力。我在实践里发现,一个“小而稳”的工具链比一个“全而乱”的工具链可靠得多。后续如果项目有时间,我会继续把 GAM 的管理类操作也封装进同一层,让 AI_agent 不止能读邮件查日历,还能真正帮企业处理账号和群组的基础运维事务。