/** * Settings dashboard card. * * Consumes the settings DTO and emits a Feishu interactive card. The handler * chain is: * * 1. invoker lock: `action.value.invoker_open_id === operator.open_id`. * 2. per-bot admin gate: `operator.open_id` MUST be one of this bot's * resolved `allowedUsers`, matching `/botconfig`. Each callback is * scoped to the bot that received it; an admin of bot A cannot use * bot B's `/dashboard *`. * 3. noop short-circuit: `dash_settings_noop` (current-value button in the * segmented control) returns a toast WITHOUT calling the Route B client. * Fail-safe for clients that don't suppress `disabled` callbacks. * 4. Sync handler: * - await the Route B PUT/GET (resolves the admin's union_id via * `resolveUserUnionId` first, since the server-side write API * still requires `ownerUnionId` in the body), * - return ONLY `{ card }` (no toast) on the success path so the * event-dispatcher passes the rebuilt card body back to Lark in * the SAME callback response. Toast + card together makes the * Lark client render the toast and the card replacement in two * separate passes, flashing the OLD card state during the gap; * card-only avoids that. Errors/permission denials/noop still * return a plain toast (they have no card to render). * * Write actions are never retried; toggling a setting twice is a real-world * effect. * * Sender identity (`unionId`) NEVER lands on `action.value`. The only field * the callback echoes from the original render is `invoker_open_id`, which * is the invoking admin's open_id (not the sender's union_id). */ import { type SettingsCardDTO } from '../../dashboard/settings-card-model.js'; import type { DaemonClient } from '../../dashboard/daemon-internal-client.js'; import { type Locale } from '../../i18n/index.js'; import type { CardActionData } from './card-handler.js'; export declare const SETTINGS_ACTION_TOGGLE: "dash_settings_toggle"; export declare const SETTINGS_ACTION_SET_TIME: "dash_settings_set_time"; export declare const SETTINGS_ACTION_REFRESH: "dash_settings_refresh"; /** Current-value segmented-control buttons send noop as a fail-safe. */ export declare const SETTINGS_ACTION_NOOP: "dash_settings_noop"; /** Builder opts intentionally exclude sender union identity. */ export interface BuildSettingsCardOpts { invokerOpenId: string; locale: Locale; canWrite: boolean; /** Overview drilldown nav state. `'overview'` β†’ footer renders * "πŸ”™ θΏ”ε›žζ€»θ§ˆ" AND every action.value carries `origin=overview` so * toggle/set_time/refresh rebuilds keep the return affordance. * Settings is single-layer (no pages) β†’ no `pageSize`. */ origin?: 'overview'; } /** Build a Feishu interactive card JSON string from the settings DTO. */ export declare function buildSettingsCard(dto: SettingsCardDTO, opts: BuildSettingsCardOpts): string; /** ─── Handler ─────────────────────────────────────────────────────────── */ export interface SettingsCardHandlerDeps { /** Legacy owner test seam; prefer `getDashboardAdminOpenIds` for new tests. */ getOwnerOpenId?: (larkAppId: string) => string | undefined; getDashboardAdminOpenIds?: (larkAppId: string) => ReadonlyArray | undefined; /** Override the union_id resolver. Production omits; tests skip Lark contact API. */ resolveUserUnionId?: (larkAppId: string, openId: string) => Promise<{ unionId?: string; }>; /** Factory returning a Route B client for the given larkAppId. */ createClient: (larkAppId: string) => DaemonClient; /** Override locale resolution; production uses the caller-supplied locale. */ locale?: Locale; } /** * Lark card-callback result envelope. event-dispatcher pass-through expects * either `{ toast }`, `{ card }`, or both β€” see `event-dispatcher.ts:390-395`. * * The handler awaits GET/PUT inline. On the success path it returns ONLY * `{ card }` (no toast). Why card-only: * - Lark's client renders toast and card replacement as two separate passes; * users briefly see the old card state between them. * - Card-only collapses that to a single pass β€” the card body itself * (`βœ“ 已开启` / `βœ“ ε·²ε…³ι—­`) is the feedback signal; users learn the * write succeeded from the new state, not from a toast. * Error / permission denial / noop still return `{ toast }` (no card to * render anyway). Round-trip Route B PUT + card rebuild fits in ~30-80ms; * well inside the `event-dispatcher` 2.5s handler timeout * (`event-dispatcher.ts:365`). */ export interface SettingsCardHandlerResult { /** Optional β€” success path now returns ONLY a `card` to avoid the * toast + card two-pass render that flashes the OLD state. Errors, * permission denials, and noop still return a toast. */ toast?: { type: 'info' | 'success' | 'error'; content: string; }; card?: { type: 'raw'; data: Record; }; } export type PatchBuildResult = { ok: true; value: unknown; } | { ok: false; error: 'invalid_field' | 'invalid_value' | 'invalid_time' | 'invalid_action'; }; /** * Build the dashboard settings patch from an action callback. Pure β€” caller * decides whether to PUT. * * Whitelisting: `next_value` MUST be the literal string * `'true'` or `'false'`. Anything else (`'yes'`, `'TRUE'`, undefined, an * object) returns `invalid_value` so an upstream callback drift cannot * silently flip a toggle. * * Time validation: HH:MM regex, no silent fallback to 04:00. */ export declare function buildPatchFromAction(action: string, value: Record, formValue: Record): PatchBuildResult; /** * Dispatch a `dash_settings_*` action callback. Awaits the Route B * GET/PUT inline. Success path returns `{ card }` (card-only β€” see the * module docstring for why we drop the toast). Errors / permission * denials / noop return a plain `{ toast }`. */ export declare function handleSettingsCardAction(data: CardActionData, larkAppId: string, deps: SettingsCardHandlerDeps): Promise; //# sourceMappingURL=settings-card.d.ts.map