Skip to content

fix(session-group): 非文本种子开的群转发原消息并补上 AI 命名 - #1379

Open
deepcoldy wants to merge 1 commit into
masterfrom
fix/session-group-forward-seed
Open

fix(session-group): 非文本种子开的群转发原消息并补上 AI 命名#1379
deepcoldy wants to merge 1 commit into
masterfrom
fix/session-group-forward-seed

Conversation

@deepcoldy

Copy link
Copy Markdown
Owner

问题

私聊 p2pMode='group' 下,用合并转发的飞书消息集合(以及图片 / 文件等任何非文本消息)开的会话群有两个毛病:

  1. 群名永远停在占位名「新会话」,走不到自动改名(AI 命名)的逻辑里;
  2. 群里只有一条「📥 @某人 从私聊发起了本次会话:(非文本消息)」,看不出这个群是怎么来的——原始内容还留在私聊里。

根因

出生流程(maybeBirthSessionGroup)的标题来源是 extractMessageTextForRouting,那是个只认 text / post 的文本窥视,图片 / 文件 / 合并转发在那里一律返回 null → 占位名回落「新会话」、引言回落「(非文本消息)」、scheduleSessionGroupTitle({ userText: '' })

关键在于空串调度不是无害的 no-opscheduleSessionGroupTitle 的同步闸(in-flight / 重试预算)先把这次尝试登记掉,if (!userText.trim()) return; 才在异步体内生效,finally 里还会 markSessionGroupTitleFailed。于是这一次空调用白烧掉 3 轮有限重试中的一轮并挂上 30s 退避——这正是「转发消息集合开的群从不改名」的直接原因。

改了什么

① AI 命名挪到「消息完整解析之后」(非文本种子)

  • 出生侧只在窥视真拿到文本时才调度,并通过新的 RoutingContext.sessionGroupTitleScheduled 告诉下游「已经调过了」;
  • 非文本种子改由递归回来的 handleNewTopicAdmitted 调度——那时 parsed.content 已经是完整内容(合并转发已展开成 <forwarded_messages>、语音已转写),标题直接从转发进来的对话本身总结,比从空串猜强得多;
  • 两处严格二选一。不能指望 title 服务自己去重:它的 titled / in-flight 闸在异步体内,出生侧刚发起的那次此刻两者都还没置上,重复调用只会再白烧一轮。

② 把原私聊消息转发进新群当第一条消息

  • 新增 forwardMessage()im.v1.message.forward)。⚠️ 这个接口的 uuidparams 里而不是 data,与 create / reply 不同,已在 transport boundary 用例里按精确形状断言住;
  • 图片 / 文件 / 合并转发没法从事件 payload 复刻,只有转发能原样带过去(附件、转发树都在)。转发件由 bot 自己发出,回声是自发消息,dispatcher 对自发消息只放行 /close不会二次触发会话
  • 引言相应简化成一行「原消息已转发到本群(见上 ⬆️)」;转发被关掉或失败时回落到旧的正文摘录形态,文本种子不丢上下文;
  • 回复锚点回落链 intro → forwarded,保证始终指向群内消息(会话 rootMessageId / 首轮引用都取这个 id),两者都失败才退化到私聊;
  • 新增 sessionGroup.forwardOrigin 开关,默认开,置 false 即回到只发引言的旧形态。

影响面

  • 共用路径src/im/lark/client.ts新增 forwardMessage,未改任何既有原语;RoutingContext 只新增一个可选字段;SessionGroupConfig 新增一个可选 key(与既有 dmReceipt 同级,同样无需 dashboard / 文档联动)。
  • 会话类型:只影响 p2pMode='group'会话群出生这一条路径。普通话题会话、solo 群会话、adopt/restore 完全不经过这里;p2pMode 非 group 的私聊不受影响。
  • 文本种子行为变化:原消息现在也会被转发进群(引言从「正文摘录」变成「见上」)。这是刻意的——群的第一条消息始终是原件,来历一眼可见。
  • 跨 CLI / 跨后端 / 跨平台:不碰适配器、PTY、后端与任何平台相关代码。
  • 失败降级:转发失败只打 info 并回落旧引言;引言失败则用转发件当锚点;两者都失败时会话照样建在群里,只是回复退化到私聊。出生的额度扣减、ctx.messageId 仍指向原私聊消息(资源下载 / 合并转发子消息的 key 依赖它)等既有不变量均未改动。

测试验证

新增 test/session-group-birth-forward-seed.test.ts(8 例,跑真实的建群递归,只替身飞书外部副作用):

