Skip to content

feat(dashboard): 群管理添加 bot 全部入群后自动收敛并优化结果展示 - #1238

Open
WaitRainbow wants to merge 1 commit into
deepcoldy:masterfrom
WaitRainbow:feat/add-bots-dialog-auto-close
Open

feat(dashboard): 群管理添加 bot 全部入群后自动收敛并优化结果展示#1238
WaitRainbow wants to merge 1 commit into
deepcoldy:masterfrom
WaitRainbow:feat/add-bots-dialog-auto-close

Conversation

@WaitRainbow

Copy link
Copy Markdown

背景

群管理里的「添加 bot」此前有三个体验问题:

  1. bot添加成功后弹窗不会关闭,外层群列表也常停在旧的成员状态,需要手动刷新;
  2. 添加结果只显示 bot 的 larkAppId,看不出是哪个 bot。
image 3. 当前群所有 bot 都已入群时,按钮仍可点击; image

改动要点

  • 成功即收敛并自动关闭:最后一批全部成功时自动关闭弹窗;仍有候选或存在失败项时保留弹窗,失败项与错误信息继续展示。

  • 本地乐观状态驱动:成功的 okIds 回传父组件先乐观标记 inChat,再复用 refreshUntilSeen 向服务端快照收敛,不再用单次后台 reload 冒充最终一致。

  • 四类竞态防护:收敛按 chat 串行 + generation/pending union,提交粒度缩到目标 chat 行,快照写入口统一为 sync-ref-first,杜绝「同群旧轮询回滚」「跨群整份快照覆盖」「同批绝对值互相覆盖」「state/ref 失配」。

  • 按钮禁用替代 toast:当前群没有可添加 bot 时,「添加 bot」按钮原生 disabled 置灰不可点;禁用态由当前 chat membership 与 bot roster 派生,数据更新后自动恢复。

image - **结果展示对齐点选框**:添加结果按「名称 +(ID)」展示,未知名称回退为 ID。 image

影响面

仅 Dashboard 群管理页添加 bot 弹窗与 groups snapshot 收敛逻辑;不影响新建群、保存 profile 等其它路径。

验证

  • bunx tsc --noEmit 通过;
  • vitest 目标套件 57/57 全绿(含组件级、父组件/轮询回写级回归、跨群不回滚、同批 canonical、失败项保留);
  • git diff --check 干净。

群管理里的「添加 bot」弹窗此前有两个体验问题:一是添加成功后弹窗不会关闭、
外层群列表也常停在旧的成员状态,需要手动刷新;二是当前群所有 bot 都已入群时,
按钮仍可点击,点了只弹一条「所有 bot 都已在群里」的 toast,比较突兀;三是添加
结果只显示 bot 的 larkAppId,看不出是哪个 bot。

本次改为本地乐观状态驱动的收敛,并配套收紧按钮可用性与结果展示:

- 最后一批全部成功时自动关闭弹窗;仍有候选或存在失败项时保留弹窗,失败项与错误
  信息继续展示
- 关闭判定走本地乐观状态:成功的 okIds 回传父组件先乐观标记 inChat,再复用
  refreshUntilSeen 向服务端快照收敛,不再用单次后台 reload 冒充最终一致
- 收敛按 chat 串行 + generation/pending union 防护,且提交粒度缩到目标 chat 行,
  快照写入口统一为 sync-ref-first,杜绝同群旧轮询回滚、跨群整份快照覆盖、同批
  绝对值互相覆盖、state/ref 失配四类竞态
- 当前群没有可添加 bot 时,「添加 bot」按钮原生 disabled 置灰不可点,禁用态由
  当前 chat membership 与 bot roster 派生,数据更新后自动恢复;不再走点击后 toast
- 添加结果按与点选框一致的「名称 +(ID)」展示,未知名称回退为 ID

影响面:仅 Dashboard 群管理页添加 bot 弹窗与 groups snapshot 收敛逻辑;不影响新建
群、保存 profile 等其它路径。

验证:bunx tsc --noEmit 通过;vitest 目标套件 57/57 全绿(含组件级与父组件/轮询回写
级回归、跨群不回滚、同批 canonical、失败项保留);git diff --check 干净。

