"use client"; import { create } from "zustand"; import { toast } from "sonner"; import { createCPAPool, deleteBackup, deleteCPAPool, fetchCPAPoolFiles, fetchCPAPools, fetchBackups, fetchSettingsConfig, runBackupNow, syncImageStorage, startCPAImport, testBackupConnection, testImageStorageConnection, updateCPAPool, updateSettingsConfig, type BackupItem, type BackupSettings, type BackupState, type CPAPool, type CPARemoteFile, type ImageStorageMode, type ImageStorageSettings, type ProxyRuntimeClearanceMode, type ProxyRuntimeEgressMode, type ProxyRuntimeSettings, type SettingsConfig, type ThirdPartyAppsSettings, } from "@/lib/api"; export const PAGE_SIZE_OPTIONS = ["50", "100", "200"] as const; export type PageSizeOption = (typeof PAGE_SIZE_OPTIONS)[number]; const DEFAULT_PROXY_RUNTIME: ProxyRuntimeSettings = { enabled: false, egress_mode: "direct", proxy_url: "", resource_proxy_url: "", skip_ssl_verify: false, reset_session_status_codes: [403], clearance: { enabled: false, mode: "none", cf_cookies: "", cf_clearance: "", user_agent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36", browser: "chrome", flaresolverr_url: "", timeout_sec: 60, refresh_interval: 3600, warm_up_on_start: false, has_cf_cookies: false, has_cf_clearance: false, }, }; const DEFAULT_THIRD_PARTY_APPS: ThirdPartyAppsSettings = { infinite_canvas: { enabled: false, url: "https://canvas.best", }, }; function normalizeProxyRuntime(value: unknown): ProxyRuntimeSettings { const source = typeof value === "object" && value !== null ? value as Partial : {}; const clearanceSource = typeof source.clearance === "object" && source.clearance !== null ? source.clearance as Partial : {}; const egressMode = source.egress_mode === "single_proxy" ? "single_proxy" : "direct"; const clearanceMode: ProxyRuntimeClearanceMode = clearanceSource.mode === "manual" || clearanceSource.mode === "flaresolverr" ? clearanceSource.mode : "none"; const statusCodes = Array.isArray(source.reset_session_status_codes) ? source.reset_session_status_codes .map((item) => Number(item)) .filter((item) => Number.isInteger(item) && item >= 100 && item <= 599) : []; return { ...DEFAULT_PROXY_RUNTIME, ...source, enabled: Boolean(source.enabled), egress_mode: egressMode as ProxyRuntimeEgressMode, proxy_url: String(source.proxy_url || ""), resource_proxy_url: String(source.resource_proxy_url || ""), skip_ssl_verify: Boolean(source.skip_ssl_verify), reset_session_status_codes: statusCodes.length > 0 ? statusCodes : [403], clearance: { ...DEFAULT_PROXY_RUNTIME.clearance, ...clearanceSource, enabled: Boolean(clearanceSource.enabled), mode: clearanceMode, cf_cookies: String(clearanceSource.cf_cookies || ""), cf_clearance: String(clearanceSource.cf_clearance || ""), user_agent: String(clearanceSource.user_agent || DEFAULT_PROXY_RUNTIME.clearance.user_agent), browser: String(clearanceSource.browser || "chrome"), flaresolverr_url: String(clearanceSource.flaresolverr_url || ""), timeout_sec: Number(clearanceSource.timeout_sec || 60), refresh_interval: Number(clearanceSource.refresh_interval || 3600), warm_up_on_start: Boolean(clearanceSource.warm_up_on_start), has_cf_cookies: Boolean(clearanceSource.has_cf_cookies), has_cf_clearance: Boolean(clearanceSource.has_cf_clearance), }, }; } function normalizeThirdPartyApps(value: unknown): ThirdPartyAppsSettings { const source = typeof value === "object" && value !== null ? value as Partial : {}; const canvas: Partial = typeof source.infinite_canvas === "object" && source.infinite_canvas ? source.infinite_canvas : {}; return { infinite_canvas: { enabled: Boolean(canvas.enabled), url: String(canvas.url || DEFAULT_THIRD_PARTY_APPS.infinite_canvas.url), }, }; } function normalizeConfig(config: SettingsConfig): SettingsConfig { const imageStorage = typeof config.image_storage === "object" && config.image_storage ? config.image_storage as ImageStorageSettings : { enabled: false, mode: "local", webdav_url: "", webdav_username: "", webdav_password: "", webdav_root_path: "chatgpt2api/images", public_base_url: "", }; const imageStorageMode: ImageStorageMode = imageStorage.enabled && imageStorage.mode === "both" ? "both" : imageStorage.enabled && imageStorage.mode === "webdav" ? "webdav" : "local"; const backup = typeof config.backup === "object" && config.backup ? config.backup as BackupSettings : { enabled: false, provider: "cloudflare_r2", account_id: "", access_key_id: "", secret_access_key: "", bucket: "", prefix: "backups", interval_minutes: 360, rotation_keep: 10, encrypt: false, passphrase: "", include: { config: true, cpa: true, sub2api: true, logs: true, image_tasks: true, accounts_snapshot: true, auth_keys_snapshot: true, images: false, }, }; return { ...config, refresh_account_interval_minute: Number(config.refresh_account_interval_minute || 5), image_retention_days: Number(config.image_retention_days || 30), image_poll_timeout_secs: Number(config.image_poll_timeout_secs || 120), image_account_concurrency: Number(config.image_account_concurrency || 3), image_settle_enabled: Boolean(config.image_settle_enabled !== false), image_check_before_hit_enabled: Boolean(config.image_check_before_hit_enabled !== false), image_remove_conversation_after_result: Boolean(config.image_remove_conversation_after_result), image_settle_secs: Number(config.image_settle_secs || 2.0), image_timeout_retry_secs: Number(config.image_timeout_retry_secs || 30), auto_remove_invalid_accounts: Boolean(config.auto_remove_invalid_accounts), auto_remove_rate_limited_accounts: Boolean(config.auto_remove_rate_limited_accounts), auto_relogin_after_refresh: Boolean(config.auto_relogin_after_refresh), log_levels: Array.isArray(config.log_levels) ? config.log_levels : [], proxy: typeof config.proxy === "string" ? config.proxy : "", base_url: typeof config.base_url === "string" ? config.base_url : "", global_system_prompt: String(config.global_system_prompt || ""), sensitive_words: Array.isArray(config.sensitive_words) ? config.sensitive_words : [], ai_review: { enabled: Boolean(config.ai_review?.enabled), base_url: String(config.ai_review?.base_url || ""), api_key: String(config.ai_review?.api_key || ""), model: String(config.ai_review?.model || ""), prompt: String(config.ai_review?.prompt || ""), }, image_storage: { enabled: Boolean(imageStorage.enabled), mode: imageStorageMode, webdav_url: String(imageStorage.webdav_url || ""), webdav_username: String(imageStorage.webdav_username || ""), webdav_password: String(imageStorage.webdav_password || ""), webdav_root_path: String(imageStorage.webdav_root_path || "chatgpt2api/images"), public_base_url: String(imageStorage.public_base_url || ""), }, proxy_runtime: normalizeProxyRuntime(config.proxy_runtime), third_party_apps: normalizeThirdPartyApps(config.third_party_apps), backup: { ...backup, enabled: Boolean(backup.enabled), account_id: String(backup.account_id || ""), access_key_id: String(backup.access_key_id || ""), secret_access_key: String(backup.secret_access_key || ""), bucket: String(backup.bucket || ""), prefix: String(backup.prefix || "backups"), interval_minutes: Number(backup.interval_minutes || 360), rotation_keep: Number(backup.rotation_keep ?? 10), encrypt: Boolean(backup.encrypt), passphrase: String(backup.passphrase || ""), include: { config: Boolean(backup.include?.config ?? true), cpa: Boolean(backup.include?.cpa ?? true), sub2api: Boolean(backup.include?.sub2api ?? true), logs: Boolean(backup.include?.logs ?? true), image_tasks: Boolean(backup.include?.image_tasks ?? true), accounts_snapshot: Boolean(backup.include?.accounts_snapshot ?? true), auth_keys_snapshot: Boolean(backup.include?.auth_keys_snapshot ?? true), images: Boolean(backup.include?.images ?? false), }, }, }; } function normalizeFiles(items: CPARemoteFile[]) { const seen = new Set(); const files: CPARemoteFile[] = []; for (const item of items) { const name = String(item.name || "").trim(); if (!name || seen.has(name)) { continue; } seen.add(name); files.push({ name, email: String(item.email || "").trim(), }); } return files; } type SettingsStore = { config: SettingsConfig | null; isLoadingConfig: boolean; isSavingConfig: boolean; backups: BackupItem[]; backupState: BackupState | null; isLoadingBackups: boolean; isRunningBackup: boolean; deletingBackupKey: string | null; isTestingBackup: boolean; isTestingImageStorage: boolean; isSyncingImageStorage: boolean; pools: CPAPool[]; isLoadingPools: boolean; deletingId: string | null; loadingFilesId: string | null; dialogOpen: boolean; editingPool: CPAPool | null; formName: string; formBaseUrl: string; formSecretKey: string; showSecret: boolean; isSavingPool: boolean; browserOpen: boolean; browserPool: CPAPool | null; remoteFiles: CPARemoteFile[]; selectedNames: string[]; fileQuery: string; filePage: number; pageSize: PageSizeOption; isStartingImport: boolean; initialize: () => Promise; loadConfig: () => Promise; saveConfig: () => Promise; loadBackups: (silent?: boolean) => Promise; runBackup: () => Promise; removeBackup: (key: string) => Promise; testBackup: () => Promise; setRefreshAccountIntervalMinute: (value: string) => void; setImageRetentionDays: (value: string) => void; setImagePollTimeoutSecs: (value: string) => void; setImageAccountConcurrency: (value: string) => void; setImageSettleEnabled: (value: boolean) => void; setImageCheckBeforeHitEnabled: (value: boolean) => void; setImageRemoveConversationAfterResult: (value: boolean) => void; setImageSettleSecs: (value: string) => void; setImageTimeoutRetrySecs: (value: string) => void; setAutoRemoveInvalidAccounts: (value: boolean) => void; setAutoRemoveRateLimitedAccounts: (value: boolean) => void; setAutoReloginAfterRefresh: (value: boolean) => void; setLogLevel: (level: string, enabled: boolean) => void; setProxy: (value: string) => void; setBaseUrl: (value: string) => void; setGlobalSystemPrompt: (value: string) => void; setSensitiveWordsText: (value: string) => void; setAIReviewField: (key: "enabled" | "base_url" | "api_key" | "model" | "prompt", value: string | boolean) => void; setImageStorageField: (key: keyof ImageStorageSettings, value: string | boolean) => void; setProxyRuntimeField: (key: K, value: ProxyRuntimeSettings[K]) => void; setProxyRuntimeClearanceField: (key: K, value: ProxyRuntimeSettings["clearance"][K]) => void; setProxyRuntimeStatusCodesText: (value: string) => void; setInfiniteCanvasField: (key: K, value: ThirdPartyAppsSettings["infinite_canvas"][K]) => void; testImageStorage: () => Promise; syncImagesToWebDAV: () => Promise; setBackupField: (key: keyof BackupSettings, value: string | boolean) => void; setBackupInclude: (key: keyof BackupSettings["include"], value: boolean) => void; loadPools: (silent?: boolean) => Promise; openAddDialog: () => void; openEditDialog: (pool: CPAPool) => void; setDialogOpen: (open: boolean) => void; setFormName: (value: string) => void; setFormBaseUrl: (value: string) => void; setFormSecretKey: (value: string) => void; setShowSecret: (checked: boolean) => void; savePool: () => Promise; deletePool: (pool: CPAPool) => Promise; browseFiles: (pool: CPAPool) => Promise; setBrowserOpen: (open: boolean) => void; toggleFile: (name: string, checked: boolean) => void; replaceSelectedNames: (names: string[]) => void; setFileQuery: (value: string) => void; setFilePage: (page: number) => void; setPageSize: (value: PageSizeOption) => void; startImport: () => Promise; }; export const useSettingsStore = create((set, get) => ({ config: null, isLoadingConfig: true, isSavingConfig: false, backups: [], backupState: null, isLoadingBackups: true, isRunningBackup: false, deletingBackupKey: null, isTestingBackup: false, isTestingImageStorage: false, isSyncingImageStorage: false, pools: [], isLoadingPools: true, deletingId: null, loadingFilesId: null, dialogOpen: false, editingPool: null, formName: "", formBaseUrl: "", formSecretKey: "", showSecret: false, isSavingPool: false, browserOpen: false, browserPool: null, remoteFiles: [], selectedNames: [], fileQuery: "", filePage: 1, pageSize: "100", isStartingImport: false, initialize: async () => { await Promise.allSettled([get().loadConfig(), get().loadPools()]); const backup = get().config?.backup; const isConfigured = Boolean( String(backup?.account_id || "").trim() && String(backup?.access_key_id || "").trim() && String(backup?.secret_access_key || "").trim() && String(backup?.bucket || "").trim(), ); if (isConfigured) { await get().loadBackups(); } else { set({ backups: [], isLoadingBackups: false }); } }, loadConfig: async () => { set({ isLoadingConfig: true }); try { const data = await fetchSettingsConfig(); const normalized = normalizeConfig(data.config); set({ config: normalized, }); } catch (error) { toast.error(error instanceof Error ? error.message : "加载系统配置失败"); } finally { set({ isLoadingConfig: false }); } }, saveConfig: async () => { const { config } = get(); if (!config) { return false; } set({ isSavingConfig: true }); try { const data = await updateSettingsConfig({ ...config, refresh_account_interval_minute: Math.max(1, Number(config.refresh_account_interval_minute) || 1), image_retention_days: Math.max(1, Number(config.image_retention_days) || 30), image_poll_timeout_secs: Math.max(1, Number(config.image_poll_timeout_secs) || 120), image_account_concurrency: Math.max(1, Number(config.image_account_concurrency) || 3), image_settle_enabled: Boolean(config.image_settle_enabled !== false), image_check_before_hit_enabled: Boolean(config.image_check_before_hit_enabled !== false), image_remove_conversation_after_result: Boolean(config.image_remove_conversation_after_result), image_settle_secs: Math.max(0.5, Number(config.image_settle_secs) || 2.0), image_timeout_retry_secs: Math.max(1, Number(config.image_timeout_retry_secs) || 30), auto_remove_invalid_accounts: Boolean(config.auto_remove_invalid_accounts), auto_remove_rate_limited_accounts: Boolean(config.auto_remove_rate_limited_accounts), auto_relogin_after_refresh: Boolean(config.auto_relogin_after_refresh), proxy: config.proxy.trim(), base_url: String(config.base_url || "").trim(), global_system_prompt: String(config.global_system_prompt || "").trim(), sensitive_words: (config.sensitive_words || []).map((item) => String(item).trim()).filter(Boolean), ai_review: { enabled: Boolean(config.ai_review?.enabled), base_url: String(config.ai_review?.base_url || "").trim(), api_key: String(config.ai_review?.api_key || "").trim(), model: String(config.ai_review?.model || "").trim(), prompt: String(config.ai_review?.prompt || "").trim(), }, image_storage: { enabled: Boolean(config.image_storage?.enabled), mode: config.image_storage?.enabled && ["webdav", "both"].includes(String(config.image_storage?.mode)) ? config.image_storage.mode : "local", webdav_url: String(config.image_storage?.webdav_url || "").trim(), webdav_username: String(config.image_storage?.webdav_username || "").trim(), webdav_password: String(config.image_storage?.webdav_password || "").trim(), webdav_root_path: String(config.image_storage?.webdav_root_path || "chatgpt2api/images").trim(), public_base_url: String(config.image_storage?.public_base_url || "").trim(), }, proxy_runtime: { ...normalizeProxyRuntime(config.proxy_runtime), proxy_url: String(config.proxy_runtime?.proxy_url || "").trim(), resource_proxy_url: String(config.proxy_runtime?.resource_proxy_url || "").trim(), reset_session_status_codes: normalizeProxyRuntime({ reset_session_status_codes: (config.proxy_runtime?.reset_session_status_codes || [403]) .map((item) => Number(item)) .filter((item) => Number.isInteger(item) && item >= 100 && item <= 599), }).reset_session_status_codes, clearance: { ...normalizeProxyRuntime(config.proxy_runtime).clearance, cf_cookies: String(config.proxy_runtime?.clearance?.cf_cookies || "").trim(), cf_clearance: String(config.proxy_runtime?.clearance?.cf_clearance || "").trim(), user_agent: String(config.proxy_runtime?.clearance?.user_agent || DEFAULT_PROXY_RUNTIME.clearance.user_agent).trim(), browser: String(config.proxy_runtime?.clearance?.browser || "chrome").trim(), flaresolverr_url: String(config.proxy_runtime?.clearance?.flaresolverr_url || "").trim(), timeout_sec: Math.max(1, Number(config.proxy_runtime?.clearance?.timeout_sec) || 60), refresh_interval: Math.max(60, Number(config.proxy_runtime?.clearance?.refresh_interval) || 3600), }, }, third_party_apps: { infinite_canvas: { enabled: Boolean(config.third_party_apps?.infinite_canvas?.enabled), url: String(config.third_party_apps?.infinite_canvas?.url || DEFAULT_THIRD_PARTY_APPS.infinite_canvas.url).trim(), }, }, backup: { ...(config.backup as BackupSettings), account_id: String(config.backup?.account_id || "").trim(), access_key_id: String(config.backup?.access_key_id || "").trim(), secret_access_key: String(config.backup?.secret_access_key || "").trim(), bucket: String(config.backup?.bucket || "").trim(), prefix: String(config.backup?.prefix || "backups").trim(), interval_minutes: Math.max(1, Number(config.backup?.interval_minutes) || 360), rotation_keep: Math.max(0, Number(config.backup?.rotation_keep) || 0), passphrase: String(config.backup?.passphrase || "").trim(), }, }); set({ config: normalizeConfig(data.config), }); window.dispatchEvent(new Event("third-party-apps-updated")); toast.success("配置已保存"); return true; } catch (error) { toast.error(error instanceof Error ? error.message : "保存系统配置失败"); return false; } finally { set({ isSavingConfig: false }); } }, setRefreshAccountIntervalMinute: (value) => { set((state) => { if (!state.config) { return {}; } return { config: { ...state.config, refresh_account_interval_minute: value, }, }; }); }, setImageRetentionDays: (value) => { set((state) => state.config ? { config: { ...state.config, image_retention_days: value } } : {}); }, setImagePollTimeoutSecs: (value) => { set((state) => state.config ? { config: { ...state.config, image_poll_timeout_secs: value } } : {}); }, setImageAccountConcurrency: (value) => { set((state) => state.config ? { config: { ...state.config, image_account_concurrency: value } } : {}); }, setImageSettleEnabled: (value) => { set((state) => state.config ? { config: { ...state.config, image_settle_enabled: value, image_check_before_hit_enabled: value } } : {}); }, setImageCheckBeforeHitEnabled: (value) => { set((state) => state.config ? { config: { ...state.config, image_check_before_hit_enabled: value } } : {}); }, setImageRemoveConversationAfterResult: (value) => { set((state) => state.config ? { config: { ...state.config, image_remove_conversation_after_result: value } } : {}); }, setImageSettleSecs: (value) => { set((state) => state.config ? { config: { ...state.config, image_settle_secs: value } } : {}); }, setImageTimeoutRetrySecs: (value) => { set((state) => state.config ? { config: { ...state.config, image_timeout_retry_secs: value } } : {}); }, setAutoRemoveInvalidAccounts: (value) => { set((state) => state.config ? { config: { ...state.config, auto_remove_invalid_accounts: value } } : {}); }, setAutoRemoveRateLimitedAccounts: (value) => { set((state) => state.config ? { config: { ...state.config, auto_remove_rate_limited_accounts: value } } : {}); }, setAutoReloginAfterRefresh: (value) => { set((state) => state.config ? { config: { ...state.config, auto_relogin_after_refresh: value } } : {}); }, setLogLevel: (level, enabled) => { set((state) => { if (!state.config) return {}; const levels = new Set(state.config.log_levels || []); if (enabled) levels.add(level); else levels.delete(level); return { config: { ...state.config, log_levels: Array.from(levels) } }; }); }, setProxy: (value) => { set((state) => { if (!state.config) { return {}; } return { config: { ...state.config, proxy: value, }, }; }); }, setBaseUrl: (value) => { set((state) => { if (!state.config) { return {}; } return { config: { ...state.config, base_url: value, }, }; }); }, setGlobalSystemPrompt: (value) => { set((state) => state.config ? { config: { ...state.config, global_system_prompt: value } } : {}); }, setSensitiveWordsText: (value) => { set((state) => state.config ? { config: { ...state.config, sensitive_words: value.split("\n") } } : {}); }, setAIReviewField: (key, value) => { set((state) => state.config ? { config: { ...state.config, ai_review: { ...(state.config.ai_review || {}), [key]: value } } } : {}); }, setImageStorageField: (key, value) => { set((state) => { if (!state.config?.image_storage) { return {}; } const next = { ...state.config.image_storage, [key]: value, }; if (key === "enabled" && !value) { next.mode = "local"; } if (key === "enabled" && value && next.mode === "local") { next.mode = "webdav"; } return { config: { ...state.config, image_storage: next, }, }; }); }, setProxyRuntimeField: (key, value) => { set((state) => { if (!state.config) { return {}; } const runtime = normalizeProxyRuntime(state.config.proxy_runtime); const nextRuntime = normalizeProxyRuntime({ ...runtime, [key]: value, }); return { config: { ...state.config, proxy_runtime: nextRuntime, }, }; }); }, setProxyRuntimeClearanceField: (key, value) => { set((state) => { if (!state.config) { return {}; } const runtime = normalizeProxyRuntime(state.config.proxy_runtime); const nextRuntime = normalizeProxyRuntime({ ...runtime, clearance: { ...runtime.clearance, [key]: value, }, }); return { config: { ...state.config, proxy_runtime: nextRuntime, }, }; }); }, setProxyRuntimeStatusCodesText: (value) => { const codes = value .split(/[,\s]+/) .map((item) => Number(item.trim())) .filter((item) => Number.isInteger(item) && item >= 100 && item <= 599); set((state) => { if (!state.config) { return {}; } const runtime = normalizeProxyRuntime(state.config.proxy_runtime); return { config: { ...state.config, proxy_runtime: normalizeProxyRuntime({ ...runtime, reset_session_status_codes: codes.length > 0 ? codes : [403], }), }, }; }); }, setInfiniteCanvasField: (key, value) => { set((state) => { if (!state.config) { return {}; } const apps = normalizeThirdPartyApps(state.config.third_party_apps); return { config: { ...state.config, third_party_apps: { ...apps, infinite_canvas: { ...apps.infinite_canvas, [key]: value, }, }, }, }; }); }, testImageStorage: async () => { set({ isTestingImageStorage: true }); try { const saved = await get().saveConfig(); if (!saved) { return; } const data = await testImageStorageConnection(); if (data.result.ok) { toast.success(`WebDAV 连接可用:HTTP ${data.result.status}`); } else { toast.error(`WebDAV 连接失败:${data.result.error ?? `HTTP ${data.result.status}`}`); } } catch (error) { toast.error(error instanceof Error ? error.message : "测试 WebDAV 失败"); } finally { set({ isTestingImageStorage: false }); } }, syncImagesToWebDAV: async () => { set({ isSyncingImageStorage: true }); try { const saved = await get().saveConfig(); if (!saved) { return; } const data = await syncImageStorage(); toast.success(`同步完成:上传 ${data.result.uploaded},跳过 ${data.result.skipped},失败 ${data.result.failed}`); } catch (error) { toast.error(error instanceof Error ? error.message : "同步图片失败"); } finally { set({ isSyncingImageStorage: false }); } }, setBackupField: (key, value) => { set((state) => { if (!state.config?.backup) { return {}; } return { config: { ...state.config, backup: { ...state.config.backup, [key]: value, }, }, }; }); }, setBackupInclude: (key, value) => { set((state) => { if (!state.config?.backup) { return {}; } return { config: { ...state.config, backup: { ...state.config.backup, include: { ...state.config.backup.include, [key]: value, }, }, }, }; }); }, loadBackups: async (silent = false) => { if (!silent) { set({ isLoadingBackups: true }); } try { const data = await fetchBackups(); set({ backups: data.items, backupState: data.state, }); } catch (error) { if (!silent) { toast.error(error instanceof Error ? error.message : "加载备份列表失败"); } } finally { if (!silent) { set({ isLoadingBackups: false }); } } }, runBackup: async () => { set({ isRunningBackup: true }); try { const saved = await get().saveConfig(); if (!saved) { return; } const data = await runBackupNow(); toast.success(`备份已完成:${data.result.key}`); await get().loadBackups(true); } catch (error) { toast.error(error instanceof Error ? error.message : "执行备份失败"); } finally { set({ isRunningBackup: false }); } }, removeBackup: async (key) => { set({ deletingBackupKey: key }); try { await deleteBackup(key); toast.success("备份已删除"); await get().loadBackups(true); } catch (error) { toast.error(error instanceof Error ? error.message : "删除备份失败"); } finally { set({ deletingBackupKey: null }); } }, testBackup: async () => { set({ isTestingBackup: true }); try { const saved = await get().saveConfig(); if (!saved) { return; } const data = await testBackupConnection(); toast.success(`R2 连接正常(HTTP ${data.result.status})`); } catch (error) { toast.error(error instanceof Error ? error.message : "测试备份连接失败"); } finally { set({ isTestingBackup: false }); } }, loadPools: async (silent = false) => { if (!silent) { set({ isLoadingPools: true }); } try { const data = await fetchCPAPools(); set({ pools: data.pools }); } catch (error) { if (!silent) { toast.error(error instanceof Error ? error.message : "加载 CPA 连接失败"); } } finally { if (!silent) { set({ isLoadingPools: false }); } } }, openAddDialog: () => { set({ editingPool: null, formName: "", formBaseUrl: "", formSecretKey: "", showSecret: false, dialogOpen: true, }); }, openEditDialog: (pool) => { set({ editingPool: pool, formName: pool.name, formBaseUrl: pool.base_url, formSecretKey: "", showSecret: false, dialogOpen: true, }); }, setDialogOpen: (open) => { set({ dialogOpen: open }); }, setFormName: (value) => { set({ formName: value }); }, setFormBaseUrl: (value) => { set({ formBaseUrl: value }); }, setFormSecretKey: (value) => { set({ formSecretKey: value }); }, setShowSecret: (checked) => { set({ showSecret: checked }); }, savePool: async () => { const { editingPool, formName, formBaseUrl, formSecretKey } = get(); if (!formBaseUrl.trim()) { toast.error("请输入 CPA 地址"); return; } if (!editingPool && !formSecretKey.trim()) { toast.error("请输入 Secret Key"); return; } set({ isSavingPool: true }); try { if (editingPool) { const data = await updateCPAPool(editingPool.id, { name: formName.trim(), base_url: formBaseUrl.trim(), secret_key: formSecretKey.trim() || undefined, }); set({ pools: data.pools, dialogOpen: false }); toast.success("连接已更新"); } else { const data = await createCPAPool({ name: formName.trim(), base_url: formBaseUrl.trim(), secret_key: formSecretKey.trim(), }); set({ pools: data.pools, dialogOpen: false }); toast.success("连接已添加"); } } catch (error) { toast.error(error instanceof Error ? error.message : "保存失败"); } finally { set({ isSavingPool: false }); } }, deletePool: async (pool) => { set({ deletingId: pool.id }); try { const data = await deleteCPAPool(pool.id); set({ pools: data.pools }); toast.success("连接已删除"); } catch (error) { toast.error(error instanceof Error ? error.message : "删除失败"); } finally { set({ deletingId: null }); } }, browseFiles: async (pool) => { set({ loadingFilesId: pool.id }); try { const data = await fetchCPAPoolFiles(pool.id); const files = normalizeFiles(data.files); set({ browserPool: pool, remoteFiles: files, selectedNames: [], fileQuery: "", filePage: 1, browserOpen: true, }); toast.success(`读取成功,共 ${files.length} 个远程账号`); } catch (error) { toast.error(error instanceof Error ? error.message : "读取远程账号失败"); } finally { set({ loadingFilesId: null }); } }, setBrowserOpen: (open) => { set({ browserOpen: open }); }, toggleFile: (name, checked) => { set((state) => { if (checked) { return { selectedNames: Array.from(new Set([...state.selectedNames, name])), }; } return { selectedNames: state.selectedNames.filter((item) => item !== name), }; }); }, replaceSelectedNames: (names) => { set({ selectedNames: Array.from(new Set(names)) }); }, setFileQuery: (value) => { set({ fileQuery: value, filePage: 1 }); }, setFilePage: (page) => { set({ filePage: page }); }, setPageSize: (value) => { set({ pageSize: value, filePage: 1 }); }, startImport: async () => { const { browserPool, selectedNames, pools } = get(); if (!browserPool) { return; } if (selectedNames.length === 0) { toast.error("请先选择要导入的账号"); return; } set({ isStartingImport: true }); try { const result = await startCPAImport(browserPool.id, selectedNames); set({ pools: pools.map((pool) => pool.id === browserPool.id ? { ...pool, import_job: result.import_job } : pool, ), browserOpen: false, }); toast.success("导入任务已启动"); } catch (error) { toast.error(error instanceof Error ? error.message : "启动导入失败"); } finally { set({ isStartingImport: false }); } }, }));