一条 47k 行的分支扔给 /ultraview,它在云端挖出两个我没敢正视的 bug

cover

大家好,我是飞飞。

今天傍晚 6 点左右,我对着一条马上要合进 master 的 develop 分支,跑了一次 /ultraview。这个分支改得挺狠:338 个文件改动,47329 行新增、18586 行删除。我提交完命令就去了趟厨房,回来一看云端已经把 2 个 finding 推过来了。一个是 bug,一个是 nit,两个如果没抓住都会合进 master。

底下我把它找到的两个问题摊开讲,顺带说说 /ultraview 的工作流程,对正在考虑要不要把它接进自己 PR 流程的人会有用。

先把这次的规模和结果摆出来

这是一个 Android RPA 项目,Kotlin 写的,业务上要跟几个外部 app 来回切换、做自动化的消息操作和任务状态管理。develop 分支我跟另一个同事改了一个多月,修了几个模块的 step 逻辑,加了两套新 step,顺带清理了一些老代码。

CLI 启动截图

/ultraview 在 CLI 里一输入,它就给我回了一行:”Free ultraview 1 of 3”。免费额度一共 3 次,这次用掉 1 次。然后告诉我它会在云端跑,预计 5-10 分钟,还给了一个 claude.ai/code 的 session URL 让我去 Web 上看进度。

这里有个关键点:扫描跑在 Anthropic 的云端,我这台笔电本地的 context 一个 token 都没占。同时段我还在另一个窗口里跟 Claude Code 聊别的事。

/ultraview 在云端跑了什么

点进 session 链接看到的是一个四阶段的进度条:

  • Setup:把 develop 和 master 两个分支的快照拉下来准备
  • Find:扫 diff,找可疑的改动,生成”候选”(candidates)
  • Verify:对每个候选做二次验证,确认是不是真问题
  • Dedupe:对确认下来的问题做去重

Find 阶段找到候选

Find 阶段大概跑了 3 分钟,告诉我找到 2 个候选。Verify 阶段比 Find 快不少,因为只用验证那 2 条。

Verify 阶段

Verify 过完是 2 confirmed、0 refuted。最后 Dedupe 出来 2 个 issue,两条都留下了,都是真问题。

这种”先广撒网找候选,再逐一 verify”的流程跟普通 /review 一把梭的打法完全不一样,我后面会展开说。

它抓到的 orphan step,差点就合进 master 了

Review 完成的两条 finding

第一个 finding 指的是我这个分支里新加的 WxNewFriendStep.finishTask(success, errorMsg)。它的实现长这样:

1
2
3
4
5
override fun finishTask(success: Boolean, errorMsg: String): Step {
delay(...)
logUtil(...)
return Step.none
}

表面看好像没问题:做了延迟,打了日志,返回 Step.none

问题是这个 finishTask 一个 cleanup hook 都没有。项目里正经的 step 完成时都要调 TaskHelper.finishBossTask(),那个方法会做一整套收尾:scheme 跳回我们自己 app 的 AI 招聘页面、sendDone 广播、悬浮窗清理、无障碍服务和屏幕常亮的 teardown、killProcessApp 把目标 app 进程干掉、StepManager.isStop = true 把状态机关掉。

ultraview 把这条 step 的每一条完成路径都列了出来:

  • finishTask(true) 的 success 路径,在 L72-74
  • onPreCheckFail / onPostCheckFail,在 L53-68
  • repeatOrFail 耗尽,在 L136-143

所有路径走完,RPA app 还停在微信里。跳回、广播、清理、kill process 这四件事一件都没做。任务看起来跑完了,实际上这条 step 的 golden path 是不可交付的,只有用户手动取消(走 TaskCancellationCenter)的那条路能 end-to-end 通过。

我盯着这条 finding 看了半分钟。我跟同事联调的时候从来没走到过这些异常路径,测试环境的 success 路径看起来也有返回,编译和静态扫描都过。这种 bug 走到生产上,表现大概就是用户的任务显示”已完成”,但我们后台永远收不到 done 广播,下一个任务也永远排不上来。

修法其实很简单,ultraview 也顺手写了:镜像隔壁 BossAutoReplyStep.finishTask 的实现,委托给 TaskHelper.finishBossTask(message = errorMsg.ifEmpty { null }),让这条 step 重新接回 finishTaskBase 的生命周期。

另一条 nit 读完比 bug 还尴尬:retryTime 单位错了 5 处

第二个 finding 标的级别是 nit,但我读完觉得比 Bug #002 更尴尬。

