import type { PlatformAdapter } from '../adapter.js'; import type { MessageRef, MessageContent, MessageContext, MessageEditContext, ActionContext, ModalSubmitContext, ModalDefinition, PlatformCapabilities, PlatformFileRef, DownloadedFile, Destination, PostMessageOpts, FileUploadOpts, ActionElement } from '../types.js'; import type { OutputStream, OpenOutputStreamOpts } from '../output-stream.js'; export interface FeishuAdapterConfig { appId: string; appSecret: string; encryptKey?: string; verificationToken?: string; adminChannel?: string; domain?: 'feishu' | 'lark'; } export declare class FeishuAdapter implements PlatformAdapter { readonly name = "feishu"; readonly capabilities: PlatformCapabilities; private client; private wsClient; private readonly domain; private eventDispatcher; private config; private messageHandler; private actionHandlers; private modalHandlers; private _adminAutoDetected; constructor(config: FeishuAdapterConfig); private static readonly PREFIX; /** Add the `feishu:` prefix (idempotent; passes through empty strings). */ private _wrap; /** Strip the `feishu:` prefix (tolerates already-bare values for back-compat). */ private _unwrap; /** True if this conduit belongs to the Feishu adapter. */ ownsConduit(conduit: string): boolean; start(): Promise; stop(): Promise; onMessage(handler: (ctx: MessageContext) => Promise): void; onMessageEdit(_handler: (ctx: MessageEditContext) => Promise): void; onAction(actionId: string, handler: (ctx: ActionContext) => Promise): void; onModalSubmit(callbackId: string, handler: (ctx: ModalSubmitContext) => Promise): void; postMessage(destination: Destination, content: MessageContent, opts?: PostMessageOpts): Promise; updateMessage(ref: MessageRef, content: MessageContent): Promise; deleteMessage(ref: MessageRef): Promise; postInteractive(destination: Destination, content: MessageContent & { actions: ActionElement[]; }, opts?: PostMessageOpts): Promise; openModal(triggerId: string, modal: ModalDefinition): Promise; private _addHourglassReaction; markQueued(ref: MessageRef): Promise; uploadFile(destination: Destination, filePath: string, opts?: FileUploadOpts): Promise; downloadFile(fileRef: PlatformFileRef, destDir: string): Promise; getPermalink(_ref: MessageRef): Promise; openOutputStream(destination: Destination, opts?: OpenOutputStreamOpts): OutputStream; private _conduitsStore; private _getConduitsStore; bindProjectConduit(projectId: string, conduitHint: string): Promise; unbindProjectConduit(projectId: string): Promise; getProjectConduits(): Promise>; resolveInboundProject(conduit: string): Promise; /** * Resolve a Destination to a concrete Feishu chat_id + kind label. * Returns channel=null for destinations that should be silently dropped * (unconfigured admin channel). */ private resolveDestination; /** * Upsert FEISHU_ADMIN_CHANNEL= into /.env so the detected * admin DM survives restarts. Mirrors SlackAdapter._persistAdminChannel. */ private _persistAdminChannel; private handleIncomingMessage; /** * Build PlatformFileRef[] from a parsed Feishu message content payload. * Feishu stores the resource key inside `content` (not on the message object): * - file / media (video): { file_key, file_name? } * - image: { image_key } * - audio: { file_key } * message_id + resourceType are stashed in `raw` so downloadFile() can call * im.v1.messageResource.get (which requires both message_id and a type param). */ private extractInboundFiles; /** * Parse a Feishu `post` (富文本/rich-text) message content payload. Feishu wraps * a single message that mixes text and images as a post, whose `content` is a 2D * array of paragraphs, each a run of element nodes: * { title?, content: [ [ {tag:'text',text}, {tag:'a',text,href}, {tag:'at',user_name}, * {tag:'img',image_key} ], ... ] } * Returns the concatenated text (paragraphs joined by '\n', non-empty title * prefixed) and the list of inline image_keys, so callers recover BOTH the text * and the images from one mixed message. */ private parsePostContent; /** * Resolve a Feishu message's human-readable text from its type + parsed content. * `post` (rich text) buries text in a 2D run array; plain text lives in * parsed.text; a few legacy payloads ship a bare non-JSON string as content. * Shared by both inbound messages and quoted-parent enrichment. */ private extractMessageText; /** * Fetch the directly-quoted parent message (one level) and project it to the same * { text, files } shape an inbound message produces, so a reply can be enriched * with what it quotes — notably files, since a Feishu file message carries no text * and thus cannot @ the bot. Returns null on any failure (best-effort). * NOTE: im.v1.message.get returns content under data.items[].body.content (a JSON * string), NOT the top-level `content` field the receive_v1 event uses. */ private fetchQuotedParent; private handleCardAction; private buildMessagePayload; private buildCardJson; private replyInThread; /** Convert RichBlock[] to Feishu card schema 2.0 elements. */ private richBlocksToFeishuElements; /** * Convert a ButtonElement to a Feishu card **schema 2.0** button. * * Card 2.0 buttons carry their interaction in a `behaviors` callback (the old * top-level `value` field is form-only in 2.0). `name` is preserved so * handleCardAction's `action.name` lookup keeps working; the callback `value` * delivers `{ actionId, value }`, which arrives as `event.action.value`. */ private actionElementToFeishu; /** * Lay buttons out as a wrapping `column_set` (one auto-width column per button). * Feishu card schema 2.0 removed the `action` container tag (error 200861: * "unsupported tag action"), so buttons must be direct body elements. * `flex_mode: 'flow'` + per-column `width: 'auto'` makes the buttons size to * their text and wrap onto multiple rows — without `flow` many buttons get * crushed into one unreadable row. */ private buttonsToColumnSet; /** * Convert ModalDefinition to a Feishu card with embedded form container. * Since Feishu has no native modal, we post a card message containing a * form that users can fill out and submit inline. */ private modalToFeishuCard; /** * Normalize Feishu form_value into the platform-agnostic ModalFieldValue format. * Feishu form_value is a flat dict { field_name: value } where field_name follows * our convention "{blockId}::{actionId}::{kind}" (kind ∈ select | multi | text). * The kind segment is what lets us map a single-select (a bare string) to * `selectedOption` rather than `value` — the downstream consumer * (interaction-handlers.ts) reads `selectedOption.value` for single-select. * We reconstruct the two-level { blockId: { actionId: ModalFieldValue } } structure. */ private normalizeFormValues; private resolveFilePath; private inferFeishuFileType; }