import type { LarkMessage, LarkMention } from '../../types.js'; interface RawEventData { sender: { sender_id: { open_id?: string; app_id?: string; user_id?: string; union_id?: string; }; sender_type: string; tenant_key?: string; }; message: { message_id: string; root_id?: string; thread_id?: string; parent_id?: string; message_type: string; content: string; chat_id: string; chat_type: string; create_time: string; mentions?: Array<{ key: string; name: string; id?: { open_id?: string; user_id?: string; union_id?: string; app_id?: string; } | string; id_type?: string; tenant_key?: string; }>; }; } /** * Extract a mention's open_id, tolerating BOTH Lark shapes for `mention.id`: * * - WebSocket event (im.message.receive_v1): `id` is an OBJECT * { open_id, union_id, user_id } ← what botmux has always seen * - Message REST API (im.message.get / list): `id` is a bare STRING "ou_xxx" * with a sibling `id_type` ("open_id" | "union_id" | "user_id") * * The two diverged in production: the API already ships the flat-string form * while the event still ships the object form. If Lark ever converges the event * onto the string form, every `m.id.open_id` read across botmux would silently * become `undefined` and @-detection (isBotMentioned) would fail closed with no * log. Reading through this helper keeps every caller robust regardless of * which shape arrives. * * For the string form we return the value only when it actually IS an open_id * (id_type === 'open_id', or absent — mentions are open_id-keyed by default). A * string carrying a non-open_id id_type (union_id / user_id, which Lark may * return when the app lacks the open_id scope) yields `undefined` rather than * being mis-compared against a botOpenId. */ export declare function mentionOpenId(m: { id?: { open_id?: string; app_id?: string; } | string | null; id_type?: string; } | null | undefined): string | undefined; export interface MentionIdentity { key?: string; name?: string; openId?: string; userId?: string; unionId?: string; appId?: string; idType?: string; } /** Extract all stable ids Lark provides for @mentions, across WS and REST shapes. */ export declare function mentionIdentity(m: { key?: string; name?: string; id?: { open_id?: string; user_id?: string; union_id?: string; app_id?: string; } | string | null; id_type?: string; } | null | undefined): MentionIdentity; /** id_type across both shapes Lark uses (snake_case REST, camelCase legacy). */ export declare function mentionIdType(m: any): string | undefined; /** * Extract a bot mention's app_id across ALL shapes Lark has been observed to * use. app_id-form bot mentions must never flow through mentionOpenId() (which * is persisted/used as an open_id), so they are matched separately: * 1. top-level camelCase `m.appId` * 2. top-level snake_case `m.app_id` * 3. string `m.id` with id_type === 'app_id' (REST bare-string OR legacy) * 4. object `m.id.app_id` * Keep this exhaustive: the realtime @-gate (isBotMentioned) depends on it, so * dropping a shape silently un-@'s a bot in that shape. */ export declare function mentionAppId(m: any): string | undefined; /** * Extract @-mentions carried by a post (rich-text) message's inline `at` nodes, * as routing-only `LarkMention`s for the --mention-back participant window. * * WHY separate from parseEventMessage().mentions: a post's `message.mentions[]` * is frequently empty — the real @s live as inline `{ tag: 'at', user_id, * user_name }` nodes in the content (the realtime @-gate isBotMentioned already * scans these). Folding them into the general `parsed.mentions` would ripple * into prompt rendering / stripLeadingMentions / mention hints, so this stays a * dedicated lane consumed ONLY by buildTurnParticipants. * * A post `at`'s `user_id` is an `ou_` open_id (in-group) or a `cli_` app_id * (out-of-group bot). We classify into openId vs appId; `all` and any other * shape are surfaced WITHOUT an executable id so the participant core marks the * window incomplete rather than inventing a candidate. Deliberately NO position * filtering (unlike mention-targets' command parsing) — every counterpart in the * turn counts, including a leading @ of the answering bot (excluded later by * self open_id/app_id). Returns [] on non-post shapes / parse errors. */ export declare function extractPostAtParticipants(message: { content?: string; } | null | undefined): LarkMention[]; /** * truth for the @-gate across every consumer (realtime routing's isBotMentioned, * the 30s poll backfill, and the dashboard preview/run-preview collector) so the * "explicit @ hands off to normal routing, not the listener" rule can never * diverge between legs. Matches by open_id OR by app_id (via mentionAppId, which * covers every observed app_id shape), and also scans post-content inline `at` * tags (post messages may not populate `mentions`). */ export declare function messageMentionsBot(message: { mentions?: any[]; content?: string; body?: { content?: string; }; } | null | undefined, larkAppId: string | undefined, botOpenId: string | undefined): boolean; export declare function extractMentionIdentities(message: { mentions?: Array<{ key?: string; name?: string; id?: { open_id?: string; user_id?: string; union_id?: string; app_id?: string; } | string | null; id_type?: string; }>; content?: string; } | null | undefined): MentionIdentity[]; export declare function mentionUnionId(m: { id?: { union_id?: string; } | string | null; id_type?: string; } | null | undefined): string | undefined; /** * When the WebSocket event delivers message_type "nonsupport", call the REST API * to fetch the real message content and patch the event data in-place. * * Also handles `interactive`: the WebSocket event only carries a simplified * fallback view of cards (often literally "请升级至最新版本客户端,以查看内容"), * so we fetch the real card JSON (including v2 `body.elements`) via REST. */ export declare function resolveNonsupportMessage(data: RawEventData, larkAppId: string): Promise; /** * Lark bundles the real v2 card JSON inside a `user_dsl` string on the * simplified interactive payload. When present, return the unwrapped v2 * body so downstream extractors see schema/body.elements directly. */ export declare function unwrapUserDslContent(rawContent: string): string | null; /** * Lark's simplified "upgrade your client" card fallback marker. When this text * shows up in a card's resolved content, the real body was stripped and must be * recovered via `im.message.get` (with `card_msg_content_type=user_card_content`). */ export declare const CARD_UPGRADE_FALLBACK = "\u8BF7\u5347\u7EA7\u81F3\u6700\u65B0\u7248\u672C\u5BA2\u6237\u7AEF"; /** * Broad check — content carries the upgrade notice *somewhere*. Used to decide * whether a card needs REST re-resolution. Deliberately a substring match: a * false positive only costs one extra `im.message.get`, and this is the only * way to catch **embedded** fallbacks — complex cards (e.g. Argos alarm cards * with nested sub-cards) render fine at the top level but bury one or more * `请升级…` placeholders mid-body where an anchored check would miss them. */ export declare function cardContentHasUpgradeFallback(content: string): boolean; /** * Narrow check — content *is* essentially just Lark's upgrade notice, not a * body that merely mentions it. Anchored at the start after stripping leading * `[图片]` / `[文件 N]` placeholders (the bare fallback renders as * `[图片]请升级至最新版本客户端,以查看内容`). Used as the replace gate when * re-resolving via REST: it keeps a card that legitimately quotes the phrase * mid-text — e.g. a message discussing this very fallback — from being * discarded, while still rejecting a REST view that came back as a bare * fallback. Makes no structural assumptions, so it's safe for the * simplified-but-real Format A shape (no schema/body/header) message.list * returns. */ export declare function isPureCardUpgradeFallback(content: string): boolean; export interface MessageResource { type: 'image' | 'file'; key: string; name: string; /** When set, download uses this message_id instead of the parent (e.g. merge_forward sub-messages). */ messageId?: string; } /** * Stateful numbering that keeps `[图片 N]` / `[文件 N]` placeholders in the * rendered text aligned with the attachment footer. The same key always gets * the same number, so duplicates across merge_forward sub-messages collapse * correctly. * * Image and file counters are INDEPENDENT — `formatAttachmentsHint` emits * `` and `` separately numbered, so a message with * one image and one file should render as `[图片 1]` + `[文件 1]`, not * `[图片 1]` + `[文件 2]`. Keys are typed via the `image:` / `file:` prefix. */ export interface ImgNumberer { assign(key: string): { num: number; isNew: boolean; }; } export declare function createImgNumberer(): ImgNumberer; export declare function extractResources(msgType: string, rawContent: string, numberer?: ImgNumberer): MessageResource[]; export declare function parseEventMessage(data: RawEventData, numberer?: ImgNumberer): { parsed: LarkMessage; resources: MessageResource[]; }; export declare function parseApiMessage(msg: any, numberer?: ImgNumberer): LarkMessage; /** * Strip leading `@` mentions from a resolved-content string so callers * can detect daemon `/commands` even when the user @-mentioned the bot first. * * Uses the structured mentions list when available (handles names with spaces); * falls back to a `@\S+` regex for cases where Lark didn't populate mentions * (e.g. some post messages where the at-tag becomes a plain `@` * string in the rendered text). */ export declare function stripLeadingMentions(content: string, mentions?: { name: string; }[]): string; /** * Extract human-readable text from an interactive card. * * Lark API returns card content in a **simplified format** (not the original card JSON): * { title: "...", elements: [[{tag:"text",text:"..."}, ...], ...] } * This is similar to post message body. We also handle the original card JSON * (header/config/elements with tag objects) for locally-cached cards. */ export declare function extractCardContent(rawContent: string, numberer?: ImgNumberer): string; /** * Marker for sub-cards Lark renders only client-side (collapsible panels, * lazy "展开" sections). Neither `im.message.get` representation returns their * body — Format A shows the upgrade fallback, Format B omits them — so we * surface an honest placeholder instead of a misleading blank or raw fallback. */ export declare const CARD_EMBEDDED_PLACEHOLDER = "[\u5361\u7247\u5185\u5D4C\u7EC4\u4EF6\uFF0C\u9700\u5728\u98DE\u4E66\u5BA2\u6237\u7AEF\u5C55\u5F00\u67E5\u770B]"; /** Wrap merged text so extractCardContent returns it verbatim downstream. */ export declare function wrapResolvedCardText(text: string): string; /** * Merge the two Lark card renderings into one complete text. Format B (full * structured) is the base — it preserves links, sub-card bodies and select * options. From Format A (server-rendered) we recover only the field VALUES B * left blank (e.g. 值班人 names) via targeted label-fill: a B label line whose * value is empty is filled from A only when A's value isn't already present in * B (so fields B already renders aren't duplicated). Sub-cards Lark serves * client-side only — which A shows as upgrade holes and B omits — get one * honest placeholder rather than a silent drop or raw "请升级" text. */ export declare function mergeCardText(textA: string, textB: string): string; /** * Resolve a card to its most complete text by unioning both `im.message.get` * representations (server-rendered Format A + full structured Format B). Used * by BOTH the live event path and `botmux history` so a single message_id * resolves identically everywhere. Returns null when neither representation * could be fetched (caller keeps whatever it already had). */ export declare function resolveMergedCardContent(larkAppId: string, messageId: string, numberer?: ImgNumberer): Promise<{ text: string; structuredContent: string; resources: MessageResource[]; } | null>; /** * Resolve an interactive event's card to its complete merged text in place. * Stores a sentinel that carries BOTH the merged text (for extractCardContent) * and the structured card JSON (for extractResources image/file extraction). * Falls back to local user_dsl unwrap if the REST merge yields nothing. Shared * by the live daemon path so forwarded cards reach the model fully parsed. */ export declare function resolveEventCard(data: RawEventData, larkAppId: string): Promise; export {}; //# sourceMappingURL=message-parser.d.ts.map