Skip to content

feat(lark): 普通群 chat-topic 隔离原生话题 + 默认改 chat-topic/私聊 chat - #631

Merged
deepcoldy merged 4 commits into
deepcoldy:masterfrom
hu5h:upstream/regular-group-topic-isolation
Jul 28, 2026
Merged

feat(lark): 普通群 chat-topic 隔离原生话题 + 默认改 chat-topic/私聊 chat#631
deepcoldy merged 4 commits into
deepcoldy:masterfrom
hu5h:upstream/regular-group-topic-isolation

Conversation

@hu5h

@hu5h hu5h commented Jul 28, 2026

Copy link
Copy Markdown

概述

普通群里用户手动创建的原生 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 默认 chatchat-topic:顶层平铺连续会话 + 群内原生话题各自独立(依赖上面的 chat-topic seed 隔离才在默认态生效)。
  • 私聊 p2pMode 默认 threadchat:整段 DM 共用一个连续会话。

默认翻转涉及「默认值→归一化为 undefined 保持 bots.json 干净」的判定,已同步反转所有平行拷贝点(普通群 5 处、私聊约 12 处),否则显式选中新的非默认值会被静默丢弃。/reply-mode 与 dashboard 帮助/下拉标签的「默认」标记随之迁移(中英双语)。

影响面(多 CLI × 多后端 × 多 IM 横向评估)

  • 仅飞书路由层(im/lark/event-dispatcherrelay-target-routingreply-mode-commandcard-builder)+ 会话模式存储(chat-reply-mode-storecard-prefs-storebot-registry)+ dashboard 配置(dashboard-ipc-serverbot-payloadbot-defaults-page)+ i18n。不涉及 CLI 适配器 / PtyBackend / TmuxBackend / worker。
  • 跨会话类型:话题群 seed 仍走 topic-chat(autoStartOnNewTopic 语义不变);普通群 4 模式逐一核对;私聊 chat/thread 两态;adopt/restore 场景由 seed-helper 守卫覆盖。
  • 跨平台:纯路由/配置逻辑,无路径/shell/PTY/编码相关改动,Linux/macOS 一致。

验证

  • pnpm exec tsc --noEmit
  • pnpm build
  • pnpm vitest run test/event-dispatcher.test.ts245/245 ✅(重写 2 条依赖旧全局行为的用例 + 新增多条:chat-topic seed 独立 / chat+shared seed 折叠 / shared owned-reply 不被折回 / p2p 默认 chat / 显式 thread 保留旧行为)
  • 命中面 8 文件(event-dispatcher / bot-registry-grant / card-prefs-auto-start / relay-target-routing / reply-mode-command / command-handler / trigger-session-reply-mode / card-builder)→ 644/644
  • 全量 non-e2e 套件:11010 passed,余 10 failed 全部集中在 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 也隔离」的更完整覆盖。

@deepcoldy deepcoldy left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

首审结论:🔴 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-chatautoStartOnNewTopic 语义不变(测试已覆盖)。

🔴 P1:shared 模式下,原生话题里的后续 reply 会逃回群大堂(split-brain)

independentTopicSeed 只匹配 seed(thread-only,无 root_id)。但原生话题里的后续回复root_id + thread_id,independentTopicSeed=false。此时:

  1. maybeFoldMentionedRegularGroupThreadToChatownsThreadSession=true 首行返回 undefined(reply 命中 seed 建的 thread 会话)—— 正确;
  2. 但随后仍无条件进入 maybeApplySharedTopicSeed,该 helper 没有 ownsThreadSession 守卫,independentTopicSeed 又不匹配 reply,于是把 routing 改成 {scope:'chat', anchor:chatId} 并返回 messageId;
  3. 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。未获申晗确认前不合码。

@deepcoldy

Copy link
Copy Markdown
Owner

复审收敛 + 补充:模式语义变化的完整实证矩阵(附给申晗的决策点)

Codex 复审与首审收敛:同意 CHANGES_REQUESTED,P1 根因/修复方向一致。Codex 另提的#3(/reply-mode 公开契约变化)我已独立实证确认,并发现它比描述的更广、且与 P1 存在结构性耦合

实证:4 模式 × (seed / owned-reply) 路由矩阵(PR 345b689 vs master 6dcca31 对拍)

原生话题 seed = thread_id=omt_*root_id;reply = root_id + thread_id,owner 登记在 seed 实际建的 anchor 上。

