Skip to content

fix(lark): 关闭/恢复会话卡片行为收口——关闭不弹 300000,恢复重出流式卡+文本 - #897

Open
47seek wants to merge 2 commits into
deepcoldy:masterfrom
47seek:fix/close-resume-stream-card
Open

fix(lark): 关闭/恢复会话卡片行为收口——关闭不弹 300000,恢复重出流式卡+文本#897
47seek wants to merge 2 commits into
deepcoldy:masterfrom
47seek:fix/close-resume-stream-card

Conversation

@47seek

@47seek 47seek commented Aug 16, 2026

Copy link
Copy Markdown
Collaborator

改了什么

关闭会话按钮

  • closeSession 新增可选 awaitWorkerExit(默认 true,其它调用方行为不变)。卡片按钮传 false:不再干等 worker 进程退出。忙碌 CLI 只在 ~7s 的 SIGKILL 兜底才退出,会冲破飞书卡片回调 ~3s 的 ACK 窗口,触发客户端 「出错了,请稍后重试 code: 300000」 toast。改为同步完成逻辑关闭即返回、worker 后台被杀;bridge send marker 清理挂到同一 fence 后台执行,不丢账。
  • 被点击的那张流式卡改为原地 patch 成「🛑 会话已关闭」卡(把关闭卡作为回调响应返回),不再新发一张卡、也不删除刚点击的卡——删刚点的卡会与回调响应竞态,同样触发 300000。私有卡模式仍走 ephemeral 发送。

恢复会话按钮

  • 先秒回空 ACK,再在后台「重发一张全新流式卡 + 撤回旧的已关闭卡 + 补发『✅ 会话已恢复』文本」。流式卡在前、文本在后,两者都保留。
  • 新增 buildStreamingCardJson 复用正常流程的完整流式卡渲染(🖥️ 头 + 用量 + 显示输出/终端/操作链接/关闭 按钮),避免恢复后长出一张残缺卡。

为什么

关闭时旧流式卡的按钮已失效,原地变成已关闭卡最干净;而流式卡本身是「恢复后继续对话」才需要的东西,所以归到 resume 路径重建。原来点关闭偶发弹 300000、点恢复只发一条文本不出卡,本次一并收口。

影响面(公共层 core/worker-pool + im/lark/card-handler

  • closeSession 新增可选参数,默认行为不变,其它 15+ 调用方(dashboard / 命令行 /close / session-manager 等)均走默认 awaitWorkerExit=true,不受影响。
  • 仅飞书卡片按钮路径改变交互。
  • 私有卡模式、adopt/transfer 会话均已分支保留原行为。

测试

  • pnpm build 通过(tsc 编译干净)。
  • 新增 test/close-stream-card-untouched.test.ts:断言 closeSession 不删流式卡。
  • 相关 test 全绿:recall-frozen-cards / session-resume / ipc-close-route / closed-session-card / card-handler-c2 / card-handler-config / card-handler-relay-pickup
  • 已在 live daemon 飞书实测:点「关闭会话」→ 原卡直接变「🛑 会话已关闭」、无 300000 toast;点「恢复会话」→ 先出全新流式卡、再出「✅ 会话已恢复」文本。

改了什么:
- 关闭会话按钮:点击后不再干等 worker 进程退出(closeSession 新增
  awaitWorkerExit 参数,默认 true 不影响其它调用方;卡片按钮传 false)。
  忙碌 CLI 只在 ~7s 的 SIGKILL 兜底才退出,会冲破飞书卡片回调 ~3s 的 ACK
  窗口,触发客户端「code: 300000」toast。改为同步完成逻辑关闭即返回、worker
  后台被杀;bridge send marker 清理挂到同一 fence 后台执行,不丢账。
- 关闭时被点的那张流式卡改为「原地 patch 成已关闭卡」(把关闭卡作为回调响应
  返回),不再新发一张卡、也不删除刚点击的卡——删刚点的卡会和回调响应竞态,
  同样触发 300000。私有卡模式仍走 ephemeral 发送。
- 恢复会话按钮:先秒回空 ACK,再在后台「重发一张全新流式卡 + 撤回旧的已关闭卡
  + 补发『会话已恢复』文本」。流式卡在前、文本在后,两者都保留(恢复才需要
  流式卡)。新增 buildStreamingCardJson 复用正常流程的完整流式卡渲染。

为什么:关闭时旧流式卡的按钮已失效,原地变成已关闭卡最干净;而流式卡本身是
「恢复后继续对话」才需要的东西,所以归到 resume 路径重建。

影响面(公共层 core/worker-pool + im/lark/card-handler):
- closeSession 新增可选参数,默认行为不变,其它 15+ 调用方不受影响。
- 仅飞书卡片按钮路径改变交互;dashboard/命令行 close 走默认 awaitWorkerExit=true。
- 私有卡模式、adopt/transfer 会话均已分支保留原行为。

测试:
- pnpm build 通过(tsc 编译干净)。
- 新增 test/close-stream-card-untouched.test.ts:断言 closeSession 不删流式卡。
- close/resume/frozen-card/card-handler 相关 test 全绿
  (recall-frozen-cards / session-resume / ipc-close-route / closed-session-card
  / card-handler-c2 / card-handler-config / card-handler-relay-pickup)。
@47seek
47seek requested a review from deepcoldy as a code owner August 16, 2026 16:05
@deepcoldy

Copy link
Copy Markdown
Owner

自动评审初步发现一个需要修正的点,最终以维护者审阅为准:

src/im/lark/card-handler.ts 的恢复分支目前用 return {}; 表示“空 ACK”,但该返回值还会经过 event-dispatcher.tsshapeCardActionResult。由于 {} 是 truthy,它会被整形成:

{"card":{"type":"raw","data":{}}}

并由当前 @larksuiteoapi/node-sdk@1.64.0 原样写入 WebSocket 回调响应;这不是“不更新 UI”的空 ACK,而是要求飞书用空对象更新卡片。飞书官方文档要求 card.data 是合法的卡片 JSON;错误卡片对应 200673。真正的空 ACK 应让 handler 返回 falsy,再由 dispatcher 统一产出 {},与仓库现有路径一致。

建议将:

return {}; // fast empty ACK

改为:

return; // dispatcher shapes this into the SDK-serializable empty ACK {}

这个问题本身不依赖后台 deleteMessage 是否抢先完成;异步删旧卡会让客户端表现更难稳定复现,但回调卡体已经确定不符合协议。另按官方错误码定义,200341 专指 3 秒内未响应;本分支快速返回时更准确的预期是错误卡片 200673,而不是 200341。参考:https://open.feishu.cn/document/feishu-cards/card-callback-communication?lang=zh-CN

恢复分支原写 `return {}` 作为快速空 ACK,但 `{}` 是 truthy,经
event-dispatcher 的 shapeCardActionResult 会被包成
`{card:{type:raw,data:{}}}`——即用空 body 卡原地 patch 刚点击的那张卡,
而非"不改 UI 的空 ACK"。空 card.data 不合协议,且与后台并行的
deleteMessage 竞争同一张卡。

改为裸 `return`(→ undefined),让 shaper 走到 falsy 分支产出合法的顶层
空 ACK `{}`,与本 handler 其它空 ACK 写法一致。同步修正上方注释里
误导性的 `(return {})` 引用。

补 event-dispatcher 测试:钉死 handler 返回 `{}` 会被 shaper 退化成
空 body 卡 patch 这一陷阱,防止有人再写回 `return {}`。

验证:pnpm build 绿;event-dispatcher / close-stream-card-untouched /
session-resume 三个 suite 共 333 tests 全绿(含新增用例)。
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