项目里的 NodeUtils.kt:451 有一个帮助方法,参数 retryTime 的 KDoc 白纸黑字写着单位是毫秒。但是同模块 BossHelper 里,调用点的写法是这样:

1
2
3
4
5
6
7
8
// 173 / 214 / 246 行,RecyclerView 查找
isLastMessageFromMe(retryTime = 3)
hasMySentMessage(retryTime = 3)
collectChatMessages(retryTime = 3)

// 837 / 838 行,"立即沟通"/"继续沟通" 按钮查找
findNodesByText(retryTime = 3)
findNodesByClassName(retryTime = 1)

看上去像是在说”重试 3 次 / 1 次”。实际上这是 3 毫秒 / 1 毫秒的 delay,相当于几乎立刻返回,第一轮找不到就超预算退出,等价于不重试。

最讽刺的是同模块的 inputAndSendMessage 写的是 retryTime = 2000,这个是对的。上一轮 commit 里我们已经修过一处,另外 5 处还留着。ultraview 把这个背景也调出来了,顺手标明了”已修对齐到 2000”的那处在哪。

修复总结

修法也直接:前三处改成 3000,后两处改成 3000 / 1000,跟 inputAndSendMessage 的 2000 对齐。

这种 bug 比 orphan step 更难靠人肉审查抓到。每一行单独看都合法、语义都对,只有把 KDoc 和调用点放在一起看才能发现单位错了。

Verify 过完它直接在本地把这两个问题修了

云端吐完 2 个 finding,CLI 这边并没有停。它把 finding 摘回本地,自己又做了一轮校验。

本地二次验证

我能看到它跑了一次 grep(”Searched for 1 pattern, read 3 files”),重新确认 Bug #002 的那条 step 确实没有任何 cleanup hook、确实是 orphan。然后它自己动手修:在目标 kt 文件顶部加了 import TaskHelper,在 finishTask 末尾加了 TaskHelper.finishBossTask(message = errorMsg.ifEmpty { null }) 的调用。retryTime 5 处按它之前列的数字改。

改完它自己跑了一次编译,编译通过。最后给我起草了 commit message:

1
fix(review): 修复 WxNewFriendStep 末回跳 + BossHelper retryTime 单位误用

从我在 CLI 里敲 /ultraview 到它把修复总结贴出来,CLI 右下角写的是 “Sautéed for 2m 3s”,这是本地阶段的耗时。加上云端扫描的 5-6 分钟,整条流水线不到 10 分钟。

跑完我一直在琢磨的一件事:Find → Verify 这个两段式架构

说实话,ultraview 能挖到 Bug #002 我一点都不意外。它真正让我停下来想的是这种”先找候选再 verify”的两段式架构,跟普通 /review 的差别比我想的大。

我过去在一条小分支上跑 /review 的体感是:它会很快扫完所有改动,然后给一把大杂烩。3 条真 bug、5 条 style nit、2 条它自己脑补的”潜在 race condition”混在一起,我得自己过一遍去掉噪声。

这次 /ultraview 给我的是 2 条 finding,两条都是真的。Find 阶段其实也跑了不少候选(扫描过程里它并没告诉我具体扫了几条),但在 Verify 阶段被过滤掉了。等推到我面前的时候,已经是”这两条我自己校验过”的结论。

对 338 文件、47k 行的 diff,这个过滤尤其重要。如果一个 reviewer 给我 20 条建议里混着 8 条误报,我第一反应可能是直接关掉窗口。它给我 2 条,我会认真看每一条。

另一个现实意义是云端跑的那部分。我本地 context 不被那 47k 行 diff 吃掉,同时段还可以在 Claude Code 里写别的代码,等 finding 推过来再看。本地跑 /review 做同样规模的 diff,我的 context 窗口基本就没空间干别的事了。

3 次 Free 用完之后的账,我自己还没算明白

Free 额度一共 3 次,用完之后的计费规则目前页面上还没明确说明。是按 PR 规模计费、按 token 走,还是必须升到 Max 套餐才解锁,我得再观察一阵。

我最想听正在做代码审查工作流的人的回答:如果 /ultraview 后面变成按量计费,你们会把它接进 PR 自动化里(每条 PR 开的时候自动跑一次),还是只把它当一个人类 reviewer 的二审工具?

特别想听两类人的答案。一类是在公司维护 monorepo 的,这种 338 文件级别的分支你们多久出一条,成本能不能接受;一类是像我这种个人/小团队项目的,有没有试过把它跟 GitHub Actions 或者自己的 CI hook 接起来。

评论区聊。