feat(lark): 普通群 chat-topic 隔离原生话题 + 默认改 chat-topic/私聊 chat - #631
Conversation
deepcoldy
left a comment
There was a problem hiding this comment.
首审结论:🔴 CHANGES_REQUESTED — 1 个 P1 阻断项(shared 模式 split-brain)
由 Claude 首审。改动本身的核心机制是对的,但引入了一个 shared 模式专属、可复现的会话分裂(split-brain)回归,需修复后再合。
白话:这个 PR 在做什么
飞书「普通群」里,用户手动新建一个原生话题(客户端「消息 → 创建话题」)时,飞书给这条 seed 消息带 thread_id=omt_* 但没有 root_id。改动前 decideRoutingWithSource 走不到 real-thread 分支(它要求 root_id && thread_id 同时在),于是这条 seed 落到 regularGroupRouting,按普通群模式处理 —— 在 shared/chat 模式下被拍进「群大堂」chat-scope 会话,这个用户显式新建的话题就被吞了。
本 PR 加了一条判据:decideRoutingWithSource 里,在 topic-chat 检查之后,若 thread_id?.startsWith('omt_') 就判为 real-thread、thread-scope、anchor=messageId —— 让原生话题 seed 起自己独立的会话。同时给 maybeApplySharedTopicSeed 加了 independentTopicSeed 开关,当这是一条「thread-only 的原生 seed」(source==='real-thread' && thread_id && !root_id)时跳过 shared 折叠,防止刚判出来的独立 seed 又被折回大堂。放在 topic-chat 检查之后是对的:话题群 seed 仍保留 source=topic-chat、autoStartOnNewTopic 语义不变(测试已覆盖)。
🔴 P1:shared 模式下,原生话题里的后续 reply 会逃回群大堂(split-brain)
independentTopicSeed 只匹配 seed(thread-only,无 root_id)。但原生话题里的后续回复带 root_id + thread_id,independentTopicSeed=false。此时:
maybeFoldMentionedRegularGroupThreadToChat因ownsThreadSession=true首行返回 undefined(reply 命中 seed 建的 thread 会话)—— 正确;- 但随后仍无条件进入
maybeApplySharedTopicSeed,该 helper 没有ownsThreadSession守卫,independentTopicSeed又不匹配 reply,于是把routing改成{scope:'chat', anchor:chatId}并返回 messageId; - chat anchor 上没有 owner → 最终走
handleNewTopic,这条 reply 落到群大堂,而不是 seed 建的独立 thread 会话 → 一个话题,两个会话。
实证(隔离树跑 decideRouting/完整 handler,PR 与 master 对拍,同一 stateful seed→登记 owner→reply 序列):
master (6dcca31b): SEED → {NT, scope:chat, anchor:chat-reply-mode} ← 折进大堂
REPLY → {TR, scope:chat, anchor:chat-reply-mode} ← 同一会话,一致
PR (345b689): SEED → {NT, scope:thread, anchor:msg-native-seed} ← 独立会话(本 PR 目的✅)
REPLY → {NT, scope:chat, anchor:chat-reply-mode} ← 逃回大堂 ❌ split-brain
对拍证明这是 PR 引入的回归,不是既存问题:master 上 seed 本就折进大堂,不产生这个独立 owner,所以既存的 maybeApplySharedTopicSeed root+thread 漏洞不可达;PR 让 seed 独立后才把它变成可达的连续性破坏。
blast radius:仅 shared 模式。 chat 模式对拍验证 reply 正确续在 thread 会话(maybeApplySharedTopicSeed 对非 shared 模式提前 return,ownsThreadSession 守卫生效);new-topic/chat-topic 不受影响。
修复方向(已在隔离树验证,零回归)
maybeApplySharedTopicSeed 是话题 seed 逻辑,不该处理已在 thread 里的 reply。在 helper 开头加一条守卫即可:
// A message already inside a native thread (root_id + thread_id) is a reply,
// not a topic seed; folding it would divert an owned native-topic thread
// session into the group lobby (split-brain). Only fold genuine seeds.
if (message?.root_id && message?.thread_id) return undefined;隔离树上加此 4 行 + 一条 reply-continuity 回归测试:event-dispatcher 全量 241/241 通过(240 既存 + 新增),FIX-PROBE 确认 reply 正确续在 {scope:thread, anchor:msg-native-seed},现有 shared 用例全绿。建议作者顺带补一条完整 handler 级 seed→reply 连续性回归(现有新测试只覆盖 decideRouting 纯函数层与 seed 单点,未覆盖 reply 在完整 dispatch 下的会话归属)。
验证记录(隔离树,钉 PR SHA 345b689)
pnpm build✅ /pnpm exec tsc --noEmit✅(EXIT 0)pnpm vitest run test/event-dispatcher.test.ts→ 240/240 ✅(含 4 条新测试实跑,非 skip)- lark 路由 blast-radius 9 文件 373/373 ✅(message-parser / reply-mode-command / relay-target-routing / trigger-session-reply-mode / forward-followup-* / card-prefs-auto-start / grant-gates / summary-command)
- 全量 non-e2e 套件因本机并发测试负载被 SIGTERM,未完整采样;codex 独立复审已将全量 4 个失败在 master 复现为基线问题
结论:核心思路正确,合并前需堵上 shared 模式 reply 逃逸这个 P1。未获申晗确认前不合码。
复审收敛 + 补充:模式语义变化的完整实证矩阵(附给申晗的决策点)Codex 复审与首审收敛:同意 实证:4 模式 × (seed / owned-reply) 路由矩阵(PR 345b689 vs master 6dcca31 对拍)原生话题 seed =
三点在首审之上的补充结论
给申晗的决策点(一句话)「普通群里用户显式创建的原生 Lark 话题,是否应无视
无论哪条,当前 SHA 都不合码。另按仓库规范:标题/commit/描述需改中文格式(建议标题 (实证:PR/master 双树钉 SHA 隔离跑 |
补充实证:codex 的「结构性 reply-as-seed 漏洞」我已确认为既存(独立于本 PR)Codex 复审指出:root+thread guard 修掉的是 PR 新增的 native-seed→reply 回归路径,但 实证(PR 345b689 vs master 6dcca31 对拍,handler 级)三条入口的共同形状 = 「shared 模式 + bot 已拥有一个 thread-scope 会话 + 收到一条 root+thread reply」。直接构造这个状态: 归属探针(靠 replyRootId 区分是哪个函数折的: 结论
诚实的范围边界我实证的是汇聚点可达(给定 shared 模式下存在 thread owner,reply 必被折回 lobby)。codex 列的三条具体入口是否都能端到端产生这个 owner(尤其 mode-switch 的 stale 会话驱逐、restore/adopt 的 owner 重建),我没有逐条跑到底 —— 这属于既存 bug 的 follow-up 范围,建议作者/申晗定语义方向后单独立项验证。它不改变本 PR 的合码判定。 双审最终建议(收敛)
|
双审收敛 —— 验收清单(供作者修复后自检,避免漏项)Claude 首审 + codex 复审已完全收敛。当前 SHA 通用(两条路线都必须满足)
若选 global(原生话题始终压过 /reply-mode)
若选 narrow(仅 chat-topic + new-topic 隔离原生话题)
Follow-up(不阻塞本 PR 产品决策)
验证基线(双审已完成)
|
申晗决策 → narrow(遵循 dashboard 配置);PR 非完全多余,但需收窄申晗判断:「应遵循 dashboard 中的 /reply-mode 配置」= narrow 语义。据此核实 PR 是否必要,结论:不是完全多余,但当前 global 形态过宽,需收窄到只修真实缺口。 核实过程(clean master 6dcca31 实证)关键问题:遵循配置的前提下, master 已有 3 条 补跑纯 seed 探针(clean master): → master 上 chat-topic 的纯话题根 seed 被折进群 lobby,违反它自己的文档契约。 这是真实缺口,不是臆想。 因此
narrow 修法(遵循配置,最小充分)
一句话:PR 修的问题真实存在(chat-topic 纯 seed),但应收窄成「只补 chat-topic」而非「全局改写」;收窄后既符合「遵循 dashboard 配置」,又消灭 shared P1。等作者按此收窄 + 补测 + 改中文规范,双审复验。 |
345b689 to
0d00d45
Compare
|
已按 review 的 global 验收路线完成修复并更新 PR:
最新 commit: |
在 hu5h 的原生话题隔离修复(0d00d45)基础上,按申晗决策做两处调整: 1. 收窄原生话题隔离到 chat-topic(遵循 /reply-mode 配置) hu5h 版对所有模式都隔离用户创建的原生话题;改为仅 chat-topic 隔离, chat/shared 按文档契约仍把原生话题折进群会话。两处 omt_ 判据都加 `resolveRegularGroupMode === 'chat-topic'` 门控: - decideRoutingWithSource 的 seed 分支; - maybeFoldMentionedRegularGroupThreadToChat 的 reply-entry 分支 (hu5h 新增,保留其"bot 从 reply 进入话题也隔离"的更完整覆盖)。 移除随之失效的 independentTopicSeed 开关(其作用已被 hu5h 的 `root_id && thread_id` 守卫覆盖,守卫保留——它同时堵住经 /t、模式切换、 restore/adopt 到达的既存 reply-as-seed split-brain)。 2. 两个 per-bot 默认值翻转 - 普通群 regularGroupReplyMode 默认 chat → chat-topic(顶层平铺 + 原生 话题各自独立;依赖上面的 chat-topic seed 隔离才在默认态生效); - 私聊 p2pMode 默认 thread → chat(整段 DM 一个连续会话)。 同步反转所有"默认值→归一化 undefined"的平行拷贝点(普通群 5 处、 私聊 ~12 处),否则显式选中新的非默认值会被静默丢弃。i18n 中英 帮助/标签的默认标记与 narrow 语义同步(还原 hu5h 的"所有模式隔离"表述)。 测试: event-dispatcher 245、命中面 8 文件 644、全量 non-e2e 与干净 master 同基线(env/时区/bwrap 预存失败)零回归;tsc/build 通过。 Co-Authored-By: Riff <noreply@riff.dev>
过程摘要:按维护者决策落地(narrow + 默认翻转)维护者(申晗)决策:B(narrow,遵循 dashboard /reply-mode 配置)+ 两个默认值翻转。 已在 @hu5h 的原生话题隔离修复(commit
验证:tsc/build ✅;event-dispatcher 245/245;命中面 8 文件 644/644;全量 non-e2e 11010 passed,余 10 failed 全在 标题/描述已更新为中文规范格式。等 @codex 复审后交维护者拍板合码。 |
|
To use Codex here, create a Codex account and connect to github. |
deepcoldy
left a comment
There was a problem hiding this comment.
复审结论:逻辑主干已收敛;narrow 门控、shared 的 root_id && thread_id 守卫、两个默认翻转的持久化/归一化拷贝点均未发现阻断性问题。当前还需收尾 1 组 P2 文案/回归覆盖,修完可直接复验批准。
P2:用户帮助把 new-topic 的实际行为说错了
当前中英文 /reply-mode 帮助与 dashboard help 均写成“仅 chat-topic 隔离用户创建的原生话题 / Only chat-topic isolates…”。但代码的既定行为是:
- native seed 在
new-topic下经regularGroupRouting返回 thread-scope; - native reply 在
new-topic下又被mode === 'new-topic'明确挡在 fold 外; - 现有 handler 测试也锁了
new-topic的原生话题 reply 为独立 thread。
因此准确表述应是:chat/shared 折回群会话;new-topic/chat-topic 都保持独立;chat-topic 的独特点是“顶层仍平铺、只让原生话题独立”。请同步修正 src/i18n/{zh,en}.ts 与 dashboard 中英 help。
同时建议把纯路由矩阵缺失的 new-topic native seed 一格补上:当前覆盖了 chat/shared seed、chat-topic seed+reply、new-topic reply,但未直接锁 seed 经 regularGroupRouting 仍为 thread 的契约。
同批清理默认翻转的旧说明
还有几处旧默认注释残留,容易让下一轮维护者误判:
card-builder.ts仍写“thread(默认)”;relay-target-routing.ts规则 3 与函数内仍写thread default;dashboard.ts的 p2p-mode 代理注释仍写“清回 per-message thread default”;event-dispatcher.ts仍写chat is the flat default;- 一条 dispatcher 测试注释仍写
regularGroupReplyMode unset(chat)。
另外 /botconfig 的 p2p chat 选项中英文没有迁移“默认/default”标记,与 dashboard 的新默认标记不一致;建议一起补齐。
独立验证(钉 head e695d417)
pnpm exec vitest run(8 个命中文件):8/8 files,644/644 tests ✅pnpm exec tsc --noEmit✅pnpm build✅git diff --check origin/master...HEAD✅- worktree clean;未改代码、未合码。
codex 复审 P2(纯文案/测试,无逻辑改动): 1. 帮助文案纠错:此前误称「仅 chat-topic 隔离原生话题」,但 new-topic 的 seed(经 regularGroupRouting 进 thread)与 reply(被 mode==='new-topic' 挡在 fold 外)实际也保持独立。改为准确表述:chat/shared 折进群会话, chat-topic 与 new-topic 都让原生话题独立(chat-topic 的独特点=顶层平铺、 仅原生话题独立)。涉及中英 /reply-mode usage + dashboard regularGroupModeHelp。 2. 补 4 模式矩阵缺格:新增「new-topic native seed → thread-scope via regularGroupRouting(source=regular-group-thread,非 omt_ 分支)」用例, 锁死「走 regularGroupRouting 但仍 thread」语义,防未来 omt_ 门控改动 误伤 new-topic seed。 3. 清理默认翻转遗留的旧注释/标记:card-builder p2p「thread(默认)」、 relay-target-routing 规则 1/3 默认标注、dashboard.ts 代理注释、 event-dispatcher regularGroupRouting「chat is flat default」、一条测试 注释 unset(chat)→unset(chat-topic);/botconfig p2p chat 选项中英补「默认」 标记,与 dashboard 一致。 测试: event-dispatcher 246/246(+1 new-topic seed 格)、blast-radius 4 文件 401/401;tsc/build 通过。运行逻辑零改动。 Co-Authored-By: Riff <noreply@riff.dev>
复审 P2 已修(纯文案/测试,运行逻辑零改动)针对 @codex CHANGES_REQUESTED(review #4799146760)的三组 P2,已推 commit
验证:event-dispatcher 246/246(+1 new-topic seed 格)、blast-radius 4 文件 401/401;tsc/build 通过; head 现为 |
|
To use Codex here, create a Codex account and connect to github. |
relay-target-routing.ts:62 的分支内注释仍写「私聊默认(thread 模式)」,但默认 翻转后该分支只有 p2pMode 显式为 'thread' 才可达(默认 chat 已在规则 1 的 `!== 'thread'` 判据返回)。改为「私聊显式 thread 模式」,与文件顶部规则块及 event-dispatcher 的平行注释一致。纯注释,零逻辑/测试改动。 Co-Authored-By: Riff <noreply@riff.dev>
deepcoldy
left a comment
There was a problem hiding this comment.
最终复审通过(钉 head da7673e8)。
前轮 P1/P2 均已闭环:
- native-topic 行为已按产品决策收窄:
chat/shared折进群会话;chat-topic/new-topic保持独立,且仅chat-topic保持顶层平铺; - shared seed helper 的
root_id && thread_id守卫保留,owned thread reply 不再被二次折回 lobby; - 普通群默认
chat-topic、私聊默认chat的读取、写盘归一化、热更新、路由、dashboard、配置卡和命令路径均一致; - 中英文帮助、默认标记与内部注释已同步,
new-topicnative seed 矩阵格已补。
独立验证:
- 4 个 blast-radius 文件:401/401 tests ✅(event-dispatcher 246、card-builder 130、relay-target-routing 13、reply-mode-command 12)
pnpm exec tsc --noEmit✅pnpm build✅git diff --check origin/master...HEAD✅- 旧默认/“仅 chat-topic”残留 grep 归零;worktree clean ✅
APPROVED。按约定这里只给 review,不执行合并;等待申晗最终确认。
📊 会话路由决策图(合并后语义)本 PR 后,「普通群消息如何映射到会话」按 消息形态 × flowchart TD
A["飞书普通群消息<br/>用户 @bot"] --> B{"消息形态?"}
B -->|"原生话题 seed<br/>thread_id=omt_*, 无 root_id"| C{"/reply-mode?"}
B -->|"话题内 reply<br/>root_id + thread_id"| D{"/reply-mode?"}
B -->|"顶层消息<br/>无 thread_id"| E{"/reply-mode?"}
C -->|"chat / shared"| C1["折进群大堂<br/>chat-scope"]
C -->|"chat-topic / new-topic"| C2["原生话题独立会话<br/>thread-scope @ seed"]
D -->|"chat / shared"| D1["折回群大堂<br/>chat-scope"]
D -->|"chat-topic / new-topic"| D2["续该话题独立会话<br/>thread-scope @ root"]
E -->|"chat / shared / chat-topic"| E1["群大堂连续会话<br/>chat-scope 顶层平铺"]
E -->|"new-topic"| E2["每条 @ 独立会话<br/>thread-scope"]
style C2 fill:#d4f4dd,stroke:#2a9d5c,color:#000
style D2 fill:#d4f4dd,stroke:#2a9d5c,color:#000
style C1 fill:#fde8d4,stroke:#d9822b,color:#000
style D1 fill:#fde8d4,stroke:#d9822b,color:#000
style E1 fill:#e8eef9,stroke:#4269b8,color:#000
style E2 fill:#e8eef9,stroke:#4269b8,color:#000
四种模式一览
私聊(DM)默认也变了
|
概述
普通群里用户手动创建的原生 Lark 话题(seed 消息带
thread_id=omt_*但无root_id)此前会被折进群「大堂」chat-scope 会话,用户显式开的话题被吞。本 PR 修复该问题,并按会话模式配置精确控制隔离范围,同时调整两个 per-bot 默认值。改了什么
1. 原生话题隔离(收窄到 chat-topic 模式)
chat-topic模式:用户创建的原生话题起独立 thread-scope 会话——无论 bot 是从话题的开场白(seed)还是后续 reply 首次进入,都保持独立(兑现 chat-topic「顶层平铺连续会话;群内原生话题各自独立会话」的契约)。chat/shared模式:按/reply-mode文档契约,原生话题仍折进群会话(不隔离)。new-topic模式:不受影响(本就每个顶层 @ 各开独立会话)。decideRoutingWithSource的 seed 分支与maybeFoldMentionedRegularGroupThreadToChat的 reply-entry 分支都以resolveRegularGroupMode === 'chat-topic'门控。2. seed-helper 守卫(修既存 split-brain)
maybeApplySharedTopicSeed开头加if (message?.root_id && message?.thread_id) return undefined:一条已在真实 thread 里的消息是 reply 而非 seed,seed helper 永不应处理它。这堵住了一个既存竞态(与本次隔离无关,PR/master 同款):bot 经/t、模式切换、restore/adopt 已拥有某 thread 会话后,该话题里的 reply 会被误折回群大堂,造成一个话题两个会话。3. 两个 per-bot 默认值翻转
regularGroupReplyMode默认chat→chat-topic:顶层平铺连续会话 + 群内原生话题各自独立(依赖上面的 chat-topic seed 隔离才在默认态生效)。p2pMode默认thread→chat:整段 DM 共用一个连续会话。默认翻转涉及「默认值→归一化为 undefined 保持 bots.json 干净」的判定,已同步反转所有平行拷贝点(普通群 5 处、私聊约 12 处),否则显式选中新的非默认值会被静默丢弃。
/reply-mode与 dashboard 帮助/下拉标签的「默认」标记随之迁移(中英双语)。影响面(多 CLI × 多后端 × 多 IM 横向评估)
im/lark/event-dispatcher、relay-target-routing、reply-mode-command、card-builder)+ 会话模式存储(chat-reply-mode-store、card-prefs-store、bot-registry)+ dashboard 配置(dashboard-ipc-server、bot-payload、bot-defaults-page)+ i18n。不涉及 CLI 适配器 / PtyBackend / TmuxBackend / worker。topic-chat(autoStartOnNewTopic语义不变);普通群 4 模式逐一核对;私聊 chat/thread 两态;adopt/restore 场景由 seed-helper 守卫覆盖。验证
pnpm exec tsc --noEmit✅pnpm build✅pnpm vitest run test/event-dispatcher.test.ts→ 245/245 ✅(重写 2 条依赖旧全局行为的用例 + 新增多条:chat-topic seed 独立 / chat+shared seed 折叠 / shared owned-reply 不被折回 / p2p 默认 chat / 显式 thread 保留旧行为)fs-policy-bwrap/schedule-card-model/scheduler/v3-distillation-runner四个文件,与干净 master(6dcca31)逐条一致 = 环境/时区/bwrap 基线,零回归说明
本 PR 在 @hu5h 的原生话题隔离修复(commit 0d00d45)基础上,按维护者决策把隔离范围从「所有模式」收窄到「仅 chat-topic」(遵循 dashboard
/reply-mode配置),并叠加两个默认值翻转。保留了 hu5h 的 seed-helper 守卫与「reply-entry 也隔离」的更完整覆盖。