上一篇把命令路由从 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 结果里的 AuthorName 是 FxRayHughes。
这就说明了问题:author 参数是精确匹配 git commit 里的 author 字段,不是模糊匹配 GitLab 用户名。Ray_Hughes 是 GitLab 用户名,FxRayHughes 是这个人 git config 里配的 user.name,两个完全不同的字符串。
字面匹配是死路
最直觉的兜底方向是做字符串处理——去掉下划线横线再做包含比较,rayhughes 是 fxrayhughes 的子串,能命中。
但这条路打一开始就是错的。
字面匹配依赖的是"用户名和 git name 有一定相关性"这个脆弱假设。换一个人可能叫 zhangsan,git name 叫 张三工作账号,完全没有任何子串关系。而且这个函数要在飞书群里对任意成员使用,不可能为每个人维护一份映射表。
正确的方向是反过来走:通过 GitLab 自己知道的身份信息来匹配,不依赖字面字符串。
用 Events API 定位用户的推送行为
GitLab 有一个 User Contribution Events 接口 /users/:id/events,它返回该用户的所有活动——包括每次 push 的分支、commit_from、commit_to。这个接口通过 userID 查询,完全不依赖字符串匹配。
流程变成:
/users?search=Ray_Hughes→ 拿到userID=11ListUserContributionEvents(userID=11, action=pushed, after, before)→ 拿到这个用户在这段时间内的所有 push 事件每个 push 事件带着
commit_from和commit_to,用 Compare API 拿这次 push 包含的所有 commits从第一条 commit 里捞到用户真实使用的 email(
bushuaichen@yeah.net)用这个 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 也可以是分支——通过 GitLab API 自动路由。数字且能查到 issue 就走 issue 路径,否则当分支处理。
这一步的关键是
字符串匹配本质上是在猜,猜的前提条件越多越容易错。把匹配工作交给知道答案的一方——GitLab 知道 userID=11 push 了哪些 commit,直接用这个事实,不需要把用户名、中文名、git name、email 前缀都列出来挨个比。身份信息越绕,就越该往 API 里找更直接的路。

参与讨论
(Participate in the discussion)
参与讨论