Skip to content

fix(lark): 修复实时卡片输出切换不刷新并兼容旧卡 - #1581

Merged
deepcoldy merged 2 commits into
deepcoldy:masterfrom
le0tan:fix/lark-streaming-card-refresh
Sep 27, 2026
Merged

deepcoldy merged 2 commits into
deepcoldy:masterfrom
le0tan:fix/lark-streaming-card-refresh

Conversation

@le0tan

@le0tan le0tan commented Sep 25, 2026

Copy link
Copy Markdown
Contributor

问题

实时卡片的显示模式按钮通过 im.v1.message.patch 更新原消息,但原始实时卡没有声明 config.update_multi: true。升级后,按钮事件可以正常 ACK,服务端状态也会变化,但飞书客户端中的卡片不会刷新。

此外,只修复新卡 payload 无法恢复升级前已经发送的旧卡,因为它们的原始消息不满足共享更新契约。

修复

  • 为公开实时卡片的初发和后续更新 payload 增加 update_multi: true
  • 为实时卡 callback action 增加版本标记
  • 识别升级前旧卡:首次点击后异步重发符合新契约的实时卡,并回收旧卡,不再对旧卡执行无效 PATCH
  • 对旧卡迁移做有界去重;迁移失败时回滚乐观状态并允许再次点击重试
  • 保留现有串行 PATCH 队列,用户触发的 PATCH 失败增加限频且脱敏的 warning 日志
  • 私有 snapshot/ephemeral 卡维持原有行为,不启用共享更新

验证

  • vitest run test/card-builder.test.ts test/card-integration.test.ts test/recall-frozen-cards.test.ts test/event-dispatcher.test.ts
    • 4 个测试文件通过
    • 709 项测试通过
  • git diff --check 通过
  • bun run build 已执行,但被仓库当前依赖环境中的既有类型声明缺口阻塞:src/im/lark/ws-proxy-agent.ts 无法找到 proxy-from-env 的声明文件;该错误与本次改动无关

未执行线上飞书实例重启或现场验证。

Co-authored-by: TRAE CLI <traecli@bytedance.com>
@deepcoldy

Copy link
Copy Markdown
Owner

你好 @le0tan,感谢提交 PR!我们已为这个 PR 创建了飞书评审群,自动评审正在进行中。系统自动拉你入群时失败了,麻烦点击下面的一年有效链接自行加入评审群,后续评审意见和沟通都会在群里同步:

https://applink.larkoffice.com/client/chat/chatter/add_by_link?link_token=6ffq77dd-6417-4b76-9ed3-590e608d53a1

也可以在名单文档中核对/更新你的飞书信息:https://bytedance.larkoffice.com/wiki/WJ1nwWbtxi89erkNGNbcgkt9nUe

本条为自动流程发出,最终评审结论以维护者审阅为准,谢谢!

@deepcoldy

Copy link
Copy Markdown
Owner

你好,感谢修复!自动评审结论:没有阻断性问题,修复方向与实现都正确,测试也比较扎实。仅有两个测试覆盖层面的建议项,方便的话建议合入前补一下:

1.(建议合前补)旧卡回收 retireMessageId 这段新逻辑目前没有任何断言覆盖

postFreshStreamingCard 中新增的回收旧卡分支(src/core/worker-pool.ts 约 4094–4106 行)在「旧卡没有 card_nonce」时是唯一能删掉旧消息的机制(parkStreamCard 要求 nonce 非空才会进 frozenCards,这种旧卡自然也不会被 recallFrozenCards 扫到)。

经变异验证:把这段整个短路(if (false && retireMessageId && …)),现有全部测试仍然通过——card-integration.test.ts 里两个新用例只断言了 sessionReply 被调用、streamCardId 更新为新卡、以及没有发生 PATCH,并未断言旧卡被 deleteMessage 回收。建议在「reposts an upgrade-era legacy card once instead of PATCHing it」用例中补一条断言,例如:

expect(deleteMessageMock).toHaveBeenCalledWith(APP_ID, legacyCardId);

