/**
 * Lark event dispatcher — handles WSClient setup, bot identity probing,
 * and message routing (group access checks, @mention detection).
 * Extracted from daemon.ts for modularity.
 */
import * as Lark from '@larksuiteoapi/node-sdk';
import { type DocSubscription } from '../../services/doc-subs-store.js';
import { type Brand } from './lark-hosts.js';
/** Set the bot's open_id. Callers should also call writeBotInfoFile() to persist. */
export declare function setBotOpenId(larkAppId: string, id: string): void;
/** Persist bot registry info to disk for agent-facing CLI subcommands to read.
 *  Merges current process's bot(s) into the existing file so that
 *  multiple daemon processes (one per bot) don't overwrite each other. */
export declare function writeBotInfoFile(dataDir: string): void;
/**
 * Ensure the bot's own open_id is resolved before @-detection. `probeBotOpenId`
 * is fired fire-and-forget at daemon startup, so events can arrive while
 * `botOpenId` is still undefined — `isBotMentioned` then can't recognize an @ as
 * ours and silently drops it. The WSClient still ACKs that dropped event, so
 * Lark never redelivers it (the @ is lost until manually re-sent). Awaiting the
 * deduped probe here closes that window: concurrent events share one probe, and
 * each is held only until the open_id lands, then processed normally.
 */
export declare function ensureBotOpenId(larkAppId: string): Promise<void>;
export declare function probeBotOpenId(larkAppId: string): Promise<void>;
export declare function checkRequiredScopes(larkAppId: string): Promise<void>;
export declare const CHAT_CACHE_TTL: number;
/** Test-only: clear callback dedupe claims between cases. */
export declare function __resetEventClaimsForTest(): void;
export declare function getGroupStats(larkAppId: string, chatId: string): Promise<{
    userCount: number;
    botCount: number;
}>;
/** Read the per-bot cross-reference: botName(lowercase) → openId as seen by larkAppId's app */
export declare function readBotOpenIdCrossRef(dataDir: string, larkAppId: string): Map<string, string>;
/** Is `senderOpenId` a registered botmux peer (from larkAppId's cross-ref)?
 *  Used to gate chat-scope foreign-bot @mention spawning to vetted peers. */
export declare function isKnownPeerBot(dataDir: string, larkAppId: string, senderOpenId: string | undefined): boolean;
/** Update the per-bot cross-reference from @mention data in an event.
 *  mentionsList comes from Lark event message.mentions array. */
export declare function updateBotOpenIdCrossRef(dataDir: string, larkAppId: string, mentionsList: Array<{
    name?: string;
    id?: {
        open_id?: string;
    };
}>): void;
/**
 * If `message` is a /introduce command, side-effect it (record observed bots
 * + send ack) and return `true` so the caller skips normal CLI routing.
 *
 * /introduce 不需要任何授权：它只是把群里别的 bot 记进花名册（observed），不授予
 * 任何对话/操作权，所以群里任何人都能用（与 /grant 的 owner 强闸门相反）。
 *
 * Consumed without side effects (still returns true) when:
 * - mentions[] minus self is empty — nothing to learn, ignore quietly
 *
 * Writes ALL mentions including self to the store. Each receiving bot's
 * daemon owns its own per-observer file (see observed-bots-store), so the
 * self entry is the only authoritative record of this app's own open_id
 * in the per-observer view — useful for `botmux bots list` self-identification.
 */
export declare function tryHandleIntroduceCommand(larkAppId: string, message: any, senderOpenId: string | undefined): Promise<boolean>;
/** Check if the bot was @mentioned in this message */
export declare function isBotMentioned(larkAppId: string, message: any, _senderOpenId: string | undefined): boolean;
export type TalkReason = 'allowedUser' | 'oncall' | 'peer' | 'allowedChatGroup' | 'open' | 'chatGrant' | 'globalGrant' | 'none';
export interface TalkEvaluation {
    allowed: boolean;
    reason: TalkReason;
    quotaKey?: string;
}
export type GrantCommandRestrictionReason = 'chatGrant' | 'globalGrant';
export declare function grantCommandRestriction(larkAppId: string, chatId: string | undefined, senderOpenId: string | undefined): {
    blocked: boolean;
    reason?: GrantCommandRestrictionReason;
};
export declare function canTalk(larkAppId: string, chatId: string | undefined, senderOpenId: string | undefined): boolean;
export declare function evaluateTalk(larkAppId: string, chatId: string | undefined, senderOpenId: string | undefined): TalkEvaluation;
export declare function canOperate(larkAppId: string, _chatId: string | undefined, senderOpenId: string | undefined): boolean;
/**
 * Check group message addressing:
 * - 'allowed'     -> sender is allowed, bot was @mentioned or solo group
 * - 'not_allowed' -> bot was @mentioned but sender is not in allowlist
 * - 'ignore'      -> not addressed to bot at all
 */
export declare function checkGroupMessageAccess(larkAppId: string, message: any, chatId: string, senderOpenId: string | undefined): Promise<'allowed' | 'not_allowed' | 'ignore'>;
/** Routing context computed from the incoming message — describes the
 *  conversational unit (`scope`) and the addressing key (`anchor`) used
 *  throughout the rest of the system. The dispatcher computes this once
 *  per message and hands it to the daemon's session handlers, so the
 *  daemon never has to re-derive it. */
