import * as Lark from '@larksuiteoapi/node-sdk';
import type { BackendType } from './adapters/backend/types.js';
import type { CliId } from './adapters/cli/types.js';
import { type Locale } from './i18n/index.js';
import type { VoiceConfig } from './services/voice/types.js';
import { type Brand } from './im/lark/lark-hosts.js';
export type ChatReplyMode = 'chat' | 'new-topic' | 'shared';
export interface OncallChat {
    /** Lark chat_id (oc_xxx) the bot was pulled into. */
    chatId: string;
    /** Default working directory used for every new topic spawned in this chat. */
    workingDir: string;
}
/**
 * Per-bot default for new group chats:
 *   - `enabled`     — when true, group chats first observed after `since` are
 *                     auto-bound to oncall on their first new-topic.
 *   - `workingDir`  — the working directory used for the auto-bind. Required
 *                     when enabled (oncall semantics: chatId ↔ workingDir).
 *   - `since`       — epoch ms when the flag was switched on. Used to gate
 *                     "new vs old" against chat-first-seen-store. Chats that
 *                     existed before `since` are left untouched, matching
 *                     "新群聊生效，老群聊不变".
 */
export interface BotDefaultOncall {
    enabled: boolean;
    workingDir: string;
    since: number;
}
export interface BotConfig {
    larkAppId: string;
    larkAppSecret: string;
    /**
     * 租户品牌：`'feishu'`（中国版，open.feishu.cn）或 `'lark'`（国际版，
     * open.larksuite.com）。缺省 / 旧 bots.json 无此字段 → 视为 `'feishu'`
     * （见 {@link normalizeBrand}），向后兼容。决定 SDK Client / WSClient 的
     * domain、所有裸 fetch 的 host、OAuth / applink 深链等——全部从这一个字段
     * 派生（见 im/lark/lark-hosts.ts）。setup 时自动识别后落盘；brand 绑定到
     * 具体 app/租户，不在运行时切换（要换平台 = 重新配/加一个 bot）。
     */
    brand?: Brand;
    /** Optional process-name suffix; the daemon's process name is rendered as `botmux-<name>` (defaults to `botmux-<index>`). */
    name?: string;
    cliId: CliId;
    cliPathOverride?: string;
    /**
     * 通用启动前缀（按空格拆 token）：worker spawn 时把启动命令拼成
     * `<wrapperCli> <CLI 参数>`（首 token 当 bin 走 PATH 解析），无需 wrapper 脚本、跨系统。
     * 典型值 `"aiden x claude"` / `"aiden x codex"`（内网网关 aiden-aiproxy + SSO），也能
     * 承载 ccr / claude-w 等任意启动器。`cliId` 仍是底层适配器（claude→claude-code、
     * codex→codex），所有适配器机制（hook / bridge / resume）照常工作；设了 wrapperCli 后
     * 它的首 token 取代 cliId 的默认 bin（cliPathOverride 不再生效）。检测到前缀是
     * `aiden x claude` 时自动剥掉 aiden 拒收的 --settings。见 src/setup/cli-selection.ts。
     */
    wrapperCli?: string;
    /**
     * Optional model name passed to the CLI at spawn time (e.g. `claude --model
     * opus`). Each adapter decides how to inject it — adapters whose CLI has no
     * `--model` flag silently ignore the field. When unset, the CLI uses its own
     * default model. Multiple bots sharing the same `cliId` can therefore run
     * different models without resorting to wrapper scripts. See each adapter's
     * `modelChoices` for the curated candidates surfaced in `botmux setup`.
     */
    model?: string;
    /**
     * If true, botmux does not add CLI-default approval/sandbox bypass flags
     * such as --yolo or --dangerously-*. Missing/false preserves legacy behavior.
     */
    disableCliBypass?: boolean;
    /**
     * Run this bot's CLI inside a per-session file sandbox (bubblewrap, Linux):
     * the agent sees only a clone of the project + a de-identified config dir,
     * never the host home/secrets/other sessions. Intended for oncall bots shared
     * with semi-trusted users. Linux-only; ignored elsewhere. Env BOTMUX_SANDBOX=1
     * forces it on regardless (testing).
     */
    sandbox?: boolean;
    /**
     * Per-bot privacy masks for the sandbox: absolute paths blanked inside the
     * overlay sandbox (dirs → empty tmpfs; files → empty placeholder). OPT-IN with
     * NO defaults — the agent reads the entire real fs natively unless a path is
     * listed here. Only meaningful when `sandbox` is true. Linux-only.
     */
    sandboxHidePaths?: string[];
    backendType?: BackendType;
    workingDir?: string;
    workingDirs?: string[];
    allowedUsers?: string[];
    allowedChatGroups?: string[];
    /** Oncall bindings: chat_id → default workingDir. Any group member can talk; allowedUsers still gates card buttons / daemon commands. */
    oncallChats?: OncallChat[];
    /** UI language for this bot: 'zh' or 'en'. Falls back to BOTMUX_LANG / LANG env when unset. */
    lang?: Locale;
    /**
     * Per-bot default working directory. When set, new topics that have no
     * oncall binding and no sibling-session inheritance skip the repo-select
     * card and spawn the CLI directly in this directory. `/cd <path>` still
     * works to switch mid-session; the next new topic falls back to this default.
     *
     * Pure runtime fallback — does NOT write any state to bots.json and does
     * NOT change the canTalk / canOperate permission model (unlike defaultOncall).
     */
    defaultWorkingDir?: string;
    /** Per-bot default: auto-bind every new group chat to oncall on first new-topic. */
    defaultOncall?: BotDefaultOncall;
    /**
     * Chat IDs that have ever been auto-bound by `defaultOncall`. Append-only.
     * Once a chat appears here, the default is permanently "spent" for it — even
     * if the user later unbinds via Groups & Bots / `/oncall unbind`, the
     * default will not re-bind it. This preserves the manual-override semantics
     * Codex flagged in review.
     */
    defaultOncallAutoboundChats?: string[];
    /** Per-chat reply mode: chat_id → 普通群 @bot 后回复形态。缺省为 chat（保持现状）。 */
    chatReplyModes?: {
        [chatId: string]: ChatReplyMode;
    };
    /** Per-chat per-user grants: chat_id → 被授权的 open_id 列表。仅放行 canTalk，不给管理命令权。 */
    chatGrants?: {
        [chatId: string]: string[];
    };
    /**
     * 全局对话授权名单：被授权在**任意群**与本 bot 对话的 open_id 列表（人或 bot 通用）。
     * 与 chatGrants 同属 talk-only —— 仅放行 canTalk / bot 路由闸，**canOperate 绝不读它**
     * （敏感操作仍仅限 allowedUsers）。这是 chatGrants 的全局版：作用域升到全局，talk-only
     * 性质不变。可由 /grant 卡片「全局」按钮写入，也可在 bots.json 手配 open_id。
     */
    globalGrants?: string[];
    /**
     * 消息额度机制（默认关闭）。`defaultLimit` 的"是否配置"本身就是开关：
     *   • 未配置（undefined）→ 关闭：无显式数字的 /grant 仍是"无限授权"（当前行为）。
     *   • 配置正整数 D    → 开启默认额度：`/grant @x`（不带数字）套用 D 条额度。
     * 显式 `/grant @x N` 的 N **恒生效**，与本字段是否配置无关（见 {@link quotaState}）。
     * 仅约束 chatGrants / globalGrants 这类 per-user talk 授权，绝不影响 canOperate。
     */
    messageQuota?: {
        defaultLimit?: number;
    };
    /**
     * scope-aware 消息额度计数（运行时状态，随授权一起持久化进 bots.json）。
     * key = `chat:${chatId}:${openId}` | `global:${openId}`，value = { limit, used }。
     * 仅在 /grant 带额度（显式数字，或开启 default 时取 default）时建记录；
     * used 达到 limit 后自动收回**对应 scope** 的授权并删除本记录。纯 talk-only。
     */
    quotaState?: {
        [quotaKey: string]: {
            limit: number;
            used: number;
        };
    };
    /**
     * 开启后：仅靠 per-user 授权（chatGrants / globalGrants）放行的发送者，禁止使用**任何
     * 斜杠命令**——botmux 自身的 DAEMON 命令、透传（PASSTHROUGH）命令、全部 `/workflow`
     * 子命令、`/introduce`、`/t`/`/topic` —— 只能普通对话。owner / allowedUsers / oncall /
     * allowedChatGroup 整群成员不受影响。判定以 slash-command invocation 命中为准（不是"凡以
     * `/` 开头的文本"，避免误伤讨论命令用法的普通对话）。默认 false（保持现状：被授权人可用透传）。
     */
    restrictGrantCommands?: boolean;
    /**
     * 用户自定义、额外放行透传给 CLI 的 slash 命令 —— 在固定的 PASSTHROUGH_COMMANDS
     * 之上扩展（例如把 CLI 支持但默认不放行的 `/goal`、`/export` 加进来）。每项必须
     * `/` 开头、小写、仅含 [a-z0-9:_-]；解析时归一化（缺失的 `/` 自动补、转小写、去重、
     * 丢弃非法项与会遮蔽 botmux daemon 命令的项）。与内置白名单合并后由
     * {@link resolvePassthroughCommands} 生效；`/list-slash-command` 可查看完整放行清单。
     * 未配置（undefined）→ 仅用内置白名单（保持现状）。
     */
    customPassthroughCommands?: string[];
    /**
     * Custom footer brand label for cards this bot sends. Three states:
     *   • `undefined` (unset)  → default `[botmux](github)` link
     *   • `''` (empty)         → brand suppressed (footer shows only 发送给 if any)
     *   • any other string     → rendered verbatim (markdown allowed)
     * Resolved via {@link resolveBrandLabel}. Pure cosmetic — does not affect
     * routing or permissions.
     */
    brandLabel?: string;
    /**
     * When true, suppress the live streaming session card entirely. The web
     * terminal still runs and the final answer still arrives via `botmux send`;
     * only the auto-updating status card is never posted/patched. Default
     * (undefined) keeps the streaming card. For users who find the live card noisy.
     */
    disableStreamingCard?: boolean;
    /**
     * Conversation mode for 1:1 private chats (DMs) with the bot:
     *   - 'thread' (default, stored as undefined): every top-level DM message
     *     starts a fresh thread-scoped session — the official/legacy behavior,
     *     keeps 1:1 chatter out of one long-running CLI process.
     *   - 'chat': route DMs as one flat, continuous chat-scoped session (all
     *     messages share the same context, similar to Hermes/OpenClaw).
     * Editable at runtime via `/botconfig p2pMode chat|thread` (owner/admin).
     */
    p2pMode?: 'thread' | 'chat';
    /** chat_id list: chats where the live streaming card is suppressed (status falls back to master's pending-card morph). Written by `/card off|on`. */
    noCardChats?: string[];
    /**
     * When true, the streaming card embeds a directly-usable WRITABLE terminal
     * link in its body (token included → anyone who can see the card can drive
     * the terminal). Default (undefined) keeps the write link behind the
     * "get write link" button, which DMs it privately to the clicker. Moot when
     * {@link disableStreamingCard} is on (no card to embed it in).
     */
    writableTerminalLinkInCard?: boolean;
    /**
     * When true, `/card` sends a **private** static snapshot card via the ephemeral
     * API, visible only to the bot's `allowedUsers` (owner / co-owners), instead of
     * the group-visible live streaming card. Talk-only grants (globalGrants /
     * chatGrants) and a bare triggerer do NOT receive it — it's owner-only. Only
     * works in plain `group` chats (topic/thread/p2p fail closed) and cannot
     * live-update (ephemeral cards can't be patched). Scoped to the `/card` command
     * only — the auto streaming card is unaffected. Default (undefined) keeps
     * `/card` group-visible & live.
     */
    privateCard?: boolean;
    /**
     * 主动开工 — 场景①. When true, the bot auto-starts a session when it is added
     * to a new chat that contains at least one of its allowedUsers (see
     * docs/specs/20260529-proactive-auto-start/). Default (undefined) = passive
     * (only spawns on @mention). Requires the `im.chat.member.bot.added_v1` event
     * to be subscribed for the app in the Feishu console.
     */
    autoStartOnGroupJoin?: boolean;
    /**
     * 主动开工 — 场景① optional pre-configured first-turn prompt. When set, it
     * becomes the user_message of the auto-started session; when unset/blank the
     * session starts with an empty user_message and the bot reads the group
     * context itself. Moot when {@link autoStartOnGroupJoin} is off.
     */
    autoStartOnGroupJoinPrompt?: string;
    /**
     * 主动开工 — 场景②. When true, in a 话题群 (topic mode) every new topic's first
     * message auto-starts a session even without an @mention (the default role +
     * the user's first message form the prompt). No effect in regular groups.
     * Default (undefined) = passive.
     */
    autoStartOnNewTopic?: boolean;
    /**
     * Per-bot DEFAULT session mode for regular Lark groups (overridable per-chat
     * via `/reply-mode` → `chatReplyModes`). Resolved by
     * `chat-reply-mode-store.regularGroupDefaultMode`.
     *   • 'chat' (or undefined) — whole group shares one flat chat-scope session
     *   • 'new-topic'           — each top-level @mention forks its own thread-scope session
     *   • 'shared'              — replies fold into a topic but reuse the one chat-scope session
     */
    regularGroupReplyMode?: ChatReplyMode;
    /**
     * Per-bot (bot-global) policy for when an @mention is required to get a reply
     * in regular Lark groups — a 3-tier ladder:
     *   • 'always' (or undefined) — @ required everywhere, including inside the
     *                               bot's own shared topics (the safe default).
     *   • 'topic'                 — @ required to start / at top level, but NOT
     *                               inside the bot's shared topics (non-@ replies
     *                               there continue the session).
     *   • 'never'                 — @ never required: non-@ messages in groups
     *                               where the bot has talk access are answered too.
     * Governs the shared-topic fold-back + the top-level @ gate. `new-topic` /
     * 话题群 topics own their own thread and continue without @ regardless (that
     * is the mode's defining behavior, not affected by this policy).
     */
    regularGroupMentionMode?: 'always' | 'topic' | 'never';
    /**
     * 飞书文档订阅入口（/subscribe-lark-doc）新订阅的默认评论触发范围：
     *   • 'mention-only'（或 undefined）— 仅评论里 @bot 才触发（默认，防噪声）
     *   • 'all'                        — 该文档所有新评论都触发
     * 单条订阅的触发范围之后可在 dashboard 逐文档改（doc-subscriptions 表）。
     */
    docSubscribeDefaultMode?: 'mention-only' | 'all';
    /**
     * Per-bot voice-engine override for the voice-summary feature. Merged OVER
     * the global `voice` block in ~/.botmux/config.json (per-bot wins field by
     * field). When this bot has usable voice creds (here or globally), its reply
     * cards render the "🔊 语音总结" button. See services/voice/types.ts.
     */
    voice?: VoiceConfig;
}
export interface BotState {
    config: BotConfig;
    client: Lark.Client;
    botOpenId?: string;
    botName?: string;
    botAvatarUrl?: string;
    resolvedAllowedUsers: string[];
    /** raw allowedUsers 条目 → 解析后的 open_id。供 /revoke 反查并删除 email 形式的 raw 条目。 */
    rawAllowedUserResolution: Map<string, string>;
}
export declare function getLoadedConfigPath(): string | undefined;
export declare function registerBot(cfg: BotConfig): BotState;
export declare function getBot(larkAppId: string): BotState;
export declare function getBotClient(larkAppId: string): Lark.Client;
/** Owner = bot 首个已授权 open_id，与「缺权限警告私信对象」同口径（见 admin 解析）。 */
export declare function getOwnerOpenId(larkAppId: string): string | undefined;
/** Bot 自身的 open_id（用于在 mention 解析时排除自己）。 */
export declare function getBotOpenId(larkAppId: string): string | undefined;
/**
 * 安全地按 appId 取 brand。未注册（如跨进程 dashboard 聚合到别的 daemon 的
 * 会话）→ 归一为 'feishu'。仅用于派生 applink 等 host，缺省 feishu 安全。
 */