Co-authored-by: TRAE CLI <traecli@bytedance.com>
@deepcoldy

deepcoldy commented Sep 4, 2026

Copy link
Copy Markdown
Owner

自动评审初步意见(Claude)

勘误(本条已于二轮复核后更新):F1 原文里「手动刷新按钮能兜底恢复」不成立,已更正;ok:true 那段举的「群主审批挂起」例子在本仓库的 bot 加群路径上也不成立,已换成真正会触发的成因。F1 的存在性与定级不变,另新增一条 F4。原文其余部分未改。

先说结论:改动方向和工程质量都很好 —— 三个体验问题定位准确,四类竞态的分析(generation / pending union / per-chat 提交 / sync-ref-first)不是纸上谈兵,mergeReconciledChat 那条「跨群快照会把别的群回滚」的推理尤其到位,注释也把"为什么这么写"讲清楚了。测试也扎实:本地在最新 origin/master4dcabacbd)上 rebase 后重跑,bunx tsc --noEmit 通过、目标两个套件 57/57 全绿,与描述一致。

下面一条是建议合入前处理的问题,其余为非阻断。


F1(建议修)pendingByChat 在轮询预算耗尽时没有清理,会把该群的收敛能力卡死

src/dashboard/web/groups.ts:438-469pendingByChat / runByChat 只在成功提交那一条路径上 deletegroups.ts:463-464)。如果某一批的 6 次轮询跑完仍未收敛,reconcile() 直接走完 for 循环返回,pending 集合原样留在 map 里

之后同一个群的任何新批次,expected 都会是「老批次的残留 id ∪ 新批次 id」。若那个残留 id 后来始终不出现在服务端快照里allExpectedInChat 就恒假 —— 这个群此后再也不会 commit。

探针验证(先在未变异代码上做了阳性对照,确认探针自身能捞到 commit):

批次A(cli_a,服务端始终不显示)→ 预算耗尽,不提交   ✅ 符合设计
批次B(cli_b,服务端已经显示)  → 期望提交,实际不提交 ❌
批次C(再来一次,同样已收敛)   → 仍然不提交         ❌ 该群被持续毒化
对照:另一个群 oc_y 不受影响 —— 泄漏是 per-chat 的

边界(这两点是二轮复核时更正的,比原文更准)

  1. 能自愈的情形:如果残留 id 只是传播慢于 6.6s 预算,它后来会出现在快照里,下一批的 union 自然满足 → 自动恢复。这是最常见的情形,也是本 PR 设计上就考虑到的。
  2. 不能自愈的情形:残留 id 始终不出现(加完又被移出群、目标 daemon 掉线等)。此时手动刷新按钮救不了 —— 我原文写的兜底是错的:reloadGroups 只是 setSnapshot(整份权威快照)完全不碰 reconciler 闭包里的两个 mapreconcilerRef 每次 mount 只创建一次,见 groups-page.tsx:1562-1564)。所以刷新只恢复一次显示,该群的下一批 add 仍然永不 commit,要到页面 remount 才清。我构造探针专门区分了这两条路径,结论如上。

所以真正限制影响面的不是"有兜底",而是"不能自愈的成因本身较少见"。修法就一行且已验证,我仍建议合入前处理。

关于 ok:true 的语义(此处更正原文)addBotToChatsrc/services/groups-store.ts:390-418)的 ok:true 含义是「Lark 返回 code 0 且该 id 不在 invalid_id_list」,它不等于「服务端快照里已可见」—— 这本来就是 reconciler 存在的前提,没有问题。但我原文举的「群主审批挂起/被拒」在本仓库的 bot 加群路径上是错的:仓库自己就把「加 bot 需群主审批」当硬失败处理(见 cli.ts:12287 的注释与回退分支),这类 id 会以 ok:false 返回,根本进不了 okIds,也就不会毒化 pending。F1 不依赖这个场景,结论不变,但论据请以上面这版为准。

建议改法(循环结束后补一段,只在自己仍是当前 generation 时清理,避免误删更新批次的状态):

    }
    // 预算耗尽仍未收敛:清掉本批次的 id,否则同群后续批次会被残留 union 阻塞。
    if (runByChat.get(chatId) === myRun) {
      pendingByChat.delete(chatId);
      runByChat.delete(chatId);
    }
  }

