feat: archive bindings on Feishu lifecycle events

This commit is contained in:
2026-07-13 15:53:28 +08:00
parent d94cc787b2
commit 82f57317df
7 changed files with 402 additions and 7 deletions
+46
View File
@@ -106,6 +106,13 @@ export interface CardActionEvent {
readonly token?: string;
}
/** A binding-ending Feishu event delivered to this application's bot. */
export interface FeishuBindingLifecycleEvent {
readonly eventId: string;
readonly chatId: string;
readonly reason: "chat_dissolved" | "bot_removed";
}
interface ContactBasicUser {
readonly name?: string | undefined;
readonly i18n_name?: {
@@ -922,6 +929,7 @@ export async function startFeishuListenerWithClient(
onMessage: (event: MessageReceiveEvent, rt: FeishuRuntime) => Promise<void>,
onCardAction?: (event: CardActionEvent, rt: FeishuRuntime) => Promise<void>,
onTerminalError?: (error: Error) => void,
onBindingLifecycle?: (event: FeishuBindingLifecycleEvent) => Promise<void>,
): Promise<FeishuRuntime> {
let state: "STARTING" | "READY" | "FAILED" = "STARTING";
let resolveReady!: () => void;
@@ -963,6 +971,14 @@ export async function startFeishuListenerWithClient(
.catch((e) => { logger.error({ err: e }, "feishu card action handler threw"); });
};
}
if (onBindingLifecycle !== undefined) {
handlers["im.chat.disbanded_v1"] = async (data) => {
await dispatchBindingLifecycleEvent(data, "chat_dissolved", onBindingLifecycle, logger);
};
handlers["im.chat.member.bot.deleted_v1"] = async (data) => {
await dispatchBindingLifecycleEvent(data, "bot_removed", onBindingLifecycle, logger);
};
}
await wsClient.start({ eventDispatcher: new lark.EventDispatcher({}).register(handlers) });
let timeout: ReturnType<typeof setTimeout> | undefined;
const startupTimeout = new Promise<never>((_resolve, reject) => {
@@ -977,6 +993,36 @@ export async function startFeishuListenerWithClient(
return rt;
}
async function dispatchBindingLifecycleEvent(
data: unknown,
reason: FeishuBindingLifecycleEvent["reason"],
onBindingLifecycle: (event: FeishuBindingLifecycleEvent) => Promise<void>,
logger: FastifyBaseLogger,
): Promise<void> {
const event = bindingLifecycleEventFrom(data, reason);
if (event === null) {
logger.warn({ reason }, "feishu binding lifecycle event missing chat id");
return;
}
await onBindingLifecycle(event);
}
function bindingLifecycleEventFrom(
data: unknown,
reason: FeishuBindingLifecycleEvent["reason"],
): FeishuBindingLifecycleEvent | null {
if (typeof data !== "object" || data === null) return null;
const event = data as { readonly chat_id?: unknown; readonly event_id?: unknown; readonly header?: { readonly event_id?: unknown } };
if (typeof event.chat_id !== "string" || event.chat_id.trim() === "") return null;
const eventId = typeof event.event_id === "string" && event.event_id !== ""
? event.event_id
: typeof event.header?.event_id === "string" && event.header.event_id !== ""
? event.header.event_id
: undefined;
if (eventId === undefined) return null;
return { chatId: event.chat_id, reason, eventId };
}
export async function startFeishuListener(
config: FeishuConfig,
logger: FastifyBaseLogger,