export interface RoutingContext {
    chatId: string;
    /** message_id of the inbound message that triggered this routing. */
    messageId: string;
    chatType: 'group' | 'p2p';
    /** 'thread' → reply_in_thread to a (real or freshly seeded) thread root.
     *  'chat'   → plain message to the chat (no threading). */
    scope: 'thread' | 'chat';
    /** Routing key. `chatId` for chat-scope, the thread root id for
     *  thread-scope (an existing rootMessageId, or this messageId when
     *  it's the seed of a brand-new thread). */
    anchor: string;
    /** Chat-scope shared-topic reply target for this turn, if any. */
    replyRootId?: string;
    larkAppId: string;
}
export interface EventHandlers {
    handleCardAction: (data: any, larkAppId: string) => Promise<any>;
    handleNewTopic: (data: any, ctx: RoutingContext) => Promise<void>;
    handleThreadReply: (data: any, ctx: RoutingContext) => Promise<void>;
    /** 主动开工 — 场景①: fired when this bot is added to a chat
     *  (`im.chat.member.bot.added_v1`). The daemon decides whether to auto-start
     *  based on the bot's `autoStartOnGroupJoin` toggle + allowedUser membership.
     *  Best-effort fire-and-forget. `operatorOpenId` is who added the bot. */
    handleBotAdded?: (chatId: string, operatorOpenId: string | undefined, larkAppId: string) => Promise<void>;
    /** Check if this bot owns an active session anchored at the given id
     *  (rootMessageId for thread-scope, chatId for chat-scope). */
    isSessionOwner?: (anchor: string, larkAppId: string) => boolean;
    /** Resolve a persisted topic reply alias back to its owning chat-scope session. */
    resolveReplyThreadAlias?: (rootId: string, chatId: string, larkAppId: string) => {
        chatId: string;
        sessionId: string;
    } | null;
    /** Fired when the dispatcher detects that a chat with a live chat-scope
     *  session has been converted to topic mode (chat_mode 'group' → 'topic'
     *  via Lark group settings). Daemon should evict the stale chat-scope
     *  session from its activeSessions map so future routing doesn't hit it
     *  and so scheduler/dashboard sends stop going through sendMessage(chatId)
     *  — which in a 话题群 wraps each top-level message in a fresh topic.
     *  Best-effort fire-and-forget; the dispatcher proceeds either way. */
    onChatModeConverted?: (chatId: string, larkAppId: string) => void;
    /** 文档评论入口（/subscribe-lark-doc）：一条命中订阅的文档评论被喂进其绑定
     *  会话。daemon 负责定位会话、投递给 worker、记录该轮回评论的落点。 */
    handleDocComment?: (ctx: DocCommentContext) => Promise<void>;
}
/** 一条已通过订阅 + 触发范围 + 自触发过滤的文档评论，交给 daemon 投递。 */
export interface DocCommentContext {
    larkAppId: string;
    /** 命中的文档订阅（含 sessionAnchor / scope / chatId）。 */
    sub: DocSubscription;
    /** 触发评论的 comment_id。 */
    commentId: string;
    /** 触发的具体回复 id（作 turnId，回评论落点也按它走）；缺省退回 commentId。 */
    replyId?: string;
    /** 评论纯文本（喂给模型的用户消息）。 */
    text: string;
    /** 评论发表者 open_id。 */
    authorOpenId?: string;
}
/**
 * Best-effort plain-text extraction from a Lark message for routing-level
 * decisions (currently: `/t` / `/topic` detection). Handles the two common
 * shapes — `text` (`{"text": "..."}`) and `post` (zh_cn/en_us nested
 * paragraphs of `text` / `at` nodes). Other types (image, file, sticker,
 * interactive, …) return null so the caller falls through to the default
 * routing path.
 *
 * Kept deliberately tiny rather than reusing parseEventMessage: the dispatcher
 * runs on every inbound event and we only need a quick text peek before the
 * permission gates / scope override; full parseEventMessage still runs once
 * inside the chosen handler.
 */
export declare function extractMessageTextForRouting(message: any): string | null;
/**
 * If the inbound message starts with `/t` / `/topic` AND the routing
 * currently lands on chat-scope, override to thread-scope anchored at
 * the inbound message_id. This makes "force topic mode" work even when
 * the bot already owns a chat-scope session in the chat — the dispatcher
 * routes to handleNewTopic at a fresh anchor instead of falling into
 * handleThreadReply on the chat-scope owner.
 *
 * Already-thread messages (real Lark 话题, p2p, 话题群) are left alone:
 * the prefix is still stripped downstream by handleNewTopic.
 */
export declare function maybeApplyForceTopicOverride(routing: {
    scope: 'thread' | 'chat';
    anchor: string;
}, message: any, messageId: string): boolean;
export declare function decideRouting(larkAppId: string, message: any): Promise<{
    scope: 'thread' | 'chat';
    anchor: string;
}>;
/**
 * Create and start the Lark WSClient with event dispatching.
 * Returns the WSClient instance for lifecycle management.
 */
export declare function startLarkEventDispatcher(larkAppId: string, larkAppSecret: string, handlers: EventHandlers, brand?: Brand): Lark.WSClient;
//# sourceMappingURL=event-dispatcher.d.ts.map