跳转到内容

PR 对话

关于代码的讨论本来就发生在 Pull request 里。PR 对话让它成为 Control Center 工作区上的一个界面,就像聊天桥接让 Slack 成为界面一样:在 PR 上 @提及服务器的 GitHub App 机器人,在它的审查线程里回复,或者加上 ai-review 标签,工作就会在 PR 的评审空间里进行——归属方式和护栏与在应用里输入的任何内容完全一致。评论还要求作者是工作区成员,标签则不然,因为它没有作者。机器人的回答会回到 GitHub 上,回到问题被提出的那条线程里。

有三项决策塑造了这项功能,而每一项都与聊天桥接为聊天所做的决策相同,只是针对代码托管平台重新表述:连接朝哪个方向建立、从外部到来的评论由什么授权,以及双方分别归到谁名下。

GitHub 为一个 App 提供的唯一推送通道是 webhook,而 webhook 需要一个入站 URL——这恰恰是这个界面刻意从不要求的东西。因此发现过程是一轮扫描:大约每分钟,每个 App 安装都会发起一次针对该别名的搜索,向 GitHub 询问提及了机器人(或其短 slug)的 PR,以及带有审查标签的开放 PR;已经有评审空间的 PR 会被读取其评论线程;所有内容都会按评论 ID 与上一轮扫描看到的结果做差分。

这些设计带来的后果值得点明:

  • 延迟是扫描间隔,而不是事件。 👀 确认会在一分钟内到达。没有什么紧急的事情依赖它。
  • 每条评论恰好处理一次。 评论 ID 在整个 GitHub 上唯一;每条评论在被判定为符合条件的那一刻就会被记录,早于任何针对它的动作。崩溃可能让某条评论的处理丢失,但绝不会让它被重复触发。
  • 离线期间的内容会被补上,而不是重放。 已见集合会持久化,因此重启后会补上停机期间到达的内容。唯一的例外是有史以来的第一轮扫描:没有可做差分的记忆,所有未处理的内容都会被静默记录——在这台服务器存在之前就给二十个 PR 打上标签的运维人员,不该回来面对二十个正在运行的审查。
  • 标签是集合,而不是事件。 搜索报告的是标签的_存在性_,因此被打上标签的 PR 只会触发一次,仅此一次。移除标签后再重新添加,对扫描来说是不可见的;重新运行的途径是 @提及。

搜索对每个安装账户在该账户自己的客户端上各运行一次,因为安装令牌只能看到自己这次安装下的仓库——一份服务器级的统一凭据会悄悄漏掉该 App 其他安装下的每一个 PR。

到来的 PR 评论除了作者登录名之外几乎没有携带别的信息——在得到证实之前,它只是一个未经认证的外部身份。机器人会用工作区成员及其已连接的 GitHub 账户构成的反向索引来解析它,该索引按 TTL 重建,因为服务器运行期间会有人登录,也会有人被邀请加入。

失败即拒绝的表现像一道阶梯,每一级都会在它拒绝提问的那条线程里给出答复:

  • 未知的登录名——未关联,或由并非本工作区成员的人关联——会被告知先登录,并确认自己属于该工作区
  • 只读成员(viewer 或 guest)会被告知自己的角色无法启动工作

这两种未知登录名的情况共用同一个答复:反向索引只会返回_本_工作区的成员,因此未关联的账户和已关联的非成员是同一种未命中。它们都不会继续。评论绝不会归到没有发送它的人名下,陌生人的话也绝不会进入他们看不到的工作区。

ai-review 标签没有作者可供解析,因此它不爬这道阶梯。添加它会在这个已经关联该仓库的工作区里启动审查。

对话的每一方都携带不同的身份,而这种差异正是这项功能的要点:

  • 在 GitHub 上,机器人就是服务器。 桥接发布的每一条评论、每一个表情回应和每一次回复,都使用 App 的安装令牌。没有人在客户端里点击过任何东西,因此没有任何操作借用某个人的账户——这正是让 webhook 和轮询都不依赖个人凭据的同一条规则。
  • 在空间里,提问算在你名下。 消息会归到作者解析出的那位成员名下,智能体运行也代表他们执行:提交会把该成员列为共同作者,空间的自主程度和行动护栏也完全像对待在应用里输入的消息那样生效。运行过程中对代码托管平台的写入仍然以 App 的身份发出——与上一条相同的规则——而不是使用该成员的个人令牌。

把审查发现发布回 GitHub 仍然是人的决定,并且在 Control Center 中做出。@提及可以_启动_一次审查;但只有人才能发布审查。

任何由机器人账户创作的内容都会在任何门禁运行之前被丢弃——包括我们自己的机器人账户,以及所有带 [bot] 后缀的账户,这样其他 App 的审查机器人也无法唤醒我们的机器人。没有这道防护,机器人自己的回答就会作为新评论到来,并永远重新触发它。

[bot] 后缀也是短名存在的理由。GitHub 把 App 账户的登录名固定为 <slug>[bot],不提供它的自动补全,也绝不把 @slug 解析到该 App——因此接受裸 slug 是_我们_的匹配器的决定,而要发现它,需要的是一条原始的评论文本搜索通道,而不是 GitHub 的 @提及索引。这项代价是被如实说明而不是被抹平的:如果一个人类用户的用户名与 App 的 slug 相同,那么他自己每一次短名 @提及都会叫到机器人。足够独特的 App 名称会让这种冲突只停留在假想层面。

  • 不自动发布。 发现会落在评审空间里;发布到 GitHub 是一个单独的人工操作
  • 不做模糊身份匹配。 作者要么精确解析到某一位成员,要么被拒绝——没有猜测式的归属
  • 不使用 GitHub App webhook。 这个界面从不注册 webhook,因此没有需要为 PR 评论暴露的入站 URL。其他产品级 webhook(流水线、工单同步)是另一套可选的设置。