import { LarkTransportDisabledError } from '../../bot-registry.js'; import { type ManagedHookOrigin } from '../../services/hook-runner.js'; import type { ChatContext } from '../../types.js'; type LarkRequestParams = Record; export interface LarkRequestOptions { /** Axios 层的真实请求超时;不设置时保持 SDK 原有行为。 */ timeoutMs?: number; /** 透传给 Axios,用于在上层截止时间到达时主动取消网络请求。 */ signal?: AbortSignal; } /** * Call a Feishu GET endpoint without a request body. * * The official SDK currently lets axios attach `{}` as `data` for generated * GET calls such as im.v1.message.list/get, im.v1.chat.get/list, * im.v1.chatMembers.isInChat and contact.v3.user.get. Some gateway * deployments reject GET-with-body and return HTTP 411 before the OpenAPI * handler sees the request. The SDK's generic `client.request()` contains an * explicit GET empty-body guard (`fix: #153`) while still using the SDK's * token/cache/auth plumbing, so route every read-only GET through it. * * `url` is the API path (e.g. `/open-apis/im/v1/chats/`); path params must * already be interpolated by the caller. Returns the parsed JSON body * (`{ code, msg, data }`), identical to the generated method's resolved value. */ export declare function larkGet(c: any, url: string, params?: LarkRequestParams, options?: LarkRequestOptions): Promise; /** Test seam for suites that replace the configured bot set at runtime. */ export declare function __testOnly_resetAllBotClients(): void; /** * Find the first locally-configured (non-apiOnly) bot that is a member of * `chatId`, using the QUIET probe clients (probeLarkLogger) — so misses for * bots not in the chat don't splash AxiosError blobs to stdout/logs the way a * plain getBotClient()+isInChat loop does. * * Used by `bots invite`'s auto-add flow to pick a proxy bot already in the * target group (Feishu requires the adding app to be a member). Returns the * matching bot's appId+cliId, or null if none of our bots are in that chat. * `preferAppId` (e.g. the current session bot) is checked first. */ export declare function findLocalBotInChat(chatId: string, preferAppId?: string): Promise<{ larkAppId: string; cliId: string; } | null>; /** Thrown when the target message has been withdrawn (Lark code 230011). */ export declare class MessageWithdrawnError extends Error { constructor(messageId: string); } /** * Re-exported from bot-registry (defined there to avoid an import cycle with * getBotClient). apiOnly bots throw this on any Feishu client request. */ export { LarkTransportDisabledError }; /** Bot-level transport gate: an apiOnly bot must never make an outbound Feishu * call. Called at the top of every write primitive. `op` names the primitive * for diagnostics. Read-only lookups (message detail, chat members) intentionally * do NOT call this — they are inert reads used by discovery, already filtered * elsewhere; only side-effecting writes are hard-gated here. Exported so other * modules with their OWN direct-Feishu implementations (e.g. doc-comment's * drive API) can enforce the same bot-level boundary from one definition. */ export declare function assertLarkTransport(larkAppId: string, op: string): void; /** * Thrown ONLY when a resource download genuinely needs (re-)authorization: no * usable User Token on disk, or the User Token was rejected as unauthorized * (HTTP 401). Callers gate the "/login" prompt on `instanceof` this — NOT on a * substring of the message — so an ordinary download failure (4xx/5xx for a * cross-tenant / card-image / withdrawn resource) is no longer misreported as * "missing User Token, please /login" even though a valid token was used. */ export declare class UserTokenMissingError extends Error { constructor(message: string); } /** * Send a message to a chat. * * `uuid` is an optional opt-in dedupe token (Feishu IM uuid field, ≤ 50 * chars, 1-hour TTL — see spike report §1.2). When supplied, the Feishu * server returns the original message_id for repeat requests within TTL, * making the send idempotent. Workflow runtime passes the attempt's * idempotencyKey here so retries don't re-send. Existing callers omit * the param and get exactly the pre-Step-6 behavior. */ export interface OutboundMessageOptions { /** The provider request is reconciling an already-attempted stable UUID. * Lark deduplicates the message, but the local outbound hook is a separate * side effect and must not be fired twice. */ suppressHook?: boolean; /** Fence the distinct post-provider hook effect. A failure drops only the * hook because the Lark message has already been accepted and must not be * reported as failed (which would invite a duplicate retry). */ beforeHook?: () => void | Promise; /** Frozen protected origin used by read-isolated hook forwarding. */ hookOrigin?: ManagedHookOrigin; } export declare function sendMessage(larkAppId: string, chatId: string, content: string, msgType?: string, uuid?: string, hookContext?: Record, options?: OutboundMessageOptions): Promise; /** * Reply to an existing message. See {@link sendMessage} for the `uuid` * dedupe parameter — same semantics apply to replies (Feishu reply API * also accepts `uuid` and yields the same 1-hour idempotent return). See * spike report §1.4 for the reply-specific test results, including the * cross-parent dedupe behavior that informs the inputHash design. */ export declare function replyMessage(larkAppId: string, messageId: string, content: string, msgType?: string, replyInThread?: boolean, uuid?: string, hookContext?: Record, options?: OutboundMessageOptions): Promise; export declare function addReaction(larkAppId: string, messageId: string, emojiType: string): Promise; export declare function removeReaction(larkAppId: string, messageId: string, reactionId: string): Promise; /** * Resolve a user's tenant-stable `union_id` from their app-scoped `open_id`. * Used by cross-daemon owner checks (e.g. /relay --create peer migrate) * to compare identities across bot namespaces — open_id alone is * app-scoped, so two daemons looking at the same physical user see * different open_ids. * * Best-effort: returns null on API failure / missing scope / empty * response, so callers can fall back to other identity strategies * instead of failing the whole flow. */ export declare function resolveUnionIdFromOpenId(larkAppId: string, openId: string): Promise; export type UserProfileLookup = { status: 'ok'; profile: { name: string; avatarUrl?: string; }; } /** Definitive: the open_id belongs to another app (99992361). */ | { status: 'cross_app'; } /** Definitive: outside this app's contact visibility scope (41050). */ | { status: 'not_visible'; } /** Definitive: no such user / malformed id (41012 / 40001). */ | { status: 'invalid_id'; } /** Transient: network / rate limit / server error — retry may succeed. */ | { status: 'error'; }; /** * 严格版用户资料查询:按原因区分确定性失败(跨应用/不可见/无效 id)与瞬时 * 失败。需要据此做决策的调用方(如替身对象解析——误把瞬时失败当跨应用会引导 * 用户删掉合法配置)用这个;只要 best-effort 名字的调用方用 {@link getUserProfile}。 */ export declare function getUserProfileStrict(larkAppId: string, userId: string, idType?: 'open_id' | 'union_id'): Promise; /** * Best-effort 拉用户资料(名字 + 头像 URL)。拿不到(缺 scope / 不在可见 * 范围 / 网络错误)返回 null,调用方自行回退占位。 */ export declare function getUserProfile(larkAppId: string, userId: string, idType?: 'open_id' | 'union_id'): Promise<{ name: string; avatarUrl?: string; } | null>; /** * Best-effort 判断一个 open_id 是否为「真人」(通讯录里查得到 user)。 * * - code 0 且返回 user 对象 → 确定是真人 → true * - 查不到 / 报错 → false。这一类同时覆盖两种情况:①bot(应用不在通讯录,必然查不到); * ②本 app 缺 `contact:user.base:readonly` 读权限(这时真人也会查不到)。 * * 用途:花名册(observed-bots-store)只应收 bot,不收真人——真人混进去会污染 * `` 误导模型。调用方语义统一为「只在 NOT-confirmed-human 时登记」: * - 有 contact 读权限(常态)→ 真人被准确剔除,登记得干净; * - 缺权限 / 查询瞬时失败(降级)→ 一律按非真人放行登记。对 /introduce 这本就「全部登记」, * 无回退损失;但对 /grant 自动登记这条**新增**路径,降级时真人会被误登记——这是个新增 * 的(窄)污染面,靠 `contact:user.base:readonly` 已是 critical scope、启动自检缺失即 DM * 管理员来收敛,不是「与现状等价」。若要彻底消除需区分 permission/network 与 user-not-found * 错误码(user-not-found 才判 bot),属后续增强。 */ export declare function isHumanOpenId(larkAppId: string, openId: string): Promise; export declare function sendUserMessage(larkAppId: string, openId: string, content: string, msgType?: string, uuid?: string, requestOptions?: LarkRequestOptions): Promise; export declare function getChatInfo(larkAppId: string, chatId: string): Promise<{ userCount: number; botCount: number; }>; /** * List the open_ids of a chat's (user) members, paginating until exhausted. * Used by the 主动开工 场景① gate to check whether any of the bot's allowedUsers * is a member of a chat the bot was just added to. Open_ids are app-scoped, so * the result is only comparable against the SAME bot's resolvedAllowedUsers. * * Throws on API failure (e.g. missing `im:chat`/member-read scope) so the * caller can decide how to degrade — it does NOT swallow errors, because a * silent empty list would look like "no allowedUser present" and wrongly * suppress auto-start. */ export declare function listChatMemberOpenIds(larkAppId: string, chatId: string): Promise; /** * Resolve a chat's display name (the user-facing group title). Returns `null` * on any failure (chatId is unknown to this bot, network error, bot not in * chat etc.) — callers should fall back to displaying the raw chatId so the * UI degrades gracefully rather than rendering "undefined". For p2p chats the * returned name may be an empty string; treat that as "no display name" and * also fall back. */ export declare function getChatName(larkAppId: string, chatId: string): Promise; /** * 获取入群自动开工所需的群上下文。群模式、群名和群描述来自同一次 * chat.get,失败时保留 unavailable,避免把读取失败误判成字段为空。 */ export declare function getChatContext(larkAppId: string, chatId: string): Promise; export declare function getChatNameAndMode(larkAppId: string, chatId: string): Promise<{ name: string | null; mode: ChatMode; }>; /** Lark chat-mode classification used by botmux to decide session scope: * - 'topic' → 话题群: every top-level message becomes a new thread, so * botmux always uses thread-scope sessions. Two underlying * Lark shapes collapse into this: * * chat_mode='topic' (rare; creation-time classification) * * group_message_type='thread' (the toggle Lark clients * expose as "话题/聊天" — flips on the fly, chat_mode stays * 'group'). This is the common case for user-converted * 话题群. * - 'group' → 普通群: top-level messages stay top-level, so botmux uses * chat-scope by default; user-initiated threads still get * their own thread-scope sessions * - 'p2p' → direct message: equivalent to 普通群 from a routing * perspective (chat-scope by default) */ export type ChatMode = 'group' | 'topic' | 'p2p'; /** Resolve the conversational topology of a chat (话题群 vs 普通群 vs p2p). * * Cached per (appId, chatId) for 5 minutes. Errors fall back to 'group' so a * flaky Lark API doesn't break message routing — chat-scope is the safer * default than incorrectly forcing a thread, since users can always reply * in-thread to escape it. * * Calling this with a chat that's already known to be p2p (from * message.chat_type === 'p2p') is fine but wasteful — prefer skipping the * call in that case. */ /** * Resolve a chat's mode by hitting the API directly. Returns `'unknown'` when * the chat type can't be confirmed (non-zero code or thrown) — it does NOT guess * `'group'`. Use this for privacy-critical gates that must fail closed (private * `/card`). Always queries the API (no cache read), but populates the shared * cache on success so a following {@link getChatMode} hits it. */ export declare function getChatModeStrict(larkAppId: string, chatId: string): Promise; export declare function getCachedChatMode(larkAppId: string, chatId: string): ChatMode | undefined; export declare function getChatMode(larkAppId: string, chatId: string, options?: { forceRefresh?: boolean; }): Promise; /** * Recall (delete) a message. Returns `true` only when Lark confirms success, * `false` on SDK throw or a non-zero response code — so callers that need to * know whether the withdraw actually happened (e.g. grant-card withdraw) can * fall back instead of assuming success. Fire-and-forget callers can ignore it. */ export declare function deleteMessage(larkAppId: string, messageId: string): Promise; /** Error code Feishu returns from `ephemeral/v1/send` when the target chat is a * topic / thread chat. Ephemeral cards only work in plain `group` chats (see * /tmp design notes: empirically code 18053 `chat can not be thread`). */ export declare const LARK_CODE_EPHEMERAL_NOT_GROUP = 18053; /** * Send a "visible-to-one-user" ephemeral card (`ephemeral/v1/send`). The card is * only shown to `openId`, sends no notification, and — unlike normal messages — * **cannot be PATCH-updated** (legacy interface). Multiple recipients require one * call each. Only works in plain `group` chats; topic/thread/p2p chats reject * with {@link LARK_CODE_EPHEMERAL_NOT_GROUP}. Returns the ephemeral message_id. */ export declare function sendEphemeralCard(larkAppId: string, chatId: string, openId: string, cardJson: string): Promise; /** * Delete a previously-sent ephemeral card (`ephemeral/v1/delete`). Ephemeral * cards CANNOT be PATCH-updated (see {@link sendEphemeralCard}), so the picker's * "in-place refresh" (page / search / select) is implemented as delete-then- * resend; this is the delete half. Best-effort: returns false on any failure * (already gone, network) rather than throwing — a stale ephemeral card lingering * is a cosmetic issue, not a correctness one, and the caller has already sent the * replacement by the time cleanup runs. */ export declare function deleteEphemeralCard(larkAppId: string, messageId: string): Promise; export declare function updateMessage(larkAppId: string, messageId: string, cardJson: string): Promise; export declare function getMessageDetail(larkAppId: string, messageId: string, options?: { userCardContent?: boolean; } & LarkRequestOptions): Promise; export declare function getMessageChatId(larkAppId: string, messageId: string, options?: LarkRequestOptions): Promise; /** Resolve the `omt_...` topic id for an `om_...` topic-root message. Topic * routing itself keeps using the root message id; this helper is only for * client AppLinks. */ export declare function getMessageThreadId(larkAppId: string, messageId: string, options?: LarkRequestOptions): Promise; export declare function downloadMessageResource(larkAppId: string, messageId: string, fileKey: string, type: 'image' | 'file', savePath: string): Promise; export declare function uploadImage(larkAppId: string, imagePath: string): Promise; export declare function uploadFile(larkAppId: string, filePath: string, opts?: { duration?: number; }): Promise; /** * Resolve emails to Lark open_ids via batch user lookup. * Accepts mixed input: items starting with "ou_" are kept as-is; everything else * must be a full email address (e.g. "alice@example.com") and is looked up. * Returns an array of open_ids (unresolvable entries are dropped with a warning). */ /** * Resolve a raw allowedUsers list (mix of `ou_*` open_ids and emails) into * open_ids, AND return a `raw entry → resolved open_id` map. The map lets * `/revoke` delete the correct raw entry (email OR open_id) from bots.json so * the revocation survives a restart. open_id entries map to themselves; * resolved emails are keyed by the EXACT raw email string from the config * (matched case-insensitively against the API's returned email) so the map key * always equals what's in `allowedUsers`. Unresolvable emails are dropped. */ /** * Per-raw-entry outcome of an allowedUsers resolve: * - `resolved` — turned into an ou_ this pass (or a literal ou_ kept as-is). * - `transient` — contact API transiently failed (throw / rate limit / 5xx); * a last-known-good cache MAY be reused for this entry. * - `definitive` — id invalid / not visible / not found (DEFINITIVE codes) or * a code-0 batch that simply didn't return this email; the * entry is genuinely gone and MUST NOT be revived from cache. */ export type EntryResolveStatus = 'resolved' | 'transient' | 'definitive'; export declare function resolveAllowedUsersWithMap(larkAppId: string, raw: string[]): Promise<{ resolved: string[]; map: Map; errored?: boolean; entryStatus: Map; }>; /** * Best-effort resolve a user's open_id → canonical union_id (+ display name) * for pairing-login. Requires `contact:user.base:readonly` scope; on failure * (no scope / API error) returns {} so callers degrade to open_id-only identity. */ export declare function resolveUserUnionId(larkAppId: string, openId: string): Promise<{ unionId?: string; name?: string; }>; export declare function resolveAllowedUsers(larkAppId: string, raw: string[]): Promise; export declare function listThreadMessages(larkAppId: string, chatId: string, rootMessageId: string, pageSize?: number): Promise; /** List chat-container messages, most-recent first but returned chronologically * (oldest → newest, capped at `pageSize`). Used by `botmux history` for * chat-scope sessions (普通群整群一会话): no thread to walk, so we walk the * chat itself. We page in Desc order so a long-running chat returns its TAIL, * not its head — that's the context the caller wants. The caller controls * how much history they get via `pageSize`. */ export declare function listChatMessages(larkAppId: string, chatId: string, pageSize?: number): Promise; export interface ChatMessageScanOptions { /** Lark page size per request. Clamped to the API max of 50. */ pageSize?: number; /** * Called while scanning newest -> oldest. Returning true stops after the * current message has been included in the returned chronological list. */ stopAfter?: (message: any, seenCount: number) => boolean; } /** Scan chat-container messages newest -> oldest until the caller's stop * condition is met, then return the scanned window chronologically. */ export declare function listChatMessagesUntil(larkAppId: string, chatId: string, options?: ChatMessageScanOptions): Promise; export interface AmbientChatMessageOptions { /** * Exclude messages at/after this timestamp (Lark create_time, milliseconds as * a string). Used by `/t` thread sessions to fetch the chat tail that existed * before the thread was opened, avoiding bot cards/replies from the new * thread polluting the context. */ beforeCreateTime?: string; /** Exclude the current thread root and its replies from the chat tail. */ excludeRootMessageId?: string; /** How many chat-container messages to scan before filtering. */ scanLimit?: number; } export declare function filterAmbientChatMessages(messages: any[], pageSize: number, options?: Pick): any[]; /** * List recent chat-container messages as ambient context for a thread session. * * This intentionally differs from `listChatMessages`: callers want the newest * `pageSize` messages AFTER filtering out the current thread and (optionally) * messages created after the thread root. We therefore may scan more than * `pageSize` items and cap only after filtering. */ export declare function listAmbientChatMessages(larkAppId: string, chatId: string, pageSize?: number, options?: AmbientChatMessageOptions): Promise; /** * Check which bots are in a chat. * * Two-source merge: * 1. **configured** — bots in `bots.json` (this daemon and sibling daemons on * the same host). Probed via `isInChat` per bot; only those actually in * the chat are returned. open_id is corrected via the per-app cross-ref. * 2. **introduce** — bots discovered passively from the `/introduce` * collaboration handshake, persisted per observer × chat in * `observed-bots--.json`. Critical for external bots * run by other botmux daemons (or even non-botmux bots) that aren't in * our bots.json but the user wants this daemon to know about. Read with * the caller's `larkAppId` so open_ids match this app's perspective. * * Configured wins on open_id collision (`source: 'configured'`); observed * entries fill in everyone else (`source: 'introduce'`). Observed entries * carry `larkAppId=""` since they don't map to any local-daemon-managed bot. */ export type ChatBotMember = { larkAppId: string; openId: string; name: string; displayName: string; source: 'configured' | 'introduce'; /** Short capability label (team-level), for roster discovery. Configured bots only. */ capability?: string; /** Whether this bot has a team-level role registered. Configured bots only. */ hasTeamRole: boolean; /** * Whether the observing app (the `larkAppId` arg) can RELIABLY @-mention this * member. Lark open_id is per-app scoped, so a bot's self-reported open_id is * not usable by another app. Reliable only when learned via cross-ref (from * @mention events) or via /introduce (observed, already observer-scoped). */ mentionable: boolean; mentionSource: 'cross-ref' | 'self' | 'observed' | 'fallback'; }; /** * A bot row returned directly by Feishu's live `/members/bots` endpoint. * Unlike {@link ChatBotMember}, this type deliberately carries no botmux-local * identity/provenance: `openId` is exactly the observer-scoped handle returned * to `larkAppId` for the current chat. */ export type CurrentChatBotMember = { openId: string; displayName: string; }; /** * A stable configured app identity bound to the receiver-scoped open_id that * Feishu returned for that bot in the current chat. */ export type CurrentChatBotAppMapping = { larkAppId: string; subjectOpenId: string; }; export type CurrentChatBotAppResolution = { ok: true; mappings: CurrentChatBotAppMapping[]; } | { ok: false; error: 'live_membership_unavailable' | 'subject_lark_app_not_configured' | 'subject_lark_app_name_unavailable' | 'subject_lark_app_not_in_chat' | 'subject_lark_app_ambiguous'; message: string; invalidSubjectLarkAppIds?: string[]; }; /** * Read the current chat's bot members from Feishu and fail closed on any API * error. This is the authorization-grade counterpart to * {@link listChatBotMembers}: it NEVER consults the 30-day observed/cross-ref * fallback and NEVER treats a cached capability failure as membership truth. * * Keep this separate from the user-facing discovery helper. `/members/bots` * is still an undocumented endpoint, so discovery may degrade gracefully; a * permission mutation must not. */ export declare function listCurrentChatBotMembers(larkAppId: string, chatId: string): Promise; /** * Resolve stable configured Lark app ids to the receiver-scoped open_ids that * may be written to the receiver's exact chatGrant. * * This is deliberately stricter than bot discovery. Identity is accepted only * when all three current signals agree: the receiver's live `/members/bots` * row, the subject app's own `is_in_chat` result, and one exact, unique * `bot_name` binding from bots-info.json. Cross-reference and observed-bot * stores are never consulted, because either can be stale or scoped to another * app. */ export declare function resolveCurrentChatBotOpenIdsByLarkAppIds(receiverLarkAppId: string, chatId: string, subjectLarkAppIds: string[]): Promise; /** * A resolved same-deployment sibling identity: the receiver-scoped open_id * `senderOpenId`, proven to belong to a locally-configured bot whose stable * `larkAppId` and unique `botName` are returned so the caller can persist the * receiver's cross-ref (botName → receiver-scoped open_id). */ export type SiblingBotResolution = { ok: true; larkAppId: string; botName: string; senderOpenId: string; } | { ok: false; reason: string; }; /** * Resolve a foreign-bot SENDER open_id (receiver-scoped) to a same-deployment * sibling, using only live authorization-grade signals — never the possibly * stale/uninitialized cross-ref or observed stores. This closes the cold-start * window where a same-machine sibling @s a receiver whose cross-ref has not yet * learned that sibling's receiver-scoped open_id (Lark open_id is per-app). * * Identity is accepted only when all signals agree, mirroring * {@link resolveCurrentChatBotOpenIdsByLarkAppIds}: * 1. the receiver's live `/members/bots` row carries `bot_id === senderOpenId`; * 2. exactly one locally-configured bot (other than the receiver) has that * exact `bot_name` in bots-info.json — a unique name binding; * 3. that candidate app independently confirms `is_in_chat` and binds to * exactly one live row for its name (the strict resolver's own re-check). * * Fails closed (returns `{ ok: false }`) on any API error, ambiguity, or name * collision, so the caller falls back to the `/grant` request card. Never * authorizes a genuine external bot: an external sender's open_id has no * locally-configured app of the same unique name, so step 2 fails. */ export declare function resolveSiblingBotBySenderOpenId(receiverLarkAppId: string, chatId: string, senderOpenId: string | undefined): Promise; export declare function listChatBotMembers(larkAppId: string, chatId: string): Promise; //# sourceMappingURL=client.d.ts.map