Skip to content

Commit a1889d1

Browse files
author
lbo728
committed
fix(feishu): stop typing-indicator log spam and stuck emoji on message-not-found
- Remove internal console.log from addTypingIndicator/removeTypingIndicator; errors now propagate so callers handle them via the structured onStartError / onStopError paths instead of raw console output. - Add isFeishuMessageNotFoundError() helper that detects Feishu error code 231003 ("The message is not found, maybe not exist or deleted"). - In reply-dispatcher, introduce typingDisabled flag: the first time start() receives a 231003 error the flag is set and all subsequent keepalive ticks are skipped, eliminating the 3-second log spam seen in #27418. - Guard start() so it skips when typingState.reactionId is already set. Previously the keepalive overwrote typingState with null on every tick (because addTypingIndicator failed silently), causing removeTypingIndicator to find no reactionId and leaving the Typing emoji permanently on the user's message. Fixes #27418
1 parent 2ca2d5a commit a1889d1

3 files changed

Lines changed: 104 additions & 33 deletions

File tree

extensions/feishu/src/reply-dispatcher.ts

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,12 @@ import { getFeishuRuntime } from "./runtime.js";
1414
import { sendMarkdownCardFeishu, sendMessageFeishu } from "./send.js";
1515
import { FeishuStreamingSession } from "./streaming-card.js";
1616
import { resolveReceiveIdType } from "./targets.js";
17-
import { addTypingIndicator, removeTypingIndicator, type TypingIndicatorState } from "./typing.js";
17+
import {
18+
addTypingIndicator,
19+
isFeishuMessageNotFoundError,
20+
removeTypingIndicator,
21+
type TypingIndicatorState,
22+
} from "./typing.js";
1823