(该文件里 deleteMessage 目前是 vi.fn(async () => true),取出 spy 引用即可断言;同时可覆盖「retireMessageId 恰好等于新卡 id / 已被 recall 回收时不重复删」的去重分支。)

2.(建议合前补)test/card-toggle.e2e.ts 被本 PR 打红——CI 不跑 e2e project,所以 CI 全绿没有暴露

实测对比(vitest run --project e2e test/card-toggle.e2e.ts):

  • master:8 个用例 6 passed / 2 failed(2 个 frozen silent label 用例在 master 上本就失败,与本次改动无关)
  • 本 PR:0 passed / 8 failed

原因分两层:

  1. 该文件对 card-builder 是全量 vi.mock 且静态 import card-handler,mock factory 里没有本次新增的 STREAMING_CARD_PATCH_VERSION 导出,模块加载即抛 No "STREAMING_CARD_PATCH_VERSION" export is defined on the ... mock。修法与 card-integration.test.ts 一致:factory 里补 STREAMING_CARD_PATCH_VERSION: '1'。
  2. 仅补常量后,仍有 5 个既有用例(current card toggle、card_nonce 缺省的 backwards compat、以及 3 个 PATCH 串行队列用例)失败:它们构造的 toggle 事件不带 stream_card_version,会被新逻辑正确识别为「旧卡」而走迁移分支(异步重发新卡而非 PATCH),与用例原本要覆盖的「当前卡走 PATCH」路径不符。给这些事件补上 stream_card_version: '1' 即可让它们继续覆盖原路径(这也正好印证迁移判据对无版本戳事件 fail-safe 到迁移侧的行为是符合预期的)。

另外两个不阻断的小建议(P3,供后续参考)

  • flushCardPatch 里新增的错误脱敏拼装与 src/bot-registry.ts 中既有的 formatLarkError 逻辑重复,后续可考虑复用;顺带一提两处目前都只读 data.log_id,而飞书有时会把请求 ID 放在 data.error.log_id,可一并完善。
  • 旧卡迁移期间的 toast 复用的是通用「后台处理中」文案,用户无从知道点击会重发一张新卡并回收旧卡,后续可考虑换成专用文案。

验证情况:tsc --noEmit 干净(PR 描述中提到的 proxy-from-env 类型报错在评审环境未复现,CI build 腿也通过,应为本地依赖环境问题);作者列出的 4 个测试文件 709 项全绿,评审另跑了二十余个邻近测试文件(frozen/pinning/authority/command 等)无回归;对迁移分支、失败回滚、迁移去重、PATCH 失败限频四处分别做了变异验证,均能精确打红对应新用例。唯一未覆盖的是飞书真机验证,合入部署后建议点一次升级前发出的旧卡确认迁移效果。

以上为自动评审的初步意见,最终以维护者审阅为准。再次感谢!

Co-authored-by: TRAE CLI <traecli@bytedance.com>
@le0tan

le0tan commented Sep 27, 2026

Copy link
Copy Markdown
Contributor Author

感谢细致评审,两个建议项已在 f3c4d0d5 补齐:

  1. card-integration.test.ts 现在显式断言无 nonce 的升级前旧卡会调用 deleteMessage(APP_ID, legacyCardId),覆盖 retireMessageId 回收分支。
  2. card-toggle.e2e.ts 已补 STREAMING_CARD_PATCH_VERSION mock,并让当前卡 toggle 事件携带 stream_card_version: '1',继续覆盖 PATCH 串行队列路径。

另外把该 E2E 文件既有的 frozenIdleLabel mock 和 'silent' 兼容也一并补上,因此本地结果从 master 基线的 6/8 提升到 8/8:

  • 相关 unit:4 files / 709 tests passed
  • card-toggle.e2e.ts:1 file / 8 tests passed
  • git diff --check passed

@deepcoldy
deepcoldy merged commit b526cf1 into deepcoldy:master Sep 27, 2026
9 checks passed
@github-actions

Copy link
Copy Markdown

🚀 Released in v3.31.0

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