Skip to content

fix(issue): 固定默认目录兜底领取仓库扫描 - #1039

Merged
deepcoldy merged 2 commits into
deepcoldy:masterfrom
pixystone:fix/issue-1038-default-dir
Aug 28, 2026
Merged

fix(issue): 固定默认目录兜底领取仓库扫描#1039
deepcoldy merged 2 commits into
deepcoldy:masterfrom
pixystone:fix/issue-1038-default-dir

Conversation

@pixystone

@pixystone pixystone commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

改了什么

  • Issue 领取继续优先使用显式 workingDir / workingDirs 扫描根
  • 显式扫描根缺失时,通过 effectiveDefaultWorkingDir 同时覆盖 fixed 与 default-oncall 模式
  • fixed/oncall 兜底目录只在自身是 Git 仓库时直接生成唯一候选,不递归扫描子目录,也不展开其 linked worktrees
  • 非仓库的大目录(例如 ~、工作区父目录)维持空候选,避免同步扫描超过飞书卡片回调时限
  • 补充 buildIssueCommandDeps 真实接线、Oncall 全链路、显式优先级和不递归扫描测试

影响面

仅影响飞书 Issue Board 的领取候选仓库解析:

  • 显式 card 扫描模式保持原行为(递归扫描仓库和 worktree)
  • fixed/default-oncall 模式新增单仓库兜底
  • 普通话题启动、Oncall 权限/绑定、CLI/后端和其它 IM 路径均不变

验证

  • bun run build(Bun 1.4.0):通过
  • Issue / working-dir / Oncall / onboarding 扩展回归:14 files / 342 tests passed
  • tsc --noEmit --pretty false:通过
  • git diff --check:通过

Fixes #1038

@deepcoldy

deepcoldy commented Aug 27, 2026

Copy link
Copy Markdown
Owner

你好 👋 先说结论:方向是对的issueWorkingDirs 抽成独立函数、显式扫描根优先而不与 default 合并,这个取舍很克制,也确实解决了 #1038 的主路径。下面两条是建议合入前调整的,另有一条仅供知悉。

ℹ️ 这是一次自动评审的初步意见,供参考;最终以维护者审阅为准。


🟠 建议一:同一个 bug 目前只修了一半,Oncall 模式的 bot 仍会复现

dashboard 上的「默认工作目录模式」是三选一,选「Oncall 模式」时 oncall-store.tssetWorkingDirMode显式清掉 defaultWorkingDirnextWorkingDir = null,两字段互斥)。所以这批 bot 走不到本 PR 新增的兜底,#1038 的症状原样保留。

本地全链路实测(registerBotbuildIssueCommandDepsreposForbuildClaimConfirmCard):

app_fixed   dirs=["~/iserver/agent-monorepo"]  候选=8  卡片阻断=false   ← 本 PR 修好了
app_oncall  dirs=[]                            候选=0  卡片阻断=true    ← 仍然复现

仓库里已经有一个现成的规范判据:effectiveDefaultWorkingDir(cfg)src/bot-registry.ts:2286),语义正是「这个 bot 的有效默认工作目录」,daemon 的 resolveBotDefaultWorkingDirresolveScheduleWorkingDir 的 layer-3、getProjectScanDirsForBot 都在用它。它的 doc 也明确写了「Reading this NEVER writes state or binds a chat to oncall」,纯读、无副作用。

建议把兜底那行换成它,顺带消除一份手写的重复判据:

-  return configuredWorkingDirs({ workingDir: cfg.defaultWorkingDir });
+  return configuredWorkingDirs({ workingDir: effectiveDefaultWorkingDir(cfg as BotConfig) });

cfgPick 需要补上 'defaultOncall'。)本地原型验过:oncall 模式返回 ["~/iserver/botmux"],fixed / card 模式行为不变,你新增的 3 个用例全绿,tsc --noEmit 通过。


🟠 建议二:兜底把「钉住的单个仓库」当成了「扫描根」,会拖出一个卡死风险

workingDir/workingDirs 的语义是扫描根(父目录,下面装着一堆仓库),而 defaultWorkingDir 的语义是钉住的那一个目录。直接把后者塞进 scanMultipleProjects(dirs, 3, …) 会有两个后果:

(a) 扫描根是大目录时会同步阻塞整个 daemon,撞上飞书卡片回调的 3s 死线。

在一台真实机器的 bots.json(49 个 bot)上跑了一遍:本 PR 会让 18 个 bot 从「不扫描」变成「开始扫描」,其中 4 个defaultWorkingDir~~/iserver 这类大目录。按 reposFor 的逐字调用形态实测:

scanMultipleProjects(['/root'], 3, { includeWorktrees: true })
→ 4005ms(三次重复:4005 / 4005 / 4004ms),撞满 budget 后中止
→ 期间 100ms 定时器实际触发 0 次(event loop 被同步占满)

(4 秒这个上限正是 DEFAULT_MAX_SCAN_MS 兜住的结果;扫描器本身跳过 dotfile / node_modules / vendor / dist,实际只走了 1074 个目录、4 万个目录项。另外 .git 命中分支里没有 budget 检查,其中的 git worktree list --porcelainexecSynctimeout: 5000,所以最坏情况还能在 4 秒之上再挂一个 git 子进程。)

