Skip to content

fix: 话题响应策略修复 + bypass 判定收敛共享#256

Merged
lishuceo merged 6 commits into
mainfrom
feat/claude-session-d29682
Jun 3, 2026
Merged

fix: 话题响应策略修复 + bypass 判定收敛共享#256
lishuceo merged 6 commits into
mainfrom
feat/claude-session-d29682

Conversation

@flagflag
Copy link
Copy Markdown
Collaborator

@flagflag flagflag commented Jun 3, 2026

变更概述

修复了话题(thread)内消息触发 bot 响应的几个 bug,并把单 bot / 多 bot 两个模式的 bypass 判定收敛到共享函数。

  • fix:多人话题里禁用所有无 @mention 的自动响应。历史教训 omt_197a3379c4cf1bb7——session 创建者发图给第三人看,bot 自作多情冲上去回复;多人讨论 bot 没法可靠判断"消息是给谁的",干脆要求显式 @
  • fix:单人话题里去掉 Qwen 二次判断,session 创建者发啥直接放行。Qwen 拿不到对话上下文,把延续上文的简短反馈(如「不用新分支,但是文档需要更新」)误判成"不是跟 bot 说话"。话题本就是 user vs bot 的对话载体,没必要二次判断
  • test:新增动态切换场景的回归测试——单人话题 → 第三人加入 → 后续消息切换到多人模式(不缓存判定,每条消息现拉历史)
  • refactor:抽出 evaluateThreadBypass() 让单 bot 和多 bot 模式共享同一份判定逻辑。event-handler 里两个分支只剩"判 @mention 状态 + 调共享函数",再不会出现一边改一边漏的策略漂移

测试计划

  • `npx vitest run src/feishu/tests/thread-participants.test.ts` 11/11 通过
  • `npx vitest run src/feishu/tests/event-handler.test.ts` 62/62 通过
  • `npm run typecheck` 干净
  • 全量 vitest 1411/1412 通过(剩 1 个失败是 `memory/quality.test.ts` 的 pre-existing flaky 用例,与本 PR 无关)
  • 部署后在多人话题里发无 @ 消息确认 bot 不响应
  • 单人话题内 session 创建者发消息确认 bot 直接响应(包括简短反馈)
  • 单人话题中第三人插话后,session 创建者后续消息要求 @ bot 才响应

🤖 Generated with Claude Code

flagflag and others added 4 commits June 3, 2026 15:52
历史教训(话题 omt_197a3379c4cf1bb7):session 创建者在多人话题
里发图,bot 自作多情冲上去回。多人讨论里 bot 无法可靠判断"这条
是给我还是给别人"——所以干脆要求显式 @bot 才回。

- 抽出 thread-participants 模块,统一拉历史 + 判断"是否有非
  session 创建者的人类参与者",失败时保守返回 true
- 单 bot 模式:图片/文档/文字 三条 bypass 路径全部加多人话题
  前置过滤
- 多 bot 模式:thread creator bypass 同样加多人话题过滤
- 单人话题保留原 Qwen 文字判断 + 图片/文档直放
- 新增 thread-participants 单测 + event-handler 多人话题场景测试
- 新增 docs/design/thread-bypass-policy.md 记录策略

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
被黎叔在话题 omt_197a5d56cc92dbe9 抓现行:发「不用新分支,但是
文档需要更新」没响应。日志显示 Qwen 判 {respond: false} —— 因
为 Qwen prompt 里只塞了消息文本本身,没上下文,看到一句像"安排
开发干活"的话就误判成"不是跟 bot 说话"。

反思:话题本就是 user vs bot 的对话载体,单人话题里 session 创
建者发的消息基本就是冲 bot 来的。多人话题已经在前一版强制 @
了,单人话题再用 Qwen 二次判断纯属画蛇添足——既增加延迟,又
引入误判。直接放行。

- 单 bot 模式:单人话题图片/文档/文字 全部直接 allow,砍掉
  checkThreadRelevance 调用
- 多 bot 模式:thread creator bypass 在单人话题直接 bypass=true
- 移除 thread-relevance import;event-handler.test 移除 Qwen
  mock,单测用例从 'semantic_check' 收敛到 'allow' / 'block'
