/** * Settings write applier — single source of truth for what * `PUT /api/settings` (`dashboard.ts:460-498`) used to do inline. * * Lives in `src/dashboard/` so both: * - the existing browser-facing `PUT /api/settings` route * - the new HMAC-gated `PUT /__daemon/settings-write` route * share the same validation + persistence path. Behaviour is byte-equivalent * to the original inline implementation; the only change is that all IO is * funnelled through `deps`, so tests don't touch `~/.botmux`. */ import type { DashboardGlobalConfig, GlobalConfig, MaintenanceConfig } from '../global-config.js'; /** * Snapshot returned by `resolveDashboardSettings` — mirrors the existing * `ResolvedDashboardSettings` interface in `dashboard.ts:69-80`. We redeclare * it locally rather than reaching across that boundary because the applier * doesn't know how the host computes the snapshot (it just calls a closure). */ export interface ResolvedDashboardSettingsView { groupNamePrefix: string; publicReadOnly: boolean; openTerminalInFeishu: boolean; enableLocalCliOpen: boolean; localCliOpenMode: 'attach' | 'resume'; chatBotDiscovery: boolean; herdrTraexPlugin: { enabled: boolean; source: string; ref: string; recommendedSource: string; recommendedRef: string; }; codexRpcInput: boolean; bypassCodexHookTrust: boolean; codexNotifier: { enabled: boolean; targetBotAppId: string | null; notifyWhen: 'locked_only' | 'always'; platformSupported: boolean; hookInstalled: boolean; botOptions?: Array<{ larkAppId: string; botName: string | null; cliId: string; recipientConfigured: boolean; recipientVerified: boolean; recipientHint: string | null; }>; targetDaemonOnline?: boolean; pendingCount?: number; workerOnline?: boolean; lastError?: { at: string; message: string; retryAt: string; } | null; }; hostOverloadAlert: { enabled: boolean; targetBotAppId: string | null; enterLoadRatio: number; enterMemUsedFrac: number; /** Bots eligible as the overload notifier (any non-apiOnly bot with a * resolvable admin recipient). Unlike codexNotifier this is NOT codex-only. */ botOptions?: Array<{ larkAppId: string; botName: string | null; cliId: string; apiOnly: boolean; recipientConfigured: boolean; recipientVerified: boolean; recipientHint: string | null; }>; /** Whether the selected target bot's daemon is currently online (else the * alert can't be delivered — the UI surfaces this). */ targetDaemonOnline?: boolean; }; noVisibleOutputHint: boolean; vcMeetingAgent: { enabled: boolean; larkCliVersion?: string | null; larkCliMeetsRequirement?: boolean; larkCliMinVersion?: string; }; maintenance: MaintenanceConfig; localDevInstall: boolean; autoUpdateSupported?: boolean; remoteAccess?: boolean; /** Machine-wide v3 Workflow feature switch. Default ON. */ workflow: { enabled: boolean; }; /** Configured schedule-task timezone override (IANA), or null/absent when * unset ⇒ the scheduler follows `hostTimeZone`. */ scheduleTimeZone?: string | null; /** Host's auto-detected local zone. */ hostTimeZone?: string; /** The TRUE effective zone (scheduleTimeZone(): env → config → host). The UI * must use this for "currently effective", not configured||host. */ effectiveScheduleTimeZone?: string; } export type ParseMaintenanceResult = { ok: true; patch: MaintenanceConfig; } | { ok: false; error: string; }; /** All IO this helper needs — injected so tests use mocks, production wires real impls. */ export interface SettingsWriteApplierDeps { /** Snapshot of `~/.botmux/config.json`. Used to look up the persisted autoUpdate state when the incoming patch doesn't change it. */ readGlobalConfig: () => GlobalConfig; /** Atomic write of dashboard-level fields. */ mergeDashboardConfig: (patch: DashboardGlobalConfig) => DashboardGlobalConfig; /** Atomic write of global-level fields (repoPickerMode / scheduleTimeZone / …). * Mirrors the real `mergeGlobalConfig`: a `null` value deletes that key. */ mergeGlobalConfig: (patch: Partial>) => void; /** Replace known notifier fields while preserving future sibling keys on disk. */ writeCodexNotifierConfig: (config: import('../global-config.js').CodexNotifierGlobalConfig) => void; /** Replace known host-overload-alert fields while preserving future sibling keys. */ writeHostOverloadAlertConfig: (config: import('../global-config.js').HostOverloadAlertGlobalConfig) => void; /** Atomic write of maintenance-level fields (autoUpdate / autoRestart). */ mergeMaintenanceConfig: (patch: MaintenanceConfig) => MaintenanceConfig; /** Set global UI locale (null = clear). Fans out to daemons via IPC. */ setGlobalLocale: (locale: 'zh' | 'en' | null) => void; /** Type-strict body validator for the maintenance segment. */ parseMaintenancePatch: (body: unknown) => ParseMaintenanceResult; /** True iff the current install is a source-checkout (auto-update unavailable). */ isLocalDevInstall: () => boolean; /** True iff the current global install is owned by a supported updater. */ isAutoUpdateSupportedInstall: () => boolean; /** Returns the post-merge view the response body echoes back to the caller. */ resolveDashboardSettings: () => ResolvedDashboardSettingsView; /** Validate locale string. */ isLocale: (v: unknown) => v is 'zh' | 'en'; /** Fan out locale reload to all online daemons. */ reloadLocaleOnAllDaemons?: () => Promise; /** 校验通知 Bot;保存关闭态配置时只校验静态配置,启用时再要求 daemon 与收件人就绪。 */ validateCodexNotifierTargetBotAppId?: (appId: string, options?: { requireReady?: boolean; }) => Promise<{ ok: true; } | { ok: false; error: string; }>; /** 校验过载告警通知 Bot:拒 unknown / apiOnly / 无可解析管理员收件人;启用时 * 额外要求目标 daemon 在线(否则告警发不出)。复用管理员解析但无 codex-only 约束。 */ validateHostOverloadAlertTargetBotAppId?: (appId: string, options?: { requireReady?: boolean; }) => Promise<{ ok: true; } | { ok: false; error: string; }>; /** Reconcile the stable core Hook command before enabling notification. */ installCodexNotifierHook?: () => void; /** locked_only currently depends on macOS IORegistry. */ isCodexNotifierPlatformSupported?: () => boolean; } /** Production deps wiring — call once per dashboard process. */ export declare function defaultSettingsWriteApplierDeps(resolveDashboardSettings: () => ResolvedDashboardSettingsView, reloadLocaleOnAllDaemons?: () => Promise): SettingsWriteApplierDeps; export type ApplySettingsWriteResult = { ok: true; settings: ResolvedDashboardSettingsView; } | { ok: false; error: ApplySettingsWriteError; feishuLoginQr?: string; }; /** * Discrete error codes — every one of these MUST match the strings the old * inline `PUT /api/settings` route returned, so callers (browser SPA, tests, * PR2 Route B) see the same wire vocabulary they had before. */ export type ApplySettingsWriteError = 'invalid_groupNamePrefix' | 'invalid_publicReadOnly' | 'invalid_openTerminalInFeishu' | 'invalid_enableLocalCliOpen' | 'invalid_localCliOpenMode' | 'invalid_chatBotDiscovery' | 'invalid_herdrTraexPlugin' | 'invalid_herdrTraexPlugin_enabled' | 'invalid_herdrTraexPlugin_source' | 'invalid_herdrTraexPlugin_ref' | 'invalid_codexRpcInput' | 'invalid_bypassCodexHookTrust' | 'invalid_codexNotifier' | 'invalid_codexNotifier_enabled' | 'invalid_codexNotifier_targetBotAppId' | 'invalid_codexNotifier_notifyWhen' | 'codexNotifier_target_required' | 'codexNotifier_target_unknown' | 'codexNotifier_target_owner_missing' | 'codexNotifier_platform_unsupported' | 'codexNotifier_hook_install_failed' | 'codexNotifier_mixed_patch_unsupported' | 'invalid_hostOverloadAlert' | 'invalid_hostOverloadAlert_enabled' | 'invalid_hostOverloadAlert_targetBotAppId' | 'invalid_hostOverloadAlert_enterLoadRatio' | 'invalid_hostOverloadAlert_enterMemUsedFrac' | 'hostOverloadAlert_target_required' | 'hostOverloadAlert_target_unknown' | 'hostOverloadAlert_target_apiOnly' | 'hostOverloadAlert_target_owner_missing' | 'hostOverloadAlert_target_offline' | 'invalid_noVisibleOutputHint' | 'invalid_repoPickerMode' | 'invalid_remoteAccess' | 'invalid_vcMeetingAgent' | 'invalid_vcMeetingAgent_enabled' | 'invalid_vcMeetingAgent_listenerBotAppId' | 'invalid_workflow' | 'invalid_workflow_enabled' | 'invalid_scheduleTimeZone' | 'invalid_whiteboard' | 'invalid_whiteboard_enabled' | 'invalid_lang' | 'invalid_maintenance' | 'local_dev_no_autoupdate' | 'unsupported_install_no_autoupdate' | 'autoupdate_required' | 'empty_patch' | string; /** 与 daemon 实际私聊收件人选择保持一致:只要求存在首个可用 open_id。 */ export declare function hasResolvedCodexNotifierRecipient(resolvedAllowedUsers: readonly string[] | undefined): boolean; /** 收件人提示只取 daemon 实际会私聊的首个 open_id,不能展示未解析的原始账号。 */ export declare function resolveCodexNotifierRecipientView(configuredAllowedUsers: readonly string[] | undefined, resolvedAllowedUsers: readonly string[] | undefined): { recipientConfigured: boolean; recipientVerified: boolean; recipientHint: string | null; }; /** * Apply a parsed (object) settings patch. Returns success with the post-merge * snapshot, or an error code string on validation failure. * * Behaviour mirrors `dashboard.ts:460-498` exactly: * - Validates dashboard toggles are booleans. * - Validates `repoPickerMode` is 'all' | 'repos'. * - Validates `lang` is a valid locale or null. * - Defers maintenance validation to `parseMaintenancePatch` (returns its error verbatim). * - Forbids enabling `autoUpdate` on a local-dev install. * - Forbids enabling `autoRestart` unless `autoUpdate` is (or is being) enabled. * - Returns `empty_patch` when no fields changed. */ export declare function applySettingsWrite(body: unknown, deps: SettingsWriteApplierDeps): Promise; //# sourceMappingURL=settings-write-applier.d.ts.map