这里有一个结构性的问题,不依赖上面的实测数字也成立project-scanner.ts:68DEFAULT_MAX_SCAN_MS = 4000,比飞书 card.action.trigger 的 3s 死线还大 1 秒。而 event-dispatcher.ts:749 那个 CARD_ACTION_ACK_TIMEOUT_MS = 2500 的兜底 ACK 是 setTimeout + Promise.race —— 同步扫描期间 timer 回调排不进 event loop,降级机制在这条路径上结构性失效。也就是说:即使 budget 精确生效,最坏情况也是「卡满 4 秒 → 超过 3s → 用户看到 200341」,而不是原来的「立刻报没扫到」。

⚖️ 需要说明的是:reposFor 的同步扫描是本 PR 之前就有的,这行你没动。但本 PR 把触发面从 31 个 bot 扩到 49 个,恰好把这 4 个大目录 bot 纳了进来,所以这个退化是本 PR 引入的。

(b) 兜底根本身是仓库时,那唯一想要的目录会被自己的 worktree 淹掉。

实测 defaultWorkingDir=~/iserver/botmux → 扫出 639 个候选includeWorktrees: true 把 636 个 linked worktree 全列了进来),rankRepos 截断到 50 并提示「另有 589 个未显示」。而用户配这个字段的本意,就是「钉住这一个目录」。

建议修法(1 为主,2 可同做):

  1. 兜底路径先试 describeProjectDir(dir)project-scanner.ts:76):是 git 仓库就直接产出唯一候选,不扫树。实测 7–11ms,且上述 18 个新增里有 14 个能命中;语义上也更贴合「钉住的目录」。剩下 4 个非仓库的大目录再落回扫描(或按修法 3 直接不兜底)。
  2. 兜底调用给 scanMultipleProjectsonBudgetExceeded(目前没传),撞上限时在卡片上说明「扫描根太大」——顺带把现有的静默截断也补上。
  3. 最保守:兜底只在 defaultWorkingDir 本身是 git 仓库时生效,否则维持「没扫到」的现状,风险面压到 0。

若采纳建议一,这层防护请同时覆盖 oncall 兜底路径——defaultOncall.workingDir 同样可能被配成大目录。(在上面那台机器的当前配置下暂时没有这样的 bot,属于前瞻性防护。)

📌 不属于本 PR 的 follow-up,不必在这里做DEFAULT_MAX_SCAN_MS 从 4000 下调到 ≤2000(压到 2500ms ACK 兜底之下),或把 scanMultipleProjects 异步化。这两项都超出 #1038 的范围,适合另开 issue,别让这个 PR 变重。


关于新增测试:三处有牙,漏了「接线」这一格

issueWorkingDirs 做了四次反向变异:

变异 结果
删掉兜底 return 🔴 1 failed ✅ 抓住
改成合并(explicit + default 都进候选) 🔴 1 failed ✅ 抓住
反向优先(default 覆盖 explicit) 🔴 1 failed ✅ 抓住
保留 issueWorkingDirs,只把调用点改回旧的内联写法 🟢 46 全绿 ❌ 漏掉

最后一行说明:新用例覆盖的是纯函数本身,buildIssueCommandDeps().workingDirs() 有没有真的接上去是没有断言的。建议补一个走 buildIssueCommandDeps() 的用例。若采纳建议一,也建议补一个 defaultOncall 形态的用例,否则新增的那半位同样测不出来。

🟡 仅供知悉,不必改

configuredWorkingDirs 会按逗号切分:defaultWorkingDir: '/root/tmp/a,b'["/root/tmp/a", "b"]。而该字段的其它所有消费方都把它当单个整体路径处理(expandHome(raw),从不切分)。路径里带逗号极其罕见,为此绕开工具函数不划算,记一笔即可。


你 PR 描述里的验证,逐条复现结果

  • vitest run --project unit test/issue-command.test.ts test/issue-command-deps.test.ts test/dashboard-bot-onboarding.test.ts3 files / 98 tests passed ✅ 与描述完全一致
  • tsc --noEmit --pretty false ✅ / git diff --check
  • 额外扩测 issue 全家桶 + working-dir + oncall + onboarding = 14 files / 365 passed
  • 新测试单独跑,确认没有写到 ~/.botmux
  • merge-tree 对当前 master 无冲突 ✅(分支落后于 master,合入前建议 rebase 一次)

辛苦了 🙏 建议一是一行替换,建议二按修法 1 也不复杂;改完再麻烦 ping 一下,我这边可以复跑一遍验证。

@pixystone
pixystone force-pushed the fix/issue-1038-default-dir branch from a9d70d4 to bf77f95 Compare August 28, 2026 03:08
@pixystone

Copy link
Copy Markdown
Contributor Author

@deepcoldy 已按反馈更新并推送:

  1. 兜底改用 effectiveDefaultWorkingDir,补齐 default-oncall 模式;
  2. fixed/oncall 目录不再进入递归扫描,只用 describeProjectDir 判断目录自身,是仓库则直接返回唯一候选,非仓库则维持空候选;
  3. 新增 buildIssueCommandDeps 接线测试、Oncall 候选生成全链路测试,以及显式扫描根优先/大目录不递归测试;
  4. 已 rebase 最新 master。

验证:Bun 1.4.0 完整 build 通过;相关扩展回归 14 files / 342 tests 通过;tsc --noEmitgit diff --check 通过。麻烦复跑确认。

@deepcoldy
deepcoldy merged commit 79d73ae into deepcoldy:master Aug 28, 2026
2 checks passed
@github-actions

Copy link
Copy Markdown

🚀 Released in v3.18.2

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

认领 issue 时扫不到仓库:dashboard 只能设 defaultWorkingDir,但 reposFor 只读 workingDir/workingDirs

2 participants