- 更新 design doc:单人/多人差异表 + 历史演进 v4 注明取消 Qwen
  的原因

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
黎叔提醒的边界:单人话题虽然总放行,但一旦动态加入第三人,
就要立即切换到多人保守模式。

实现层面早就是对的——threadHasOtherHumanParticipant 每次都
现拉 fetchRecentMessages,没缓存。但语义没有被显式断言,
所以补一个 stateful mock 测试 + 在 design doc 里加"动态切换"
小节固化下来。

- thread-participants.test.ts 新增 transitions from solo to
  multi-user as third party joins
- design doc「多人话题的定义」下加「动态切换」小节,说明
  每条消息都实时拉 + 第三人插话的处理流程

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
event-handler.ts 里单 bot 和多 bot 模式各有一套 thread bypass 逻辑(session
创建者校验 + 多人话题保守),代码近乎完全重复。之前 deb97d4 加多人话题保守
时改了多 bot 分支,差点漏改单 bot;后续每加一条规则都得改两份,迟早飘。

把 bypass 判定抽到 thread-participants.ts 的 evaluateThreadBypass(),event-handler
里两个分支只剩 "判 @mention 状态 + 调共享函数"。返回带 reason 的结果对象
(no_session / not_creator / multi_user / solo),日志保持各分支独立。

- 新增 8 条 evaluateThreadBypass 单测覆盖所有 reason 分支 + agentId 透传
- 补 chat-history-fork.test.ts 和 event-handler.test.ts 里 security mock
  漏导出的 isOwner(event-handler.ts 现在模块级常量直接引用 isOwner)
- 更新 docs/design/thread-bypass-policy.md,标注共享判定与 v5 演进

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Jun 3, 2026

Claude finished @flagflag's task in 3m 37s —— View job


Code Review: 话题响应策略修复 + bypass 判定收敛

  • 读取 PR diff 与改动文件
  • 审查 thread-participants.ts(共享 bypass 判定)
  • 审查 event-handler.ts 改动
  • 审查测试与设计文档
  • 汇总评审结论

✅ Approved — 高质量重构,未发现需要阻塞的问题

这个 PR 把单 bot / 多 bot 两条分支的 bypass 判定收敛到 evaluateThreadBypass(),逻辑清晰、依赖注入到位、测试覆盖充分、还配了一份认真的设计文档。我逐文件读了源码并核对了跨模块调用,确认行为一致。

做得好的地方

  • 判定收敛彻底event-handler.ts:766:792 两处分支现在只剩"判 @ 状态 + 调共享函数",从根上消除了"一边改一边漏"的策略漂移风险。reason 枚举(no_session/not_creator/multi_user/solo)让两个分支的日志分流也很清晰。
  • 依赖注入 + 纯函数拆分hasOtherHumanInMessages() 抽成纯函数单测,ThreadBypassDeps 注入 client/getThreadSession/isOwner,测试不需要碰真实飞书 API。threadBypassDeps 作为 module-level singleton 构造一次复用,符合 CLAUDE.md 的 singleton 约定。
  • 保守 fallback 正确fetchRecentMessages 失败时返回 true(视为多人话题,要求 @),宁可让用户多 @ 一次也不乱插嘴 —— 与 omt_197a3379c4cf1bb7 的历史教训一致。
  • 动态切换有回归覆盖transitions from solo to multi-user as third party joins 显式验证了"不缓存判定、每条消息现拉历史"的语义,这正是最容易回归的点。
  • 签名核对无误fetchRecentMessages(threadId, 'thread', limit, chatId)client.ts:588 的签名(chatIdForBotDiscovery 第 4 参)一致;senderId = item.sender?.idsenderType app/user 区分也对得上。messageId/chatId 在两处调用点均在作用域内。

一处非阻塞的小提醒(doc/impl 语义差,confidence ~70,不强制改)

设计文档 thread-bypass-policy.md:89 写"多人判定基于『曾经有过』而非『最近 N 条』",但实现实际只看 fetchRecentMessages最近 10 条thread-participants.ts:43 默认 limit=10)。在一个较长的话题里,如果第三人是在 11 条之前发的言、之后一直只有 session 创建者和 bot,那么创建者的新消息会被判成 solo 重新放行。多数话题 10 条窗口够用,但严格意义上这不是"一旦多人化就永久保守"。建议要么把文档措辞改成"最近 N 条窗口内",要么明确这是有意的窗口取舍。两者择一即可,不影响合并。