$ npx vitest run test/session-group-birth-forward-seed.test.ts
 ✓ test/session-group-birth-forward-seed.test.ts (8 tests) 1153ms

 Test Files  1 passed (1)
      Tests  8 passed (8)

覆盖:合并转发种子会被转发 + 引言不再是「(非文本消息)」;AI 命名恰好调度一次userText 是展开后的 <forwarded_messages>;占位名仍是「新会话」且 titled 未置;转发失败 → 引言回落且锚点走 intro;引言失败 → 锚点走转发件(不是私聊消息 id);文本种子对照组仍在出生侧调度一次;forwardOrigin: false 时不转发但命名照常。

相关既有套件 + 边界:

$ npx vitest run test/lark-transport-boundary.test.ts test/session-group-birth-workingdir.test.ts \
    test/session-group-birth-quota.test.ts test/session-group-birth-anchor.test.ts \
    test/session-groups-store.test.ts test/message-quota-enforcement.test.ts
 Test Files  6 passed (6)
      Tests  94 passed (94)

$ npx tsc --noEmit      # 无输出
$ bun run build         # 通过

全量:

$ bun run test
 Test Files  12 failed | 1300 passed | 1 skipped (1313)
      Tests  39 failed | 22727 passed | 44 skipped (22810)

这 12 个失败与本改动无关,已逐一证实:

  • 其中 4 个(daemon-pinned-working-dirgroup-join-shared-routingdoc-comment-audit-gatedoc-comment-daemon-concurrency)单独重跑全绿(4 passed / 69 tests passed),是全量并发下的资源争用抖动;
  • 另外 8 个是环境依赖,在同一 commit 的干净基线 worktreee636f93cb,未带本 PR 任何改动)上跑,失败文件与失败数逐条一致
基线 e636f93cb:  Test Files  8 failed (8)   Tests  37 failed | 92 passed (129)
  4 linux-isolation / 2 mojo-launcher-env-quarantine / 4 plugin-mcp-gateway /
  2 plugin-mcp-sandbox / 5 plugin-registry-sandbox-read / 1 sandbox-session-data-dir /
  1 session-store-sqlite-bun-import / 18 session-store-sqlite-poisoned-recovery

原因两类:① 本机 bun 是 1.4.0、仓库钉 1.4.2(用例里直接断言 expected '1.4.0' to be '1.4.2'),影响两个 sqlite 套件;② 其余依赖 user namespace / bwrap 等沙箱能力。

Live 验证未做(会切走全局 shim 影响所有 bot),本改动的外部副作用只有一次 im.v1.message.forward 调用,已在 transport boundary 用例里按精确请求形状断言。

🤖 Generated with Claude Code

私聊 group 模式下,用「合并转发消息集合 / 图片 / 文件」开的会话群会停在占位名
「新会话」,群里只留一条「(非文本消息)」,看不出这个群是怎么来的。

根因:出生流程的标题来源是 extractMessageTextForRouting 这个文本窥视,它只认
text/post,非文本种子拿到空串。而 scheduleSessionGroupTitle 的空串拦截在**异步体
内**——同步闸已经把这次尝试登记掉了,于是空调用白烧一轮有限重试(共 3 轮)并挂上
30s 退避,群就再也改不了名。

改动:
- 出生侧只在窥视真拿到文本时才调度 AI 命名,并用 RoutingContext.sessionGroupTitleScheduled
  告知下游;非文本种子改由递归回来的 handleNewTopic 在**消息完整解析之后**调度
  (合并转发已展开成 <forwarded_messages>、语音已转写),标题直接从转发内容里总结。
  两处严格二选一,避免重复调用再烧一轮重试。
- 新增 forwardMessage(im.v1.message.forward,注意 uuid 在 params 而非 data),
  出生时把原私聊消息转发进新群当第一条消息:图片/文件/合并转发没法从事件里复刻,
  只有转发能原样带过去。转发件由 bot 自己发出,回声是自发消息,dispatcher 只放行
  /close,不会二次触发会话。
- 引言相应简化为一行「原消息已转发到本群」;转发被关掉或失败时回落到旧的正文摘录形态。
- 回复锚点回落链 intro → forwarded,保证始终指向群内消息(会话 rootMessageId /
  首轮引用都取这个 id),两者都失败才退化到私聊。
- 新增 sessionGroup.forwardOrigin 开关(默认开)。

Co-Authored-By: Claude Code <noreply@anthropic.com>
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.

1 participant