本地打上这段验证过:探针里「B 应该提交」由红转绿,且你原有的 57 个用例全部保持绿。如果采纳,建议顺手补一个「A 耗尽预算后 B 仍能提交」的回归用例 —— 现有 6 个 reconciler 用例都没覆盖「耗尽而不收敛」这条出口。

可对比:被替换掉的 refreshUntilSeengroups-page.tsx:1532没有跨调用状态expectedBotIds 是入参,所以不存在这个问题;状态是新引入的 closure map 带来的。


F2(非阻断)空 roster 时按钮禁用,tooltip 语义是反的

chatHasAddableBotsbots 为空数组时返回 false。此时每一行的按钮都会变灰并提示「所有 bot 都已在群里」,而真实情况恰恰相反(一个 bot 都没有)。快照尚未加载完、或全部 bot 离线时会出现。

页面在建群路径上已有正确措辞可复用(groups.noBotsOnline:「没有在线 bot。请先重启 daemon。」,见 groups-page.tsx:1637)。建议 tooltip 按 bots.length === 0 分支选词;禁用本身没问题。

F3(nit)setSnapshot 的函数式分支现在是死代码

改造后 6 个调用点(groups-page.tsx:1526/1543/1570/1588/1656/1669全部传绝对值typeof next === 'function' 分支无人走到。不影响正确性(保留它也让 sync-ref-first 语义更完整),纯可选。

F4(非阻断,新增)reconciler 之外的整份快照写入方,仍会造成 guard #3 要防的那类回滚

四类竞态防护都在 reconciler 内部。但 reconciler 之外还有整份 snapshot 的写入方:refreshUntilSeen(建群路径,groups-page.tsx:1543)和各 dialog 的 onReloadGroups({ force: true }),它们仍然是 setSnapshot(整份)

当 add-bots 的乐观态正在等收敛时,若建群轮询恰好命中、或用户在别的 dialog 做了操作,就会把别的群的乐观 inChat 回滚成服务端旧值 —— 正是 guard #3mergeReconciledChat)针对的同一类问题,只是来自另一个写入方。我用探针对比过两种提交方式:整份覆盖会把未传播的 X 回滚,row-scoped merge 则保留。

判为非阻断:reconciler 闭包状态不受影响,下一轮 poll 会重新 commit 该行 → 可自愈。建议后续refreshUntilSeen 的 commit 也 row-scope(直接复用 mergeReconciledChat),不必在本 PR 里做。


验证口径

  • 已在最新 origin/master4dcabacbd)上本地 rebase(未 force-push,未改动你的提交);rebase 无冲突,且 rebase 前后 diff 内容逐字相同,无 base 漂移。
  • 期间 origin/master 有 7 个 commit 落地,与本 PR 有文件重叠的只有 src/dashboard/web/i18n.tsfeat(dashboard): Dashboard 链接按平台绑定收敛 token,并给模型钉安全提示 #1241 改的是别的 key),无语义冲突。
  • bunx tsc --noEmit 通过;目标两套件 57/57(F1 修法打上前后各跑一遍,均全绿)。
  • 全量 vitest --project unit 本机有失败,但已确认与本 PR 无关:同一 worktree 同一环境下 origin/master 基线跑同一批文件同样失败(PR 28 / master 27),差集里唯一的 test/hook-runner.test.ts 单独隔离重跑时在 master 上 3 次里也红 2 次,属本机既有 flake(失败全是 timeout/环境类,且没有一个失败文件 import 本 PR 改到的模块)。
  • 顺带核过没问题的:unmount 后的 onCommittedrefreshRoleProfileContextmountedRef 守卫)、handleBotsAdded 同步读-改-写无 await 间隙、dialog 的 effectiveExclude 与父组件乐观态双写一致、submit 有 disabled 防重入、disabled/titleCreateActionButton 透传到原生 button。
  • 探针与临时改动均已还原,worktree 干净。

以上是自动评审的初步意见,最终以维护者审阅为准。整体质量不错,F1 那条修法只有一行且已验证不影响你现有用例,建议带上它和一个回归用例再合。辛苦!

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.

2 participants