测试:本环境的 bash 受限无法直接跑 vitest/typecheck,依据 PR 描述 thread-participants 11/11、event-handler 62/62、typecheck 干净。代码层面我没看到会导致这些用例失败的问题。

整体评价:逻辑正确、可读性好、测试与文档到位,符合仓库测试政策(feat+test、bugfix+回归)。👍

冲突解决思路:
- 保留 main 的 resolveMentionGate 白名单架构(每条放行条件返回命名 reason)
- 内部 bypass 段改用本分支抽出的 evaluateThreadBypass 共享判定
  - 多 bot:通过返回 'thread_bypass'(不再有 _exclusive 后缀)
  - 单 bot:通过返回 'thread_session_owner'
- 多人话题保守拦截 + 移除 Qwen 二次判断的语义全部保留

同步更新:
- mention-gate.test.ts:去掉过时的 Qwen mock 和 _exclusive 期望
- restart-history.test.ts:补 isOwner mock(main 新增的 security 依赖)
- 删除未使用的 formatThreadContext 辅助函数

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Jun 3, 2026

Claude finished @flagflag's task in 1m 34s —— View job


Code Review: 话题响应策略修复 + bypass 判定收敛(synchronize 复审)

  • 拉取 PR commits 与 diff
  • 审查 thread-participants.ts(共享 bypass 判定)
  • 审查 event-handler.ts 两处调用点 + 合并进来的 resolveMentionGate 白名单架构
  • 核对设计文档与测试覆盖
  • 汇总评审结论

✅ Approved — 合并后的集成依然干净,未发现阻塞问题

本次 synchronize 主要是合并了 main 的 resolveMentionGate 白名单架构(c064e20)。我重点核对了合并后 bypass 判定与白名单门的衔接,确认行为一致、无策略漂移。

核对要点

  • 白名单门集成正确resolveMentionGate 现在是"必须返回明确放行理由否则拦截"的白名单模式(event-handler.ts:917)。两处 bypass 调用(多 bot :574、单 bot :602)在命中 solo 时分别返回 'thread_bypass' / 'thread_session_owner' 作为放行理由,其余 reason 落回 undefined 被门拦截 —— 语义闭合,没有"既不放行也不拦截"的中间态。
  • 依赖注入复用得当threadBypassDeps:191)作为 module-level singleton 构造一次,client/getThreadSession/isOwner 全部注入,符合 CLAUDE.md singleton 约定,测试无需触碰真实飞书 API。
  • 保守 fallback 一致fetchRecentMessages 失败时 threadHasOtherHumanParticipant 返回 truethread-participants.ts:68-74),多人话题保守要求 @ —— 与 omt_197a3379c4cf1bb7 历史教训一致。
  • 跨模块签名无误fetchRecentMessages(threadId, 'thread', limit, chatId) 第 4 参 chatIdclient.ts 签名对齐;hasOtherHumanInMessages 排除当前消息 + 只看 senderType === 'user' 的逻辑正确。
  • 动态切换有回归覆盖thread-participants.test.ts > transitions from solo to multi-user 验证了"不缓存、每条现拉历史"的语义。

一处遗留的非阻塞提醒(doc/impl 语义差,confidence ~70,沿用上次评审,未阻塞)

设计文档 thread-bypass-policy.md:89 仍写"多人判定基于『曾经有过』而非『最近 N 条』……话题一旦多人化就一直保持保守",但实现只看 fetchRecentMessages最近 10 条thread-participants.ts:63 默认 limit=10)。若第三人在 11 条之前发言、之后只剩创建者与 bot,创建者新消息会重新判成 solo 放行 —— 严格说并非"永久保守"。建议把文档措辞改成"最近 N 条窗口内",或明确这是有意的窗口取舍。两者择一,不影响合并。