export declare function getBotBrand(larkAppId: string | undefined): Brand;
export declare function getAllBots(): BotState[];
/** Lookup the oncall binding for a given bot+chat, if any. */
export declare function findOncallChat(larkAppId: string, chatId: string): OncallChat | undefined;
export declare function findOncallChatForAnyBot(chatId: string): OncallChat | undefined;
export declare function isChatOncallBoundForAnyBot(chatId: string): boolean;
/**
 * The configured brand label for a bot, or `undefined` when unset (`''` = off
 * is preserved). Prefers the in-memory registry (daemon hot path); falls back
 * to a mtime-cached read of bots.json so the CLI process — which never loads
 * the registry — still resolves the sending bot's brand. Callers feed the
 * result into {@link brandFooterSegment} for the unset→default / ''→off rule.
 */
export declare function resolveBrandLabel(larkAppId: string): string | undefined;
/**
 * Load bot configurations from one of (in priority order):
 * 1. BOTS_CONFIG env var — path to a JSON file
 * 2. ~/.botmux/bots.json — default config path
 */
export declare function loadBotConfigs(): BotConfig[];
/** Pure parser: bots.json text → BotConfig[]. Exported for testing & reuse. */
export declare function parseBotConfigsFromText(jsonText: string): BotConfig[];
//# sourceMappingURL=bot-registry.d.ts.map