import { type ChannelName } from "./channels.js"; import type { SettingsService } from "./settings-rpc.js"; import type { FeishuOnboardingRegistry } from "./feishu-onboarding.js"; import { type CredentialStore } from "./credential-store.js"; import type { LiveConnectSection } from "./namespace.js"; import type { ChannelConnectionStatus } from "./channel-status.js"; export interface SettingsServiceOptions { /** JSON file to persist non-secret settings when no namespace is live (omit = in-memory only). */ statePath?: string; /** Known channel names (default: built-in channels). */ channelNames?: readonly ChannelName[]; /** Report credential presence per channel (optional; default: all false). */ credentialStore?: CredentialStore; /** Error sink (default: console.error). */ log?: (msg: string) => void; /** * Seed config used when no state file exists (or `statePath` is unset), so the * pane reflects the plugin's live config rather than an empty default. The * live config carries only the settings-shape keys (`channels`, `channelDefaults`, * per-channel) that the pane edits. */ initialConfig?: Record; /** * The live settings namespace, if one is installed. A **getter**, not a value: * this service is built during the plugin's `apply`, while the namespace is * registered later (the `settings` service is a `dsh-base` row that loads * after a user plugin), so the handle does not exist yet at construction time. */ live?: () => LiveConnectSection | undefined; /** * Called after a credential write lands in the store, so the running adapters * pick the new secret up. * * Writing the store is not enough on its own: an adapter holds the secret it * was started with, and the only thing that re-applies one is the plugin's * `reconcile` — which the settings namespace fires on a *config* write. The * pane saves config and credentials as two separate calls, so without this * hook a rotated `appSecret` stayed inert until the next host restart while * the pane reported 「已保存」 — the user's fix for a broken bot would appear * to have done nothing. * * A returned promise is **awaited** before the snapshot is built, which is * what puts the re-apply's own outcome (a channel that failed to restart, see * {@link channelFailures}) into the snapshot the pane receives instead of the * next one. The hook was fire-and-forget while `reconcile` was — both are * promises that settle quickly (adapters are applied synchronously). */ onCredentialsSaved?: () => void | Promise; /** * Channels whose adapter failed to start, as `{channel: reason}` — read * after a write to report a channel that the save left *not running*. * * A save can succeed and still not produce a working bot: `ChannelRuntime` * logs a throwing `apply` and starts the remaining channels anyway, so the * write is committed and the pane would say 「已保存」 over a channel that is * silently down. This is the read side of that: the runtime keeps the last * failure per channel, and the snapshot reports the ones for channels this * write touched. * * Failures are cleared by a later successful start (see `ChannelRuntime`), so * this stays a statement about the *current* state and not a growing history. */ channelFailures?: () => Record; /** * Per-channel **access** state for the pane's second badge — whether the * channel is switched on, up, or actually connected — as * `{channel: {state, attempts?}}`. See `channel-status.ts` for how a state is * composed; this option is only the plumbing. * * **The parameter is the service's own `enabled` array**, passed straight * through to the callback. That is a deliberate one-way dependency and not a * redundant argument: the badge's notion of 「未启用」 and the snapshot's own * `enabled` field then cannot disagree, because they are the same value. * * Its absence is meaningful and preserved: when this is not wired, the * snapshot carries **no** `channelStatus` key at all and the pane renders no * badge — an older host, or a boot where the channel runtime was never * created. That is why the snapshot attaches this *unconditionally* when the * option exists, breaking the surrounding "spread only when non-empty" * convention: elsewhere an empty list is merely an add-nothing, but here * 「未启用」 and 「未运行」 are themselves answers, and collapsing them into the * absent key would erase the distinction the pane depends on. * * Like {@link channelFailures} this is a plain getter (the runtime exists by * the time this is called), and a throwing probe is logged and dropped rather * than failing the snapshot. */ channelStatus?: (enabled: readonly ChannelName[]) => Record | undefined; /** * The one-click Feishu bot creation registry, if this host has one. Optional, * and like {@link live} a **getter, not a value**: the registry needs * `save()` — the enable step is a settings write, and it must take the exact * same path as a pane save (a partial write here would be a deletion) — while * this service is what `save()` belongs to. So the host assigns the registry * after building the service, and the getter is read per call. * * Absent (or resolving to `undefined`) means the three `onboarding.*` RPC * endpoints answer `unsupported` rather than pretending. */ onboarding?: () => FeishuOnboardingRegistry | undefined; /** * The DSH-wide default model, for the pane's read-only row. * * Read-only **by design**: `agentDefaultModel.saveSelection()` exists and is * what the `/model` and `/reasoning` chat commands use, so wiring a write * here is technically possible — and deliberately not done. That selection is * DSH's, shared with every other session and plugin; a settings pane that * silently moved it would change other people's runs as a side effect of * editing the Feishu bridge. The pane says where to change it instead. * * A getter because `agentDefaultModel` arrives through injection and may not * be present when this service is built. Returning undefined, throwing, or * answering with an empty provider/model all mean the same thing: drop the * row rather than render a control the user cannot use. */ agentModel?: () => { provider?: string; model?: string; } | undefined; /** * General-settings keys a shared config on this machine overrides, for the * pane's provenance note. Empty (or absent) is the normal case — no shared * config, ordinary editable rows. */ sharedOverrideKeys?: () => string[]; } /** * Build a settings service backed by a JSON state file. `get`/`status` read the * current state; `save` merges and persists. A missing/corrupt state file is * treated as an empty config (never throws on read). */ export declare function createSettingsService(options?: SettingsServiceOptions): SettingsService; //# sourceMappingURL=settings-service.d.ts.map