1924
/** Detect if text contains markdown elements that benefit from card rendering */
2025
function shouldUseCard(text: string): boolean {
@@ -38,9 +43,19 @@ export function createFeishuReplyDispatcher(params: CreateFeishuReplyDispatcherP
3843
const prefixContext = createReplyPrefixContext({ cfg, agentId });
3944

4045
let typingState: TypingIndicatorState | null = null;
46+
// Set to true when the message is no longer available (Feishu error 231003).
47+
// Once disabled, all subsequent typing-start attempts are skipped so the
48+
// keepalive loop does not produce repeated 400 errors in the logs.
49+
let typingDisabled = false;
4150
const typingCallbacks = createTypingCallbacks({
4251
start: async () => {
43-
if (!replyToMessageId) {
52+
if (!replyToMessageId || typingDisabled) {
53+
return;
54+
}
55+
// Skip if we already hold a valid reaction ID — the keepalive should
56+
// not try to add duplicate reactions on every tick, which would
57+
// overwrite `typingState` with null and leave the emoji stuck.
58+
if (typingState?.reactionId) {
4459
return;
4560
}
4661
typingState = await addTypingIndicator({ cfg, messageId: replyToMessageId, accountId });
@@ -49,16 +64,24 @@ export function createFeishuReplyDispatcher(params: CreateFeishuReplyDispatcherP
4964
if (!typingState) {
5065
return;
5166
}
52-
await removeTypingIndicator({ cfg, state: typingState, accountId });
67+
const stateToRemove = typingState;
5368
typingState = null;
69+
await removeTypingIndicator({ cfg, state: stateToRemove, accountId });
5470
},
55-
onStartError: (err) =>
71+
onStartError: (err) => {
72+
if (isFeishuMessageNotFoundError(err)) {
73+
// Message was deleted or expired — disable the typing indicator for
74+
// this session to prevent log spam on every keepalive tick.
75+
typingDisabled = true;
76+
return;
77+
}
5678
logTypingFailure({
5779
log: (message) => params.runtime.log?.(message),
5880
channel: "feishu",
5981
action: "start",
6082
error: err,
61-
}),
83+
});
84+
},
6285
onStopError: (err) =>
6386
logTypingFailure({
6487
log: (message) => params.runtime.log?.(message),
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { describe, expect, it } from "vitest";
2+
import { isFeishuMessageNotFoundError } from "./typing.js";
3+
4+
describe("isFeishuMessageNotFoundError", () => {
5+
it("returns true for Axios error with code 231003 in response.data", () => {
6+
const err = { response: { data: { code: 231003, msg: "The message is not found" } } };
7+
expect(isFeishuMessageNotFoundError(err)).toBe(true);
8+
});
9+
10+
it("returns true for error with code 231003 at top level", () => {
11+
const err = { code: 231003, message: "not found" };
12+
expect(isFeishuMessageNotFoundError(err)).toBe(true);
13+
});
14+
15+
it("returns false for other Feishu error codes", () => {
16+
const err = { response: { data: { code: 99991672, msg: "Permission denied" } } };
17+
expect(isFeishuMessageNotFoundError(err)).toBe(false);
18+
});
19+
20+
it("returns false for non-object errors", () => {
21+
expect(isFeishuMessageNotFoundError(null)).toBe(false);
22+
expect(isFeishuMessageNotFoundError(undefined)).toBe(false);
23+
expect(isFeishuMessageNotFoundError("some string error")).toBe(false);
24+
expect(isFeishuMessageNotFoundError(42)).toBe(false);
25+
});
26+
27+
it("returns false for errors without a code field", () => {
28+
const err = new Error("network error");
29+
expect(isFeishuMessageNotFoundError(err)).toBe(false);
30+
});
31+
32+
it("returns false for errors with response.data but no code", () => {
33+
const err = { response: { data: { msg: "unknown error" } } };
34+
expect(isFeishuMessageNotFoundError(err)).toBe(false);
35+
});
36+
});

extensions/feishu/src/typing.ts

Lines changed: 40 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,35 @@ import { createFeishuClient } from "./client.js";
77
// Full list: https://github.com/go-lark/lark/blob/main/emoji.go
88
const TYPING_EMOJI = "Typing"; // Typing indicator emoji
99

10+
/** Feishu error code: the target message was not found (deleted or expired). */
11+
const FEISHU_ERROR_MESSAGE_NOT_FOUND = 231003;
12+
1013
export type TypingIndicatorState = {
1114
messageId: string;
1215
reactionId: string | null;
1316
};
1417

1518
/**
16-
* Add a typing indicator (reaction) to a message
19+
* Returns true when the Feishu API error indicates the message no longer
20+
* exists (code 231003). In that case the caller should stop retrying.
21+
*/
22+
export function isFeishuMessageNotFoundError(err: unknown): boolean {
23+
if (!err || typeof err !== "object") return false;
24+
// Feishu SDK wraps Axios errors: err.response.data carries the API error body.
25+
const data = (err as { response?: { data?: unknown } }).response?.data;
26+
if (data && typeof data === "object") {
27+
const code = (data as { code?: number }).code;
28+
if (code === FEISHU_ERROR_MESSAGE_NOT_FOUND) return true;
29+
}
30+
// Some SDK versions also expose the code at the top level.
31+
if ((err as { code?: number }).code === FEISHU_ERROR_MESSAGE_NOT_FOUND) return true;
32+
return false;
33+
}
34+
35+
/**
36+
* Add a typing indicator (reaction) to a message.
37+
* Throws on failure so callers can decide how to handle it (e.g. disable
38+
* retries when the message no longer exists).
1739
*/
1840
export async function addTypingIndicator(params: {
1941
cfg: ClawdbotConfig;
@@ -28,26 +50,21 @@ export async function addTypingIndicator(params: {
2850

2951
const client = createFeishuClient(account);
3052

31-
try {
32-
const response = await client.im.messageReaction.create({
33-
path: { message_id: messageId },
34-
data: {
35-
reaction_type: { emoji_type: TYPING_EMOJI },
36-
},
37-
});
53+
const response = await client.im.messageReaction.create({
54+
path: { message_id: messageId },
55+
data: {
56+
reaction_type: { emoji_type: TYPING_EMOJI },
57+
},
58+
});
3859

39-
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- SDK response type
40-
const reactionId = (response as any)?.data?.reaction_id ?? null;
41-
return { messageId, reactionId };
42-
} catch (err) {
43-
// Silently fail - typing indicator is not critical
44-
console.log(`[feishu] failed to add typing indicator: ${err}`);
45-
return { messageId, reactionId: null };
46-
}
60+
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- SDK response type
61+
const reactionId = (response as any)?.data?.reaction_id ?? null;
62+
return { messageId, reactionId };
4763
}
4864

4965
/**
50-
* Remove a typing indicator (reaction) from a message
66+
* Remove a typing indicator (reaction) from a message.
67+
* Throws on failure so callers can log via the structured error path.
5168
*/
5269
export async function removeTypingIndicator(params: {
5370
cfg: ClawdbotConfig;
@@ -66,15 +83,10 @@ export async function removeTypingIndicator(params: {
6683

6784
const client = createFeishuClient(account);
6885

69-
try {
70-
await client.im.messageReaction.delete({
71-
path: {
72-
message_id: state.messageId,
73-
reaction_id: state.reactionId,
74-
},
75-
});
76-
} catch (err) {
77-
// Silently fail - cleanup is not critical
78-
console.log(`[feishu] failed to remove typing indicator: ${err}`);
79-
}
86+
await client.im.messageReaction.delete({
87+
path: {
88+
message_id: state.messageId,
89+
reaction_id: state.reactionId,
90+
},
91+
});
8092
}

0 commit comments

Comments
 (0)