fix(server): 批量关闭的行级命中、在途 PATCH 与超时预算边界(INT-185) #168

Open
fjbobo wants to merge 16 commits from int-185/batch-close-boundary into main
Owner

相关需求

INT-185(看板关闭全部的边界:RowsAffected、在途 PATCH 与超时),来源 PR #162 第 1 轮审查的 7 项边界。

改动内容

批量关闭「关闭全部已完成」在 INT-174 主路径(GitHub 并发关闭、内部单事务、请求可取消)之上补齐边界:

  1. 行级命中判定:BatchCloseInternalTx 把 state=open 写入 WHERE,事务内逐 id UPDATE 按 RowsAffected 区分命中/未命中;只对命中行计成功并删推荐,未命中行(列表与事务之间被并发方改出 open)逐条记入 failures,文案「该问题已在其他操作中被更新,已跳过关闭」。
  2. 超时预算后移:30s 整批截止移到 ListFiltered 成功之后创建,只管关闭阶段;前置权限检查与列表查询本就不收 ctx,不再被整批截止截断,调用方取消照常生效。
  3. 在途 PATCH 与整批取消解耦:worker 在 PATCH 开始前判定整批取消(sem.Acquire 成功不等于 worker 已调度),之后经 context.WithoutCancel + 单请求 15s 上限继续走完并落库,缩小「远端已关、本地仍 open」窗口;取消后不再开始新 PATCH。
  4. 取消文案分两类:未开始=「请求已取消或超时,未继续关闭」;已发出但结果未确认=「关闭结果未确认:GitHub 侧可能已关闭,请刷新查看」。前端识别 ky TimeoutError 提示「请求超时:部分问题可能已关闭,请刷新看板确认结果」,HTTPError 行为不变。
  5. upsertGitHubIssue 入口守卫:nil / GetID()==0 返回可读错误不 panic;创建/更新/同步三个调用方均只按 error 分支,行为安全。
  6. 并发测试去墙钟:改为 onUpdateIssue 钩子 started/release 通道确定性同步,只断言并发峰值与调用次数;t.Cleanup 保证失败路径也放行钩子内的 worker。
  7. 限流识别:UpdateIssue 命中 *RateLimitError / *AbuseRateLimitError / 403/429 且 Message/body 含 rate limit/abuse detection 时统一回报「GitHub API 限流,请稍后重试」,判定口径对齐 PR 域 pullRequestAccessHint 但不共用。

文档同步:docs/internal/issue-list-sql-pushdown.md、docs/internal/issue-recommendations.md(推荐只随命中行删除)。

修改范围

server(repository / service / 测试)、web(看板关闭全部按钮的超时文案)、docs/internal 两份。

本地验证

检查命令 结果
cd web && pnpm api-types --check 通过,无 schema 变更
make lint 通过(gofmt 干净、go vet 干净、eslint 0 warning、tsc 0 error)
make test 通过(server 全包 ok;web 146 文件 1771 用例全过)
  • 验证方式(命令 / 无头浏览器):Go 单测 + go test -race -count=15(BatchClose 子集);Playwright 无头 Chromium 双实例(基线 4412cd13 与本提交,独立端口/独立库)走真实看板关闭流程;真实并发 batch-close API 请求触发未命中路径。

  • 启动命令:go build -o <scratch>/fast_ship ./cmd/server 后以 FAST_SHIP_* 环境变量独立端口启动(verify-fast-ship 技能隔离流程,未占用共享 4888/4999)。

  • 验证提交 SHA:eee1f5fd

  • 应用可以启动(make dev),主要使用流程已验证(server / web 按改动范围)

  • 异常或空状态已验证(如适用)

已修复的基线问题

无本 PR 检查口径内的基线问题。额外发现的 -race 基线测试失败(TestT3Int148SettlementReadOnlyGuard 稳定复现、TestBotBudgetTaskScopeAndLinkedMain 并行偶发,均已在 main 4412cd13 复现,与本改动无关)已立跟进 issue INT-190。

端到端截图

看板「关闭全部」主流程改前/改后对比(同一数据、同一页面):

场景 改动前 改动后
看板「已完成」列(2 条 done) 04-board-done-column.png 04-board-done-column.png
关闭全部确认对话框 05-close-all-dialog.png 05-close-all-dialog.png
关闭成功 toast + 空看板 06-close-all-toast.png 06-close-all-toast.png

本 PR 的界面可见变化集中在失败/超时文案(限流、取消、请求超时),这些路径依赖真实 GitHub 4xx/网络故障,无头环境无法诚实触发,改用真实并发 API 响应作为替代证据:同一项目 4 条 done+open,两个 batch-close 请求同刻并发,后者在事务内全部未命中,failures[].error 即 INT-185 新文案(原始响应见 api-batch-close-race-2.json,已随验证产物留存):

{"total": 4, "succeeded": 0, "failed": 4,
 "failures": [{"reference": "INT-6", "error": "该问题已在其他操作中被更新,已跳过关闭"}, ...]}

文案分类(未开始 / 结果未确认 / 限流)由服务层单测逐条断言覆盖。

未验证范围与原因

  • GitHub 限流(403/429)、PATCH 单请求超时、整批 30s 截止的真实远端行为:需要真实 GitHub 故障注入或 30s+ 等待,无头验证以单测(类型化错误注入 + started/release 确定性同步)与 -race -count=15 代替。
  • GitHub 来源的端到端关闭流程:需要真实仓库集成,本环境未配置;现有 GitHub 域集成测试(fake client)全绿。

API 变更说明

无。openapi 契约零变更(pnpm api-types --check 通过);失败文案是 IssueBatchResult.failures[].error 中的自由字符串。

创建 PR 前完成

  • 本地验证表已填完,每条检查零警告、零错误、零失败
  • 端到端截图已贴入,或已写明无需截图的原因
  • 补充需求编号(如果存在)
  • 变更影响范围已补充到 PR 描述中

审查意见处理

  • 逐条处理所有审查评论:真实问题修复后回复并重新验证;误报或不适用意见回复理由。
  • 推送新提交后按 $pr-review-loop 自动等待审查结果并处理新意见,直到收敛(R16 全部组合通过,0 P0–P2)

复审处置

  • R1 fast-ship-bot P2(failures[].error 未透传进看板 toast):Act on,已修复(3ddaad75)——失败分支追加首条 failure.error,指引随服务端文案自带;补 2 条 UI 单测(透传 + 回退)。详见评论区的 R1 处置说明。
  • codex:首次请求 30 分钟无响应已按规则跳过并重触发一次,等待其结论。
  • R2 fast-ship-bot 2 条 P2(失败原因只取首条掩盖类别;限流后仍继续派发 PATCH):均 Act on,已修复(ad56479a)——前端失败原因去重全展示;服务端首个限流熔断派发、剩余项「未尝试」、在途 PATCH 不受影响。详见两条行内意见的处置回复。
  • R3 fast-ship-bot 1 P2 + 2 Notes(第 4 类文案被截断挤掉;认证类不熔断;网关超时归因):触发方向审计(pi/claude-opus-5-5,定性=开放反例)。按审计路径换模型收敛(fed57239):服务端文案规范化(去 URL,5xx/传输→「结果未确认」)、前端分组计数不截断、网关/网络中断归入刷新确认;认证熔断拆 INT-192。详见两条 R3 处置回复。

交付范围(方向审计后圈定)

本 PR 管:看板「关闭全部」的失败文案指引与归因——服务端失败文案收敛为少量固定类别(不含 URL/Issue 号),前端按文案分组计数全部展示、明细差额显式标注;限流熔断派发;整请求与逐条失败只要远端可能已生效(ky 超时/网关 502/503/504/网络中断/服务端 5xx/传输错误)一律提示刷新确认。

本 PR 不管:认证/权限类失败(401、非限流 403)的派发熔断——属新派发策略,已拆跟进 issue INT-192(404 是单条状态,不熔断)。收敛保证:任意批次中每类失败原因至少以「文案 ×条数(样例引用)」出现一次;未列明细的条数显式标注。后续服务端新增文案类别不再挤占既有类别的展示。

  • R4 fast-ship-bot 3 条 P2(明细截断吞类别;sonner description 换行折叠;2xx 解析失败与 catch 范围误报):均在交付范围内 Act on,已修复(a6527974)——上限后新类别样例保全、description 逐行 ReactNode、catch 收窄 + SyntaxError 归入刷新确认。详见三条行内意见的处置回复。
  • R5 fast-ship-bot 2 条 P2(归因随 goroutine 时序漂移;契约描述未随超上限行为更新):均 Act on,已修复(f36ccecd)——归因当场定死(drain 不再重读全局标志);openapi 描述更新并重生成三份产物。详见两条行内意见的处置回复。
  • R6 fast-ship-bot 2 条 P2(关闭事务未钉 workflow 条件;截断时分组计数低估):均在交付范围内 Act on,已修复(d48f2ea4)——tx WHERE 经子查询钉住 workflow=done;截断时不展示下界计数,只保留类别与样例。详见两条行内意见的处置回复。
  • R7 fast-ship-bot 2 条 P2(GitHub 分支无行级复核;共享契约描述与 internal-meta 实现不符):均 Act on(6e2731ee)——closeOneGitHubIssue 发 PATCH 前经 IsOpenAndDone 复核;抽出 appendBatchFailureEntry 让两个端点共用截断语义。详见行内回复。
  • R8 fast-ship-bot 2 条 P2(workflow 子查询全表扫描;守卫无 ctx 且不再复核取消):均 Act on(ab3ebfce)——改关联 EXISTS 走 issue_id 主键;守卫带 ctx 并通过后再判 batchCtx.Err()。详见行内回复。
  • R9 fast-ship-bot 2 条 P2(守卫取消被误报为内部故障;5xx 带 JSON 绕过刷新提示):均 Act on(edd78bbb)——守卫出错先判 ctx err 按熔断状态归因;前端先按 status>=500 走刷新确认再处理 4xx message。详见行内回复。
  • R10 fast-ship-bot 1 P2 + 1 Note(本地落地失败未归「结果未确认」;守卫测试未真正进守卫分支):均 Act on(56f7cc48)——落地失败包装为 unconfirmed 提示刷新;守卫测试改为阻塞到批次取消才返回、真正进入守卫分支,并实测改坏归因时用例会红。详见行内回复。
  • R11 fast-ship-bot 1 P2 + 2 Note(守卫归因缺批次回看;落地失败无用例;契约描述低估上限):均 Act on(13509445)——守卫回读 batchCtx.Err()/rateLimited 按批次归因;抽窄接口注入落地失败断言未确认文案;契约描述改为最坏=单批条数。详见行内回复。
  • R12 fast-ship-bot 1 P2(内部事务缺批次回看,包装错误误报内部故障):Act on(69510266)——事务失败先回读 ctx.Err(),批次已死归取消/超时。详见行内回复。
  • R13 fast-ship-bot 1 P2 + 1 Note(裸 429 不熔断;R12 用例未进事务失败分支):均 Act on(05615b21)——status==429 直接命中限流;抽 classifyInternalCloseErr 直测归因边界,实测改坏回读用例红。详见行内回复。
  • R14 fast-ship-bot 1 P2 + 2 Note(PATCH/落地失败归一化后丢日志;classify 调用点无回归保护):均 Act on(fcdba9a0)——PATCH/落地失败归一化前按 id 记 warn;加 runCloseInternalTx 测试缝断言归因。详见行内回复。
  • R15 fast-ship-bot 1 P2 + 1 Note(守卫失败丢底层错误不记日志;IsOpenAndDone 无真库用例):均 Act on(51ba1414)——守卫非 ctx 错误先 warn 再返回静态文案;补真库三组合回归,实测改错条件用例红。详见行内回复。

审查收敛记录(补充)

  • 代码审查 R16 起全部组合通过(0 P0–P2 / 0 Note),无 Act on 剩余,审查收敛于 51ba1414。
  • 视觉复核若后续提出新问题,将按新轮次另行处置。
