fix(feishu): reopen retryable bot menu replay

This commit is contained in:
Peter Steinberger
2026-05-29 22:52:10 +01:00
parent fadd275e7b
commit cbd492d680
4 changed files with 45 additions and 12 deletions

View File

@@ -216,6 +216,26 @@ export async function recordProcessedFeishuMessage(
return await tryRecordMessagePersistent(normalizedMessageId, namespace, log);
}
export async function forgetProcessedFeishuMessage(
messageId: string | undefined | null,
namespace = "global",
log?: (...args: unknown[]) => void,
): Promise<boolean> {
const normalizedNamespace = normalizeNamespace(namespace);
const normalizedMessageId = normalizeMessageId(messageId);
if (!normalizedMessageId) {
return false;
}
memory.delete(memoryKey(normalizedNamespace, normalizedMessageId));
const key = dedupeStoreKey(normalizedNamespace, normalizedMessageId);
try {
return openDedupStore(normalizedNamespace).delete(key);
} catch (error) {
log?.(`feishu-dedup: persistent delete failed: ${String(error)}`);
return false;
}
}
export async function hasProcessedFeishuMessage(
messageId: string | undefined | null,
namespace = "global",

View File

@@ -4,6 +4,7 @@ import { handleFeishuMessage, type FeishuMessageEvent } from "./bot.js";
import { maybeHandleFeishuQuickActionMenu } from "./card-ux-launcher.js";
import {
claimUnprocessedFeishuMessage,
forgetProcessedFeishuMessage,
recordProcessedFeishuMessage,
releaseFeishuMessageProcessing,
} from "./dedup.js";
@@ -131,18 +132,20 @@ export function createFeishuBotMenuHandler(params: {
.then(async (handledMenu) => {
if (handledMenu) {
await recordProcessedFeishuMessage(syntheticMessageId, accountId, log);
releaseFeishuMessageProcessing(syntheticMessageId, accountId);
return;
}
return await handleLegacyMenu();
})
.catch(async (err) => {
if (isFeishuRetryableSyntheticEventError(err)) {
releaseFeishuMessageProcessing(syntheticMessageId, accountId);
await forgetProcessedFeishuMessage(syntheticMessageId, accountId, log);
} else {
await recordProcessedFeishuMessage(syntheticMessageId, accountId, log);
}
throw err;
})
.finally(() => {
releaseFeishuMessageProcessing(syntheticMessageId, accountId);
});
if (fireAndForget) {
promise.catch((err) => {

View File

@@ -1,5 +1,5 @@
import { afterAll, afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import type { ClawdbotConfig } from "../runtime-api.js";
import type { ClawdbotConfig, RuntimeEnv } from "../runtime-api.js";
import { expectFirstSentCardUsesFillWidthOnly } from "./card-test-helpers.js";
import { createFeishuBotMenuHandler } from "./monitor.bot-menu-handler.js";
@@ -40,15 +40,18 @@ function createBotMenuEvent(params: { eventKey: string; timestamp: string }) {
};
}
async function registerHandlers() {
return createFeishuBotMenuHandler({
cfg: {} as ClawdbotConfig,
accountId: "default",
runtime: {
async function registerHandlers(params: { runtime?: RuntimeEnv } = {}) {
const runtime =
params.runtime ??
({
log: vi.fn(),
error: vi.fn(),
exit: vi.fn(),
},
} as RuntimeEnv);
return createFeishuBotMenuHandler({
cfg: {} as ClawdbotConfig,
accountId: "default",
runtime,
chatHistories: new Map(),
fireAndForget: true,
getBotOpenId: () => "ou_bot",
@@ -163,7 +166,8 @@ describe("Feishu bot menu handler", () => {
});
it("reopens replay for explicit retryable fallback failures", async () => {
const onBotMenu = await registerHandlers();
const runtime = { log: vi.fn(), error: vi.fn(), exit: vi.fn() } as RuntimeEnv;
const onBotMenu = await registerHandlers({ runtime });
sendCardFeishuMock
.mockImplementationOnce(async () => {
throw new Error("boom");
@@ -180,9 +184,16 @@ describe("Feishu bot menu handler", () => {
.mockResolvedValueOnce(undefined);
await onBotMenu(createBotMenuEvent({ eventKey: "quick-actions", timestamp: "1700000000004" }));
await vi.waitFor(() => {
expect(runtime.error).toHaveBeenCalledWith(
"feishu[default]: error handling bot menu event: FeishuRetryableSyntheticEventError: retry me",
);
});
await onBotMenu(createBotMenuEvent({ eventKey: "quick-actions", timestamp: "1700000000004" }));
expect(sendCardFeishuMock).toHaveBeenCalledTimes(2);
expect(handleFeishuMessageMock).toHaveBeenCalledTimes(1);
await vi.waitFor(() => {
expect(handleFeishuMessageMock).toHaveBeenCalledTimes(2);
});
});
});

View File

@@ -14,7 +14,6 @@
"dist/extensions/signal/runtime-api.js",
"dist/extensions/telegram/runtime-api.js",
"dist/extensions/telegram/runtime-setter-api.js",
"dist/extensions/tokenjuice/runtime-api.js",
"dist/extensions/webhooks/runtime-api.js",
"dist/extensions/workboard/runtime-api.js",
"dist/extensions/zai/runtime-api.js"