上一篇把命令路由从 on_message.lua 搬回 Go 之后,飞书群里的查询命令一直稳定运行。这篇讲一个让我调了半天的问题:按人查提交记录

需求:在群里查某人今天做了什么

场景很直白。产品在飞书群里发一条 查询 @Ray_Hughes 昨日,机器人应该回一张卡片,列出昨天这个人在哪些分支推了哪些 commit,以及这些分支对应的 issue 是什么。

这个需求的难点不在 AI 整理,不在卡片渲染,而在于一个基础问题:怎么准确地从 GitLab 拿到某个人的提交?

最直觉的方案:author 参数过滤

GitLab 的 List Repository Commits API 有一个 author 参数,文档说支持"author name or email"过滤。

想当然写下去:

commits, _, _ := client.Commits.ListCommits(projectPath, &gitlab.ListCommitsOptions{
    Author: gitlab.Ptr("Ray_Hughes"),
    Since:  &sinceTime,
    Until:  &untilTime,
})

结果返回 0 条。

加日志,确认请求到了、项目路径对了、时间窗口对了。然后把 author 换成 卜帅辰(飞书里的中文名),还是 0 条。

拉一页全量 commits 看看,fallback 结果里的 AuthorNameFxRayHughes

这就说明了问题:author 参数是精确匹配 git commit 里的 author 字段,不是模糊匹配 GitLab 用户名Ray_Hughes 是 GitLab 用户名,FxRayHughes 是这个人 git config 里配的 user.name,两个完全不同的字符串。

字面匹配是死路

最直觉的兜底方向是做字符串处理——去掉下划线横线再做包含比较,rayhughesfxrayhughes 的子串,能命中。

但这条路打一开始就是错的。

字面匹配依赖的是"用户名和 git name 有一定相关性"这个脆弱假设。换一个人可能叫 zhangsan,git name 叫 张三工作账号,完全没有任何子串关系。而且这个函数要在飞书群里对任意成员使用,不可能为每个人维护一份映射表。

正确的方向是反过来走:通过 GitLab 自己知道的身份信息来匹配,不依赖字面字符串。

用 Events API 定位用户的推送行为

GitLab 有一个 User Contribution Events 接口 /users/:id/events,它返回该用户的所有活动——包括每次 push 的分支、commit_from、commit_to。这个接口通过 userID 查询,完全不依赖字符串匹配。

流程变成:

  1. /users?search=Ray_Hughes → 拿到 userID=11

  2. ListUserContributionEvents(userID=11, action=pushed, after, before) → 拿到这个用户在这段时间内的所有 push 事件

  3. 每个 push 事件带着 commit_fromcommit_to,用 Compare API 拿这次 push 包含的所有 commits

  4. 从第一条 commit 里捞到用户真实使用的 email(bushuaichen@yeah.net

  5. 用这个 email 再补一遍全量 commits,确保不遗漏

这条路的关键是:GitLab 自己知道哪些 push 行为属于哪个用户,我们不需要猜,直接拿这个事实。

events, _, _ := client.Users.ListUserContributionEvents(userID, &gitlab.ListContributionEventsOptions{
    Action: gitlab.Ptr(gitlab.PushedEventType),
    After:  &afterDate,
    Before: &beforeDate,
})

for _, ev := range events {
    from := ev.PushData.CommitFrom
    to   := ev.PushData.CommitTo
    compareResp, _, _ := client.Repositories.Compare(projectPath, &gitlab.CompareOptions{
        From: gitlab.Ptr(from),
        To:   gitlab.Ptr(to),
    })
    for _, c := range compareResp.Commits {
        commitSet[c.ID] = c
    }
}

token 权限不足时的兜底

实际运行时发现另一个问题:我用的是 GitLab Project Access Token,read_repository 权限,拿不到其他用户的 email 字段(API 返回空)。

这意味着策略 5 的"用 email 补全"在这个 token 下不可用。

兜底方案:当 emailSet 为空时,直接把 events API 里这个用户 push 过的所有 commits 全算作该用户的——因为 events API 已经通过 userID 确认这些 push 属于这个人,没有必要再做 email 二次过滤。

// emailSet 为空说明拿不到 email,直接信任 events
if discoveredEmail != "" {
    emailSet[discoveredEmail] = true
} else {
    // events + compare 结果已经是该用户的,直接用
}

这个判断让查询在 Project Access Token 下也能正确工作,不依赖 Group/Admin token 的额外权限。

带分支的返回结构

原来 git.author_commits 只返回一个 commit 列表,没有分支信息。但需求是按分支分组展示,并且要从分支名里提取 issue 号。

Go 侧改成在 commitSet 里记录每条 commit 来自哪个分支:

type commitEntry struct {
    commit *gitlab.Commit
    branch string
}
commitSet := map[string]*commitEntry{}

返回给 Lua 时每条 entry 多一个 branch 字段,Lua 侧按 branch 分组,再用正则从分支名里提取 issue 号(feature/xxx-338#338),调 GitLab issue 接口拿标题,最后把"分支 + issue 标题 + commits 列表"一起扔给 AI 做分析。

效果是这样的:

📊 Ray_Hughes 昨天动态
共 19 个提交,涉及 3 个分支

#338 磁吸系统重构 (14 commits)
#354 控件统一膨胀偏移 (1 commit)
develop (4 commits)

摘要:
磁吸系统推进至两层判优模型阶段,新增计算特征点(垂足/切点/质心),
完成磁吸触发全局化迁移,工具层不再直接驱动磁吸逻辑...

现在的查询能力

目前支持的完整查询形式:

命令

说明

查询 #338 进度

查 issue 或分支今天的 commits

查询 #338 昨日 查询 #338 前天

按日期

查询 #338 总进度

所有历史 commits

查询 #338 总规划

把 issue 内容 AI 整理成卡片

查询 develop 本月进展

按分支名查

查询 @Ray_Hughes 昨日

按人查,自动路由 userID + events

#338 可以是 issue 也可以是分支——通过 GitLab API 自动路由。数字且能查到 issue 就走 issue 路径,否则当分支处理。

这一步的关键是

字符串匹配本质上是在猜,猜的前提条件越多越容易错。把匹配工作交给知道答案的一方——GitLab 知道 userID=11 push 了哪些 commit,直接用这个事实,不需要把用户名、中文名、git name、email 前缀都列出来挨个比。身份信息越绕,就越该往 API 里找更直接的路。