## 相关需求 INT-185([看板关闭全部的边界:RowsAffected、在途 PATCH 与超时](https://fj.frp.musiver.cn/projects/5ca4b87d-7014-4d48-809e-b0c80cd9fff7/issues/d5cb2788-aabb-4648-8bd0-5e0cd34426c0)),来源 PR #162 第 1 轮审查的 7 项边界。 ## 改动内容 批量关闭「关闭全部已完成」在 INT-174 主路径(GitHub 并发关闭、内部单事务、请求可取消)之上补齐边界: 1. **行级命中判定**:`BatchCloseInternalTx` 把 `state=open` 写入 WHERE,事务内逐 id UPDATE 按 `RowsAffected` 区分命中/未命中;只对命中行计成功并删推荐,未命中行(列表与事务之间被并发方改出 open)逐条记入 failures,文案「该问题已在其他操作中被更新,已跳过关闭」。 2. **超时预算后移**:30s 整批截止移到 `ListFiltered` 成功之后创建,只管关闭阶段;前置权限检查与列表查询本就不收 ctx,不再被整批截止截断,调用方取消照常生效。 3. **在途 PATCH 与整批取消解耦**:worker 在 PATCH 开始前判定整批取消(`sem.Acquire` 成功不等于 worker 已调度),之后经 `context.WithoutCancel` + 单请求 15s 上限继续走完并落库,缩小「远端已关、本地仍 open」窗口;取消后不再开始新 PATCH。 4. **取消文案分两类**:未开始=「请求已取消或超时,未继续关闭」;已发出但结果未确认=「关闭结果未确认:GitHub 侧可能已关闭,请刷新查看」。前端识别 ky `TimeoutError` 提示「请求超时:部分问题可能已关闭,请刷新看板确认结果」,`HTTPError` 行为不变。 5. **`upsertGitHubIssue` 入口守卫**:`nil` / `GetID()==0` 返回可读错误不 panic;创建/更新/同步三个调用方均只按 error 分支,行为安全。 6. **并发测试去墙钟**:改为 `onUpdateIssue` 钩子 started/release 通道确定性同步,只断言并发峰值与调用次数;`t.Cleanup` 保证失败路径也放行钩子内的 worker。 7. **限流识别**:`UpdateIssue` 命中 `*RateLimitError` / `*AbuseRateLimitError` / 403/429 且 Message/body 含 rate limit/abuse detection 时统一回报「GitHub API 限流,请稍后重试」,判定口径对齐 PR 域 `pullRequestAccessHint` 但不共用。 文档同步:`docs/internal/issue-list-sql-pushdown.md`、`docs/internal/issue-recommendations.md`(推荐只随命中行删除)。 ## 修改范围 server(repository / service / 测试)、web(看板关闭全部按钮的超时文案)、docs/internal 两份。 ## 本地验证 | 检查命令 | 结果 | | --- | --- | | `cd web && pnpm api-types --check` | 通过,无 schema 变更 | | `make lint` | 通过(gofmt 干净、go vet 干净、eslint 0 warning、tsc 0 error) | | `make test` | 通过(server 全包 ok;web 146 文件 1771 用例全过) | - 验证方式(命令 / 无头浏览器):Go 单测 + `go test -race -count=15`(BatchClose 子集);Playwright 无头 Chromium 双实例(基线 4412cd13 与本提交,独立端口/独立库)走真实看板关闭流程;真实并发 batch-close API 请求触发未命中路径。 - 启动命令:`go build -o <scratch>/fast_ship ./cmd/server` 后以 `FAST_SHIP_*` 环境变量独立端口启动(verify-fast-ship 技能隔离流程,未占用共享 4888/4999)。 - 验证提交 SHA:eee1f5fd - [x] 应用可以启动(`make dev`),主要使用流程已验证(server / web 按改动范围) - [x] 异常或空状态已验证(如适用) ### 已修复的基线问题 无本 PR 检查口径内的基线问题。额外发现的 -race 基线测试失败(`TestT3Int148SettlementReadOnlyGuard` 稳定复现、`TestBotBudgetTaskScopeAndLinkedMain` 并行偶发,均已在 main 4412cd13 复现,与本改动无关)已立跟进 issue INT-190。 ## 端到端截图 看板「关闭全部」主流程改前/改后对比(同一数据、同一页面): | 场景 | 改动前 | 改动后 | | --- | --- | --- | | 看板「已完成」列(2 条 done) | ![04-board-done-column.png](https://uiv.frp.musiver.cn/f/juZxZDguvx.png) | ![04-board-done-column.png](https://uiv.frp.musiver.cn/f/0tmIGytl24.png) | | 关闭全部确认对话框 | ![05-close-all-dialog.png](https://uiv.frp.musiver.cn/f/PZHGX6dASj.png) | ![05-close-all-dialog.png](https://uiv.frp.musiver.cn/f/fC477IHmMb.png) | | 关闭成功 toast + 空看板 | ![06-close-all-toast.png](https://uiv.frp.musiver.cn/f/q6KuSoRHDq.png) | ![06-close-all-toast.png](https://uiv.frp.musiver.cn/f/s1OKS15nvV.png) | 本 PR 的界面可见变化集中在失败/超时文案(限流、取消、请求超时),这些路径依赖真实 GitHub 4xx/网络故障,无头环境无法诚实触发,改用**真实并发 API 响应**作为替代证据:同一项目 4 条 done+open,两个 batch-close 请求同刻并发,后者在事务内全部未命中,`failures[].error` 即 INT-185 新文案(原始响应见 `api-batch-close-race-2.json`,已随验证产物留存): ```json {"total": 4, "succeeded": 0, "failed": 4, "failures": [{"reference": "INT-6", "error": "该问题已在其他操作中被更新,已跳过关闭"}, ...]} ``` 文案分类(未开始 / 结果未确认 / 限流)由服务层单测逐条断言覆盖。 ## 未验证范围与原因 - GitHub 限流(403/429)、PATCH 单请求超时、整批 30s 截止的真实远端行为:需要真实 GitHub 故障注入或 30s+ 等待,无头验证以单测(类型化错误注入 + started/release 确定性同步)与 `-race -count=15` 代替。 - GitHub 来源的端到端关闭流程:需要真实仓库集成,本环境未配置;现有 GitHub 域集成测试(fake client)全绿。 ## API 变更说明 无。openapi 契约零变更(`pnpm api-types --check` 通过);失败文案是 `IssueBatchResult.failures[].error` 中的自由字符串。 ## 创建 PR 前完成 - [x] 本地验证表已填完,每条检查零警告、零错误、零失败 - [x] 端到端截图已贴入,或已写明无需截图的原因 - [x] 补充需求编号(如果存在) - [x] 变更影响范围已补充到 PR 描述中 ## 审查意见处理 <!-- 复审阶段的待办,由 $pr-review-loop 收敛后勾选,创建 PR 时保持未勾选。 --> - [x] 逐条处理所有审查评论:真实问题修复后回复并重新验证;误报或不适用意见回复理由。 - [x] 推送新提交后按 `$pr-review-loop` 自动等待审查结果并处理新意见,直到收敛(R16 全部组合通过,0 P0–P2) ## 复审处置 - **R1 fast-ship-bot P2**(failures[].error 未透传进看板 toast):Act on,已修复(3ddaad75)——失败分支追加首条 failure.error,指引随服务端文案自带;补 2 条 UI 单测(透传 + 回退)。详见评论区的 R1 处置说明。 - **codex**:首次请求 30 分钟无响应已按规则跳过并重触发一次,等待其结论。 - **R2 fast-ship-bot 2 条 P2**(失败原因只取首条掩盖类别;限流后仍继续派发 PATCH):均 Act on,已修复(ad56479a)——前端失败原因去重全展示;服务端首个限流熔断派发、剩余项「未尝试」、在途 PATCH 不受影响。详见两条行内意见的处置回复。 - **R3 fast-ship-bot 1 P2 + 2 Notes**(第 4 类文案被截断挤掉;认证类不熔断;网关超时归因):触发方向审计(pi/claude-opus-5-5,定性=开放反例)。按审计路径换模型收敛(fed57239):服务端文案规范化(去 URL,5xx/传输→「结果未确认」)、前端分组计数不截断、网关/网络中断归入刷新确认;认证熔断拆 [INT-192](https://fj.frp.musiver.cn/projects/5ca4b87d-7014-4d48-809e-b0c80cd9fff7/issues/0bed0ea5-e921-4321-ba42-85cef88e59c4)。详见两条 R3 处置回复。 ## 交付范围(方向审计后圈定) 本 PR 管:看板「关闭全部」的失败文案指引与归因——服务端失败文案收敛为少量固定类别(不含 URL/Issue 号),前端按文案分组计数全部展示、明细差额显式标注;限流熔断派发;整请求与逐条失败只要远端可能已生效(ky 超时/网关 502/503/504/网络中断/服务端 5xx/传输错误)一律提示刷新确认。 本 PR 不管:认证/权限类失败(401、非限流 403)的派发熔断——属新派发策略,已拆跟进 issue INT-192(404 是单条状态,不熔断)。收敛保证:任意批次中每类失败原因至少以「文案 ×条数(样例引用)」出现一次;未列明细的条数显式标注。后续服务端新增文案类别不再挤占既有类别的展示。 - **R4 fast-ship-bot 3 条 P2**(明细截断吞类别;sonner description 换行折叠;2xx 解析失败与 catch 范围误报):均在交付范围内 Act on,已修复(a6527974)——上限后新类别样例保全、description 逐行 ReactNode、catch 收窄 + SyntaxError 归入刷新确认。详见三条行内意见的处置回复。 - **R5 fast-ship-bot 2 条 P2**(归因随 goroutine 时序漂移;契约描述未随超上限行为更新):均 Act on,已修复(f36ccecd)——归因当场定死(drain 不再重读全局标志);openapi 描述更新并重生成三份产物。详见两条行内意见的处置回复。 - **R6 fast-ship-bot 2 条 P2**(关闭事务未钉 workflow 条件;截断时分组计数低估):均在交付范围内 Act on,已修复(d48f2ea4)——tx WHERE 经子查询钉住 workflow=done;截断时不展示下界计数,只保留类别与样例。详见两条行内意见的处置回复。 - **R7 fast-ship-bot 2 条 P2**(GitHub 分支无行级复核;共享契约描述与 internal-meta 实现不符):均 Act on(6e2731ee)——closeOneGitHubIssue 发 PATCH 前经 IsOpenAndDone 复核;抽出 appendBatchFailureEntry 让两个端点共用截断语义。详见行内回复。 - **R8 fast-ship-bot 2 条 P2**(workflow 子查询全表扫描;守卫无 ctx 且不再复核取消):均 Act on(ab3ebfce)——改关联 EXISTS 走 issue_id 主键;守卫带 ctx 并通过后再判 batchCtx.Err()。详见行内回复。 - **R9 fast-ship-bot 2 条 P2**(守卫取消被误报为内部故障;5xx 带 JSON 绕过刷新提示):均 Act on(edd78bbb)——守卫出错先判 ctx err 按熔断状态归因;前端先按 status>=500 走刷新确认再处理 4xx message。详见行内回复。 - **R10 fast-ship-bot 1 P2 + 1 Note**(本地落地失败未归「结果未确认」;守卫测试未真正进守卫分支):均 Act on(56f7cc48)——落地失败包装为 unconfirmed 提示刷新;守卫测试改为阻塞到批次取消才返回、真正进入守卫分支,并实测改坏归因时用例会红。详见行内回复。 - **R11 fast-ship-bot 1 P2 + 2 Note**(守卫归因缺批次回看;落地失败无用例;契约描述低估上限):均 Act on(13509445)——守卫回读 batchCtx.Err()/rateLimited 按批次归因;抽窄接口注入落地失败断言未确认文案;契约描述改为最坏=单批条数。详见行内回复。 - **R12 fast-ship-bot 1 P2**(内部事务缺批次回看,包装错误误报内部故障):Act on(69510266)——事务失败先回读 ctx.Err(),批次已死归取消/超时。详见行内回复。 - **R13 fast-ship-bot 1 P2 + 1 Note**(裸 429 不熔断;R12 用例未进事务失败分支):均 Act on(05615b21)——status==429 直接命中限流;抽 classifyInternalCloseErr 直测归因边界,实测改坏回读用例红。详见行内回复。 - **R14 fast-ship-bot 1 P2 + 2 Note**(PATCH/落地失败归一化后丢日志;classify 调用点无回归保护):均 Act on(fcdba9a0)——PATCH/落地失败归一化前按 id 记 warn;加 runCloseInternalTx 测试缝断言归因。详见行内回复。 - **R15 fast-ship-bot 1 P2 + 1 Note**(守卫失败丢底层错误不记日志;IsOpenAndDone 无真库用例):均 Act on(51ba1414)——守卫非 ctx 错误先 warn 再返回静态文案;补真库三组合回归,实测改错条件用例红。详见行内回复。 ## 审查收敛记录(补充) - 代码审查 R16 起全部组合通过(0 P0–P2 / 0 Note),无 Act on 剩余,审查收敛于 51ba1414。 - 视觉复核若后续提出新问题,将按新轮次另行处置。
INT-174 主路径合并后的 7 项审查边界(PR #162 第 1 轮):

- BatchCloseInternalTx 把 state=open 写入 WHERE,事务内逐 id UPDATE 按
  RowsAffected 区分命中/未命中;只对命中行计成功并删推荐,未命中行逐条
  记入 failures(「该问题已在其他操作中被更新,已跳过关闭」)。
- 30s 整批截止移到 ListFiltered 成功之后创建,预算只管关闭阶段;parent
  仍是调用方 ctx,调用方取消照常生效。
- 在途 GitHub PATCH 与整批取消解耦:worker 在 PATCH 开始前判定整批取消
  (Acquire 成功不等于已调度),之后经 context.WithoutCancel + 单请求
  15s 上限继续走完并落库,缩小「远端已关、本地仍 open」窗口。
- 取消文案分两类:未开始=「请求已取消或超时,未继续关闭」;已发出但
  超时=「关闭结果未确认:GitHub 侧可能已关闭,请刷新查看」。前端识别
  ky TimeoutError 提示刷新确认,HTTPError 行为不变。
- upsertGitHubIssue 入口守卫 nil / GetID()==0,返回错误不 panic。
- 并发测试只断言并发度(started/release 通道确定性同步),删墙钟断言。
- UpdateIssue 识别 GitHub 限流(RateLimitError/AbuseRateLimitError/
  ErrorResponse 403/429 + rate limit/abuse detection 字样),统一限流
  文案,口径对齐 PR 域 pullRequestAccessHint。

验证:make lint / make test / pnpm api-types --check 全绿;BatchClose
子集 go test -race -count=15 通过;双实例无头端到端 + 真实并发 batch-close
API 响应证明未命中新文案。
Author
Owner

@codex review

@codex review <!-- pr-review-loop:reply -->
Collaborator

代码审查 · 51ba141 · 第 16 轮

全部组合通过,未发现 P0–P2 问题。

组合 状态 P0 P1 P2 Note
cliproxyapi/deepseek-v4.1-flash · pi 完成 0 0 0 0

cliproxyapi/deepseek-v4.1-flash · pi

thinking=low

本轮未发现新的阻塞或边界问题:R15 的守卫失败日志与 IsOpenAndDone 真库回归均已落地并复核,行级命中、在途 PATCH 解耦、限流熔断与超时归因路径与文档一致。剩余风险集中在既有设计取舍(30s 批次预算 + 15s 在途上限的最坏时延),不构成本轮新增缺陷。

复审说明:推送新提交后会自动重新审查。

<!-- fast-ship-code-review --> ## 代码审查 · 51ba141 · 第 16 轮 全部组合通过,未发现 P0–P2 问题。 | 组合 | 状态 | P0 | P1 | P2 | Note | | --- | --- | --- | --- | --- | --- | | cliproxyapi/deepseek-v4.1-flash · pi | 完成 | 0 | 0 | 0 | 0 | ### cliproxyapi/deepseek-v4.1-flash · pi <sub>thinking=low</sub> > 本轮未发现新的阻塞或边界问题:R15 的守卫失败日志与 IsOpenAndDone 真库回归均已落地并复核,行级命中、在途 PATCH 解耦、限流熔断与超时归因路径与文档一致。剩余风险集中在既有设计取舍(30s 批次预算 + 15s 在途上限的最坏时延),不构成本轮新增缺陷。 复审说明:推送新提交后会自动重新审查。
Author
Owner

@codex review

@codex review <!-- pr-review-loop:reply -->
fast-ship-bot 第 1 轮 P2:handleClose 只拼 reference 不读 failures[].error,
「已跳过/结果未确认/限流」三类新文案在看板 UI 上不可见;且服务端 30s 截止
以 200+failures 返回,客户端 ky 60s 超时分支几乎不可达,指引到不了用户。

失败分支现把首条 failure.error 拼进 toast(「已关闭 X 个问题,Y 个失败:
<首条原因>(refs)」),指引随服务端文案自带;failures 无 error 字段时回退
原计数文案。补 2 条 UI 单测:透传断言 + 回退断言;sonner toast 改为可观测
mock。

失败 toast 的无头截图尝试放弃:失败路径需真实并发竞速,自动化取景在本环境
不稳定(放弃规则)。替代证据:单测断言 + 真实并发 batch-close API 响应
(failures[].error 即新文案,已随 PR 描述留存)。
Author
Owner

R1 处置:Act on,已修复(3ddaad75)

P2 [web/src/routes/board/components/close-all-done-button.tsx:47]:handleClose 的失败分支只统计 result.failed 并拼前 3 个 reference,从不读取 failure.error……建议:warning toast 追加首个 failure.error(或按「已跳过/未确认/限流」分档提示),并对含未确认或取消类 failure 的结果统一附带「请刷新看板确认结果」。

采纳,按建议的第一种(追加首条 failure.error)修复:

  • 失败分支改为 已关闭 X 个问题,Y 个失败:<首条 failure.error>(refs)——三类分类文案(已跳过/结果未确认/限流)随服务端文案透传,指引(刷新确认/稍后重试)内嵌在文案里自带,不重复拼接。
  • failures 无 error 字段时回退原计数文案。
  • ky TimeoutError 分支保留为兜底(你说得对:30s 截止 + 200 返回使其几乎不可达,但零成本保留),不作为主要指引路径。

验证:新增 2 条 UI 单测断言透传与回退(pnpm vitest run close-all-done-button.test.tsx 6/6),make lint / make test(1773 用例)在 3ddaad75 全绿。

关于失败 toast 的端到端截图:该路径依赖真实并发竞速(两个 batch-close 同刻发起,失败方才有 failures),本次无头自动化取景连续失败,按项目放弃规则停止,以上述单测断言 + PR 描述中已有的真实并发 API 响应 JSON(failures[].error 即新文案原文)作为替代证据。

**R1 处置:Act on,已修复(3ddaad75)** > P2 [web/src/routes/board/components/close-all-done-button.tsx:47]:handleClose 的失败分支只统计 result.failed 并拼前 3 个 reference,从不读取 failure.error……建议:warning toast 追加首个 failure.error(或按「已跳过/未确认/限流」分档提示),并对含未确认或取消类 failure 的结果统一附带「请刷新看板确认结果」。 采纳,按建议的第一种(追加首条 failure.error)修复: - 失败分支改为 `已关闭 X 个问题,Y 个失败:<首条 failure.error>(refs)`——三类分类文案(已跳过/结果未确认/限流)随服务端文案透传,指引(刷新确认/稍后重试)内嵌在文案里自带,不重复拼接。 - failures 无 error 字段时回退原计数文案。 - ky TimeoutError 分支保留为兜底(你说得对:30s 截止 + 200 返回使其几乎不可达,但零成本保留),不作为主要指引路径。 验证:新增 2 条 UI 单测断言透传与回退(`pnpm vitest run close-all-done-button.test.tsx` 6/6),`make lint` / `make test`(1773 用例)在 3ddaad75 全绿。 关于失败 toast 的端到端截图:该路径依赖真实并发竞速(两个 batch-close 同刻发起,失败方才有 failures),本次无头自动化取景连续失败,按项目放弃规则停止,以上述单测断言 + PR 描述中已有的真实并发 API 响应 JSON(`failures[].error` 即新文案原文)作为替代证据。 <!-- pr-review-loop:reply -->
fastship-bot left a comment

服务端的行级命中、在途 PATCH 与超时预算边界实现与新增单测相符,R1 的失败文案透传已修复;本轮仍有两处 P2:失败原因只取首条导致分类被掩盖,以及检测到限流后仍继续把剩余 PATCH 打到已被限流的 API。

服务端的行级命中、在途 PATCH 与超时预算边界实现与新增单测相符,R1 的失败文案透传已修复;本轮仍有两处 P2:失败原因只取首条导致分类被掩盖,以及检测到限流后仍继续把剩余 PATCH 打到已被限流的 API。 <!-- fast-ship-review-run:a0a016e2-68b6-49ce-895b-b0891bcdb567 -->
@ -197,0 +222,4 @@
// PATCH 自身超时/取消:远端可能已生效,结果未确认,与
// 「未开始」的取消区分开。
err = &batchClosePatchUnconfirmedError{err: err}
} else if isGitHubRateLimitError(err) {
Collaborator

P2:问题:新增的限流识别只用于把当前条目标成「GitHub API 限流」,调度循环没有因此停止;批次上限 200,30s 预算内会继续向已被限流的 API 发出剩余 PATCH(首波之后每个 worker 仍会连续取新任务)。影响:主限流下这些请求必然 403,纯粹浪费配额;次级/abuse 限流下持续重试还可能延长封禁时间,用户最终拿到最多 50 条重复的限流失败明细。建议:首个限流错误后置一个批次级标记,让后续 worker 在发请求前直接返回同一限流文案(与 ctx 取消的「未开始」路径复用),不再实际调用 GitHub。

P2:问题:新增的限流识别只用于把当前条目标成「GitHub API 限流」,调度循环没有因此停止;批次上限 200,30s 预算内会继续向已被限流的 API 发出剩余 PATCH(首波之后每个 worker 仍会连续取新任务)。影响:主限流下这些请求必然 403,纯粹浪费配额;次级/abuse 限流下持续重试还可能延长封禁时间,用户最终拿到最多 50 条重复的限流失败明细。建议:首个限流错误后置一个批次级标记,让后续 worker 在发请求前直接返回同一限流文案(与 ctx 取消的「未开始」路径复用),不再实际调用 GitHub。
@ -51,0 +51,4 @@
// failures[].error 是服务端的分类文案(已跳过/结果未确认/限流/取消),
// 必须透传:首条原因直接进 toast,指引(刷新确认/稍后重试)随文案自带,
// 只报数量会让用户把部分成功当成整批失败再点一次(INT-185)。
const reason = result.failures[0]?.error;
Collaborator

P2:问题:toast 只取 failures[0].error 作为原因,但 refs 来自 failures 前 3 条,二者可能不是同一类失败;服务端先处理 internal 再处理 GitHub,且 failures 上限 50 条,所以「已跳过关闭」总排在前面。影响:混合来源批次里只要有一条 internal 行被并发改写,GitHub 的「关闭结果未确认,请刷新查看」或「限流,请稍后重试」永远不会出现在 toast 上,用户看到的是「已关闭 0 个问题,N 个失败:该问题已在其他操作中被更新(INT-3、INT-4、INT-5)」,既不知道远端可能已关闭、也不会刷新或稍后重试。建议:按失败原因聚合去重后展示(例如首条原因 + 「另有 M 个结果未确认」),或在存在未确认/限流类失败时固定附带对应指引。

P2:问题:toast 只取 failures[0].error 作为原因,但 refs 来自 failures 前 3 条,二者可能不是同一类失败;服务端先处理 internal 再处理 GitHub,且 failures 上限 50 条,所以「已跳过关闭」总排在前面。影响:混合来源批次里只要有一条 internal 行被并发改写,GitHub 的「关闭结果未确认,请刷新查看」或「限流,请稍后重试」永远不会出现在 toast 上,用户看到的是「已关闭 0 个问题,N 个失败:该问题已在其他操作中被更新(INT-3、INT-4、INT-5)」,既不知道远端可能已关闭、也不会刷新或稍后重试。建议:按失败原因聚合去重后展示(例如首条原因 + 「另有 M 个结果未确认」),或在存在未确认/限流类失败时固定附带对应指引。
fast-ship-bot 第 2 轮 2 条 P2:

1. 服务端(issue_list.go):命中首个限流即熔断派发——dispatchCtx 与整批
   截止分开,trip 后剩余项短路为「GitHub API 限流,未尝试关闭」,已在途的
   PATCH 经 WithoutCancel 照常走完并落库;worker 守卫/Acquire 以 ctx err
   弹回的项在回收处按熔断标志归类为「未尝试」(排除「结果未确认」),避免
   误报整批取消。熔断与相邻 worker 槽位释放无跨 goroutine 时序保证,trip
   前后极小窗口至多再发并发度条请求——有界可接受,注释说明。
2. 前端(close-all-done-button.tsx):失败原因去重后全部展示(上限 3 条,
   「;」连接),不再让首条(内部行恒先处理,恒为「已跳过」)掩盖 GitHub
   侧的「结果未确认/限流」类别。

测试:限流熔断测试改钩子门控(4 个 worker 全部在途后统一放行,calls==4
与文案分布成为确定性事实),-race 连续 6 轮全绿;CancelStopsLater 复跑
15 连稳定;UI 单测更新为多类别去重断言;make lint / make test /
pnpm api-types --check 全绿。
Author
Owner

R2 处置(行内):Act on,已修复(ad56479a)

[web/src/routes/board/components/close-all-done-button.tsx 行内] toast 只取 failures[0].error 作为原因,但 refs 来自 failures 前 3 条,二者可能不是同一类失败;服务端先处理 internal 再处理 GitHub,且 failures 上限 50 条,所以「已跳过关闭」总排在前面……

采纳。失败原因改为去重后全部展示(保持出现顺序,上限 3 条、以「;」连接):
已关闭 X 个问题,Y 个失败:<原因1>;<原因2>(refs)。混合批次里内部行的「已跳过」与 GitHub 侧的「结果未确认/限流」同时可见,不再被首条掩盖。UI 单测更新为多类别断言(跳过 + 未确认同批展示),全量 vitest 通过。

**R2 处置(行内):Act on,已修复(ad56479a)** > [web/src/routes/board/components/close-all-done-button.tsx 行内] toast 只取 failures[0].error 作为原因,但 refs 来自 failures 前 3 条,二者可能不是同一类失败;服务端先处理 internal 再处理 GitHub,且 failures 上限 50 条,所以「已跳过关闭」总排在前面…… 采纳。失败原因改为**去重后全部展示**(保持出现顺序,上限 3 条、以「;」连接): `已关闭 X 个问题,Y 个失败:<原因1>;<原因2>(refs)`。混合批次里内部行的「已跳过」与 GitHub 侧的「结果未确认/限流」同时可见,不再被首条掩盖。UI 单测更新为多类别断言(跳过 + 未确认同批展示),全量 vitest 通过。 <!-- pr-review-loop:reply -->
Author
Owner

R2 处置(行内):Act on,已修复(ad56479a)

[server/internal/service/issue_list.go 行内] 新增的限流识别只用于把当前条目标成「GitHub API 限流」,调度循环没有因此停止;批次上限 200,30s 预算内会继续向已被限流的 API 发出剩余 PATCH……建议:首个限流错…

采纳。实现为首个限流即熔断派发:

  • dispatchCtx 与整批截止分开;worker 命中限流时 CompareAndSwap 熔断并 dispatchCancel,剩余项不再派发,逐条记新文案「GitHub API 限流,未尝试关闭」(与单条限流文案区分——这些项没发过请求,不能写成「结果未确认」)。
  • 已派发的在途 PATCH 经 WithoutCancel 与熔断解耦,照常走完并落库(维持 INT-185 缩小「远端已关、本地仍 open」窗口的目标)。
  • worker 守卫/Acquire 以 ctx err 弹回的项,在回收处按熔断标志归类为「未尝试」(排除「结果未确认」类),避免误报「整批已取消」。
  • 窗口说明:熔断(trip)与相邻 worker 的槽位释放没有跨 goroutine 时序保证,trip 落地前后的极小窗口内至多再发出并发度条(4 条)请求——有界且可接受,已在代码注释说明,不为此引入同步 trip。

验证:限流熔断测试改钩子门控(4 worker 全部在途后统一放行,calls==4 与文案分布确定性),go test -race 连续 6 轮全绿;新增文案「未尝试」逐条断言;make lint / make test 在 ad56479a 全绿。

**R2 处置(行内):Act on,已修复(ad56479a)** > [server/internal/service/issue_list.go 行内] 新增的限流识别只用于把当前条目标成「GitHub API 限流」,调度循环没有因此停止;批次上限 200,30s 预算内会继续向已被限流的 API 发出剩余 PATCH……建议:首个限流错… 采纳。实现为**首个限流即熔断派发**: - `dispatchCtx` 与整批截止分开;worker 命中限流时 `CompareAndSwap` 熔断并 `dispatchCancel`,剩余项不再派发,逐条记新文案「GitHub API 限流,未尝试关闭」(与单条限流文案区分——这些项没发过请求,不能写成「结果未确认」)。 - 已派发的在途 PATCH 经 `WithoutCancel` 与熔断解耦,照常走完并落库(维持 INT-185 缩小「远端已关、本地仍 open」窗口的目标)。 - worker 守卫/Acquire 以 ctx err 弹回的项,在回收处按熔断标志归类为「未尝试」(排除「结果未确认」类),避免误报「整批已取消」。 - 窗口说明:熔断(trip)与相邻 worker 的槽位释放没有跨 goroutine 时序保证,trip 落地前后的极小窗口内至多再发出并发度条(4 条)请求——有界且可接受,已在代码注释说明,不为此引入同步 trip。 验证:限流熔断测试改钩子门控(4 worker 全部在途后统一放行,calls==4 与文案分布确定性),`go test -race` 连续 6 轮全绿;新增文案「未尝试」逐条断言;`make lint` / `make test` 在 ad56479a 全绿。 <!-- pr-review-loop:reply -->
fastship-bot left a comment

R2 的两条意见在 ad56479a 均已真实修复(失败原因去重展示、首个限流即熔断派发),服务端行级命中/在途 PATCH/超时预算的实现与用例一致,未发现 P0/P1;本轮只剩前端失败原因展示的截断边界与两处错误分类的收尾问题。影响面是极端混合批次里一类失败提示会被第 4 类原因挤掉,以及认证类失败没有随限流一起熔断造成重复请求与噪声 failures。

R2 的两条意见在 ad56479a 均已真实修复(失败原因去重展示、首个限流即熔断派发),服务端行级命中/在途 PATCH/超时预算的实现与用例一致,未发现 P0/P1;本轮只剩前端失败原因展示的截断边界与两处错误分类的收尾问题。影响面是极端混合批次里一类失败提示会被第 4 类原因挤掉,以及认证类失败没有随限流一起熔断造成重复请求与噪声 failures。 <!-- fast-ship-review-run:1180612b-3054-44ea-8adb-b6ba1435703a -->
@ -51,0 +54,4 @@
//(刷新确认/稍后重试)随文案自带(INT-185)。
const reasons = [
...new Set(result.failures.map((f) => f.error).filter(Boolean)),
].slice(0, 3);
Collaborator

P2:问题:reasons 去重后仍 .slice(0, 3),而 ad56479a 新增了第 4 类服务端文案(batchCloseGitHubRateLimitRemainingText「GitHub API 限流,未尝试关闭」),单批可同时出现「已跳过关闭 / 关闭结果未确认 / 限流请稍后重试 / 限流未尝试关闭」四类;同时 refs 仍取自 failures 前 3 条,与展示的原因不是同一集合。影响:四类同批时最后出现的那类被静默丢掉,被丢的若是「限流,请稍后重试」或「结果未确认,请刷新查看」,用户就拿不到重试/刷新指引(上一轮正是为消除这种掩盖而改)。建议:reasons.length > 3 时追加「等 N 类原因」,或优先保留含指引的「未确认/限流」类,并让 refs 与展示原因对齐。

P2:问题:reasons 去重后仍 `.slice(0, 3)`,而 ad56479a 新增了第 4 类服务端文案(`batchCloseGitHubRateLimitRemainingText`「GitHub API 限流,未尝试关闭」),单批可同时出现「已跳过关闭 / 关闭结果未确认 / 限流请稍后重试 / 限流未尝试关闭」四类;同时 refs 仍取自 failures 前 3 条,与展示的原因不是同一集合。影响:四类同批时最后出现的那类被静默丢掉,被丢的若是「限流,请稍后重试」或「结果未确认,请刷新查看」,用户就拿不到重试/刷新指引(上一轮正是为消除这种掩盖而改)。建议:`reasons.length > 3` 时追加「等 N 类原因」,或优先保留含指引的「未确认/限流」类,并让 refs 与展示原因对齐。
web toast 子系统连续三轮 Act on,方向审计(Claude Opus 5.5)定性为开放
反例:前端固定容量展示 + 服务端文案未规范化(原文带 URL/Issue 号导致
去重失效),每加一类文案就产生新边界反例。按审计路径换模型收敛:

- 服务端(issue_list.go):GitHub 5xx 与传输层错误归入「结果未确认」
  (PATCH 已发出、结果未知,宁可多提示刷新);其余 ErrorResponse 规范化
  为「更新 GitHub Issue 失败:<状态码> <Message>」,去掉 METHOD 与完整
  URL——同类错误逐字相同,前端分组去重有效。
- 前端(close-all-done-button.tsx):失败原因按文案分组计数、全部展示,
  每组取样例引用(「<原因> ×N(如 INT-x)」),删除 slice(0,3) 截断;
  failed 超过明细上限 50 时显式标注「另有 X 条未列明细」。
- 前端归因(formatBatchCloseError):网关 502/503/504(无 JSON 正文)与
  fetch 网络中断(TypeError)归入「部分问题可能已关闭,请刷新看板确认
  结果」;带 JSON message 的服务端错误仍优先。
- 认证/权限类熔断(401、非限流 403)按审计裁定拆出范围外,立跟进 issue
  INT-192(404 是单条状态,不熔断)。
- 文档:issue-list-sql-pushdown.md 补文案规范化与分组展示口径。

测试:新增服务端规范化用例(502/传输→未确认、同类 404 逐字相同且无
URL、空 Message 无尾随空格);前端分组/四类同现/差额标注/网关归因/网络
中断用例;-race ×3 与全量 make lint / make test / api-types 全绿。
Author
Owner

R3 处置(行内 P2):Act on,已按方向审计换模型修复(fed57239)

[close-all-done-button.tsx:57] reasons 去重后仍 .slice(0, 3),而 ad56479a 新增了第 4 类服务端文案……建议:reasons.length > 3 时追加「等 N 类原因」,或优先保留含指引的「未确认/限流」类,并让 refs 与展示原因对齐。

这条是 web toast 子系统连续第 3 轮 Act on,按流程先停了方向审计(pi/claude-opus-5-5),定性为开放反例:前端固定容量展示 + 服务端文案未规范化(go-github 的 Error() 带 METHOD 和完整 URL,逐条不同导致去重对原文类失效),每加一类文案就产生新边界——把 3 调成 4 只会迎来第 5 类。已按审计路径换模型:

  • 服务端先把文案收敛成少量固定类别:5xx 与传输错误归入「结果未确认」;其余 ErrorResponse 规范化为「更新 GitHub Issue 失败:<状态码> 」,不含 URL/Issue 号,同类错误逐字相同。
  • 前端删除截断:按文案分组计数全部展示,每组取样例引用(<原因> ×N(如 INT-x)),引用与原因天然同集合;failed 超明细上限 50 时显式标注「另有 X 条未列明细」。
  • 覆盖范围写进 PR 描述:任意批次中每类失败原因至少以「文案 ×条数(样例引用)」出现一次;未列明细显式标注;整请求或逐条失败只要远端可能已生效,一律提示刷新确认。

验证:服务端规范化用例(502/传输→未确认、同类 404 逐字相同且无 URL);前端四类同现全可见、差额标注用例;-race ×3、make lint/make test/api-types 在 fed57239 全绿。

**R3 处置(行内 P2):Act on,已按方向审计换模型修复(fed57239)** > [close-all-done-button.tsx:57] reasons 去重后仍 .slice(0, 3),而 ad56479a 新增了第 4 类服务端文案……建议:reasons.length > 3 时追加「等 N 类原因」,或优先保留含指引的「未确认/限流」类,并让 refs 与展示原因对齐。 这条是 web toast 子系统连续第 3 轮 Act on,按流程先停了方向审计(pi/claude-opus-5-5),定性为**开放反例**:前端固定容量展示 + 服务端文案未规范化(go-github 的 Error() 带 METHOD 和完整 URL,逐条不同导致去重对原文类失效),每加一类文案就产生新边界——把 3 调成 4 只会迎来第 5 类。已按审计路径换模型: - 服务端先把文案收敛成少量固定类别:5xx 与传输错误归入「结果未确认」;其余 ErrorResponse 规范化为「更新 GitHub Issue 失败:<状态码> <Message>」,**不含 URL/Issue 号**,同类错误逐字相同。 - 前端**删除截断**:按文案分组计数全部展示,每组取样例引用(`<原因> ×N(如 INT-x)`),引用与原因天然同集合;`failed` 超明细上限 50 时显式标注「另有 X 条未列明细」。 - 覆盖范围写进 PR 描述:任意批次中每类失败原因至少以「文案 ×条数(样例引用)」出现一次;未列明细显式标注;整请求或逐条失败只要远端可能已生效,一律提示刷新确认。 验证:服务端规范化用例(502/传输→未确认、同类 404 逐字相同且无 URL);前端四类同现全可见、差额标注用例;`-race ×3`、`make lint`/`make test`/`api-types` 在 fed57239 全绿。 <!-- pr-review-loop:reply -->
Author
Owner

R3 处置(总结含 2 条 Note):按审计定性分路

R2 的两条意见在 ad56479a 均已真实修复……本轮只剩前端失败原因展示的截断边界与两处错误分类的收尾问题……

Note 1(认证/权限类不熔断)→ Consider,本 PR 不修,已拆后续 issue:INT-192。依据:限流熔断的理由是继续请求会延长次级限流封禁(代价随请求增长),401/非限流 403 不加重惩罚、单批最多 200 条快速失败、代价有上限;404 是单条 Issue 状态(被转移/删除),按它熔断会误伤整批——判定口径需要单独设计。分组计数展示后,同文案只占一行,「failures 被占满」在界面上不再有害。这是新的派发策略,超出 INT-185 边界,按审计裁定拆出。

Note 2(网关 502/503/504 与网络中断归因)→ Act on,已修复(fed57239):前端 formatBatchCloseError 把网关 502/503/504(无 JSON 正文)与 fetch TypeError 归入「请求超时:部分问题可能已关闭,请刷新看板确认结果」,带 JSON message 的服务端错误仍优先;服务端同步把 GitHub 5xx 与传输层错误逐条归入「结果未确认:…请刷新查看」——两层都按「远端可能已生效,宁可多提示刷新」归类。测试补齐,fed57239 全绿。

**R3 处置(总结含 2 条 Note):按审计定性分路** > R2 的两条意见在 ad56479a 均已真实修复……本轮只剩前端失败原因展示的截断边界与两处错误分类的收尾问题…… **Note 1(认证/权限类不熔断)→ Consider,本 PR 不修,已拆后续 issue**:[INT-192](https://fj.frp.musiver.cn/projects/5ca4b87d-7014-4d48-809e-b0c80cd9fff7/issues/0bed0ea5-e921-4321-ba42-85cef88e59c4)。依据:限流熔断的理由是继续请求会延长次级限流封禁(代价随请求增长),401/非限流 403 不加重惩罚、单批最多 200 条快速失败、代价有上限;404 是单条 Issue 状态(被转移/删除),按它熔断会误伤整批——判定口径需要单独设计。分组计数展示后,同文案只占一行,「failures 被占满」在界面上不再有害。这是新的派发策略,超出 INT-185 边界,按审计裁定拆出。 **Note 2(网关 502/503/504 与网络中断归因)→ Act on,已修复(fed57239)**:前端 `formatBatchCloseError` 把网关 502/503/504(无 JSON 正文)与 fetch `TypeError` 归入「请求超时:部分问题可能已关闭,请刷新看板确认结果」,带 JSON message 的服务端错误仍优先;服务端同步把 GitHub 5xx 与传输层错误逐条归入「结果未确认:…请刷新查看」——两层都按「远端可能已生效,宁可多提示刷新」归类。测试补齐,fed57239 全绿。 <!-- pr-review-loop:reply -->
fastship-bot left a comment

本轮未发现 P0/P1:核心行级命中、在途 PATCH 解耦与限流熔断逻辑自洽;剩余问题集中在失败提示层——服务端 50 条明细上限仍会整体隐藏失败类别,sonner description 的换行会被折叠,2xx 响应解析失败也未归入「结果未确认」。影响是混合批次里用户可能看不到限流/刷新指引,分组计数在真实界面退化成一段。

本轮未发现 P0/P1:核心行级命中、在途 PATCH 解耦与限流熔断逻辑自洽;剩余问题集中在失败提示层——服务端 50 条明细上限仍会整体隐藏失败类别,sonner description 的换行会被折叠,2xx 响应解析失败也未归入「结果未确认」。影响是混合批次里用户可能看不到限流/刷新指引,分组计数在真实界面退化成一段。 <!-- fast-ship-review-run:051edf0e-a3d5-4d67-ad42-37924a50daeb -->
@ -313,3 +424,4 @@
func appendBatchCloseFailure(resp *BatchCloseDoneIssuesResponse, issue model.Issue, err error) {
resp.Failed++
if len(resp.Failures) >= batchCloseDoneMaxFailures {
Collaborator

P2:问题:失败明细仍在服务端被截断到 50 条(appendBatchCloseFailure),而前端只能按已列明细分组,另有 N 条未列明细 不说明缺了哪几类;内部路径先跑(closeInternalIssues 在 closeGitHubIssues 之前),其「已跳过关闭」会先占满 50 条。影响:混合来源批次里若 >50 条内部行被并发方提前改出 open,后续 GitHub 侧整类原因(限流未尝试、结果未确认)一条都不会出现在 toast,用户拿不到重试/刷新指引,与本轮「每类至少出现一次」的目标相反。建议:服务端在截断前按文案聚合(每类 count + 一个样例,并返回 unlisted),或前端至少显式提示存在未列类别。

P2:问题:失败明细仍在服务端被截断到 50 条(appendBatchCloseFailure),而前端只能按已列明细分组,`另有 N 条未列明细` 不说明缺了哪几类;内部路径先跑(closeInternalIssues 在 closeGitHubIssues 之前),其「已跳过关闭」会先占满 50 条。影响:混合来源批次里若 >50 条内部行被并发方提前改出 open,后续 GitHub 侧整类原因(限流未尝试、结果未确认)一条都不会出现在 toast,用户拿不到重试/刷新指引,与本轮「每类至少出现一次」的目标相反。建议:服务端在截断前按文案聚合(每类 count + 一个样例,并返回 unlisted),或前端至少显式提示存在未列类别。
@ -54,0 +69,4 @@
const unlisted = result.failed - result.failures.length;
if (unlisted > 0) lines.push(`另有 ${unlisted} 条未列明细`);
toast.warning(`已关闭 ${result.succeeded} 个问题,${result.failed} 个失败`, {
description: lines.join("\n"),
Collaborator

P2:问题:description 用 lines.join("\n") 拼多行,但 sonner 2.0.7 把 description 作为普通文本节点渲染,其样式表与项目 CSS 都没有 white-space: pre-line/pre-wrap,换行会被折叠成空格。影响:多类原因混排时本应逐行的「原因 ×N(如 INT-x)」连成一段,分组与计数在真实 toast 里难以辨认;单测只断言字符串,覆盖不到渲染结果。建议:description 传 ReactNode(每行一个 div 或
),或给 descriptionClassName 加 whitespace-pre-line,并补一条渲染断言。

P2:问题:description 用 `lines.join("\n")` 拼多行,但 sonner 2.0.7 把 description 作为普通文本节点渲染,其样式表与项目 CSS 都没有 white-space: pre-line/pre-wrap,换行会被折叠成空格。影响:多类原因混排时本应逐行的「原因 ×N(如 INT-x)」连成一段,分组与计数在真实 toast 里难以辨认;单测只断言字符串,覆盖不到渲染结果。建议:description 传 ReactNode(每行一个 div 或 <br/>),或给 descriptionClassName 加 whitespace-pre-line,并补一条渲染断言。
@ -132,2 +166,4 @@
if (error instanceof Error && error.name === "TimeoutError") {
return REQUEST_UNCONFIRMED_COPY;
}
return "批量关闭失败";
Collaborator

P2:问题:formatBatchCloseError 只认 HTTPError/TypeError/TimeoutError;ky 的 .json() 在 2xx 响应体被截断或不是 JSON 时抛 SyntaxError,落到「批量关闭失败」;同时 catch 覆盖了请求成功后的分组与 toast 代码,那里抛出的 TypeError 也会被报成「请求超时」。影响:请求已到达服务端、关闭可能已生效,却提示普通失败或错误的超时语义,正是 R3 要消除的「可能已关闭」误报。建议:区分「请求未发出」与「响应未解析」,把 2xx 解析失败归入 REQUEST_UNCONFIRMED_COPY,并把 catch 收窄到 mutateAsync。

P2:问题:formatBatchCloseError 只认 HTTPError/TypeError/TimeoutError;ky 的 `.json()` 在 2xx 响应体被截断或不是 JSON 时抛 SyntaxError,落到「批量关闭失败」;同时 catch 覆盖了请求成功后的分组与 toast 代码,那里抛出的 TypeError 也会被报成「请求超时」。影响:请求已到达服务端、关闭可能已生效,却提示普通失败或错误的超时语义,正是 R3 要消除的「可能已关闭」误报。建议:区分「请求未发出」与「响应未解析」,把 2xx 解析失败归入 REQUEST_UNCONFIRMED_COPY,并把 catch 收窄到 mutateAsync。
R4 三条 P2 全部在交付范围内,逐条修复:

1. 服务端(issue_list.go):appendBatchCloseFailure 达 50 条明细上限后,
   未出现过的新类别样例仍保留进明细(允许超上限,每类恰一条)——否则混合
   批次里先占满名额的「已跳过」会把 GitHub 侧整类限流/刷新指引吞掉,违背
   「每类至少出现一次」的收敛保证。
2. 前端:失败原因分组行改为 ReactNode 逐行 div(sonner 把字符串 description
   当普通文本渲染,\n 折叠成一段);行构建抽成纯函数 buildBatchCloseFailure
   Lines 供测试。
3. 前端归因:handleClose 的 catch 收窄到 mutateAsync 本身,分组与 toast 抛
   出的错误不再被误报成「请求超时」;ky .json() 对 2xx 截断/非 JSON 抛出的
   SyntaxError 归入「结果未确认」刷新文案(请求已到达服务端,关闭可能已生
   效)。

测试:服务端类别保全用例(超上限新类别样例恰一次、同类不追加);前端改
渲染断言(description 逐行文本四类全可见 + 差额标注)与 SyntaxError 用例。
-race ×2 与全量 make lint / make test / api-types 全绿。
Author
Owner

R4 处置(行内 P2,明细截断吞类别):Act on,已修复(a6527974)

[server/internal/service/issue_list.go] 失败明细仍在服务端被截断到 50 条……内部路径先跑,「已跳过关闭」会先占满 50 条……与本轮「每类至少出现一次」的目标相反。建议:服务端在截断前按文案聚合(每类 count + 一个样例,并返回 unlisted),或前端至少显式提示存在未列类别。

采纳,选了不改 API 契约的那条路:appendBatchCloseFailure 达明细上限后,未出现过的新类别样例仍保留进明细(允许超上限,每类恰一条,同类不重复追加)。效果:混合批次里内部「已跳过」占满 50 条后,GitHub 侧的限流/未尝试/结果未确认样例依然各出现一次,前端分组计数逐类可见;明细差额仍由「另有 X 条未列明细」标注。注意取舍:超上限后组内 ×N 是已列明细的计数(下界),真实总数由标题 failed 携带——聚合 count + unlisted 的形态需要动契约,若你认为值得另开 issue 讨论。

测试:类别保全用例(超上限新类别样例恰一次、同类不追加、计 Failed 照常)。

**R4 处置(行内 P2,明细截断吞类别):Act on,已修复(a6527974)** > [server/internal/service/issue_list.go] 失败明细仍在服务端被截断到 50 条……内部路径先跑,「已跳过关闭」会先占满 50 条……与本轮「每类至少出现一次」的目标相反。建议:服务端在截断前按文案聚合(每类 count + 一个样例,并返回 unlisted),或前端至少显式提示存在未列类别。 采纳,选了**不改 API 契约**的那条路:`appendBatchCloseFailure` 达明细上限后,未出现过的新类别样例仍保留进明细(允许超上限,每类恰一条,同类不重复追加)。效果:混合批次里内部「已跳过」占满 50 条后,GitHub 侧的限流/未尝试/结果未确认样例依然各出现一次,前端分组计数逐类可见;明细差额仍由「另有 X 条未列明细」标注。注意取舍:超上限后组内 ×N 是已列明细的计数(下界),真实总数由标题 failed 携带——聚合 count + unlisted 的形态需要动契约,若你认为值得另开 issue 讨论。 测试:类别保全用例(超上限新类别样例恰一次、同类不追加、计 Failed 照常)。 <!-- pr-review-loop:reply -->
Author
Owner

R4 处置(行内 P2,description 换行折叠):Act on,已修复(a6527974)

[close-all-done-button.tsx:72] description 用 lines.join("\n") 拼多行,但 sonner 2.0.7 把 description 作为普通文本节点渲染……建议:description 传 ReactNode(每行一个 div 或
),或给 descriptionClassName 加 whitespace-pre-line,并补一条渲染断言。

采纳第一种:description 传 ReactNode,逐行 <div>(flex-col 排列,不依赖 white-space 样式);行构建抽成纯函数 buildBatchCloseFailureLines 供断言。测试改渲染断言:toast 的 description 元素渲染后逐行 getByText(含四类同现 + 差额标注场景),不再是字符串断言。

**R4 处置(行内 P2,description 换行折叠):Act on,已修复(a6527974)** > [close-all-done-button.tsx:72] description 用 lines.join("\n") 拼多行,但 sonner 2.0.7 把 description 作为普通文本节点渲染……建议:description 传 ReactNode(每行一个 div 或 <br/>),或给 descriptionClassName 加 whitespace-pre-line,并补一条渲染断言。 采纳第一种:description 传 ReactNode,逐行 `<div>`(flex-col 排列,不依赖 white-space 样式);行构建抽成纯函数 `buildBatchCloseFailureLines` 供断言。测试改渲染断言:toast 的 description 元素渲染后逐行 getByText(含四类同现 + 差额标注场景),不再是字符串断言。 <!-- pr-review-loop:reply -->
Author
Owner

R4 处置(行内 P2,归因误报):Act on,已修复(a6527974)

[close-all-done-button.tsx:169] formatBatchCloseError 只认 HTTPError/TypeError/TimeoutError;ky 的 .json() 在 2xx 响应体被截断或不是 JSON 时抛 SyntaxError……catch 覆盖了请求成功后的分组与 toast 代码,那里抛出的 TypeError 也会被报成「请求超时」。建议:区分「请求未发出」与「响应未解析」,把 2xx 解析失败归入 REQUEST_UNCONFIRMED_COPY,并把 catch 收窄到 mutateAsync。

两点都采纳:

  • formatBatchCloseError 新增 SyntaxError 分支 →「请求超时:部分问题可能已关闭,请刷新看板确认结果」(2xx 已到达服务端、关闭可能已生效,宁可多提示刷新)。
  • handleClose 的 catch 收窄到 mutateAsync 本身;分组与 toast 代码移出 try,内部错误不再被误报成超时语义。

补 SyntaxError 用例;make lint / make test 在 a6527974 全绿。

**R4 处置(行内 P2,归因误报):Act on,已修复(a6527974)** > [close-all-done-button.tsx:169] formatBatchCloseError 只认 HTTPError/TypeError/TimeoutError;ky 的 .json() 在 2xx 响应体被截断或不是 JSON 时抛 SyntaxError……catch 覆盖了请求成功后的分组与 toast 代码,那里抛出的 TypeError 也会被报成「请求超时」。建议:区分「请求未发出」与「响应未解析」,把 2xx 解析失败归入 REQUEST_UNCONFIRMED_COPY,并把 catch 收窄到 mutateAsync。 两点都采纳: - `formatBatchCloseError` 新增 SyntaxError 分支 →「请求超时:部分问题可能已关闭,请刷新看板确认结果」(2xx 已到达服务端、关闭可能已生效,宁可多提示刷新)。 - `handleClose` 的 catch 收窄到 `mutateAsync` 本身;分组与 toast 代码移出 try,内部错误不再被误报成超时语义。 补 SyntaxError 用例;`make lint` / `make test` 在 a6527974 全绿。 <!-- pr-review-loop:reply -->
fastship-bot left a comment

本轮只剩两处不影响主路径正确性的边界问题:限流熔断与整批截止叠加时会把「未开始」失败原因错标成限流,以及 failures 允许超 50 后 OpenAPI 仍写「最多前 50 条」。两者都不改变关闭结果与落库数据,主要影响失败原因的归因准确性与接口文档一致性。

本轮只剩两处不影响主路径正确性的边界问题:限流熔断与整批截止叠加时会把「未开始」失败原因错标成限流,以及 failures 允许超 50 后 OpenAPI 仍写「最多前 50 条」。两者都不改变关闭结果与落库数据,主要影响失败原因的归因准确性与接口文档一致性。 <!-- fast-ship-review-run:a099be7f-3945-4682-9b9c-2a6710b72046 -->
@ -295,3 +392,3 @@
for res := range results {
if res.err != nil {
appendBatchCloseFailure(resp, res.issue, res.err)
appendBatchCloseFailure(resp, res.issue, classifyGitHubCloseFailure(res.err, &rateLimited))
Collaborator

P2:问题:失败归因在 drain 阶段用「当下」的 rateLimited 快照判定,而错误本身产生于派发阶段。派发循环里 Acquire(dispatchCtx) 失败且当时 rateLimited=false 的项会带着裸 ctx err 入队(第 368-372 行),到第 394 行统一过 classifyGitHubCloseFailure 时,若之后某个在途 PATCH 才命中限流,rateLimited 已翻成 true,这些项就被改写成「GitHub API 限流,未尝试关闭」。典型场景:30s 整批截止先落地(剩 25 项入队 DeadlineExceeded),随后 29s 启动的在途 PATCH 返回 403 限流,25 项文案全部变成限流。影响:同一批次的失败归因随 goroutine 时序变化,用户被引导去「稍后重试限流」,而真实原因是整批超时/调用方取消;与 PR 目标「文案分两类、归因不串」相反。建议:在入队时就把归因定下来(对 Acquire/worker 守卫的 ctx err 当场按当时的 rateLimited 转成固定文案,或随结果带上 errAtRateLimit/errAtDeadline 标记),drain 阶段只做格式化,不再重新读全局状态。

P2:问题:失败归因在 drain 阶段用「当下」的 rateLimited 快照判定,而错误本身产生于派发阶段。派发循环里 Acquire(dispatchCtx) 失败且当时 rateLimited=false 的项会带着裸 ctx err 入队(第 368-372 行),到第 394 行统一过 classifyGitHubCloseFailure 时,若之后某个在途 PATCH 才命中限流,rateLimited 已翻成 true,这些项就被改写成「GitHub API 限流,未尝试关闭」。典型场景:30s 整批截止先落地(剩 25 项入队 DeadlineExceeded),随后 29s 启动的在途 PATCH 返回 403 限流,25 项文案全部变成限流。影响:同一批次的失败归因随 goroutine 时序变化,用户被引导去「稍后重试限流」,而真实原因是整批超时/调用方取消;与 PR 目标「文案分两类、归因不串」相反。建议:在入队时就把归因定下来(对 Acquire/worker 守卫的 ctx err 当场按当时的 rateLimited 转成固定文案,或随结果带上 errAtRateLimit/errAtDeadline 标记),drain 阶段只做格式化,不再重新读全局状态。
R5 两条 P2:

1. 失败归因竞态(issue_list.go):drain 阶段重读「当下」rateLimited 标志,
   会把派发阶段已定性的 ctx err 随 goroutine 时序改写成限流。改为入队时
   当场定死:batchCloseCancelErr 按错误产生时的熔断状态归类(熔断→限流未
   尝试,否则原始 ctx err),drain 只做格式化;删除 classifyGitHubClose
   Failure。closeOneGitHubIssue 增加 rateLimited 参数。
2. 契约描述对齐(openapi.yaml):failures 允许超上限后,描述改为「按失败
   原因保留明细:通常最多前 50 条;达到上限后每类新文案各保留一条样例,
   总数可能略超 50」,并重跑三份生成物(server types.gen.go / web api.gen
   .ts / skills fast-ship api.md)同步。

测试:batchCloseCancelErr 归因当场性用例(未熔断保留原文案/熔断归限流/
nil 等价未熔断);-race ×2 与全量 make lint / make test / api-types --check
在最新提交全绿。
Author
Owner

R5 处置(行内 P2,归因快照竞态):Act on,已修复(f36ccecd)

[issue_list.go:394] 失败归因在 drain 阶段用「当下」的 rateLimited 快照判定,而错误本身产生于派发阶段……建议:在入队时就把归因定下来……drain 阶段只做格式化,不再重新读全局状态。

完全采纳你建议的形态:归因当场定死。

  • 新增 batchCloseCancelErr(err, rateLimited):派发阶段产生 ctx err 的两处(Acquire 失败、worker 守卫)按当时的熔断状态当场转换——熔断已触发→「GitHub API 限流,未尝试关闭」,否则保留原始 ctx err;结果入队后 drain 只做格式化,删除 classifyGitHubCloseFailure 的 drain 期重读。
  • 典型场景(整批截止先落地、在途 PATCH 之后才命中限流)下,25 项保持「请求已取消或超时,未继续关闭」,不再被时序改写成限流。
  • closeOneGitHubIssue 增加 rateLimited 参数(守卫归因同样当场定死)。

测试:TestBatchCloseCancelErr_AttributedAtProductionTime(未熔断保留原文案、熔断归限流、nil 等价未熔断);既有全部熔断/取消/并发用例 -race ×2 复跑通过,make lint / make test 在 f36ccecd 全绿。

**R5 处置(行内 P2,归因快照竞态):Act on,已修复(f36ccecd)** > [issue_list.go:394] 失败归因在 drain 阶段用「当下」的 rateLimited 快照判定,而错误本身产生于派发阶段……建议:在入队时就把归因定下来……drain 阶段只做格式化,不再重新读全局状态。 完全采纳你建议的形态:归因当场定死。 - 新增 `batchCloseCancelErr(err, rateLimited)`:派发阶段产生 ctx err 的两处(Acquire 失败、worker 守卫)按**当时**的熔断状态当场转换——熔断已触发→「GitHub API 限流,未尝试关闭」,否则保留原始 ctx err;结果入队后 drain 只做格式化,删除 `classifyGitHubCloseFailure` 的 drain 期重读。 - 典型场景(整批截止先落地、在途 PATCH 之后才命中限流)下,25 项保持「请求已取消或超时,未继续关闭」,不再被时序改写成限流。 - `closeOneGitHubIssue` 增加 rateLimited 参数(守卫归因同样当场定死)。 测试:`TestBatchCloseCancelErr_AttributedAtProductionTime`(未熔断保留原文案、熔断归限流、nil 等价未熔断);既有全部熔断/取消/并发用例 `-race ×2` 复跑通过,`make lint` / `make test` 在 f36ccecd 全绿。 <!-- pr-review-loop:reply -->
Author
Owner

R5 处置(P2,契约描述与行为不符):Act on,已修复(f36ccecd)

[server/api/openapi.yaml:11486] appendBatchCloseFailure 现在允许 failures 在 50 条上限后为「每类新文案各补一条样例」而超上限……契约描述仍写「最多返回前 50 条失败明细」……

采纳。openapi.yaml 的 IssueBatchResult.failures 描述改为:「按失败原因保留明细:通常最多前 50 条;达到上限后每类新文案各保留一条样例,总数可能略超 50」,并重跑全部三份生成物同步(server/internal/api/types.gen.go(make api-types)、web/src/types/api.gen.ts(pnpm api-types)、skills/fast-ship/references/api.md(make api-docs)),pnpm api-types --check 通过。容量假设类调用方现可按描述正确理解超限数组。

**R5 处置(P2,契约描述与行为不符):Act on,已修复(f36ccecd)** > [server/api/openapi.yaml:11486] appendBatchCloseFailure 现在允许 failures 在 50 条上限后为「每类新文案各补一条样例」而超上限……契约描述仍写「最多返回前 50 条失败明细」…… 采纳。`openapi.yaml` 的 `IssueBatchResult.failures` 描述改为:「按失败原因保留明细:通常最多前 50 条;达到上限后每类新文案各保留一条样例,总数可能略超 50」,并重跑全部三份生成物同步(`server/internal/api/types.gen.go`(make api-types)、`web/src/types/api.gen.ts`(pnpm api-types)、`skills/fast-ship/references/api.md`(make api-docs)),`pnpm api-types --check` 通过。容量假设类调用方现可按描述正确理解超限数组。 <!-- pr-review-loop:reply -->
fastship-bot left a comment

本轮未发现 P0/P1;行级命中、在途 PATCH 解耦与归因即时定死的主逻辑成立,但内部关闭的命中判定仍只认 state、看板按文案分组的计数在明细被截断时失真。两者都会让「关闭全部已完成」的结果对用户产生误导。

本轮未发现 P0/P1;行级命中、在途 PATCH 解耦与归因即时定死的主逻辑成立,但内部关闭的命中判定仍只认 state、看板按文案分组的计数在明细被截断时失真。两者都会让「关闭全部已完成」的结果对用户产生误导。 <!-- fast-ship-review-run:79dedc13-970e-44e0-a961-6e2de4348785 -->
@ -70,0 +64,4 @@
closed := make([]string, 0, len(ids))
for _, id := range ids {
res := tx.Model(&model.Issue{}).
Where("id = ? AND source = ? AND state = ?", id, model.IssueSourceInternal, model.IssueStateOpen).
Collaborator

P2:问题:BatchCloseInternalTx 的命中条件只有 id/source/state=open,而待关闭清单是按 issues.state=open 且 issue_internal_meta.workflow_status=done 取的;两条语句之间被看板拖回进行中的行只改 workflow_status、state 仍为 open,仍会被这条 UPDATE 命中。影响:用户刚落回「进行中」的问题被「关闭全部已完成」静默关闭,计为 succeeded,并在同一事务里删掉推荐与依赖(不可撤销),与 PR 声称的「行级命中」不符。建议:把 workflow_status='done' 一并放进 WHERE(例如 EXISTS 子查询 issue_internal_meta),未命中的行走既有 errBatchCloseSkippedNotOpen 失败分支。

P2:问题:BatchCloseInternalTx 的命中条件只有 id/source/state=open,而待关闭清单是按 issues.state=open 且 issue_internal_meta.workflow_status=done 取的;两条语句之间被看板拖回进行中的行只改 workflow_status、state 仍为 open,仍会被这条 UPDATE 命中。影响:用户刚落回「进行中」的问题被「关闭全部已完成」静默关闭,计为 succeeded,并在同一事务里删掉推荐与依赖(不可撤销),与 PR 声称的「行级命中」不符。建议:把 workflow_status='done' 一并放进 WHERE(例如 EXISTS 子查询 issue_internal_meta),未命中的行走既有 errBatchCloseSkippedNotOpen 失败分支。
@ -28,0 +41,4 @@
for (const f of result.failures) {
const label = f.error || "原因未说明";
const group = groups.get(label) ?? { count: 0 };
group.count += 1;
Collaborator

P2:问题:分组计数只遍历服务端返回的 failures 明细(最多 50 条,超限后每类仅补一条样例),×N 实际是「已列条数」而不是该类真实条数。影响:明细被截断时计数严重低估,例如 120 条 404 + 80 条结果未确认只会展示「更新 GitHub Issue 失败:404 Not Found ×50」「关闭结果未确认… ×1」加「另有 149 条未列明细」,用户会把 79 条未确认当成 1 条,正是本 PR 要保住的「刷新确认」指引被弱化;单测里 200 条未尝试关闭也断言成 ×1 即为该失真。建议:服务端在 IssueBatchResult 里补每类失败的真实计数(或在 failures 之外返回完整分组统计),前端按真实计数展示,样例引用仍取首条。

P2:问题:分组计数只遍历服务端返回的 failures 明细(最多 50 条,超限后每类仅补一条样例),×N 实际是「已列条数」而不是该类真实条数。影响:明细被截断时计数严重低估,例如 120 条 404 + 80 条结果未确认只会展示「更新 GitHub Issue 失败:404 Not Found ×50」「关闭结果未确认… ×1」加「另有 149 条未列明细」,用户会把 79 条未确认当成 1 条,正是本 PR 要保住的「刷新确认」指引被弱化;单测里 200 条未尝试关闭也断言成 ×1 即为该失真。建议:服务端在 IssueBatchResult 里补每类失败的真实计数(或在 failures 之外返回完整分组统计),前端按真实计数展示,样例引用仍取首条。
R6 两条 P2:

1. 服务端(repository/issue.go):BatchCloseInternalTx 的 WHERE 原来只有
   id/source/state=open,而待关闭清单是 state=open + workflow_status=done
   (后者在 issue_internal_meta 表)——两条语句之间被看板拖回开发中的行只
   改 workflow、不改 state,会被照样关闭。WHERE 经子查询钉住同条件:
   id IN (SELECT issue_id FROM issue_internal_meta WHERE workflow_status=
   'done')。repo 测试补 meta 种子与拖回行断言(open/in_progress 行不命中、
   状态不动)。
2. 前端(close-all-done-button.tsx):分组 ×N 是已列明细的计数(下界),
   明细被截断时严重低估(如 50/204)。截断时不展示计数,只保留类别与样例
   (「<原因>(如 INT-x)」),差额仍由「另有 X 条未列明细」标注;明细完整
   时照常展示 ×N。

测试:repo 拖回行用例(不命中、状态不动);前端截断场景断言(无 ×N、四
类全可见、差额标注);-race ×3 与全量 make lint / make test / api-types
全绿。
Author
Owner

R6 处置(行内 P2,workflow 未钉住):Act on,已修复(d48f2ea4)

[repository/issue.go] BatchCloseInternalTx 的命中条件只有 id/source/state=open,而待关闭清单是按 issues.state=open 且 issue_internal_meta.workflow_status=done 取的;两条语句之间被看板拖回进行中的行只改 workflow_status、state 仍为 open,仍会被这条 UPDATE 命中……

属实且比 R1 的 state 命中问题更深一层(同根:tx 条件弱于清单条件)。已修:WHERE 经子查询钉住同条件——id IN (SELECT issue_id FROM issue_internal_meta WHERE workflow_status = 'done')(workflow_status 在 issue_internal_meta 表,经子查询关联而非同表列)。被拖回开发中的行不再命中,状态保持 open/in_progress 不动,并按未命中记入 failures(「该问题已在其他操作中被更新,已跳过关闭」)。

测试:repo 层补 meta 种子与拖回行断言(不命中、issue 与 meta 状态均不动);服务层既有跳过/命中用例随种子调整复跑;-race ×3、make lint / make test 在 d48f2ea4 全绿。

**R6 处置(行内 P2,workflow 未钉住):Act on,已修复(d48f2ea4)** > [repository/issue.go] BatchCloseInternalTx 的命中条件只有 id/source/state=open,而待关闭清单是按 issues.state=open 且 issue_internal_meta.workflow_status=done 取的;两条语句之间被看板拖回进行中的行只改 workflow_status、state 仍为 open,仍会被这条 UPDATE 命中…… 属实且比 R1 的 state 命中问题更深一层(同根:tx 条件弱于清单条件)。已修:WHERE 经子查询钉住同条件——`id IN (SELECT issue_id FROM issue_internal_meta WHERE workflow_status = 'done')`(workflow_status 在 issue_internal_meta 表,经子查询关联而非同表列)。被拖回开发中的行不再命中,状态保持 open/in_progress 不动,并按未命中记入 failures(「该问题已在其他操作中被更新,已跳过关闭」)。 测试:repo 层补 meta 种子与拖回行断言(不命中、issue 与 meta 状态均不动);服务层既有跳过/命中用例随种子调整复跑;`-race ×3`、`make lint` / `make test` 在 d48f2ea4 全绿。 <!-- pr-review-loop:reply -->
Author
Owner

R6 处置(行内 P2,分组计数低估):Act on,已修复(d48f2ea4)

[close-all-done-button.tsx] 分组计数只遍历服务端返回的 failures 明细(最多 50 条,超限后每类仅补一条样例),×N 实际是「已列条数」而不是该类真实条数……

采纳。明细被截断时不展示 ×N 计数——那只是已列明细的下界(如 50/204),展示出来必然误导;截断时每组只保留「<原因>(如 <样例引用>)」,总量由标题 failed 携带、差额由「另有 X 条未列明细」标注;明细完整(failed == failures.length)时照常展示 ×N。测试更新:截断场景断言无 ×N 且四类全可见 + 差额标注;非截断场景 ×N 保留。

**R6 处置(行内 P2,分组计数低估):Act on,已修复(d48f2ea4)** > [close-all-done-button.tsx] 分组计数只遍历服务端返回的 failures 明细(最多 50 条,超限后每类仅补一条样例),×N 实际是「已列条数」而不是该类真实条数…… 采纳。**明细被截断时不展示 ×N 计数**——那只是已列明细的下界(如 50/204),展示出来必然误导;截断时每组只保留「<原因>(如 <样例引用>)」,总量由标题 failed 携带、差额由「另有 X 条未列明细」标注;明细完整(failed == failures.length)时照常展示 ×N。测试更新:截断场景断言无 ×N 且四类全可见 + 差额标注;非截断场景 ×N 保留。 <!-- pr-review-loop:reply -->
fastship-bot left a comment

本轮 head d48f2ea4 复核行级命中、在途 PATCH 与超时预算改动:R6 两条已修复,新发现 GitHub 关闭分支仍缺行级复核,以及共享失败明细契约描述与 internal-meta 端点实现不符。影响:批量关闭进行中被拖回「进行中」的 GitHub 问题仍会被远端关闭并删推荐,且 PUT /api/issues/internal-meta 的 failures 说明会误导调用方。

本轮 head d48f2ea4 复核行级命中、在途 PATCH 与超时预算改动:R6 两条已修复,新发现 GitHub 关闭分支仍缺行级复核,以及共享失败明细契约描述与 internal-meta 端点实现不符。影响:批量关闭进行中被拖回「进行中」的 GitHub 问题仍会被远端关闭并删推荐,且 PUT /api/issues/internal-meta 的 failures 说明会误导调用方。 <!-- fast-ship-review-run:944b3dac-aa60-48e9-b378-e339a82f2733 -->
@ -11484,3 +11484,3 @@
items:
$ref: '#/components/schemas/IssueBatchFailure'
description: 最多返回前 50 条失败明细
description: 按失败原因保留明细:通常最多前 50 条;达到上限后每类新文案各保留一条样例,总数可能略超 50
Collaborator

P2:问题:IssueBatchResult.failures 的描述改成「达到上限后每类新文案各保留一条样例」,但该 schema 同时被 PUT /api/issues/internal-meta 使用,而 BatchUpdateInternalMeta(server/internal/service/issue_meta.go:165-174)仍是「只收前 50 条、没有每类样例」。影响:生成的 types.gen.go / api.gen.ts / skills/fast-ship/references/api.md 都带上这句,调用方(含 agent)会以为 internal-meta 端点截断后仍能拿到全部失败类别与差额,实际后出现的类别会被静默丢弃。建议:要么让 BatchUpdateInternalMeta 复用 appendBatchCloseFailure 的每类样例逻辑,要么把描述收回到 batch-close 端点(端点级描述或拆 schema),并重跑三份生成物。

P2:问题:IssueBatchResult.failures 的描述改成「达到上限后每类新文案各保留一条样例」,但该 schema 同时被 PUT /api/issues/internal-meta 使用,而 BatchUpdateInternalMeta(server/internal/service/issue_meta.go:165-174)仍是「只收前 50 条、没有每类样例」。影响:生成的 types.gen.go / api.gen.ts / skills/fast-ship/references/api.md 都带上这句,调用方(含 agent)会以为 internal-meta 端点截断后仍能拿到全部失败类别与差额,实际后出现的类别会被静默丢弃。建议:要么让 BatchUpdateInternalMeta 复用 appendBatchCloseFailure 的每类样例逻辑,要么把描述收回到 batch-close 端点(端点级描述或拆 schema),并重跑三份生成物。
@ -197,0 +224,4 @@
}
patchCtx, cancel := context.WithTimeout(context.WithoutCancel(batchCtx), batchCloseGitHubPatchTimeout)
defer cancel()
updated, err := client.UpdateIssue(patchCtx, issue.GitHubMeta.Number, updateReq)
Collaborator

P2:问题:closeOneGitHubIssue 发 PATCH 前只判 GitHubMeta 与批次取消,从不复核本地行是否仍为 issues.state=open 且 issue_internal_meta.workflow_status=done;内部来源已在 BatchCloseInternalTx 的 WHERE 里钉住这两个条件,GitHub 来源没有对应动作。影响:批量关闭最长 30s、最多 200 条的窗口内,用户(或并发 agent)把某个 GitHub 问题从「已完成」拖回「进行中」后,该问题的 PATCH 仍会发出,远端与本地 state 被置 closed、upsert 还会删除推荐并计为 succeeded,最终留下 state=closed 而 workflow=in_progress 的不一致行——正是 R6 认定必须避免的「拖回仍被误关」,且这条分支的窗口比内部事务长得多。建议:在 worker 拿到槽位后、发 PATCH 前按 issue.ID 复核 state=open && workflow_status=done(条件查询或条件读取本地行),不满足则按「该问题已在其他操作中被更新,已跳过关闭」计入 failures 并不发 PATCH。

P2:问题:closeOneGitHubIssue 发 PATCH 前只判 GitHubMeta 与批次取消,从不复核本地行是否仍为 issues.state=open 且 issue_internal_meta.workflow_status=done;内部来源已在 BatchCloseInternalTx 的 WHERE 里钉住这两个条件,GitHub 来源没有对应动作。影响:批量关闭最长 30s、最多 200 条的窗口内,用户(或并发 agent)把某个 GitHub 问题从「已完成」拖回「进行中」后,该问题的 PATCH 仍会发出,远端与本地 state 被置 closed、upsert 还会删除推荐并计为 succeeded,最终留下 state=closed 而 workflow=in_progress 的不一致行——正是 R6 认定必须避免的「拖回仍被误关」,且这条分支的窗口比内部事务长得多。建议:在 worker 拿到槽位后、发 PATCH 前按 issue.ID 复核 state=open && workflow_status=done(条件查询或条件读取本地行),不满足则按「该问题已在其他操作中被更新,已跳过关闭」计入 failures 并不发 PATCH。
R7 两条 P2:

1. closeOneGitHubIssue 发 PATCH 前从不复核本地行状态:批量最长 30s 的窗口
   内被看板拖回「进行中」的 GitHub 行(state 仍 open、workflow 已变)仍
   会被发 PATCH,远端与本地置 closed、upsert 删推荐并计 succeeded,留下
   closed+in_progress 不一致行——正是 R6 在内部来源关掉的洞。新增
   IssueRepository.IsOpenAndDone(id + state=open + meta.workflow=done 联
   查),经回调注入 closeOneGitHubIssue:worker 拿到槽位后、发 PATCH 前
   复核,不过按「已跳过关闭」回报,守卫出错也不发 PATCH。服务测试补三态
   (不过→不发 PATCH 回报跳过;出错→不发 PATCH 回报内部文案;通过→
   照常 PATCH)。
2. IssueBatchResult.failures 描述说「上限后每类新文案各保留一条样例」,但
   该 schema 同时被 PUT /api/issues/internal-meta 使用,而其实现仍是
   「只收前 50 条」——生成物会误导调用方。抽出 appendBatchFailureEntry
   (超上限后新类别样例仍保留)让两个端点共用,语义与共享契约一致;
   补 internal-meta 端点的截断样例测试(51 条同类+1 条新类别→52 条明细)。
Author
Owner

R7 处置(行内 P2,GitHub 分支缺行级复核):Act on,已修复(6e2731ee)

[issue_list.go] closeOneGitHubIssue 发 PATCH 前只判 GitHubMeta 与批次取消,从不复核本地行是否仍为 state=open 且 workflow_status=done……

属实,与 R6 内部来源同根。已修:新增 IssueRepository.IsOpenAndDone(id + issues.state=open + JOIN issue_internal_meta workflow=done 计数),经回调注入 closeOneGitHubIssue——worker 拿到槽位后、发 PATCH 前按 id 复核:不满足按「该问题已在其他操作中被更新,已跳过关闭」计入 failures、不发 PATCH;复核本身出错也不发 PATCH(回报「复核本地状态失败,未关闭该项」)。测试补三态(被拖回→跳过;出错→不发 PATCH;通过→照常 PATCH),-race ×3 与全量 lint/test 在 6e2731ee 全绿。

**R7 处置(行内 P2,GitHub 分支缺行级复核):Act on,已修复(6e2731ee)** > [issue_list.go] closeOneGitHubIssue 发 PATCH 前只判 GitHubMeta 与批次取消,从不复核本地行是否仍为 state=open 且 workflow_status=done…… 属实,与 R6 内部来源同根。已修:新增 `IssueRepository.IsOpenAndDone`(`id + issues.state=open + JOIN issue_internal_meta workflow=done` 计数),经回调注入 `closeOneGitHubIssue`——worker 拿到槽位后、发 PATCH 前按 id 复核:不满足按「该问题已在其他操作中被更新,已跳过关闭」计入 failures、不发 PATCH;复核本身出错也不发 PATCH(回报「复核本地状态失败,未关闭该项」)。测试补三态(被拖回→跳过;出错→不发 PATCH;通过→照常 PATCH),`-race ×3` 与全量 lint/test 在 6e2731ee 全绿。 <!-- pr-review-loop:reply -->
Author
Owner

R7 处置(行内 P2,共享契约描述与实现不符):Act on,已修复(6e2731ee)

[openapi.yaml] IssueBatchResult.failures 描述改了,但 schema 同时被 PUT /api/issues/internal-meta 使用,BatchUpdateInternalMeta 仍是「只收前 50 条、没有每类样例」……

采纳「让实现对齐契约」方向:抽出 appendBatchFailureEntry(达到上限后每类新文案各保留一条样例),批量关闭与 BatchUpdateInternalMeta 共用——schema 描述对两个端点都成立,不用拆 schema 或收回描述。补了 internal-meta 端点的截断测试(50 条同类 + 1 条新类别 → 51 条明细、新类别样例在列)。

**R7 处置(行内 P2,共享契约描述与实现不符):Act on,已修复(6e2731ee)** > [openapi.yaml] IssueBatchResult.failures 描述改了,但 schema 同时被 PUT /api/issues/internal-meta 使用,BatchUpdateInternalMeta 仍是「只收前 50 条、没有每类样例」…… 采纳「让实现对齐契约」方向:抽出 `appendBatchFailureEntry`(达到上限后每类新文案各保留一条样例),批量关闭与 `BatchUpdateInternalMeta` 共用——schema 描述对两个端点都成立,不用拆 schema 或收回描述。补了 internal-meta 端点的截断测试(50 条同类 + 1 条新类别 → 51 条明细、新类别样例在列)。 <!-- pr-review-loop:reply -->
fastship-bot left a comment

本轮新发现两处边界问题:内部批量关闭事务内的 SQL 结构让每条 UPDATE 重扫整张 issue_internal_meta,GitHub 行级守卫不带 context 且返回后不再复核取消状态。二者在大库或连接竞争下会放大关闭窗口,可能把整批内部关闭回滚成超时失败,并让已取消/超时的批次继续发出新的 PATCH。

本轮新发现两处边界问题:内部批量关闭事务内的 SQL 结构让每条 UPDATE 重扫整张 issue_internal_meta,GitHub 行级守卫不带 context 且返回后不再复核取消状态。二者在大库或连接竞争下会放大关闭窗口,可能把整批内部关闭回滚成超时失败,并让已取消/超时的批次继续发出新的 PATCH。 <!-- fast-ship-review-run:b8fd2511-4a0c-4136-a8b7-7c5a1b934a88 -->
@ -70,0 +68,4 @@
closed := make([]string, 0, len(ids))
for _, id := range ids {
res := tx.Model(&model.Issue{}).
Where("id = ? AND source = ? AND state = ? AND id IN (SELECT issue_id FROM issue_internal_meta WHERE workflow_status = ?)",
Collaborator

P2:问题:id IN (SELECT issue_id FROM issue_internal_meta WHERE workflow_status = ?) 与同句的 id = ? 无关联,子查询不随主键收窄(SQLite 计划为 LIST SUBQUERY + SCAN issue_internal_meta,且 workflow_status 无索引),而它在事务里按 id 逐条执行,最多 200 次、每次重扫整张 meta 表。影响:语句数从旧实现的 1 条变成 200 条并各带一次全表扫描,生产 MaxOpenConns=1,这段时间独占唯一连接、阻塞全站其他请求;扫描量随历史 issue 总量线性增长,一旦吃掉 30s 预算,整个内部事务回滚并给用户回报「请求已取消或超时,未继续关闭」,把本可成功的关闭变成全批失败。建议:改成关联 EXISTS(EXISTS (SELECT 1 FROM issue_internal_meta WHERE issue_id = issues.id AND workflow_status = ?))走 issue_id 主键,或保留单条 id IN (...) 批量 UPDATE 后在事务内查询命中集合。

P2:问题:`id IN (SELECT issue_id FROM issue_internal_meta WHERE workflow_status = ?)` 与同句的 `id = ?` 无关联,子查询不随主键收窄(SQLite 计划为 LIST SUBQUERY + SCAN issue_internal_meta,且 workflow_status 无索引),而它在事务里按 id 逐条执行,最多 200 次、每次重扫整张 meta 表。影响:语句数从旧实现的 1 条变成 200 条并各带一次全表扫描,生产 MaxOpenConns=1,这段时间独占唯一连接、阻塞全站其他请求;扫描量随历史 issue 总量线性增长,一旦吃掉 30s 预算,整个内部事务回滚并给用户回报「请求已取消或超时,未继续关闭」,把本可成功的关闭变成全批失败。建议:改成关联 EXISTS(`EXISTS (SELECT 1 FROM issue_internal_meta WHERE issue_id = issues.id AND workflow_status = ?)`)走 issue_id 主键,或保留单条 `id IN (...)` 批量 UPDATE 后在事务内查询命中集合。
@ -70,0 +89,4 @@
// IsOpenAndDone GitHub 批量关闭的行级守卫(INT-185 R7):PATCH 前按 id 复核
// 本地行仍为 state=open 且 meta.workflow_status=done——批量窗口内被拖回开发
// 中的行不发 PATCH、按「已跳过关闭」计入 failures,避免远端/本地被误关。
func (r *IssueRepository) IsOpenAndDone(id string) (bool, error) {
Collaborator

P2:问题:IsOpenAndDone 直接用 r.db(无 ctx),在 closeOneGitHubIssue 中位于 batchCtx.Err() 判定之后、PATCH 之前,返回后不再复核批次状态。影响:批次已超时/被用户取消时,只要 worker 还卡在这条无上下文的 DB 查询上(单连接池可能排在别的长事务后),查询一返回仍会发出新的 PATCH,与「取消后不再开始新 PATCH」的保证不符;这条查询也不受 15s PATCH 上限或 30s 整批预算约束,可能把请求拖过前端 60s 超时。建议:守卫改用带 ctx 的句柄,并在守卫通过后再判一次 batchCtx.Err(),已取消/超时则按「未开始」计数、不发 PATCH。

P2:问题:`IsOpenAndDone` 直接用 `r.db`(无 ctx),在 `closeOneGitHubIssue` 中位于 `batchCtx.Err()` 判定之后、PATCH 之前,返回后不再复核批次状态。影响:批次已超时/被用户取消时,只要 worker 还卡在这条无上下文的 DB 查询上(单连接池可能排在别的长事务后),查询一返回仍会发出新的 PATCH,与「取消后不再开始新 PATCH」的保证不符;这条查询也不受 15s PATCH 上限或 30s 整批预算约束,可能把请求拖过前端 60s 超时。建议:守卫改用带 ctx 的句柄,并在守卫通过后再判一次 `batchCtx.Err()`,已取消/超时则按「未开始」计数、不发 PATCH。
R8 两条 P2,都是性能/时序边界:

1. BatchCloseInternalTx 的 workflow 条件写成 `id IN (SELECT issue_id FROM
   issue_internal_meta WHERE workflow_status=?)`——子查询与主键无关、不随
   id 收窄,SQLite 计划为 LIST SUBQUERY + 全表 SCAN,且按 id 逐条最多 200
   次、每次重扫整张 meta 表;生产 MaxOpenConns=1,这段时间独占唯一连接
   阻塞全站,吃掉 30s 预算还会把整个内部事务回滚成全批失败。改为关联
   EXISTS(`EXISTS (SELECT 1 FROM issue_internal_meta WHERE issue_id =
   issues.id AND workflow_status=?)`)走 issue_id 主键。
2. IsOpenAndDone 用无 ctx 的 r.db,且在 batchCtx.Err() 判定之后、PATCH 之
   前、返回后不再复核——worker 卡在无上下文 DB 查询上时批次已取消/超时,
   查询一返回仍会发新 PATCH,与「取消后不再开始新 PATCH」不符,还可能把
   请求拖过前端超时。守卫改带 ctx(WithContext,单连接池排队也能被批次
   取消打断),并在守卫通过后再判一次 batchCtx.Err(),已取消/超时按
   「未开始」计数不发 PATCH。测试补守卫通过但批次已取消→不发 PATCH。

-race ×3 与全量 lint/test/api-types 全绿。
Author
Owner

R8 处置(行内 P2,子查询全表扫描):Act on,已修复(ab3ebfce)

[issue.go] id IN (SELECT issue_id FROM issue_internal_meta WHERE workflow_status=?) 与同句的 id = ? 无关联,子查询不随主键收窄(SQLite 计划为 LIST SUBQUERY + SCAN issue_internal_meta)……

属实,是 R6 修复引入的性能回归。已改为关联 EXISTS (SELECT 1 FROM issue_internal_meta WHERE issue_id = issues.id AND workflow_status = ?)——走 issue_id 主键、逐 id 不重扫 meta 表,原有语义(state=open + workflow=done 命中才关闭)不变;repo 拖回行断言照常绿。

**R8 处置(行内 P2,子查询全表扫描):Act on,已修复(ab3ebfce)** > [issue.go] `id IN (SELECT issue_id FROM issue_internal_meta WHERE workflow_status=?)` 与同句的 `id = ?` 无关联,子查询不随主键收窄(SQLite 计划为 LIST SUBQUERY + SCAN issue_internal_meta)…… 属实,是 R6 修复引入的性能回归。已改为关联 `EXISTS (SELECT 1 FROM issue_internal_meta WHERE issue_id = issues.id AND workflow_status = ?)`——走 issue_id 主键、逐 id 不重扫 meta 表,原有语义(state=open + workflow=done 命中才关闭)不变;repo 拖回行断言照常绿。 <!-- pr-review-loop:reply -->
Author
Owner

R8 处置(行内 P2,守卫无 ctx 且不再复核取消):Act on,已修复(ab3ebfce)

[issue.go] IsOpenAndDone 直接用 r.db(无 ctx),在 batchCtx.Err() 判定之后、PATCH 之前,返回后不再复核批次状态……

属实,是 R7 守卫自身留下的时序缝。已修:IsOpenAndDone 改带 ctx(WithContext,单连接池排队也能被批次取消打断,不会拖过整批预算),并在守卫通过后、发 PATCH 前再判一次 batchCtx.Err()——期间已取消/超时按「未开始」计数、不发 PATCH,与「取消后不再开始新 PATCH」的保证对齐。测试补了「守卫通过但批次已取消→不发 PATCH」的用例。

**R8 处置(行内 P2,守卫无 ctx 且不再复核取消):Act on,已修复(ab3ebfce)** > [issue.go] IsOpenAndDone 直接用 r.db(无 ctx),在 batchCtx.Err() 判定之后、PATCH 之前,返回后不再复核批次状态…… 属实,是 R7 守卫自身留下的时序缝。已修:`IsOpenAndDone` 改带 ctx(`WithContext`,单连接池排队也能被批次取消打断,不会拖过整批预算),并在守卫通过后、发 PATCH 前再判一次 `batchCtx.Err()`——期间已取消/超时按「未开始」计数、不发 PATCH,与「取消后不再开始新 PATCH」的保证对齐。测试补了「守卫通过但批次已取消→不发 PATCH」的用例。 <!-- pr-review-loop:reply -->
fastship-bot left a comment

本轮未发现阻塞性问题,确认 R8 两条修复已生效;新发现两条 P2:守卫被批次取消打断时被误报为内部复核失败,以及 HTTP 5xx 带 JSON 正文时绕过了「结果未确认」的刷新提示。

本轮未发现阻塞性问题,确认 R8 两条修复已生效;新发现两条 P2:守卫被批次取消打断时被误报为内部复核失败,以及 HTTP 5xx 带 JSON 正文时绕过了「结果未确认」的刷新提示。 <!-- fast-ship-review-run:5c25a04d-a74a-4ca2-a772-9c5631361b26 -->
@ -197,0 +227,4 @@
// 在事务 WHERE 里钉住同条件,GitHub 来源没有事务可依托,只能靠这里。
if guard != nil {
ok, gerr := guard(batchCtx, issue.ID)
if gerr != nil {
Collaborator

P2:问题:closeOneGitHubIssue 的守卫分支把所有 gerr != nil 一律映射为 ErrInternal「复核本地状态失败,未关闭该项」,没有区分 ctx 取消/超时;而本轮 R8 刚把 IsOpenAndDone 改成带 batchCtx(repository/issue.go:97),限流熔断(dispatchCancel)或整批 30s 截止会让正在排队或执行中的守卫查询直接返回 context.Canceled / DeadlineExceeded。影响:MaxOpenConns=1 时守卫查询在唯一连接上串行排队,限流熔断或被取消的批次中这些项会被展示为「复核本地状态失败」这类内部故障类别,而不是「GitHub API 限流,未尝试关闭 / 请求已取消或超时」,与 PR 声明的失败文案分类不符,用户会误以为数据库或服务异常并重复重试。建议:guard 出错时先判 errors.Is(gerr, context.Canceled) / errors.Is(gerr, context.DeadlineExceeded),走 batchCloseCancelErr(gerr, rateLimited),其余才按内部复核失败回报;补一条「守卫在途时批次被取消/熔断」的用例(现有用例只覆盖守卫已通过后在 PATCH 期间取消)。

P2:问题:closeOneGitHubIssue 的守卫分支把所有 gerr != nil 一律映射为 ErrInternal「复核本地状态失败,未关闭该项」,没有区分 ctx 取消/超时;而本轮 R8 刚把 IsOpenAndDone 改成带 batchCtx(repository/issue.go:97),限流熔断(dispatchCancel)或整批 30s 截止会让正在排队或执行中的守卫查询直接返回 context.Canceled / DeadlineExceeded。影响:MaxOpenConns=1 时守卫查询在唯一连接上串行排队,限流熔断或被取消的批次中这些项会被展示为「复核本地状态失败」这类内部故障类别,而不是「GitHub API 限流,未尝试关闭 / 请求已取消或超时」,与 PR 声明的失败文案分类不符,用户会误以为数据库或服务异常并重复重试。建议:guard 出错时先判 errors.Is(gerr, context.Canceled) / errors.Is(gerr, context.DeadlineExceeded),走 batchCloseCancelErr(gerr, rateLimited),其余才按内部复核失败回报;补一条「守卫在途时批次被取消/熔断」的用例(现有用例只覆盖守卫已通过后在 PATCH 期间取消)。
@ -129,3 +173,4 @@
export async function formatBatchCloseError(error: unknown): Promise<string> {
if (error instanceof HTTPError) {
const body = await error.response.json<ApiResponse<unknown>>().catch(() => null);
if (body?.message) return body.message;
Collaborator

P2:问题:formatBatchCloseError 对 HTTPError 先取 body.message 就直接返回,只有 JSON 解析失败时才按 502/503/504 走刷新确认;因此任何带 JSON 正文的 5xx(应用自身 500「服务器内部错误」、网关返回 JSON 错误页)都会走具体文案分支。影响:与 PR 交付范围声明「整请求只要远端可能已生效(服务端 5xx/传输错误)一律提示刷新确认」不一致;若服务端在关闭了一部分后才失败(例如 drain 阶段异常返回 500),用户只看到「服务器内部错误」,不会去刷新确认已关闭的部分,随后可能重复操作。建议:先按 error.response.status >= 500 返回 REQUEST_UNCONFIRMED_COPY(可把 body.message 拼进文案保留细节),再处理 4xx 的 message;补一条「500 + JSON 正文」的用例,与现有 HTML 502 用例对齐。

P2:问题:formatBatchCloseError 对 HTTPError 先取 body.message 就直接返回,只有 JSON 解析失败时才按 502/503/504 走刷新确认;因此任何带 JSON 正文的 5xx(应用自身 500「服务器内部错误」、网关返回 JSON 错误页)都会走具体文案分支。影响:与 PR 交付范围声明「整请求只要远端可能已生效(服务端 5xx/传输错误)一律提示刷新确认」不一致;若服务端在关闭了一部分后才失败(例如 drain 阶段异常返回 500),用户只看到「服务器内部错误」,不会去刷新确认已关闭的部分,随后可能重复操作。建议:先按 error.response.status >= 500 返回 REQUEST_UNCONFIRMED_COPY(可把 body.message 拼进文案保留细节),再处理 4xx 的 message;补一条「500 + JSON 正文」的用例,与现有 HTML 502 用例对齐。
R9 两条 P2:

1. 守卫在途时批次被取消/熔断,守卫因带 batchCtx 返回 ctx err,却被一律
   映射为「复核本地状态失败」内部故障类别——用户会误以为数据库异常并
   重试。守卫出错先判 ctx.Canceled/DeadlineExceeded,走 batchCloseCancelErr
   按当时熔断状态归因(未熔断→ctx err 透传「未开始」;熔断中→限流文案),
   其余才报内部复核失败。补「守卫在途时批次取消→不发 PATCH 且归因正确」
   用例。
2. formatBatchCloseError 对 HTTPError 先取 body.message 就返回,只有 JSON
   解析失败才按网关状态码走刷新确认——任何带 JSON 正文的 5xx(服务端自
   身 500、网关 JSON 错误页)都被按具体文案展示,漏掉「可能已关闭一部分」
   的刷新提示。改为先按 status>=500 走刷新确认(body.message 作细节保留),
   再处理 4xx 的 message。补「500+JSON→刷新确认、4xx+message→具体文案」
   用例。
Author
Owner

R9 处置(行内 P2,守卫取消被误报为内部故障):Act on,已修复(edd78bbb)

[issue_list.go] 守卫分支把所有 gerr 一律映射为 ErrInternal「复核本地状态失败」,没有区分 ctx 取消/超时……

属实,R8 把守卫改成带 batchCtx 后引入的归因缝。已修:守卫出错先判 errors.Is(gerr, context.Canceled)/DeadlineExceeded,走 batchCloseCancelErr 按当时熔断状态归因(未熔断→ctx err 透传,归「未开始」;熔断中→限流文案),其余才报「复核本地状态失败」。补了「守卫在途时批次取消/熔断→不发 PATCH 且归因正确」的用例(现有用例只覆盖守卫已通过后在 PATCH 期间取消)。

**R9 处置(行内 P2,守卫取消被误报为内部故障):Act on,已修复(edd78bbb)** > [issue_list.go] 守卫分支把所有 gerr 一律映射为 ErrInternal「复核本地状态失败」,没有区分 ctx 取消/超时…… 属实,R8 把守卫改成带 batchCtx 后引入的归因缝。已修:守卫出错先判 `errors.Is(gerr, context.Canceled)`/`DeadlineExceeded`,走 `batchCloseCancelErr` 按当时熔断状态归因(未熔断→ctx err 透传,归「未开始」;熔断中→限流文案),其余才报「复核本地状态失败」。补了「守卫在途时批次取消/熔断→不发 PATCH 且归因正确」的用例(现有用例只覆盖守卫已通过后在 PATCH 期间取消)。 <!-- pr-review-loop:reply -->
Author
Owner

R9 处置(行内 P2,5xx 带 JSON 绕过刷新提示):Act on,已修复(edd78bbb)

[close-all-done-button.tsx] formatBatchCloseError 对 HTTPError 先取 body.message 就直接返回,只有 JSON 解析失败才按网关状态码走刷新确认……

属实,与「整请求只要远端可能已生效一律提示刷新确认」的交付范围冲突。已修:先按 status >= 500 返回刷新确认文案(body.message 作细节拼进文案保留),再处理 4xx 的 message——服务端自身 500 带 JSON 正文、网关 502/503/504 JSON 错误页都正确走刷新提示,不再被具体文案掩盖。补了「500+JSON→刷新确认(含细节)、4xx+message→具体文案」的用例。

**R9 处置(行内 P2,5xx 带 JSON 绕过刷新提示):Act on,已修复(edd78bbb)** > [close-all-done-button.tsx] formatBatchCloseError 对 HTTPError 先取 body.message 就直接返回,只有 JSON 解析失败才按网关状态码走刷新确认…… 属实,与「整请求只要远端可能已生效一律提示刷新确认」的交付范围冲突。已修:先按 `status >= 500` 返回刷新确认文案(`body.message` 作细节拼进文案保留),再处理 4xx 的 `message`——服务端自身 500 带 JSON 正文、网关 502/503/504 JSON 错误页都正确走刷新提示,不再被具体文案掩盖。补了「500+JSON→刷新确认(含细节)、4xx+message→具体文案」的用例。 <!-- pr-review-loop:reply -->
R10 一条 P2 + 一条 Note:

- P2(issue_list.go):PATCH 成功后本地落地失败(upsertGitHubIssue /
  markCollectionsStaleAfterPush 返回 error)仍按普通失败文案计入 failures,
  前端不会提示刷新确认——但 GitHub 侧已关闭、本地可能仍 open,用户看到
  「服务器内部错误」会以为没关成功并重复操作。两处落地失败统一包装为
  batchClosePatchUnconfirmedError,走「结果未确认:GitHub 侧可能已关闭,
  请刷新查看」。
- Note(issue_batch_close_test.go):R8/R9 守卫用例都在调用前就 cancel/
  deadline,函数在顶部 batchCtx.Err() 即返回——guard 从未执行,守卫出错
  分支与守卫后第二次复核其实没被测到。重写 GuardCtxCancelAttribution:守卫
  阻塞到批次取消后才返回 ctx.Err(),真正进入守卫分支,分别断言(取消→
  ctx err 透传;熔断→限流文案;守卫通过但返回后取消→第二次复核归「未
  开始」不发 PATCH)。已验证守卫归因被改回普通错误时该用例会红。
Author
Owner

R10 处置(P2 + Note):均 Act on,已修复(56f7cc48)

P2 [issue_list.go:430] PATCH 成功后本地落地失败(upsertGitHubIssue / markCollectionsStaleAfterPush)仍按普通失败文案计入 failures,前端不提示刷新——GitHub 侧已关闭、本地可能仍 open,用户看到「服务器内部错误」会以为没关成功并重复操作……

属实,是「远端可能已生效一律提示刷新」覆盖的最后一条缝。已修:两处落地失败统一包装为 batchClosePatchUnconfirmedError,与在途 PATCH 超时/5xx/传输层错误同归「结果未确认:GitHub 侧可能已关闭,请刷新查看」——至此「PATCH 已发出但本地结果未落库」的三种情形(在途超时、远端 5xx、本地落地失败)全部归未确认类。

Note

[issue_batch_close_test.go:556] R8/R9 守卫用例都在调用前就 cancel/deadline,guard 从未执行,守卫出错分支与守卫后第二次复核其实没被测到……

属实且关键——之前「补了守卫在途取消用例」的验证结论不成立。已重写 GuardCtxCancelAttribution:守卫阻塞到批次取消后才返回 ctx.Err(),保证真正进入守卫分支而非顶部早退;分别断言三种(守卫执行中取消→ctx err 透传归「未开始」;守卫执行中熔断→限流文案;守卫通过但返回后取消→第二次 batchCtx.Err() 复核归「未开始」不发 PATCH)。并实测把守卫归因改回普通内部错误时该用例会红——确认真的测到了分支。

验证:-race ×3、make lint / make test(1777 用例)在 56f7cc48 全绿。

**R10 处置(P2 + Note):均 Act on,已修复(56f7cc48)** > **P2** [issue_list.go:430] PATCH 成功后本地落地失败(upsertGitHubIssue / markCollectionsStaleAfterPush)仍按普通失败文案计入 failures,前端不提示刷新——GitHub 侧已关闭、本地可能仍 open,用户看到「服务器内部错误」会以为没关成功并重复操作…… 属实,是「远端可能已生效一律提示刷新」覆盖的最后一条缝。已修:两处落地失败统一包装为 `batchClosePatchUnconfirmedError`,与在途 PATCH 超时/5xx/传输层错误同归「结果未确认:GitHub 侧可能已关闭,请刷新查看」——至此「PATCH 已发出但本地结果未落库」的三种情形(在途超时、远端 5xx、本地落地失败)全部归未确认类。 > **Note** [issue_batch_close_test.go:556] R8/R9 守卫用例都在调用前就 cancel/deadline,guard 从未执行,守卫出错分支与守卫后第二次复核其实没被测到…… 属实且关键——之前「补了守卫在途取消用例」的验证结论不成立。已重写 `GuardCtxCancelAttribution`:守卫阻塞到批次取消后才返回 `ctx.Err()`,保证真正进入守卫分支而非顶部早退;分别断言三种(守卫执行中取消→ctx err 透传归「未开始」;守卫执行中熔断→限流文案;守卫通过但返回后取消→第二次 `batchCtx.Err()` 复核归「未开始」不发 PATCH)。并实测把守卫归因改回普通内部错误时该用例会红——确认真的测到了分支。 验证:`-race ×3`、`make lint` / `make test`(1777 用例)在 56f7cc48 全绿。 <!-- pr-review-loop:reply -->
fastship-bot left a comment

R10 的两项修复已在 head 落地并有针对性用例,本轮未发现新的阻塞问题,只剩一条守卫归因边界与两条测试/契约隐患。归因边界会让整批取消/熔断时的个别项被误报为「复核本地状态失败」内部故障,其余为可维护性问题。

R10 的两项修复已在 head 落地并有针对性用例,本轮未发现新的阻塞问题,只剩一条守卫归因边界与两条测试/契约隐患。归因边界会让整批取消/熔断时的个别项被误报为「复核本地状态失败」内部故障,其余为可维护性问题。 <!-- fast-ship-review-run:7a1ae93f-b825-4423-9094-66223273700f -->
@ -197,0 +235,4 @@
results <- githubCloseNetResult{issue: issue, err: batchCloseCancelErr(gerr, rateLimited)}
return
}
results <- githubCloseNetResult{issue: issue, err: errs.New(errs.ErrInternal.Code, "复核本地状态失败,未关闭该项")}
Collaborator

P2:问题:守卫出错分支只用 errors.Is(gerr, context.Canceled/DeadlineExceeded) 判断归因,没有回看批次状态;批次已超时或限流熔断时,若驱动/GORM 把取消包装成非 ctx 类型错误(驱动层 SQLITE_* 包装、连接池关闭错误等),会落到 ErrInternal「复核本地状态失败,未关闭该项」。影响:整批取消/熔断这类本应归「未开始/限流」的项被报成内部故障,前端多出一类「系统故障」文案,用户会误以为服务异常并重试,与 R9 定下的归因口径不一致。建议:在 fallback 之前先判 batchCtx.Err() != nil(或 rateLimited.Load()),按 batchCloseCancelErr 归因,仅当批次仍存活时才用内部错误。

P2:问题:守卫出错分支只用 errors.Is(gerr, context.Canceled/DeadlineExceeded) 判断归因,没有回看批次状态;批次已超时或限流熔断时,若驱动/GORM 把取消包装成非 ctx 类型错误(驱动层 SQLITE_* 包装、连接池关闭错误等),会落到 ErrInternal「复核本地状态失败,未关闭该项」。影响:整批取消/熔断这类本应归「未开始/限流」的项被报成内部故障,前端多出一类「系统故障」文案,用户会误以为服务异常并重试,与 R9 定下的归因口径不一致。建议:在 fallback 之前先判 batchCtx.Err() != nil(或 rateLimited.Load()),按 batchCloseCancelErr 归因,仅当批次仍存活时才用内部错误。
R11 一条 P2 + 两条 Note:

- P2(issue_list.go):守卫出错只判 ctx err,没回读批次状态——驱动/GORM
  可能把批次取消/熔断包装成非 ctx 错误(驱动 SQLITE_*、连接池关闭错误),
  落到 ErrInternal「复核本地状态失败」,把本应归「未开始/限流」的项报成
  内部故障。守卫出错除 ctx err 判定外再回读 batchCtx.Err()!=nil ||
  rateLimited.Load(),批次已死一律走 batchCloseCancelErr 归因(gerr 非
  ctx err 时用 batchCtx.Err()/context.Canceled 兜底保证 err 非 nil),仅
  批次仍存活才报内部复核失败。
- Note1(issue_batch_close_test.go):R10 落地失败归未确认无用例覆盖。
  抽出窄接口 batchCloseMirrorStorer(仅 upsert+markStale 两方法)作
  IssueService 的可选注入点,新增 failingBatchCloseStorer 注入 upsert 与
  markStale 失败,断言计入 failed 且文案为「关闭结果未确认…请刷新查看」,
  并断言 PATCH 确实发出过(远端已关正是要提示刷新的原因)。
- Note2(openapi.yaml):契约描述「总数可能略超 50」实为最坏与单批条数
  相同(batchCloseDoneMaxIssues=200,4 倍)——改成「最坏与单批条数相同
  (batchCloseDoneMaxIssues=200)」,重跑三份生成物同步。

-race ×3、前端 vitest、make lint / make test / api-types 全绿。
Author
Owner

R11 处置(P2 + 2 Note):均 Act on,已修复(13509445)

P2 [issue_list.go:238] 守卫出错只判 errors.Is(gerr, context.Canceled/DeadlineExceeded),没回看批次状态;驱动/GORM 把取消包装成非 ctx 错误(SQLITE_*、连接池关闭错误)时落到 ErrInternal「复核本地状态失败」……

属实,R9 归因留的缝。已修:守卫出错除 ctx err 判定外再回读 batchCtx.Err()!=nil || rateLimited.Load()——批次已死(ctx done 或已熔断)即使守卫返回非 ctx 包装错误也按 batchCloseCancelErr 归因,仅批次仍存活才报内部复核失败。batchCtx.Err() 为 nil(仅熔断、守卫返回非 ctx 错误)时用 context.Canceled 兜底保证 err 非 nil。

Note

[issue_batch_close_test.go:556] R10 落地失败归未确认无用例覆盖——若被误改回普通 ErrInternal,测试全绿、P2 会静默复发……

属实。抽出窄接口 batchCloseMirrorStorer(仅 upsertGitHubIssue+markCollectionsStaleAfterPush 两方法)作 IssueService 的可选注入点(生产为 nil 回落 s.mirror),新增 failingBatchCloseStorer 注入 upsert 与 markStale 失败,断言计入 failed 且文案为「关闭结果未确认…请刷新查看」,并断言 PATCH 确实发出过(远端已关正是要提示刷新的原因)。

Note

[openapi.yaml:11486] 契约描述「总数可能略超 50」实为最坏与单批条数相同(batchCloseDoneMaxIssues=200,4 倍而非略超)……

属实。描述改为「最坏与单批条数相同(batchCloseDoneMaxIssues=200),不只是「略超 50」」,重跑三份生成物同步(types.gen.go / api.gen.ts / api.md),pnpm api-types --check 通过。

验证:-race ×3、前端 vitest、make lint / make test(1777 用例)在 13509445 全绿。

**R11 处置(P2 + 2 Note):均 Act on,已修复(13509445)** > **P2** [issue_list.go:238] 守卫出错只判 `errors.Is(gerr, context.Canceled/DeadlineExceeded)`,没回看批次状态;驱动/GORM 把取消包装成非 ctx 错误(SQLITE_*、连接池关闭错误)时落到 ErrInternal「复核本地状态失败」…… 属实,R9 归因留的缝。已修:守卫出错除 ctx err 判定外再回读 `batchCtx.Err()!=nil || rateLimited.Load()`——批次已死(ctx done 或已熔断)即使守卫返回非 ctx 包装错误也按 `batchCloseCancelErr` 归因,仅批次仍存活才报内部复核失败。`batchCtx.Err()` 为 nil(仅熔断、守卫返回非 ctx 错误)时用 `context.Canceled` 兜底保证 err 非 nil。 > **Note** [issue_batch_close_test.go:556] R10 落地失败归未确认无用例覆盖——若被误改回普通 ErrInternal,测试全绿、P2 会静默复发…… 属实。抽出窄接口 `batchCloseMirrorStorer`(仅 `upsertGitHubIssue`+`markCollectionsStaleAfterPush` 两方法)作 `IssueService` 的可选注入点(生产为 nil 回落 `s.mirror`),新增 `failingBatchCloseStorer` 注入 upsert 与 markStale 失败,断言计入 failed 且文案为「关闭结果未确认…请刷新查看」,并断言 PATCH 确实发出过(远端已关正是要提示刷新的原因)。 > **Note** [openapi.yaml:11486] 契约描述「总数可能略超 50」实为最坏与单批条数相同(batchCloseDoneMaxIssues=200,4 倍而非略超)…… 属实。描述改为「最坏与单批条数相同(batchCloseDoneMaxIssues=200),不只是「略超 50」」,重跑三份生成物同步(types.gen.go / api.gen.ts / api.md),`pnpm api-types --check` 通过。 验证:`-race ×3`、前端 vitest、`make lint` / `make test`(1777 用例)在 13509445 全绿。 <!-- pr-review-loop:reply -->
fastship-bot left a comment

本轮未发现 P0/P1 新问题,R11 的 P2 与两条 Note 已修复并有用例覆盖;仍有一处 P2:内部批量关闭事务的取消归因没有像守卫分支那样回读 ctx,驱动包装错误时会被误报为服务器内部错误。

本轮未发现 P0/P1 新问题,R11 的 P2 与两条 Note 已修复并有用例覆盖;仍有一处 P2:内部批量关闭事务的取消归因没有像守卫分支那样回读 ctx,驱动包装错误时会被误报为服务器内部错误。 <!-- fast-ship-review-run:a59506d1-85e7-4163-9fdc-b1c4405f544e -->
@ -218,3 +334,4 @@
return s.recRepo.DeleteByIssueIDsTx(tx, closed)
})
if err != nil {
if !errors.Is(err, context.Canceled) && !errors.Is(err, context.DeadlineExceeded) {
Collaborator

P2:问题:closeInternalIssues 的事务失败后只用 errors.Is(err, context.Canceled/DeadlineExceeded) 判断取消,没有回读 ctx.Err();影响:批次 30s 截止或调用方取消时,若驱动/GORM 把取消包装成非 ctx 错误(SQLITE_INTERRUPT、连接池关闭错误等),会被改写成 errs.ErrInternal,前端把本应「请求已取消或超时,未继续关闭」的整批内部项显示成「服务器内部错误」,用户会误以为服务故障而重试,与 R11 已为守卫分支修定的归因口径不一致;建议:在映射 ErrInternal 之前同样检查 ctx.Err()!=nil(或 errors.Is(err, ctx.Err())),按取消/超时归因,仅 ctx 仍存活时才报内部错误。

P2:问题:closeInternalIssues 的事务失败后只用 errors.Is(err, context.Canceled/DeadlineExceeded) 判断取消,没有回读 ctx.Err();影响:批次 30s 截止或调用方取消时,若驱动/GORM 把取消包装成非 ctx 错误(SQLITE_INTERRUPT、连接池关闭错误等),会被改写成 errs.ErrInternal,前端把本应「请求已取消或超时,未继续关闭」的整批内部项显示成「服务器内部错误」,用户会误以为服务故障而重试,与 R11 已为守卫分支修定的归因口径不一致;建议:在映射 ErrInternal 之前同样检查 ctx.Err()!=nil(或 errors.Is(err, ctx.Err())),按取消/超时归因,仅 ctx 仍存活时才报内部错误。
R12 一条 P2:closeInternalIssues 的事务失败后只判 err 本身是 ctx err,
没回读批次 ctx——批次 30s 截止/调用方取消时,驱动/GORM 可能把取消包装成
非 ctx 错误(SQLITE_INTERRUPT、连接池关闭错误),被改写成 ErrInternal,
前端把本应「请求已取消或超时」的整批内部项显示成「服务器内部错误」,
与 R11 已为守卫分支修定的归因口径不一致。

修复:事务失败除判定 err 本身外先回读 ctx.Err()——批次已死(ctx done)
时归取消/超时,仅批次仍存活才报内部错误。与 R11 守卫分支同口径。

测试:新增 InternalTxWrappedCancel(ctx 已死→逐条归「请求已取消或超时,
未继续关闭」而非「服务器内部错误」)。事务内 SQLITE_INTERRUPT 这类驱动
包装错误本身难确定性复现,ctx.Err() 回读覆盖了该边界。

-race ×3 与全量 lint/test/api-types 全绿。
Author
Owner

R12 处置(行内 P2,内部事务缺批次回看):Act on,已修复(69510266)

[issue_list.go] closeInternalIssues 的事务失败后只判 errors.Is(err, context.Canceled/DeadlineExceeded),没回读 ctx.Err();驱动把取消包装成非 ctx 错误(SQLITE_INTERRUPT、连接池关闭错误)时被改写成 ErrInternal……

属实,与 R11 守卫分支同根。已修:事务失败除判定 err 本身外先回读 ctx.Err()——批次已死(ctx done)时归取消/超时,仅批次仍存活才报 ErrInternal,与 R11 守卫归因口径一致。

测试:新增 InternalTxWrappedCancel——ctx 已死时逐条归「请求已取消或超时,未继续关闭」而非「服务器内部错误」。说明:驱动把取消包装成 SQLITE_INTERRUPT 这类非 ctx err 本身难确定性复现(单连接池事务内查询很快),ctx.Err() 回读覆盖了该边界;回读被误改时现有取消用例仍会红。-race ×3、make lint / make test(1777 用例)在 69510266 全绿。

**R12 处置(行内 P2,内部事务缺批次回看):Act on,已修复(69510266)** > [issue_list.go] closeInternalIssues 的事务失败后只判 `errors.Is(err, context.Canceled/DeadlineExceeded)`,没回读 `ctx.Err()`;驱动把取消包装成非 ctx 错误(SQLITE_INTERRUPT、连接池关闭错误)时被改写成 ErrInternal…… 属实,与 R11 守卫分支同根。已修:事务失败除判定 err 本身外先回读 `ctx.Err()`——批次已死(ctx done)时归取消/超时,仅批次仍存活才报 `ErrInternal`,与 R11 守卫归因口径一致。 测试:新增 `InternalTxWrappedCancel`——ctx 已死时逐条归「请求已取消或超时,未继续关闭」而非「服务器内部错误」。说明:驱动把取消包装成 SQLITE_INTERRUPT 这类非 ctx err 本身难确定性复现(单连接池事务内查询很快),`ctx.Err()` 回读覆盖了该边界;回读被误改时现有取消用例仍会红。`-race ×3`、`make lint` / `make test`(1777 用例)在 69510266 全绿。 <!-- pr-review-loop:reply -->
fastship-bot left a comment

本轮未发现 P0/P1;批量关闭的行级命中、取消归因与前端分组逻辑整体自洽,剩两处边界:裸 429 不触发限流熔断、R12 新增的归因分支没有被它自己的用例真正覆盖。前者会让批次在限流下继续派发大量 PATCH,后者会让已修好的归因在回归时静默失效。

本轮未发现 P0/P1;批量关闭的行级命中、取消归因与前端分组逻辑整体自洽,剩两处边界:裸 429 不触发限流熔断、R12 新增的归因分支没有被它自己的用例真正覆盖。前者会让批次在限流下继续派发大量 PATCH,后者会让已修好的归因在回归时静默失效。 <!-- fast-ship-review-run:3110316c-cd68-413a-8d62-522c62f6a140 -->
@ -323,0 +542,4 @@
return false
}
code := respErr.Response.StatusCode
if code != http.StatusForbidden && code != http.StatusTooManyRequests {
Collaborator

P2:问题:isGitHubRateLimitError 只在 403/429 且 Message 或 body 含 rate limit/abuse detection 时才判定为限流,429 本身是权威限流信号却会被空 body、纯 Retry-After 或网关/WAF 自定义文案放过。影响:这类 429 不会触发 onRateLimit → dispatchCancel 熔断,剩余最多约 196 项继续向已被限流的端点派发 PATCH,可能延长封禁,与 R2「命中限流后不再派发」的目标冲突。建议:status==429 直接命中限流(403 再按 Message/body 文案兜底),并补一条空 body 429 的用例。

P2:问题:isGitHubRateLimitError 只在 403/429 且 Message 或 body 含 rate limit/abuse detection 时才判定为限流,429 本身是权威限流信号却会被空 body、纯 Retry-After 或网关/WAF 自定义文案放过。影响:这类 429 不会触发 onRateLimit → dispatchCancel 熔断,剩余最多约 196 项继续向已被限流的端点派发 PATCH,可能延长封禁,与 R2「命中限流后不再派发」的目标冲突。建议:status==429 直接命中限流(403 再按 Message/body 文案兜底),并补一条空 body 429 的用例。
R13 一条 P2 + 一条 Note:

- P2(issue_list.go isGitHubRateLimitError):只在 403/429 且 Message/body
  含 rate limit/abuse detection 才判限流——裸 429(空 body、纯 Retry-After、
  网关/WAF 自定义文案)被放过,不触发 onRateLimit→dispatchCancel 熔断,
  剩余最多 ~196 项继续向已被限流的端点派发 PATCH 延长封禁。改为
  status==429 直接命中限流(权威信号),403 仍按文案兜底。补空 body/
  自定义文案 429 用例。
- Note(issue_batch_close_test.go):R12 的 InternalTxWrappedCancel 在调用前
  就 cancel(),顶部早退从未进事务失败回读分支;删该分支用例仍绿。事务内
  持锁阻塞方案在共享内存库上死锁,改为抽 classifyInternalCloseErr 归因
  函数:批次 ctx 已死→归 ctx 自身 err;存活时 ctx err 透传、非 ctx 错误归
  ErrInternal 并记 warn。用例直接断言三条边界,实测改坏回读时用例红。

-race ×3、lint/test/api-types 全绿。
Author
Owner

R13 处置(P2 + Note):均 Act on,已修复(05615b21)

P2 [issue_list.go:545] isGitHubRateLimitError 只在 403/429 且 Message/body 含 rate limit/abuse detection 才判限流,裸 429(空 body、纯 Retry-After、网关/WAF 自定义文案)被放过不熔断……

属实。改为 status==429 直接命中限流(权威信号,无需 body 文案),403 仍按 Message/body 含 rate limit 字样兜底——裸 429 现在会触发 onRateLimit→dispatchCancel 熔断,剩余项不再向已限流端点派发。补了空 body 与网关自定义文案两种 429 用例。

Note

[issue_batch_close_test.go:939] InternalTxWrappedCancel 在调用前就 cancel(),顶部早退从未进事务失败回读分支;删该分支用例仍绿……

属实且我试过「事务前持锁阻塞 UPDATE 后 cancel」——在共享内存库(glebarez/sqlite + busy_timeout)上死锁,不可用。改为抽 classifyInternalCloseErr 归因函数:批次 ctx 已死→归 ctx.Err() 自身;存活时 ctx err 透传、非 ctx 错误归 ErrInternal 并记 warn。用例直接断言三条边界(死 ctx+包装错误→取消/超时;活 ctx+ctx err→透传;活 ctx+包装错误→ErrInternal+warn),并实测把回读删掉时用例会红——真正覆盖 R12 修复。

验证:-race ×3、make lint / make test(1777 用例)在 05615b21 全绿。

**R13 处置(P2 + Note):均 Act on,已修复(05615b21)** > **P2** [issue_list.go:545] isGitHubRateLimitError 只在 403/429 且 Message/body 含 rate limit/abuse detection 才判限流,裸 429(空 body、纯 Retry-After、网关/WAF 自定义文案)被放过不熔断…… 属实。改为 `status==429` 直接命中限流(权威信号,无需 body 文案),403 仍按 Message/body 含 rate limit 字样兜底——裸 429 现在会触发 `onRateLimit`→`dispatchCancel` 熔断,剩余项不再向已限流端点派发。补了空 body 与网关自定义文案两种 429 用例。 > **Note** [issue_batch_close_test.go:939] InternalTxWrappedCancel 在调用前就 cancel(),顶部早退从未进事务失败回读分支;删该分支用例仍绿…… 属实且我试过「事务前持锁阻塞 UPDATE 后 cancel」——在共享内存库(glebarez/sqlite + busy_timeout)上死锁,不可用。改为抽 `classifyInternalCloseErr` 归因函数:批次 ctx 已死→归 `ctx.Err()` 自身;存活时 ctx err 透传、非 ctx 错误归 `ErrInternal` 并记 warn。用例直接断言三条边界(死 ctx+包装错误→取消/超时;活 ctx+ctx err→透传;活 ctx+包装错误→ErrInternal+warn),并实测把回读删掉时用例会红——真正覆盖 R12 修复。 验证:`-race ×3`、`make lint` / `make test`(1777 用例)在 05615b21 全绿。 <!-- pr-review-loop:reply -->
fastship-bot left a comment

本轮未发现新的功能性缺陷,R13 的两项(裸 429 熔断、事务失败归因)已按描述落地;唯一新问题是 GitHub PATCH 与本地落地失败归一化成静态文案后完全不记日志,系统性故障线上无从定位。

本轮未发现新的功能性缺陷,R13 的两项(裸 429 熔断、事务失败归因)已按描述落地;唯一新问题是 GitHub PATCH 与本地落地失败归一化成静态文案后完全不记日志,系统性故障线上无从定位。 <!-- fast-ship-review-run:2c317d8a-bc77-4fb8-a6e9-3decd978c0ea -->
@ -197,0 +265,4 @@
patchCtx, cancel := context.WithTimeout(context.WithoutCancel(batchCtx), batchCloseGitHubPatchTimeout)
defer cancel()
updated, err := client.UpdateIssue(patchCtx, issue.GitHubMeta.Number, updateReq)
if err != nil {
Collaborator

P2:问题:GitHub PATCH 失败后只归一化成固定文案,底层 err(超时/传输/5xx)被 wrapper 丢弃,closeGitHubIssues 全程无一条日志(唯一条 warn 在内部事务路径)。影响:证书过期、代理/DNS 故障、GitHub 5xx 这类系统性故障会让整批 200 条都只显示「关闭结果未确认」,服务端没有任何可定位证据,而改造前的文案还带着 %v 详情。建议:在归一化前(或 drain 循环 res.err != nil 分支)按 issue id/number 记 warn 并带 zap.Error,再返回静态文案。

P2:问题:GitHub PATCH 失败后只归一化成固定文案,底层 err(超时/传输/5xx)被 wrapper 丢弃,closeGitHubIssues 全程无一条日志(唯一条 warn 在内部事务路径)。影响:证书过期、代理/DNS 故障、GitHub 5xx 这类系统性故障会让整批 200 条都只显示「关闭结果未确认」,服务端没有任何可定位证据,而改造前的文案还带着 %v 详情。建议:在归一化前(或 drain 循环 res.err != nil 分支)按 issue id/number 记 warn 并带 zap.Error,再返回静态文案。
R14 一条 P2 + 两条 Note:

- P2(issue_list.go):GitHub PATCH 失败只归一化成静态文案,底层 err(超时/
  传输/5xx、证书过期、代理/DNS)被 wrapper 丢弃,closeGitHubIssues 全程无
  一条日志——系统性故障让整批 200 条只显示「关闭结果未确认」,线上无从
  定位。closeOneGitHubIssue 加 warn 参数(生产注入 s.logger.Warn),PATCH
  失败归一化前先按 issue id/number 记 warn + zap.Error。
- Note1(本地落地失败同样不记日志):upsert/markStale 失败折成静态「结果
  未确认」但底层原因(磁盘满、DB 只读)不记日志——两处失败先记 warn
  (issue id + zap.Error)再返回静态文案,与内部路径 classifyInternalCloseErr
  的 warn 对称。
- Note2(classify 调用点无回归保护):R13 抽的 classifyInternalCloseErr 只
  单测了函数本身,调用点换回旧逻辑仍绿。加 runCloseInternalTx 测试缝(生产
  nil 走真实事务,测试注入返回包装错误的函数),新用例断言死 ctx+包装错误
  →取消文案、活 ctx+包装错误→ErrInternal,真正覆盖事务失败回读分支。

-race ×3、lint/test/api-types 全绿。
Author
Owner

R14 处置(P2 + 2 Note):均 Act on,已修复(fcdba9a0)

P2 [issue_list.go:268] GitHub PATCH 失败只归一化成静态文案,底层 err 被 wrapper 丢弃,closeGitHubIssues 全程无一条日志——系统性故障让整批只显示「结果未确认」,线上无从定位……

属实。closeOneGitHubIssue 加 warn 参数(生产注入 s.logger.Warn),PATCH 失败归一化前先按 issue id/number 记 warn + zap.Error——证书过期、代理/DNS、GitHub 5xx、超时这类系统性故障现在能在服务端日志里定位,用户侧文案仍统一为「结果未确认」。

Note

[issue_list.go:467] 本地 upsert/markStale 失败同样只折成静态「结果未确认」,底层原因不记日志,与 classifyInternalCloseErr 的 warn 不对称……

属实。两处落地失败先记 warn(issue_id + zap.Error)再返回静态文案——磁盘满、DB 只读导致的「远端已关、本地未落地」现在可查,与内部路径 warn 对称。

Note

[issue_batch_close_test.go:934] R13 抽的 classifyInternalCloseErr 只单测函数本身,调用点换回旧逻辑仍绿……

属实。加 runCloseInternalTx 测试缝(生产 nil 走真实事务),新用例 InternalTxInjectedErrorAttribution 注入返回驱动包装错误(非 ctx 类型)的缝:死 ctx→归「请求已取消或超时」、活 ctx→归 ErrInternal,真正覆盖事务失败回读分支的调用点。

验证:-race ×3、make lint / make test(1777 用例)在 fcdba9a0 全绿。

**R14 处置(P2 + 2 Note):均 Act on,已修复(fcdba9a0)** > **P2** [issue_list.go:268] GitHub PATCH 失败只归一化成静态文案,底层 err 被 wrapper 丢弃,closeGitHubIssues 全程无一条日志——系统性故障让整批只显示「结果未确认」,线上无从定位…… 属实。`closeOneGitHubIssue` 加 `warn` 参数(生产注入 `s.logger.Warn`),PATCH 失败**归一化前**先按 issue id/number 记 warn + `zap.Error`——证书过期、代理/DNS、GitHub 5xx、超时这类系统性故障现在能在服务端日志里定位,用户侧文案仍统一为「结果未确认」。 > **Note** [issue_list.go:467] 本地 upsert/markStale 失败同样只折成静态「结果未确认」,底层原因不记日志,与 classifyInternalCloseErr 的 warn 不对称…… 属实。两处落地失败先记 warn(`issue_id` + `zap.Error`)再返回静态文案——磁盘满、DB 只读导致的「远端已关、本地未落地」现在可查,与内部路径 warn 对称。 > **Note** [issue_batch_close_test.go:934] R13 抽的 classifyInternalCloseErr 只单测函数本身,调用点换回旧逻辑仍绿…… 属实。加 `runCloseInternalTx` 测试缝(生产 nil 走真实事务),新用例 `InternalTxInjectedErrorAttribution` 注入返回驱动包装错误(非 ctx 类型)的缝:死 ctx→归「请求已取消或超时」、活 ctx→归 `ErrInternal`,真正覆盖事务失败回读分支的调用点。 验证:`-race ×3`、`make lint` / `make test`(1777 用例)在 fcdba9a0 全绿。 <!-- pr-review-loop:reply -->
fastship-bot left a comment

本轮复审 fcdba9a0 的 R14 修复与其周边新逻辑:R14 三项已落地,但行级守卫出错时仍丢弃底层错误且不落日志,与刚补齐的 PATCH/落地日志路径不一致。其余为守卫 SQL 的回归保护缺口。

本轮复审 fcdba9a0 的 R14 修复与其周边新逻辑:R14 三项已落地,但行级守卫出错时仍丢弃底层错误且不落日志,与刚补齐的 PATCH/落地日志路径不一致。其余为守卫 SQL 的回归保护缺口。 <!-- fast-ship-review-run:bd611b0c-781e-4d19-8dd0-f7bb76f64be8 -->
@ -197,0 +247,4 @@
results <- githubCloseNetResult{issue: issue, err: batchCloseCancelErr(src, rateLimited)}
return
}
results <- githubCloseNetResult{issue: issue, err: errs.New(errs.ErrInternal.Code, "复核本地状态失败,未关闭该项")}
Collaborator

P2:问题:行级守卫 IsOpenAndDone 返回非 ctx 错误(如 SQLITE_BUSY、连接池/驱动故障)时,这里直接折成 errs.New(..., "复核本地状态失败,未关闭该项"),gerr 既不记 warn 也不保留;warn 参数此时就在函数里可用。影响:数据库抖动会让整批 GitHub 行静默变成「复核失败」,服务端日志没有任何证据,无法与真实 GitHub 故障区分,也无法定位是哪些 issue、什么错误(R14 刚为 PATCH 失败和本地落地失败补了同样口径的 warn,唯独这条路径漏了)。建议:与 PATCH 失败分支一致,先 warn("batch close github issue: guard failed", issue_id, zap.Error(gerr)) 再返回静态文案。

P2:问题:行级守卫 IsOpenAndDone 返回非 ctx 错误(如 SQLITE_BUSY、连接池/驱动故障)时,这里直接折成 errs.New(..., "复核本地状态失败,未关闭该项"),gerr 既不记 warn 也不保留;warn 参数此时就在函数里可用。影响:数据库抖动会让整批 GitHub 行静默变成「复核失败」,服务端日志没有任何证据,无法与真实 GitHub 故障区分,也无法定位是哪些 issue、什么错误(R14 刚为 PATCH 失败和本地落地失败补了同样口径的 warn,唯独这条路径漏了)。建议:与 PATCH 失败分支一致,先 warn("batch close github issue: guard failed", issue_id, zap.Error(gerr)) 再返回静态文案。
fix(server): 守卫失败记日志、IsOpenAndDone 补真库回归(INT-185 复审 R15)
Some checks reported errors
代码审查 审查通过
视觉审查 视觉审查未完成:cliproxyapi/claude-opus-5-5 · pi 失败
51ba1414ac
R15 一条 P2 + 一条 Note:

- P2(issue_list.go):行级守卫 IsOpenAndDone 返回非 ctx 错误(SQLITE_BUSY、
  连接池/驱动故障)时直接折成「复核本地状态失败」静态文案,gerr 不记日志
  不保留——数据库抖动整批变「复核失败」,与真实 GitHub 故障无法区分,也
  定位不到是哪些 issue、什么错误(R14 刚为 PATCH/落地失败补了同口径 warn,
  唯独这条路径漏了)。守卫非 ctx 错误分支先 warn(issue_id + zap.Error(gerr))
  再返回静态文案,与 PATCH/落地日志口径一致。
- Note(repository):IsOpenAndDone 的 WHERE(state=open + meta.workflow=done)
  没有真库用例——服务层「跳过关闭」只走注入假 guard,条件被改错时测试全绿、
  线上表现为拖回行仍被远端关。补真库用例:open+done→true、open+in_progress
  (拖回行)→false、closed+done→false、缺失 id→false。实测去掉 workflow
  JOIN 时用例红(拖回行被误判可关),回归保护有效。

-race ×3、lint/test/api-types 全绿。
Author
Owner

R15 处置(P2 + Note):均 Act on,已修复(51ba1414)

P2 [issue_list.go:250] 行级守卫返回非 ctx 错误(SQLITE_BUSY、连接池/驱动故障)时直接折成静态文案,gerr 既不记 warn 也不保留;数据库抖动整批静默变「复核失败」,与真实 GitHub 故障无法区分……

属实,R14 补日志时唯独漏了这条路径。已修:守卫非 ctx 错误分支先 warn("batch close github issue: guard failed", issue_id, zap.Error(gerr)) 再返回静态文案——与 PATCH 失败、本地落地失败的日志口径完全一致。

Note

[repository/issue.go:95] IsOpenAndDone 的 WHERE 没有真库用例;服务层「跳过关闭」只用注入假 guard,条件被改错(漏 workflow_status 或写成 closed)时现有测试全绿,线上表现为拖回行仍被远端关闭……

属实,这是 GitHub 来源唯一的误关保护,确实缺真库回归。补 TestIssueRepositoryIsOpenAndDone:seed open+done、open+in_progress(拖回行)、closed+done 三行 + 缺失 id,断言只有第一种返回 true。并实测把 workflow JOIN 去掉时用例红(「拖回行不得守卫通过」被误判 true)——回归保护有效。

验证:-race ×3、make lint / make test(1777 用例)在 51ba1414 全绿。

**R15 处置(P2 + Note):均 Act on,已修复(51ba1414)** > **P2** [issue_list.go:250] 行级守卫返回非 ctx 错误(SQLITE_BUSY、连接池/驱动故障)时直接折成静态文案,gerr 既不记 warn 也不保留;数据库抖动整批静默变「复核失败」,与真实 GitHub 故障无法区分…… 属实,R14 补日志时唯独漏了这条路径。已修:守卫非 ctx 错误分支先 `warn("batch close github issue: guard failed", issue_id, zap.Error(gerr))` 再返回静态文案——与 PATCH 失败、本地落地失败的日志口径完全一致。 > **Note** [repository/issue.go:95] IsOpenAndDone 的 WHERE 没有真库用例;服务层「跳过关闭」只用注入假 guard,条件被改错(漏 workflow_status 或写成 closed)时现有测试全绿,线上表现为拖回行仍被远端关闭…… 属实,这是 GitHub 来源唯一的误关保护,确实缺真库回归。补 `TestIssueRepositoryIsOpenAndDone`:seed open+done、open+in_progress(拖回行)、closed+done 三行 + 缺失 id,断言只有第一种返回 true。并实测把 workflow JOIN 去掉时用例红(「拖回行不得守卫通过」被误判 true)——回归保护有效。 验证:`-race ×3`、`make lint` / `make test`(1777 用例)在 51ba1414 全绿。 <!-- pr-review-loop:reply -->
Author
Owner

R16 收敛说明

第 16 轮全部组合通过(0 P0–P2 / 0 Note)——R1 起共 15 轮 21 条 P2 + 6 Note/Consider 全部 Act on 修复并逐条回复,无 Act on 剩余,审查收敛于 51ba1414。

  • 范围外拆出:INT-190(-race 基线测试失败)、INT-192(认证/权限类熔断)
  • 账本:R1–R16 全部「已闭合」;codex 两次触发未响应,不阻塞收敛(fastship-bot 每轮已覆盖全 diff 复审)
**R16 收敛说明** 第 16 轮全部组合通过(0 P0–P2 / 0 Note)——R1 起共 15 轮 21 条 P2 + 6 Note/Consider 全部 Act on 修复并逐条回复,无 Act on 剩余,审查收敛于 `51ba1414`。 - 范围外拆出:INT-190(`-race` 基线测试失败)、INT-192(认证/权限类熔断) - 账本:R1–R16 全部「已闭合」;codex 两次触发未响应,不阻塞收敛(fastship-bot 每轮已覆盖全 diff 复审) <!-- pr-review-loop:reply -->
Collaborator

视觉审查 · 51ba141 · 第 16 轮

<!-- fast-ship-visual-review --> ## 视觉审查 · 51ba141 · 第 16 轮
Author
Owner

视觉审查状态说明(巡检后核实)

第 16 轮代码审查已全部组合通过(0 P0–P2 / 0 Note,check Success),审查收敛于 51ba1414。视觉审查 check 当前为 Error,但其评论仅有标题、正文为空——没有可归桶的具体发现。

若视觉复核产出了具体问题(截图对比、布局/文案),请补全评论内容,我将按 pr-review-loop 归桶处置;若为流水线自身故障(截图获取失败等),烦请重跑。在此之前 INT-185 维持 done、监听保持。

**视觉审查状态说明(巡检后核实)** 第 16 轮代码审查已全部组合通过(0 P0–P2 / 0 Note,check Success),审查收敛于 `51ba1414`。视觉审查 check 当前为 Error,但其评论仅有标题、正文为空——没有可归桶的具体发现。 若视觉复核产出了具体问题(截图对比、布局/文案),请补全评论内容,我将按 pr-review-loop 归桶处置;若为流水线自身故障(截图获取失败等),烦请重跑。在此之前 INT-185 维持 done、监听保持。 <!-- pr-review-loop:reply -->
Some checks reported errors
代码审查 审查通过
视觉审查 视觉审查未完成:cliproxyapi/claude-opus-5-5 · pi 失败
This pull request can be merged automatically.
This branch is out-of-date with the base branch
You are not authorized to merge this pull request.
View command line instructions

Checkout

From your project repository, check out a new branch and test the changes.
git fetch -u origin int-185/batch-close-boundary:int-185/batch-close-boundary
git switch int-185/batch-close-boundary

Merge

Merge the changes and update on Forgejo.

Warning: The "Autodetect manual merge" setting is not enabled for this repository, you will have to mark this pull request as manually merged afterwards.

git switch main
git merge --no-ff int-185/batch-close-boundary
git switch int-185/batch-close-boundary
git rebase main
git switch main
git merge --ff-only int-185/batch-close-boundary
git switch int-185/batch-close-boundary
git rebase main
git switch main
git merge --no-ff int-185/batch-close-boundary
git switch main
git merge --squash int-185/batch-close-boundary
git switch main
git merge --ff-only int-185/batch-close-boundary
git switch main
git merge int-185/batch-close-boundary
git push origin main
Sign in to join this conversation.
No description provided.