/** * Feishu lark client + long-connection event dispatcher. * * Uses lark's WebSocket long-connection mode (no public callback endpoint). The * `EventDispatcher` registers `im.message.receive_v1`; the SDK delivers the raw * event to our handler. This version of the SDK does not auto-normalize on the * ws path — `normalize()` is a separate helper — so we work with the raw event * shape directly (it carries chat_id, message_type, content, mentions). * * ADR-0001: the bot operates inside a project's bound group. It does not own * the lock (ADR-0002) and is not the session owner. */ import * as lark from "@larksuiteoapi/node-sdk"; import type { FastifyBaseLogger } from "fastify"; export interface FeishuConfig { readonly appId: string; readonly appSecret: string; readonly botOpenId: string; } export interface FeishuRuntime { readonly client: lark.Client; readonly logger: FastifyBaseLogger; } /** Raw `im.message.receive_v1` event payload (subset we use). */ export interface MessageReceiveEvent { readonly message: { readonly message_id: string; readonly chat_id: string; readonly chat_type: string; readonly message_type: string; readonly content: string; readonly mentions?: ReadonlyArray<{ readonly key: string; readonly id: { readonly open_id?: string }; readonly name: string; }>; }; readonly sender: { readonly sender_id: { readonly open_id?: string }; readonly sender_type: string; }; } /** Send a text message to a chat. */ export async function sendText(rt: FeishuRuntime, chatId: string, text: string): Promise { // SDK exposes im.v1 dynamically at runtime; the generated types are weak // there, so cast through the known shape. const client = rt.client as unknown as { im: { v1: { message: { create: (p: unknown) => Promise } } }; }; await client.im.v1.message.create({ params: { receive_id_type: "chat_id" }, data: { receive_id: chatId, msg_type: "text", content: JSON.stringify({ text }), }, }); } /** * Start the lark WebSocket client and route `im.message.receive_v1` events to * `onMessage`. The ws connection lives in the background; this resolves the * runtime handle. */ export function startFeishuListener( config: FeishuConfig, logger: FastifyBaseLogger, onMessage: (event: MessageReceiveEvent, rt: FeishuRuntime) => Promise, ): FeishuRuntime { const client = new lark.Client({ appId: config.appId, appSecret: config.appSecret, appType: lark.AppType.SelfBuild, }); const wsClient = new lark.WSClient({ appId: config.appId, appSecret: config.appSecret, domain: lark.Domain.Feishu, loggerLevel: lark.LoggerLevel.info, }); const rt: FeishuRuntime = { client, logger }; void wsClient.start({ eventDispatcher: new lark.EventDispatcher({}).register({ "im.message.receive_v1": async (data) => { try { await onMessage(data as MessageReceiveEvent, rt); } catch (e) { logger.error({ err: e }, "feishu message handler threw"); } }, }), }); return rt; }