Fix this →(分支 feat/claude-session-d29682

测试:本环境 bash 受限无法直接跑 vitest/typecheck。依据 PR 描述:thread-participants 11/11、event-handler 62/62、typecheck 干净、全量 1411/1412(剩 1 个为 memory/quality.test.ts pre-existing flaky,与本 PR 无关)。代码层面我没看到会导致用例失败的问题。

整体评价:合并后逻辑闭合、判定收敛彻底、测试与设计文档到位,符合仓库测试政策。👍

reviewer 指出 doc/impl 语义差:文档写"曾经有过"+"话题一旦多人化就一直保持保守",
但实现是 fetchRecentMessages 默认 limit=10,只看最近 N 条窗口。

把决策表里的措辞改成"最近 N 条窗口(默认 N=10)",并把工程取舍和已知边界
(沉寂多轮再单人续聊会被重判 solo)写明,与代码现状对齐。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@flagflag
Copy link
Copy Markdown
Collaborator Author

flagflag commented Jun 3, 2026

Addressed — docs/design/thread-bypass-policy.md:89 已改为"最近 N 条窗口(默认 N=10)",把工程取舍和"沉寂多轮再单人续聊会被重判 solo"的已知边界写明,与 thread-participants.ts 实现对齐。见 c385f05

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Jun 3, 2026

Claude finished @flagflag's task in 1m 13s —— View job


Code Review: 话题响应策略修复 + bypass 判定收敛(doc 修正复审)

  • 拉取最新 commits(含 c385f05 文档修正)
  • 核对 docs/design/thread-bypass-policy.md:89 文档/实现语义对齐
  • 复核 thread-participants.ts 共享判定逻辑
  • 复核 event-handler.ts 两处 bypass 调用点 + resolveMentionGate 白名单衔接
  • 汇总评审结论

✅ Approved — 上次的 doc/impl 语义差已修复,无新增问题

本次 synchronize 是针对上两轮评审里那条非阻塞提醒的定向修复(c385f05),我核对后确认已对齐。

已解决:doc/impl 语义差(前两轮 confidence ~70)

  • thread-bypass-policy.md:89 现在写的是 "多人判定基于『最近 N 条窗口』(默认 N=10)",并明确写出工程取舍与已知边界——"第三人若在 N 条之前发言、之后只剩创建者与 bot,新消息会重新判成 solo 放行"。措辞与 thread-participants.ts:63limit: number = 10 默认值、fetchRecentMessages 一次拉取的实现完全一致,不再有"曾经有过 / 永久保守"的过度承诺。✅

复核要点(均无回归)

  • 白名单门衔接闭合:两处 bypass 调用命中 solo 时分别返回 'thread_bypass'(多 bot,event-handler.ts:578)/ 'thread_session_owner'(单 bot,:614)作为放行理由;multi_user/no_session/not_creator 落回 undefinedresolveMentionGate 白名单拦截(:922),无"既不放行也不拦截"的中间态。
  • 依赖注入 + singletonthreadBypassDeps:191)module-level 构造一次,注入 client/getThreadSession/isOwner,符合 CLAUDE.md singleton 约定。
  • 保守 fallback 正确threadHasOtherHumanParticipantfetchRecentMessages 抛错时返回 truethread-participants.ts:68-74),多人话题宁可多 @ 一次 —— 与 omt_197a3379c4cf1bb7 历史教训一致。
  • 纯函数判定无误hasOtherHumanInMessages 正确排除当前消息(m.messageId !== currentMessageId)、只看 senderType === 'user'、跳过空 senderId
  • 动态切换有回归覆盖thread-participants.test.ts 的 solo→multi-user 切换用例验证了"不缓存、每条现拉历史"的语义。

测试:本环境 bash 受限无法直接跑 vitest/typecheck。依据 PR 描述:thread-participants 11/11、event-handler 62/62、typecheck 干净、全量 1411/1412(剩 1 个为 memory/quality.test.ts 的 pre-existing flaky,与本 PR 无关)。代码层面未见会导致用例失败的问题。

整体评价:判定收敛彻底、依赖注入到位、文档与实现已对齐、测试覆盖充分。先前唯一的非阻塞提醒已闭环,无遗留项。👍
· 分支 feat/claude-session-d29682

@lishuceo lishuceo merged commit ba64404 into main Jun 3, 2026
4 checks passed
@lishuceo lishuceo deleted the feat/claude-session-d29682 branch June 3, 2026 09:49
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