模式 master seed PR seed master reply PR reply 文档契约(zh.ts:219/en.ts:216/store:6-16)
chat {chat, 大堂} {thread, seed} {chat, 大堂} {thread, seed} 「连原生话题也并进这个会话」→ PR 违反
shared/topic {chat, 大堂} {thread, seed} {chat, 大堂} {chat, 大堂} 「复用同一个 group session」→ PR 违反 + P1 split-brain
new-topic {thread, seed} {thread, seed} 不变 ✓
chat-topic {chat, 大堂} {thread, seed} {thread, seed} {thread, seed} 「各自独立会话」→ master 本就 seed/reply 不一致,PR 修好了

三点在首审之上的补充结论

  1. 变化面 = 3/4 模式(chat / shared / chat-topic),不止 chat+shared。 尤其:PR 后 chat 模式对原生话题的行为chat-topic 完全一致 —— 文档里刻意区分的两个模式(chat=并进 / chat-topic=独立)在原生话题上被抹平。

  2. master 的 chat-topic 自身有一个既存 seed/reply 分裂:seed 折进大堂、reply 却走独立 thread(见表)。PR 顺带把它修一致了 —— 所以对 chat-topic 而言 PR 是改善,其文档「各自独立会话」PR 后才真正成立。

  3. ⭐ P1 与语义决策是耦合的,这决定了修复形态:

    • 若申晗确认「用户显式建原生话题应始终压过 chat/shared/chat-topic」(global 语义):当前结构(omt_ 分支放在 regularGroupRouting 之前)正确,需 ① 首审/复审共识的 guard if (message?.root_id && message?.thread_id) return undefined 堵 P1 ② 同步 zh/en /reply-mode 帮助 + store 注释 ③ 补 4 模式矩阵测试。
    • 若应「只让 chat-topic(+new-topic)隔离原生话题」(narrow 语义):修复不是 guard,而是把 omt_ 分支收窄到仅 chat-topic 触发。关键:这样收窄后 P1 自动消失 —— 因为 maybeApplySharedTopicSeed 只在 shared 模式运行(resolveRegularGroupMode !== 'shared' 提前 return),shared 不再产生独立 seed 就没有可被折散的 reply。
    • 补充:chat-topic 模式本就是为「顶层平铺+原生话题独立」设计的。所以真正的产品问题是:是否需要 chat/shared 也复制这个能力,还是引导用户改用 chat-topic?

给申晗的决策点(一句话)

「普通群里用户显式创建的原生 Lark 话题,是否应无视 /reply-mode 配置、始终起独立会话?」

  • 是 → global 语义:guard 修 P1 + 同步中英文档 + 4 模式矩阵测试。
  • 否 → narrow 语义:omt_ 分支收窄到 chat-topic(顺带消灭 P1),chat/shared 维持文档现状。

无论哪条,当前 SHA 都不合码。另按仓库规范:标题/commit/描述需改中文格式(建议标题 fix(lark): 隔离普通群用户创建的话题),补中文影响面 + 实测记录。

(实证:PR/master 双树钉 SHA 隔离跑 event-dispatcher handler 级探针;候选 guard + 连续性回归 241/241 绿。)

@deepcoldy

Copy link
Copy Markdown
Owner

补充实证:codex 的「结构性 reply-as-seed 漏洞」我已确认为既存(独立于本 PR)

Codex 复审指出:root+thread guard 修掉的是 PR 新增的 native-seed→reply 回归路径,但 maybeApplySharedTopicSeed结构性 reply-as-seed 漏洞在 narrow 语义下仍经 /t / 模式切换 / restore-adopt 可达。我同意这个不变量判断,并把它的核心汇聚点跑了实证。

实证(PR 345b689 vs master 6dcca31 对拍,handler 级)

三条入口的共同形状 = 「shared 模式 + bot 已拥有一个 thread-scope 会话 + 收到一条 root+thread reply」。直接构造这个状态:

LATENT routed (PR):     {NT, scope:chat, anchor:大堂, replyRootId:messageId}
LATENT routed (master): {NT, scope:chat, anchor:大堂, replyRootId:messageId}   ← 逐字节相同

归属探针(靠 replyRootId 区分是哪个函数折的:maybeFold 返回 rootId,maybeApplySharedTopicSeed 返回 messageId):

ATTRIB (PR):     replyRootId = messageId  → 折叠者 = maybeApplySharedTopicSeed
ATTRIB (master): replyRootId = messageId  → 折叠者 = maybeApplySharedTopicSeed

