copilot-pr-autopilot 实战:用 GraphQL 列出并分类 PR 全部未解决审查线程(Step 3)
【免费下载链接】awesome-copilotCommunity-contributed instructions, agents, skills, and configurations to help you make the most of GitHub Copilot.项目地址: https://gitcode.com/GitHub_Trending/aw/awesome-copilot
本文以 awesome-copilot 仓库中的copilot-pr-autopilot技能为背景,深入讲解其审查循环的第 3 步:如何通过ghCLI 与 GitHub GraphQL API 一次性拉取某个 Pull Request 上所有审查者(Copilot、人类、github-advanced-security及其他机器人)留下的未解决线程,并对每个线程的作者进行分类。读完本文,你将掌握03-list-open-threads.ps1的输入输出契约、Path字段的解析规则、author_class的判定逻辑,以及这些数据如何驱动后续的分诊(triage)、修复、回复与收敛判定。
在自动审查循环中的位置
copilot-pr-autopilot的完整循环是一条 1 → 9 步的闭环:请求审查 → 等待审查 →列出线程→ 分诊 → 修复 → 构建测试 → 提交推送 → 回复/解决 → 收敛校验,未收敛则回到第 1 步,直至第 9 步返回Converged: true才执行一次性清理并结束。完整流程见 skills/copilot-pr-autopilot/SKILL.md,循环编排见 skills/copilot-pr-autopilot/references/orchestration.md。
第 3 步是循环的数据入口:它把所有"待处理事项"从 GitHub 拉下来并标准化,第 4 步的分诊(triage)完全依赖这份分类表。按编排约定,本步由explore类型的子代理执行,预算 5 分钟(默认 5 分钟/次子代理调用,见 orchestration.md 中的时间盒协议)。
输入与返回契约
本步的输入只有一个必填项:
| 输入 | 说明 |
|---|---|
PrNumber | 目标 Pull Request 的编号 |
输出是一张表格,每个未解决线程一行,列为:
{ thread_id, file, line, author, author_class, severity, summary }其中各字段含义:
| 字段 | 含义 |
|---|---|
thread_id | 线程在 GitHub 上的唯一 ID(GraphQL 节点id),第 8 步回复/解决时要用它 |
file | 线程锚定的文件路径 |
line | 线程锚定的行号;文件级/PR 级评论没有行锚点 |
author | 发起评论的作者login(原始值,可能带[bot]后缀) |
author_class | 作者分类,取值copilot或human-or-bot,由原始author.login推导 |
severity | 严重程度(供分诊参考,第 4 步会结合分诊规则做最终决策) |
summary | 评论内容摘要 |
这张表必须原样传给第 4 步 —— 分诊规则表完全依赖它来区分"哪些线程由循环自己处置、哪些默认升级给用户"。
核心命令:一行脚本拉取全部未解决线程
运行本步的唯一入口是仓库中打包好的 PowerShell 脚本:
pwsh ./scripts/03-list-open-threads.ps1 -PrNumber <n>脚本路径为 skills/copilot-pr-autopilot/scripts/03-list-open-threads.ps1。该脚本返回所有审查者(Copilot、人类、github-advanced-security、其他机器人)的未解决线程,而不是只返回 Copilot 的 —— 这正是本步的设计要点:循环不仅要处理 Copilot 的评论,还要能识别出哪些线程是"人类/其他机器人"的,从而在分诊阶段默认将其升级给用户处理。
脚本还支持几个可选参数:
| 参数 | 默认值 | 说明 |
|---|---|---|
-Owner/-Repo | 从gh repo view自动解析 | 指定仓库坐标;两者必须同时传或不传,只传一个会被拒绝 |
-PrNumber | 必填 | PR 编号 |
-MaxBodyLength | 500 | 截断Body列到该字符数,传0关闭;超长正文会以…结尾 |
输出格式
脚本以单行压缩 JSON输出(不是 PowerShell 的格式化表格),便于直接管道给ConvertFrom-Json或jq:
{"PrNumber":122,"Owner":"octo","Repo":"demo","OpenThreadCount":3,"Threads":[ {"ThreadId":"PRRT_xxx","Author":"copilot-pull-request-reviewer[bot]","Path":"src/foo.js:42","CreatedAt":"2026-09-11T12:00:00Z","Body":"...summary..."}, ... ]}Path 字段的解析规则(文档明确要求的调用方约定)
脚本在输出线程时,会把文件与行号合并成Path:
- 评论锚定在具体行时,
Path形如src/foo.js:42(<file>:<line>); - 没有行锚点(文件级评论、PR 级评论)时,
Path只有<file>,不带:<line>后缀。
调用方在解析时必须只做"最后冒号切分 + 整数校验":仅在:后缀能被解析为整数时才拆出line,否则把整个Path当作文件路径处理。这避免了把文件名中含:的情况(如 Windows 路径或带行号风格的文件名)误判为行号。
作者字段的退化处理
脚本对author字段做了兜底:若评论的author缺失或login为空(如已删除账号),统一输出(deleted),保证下游解析不会因缺字段而崩溃。
author_class 分类规则
拿到每行线程后,需要把原始author.login映射为两个类别之一:
原始author.login | author_class |
|---|---|
copilot-pull-request-reviewer | copilot |
copilot-pull-request-reviewer[bot] | copilot |
其他任何值(人类、github-advanced-security、其他机器人) | human-or-bot |
文档强调一个关键陷阱:[bot]后缀只出现在部分接口表面。GitHub GraphQL 在通过requestedReviewer.login引用时返回copilot-pull-request-reviewer,而在通过 review 的author.login引用时可能返回copilot-pull-request-reviewer[bot]—— 二者是同一个参与者,必须同时匹配两种形式。
这个规则在仓库中是被集中定义的,而非散落在各脚本里。scripts/_lib.ps1 用一条规范化正则实现了它:
Set-Variable -Name 'CopilotPrAutopilot_CopilotReviewerLoginRegex' ` -Value '(?i)^copilot-pull-request-reviewer(\[bot\])?$' ` -Option ReadOnly -Force -Scope Script(?i)忽略大小写,(\[bot\])?让[bot]后缀成为可选,从而一次匹配两种表面。集中定义的意义在于:第 1 步(触发)、第 2 步(状态快照)和第 10 步(清理)的脚本都依赖这条正则来识别 Copilot 审查者,如果登录名将来发生变化,只需改这一处。为了兼容早期调用方,_lib.ps1还保留了不带前缀的别名$CopilotReviewerLoginRegex。
分类完成后的表格交给第 4 步,分诊规则按author_class决定处置权(详见 skills/copilot-pr-autopilot/references/04-triage.md):
copilot线程 → 循环自主处置,按 ROI vs Risk 规则决定 fix / decline / escalate;human-or-bot(人类审查者、github-advanced-security等)→默认escalate-to-user,除非用户明确把这些线程纳入循环范围。本步只负责标记,真正的策略由分诊应用。
源码级实现:GraphQL 查询与分页
03-list-open-threads.ps1的实现可以从几个角度拆解。
查询结构
脚本使用一条 GraphQL 查询,按 100 条一批分页遍历reviewThreads:
query($owner: String!, $repo: String!, $pr: Int!, $after: String) { repository(owner: $owner, name: $repo) { pullRequest(number: $pr) { reviewThreads(first: 100, after: $after) { pageInfo { endCursor hasNextPage } nodes { id isResolved comments(first: 1) { nodes { author { login } body path line createdAt } } } } } } }几个关键设计点:
comments(first: 1)只取每个线程的"发起评论"。path、line、body、author都来自这条发起评论;同一线程的回复链故意不在此处展开—— 脚本的定位是"分诊的输入",不是"对话历史阅读器"。isResolved过滤:do...while循环先把所有页拉完(pageInfo.hasNextPage驱动,endCursor翻页),再在内存里用Where-Object { -not $_.isResolved }过滤出未解决线程,即"未解决"状态是唯一的事实来源。- 畸形线程跳过而非崩溃:若某个线程没有发起评论(
comments.nodes.Count -eq 0),脚本continue跳过而不是抛异常,保证单个坏数据不影响整批结果。
数据规范化
输出前脚本做了三层清洗:
- 正文换行折叠:
body中的 CR/LF 被替换为空格,避免多行评论破坏 JSON/表格结构; - 正文截断:超过
MaxBodyLength(默认 500 字符)的部分用…截断 —— 长 Copilot 评论会占据 stdout 并拖慢分诊; - 时间戳统一:
createdAt通过_lib.ps1的Format-IsoUtcString规范化为 ISO-8601 UTC 字符串(yyyy-MM-ddTHH:mm:ssZ)。这是因为ConvertFrom-Json会把 ISO 时间自动反序列化成[datetime],其默认.ToString()依赖区域设置、不可往返,统一格式化后 01/02/03 各步骤输出的时间契约完全一致。
底层基础设施:_lib.ps1的共享函数
脚本在开头执行. "$PSScriptRoot/_lib.ps1"(dot-source),这会立刻触发Assert-GhReady前置检查:若gh不存在或gh auth status失败,脚本在做任何工作之前就以一条可操作的错误信息终止(提示安装命令和gh auth login),调用方应将这条消息原样转达给用户并停止循环,不要重试或绕过。此外还依赖两个关键 helper:
Invoke-GhGraphQL:包装gh api graphql,同时检查进程退出码和响应体里的errors数组,把 GraphQL 错误(含type、path、extensions.code)聚合成可读信息,避免出现裸的Unexpected character encountered;Resolve-RepoCoords:未显式传-Owner/-Repo时用gh repo view自动解析本地仓库坐标。
跨版本 PowerShell 兼容性
_lib.ps1专门处理了一个 Windows PowerShell 5.1 的原生命令参数传递 bug:含内嵌双引号的参数在 5.1 下会被错误拆分。因此Invoke-Gh会把含"的-f/-F参数重写为-F field=@<tempfile>(内容先写入临时文件,gh从文件读取),同时用-F而非-f,因为gh的-f不会展开@前缀。整个脚本族在 PowerShell 5.1+ 与 7+ 下行为一致(SKILL.md 声明两种运行时均已测试)。
下游消费:这张表如何驱动整个循环
第 3 步的产出在第 4 步、第 5 步、第 8 步被依次消费:
- 第 4 步分诊:按
{ thread_id, action, rationale }返回决策,action ∈ fix | decline | escalate-to-user。规则优先级:审查者类型策略 → ROI vs Risk → fix/decline 规则清单 → 项目特定策略钩子 → 冲突评论解决(防振荡)→ 升级规则。其中"振荡硬停"不可协商:如果代理准备重做自己在之前某轮已经回退的编辑,必须立即升级给用户(见 04-triage.md)。 - 第 5 步修复:只消费
fix行,派发并行修复子代理(最多 5 个并行、每个 5 分钟)。 - 第 8 步回复/解决:消费完整表。
fix/decline行回复并解决;escalate-to-user行回复但加-NoResolve保持线程打开(08-reply-resolve.md),这是给人类合并负责人留的显式交接,因此第 9 步的收敛可以在OpenThreadCount > 0时依然成功。
Gotchas:三类必须记住的坑
文档在 Gotchas 中强调三个易错点:
[bot]后缀出现在部分接口表面—— 匹配copilot-pull-request-reviewer和copilot-pull-request-reviewer[bot]两种形式,二者是同一个参与者。仓库中用_lib.ps1里的正则一次性解决,但手工核对日志/输出时仍要注意。- 人类 / advanced-security 线程默认
escalate-to-user—— 本步的分类只是标记,第 4 步才应用策略。自动回复或自动解决人类审查线程会掩盖未处理的问题,这在社交层面是错误的。 - 未解决状态是唯一事实来源—— "已过时但未解决"的线程依然会出现,这是正确行为,不要过滤掉它们;它们和其他未解决线程一样,在第 8 步被正常回复与解决(第 10 步的
10-cleanup-outdated.ps1只是最终兜底,不是主机制)。
实际运行注意事项
- 运行前提是
ghCLI 已安装并完成认证(gh auth login),脚本会在 dot-source_lib.ps1时自动校验;需要Triage/Write权限才能触发 Copilot 审查,但列出线程不需要—— 任何能读取 PR 的认证用户都可以运行本步。 - 若循环处于单次迭代模式(Copilot Code Review 未在仓库/账号启用,
01-request-review.ps1触发失败后的降级路径),本步依然照常运行:它列出的是已存在的全部审查线程(人类、advanced-security 等),随后按步骤 3→8 跑一遍,收敛布尔在单次迭代下坍缩为OpenThreadsAwaitingReply == 0。详见 orchestration.md 的单次迭代回退。 - 遇到 API 行为异常时,先查阅 skills/copilot-pr-autopilot/references/api-quirks.md —— 其中记录了已验证的
requestReviewsByLogin触发路径、latestReviews陈旧缓存陷阱(应改用reviews(last:100))、以及三个 GraphQL 易错点(requestReviewsByLoginvsrequestReviews、botLoginsvsuserLogins、App slugcopilot-pull-request-reviewervs 显示登录名Copilot)。
小结
第 3 步是 copilot-pr-autopilot 循环的"眼睛":用一条分页 GraphQL 查询把 PR 上所有审查者的未解决线程拉到本地,规范化为单行 JSON,附上author_class分类,再交给分诊步骤决定每个线程的命运。它的核心工程决策 —— 未解决状态即事实来源、只取发起评论、路径行号只在可解析为整数时切分、Copilot 登录名双形式匹配、前置gh就绪检查、PS 5.1/7 双运行时兼容 —— 保证了循环可以在任意启用了 Copilot Code Review 的仓库上稳定、可审计地运转。想继续深入,可以从 scripts/03-list-open-threads.ps1 的源码、scripts/_lib.ps1 的共享基础设施,以及 references/04-triage.md 的分诊规则表开始读起。
【免费下载链接】awesome-copilotCommunity-contributed instructions, agents, skills, and configurations to help you make the most of GitHub Copilot.项目地址: https://gitcode.com/GitHub_Trending/aw/awesome-copilot
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考