fix(feishu): suppress stale replay typing indicators (#30709) (thanks @arkyu2077)

This commit is contained in:
Peter Steinberger
2026-03-02 03:52:56 +00:00
parent 7fbc40f821
commit 02b1958760
3 changed files with 67 additions and 2 deletions

View File

@@ -117,6 +117,7 @@ Docs: https://docs.openclaw.ai
- Telegram/Voice fallback reply chunking: apply reply reference, quote text, and inline buttons only to the first fallback text chunk when voice delivery is blocked, preventing over-quoted multi-chunk replies. Landed from contributor PR #31067 by @xdanger. Thanks @xdanger.
- Feishu/System preview prompt leakage: stop enqueuing inbound Feishu message previews as system events so user preview text is not injected into later turns as trusted `System:` context. Landed from contributor PR #31209 by @stakeswky. Thanks @stakeswky.
- Feishu/Multi-account + reply reliability: add `channels.feishu.defaultAccount` outbound routing support with schema validation, keep quoted-message extraction text-first (post/interactive/file placeholders instead of raw JSON), route Feishu video sends as `msg_type: "file"`, and avoid websocket event blocking by using non-blocking event handling in monitor dispatch. Landed from contributor PRs #29610, #30432, #30331, and #29501. Thanks @hclsys, @bmendonca3, @patrick-yingxi-pan, and @zwffff.
- Feishu/Typing replay suppression: skip typing indicators for stale replayed inbound messages after compaction using message-age checks with second/millisecond timestamp normalization, preventing old-message reaction floods while preserving typing for fresh messages. Landed from contributor PR #30709 by @arkyu2077. Thanks @arkyu2077.
## Unreleased

View File

@@ -116,6 +116,59 @@ describe("createFeishuReplyDispatcher streaming behavior", () => {
expect(addTypingIndicatorMock).not.toHaveBeenCalled();
});
it("skips typing indicator for stale replayed messages", async () => {
createFeishuReplyDispatcher({
cfg: {} as never,
agentId: "agent",
runtime: {} as never,
chatId: "oc_chat",
replyToMessageId: "om_parent",
messageCreateTimeMs: Date.now() - 3 * 60_000,
});
const options = createReplyDispatcherWithTypingMock.mock.calls[0]?.[0];
await options.onReplyStart?.();
expect(addTypingIndicatorMock).not.toHaveBeenCalled();
});
it("treats second-based timestamps as stale for typing suppression", async () => {
createFeishuReplyDispatcher({
cfg: {} as never,
agentId: "agent",
runtime: {} as never,
chatId: "oc_chat",
replyToMessageId: "om_parent",
messageCreateTimeMs: Math.floor((Date.now() - 3 * 60_000) / 1000),
});
const options = createReplyDispatcherWithTypingMock.mock.calls[0]?.[0];
await options.onReplyStart?.();
expect(addTypingIndicatorMock).not.toHaveBeenCalled();
});
it("keeps typing indicator for fresh messages", async () => {
createFeishuReplyDispatcher({
cfg: {} as never,
agentId: "agent",
runtime: {} as never,
chatId: "oc_chat",
replyToMessageId: "om_parent",
messageCreateTimeMs: Date.now() - 30_000,
});
const options = createReplyDispatcherWithTypingMock.mock.calls[0]?.[0];
await options.onReplyStart?.();
expect(addTypingIndicatorMock).toHaveBeenCalledTimes(1);
expect(addTypingIndicatorMock).toHaveBeenCalledWith(
expect.objectContaining({
messageId: "om_parent",
}),
);
});
it("keeps auto mode plain text on non-streaming send path", async () => {
createFeishuReplyDispatcher({
cfg: {} as never,

View File

@@ -25,6 +25,16 @@ function shouldUseCard(text: string): boolean {
/** Maximum age (ms) for a message to receive a typing indicator reaction.
* Messages older than this are likely replays after context compaction (#30418). */
const TYPING_INDICATOR_MAX_AGE_MS = 2 * 60_000;
const MS_EPOCH_MIN = 1_000_000_000_000;
function normalizeEpochMs(timestamp: number | undefined): number | undefined {
if (!Number.isFinite(timestamp) || timestamp === undefined || timestamp <= 0) {
return undefined;
}
// Defensive normalization: some payloads use seconds, others milliseconds.
// Values below 1e12 are treated as epoch-seconds.
return timestamp < MS_EPOCH_MIN ? timestamp * 1000 : timestamp;
}
export type CreateFeishuReplyDispatcherParams = {
cfg: ClawdbotConfig;
@@ -72,9 +82,10 @@ export function createFeishuReplyDispatcher(params: CreateFeishuReplyDispatcherP
}
// Skip typing indicator for old messages — likely replays after context
// compaction that would flood users with stale notifications (#30418).
const messageCreateTimeMs = normalizeEpochMs(params.messageCreateTimeMs);
if (
params.messageCreateTimeMs &&
Date.now() - params.messageCreateTimeMs > TYPING_INDICATOR_MAX_AGE_MS
messageCreateTimeMs !== undefined &&
Date.now() - messageCreateTimeMs > TYPING_INDICATOR_MAX_AGE_MS
) {
return;
}