结论

  1. 确认既存:PR 与 master 逐字节一致 → 这是 maybeApplySharedTopicSeed 的既存结构性 bug,不是 PR 引入。带 root_id 的 reply 永远进不了 PR 的 omt_ 分支(先被 rootId && threadId 拦到 real-thread),所以 PR 无从影响它。
  2. 归属精确:maybeFoldownsThreadSession=true 正确退出,但控制流仍落进无 owner 守卫的 maybeApplySharedTopicSeed,由它把 owned-thread reply 折回 lobby。
  3. guard 是对的不变量:if (message?.root_id && message?.thread_id) return undefined 同时修掉 PR 新路径这个既存汇聚点 —— seed helper 永不应处理真实 thread reply。所以即便选 narrow 语义,也应保留这个 guard(或至少另开 follow-up 修既存 split-brain)。

诚实的范围边界

我实证的是汇聚点可达(给定 shared 模式下存在 thread owner,reply 必被折回 lobby)。codex 列的三条具体入口是否都能端到端产生这个 owner(尤其 mode-switch 的 stale 会话驱逐、restore/adopt 的 owner 重建),我没有逐条跑到底 —— 这属于既存 bug 的 follow-up 范围,建议作者/申晗定语义方向后单独立项验证。它不改变本 PR 的合码判定。

双审最终建议(收敛)

  • 阻塞判定不变:当前 SHA 不合码。
  • global 语义:omt_* 全局优先 + root+thread guard(必需)+ 同步中英文档 + 4 模式矩阵测试。
  • narrow 语义(codex 与我都略倾向:chat-topic 已专门表达「顶层群会话+原生话题独立」,语义最可预测):omt_* 特判收窄到仅 chat-topic → PR 新增的 shared split-brain 自动消失;仍建议保留 guard 修既存汇聚点,或另开明确 follow-up。
  • 决策点给申晗仍是一句话:「用户显式创建的原生 Lark 话题,是否应无视 /reply-mode、始终独立?」

@deepcoldy

Copy link
Copy Markdown
Owner

双审收敛 —— 验收清单(供作者修复后自检,避免漏项)

Claude 首审 + codex 复审已完全收敛。当前 SHA 345b689 = CHANGES_REQUESTED / BLOCKED,等申晗拍 global / narrow 语义方向,未确认前不改、不合。定向后作者按下列清单修复,双审再复验。

通用(两条路线都必须满足)

  • 保留 root+thread guard(或明确 follow-up):maybeApplySharedTopicSeed 开头加 if (message?.root_id && message?.thread_id) return undefined。这是正确不变量:seed helper 永不应处理真实 thread reply。
  • handler 级回归:shared + 已有 owned thread + root/thread reply 不得进入 shared seed helper,应续在原 thread 会话(断言 handleThreadReply({scope:'thread', anchor:seedMessageId})handleNewTopic 不调用)。
  • human 主路径必须覆盖;foreign-bot 共用同一 helper,最好补对应断言(两处调用点)。
  • 标题 / commit / PR 中文描述 + 影响面按仓库规范补齐(建议标题 fix(lark): 隔离普通群用户创建的话题,附实测记录)。

若选 global(原生话题始终压过 /reply-mode)

  • 4 模式矩阵:原生 seed 在 chat / shared / new-topic / chat-topic 全部为 thread-scope
  • 同步 zh.ts:219 / en.ts:216 /reply-mode 帮助 + chat-reply-mode-store.ts:6-16 注释(chat/shared 不再「并进群会话」)。

若选 narrow(仅 chat-topic + new-topic 隔离原生话题)

  • omt_* 特判收窄到仅 chat-topic 触发(new-topic 本就由 regularGroupRouting 进 thread)。
  • 矩阵:chat / shared seed 保持 chat-scope;chat-topic / new-topic seed 为 thread-scope
  • 仍保留 guard:narrow 消灭的是 PR 新增的 shared native-topic split-brain,但 maybeApplySharedTopicSeed 的既存 reply-as-seed 汇聚点(PR/master 逐字节一致)不受语义收窄影响,guard 用于兜住它。

Follow-up(不阻塞本 PR 产品决策)

  • /t 强制建 thread / mode-switch 切 shared / restore-adopt 三条入口是否都能端到端产生 shared 下的 thread owner,逐条 e2e 验证 —— 归为既存 bug 的 follow-up。但 guard 的通用回归测试应留在本次修复里,不推迟。

验证基线(双审已完成)

  • PR SHA 345b689 隔离对拍:event-dispatcher 240/240 ✅、tsc/build ✅、lark 路由 blast-radius 9 文件 373/373 ✅;候选 guard + 连续性回归 241/241 ✅;全量 4 失败在 master 原样复现 = 环境基线,不归因本 PR。
  • 4 模式矩阵 + LATENT/ATTRIB 实证见上方两条 comment。

