gogcli 文档评论轮询指南:用gog docs comments poll持久化监听 Google Docs 评论变化
【免费下载链接】gogcliGoogle Workspace in your terminal.项目地址: https://gitcode.com/GitHub_Trending/gogcl/gogcli
gog docs comments poll是 gogcli 提供的持久化轮询命令,用于持续监听 Google Docs 文档中新增或被修改的评论,并把已处理进度(watermark)写入本地 JSON 状态文件,从而做到断点续传、不重不漏。本文以该命令为主线,完整讲解其用法、状态文件机制、输出格式、Shell Hook 事件回调与源码级实现原理,帮助你在终端、CI 或自动化工作流中可靠地消费 Google Docs 评论流。
命令概览与定位
gog docs comments poll属于gog docs comments命令族,负责"Poll new and modified comments with persisted state"(轮询新增/修改的评论并持久化状态)。它的核心价值在于:Google Drive Comments API 只提供基于modifiedTime的时间过滤,而不是真正的推送通道;轮询命令通过本地状态文件记住"上次看到哪里",让每次轮询只产出真正的新事件,即使进程重启也不会重复消费历史评论。
gog docs (doc) comments poll --state-file=STRING <docId> [flags]其中<docId>是位置参数,接受 Google Doc ID 或完整文档 URL(源码中通过normalizeGoogleID归一化,见 internal/cmd/docs_comments_poll.go)。--state-file为必填参数,用于指定存储评论时间水印的 JSON 文件。
该命令所属的命令族(见 docs/commands/gog-docs-comments.md)还包括:
gog docs comments add— 添加评论gog docs comments get— 按 ID 获取评论gog docs comments list— 列出文档评论gog docs comments locate— 将评论引用解析为 Docs API 索引区间gog docs comments reply— 回复评论gog docs comments resolve— 将评论标记为已解决gog docs comments reopen— 重新打开已解决的评论gog docs comments delete— 删除评论
轮询命令与gog drive changes poll(Drive 变更轮询)共享同一套轮询基础设施,两篇文章可对照阅读 docs/polling.md。
命令级参数详解
命令自身定义了 6 个业务参数(源码见 internal/cmd/docs_comments_poll.go),其余为 gogcli 全局通用参数。
业务专属参数
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
docId(位置参数) | string | 必填 | Google Doc ID 或 URL |
--state-file | string | 必填 | 存储评论时间水印的 JSON 文件 |
--interval | time.Duration | 60s | 两次轮询之间的延迟 |
--include-resolved(别名--resolved) | bool | false | 是否包含已解决的评论 |
--on-new | string | 空 | 每条评论触发的本地 Shell 命令,事件 JSON 通过 stdin 传入 |
--max-iterations | int | 0 | 轮询 N 次后停止;0 表示持续运行直到被中断 |
--max(别名--limit) | int64 | 100 | 每页 API 最多获取的评论数 |
参数校验规则(源码约束)
从 internal/cmd/docs_comments_poll.go 可以看出以下硬性校验:
docId为空直接报错empty docId;--interval必须大于 0,否则报--interval must be greater than zero;--max-iterations必须>= 0;--max必须大于 0。
此外--state-file缺失时会报missing --state-file,路径会经过config.ExpandPath展开(支持~等),见 internal/cmd/poll_helpers.go。
全局通用参数
以下参数对所有 gogcli 命令生效,轮询场景中尤其常用:
| Flag | Type | Default | Help |
|---|---|---|---|
--access-token | string | 直接使用提供的访问令牌(绕过存储的刷新令牌;令牌约 1 小时过期) | |
-a--account--acct | string | 认证 Google API 命令使用的账户邮箱、别名或 auto | |
--client | string | OAuth 客户端名(选择存储的凭证和令牌桶) | |
--color | string | auto | 颜色输出:auto|always|never |
-n--dry-run--dryrun--noop--preview | bool | 不做任何更改;打印预期操作并成功退出 | |
-y--force--assume-yes--yes | bool | 跳过破坏性命令的确认提示 | |
--home | string | 覆盖 gogcli 的 config/data/state/cache 根目录(等价于GOG_HOME) | |
-j--json--machine | bool | false | 向 stdout 输出 JSON(最适合脚本化) |
--no-input--non-interactive--noninteractive | bool | 永不提示;失败即报错(适合 CI) | |
-p--plain--tsv | bool | false | 向 stdout 输出稳定、可解析的 TSV 文本 |
--quota-project | string | 用于结算 API 用量的 Google Cloud 项目(作为 X-Goog-User-Project 发送) | |
--readonly | bool | false | 运行时阻止变更类 API 请求;auth add 也请求只读 OAuth 范围 |
--results-only | bool | JSON 模式下只输出主结果(丢弃 nextPageToken 等信封字段) | |
--select--pick--project | string | JSON 模式下选择逗号分隔的字段(支持点路径) | |
-v--verbose | bool | 启用详细日志 | |
--version | kong.VersionFlag | 打印版本并退出 | |
--wrap-untrusted | bool | false | JSON/raw 输出中将获取的文本字段包裹在外部不可信内容标记中 |
快速上手:最小可用示例
先进行一次最简单的轮询,把评论事件以 JSON 形式输出:
gog docs comments poll <docId> \ --state-file ~/.local/state/gog/doc-comments.json \ --json命令启动后会立即轮询一次,然后按--interval(默认 60 秒)周期性重复。首次运行时,状态文件不存在,命令会以当前 UTC 时间为初始 watermark 创建状态文件,因此不会重放历史评论(见 docs/polling.md)。
其他高频场景:
# 每 30 秒轮询一次 gog docs comments poll <docId> \ --state-file ~/.local/state/gog/doc-comments.json \ --interval 30s \ --json # 有界运行:只轮询 3 次后退出(适合 CI 与测试) gog docs comments poll <docId> \ --state-file comments.json \ --max-iterations 3 \ --json # 同时包含已解决的评论 gog docs comments poll <docId> \ --state-file comments.json \ --include-resolved \ --json终止方式:SIGINT(Ctrl+C)与SIGTERM都会优雅停止轮询器,已完成迭代的状态已经持久化,重启后可无缝续跑(源码通过signal.NotifyContext(ctx, os.Interrupt, syscall.SIGTERM)实现,见 internal/cmd/poll_helpers.go)。
状态文件机制:水印 + 已见 ID
这是本命令最核心的机制。状态文件是一个本地 JSON 文件,其结构定义在源码中(见 internal/cmd/docs_comments_poll.go):
{ "version": 1, "doc_id": "1abc...", "watermark": "2026-06-11T10:00:01Z", "seen_ids": ["c1", "c2"], "include_resolved": false, "updated_at": "2026-06-11T10:00:01Z" }字段含义:
| 字段 | 说明 |
|---|---|
version | 状态文件格式版本(当前为 1,常量pollStateVersion) |
doc_id | 绑定的文档 ID,防止状态文件被错误复用到其他文档 |
watermark | 已处理到的评论modifiedTime时间戳(RFC3339Nano) |
seen_ids | 在水印时间戳上已交付过的评论 ID 列表 |
include_resolved | 生成状态时使用的--include-resolved设置 |
updated_at | 状态文件最近更新时间 |
为什么需要 seen_ids?
Drive Comments API 的时间过滤是闭区间(inclusive)语义:startModifiedTime会包含该时间点上修改的评论。如果多条评论共享同一个modifiedTime,仅靠单一 watermark 无法区分"已交付"与"未交付"。因此状态同时记录:
- 最新时间戳(watermark);
- 在该时间戳上已经交付过的评论 ID(seen_ids)。
这样,同一时间点的"新同伴"仍会交付一次,而不会把 watermark 推进过尚未见到的评论(见 docs/polling.md 的 "State" 一节)。
状态文件的持久化约束
从 internal/cmd/poll_helpers.go 可以看到状态写入的关键行为:
- 写入使用原子写(
config.WriteFileAtomic),避免半写状态; - 文件权限为
0600,只允许所有者读写(测试TestDriveChangesPollPersistsFilteredBatch中对 0600 权限有明确断言); - 目录不存在时自动以
0700创建; - 状态文件为空或不存在时视为全新开始,不重放历史;
- 只有所有输出与 Hook 都成功之后才推进状态;输出或 Hook 失败会返回错误并保留上一轮游标,下一轮会重试该事件——因此消费者必须容忍重复交付(见 docs/polling.md)。
状态文件绑定与冲突防护
- 状态文件绑定到文档 ID:若状态中的
doc_id与命令行传入的docId不匹配,命令直接报错(poll state doc_id ... does not match docId ...); - 状态文件也绑定
--include-resolved设置:两者不一致时报错; - Drive 评论轮询的状态按文档与
--include-resolved设置隔离;同一个状态文件只允许一个轮询器使用,并发写入会互相覆盖游标; - 想重新开始一个新的评论流,删除状态文件或换一个新路径即可。
输出格式:TSV 与 NDJSON
轮询器的输出行为由全局输出参数控制。
默认/TSV 模式(--plain/--tsv)
每条新评论输出一行制表符分隔文本(格式见 internal/cmd/docs_comments_poll.go):
comment <commentId> <作者显示名> <评论内容> <修改时间RFC3339Nano> <是否已解决:true/false>字段依次为:comment(固定标记)、评论 ID、作者显示名、评论内容、修改时间(RFC3339Nano)、是否已解决。
JSON 模式(--json/--machine)
stdout 输出换行分隔的 JSON(NDJSON):每条评论一个对象,结构为:
{"kind":"docs_comment","docId":"1abc...","comment":{ ...drive.Comment 完整对象... }}字段定义见源码中的docsCommentPollEvent(internal/cmd/docs_comments_poll.go):
kind:固定为"docs_comment";docId:文档 ID;comment:Google Drive API 的完整Comment对象(包含 id、content、author、createdTime、modifiedTime、resolved、quotedFileContent、anchor 等字段)。
空轮询不产生任何 stdout。没有新评论时,命令只是静默等待下一个周期。测试TestDocsCommentsPollPersistsWatermarkAndSeenIDs对 NDJSON 输出的每行都做了json.Valid校验(见 internal/cmd/poll_test.go)。
Shell Hook:用--on-new对接自动化
--on-new是本命令的自动化核心:为每一条新评论运行一个本地 Shell 命令,事件 JSON 通过标准输入(stdin)传入。
gog docs comments poll <docId> \ --state-file comments.json \ --on-new './handle-comment'Hook 的安全模型
- Hook 是显式信任的本地命令,Google 提供的内容绝不会被插值进命令字符串——事件 JSON 只通过 stdin 传递,因此即使评论内容包含恶意 Shell 片段也无法注入命令;
- Hook 通过平台 Shell 执行(Linux/macOS 为
/bin/sh -c,Windows 为cmd.exe /D /S /C),没有沙箱,只应使用固定的、操作者控制的命令,绝不能根据 Google 内容动态拼接命令(见 docs/polling.md 与 internal/cmd/poll_helpers.go); - Hook 的 stdout 与 stderr 都重定向到 gog 的 stderr,保证事件 stdout 始终可解析。
事件顺序与失败语义
- Docs 命令按modified-time 升序 + 评论 ID 顺序为每条评论调用一次
--on-new(--on-change是 Drive 命令对应物,按批次调用); - Hook 串行执行;
- 状态只在输出与所有 Hook 全部成功之后才推进。任一 Hook 失败即返回错误并保留上一轮游标,事件在下次运行时重试(源码测试
TestDocsCommentsPollHookFailureRetainsWatermark验证了这一点:Hook 失败后 watermark 保持不变,见 internal/cmd/poll_test.go)。
一个实用的 Hook 示例——把每条新评论追加到本地日志并推送通知:
gog docs comments poll <docId> \ --state-file ~/.local/state/gog/doc-comments.json \ --json \ --on-new './handle-comment'其中handle-comment脚本从 stdin 读取 NDJSON 事件并做业务处理(如通知、归档、转发到聊天工具等)。
源码级运行流程
结合 internal/cmd/docs_comments_poll.go 与 internal/cmd/poll_helpers.go,一次轮询迭代的完整链路如下:
- 注册信号上下文:
pollSignalContext监听SIGINT/SIGTERM,支持优雅退出; - 参数归一化与校验:
normalizeGoogleID归一化 docId;expandPollStatePath展开状态路径;校验--interval、--max-iterations、--max的取值; - 干跑支持:
--dry-run时打印预期操作(文档 ID、状态路径、interval、include_resolved、max_iterations、max、是否配置 Hook)后直接退出,不产生任何 API 调用; - 建立 Drive 服务:
requireDriveService获取已认证的 Drive API 客户端; - 加载或初始化状态:状态文件不存在时以当前 UTC 时间为初始 watermark 创建状态(
Version写入pollStateVersion); - 拉取评论:调用
listDriveComments,使用startModifiedTime = state.Watermark的时间过滤、max分页、all全页拉取、includeResolved: true模式(解决与否在本地二次过滤,确保 watermark 也能越过被排除的已解决评论); - 本地过滤与排序:
filterPolledDriveComments按 watermark + seen_ids 去重,并按时间、评论 ID 稳定排序; - 已解决过滤:未开启
--include-resolved时,filterPolledCommentsByResolved剔除Resolved=true的评论; - 输出与 Hook:逐条写出事件(TSV 或 NDJSON),并调用
--on-newHook; - 推进状态:
advanceDocsCommentsPollState更新 watermark 与 seen_ids,原子写回状态文件; - 循环等待:
--max-iterations > 0且达到次数则正常退出;否则waitForPollInterval睡眠--interval后进入下一轮(可被信号/取消打断)。
从源码结构看,轮询逻辑通过pollRuntime(now/runHook/wait三个可注入函数)与底层解耦,这也是测试能够用假时钟、假 Hook 完整验证状态推进的原因。
测试验证:状态机的可靠性保证
仓库的 internal/cmd/poll_test.go 为评论轮询提供了系统性的回归测试,可以作为理解行为的"活文档":
| 测试 | 验证点 |
|---|---|
TestDocsCommentsPollPersistsWatermarkAndSeenIDs | 首轮拉取后 watermark 推进到最新修改时间,seen_ids 正确记录;NDJSON 每行合法 |
TestDocsCommentsPollSkipsSeenAtInclusiveWatermark | 闭区间语义下跳过已交付 ID,同一时间点的新评论仍交付 |
TestDocsCommentsPollAdvancesPastExcludedResolvedComments | 被排除的已解决评论不输出、不触发 Hook,但 watermark 仍越过它 |
TestDocsCommentsPollHookFailureRetainsWatermark | Hook 失败时 watermark 保持不变,事件可重试 |
TestWaitForPollIntervalCanceled | 等待周期可被取消,支持优雅退出 |
这些测试全部使用内存 HTTP 测试服务(newDriveTestService)模拟 Drive API,无需真实网络与凭据即可运行。
与gog drive changes poll的异同
两者共享同一套轮询框架(状态持久化、原子写、NDJSON 输出、Hook 机制),但关注点不同:
| 维度 | docs comments poll | drive changes poll |
|---|---|---|
| 监听对象 | 单个文档的评论 | Drive 中的文件变更流 |
| 状态核心 | watermark(时间)+ seen_ids | start page token |
| Hook 触发粒度 | 每条评论一次--on-new | 每个非空过滤批次一次--on-change |
| 状态绑定 | 文档 ID +--include-resolved | --drive |
两者的通用规则包括:状态文件0600权限原子写、空轮询不输出、Hook 失败不推进游标、消费者需容忍重复投递。详细对比见 docs/polling.md。
适用场景与最佳实践
典型场景:
- 评论流式消费:把 Google Docs 上的评审意见实时转发到聊天工具、工单系统或日志;
- 定时巡检:与
cron或 systemd timer 配合,用--max-iterations 1做单次快照,或长时间驻留做连续监听; - 自动化审阅工作流:配合
gog docs comments resolve、gog docs comments reply实现"新评论 → 处理 → 回复/标记解决"的闭环。
最佳实践:
- 每个状态文件只运行一个轮询器,避免并发覆盖游标;
- 把状态文件放在持久目录(如
~/.local/state/gog/),让重启后无缝续跑; - 如需重新开始监听,删除状态文件或换新路径;
- Hook 使用固定命令,绝不使用 Google 内容拼接命令字符串;
- 业务消费者设计为幂等,容忍重复交付;
- 批量处理、夜间作业可用
--max-iterations N做有界运行,避免进程无限驻留。
相关命令文档:gog docs comments、命令索引;通用轮询机制见 docs/polling.md;更多用法示例见 docs/examples.md。
【免费下载链接】gogcliGoogle Workspace in your terminal.项目地址: https://gitcode.com/GitHub_Trending/gogcl/gogcli
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考