import type { ChannelAdapter, ChoiceOption, ChoicePrompt, ChoiceResult, InboundMessage, OutboundTarget, SummaryCard } from "../../types.js"; import type { Language } from "../../i18n.js"; import { type ChannelConnectionStatus } from "../../settings/channel-status.js"; /** Pad every label with trailing spaces up to the same display width. */ export declare function padLabels(options: readonly ChoiceOption[]): ChoiceOption[]; /** * Render options as an equal-width button grid: 2 buttons per row via * `column_set` (two weighted columns), padding the last row with empty * columns so every row is a uniform 2-cell layout. Labels are padded to the * same display width so the buttons themselves render uniformly. Destructive * actions (labels starting with ❌) render as red danger buttons. */ export declare function buildButtonGrid(options: readonly ChoiceOption[], columnsPerRow?: number): unknown[]; /** * Render options as a single-select dropdown (`select_static`) inside an * `action` container. Selecting an entry fires the card action with * `action.tag === "select_static"` and the picked option id in `action.option`. */ export declare function buildSelectMenu(options: readonly ChoiceOption[], placeholder?: string, initial?: string): unknown[]; /** * Assemble the card elements for a choice prompt. When `sections` is given, * the options are split into titled groups — each with a bold section caption * and a divider before it — instead of one flat grid. Options not listed in * any section (typically the trailing exit/back buttons) are rendered after * the sections, separated by a divider. * * @param prompt - The choice prompt to render * @param defaultColumns - Default number of columns per row (default: 2) */ export declare function buildChoiceElements(prompt: ChoicePrompt, defaultColumns?: number): unknown[]; /** Classify a local file name for the Feishu upload API (image vs stream file). */ export declare function classifyFeishuFile(filename: string): "image" | "file"; /** Strip path separators / control chars from a downloaded file's name. */ export declare function sanitizeFileName(name: string): string; /** * Extract a human-readable error detail. Feishu business errors (missing * permission, invalid resource, …) arrive as HTTP 200/400 with the real * `{code, msg}` in the response body; prefer that over the axios fallback * message like "Request failed with status code 400" that hides the cause. */ export declare function extractErrorDetail(error: unknown): string; export interface FeishuConfig { appId?: string; appSecret?: string; transport?: "websocket" | "webhook"; verificationToken?: string; encryptKey?: string; /** HTTP port for `transport: "webhook"` (default 9000). */ webhookPort?: number; /** HTTP path the Feishu event callback posts to (default "/"). */ webhookPath?: string; /** Group messages must @-mention the bot (default true). */ requireMention?: boolean; /** Single-chat policy (SDK values): open / allowlist / pair / disabled. */ dmMode?: "open" | "allowlist" | "pair" | "disabled"; /** Bind one DSH session per group thread instead of one per chat (default false). */ threadIsolation?: boolean; /** * Allow the interactive one-click onboarding flow when no credentials are * configured (default true). Set `false` to never start it — the adapter then * just warns, which is what a supervisor-managed deployment wants. */ onboarding?: boolean; /** User-facing message language: `zh` (default) or `en`. */ language?: Language; } export declare class FeishuAdapter implements ChannelAdapter { private readonly logger?; private readonly isChatAllowed?; readonly id = "feishu"; private readonly channel; private readonly pendingChoices; /** Cards whose prompt just retired: their taps are mid-redraw noise, not staleness. */ private readonly absorbingChoices; /** Cards whose stale-button tap was already noticed (dedupe, self-clearing). */ private readonly staleNoticed; private readonly staleTimers; private readonly t; private readonly transport; private readonly webhookPort; private readonly webhookPath; private readonly threadIsolation; private server?; private handler?; constructor(config: FeishuConfig, logger?: { info?: (...args: unknown[]) => void; warn?: (...args: unknown[]) => void; error?: (...args: unknown[]) => void; } | undefined, isChatAllowed?: ((channel: string, chatKey: string, senderKey: string) => boolean) | undefined); onInbound(handler: (msg: InboundMessage) => void | Promise): void; /** * The SDK's own view of the socket, which is the only honest source for * 「已连接」 — the runtime can say a channel started, not that it is up. * * `undefined` covers both "the transport holds no socket" (webhook mode) and * "the SDK has nothing to report yet"; the settings pane renders those as * `running`, i.e. what the runtime actually knows. A state the SDK adds later * is narrowed out rather than passed through, so an unrecognized string can * never reach the pane as an untranslated badge. */ connectionStatus(): ChannelConnectionStatus | undefined; /** Resource types downloadable via `im.v1.messageResource.get`. Stickers are not supported by the Feishu API. */ private static readonly DOWNLOADABLE_TYPES; /** Download attached images / files into local temp dirs; return their paths (and any failures). */ private downloadResources; /** * Download a resource inside a user message. * * Note: do NOT use the SDK's `downloadResource(fileKey, type)` — it calls * `im.v1.image.get` / `im.v1.file.get` (download image / download file), and * per the Feishu docs those endpoints **can only download resources uploaded * by the bot itself**. Resources inside user-sent messages must be fetched * with "get resource file from message" `im.v1.messageResource.get` (with * message_id + type=image/file/audio/video), otherwise it returns HTTP 400. */ private downloadMessageResource; /** * Housekeeping for the per-channel temp dirs: remove downloads older than * 24h so a busy bot doesn't fill the OS temp partition. Best-effort — a * failure to clean is logged, never fatal. */ private cleanupTempDirs; start(): Promise; stop(): Promise; /** Base chat id for outbound sends (strips any `:thread=` suffix). */ private chatIdOf; sendText(target: OutboundTarget, text: string): Promise; /** Post a text message and return its id, so a repeatable status line can be rewritten. */ sendTextWithId(target: OutboundTarget, text: string): Promise<{ messageId: string; }>; /** Rewrite a text message this adapter posted. See {@link sendTextWithId}. */ updateText(messageId: string, text: string): Promise; sendCard(target: OutboundTarget, card: SummaryCard): Promise; /** * Post a card and return its id, so it can be rewritten in place later. * * Feishu can do this for any card it sent, which is what makes a live subtask list * possible: the plan is posted once and then edited as steps complete, instead of * the chat filling up with one bubble per update. */ sendCardWithId(target: OutboundTarget, card: SummaryCard): Promise<{ messageId: string; }>; /** Rewrite a card this adapter posted. See {@link sendCardWithId}. */ updateCard(messageId: string, card: SummaryCard): Promise; streamText(target: OutboundTarget, chunks: AsyncIterable): Promise; /** * Deliver a local file to the chat. The SDK uploads the source itself: * images are sent inline, everything else as an attachment. */ sendFile(target: OutboundTarget, filePath: string, options?: { filename?: string; }): Promise; /** * Build the send options shared by text/markdown deliveries: an optional * reply target and @-mentions (SDK renders `` prefixes for * both text and post messages, so group completion cards can nudge the * requester). */ private sendOpts; /** * Swallow taps on `messageId` for a short grace window (self-clearing). * Called whenever a prompt retires while its card may still be on screen. */ private absorbChoice; promptChoice(target: OutboundTarget, prompt: ChoicePrompt, updateMessageId?: string, signal?: AbortSignal, onPresented?: (messageId: string) => void): Promise; closeMenu(messageId: string, summary: string): Promise; /** * Remove a card outright, as the last resort when it cannot be rewritten. * * The buttons are the problem, not the text: a settled card whose options are still live * absorbs every tap silently. If the redraw fails there is no way to keep the card *and* * disarm it, so it goes. */ private recallCard; } //# sourceMappingURL=adapter.d.ts.map