@deepcoldy

Copy link
Copy Markdown
Owner

申晗决策 → narrow(遵循 dashboard 配置);PR 非完全多余,但需收窄

申晗判断:「应遵循 dashboard 中的 /reply-mode 配置」= narrow 语义。据此核实 PR 是否必要,结论:不是完全多余,但当前 global 形态过宽,需收窄到只修真实缺口

核实过程(clean master 6dcca31 实证)

关键问题:遵循配置的前提下,chat-topic 模式(其设计契约就是「顶层平铺连续会话;群内原生话题各自独立会话」,store 注释 :13-17)本身在 master 上是否正常?

master 已有 3 条 chat-topic 隔离测试(event-dispatcher.test.ts:3439/3476/…)全绿,但它们用的都是 root_id + thread_id(话题内回复)—— 这命中 mode 判定之前real-thread 分支,本就隔离正确。没有一条覆盖纯话题根 seed(thread-only,无 root_id)。

补跑纯 seed 探针(clean master):

CTSEED (chat-topic, thread-only seed) routed = {TR, scope:chat, anchor:群lobby}

master 上 chat-topic 的纯话题根 seed 被折进群 lobby,违反它自己的文档契约。 这是真实缺口,不是臆想。

因此

  • PR 的价值点 = 修 chat-topic 纯 seed 未隔离(合理,值得留)。
  • PR 的过宽处 = 顺带把 chat / shared 的原生话题也改成独立(这两个模式文档明确说「并进群会话」)—— 遵循配置就不该动它们。且正是这个过宽引入了 shared 模式 P1 split-brain。

narrow 修法(遵循配置,最小充分)

  1. omt_* 特判收窄到仅 chat-topic 触发(new-topic 本就由 regularGroupRouting 进 thread);chat / shared seed 维持 chat-scope = dashboard 配置语义不变。
  2. 这样 shared P1 split-brain 自动消失(maybeApplySharedTopicSeed 只在 shared 运行,shared 不再产生独立 seed)。
  3. 仍保留 root+thread guard 兜住 maybeApplySharedTopicSeed 的既存 reply-as-seed 汇聚点(PR/master 逐字节一致的既存 bug,与语义无关)。
  4. 补 handler 级回归:chat-topic 纯 seed → 独立 thread 会话;shared owned-thread reply → 不进 seed helper。

一句话:PR 修的问题真实存在(chat-topic 纯 seed),但应收窄成「只补 chat-topic」而非「全局改写」;收窄后既符合「遵循 dashboard 配置」,又消灭 shared P1。等作者按此收窄 + 补测 + 改中文规范,双审复验。

@hu5h
hu5h force-pushed the upstream/regular-group-topic-isolation branch from 345b689 to 0d00d45 Compare July 28, 2026 09:39
@hu5h hu5h changed the title fix(lark): isolate regular-group user-created topics fix(lark): 隔离普通群用户创建的话题 Jul 28, 2026
@hu5h

hu5h commented Jul 28, 2026

Copy link
Copy Markdown
Author

已按 review 的 global 验收路线完成修复并更新 PR:

  • 修复 shared 模式 native topic reply split-brain;
  • 真实 omt_* 原生话题在四种 reply mode 下均保持独立,包括 bot 首次从已有话题回复接入;
  • synthetic/shared alias 保持原折叠语义;
  • 补齐 handler 级连续性与四模式矩阵回归,并同步中英文公开契约文案。

最新 commit:0d00d45a
验证:dispatcher 248/248、关联测试 175/175、tsc、build、diff check 均通过;深度只读交叉 review 已 APPROVE。

在 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>
@deepcoldy deepcoldy changed the title fix(lark): 隔离普通群用户创建的话题 feat(lark): 普通群 chat-topic 隔离原生话题 + 默认改 chat-topic/私聊 chat Jul 28, 2026
@deepcoldy

Copy link
Copy Markdown
Owner

过程摘要:按维护者决策落地(narrow + 默认翻转)

维护者(申晗)决策:B(narrow,遵循 dashboard /reply-mode 配置)+ 两个默认值翻转

已在 @hu5h 的原生话题隔离修复(commit 0d00d45)基础上,以 fast-forward 追加一个 commit(e695d417,非破坏性,保留 hu5h 提交与作者归属)落地:

  1. 收窄隔离到 chat-topic:hu5h 版对所有模式隔离原生话题;改为仅 chat-topic 隔离,chat/shared 按文档契约仍折进群会话。两处 omt_ 判据(seed 分支 + hu5h 新增的 reply-entry 分支)都加 resolveRegularGroupMode === 'chat-topic' 门控。保留 hu5h 的 root_id && thread_id seed-helper 守卫(它修的既存 split-brain 与语义收窄无关,必须保留)。移除随之失效的 independentTopicSeed 开关。
  2. 默认翻转:普通群 chat → chat-topic、私聊 thread → chat,同步反转全部归一化平行拷贝点(普通群 5 处、私聊 ~12 处)+ i18n 默认标记。

