gog contacts dedupe 深度解析:gogcli 中 Google 通讯录重复联系人发现与合并的实现机制
2026/9/17 20:32:33 网站建设 项目流程

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 的结构体定义可以看到该命令的四个专属参数:

参数类型默认值含义
--matchstringemail,phone参与匹配的字段:emailphonename
--max(别名--limitint640最多扫描多少个联系人,0表示不限
--resource[]string将去重范围限定到精确的联系人资源名(people/...),可重复指定
--apply(别名--mergeboolfalse执行合并并删除冗余联系人,需要确认
--fail-empty(别名--non-empty--require-resultsboolfalse未发现重复时以退出码 3 退出

此外它还继承了 gog 的全局参数体系(多账号、输出格式、确认与只读控制等),完整参数表见下文“完整参数表”一节。

基本用法:预览重复分组

三种最常用的预览形态(继承自 docs/contacts-dedupe.md 的 Basic Use 章节):

gog contacts dedupe gog contacts dedupe --json gog contacts dedupe --max 500 --json
  • gog 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.comada@example.com会被判为同一键。
  • 电话(normalizeContactPhone):只保留数字字符。因此+1 (555) 010015550100匹配。
  • 姓名(normalizeContactName):转小写、压缩内部空白。" ada lovelace ""Ada Lovelace"会被判为同键——这也是官方文档提示姓名匹配会产生误报的原因。

单元测试 TestBuildContactsDedupeGroupsTransitive 印证了上述规则:邮箱大小写差异与电话格式差异都能把三条联系人连成一组,且matched_on输出为排序后的键列表(如email:ada@example.comphone:15550100)。

分组算法:基于并查集的传递性去重

“疑似重复”的判定并非两两比较,而是 buildContactsDedupeGroups 中的并查集(union-find)

  1. 为每个联系人计算所有匹配键(email:/phone:/name:前缀 + 归一化值);
  2. 相同键的联系人互相union,因此重复关系是传递性的:A 与 B 共享邮箱、B 与 C 共享电话,则 A、B、C 会归入同一组;
  3. 只保留成员数 ≥ 2 的组,并记录“组内至少出现两次”的键作为matched_on(见 groupKeys 过滤逻辑);
  4. 组按主联系人资源名字典序排序,输出稳定。

每个组会选出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);
  • 次级联系人含coverPhotosskills等 API 无法更新的字段时,提示改到 Google Contacts 网页端合并;
  • 单例字段冲突:namesbirthdaysbiographiesgenders合并后若出现多于一个值,mergeContactsDedupeSingleton 报conflicting ... cannot be merged safely错误。

通过校验后,mergeContactsDedupeFields 把全部 24 个 API 可更新字段(contactsDedupeMutableFields:addressesbiographiesbirthdayscalendarUrlsclientDataemailAddresseseventsexternalIdsgendersimClientsinterestslocaleslocationsmembershipsmiscKeywordsnamesnicknamesoccupationsorganizationsphoneNumbersrelationssipAddressesurlsuserDefined)取并集:

  • 邮箱与电话按各自的归一化键去重(mergeContactsDedupeKeyedItems),保证ada@example.com/ADA@example.com只留一条;
  • 其余字段按 JSON 序列化后整体去重(cleanContactsDedupeItem 会先剔除metadataformattedType再比较);
  • 不可变字段(照片、封面、技能等)在克隆 primary 后被 clearNonMutableContactFields 清空,避免误传给 API;
  • 最终UpdateFields是合并结果中实际非空且可更新的字段列表(populatedContactsDedupeFields 用反射检查各字段切片长度),若为空则拒绝该组。

第三步:执行变更。applyContactsDedupePlans 按组顺序执行:先UpdateContact把并集数据写入 primary,再对每个冗余联系人在删除前立即重新People.Get拉取 metadata 并比对 etag(L397-L415)——如果该联系人在预览之后被修改过,命令中止且不删除该联系人,提示重新运行。这一“更新在前、删除在后、删除前再校验”的顺序保证了:数据已经复制到 primary 的冗余联系人即使删除失败也不会丢失;中途失败时命令停止并报告“已完成几组/删除几条”,已复制的数据留在 primary 上,未删除的联系人保持完整。

确认环节由 dryRunAndConfirmDestructive 统一处理:带--dry-run时只打印计划(含update_fieldsdelete明细)并成功退出;否则在交互终端要求确认,--force跳过确认,--no-input模式下遇到需要确认的场景会直接失败而非挂起,适合 CI。

JSON 输出结构

预览模式输出(writeContactsDedupe):

字段含义
scanned扫描的联系人总数
groups疑似重复组列表
groups[].primary假设执行合并时 gog 将保留的联系人(摘要含resourcenameemailsphones
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;
  • 拒斥存在冲突单例字段(namesbirthdaysbiographiesgenders)的组;
  • 拒斥带照片或含 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-tokenstring直接使用给定 access token(绕过已存 refresh token;token 约 1 小时过期)
-a/--account/--acctstring账号邮箱、别名或auto
--apply/--mergebool合并重复组并删除冗余联系人(需要确认)
--clientstringOAuth 客户端名(选择已存凭据 + token bucket)
--colorstringauto颜色输出:auto/always/never
--disable-commandsstring禁用命令列表,逗号分隔,支持点路径
-n/--dry-run/--dryrun/--noop/--previewbool不做变更,打印将执行的动作后成功退出
--enable-commandsstring启用命令前缀列表,逗号分隔,支持点路径(收窄 CLI)
--enable-commands-exactstring精确启用的命令列表;父命令不会启用子命令
--fail-empty/--non-empty/--require-resultsbool无重复时以退出码 3 退出
-y/--force/--assume-yes/--yesbool跳过破坏性命令的确认
--gmail-no-sendboolfalse阻止 Gmail 发送操作(agent 安全)
-h/--helpkong.helpFlag显示上下文帮助
--homestring覆盖 gogcli 配置/数据/状态/缓存根目录(等价于 GOG_HOME)
-j/--json/--machineboolfalse向 stdout 输出 JSON(最便于脚本使用)
--matchstringemail,phone匹配字段:email,phone,name
--max/--limitint640最多扫描的联系人数量(0 = 全部)
--no-input/--non-interactive/--noninteractivebool从不交互提问,需要提问时直接失败(CI 友好)
-p/--plain/--tsvboolfalse输出稳定、可解析的文本(TSV,无颜色)
--quota-projectstring用于 API 计费的 GCP 项目(以 X-Goog-User-Project 发送;部分 API 在配合 --access-token 或 ADC 时必需)
--readonlyboolfalse运行时阻止变更类 API 请求;auth add 同时请求只读 OAuth scope
--resource[]string将去重限定到精确联系人资源名(people/...),可重复
--results-onlyboolJSON 模式下只输出主结果(丢弃 envelope 字段如 nextPageToken)
--select/--pick/--projectstringJSON 模式下选择逗号分隔的字段(尽力而为,支持点路径)
-v/--verbosebool启用详细日志
--versionkong.VersionFlag打印版本并退出
--wrap-untrustedboolfalseJSON/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),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询