gog contacts dedupe 深度解析:gogcli 中 Google 通讯录重复联系人发现与合并的实现机制
【免费下载链接】gogcliGoogle Workspace in your terminal.项目地址: https://gitcode.com/GitHub_Trending/gogcl/gogcli
gogcli 的gog contacts dedupe子命令用于在终端中排查并合并 Google 通讯录(Google Contacts)里的疑似重复联系人。本文基于当前仓库的命令参考页 gog-contacts-dedupe.md 与实现文档 contacts-dedupe.md 展开,覆盖完整的命令用法、参数语义、JSON 输出结构与合并流程,并结合 internal/cmd/contacts_dedupe.go 和 internal/cmd/contacts_dedupe_apply.go 的源码,说明去重分组算法、主联系人选择策略与删除前的 etag 二次校验等安全机制。读完后你可以安全地把该命令用于人工审查与 CI 自动化两条路径。
命令定位与默认行为
gog contacts dedupe的核心能力是“找出疑似重复的个人联系人,可选地执行合并”。其使用形态为:
gog contacts (contact) dedupe [flags]它属于 gog contacts 命令族(命令索引见 docs/commands/README.md)。行为上有一个关键默认:预览(preview)是默认模式——不带--apply时,命令只输出重复分组与合并计划,不触碰任何联系人;显式加上--apply后才会执行“合并字段 + 删除冗余联系人”的变更操作。
从源码 ContactsDedupeCmd 的结构体定义可以看到该命令的四个专属参数:
| 参数 | 类型 | 默认值 | 含义 |
|---|---|---|---|
--match | string | email,phone | 参与匹配的字段:email、phone、name |
--max(别名--limit) | int64 | 0 | 最多扫描多少个联系人,0表示不限 |
--resource | []string | 无 | 将去重范围限定到精确的联系人资源名(people/...),可重复指定 |
--apply(别名--merge) | bool | false | 执行合并并删除冗余联系人,需要确认 |
--fail-empty(别名--non-empty、--require-results) | bool | false | 未发现重复时以退出码 3 退出 |
此外它还继承了 gog 的全局参数体系(多账号、输出格式、确认与只读控制等),完整参数表见下文“完整参数表”一节。
基本用法:预览重复分组
三种最常用的预览形态(继承自 docs/contacts-dedupe.md 的 Basic Use 章节):
gog contacts dedupe gog contacts dedupe --json gog contacts dedupe --max 500 --jsongog contacts dedupe:以表格形式输出重复分组,适合人工快速浏览。- 追加
--json:输出结构化 JSON,便于脚本和 Agent 消费。 - 追加
--max 500:限制扫描规模。--max 0仍会扫描所有不同的分页;正数则达到该联系人数量即停止。
匹配字段与归一化规则
默认匹配使用归一化后的邮箱与电话:
gog contacts dedupe --match email,phone姓名的匹配是可选开启的,因为可能产生误报:
gog contacts dedupe --match email,phone,name源码 parseContactsDedupeMatch 会忽略大小写与空格解析--match,且要求至少启用一个字段,否则返回 usage 错误。三种键的归一化函数决定了“什么算同一个值”:
- 邮箱(normalizeContactEmail):
TrimSpace+ 转小写。因此ADA@example.com与ada@example.com会被判为同一键。 - 电话(normalizeContactPhone):只保留数字字符。因此
+1 (555) 0100与15550100匹配。 - 姓名(normalizeContactName):转小写、压缩内部空白。
" ada lovelace "与"Ada Lovelace"会被判为同键——这也是官方文档提示姓名匹配会产生误报的原因。
单元测试 TestBuildContactsDedupeGroupsTransitive 印证了上述规则:邮箱大小写差异与电话格式差异都能把三条联系人连成一组,且matched_on输出为排序后的键列表(如email:ada@example.com、phone:15550100)。
分组算法:基于并查集的传递性去重
“疑似重复”的判定并非两两比较,而是 buildContactsDedupeGroups 中的并查集(union-find):
- 为每个联系人计算所有匹配键(
email:/phone:/name:前缀 + 归一化值); - 相同键的联系人互相
union,因此重复关系是传递性的:A 与 B 共享邮箱、B 与 C 共享电话,则 A、B、C 会归入同一组; - 只保留成员数 ≥ 2 的组,并记录“组内至少出现两次”的键作为
matched_on(见 groupKeys 过滤逻辑); - 组按主联系人资源名字典序排序,输出稳定。
每个组会选出primary(合并时保留的联系人)。chooseContactsDedupePrimary 按 contactsDedupeScore 打分:有主名称 +2,每有一个邮箱/电话 +2,有组织 +1,有网址 +1;分数相同时资源名更小的优先。从测试用例看,信息更完整(邮箱 + 电话)的people/2会胜过只有邮箱的people/1成为 primary。
数据获取路径:分页、范围限定与死循环防护
扫描联系人有两条路径,由是否传入--resource决定(Run 中的分支):
全量分页扫描(contactsDedupeList):
- 调用
People.Connections.List,仅读取READ_SOURCE_TYPE_CONTACT来源(即个人通讯录数据); - 每页请求 500 条;当
--max为正数时,若剩余配额小于 500 会自动缩小pageSize,并在累计达到--max时立即返回; - 每页请求前都经过 pageTokenGuard.check:若出现重复的 page token,命令会报
pagination loop: repeated page token并立即中止扫描。这是文档中“Repeated page tokens stop the scan before a plan or contact update”的实现依据,TestContactsDedupeExecuteRejectsRepeatedPageToken 验证了服务返回卡死 token 时命令在第二次列表调用后即报错、且无任何部分输出。
精确资源扫描(contactsDedupeGetResources):
--resource必须是people/...形式,normalizeContactsDedupeResources 会做前缀校验、去空格并按首次出现顺序去重;- 逐个调用
People.Get获取,天然跳过分页——这也是为什么 Run 中--max与--resource不可同时使用(直接返回 usage 错误)。
两条路径都只读取contactsReadMask中列出的字段(names,emailAddresses,phoneNumbers,birthdays,organizations,urls,定义于 contacts_crud.go),即文档所述“只读取 contact-source 数据”。
执行合并:--apply的完整流程
People API 不暴露 Google Contacts 原生的“合并”操作,因此 gogcli 自己实现了“字段并集 + 顺序删除”的两阶段流程(docs/contacts-dedupe.md 的 Safety 章节与 contacts_dedupe_apply.go 一致)。
推荐的操作序列
先审查合并计划:
gog contacts dedupe --json再在不改动联系人的前提下检查精确的变更计划(注意这里同时带--apply和--dry-run:dry-run 会展示--apply将要执行的全部动作,然后退出):
gog contacts dedupe --apply --dry-run --json在自动化场景中,从已审查的预览结果中复制联系人资源名,把 dry-run 与 apply 都限定到同一组精确资源上:
gog contacts dedupe \ --resource people/123 \ --resource people/456 \ --apply \ --dry-run \ --json交互式应用合并(会要求确认):
gog contacts dedupe --apply非交互式自动化必须显式跳过确认(--force/-y/--assume-yes/--yes四者等价):
gog contacts dedupe \ --resource people/123 \ --resource people/456 \ --apply \ --force \ --json源码中的 apply 阶段拆解
--apply路径在 Run 中依次执行三步:
第一步:准备计划(refresh + plan)。prepareContactsDedupeApply 对每个组先调用 refreshContactsDedupeGroup,用完整的 apply 读掩码(可变字段 +coverPhotos,metadata,photos,skills)重新拉取每个成员,并重新分组验证:如果刷新后成员不再构成同一个重复组(比如联系人刚被外部修改),直接报错提示重新预览,而不是继续执行陈旧计划。
第二步:构建单组计划。buildContactsDedupeApplyPlan 包含多重拒斥条件,任何一条触发都会中止该组的合并:
- primary 缺少 contact-source etag 时拒绝(etag 提取见 contactSourceETag,优先取
CONTACT类型来源的 etag); - 任何次级联系人带照片则拒绝——People API 无法通过
updateContact迁移照片(hasContactPhoto); - 次级联系人含
coverPhotos或skills等 API 无法更新的字段时,提示改到 Google Contacts 网页端合并; - 单例字段冲突:
names、birthdays、biographies、genders合并后若出现多于一个值,mergeContactsDedupeSingleton 报conflicting ... cannot be merged safely错误。
通过校验后,mergeContactsDedupeFields 把全部 24 个 API 可更新字段(contactsDedupeMutableFields:addresses、biographies、birthdays、calendarUrls、clientData、emailAddresses、events、externalIds、genders、imClients、interests、locales、locations、memberships、miscKeywords、names、nicknames、occupations、organizations、phoneNumbers、relations、sipAddresses、urls、userDefined)取并集:
- 邮箱与电话按各自的归一化键去重(
mergeContactsDedupeKeyedItems),保证ada@example.com/ADA@example.com只留一条; - 其余字段按 JSON 序列化后整体去重(cleanContactsDedupeItem 会先剔除
metadata与formattedType再比较); - 不可变字段(照片、封面、技能等)在克隆 primary 后被 clearNonMutableContactFields 清空,避免误传给 API;
- 最终
UpdateFields是合并结果中实际非空且可更新的字段列表(populatedContactsDedupeFields 用反射检查各字段切片长度),若为空则拒绝该组。
第三步:执行变更。applyContactsDedupePlans 按组顺序执行:先UpdateContact把并集数据写入 primary,再对每个冗余联系人在删除前立即重新People.Get拉取 metadata 并比对 etag(L397-L415)——如果该联系人在预览之后被修改过,命令中止且不删除该联系人,提示重新运行。这一“更新在前、删除在后、删除前再校验”的顺序保证了:数据已经复制到 primary 的冗余联系人即使删除失败也不会丢失;中途失败时命令停止并报告“已完成几组/删除几条”,已复制的数据留在 primary 上,未删除的联系人保持完整。
确认环节由 dryRunAndConfirmDestructive 统一处理:带--dry-run时只打印计划(含update_fields与delete明细)并成功退出;否则在交互终端要求确认,--force跳过确认,--no-input模式下遇到需要确认的场景会直接失败而非挂起,适合 CI。
JSON 输出结构
预览模式输出(writeContactsDedupe):
| 字段 | 含义 |
|---|---|
scanned | 扫描的联系人总数 |
groups | 疑似重复组列表 |
groups[].primary | 假设执行合并时 gog 将保留的联系人(摘要含resource、name、emails、phones) |
groups[].merged | 合并后的邮箱/电话并集,用于预览 |
groups[].matched_on | 触发该组的重复键(email:.../phone:.../name:...),已排序 |
groups[].members | 组内全部联系人(primary 在前) |
应用模式额外输出(contactsDedupeApplyPayload 与 writeContactsDedupeApplyResult):
| 字段 | 含义 |
|---|---|
applied | 是否真正执行了变更 |
groups_merged | 完成合并的组数 |
contacts_deleted | 删除的冗余联系人数 |
groups[].update_fields | 本次 union 写入 primary 的 People API 字段列表 |
groups[].delete | 数据复制后被删除的冗余联系人列表 |
非 JSON 场景:表格输出重复分组;--plain/-p时应用结果输出applied\ttrue等稳定 TSV 行;普通终端则打印一行Merged N duplicate contact group(s); deleted M redundant contact(s)汇总。未发现重复时打印No duplicate contacts found (scanned N)。
安全边界汇总
contacts dedupe在没有--apply时是完全只读的。启用 apply 后(对照 docs/contacts-dedupe.md 的 Safety 章节与源码逐条对应):
- 需要确认,除非提供
--force; - 遵守
--dry-run(别名--dryrun、--noop、--preview,即-n); - 支持可重复的
--resource范围限定,让自动化只作用于已审查的精确联系人集合; - 只读取 contact-source 数据(
READ_SOURCE_TYPE_CONTACT); - 规划前刷新每个联系人,且刷新后重新验证分组仍成立;
- 先更新选定的 primary,再删除冗余联系人;
- 删除每个冗余联系人前重新校验其 etag;
- 拒斥存在冲突单例字段(
names、birthdays、biographies、genders)的组; - 拒斥带照片或含 People API 无法通过
updateContact保留字段的次级联系人。
另外,全局--readonly标志可以在运行时直接拦截所有变更类 API 请求,为 agent 场景提供额外保险。
定时检查:--fail-empty退出码约定
在计划任务中,如果希望把“没有重复联系人”作为可区分的信号,使用--fail-empty(别名--non-empty、--require-results):
gog contacts dedupe --fail-empty未发现重复时命令以退出码 3 退出。实现见 failEmptyExit:emptyResultsExitCode = 3,未启用该标志时返回 nil(正常退出)。这意味着调度系统可以把 0(有重复/正常预览)与 3(无重复)分别处理。
完整参数表
以下参数表继承自自动生成的命令参考页 docs/commands/gog-contacts-dedupe.md(该页由gog schema --json生成,make docs-commands刷新),其中全局参数适用于所有 gog 命令,此处一并列出以便检索:
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
--access-token | string | 直接使用给定 access token(绕过已存 refresh token;token 约 1 小时过期) | |
-a/--account/--acct | string | 账号邮箱、别名或auto | |
--apply/--merge | bool | 合并重复组并删除冗余联系人(需要确认) | |
--client | string | OAuth 客户端名(选择已存凭据 + token bucket) | |
--color | string | auto | 颜色输出:auto/always/never |
--disable-commands | string | 禁用命令列表,逗号分隔,支持点路径 | |
-n/--dry-run/--dryrun/--noop/--preview | bool | 不做变更,打印将执行的动作后成功退出 | |
--enable-commands | string | 启用命令前缀列表,逗号分隔,支持点路径(收窄 CLI) | |
--enable-commands-exact | string | 精确启用的命令列表;父命令不会启用子命令 | |
--fail-empty/--non-empty/--require-results | bool | 无重复时以退出码 3 退出 | |
-y/--force/--assume-yes/--yes | bool | 跳过破坏性命令的确认 | |
--gmail-no-send | bool | false | 阻止 Gmail 发送操作(agent 安全) |
-h/--help | kong.helpFlag | 显示上下文帮助 | |
--home | string | 覆盖 gogcli 配置/数据/状态/缓存根目录(等价于 GOG_HOME) | |
-j/--json/--machine | bool | false | 向 stdout 输出 JSON(最便于脚本使用) |
--match | string | email,phone | 匹配字段:email,phone,name |
--max/--limit | int64 | 0 | 最多扫描的联系人数量(0 = 全部) |
--no-input/--non-interactive/--noninteractive | bool | 从不交互提问,需要提问时直接失败(CI 友好) | |
-p/--plain/--tsv | bool | false | 输出稳定、可解析的文本(TSV,无颜色) |
--quota-project | string | 用于 API 计费的 GCP 项目(以 X-Goog-User-Project 发送;部分 API 在配合 --access-token 或 ADC 时必需) | |
--readonly | bool | false | 运行时阻止变更类 API 请求;auth add 同时请求只读 OAuth scope |
--resource | []string | 将去重限定到精确联系人资源名(people/...),可重复 | |
--results-only | bool | JSON 模式下只输出主结果(丢弃 envelope 字段如 nextPageToken) | |
--select/--pick/--project | string | JSON 模式下选择逗号分隔的字段(尽力而为,支持点路径) | |
-v/--verbose | bool | 启用详细日志 | |
--version | kong.VersionFlag | 打印版本并退出 | |
--wrap-untrusted | bool | false | JSON/raw 输出中为拉取到的文本字段包裹外部不可信内容标记 |
相关页面
- Raw API Dumps:通过原始 API 通道直接访问 People API 的说明。
- 生成的 contacts 命令页:
gog contacts全部子命令参考。 gog contacts export:导出联系人。gog contacts raw:contacts 原始 API 调用。
实现与测试证据集中在 internal/cmd/contacts_dedupe.go、internal/cmd/contacts_dedupe_apply.go,测试覆盖见 internal/cmd/contacts_dedupe_test.go 与 internal/cmd/contacts_dedupe_apply_test.go;设计文档见 docs/contacts-dedupe.md。
【免费下载链接】gogcliGoogle Workspace in your terminal.项目地址: https://gitcode.com/GitHub_Trending/gogcl/gogcli
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考