验证:tsc/build ✅;event-dispatcher 245/245;命中面 8 文件 644/644;全量 non-e2e 11010 passed,余 10 failed 全在 fs-policy-bwrap/schedule-card-model/scheduler/v3-distillation-runner(env/时区/bwrap),与干净 master 逐条一致 = 零回归

标题/描述已更新为中文规范格式。等 @codex 复审后交维护者拍板合码。

@chatgpt-codex-connector

Copy link
Copy Markdown

To use Codex here, create a Codex account and connect to github.

@deepcoldy deepcoldy left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

复审结论:逻辑主干已收敛;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>
@deepcoldy

Copy link
Copy Markdown
Owner

复审 P2 已修(纯文案/测试,运行逻辑零改动)

针对 @codex CHANGES_REQUESTED(review #4799146760)的三组 P2,已推 commit c493263f(在 e695d41 上 fast-forward):

  1. 帮助文案纠错:此前误称「仅 chat-topic 隔离原生话题」。实测 new-topic 的 native seed 经 regularGroupRouting 也得 thread-scope(source=regular-group-thread)、reply 又被 mode==='new-topic' 挡在 fold 外——所以 new-topic 同样让原生话题独立。改为准确表述:chat/shared 折进群会话;chat-topic 与 new-topic 都让原生话题独立(chat-topic 的独特点=顶层平铺、仅原生话题独立)。涉及中英 /reply-mode usage + dashboard regularGroupModeHelp 共 4 处字符串。

  2. 补 4 模式矩阵缺格:新增 new-topic mode: a native topic seed is thread-scope via regularGroupRouting (not the omt_ branch) 用例,断言 {scope:thread, anchor:messageId},锁死「走 regularGroupRouting 但仍 thread」语义,防未来 omt_ 门控改动误伤 new-topic seed。

  3. 清理默认翻转遗留注释/标记(5 处 + /botconfig):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 选项中英补「默认/default」标记,与 dashboard 一致。

验证:event-dispatcher 246/246(+1 new-topic seed 格)、blast-radius 4 文件 401/401;tsc/build 通过;grep '仅 chat-topic'/'Only chat-topic' 归零。运行逻辑未动(narrow 门控 / shared guard / 默认归一化拷贝点全部保持)。

head 现为 c493263f@codex 可钉新 SHA 复跑。

@chatgpt-codex-connector

Copy link
Copy Markdown

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 deepcoldy left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

最终复审通过(钉 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-topic native 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,不执行合并;等待申晗最终确认。

@deepcoldy
deepcoldy merged commit 271c8a3 into deepcoldy:master Jul 28, 2026
@deepcoldy

Copy link
Copy Markdown
Owner

📊 会话路由决策图(合并后语义)

本 PR 后,「普通群消息如何映射到会话」按 消息形态 × /reply-mode 模式 决策。核心变化:原生话题(用户在飞书里手动「创建话题」)的隔离,收窄到只在 chat-topic / new-topic 生效;chat / shared 按配置仍折进群大堂会话。

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
Loading

四种模式一览

模式 顶层消息 用户创建的原生话题 说明
chat 群大堂连续会话 🟠 折进群大堂 全群一个连续会话
shared 群大堂连续会话 🟠 折进群大堂 顶层话题展示,复用群会话
chat-topic(普通群新默认) 群大堂连续会话(顶层平铺) 🟢 各自独立会话 顶层平铺 + 原生话题独立
new-topic 🟢 每条 @ 各自独立会话 🟢 各自独立会话 每个顶层 @ 都开独立会话

本 PR 修的 bug:此前 chat-topic 模式下,用户新建话题的**开场白(seed)**会被误折进群大堂,只有后续回复才隔离——违反 chat-topic「原生话题各自独立」的契约。现已修复,chat-topic 从话题第一句起就独立。同时修了一个既存竞态(bot 已拥有话题会话时,该话题内回复在 shared 模式被误折回大堂)。

私聊(DM)默认也变了

p2pMode 默认 threadchat:整段私聊共用一个连续会话(而非每条消息各起独立会话)。可用 /reply-mode topic 或 dashboard 切回 thread。

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.

3 participants