Spaces:
Paused
Paused
| "use client"; | |
| import { create } from "zustand"; | |
| import { toast } from "sonner"; | |
| import { | |
| createCPAPool, | |
| deleteBackup, | |
| deleteCPAPool, | |
| fetchCPAPoolFiles, | |
| fetchCPAPools, | |
| fetchBackups, | |
| fetchRegisterConfig, | |
| resetRegister as resetRegisterApi, | |
| fetchSettingsConfig, | |
| runBackupNow, | |
| startCodexRegister as startCodexRegisterApi, | |
| startRegister, | |
| startCPAImport, | |
| stopRegister, | |
| testBackupConnection, | |
| updateCPAPool, | |
| updateRegisterConfig, | |
| updateSettingsConfig, | |
| type BackupItem, | |
| type BackupSettings, | |
| type BackupState, | |
| type CPAPool, | |
| type CPARemoteFile, | |
| type RegisterConfig, | |
| type SettingsConfig, | |
| } from "@/lib/api"; | |
| export const PAGE_SIZE_OPTIONS = ["50", "100", "200"] as const; | |
| export type PageSizeOption = (typeof PAGE_SIZE_OPTIONS)[number]; | |
| const DEFAULT_HERO_SMS_COUNTRY_POOL = [6, 117, 31, 33, 2, 39, 48, 37, 13, 40, 15, 8, 129, 32, 86, 173, 43, 49, 34, 7, 85, 27, 172, 63, 56, 177, 54, 24, 1, 46, 175, 14, 67, 83, 59, 187, 36]; | |
| const DEFAULT_HERO_SMS_COUNTRY_BLACKLIST = [16, 10, 4]; | |
| function normalizeConfig(config: SettingsConfig): SettingsConfig { | |
| 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, | |
| register: 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), | |
| auto_remove_invalid_accounts: Boolean(config.auto_remove_invalid_accounts), | |
| auto_remove_rate_limited_accounts: Boolean(config.auto_remove_rate_limited_accounts), | |
| 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 || ""), | |
| }, | |
| 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), | |
| register: Boolean(backup.include?.register ?? 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<string>(); | |
| 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; | |
| } | |
| function normalizeRegisterHeroSmsForSave(heroSms: RegisterConfig["hero_sms"]): RegisterConfig["hero_sms"] { | |
| const countryBlacklist = Array.isArray(heroSms.country_blacklist) && heroSms.country_blacklist.length > 0 | |
| ? heroSms.country_blacklist.map((item) => Number(item)).filter((item) => item > 0) | |
| : DEFAULT_HERO_SMS_COUNTRY_BLACKLIST; | |
| const blacklist = Array.from(new Set([...DEFAULT_HERO_SMS_COUNTRY_BLACKLIST, ...countryBlacklist])); | |
| const countryPool = Array.isArray(heroSms.country_pool) && heroSms.country_pool.length > 0 | |
| ? heroSms.country_pool.map((item) => Number(item)).filter((item) => item > 0) | |
| : DEFAULT_HERO_SMS_COUNTRY_POOL; | |
| const filteredCountryPool = Array.from(new Set(countryPool)).filter((item) => !blacklist.includes(item)); | |
| const finalCountryPool = filteredCountryPool.length > 0 ? filteredCountryPool : DEFAULT_HERO_SMS_COUNTRY_POOL.filter((item) => !blacklist.includes(item)); | |
| const maxPriceUsd = Math.max(0.001, Number(heroSms.max_price_usd) || 0.1); | |
| const minPriceUsd = Math.min(maxPriceUsd, Math.max(0, Number(heroSms.min_price_usd) || 0.045)); | |
| return { | |
| ...heroSms, | |
| service: "dr", | |
| country: finalCountryPool[0] || 6, | |
| country_pool: finalCountryPool, | |
| country_blacklist: blacklist, | |
| operator: "any", | |
| wait_timeout: Math.min(45, Math.max(1, Number(heroSms.wait_timeout) || 45)), | |
| poll_interval: Math.min(5, Math.max(1, Number(heroSms.poll_interval) || 5)), | |
| reuse_activation_id: "", | |
| reuse_phone: "", | |
| auto_buy: true, | |
| min_price_usd: minPriceUsd, | |
| max_price_usd: maxPriceUsd, | |
| cancel_on_send_fail: true, | |
| }; | |
| } | |
| type SettingsStore = { | |
| config: SettingsConfig | null; | |
| isLoadingConfig: boolean; | |
| isSavingConfig: boolean; | |
| backups: BackupItem[]; | |
| backupState: BackupState | null; | |
| isLoadingBackups: boolean; | |
| isRunningBackup: boolean; | |
| deletingBackupKey: string | null; | |
| isTestingBackup: boolean; | |
| registerConfig: RegisterConfig | null; | |
| isLoadingRegister: boolean; | |
| isSavingRegister: 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<void>; | |
| loadConfig: () => Promise<void>; | |
| saveConfig: () => Promise<boolean>; | |
| loadBackups: (silent?: boolean) => Promise<void>; | |
| runBackup: () => Promise<void>; | |
| removeBackup: (key: string) => Promise<void>; | |
| testBackup: () => Promise<void>; | |
| setRefreshAccountIntervalMinute: (value: string) => void; | |
| setImageRetentionDays: (value: string) => void; | |
| setImagePollTimeoutSecs: (value: string) => void; | |
| setImageAccountConcurrency: (value: string) => void; | |
| setAutoRemoveInvalidAccounts: (value: boolean) => void; | |
| setAutoRemoveRateLimitedAccounts: (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; | |
| setBackupField: (key: keyof BackupSettings, value: string | boolean) => void; | |
| setBackupInclude: (key: keyof BackupSettings["include"], value: boolean) => void; | |
| loadRegister: (silent?: boolean) => Promise<void>; | |
| setRegisterConfig: (config: RegisterConfig) => void; | |
| setRegisterProxy: (value: string) => void; | |
| setRegisterTotal: (value: string) => void; | |
| setRegisterThreads: (value: string) => void; | |
| setRegisterMode: (value: "total" | "quota" | "available") => void; | |
| setRegisterTargetQuota: (value: string) => void; | |
| setRegisterTargetAvailable: (value: string) => void; | |
| setRegisterCheckInterval: (value: string) => void; | |
| setRegisterMailField: (key: "request_timeout" | "wait_timeout" | "wait_interval", value: string) => void; | |
| setRegisterHeroSmsField: (key: keyof RegisterConfig["hero_sms"], value: string | boolean) => void; | |
| addRegisterProvider: () => void; | |
| updateRegisterProvider: (index: number, updates: Record<string, unknown>) => void; | |
| deleteRegisterProvider: (index: number) => void; | |
| saveRegister: () => Promise<void>; | |
| toggleRegister: () => Promise<void>; | |
| startCodexRegister: () => Promise<void>; | |
| resetRegister: () => Promise<void>; | |
| loadPools: (silent?: boolean) => Promise<void>; | |
| 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<void>; | |
| deletePool: (pool: CPAPool) => Promise<void>; | |
| browseFiles: (pool: CPAPool) => Promise<void>; | |
| 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<void>; | |
| }; | |
| export const useSettingsStore = create<SettingsStore>((set, get) => ({ | |
| config: null, | |
| isLoadingConfig: true, | |
| isSavingConfig: false, | |
| backups: [], | |
| backupState: null, | |
| isLoadingBackups: true, | |
| isRunningBackup: false, | |
| deletingBackupKey: null, | |
| isTestingBackup: false, | |
| registerConfig: null, | |
| isLoadingRegister: true, | |
| isSavingRegister: 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), | |
| auto_remove_invalid_accounts: Boolean(config.auto_remove_invalid_accounts), | |
| auto_remove_rate_limited_accounts: Boolean(config.auto_remove_rate_limited_accounts), | |
| 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(), | |
| }, | |
| 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), | |
| }); | |
| 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 } } : {}); | |
| }, | |
| 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 } } : {}); | |
| }, | |
| 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 } } } : {}); | |
| }, | |
| 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 }); | |
| } | |
| }, | |
| loadRegister: async (silent = false) => { | |
| if (!silent) set({ isLoadingRegister: true }); | |
| try { | |
| const data = await fetchRegisterConfig(); | |
| set({ registerConfig: data.register }); | |
| } catch (error) { | |
| if (!silent) toast.error(error instanceof Error ? error.message : "加载注册配置失败"); | |
| } finally { | |
| if (!silent) set({ isLoadingRegister: false }); | |
| } | |
| }, | |
| setRegisterConfig: (config) => { | |
| set({ registerConfig: config, isLoadingRegister: false }); | |
| }, | |
| setRegisterProxy: (value) => { | |
| set((state) => state.registerConfig ? { registerConfig: { ...state.registerConfig, proxy: value } } : {}); | |
| }, | |
| setRegisterTotal: (value) => { | |
| set((state) => state.registerConfig ? { registerConfig: { ...state.registerConfig, total: Number(value) || 0 } } : {}); | |
| }, | |
| setRegisterThreads: (value) => { | |
| set((state) => state.registerConfig ? { registerConfig: { ...state.registerConfig, threads: Number(value) || 0 } } : {}); | |
| }, | |
| setRegisterMode: (value) => { | |
| set((state) => state.registerConfig ? { registerConfig: { ...state.registerConfig, mode: value } } : {}); | |
| }, | |
| setRegisterTargetQuota: (value) => { | |
| set((state) => state.registerConfig ? { registerConfig: { ...state.registerConfig, target_quota: Number(value) || 0 } } : {}); | |
| }, | |
| setRegisterTargetAvailable: (value) => { | |
| set((state) => state.registerConfig ? { registerConfig: { ...state.registerConfig, target_available: Number(value) || 0 } } : {}); | |
| }, | |
| setRegisterCheckInterval: (value) => { | |
| set((state) => state.registerConfig ? { registerConfig: { ...state.registerConfig, check_interval: Number(value) || 0 } } : {}); | |
| }, | |
| setRegisterMailField: (key, value) => { | |
| set((state) => state.registerConfig ? { | |
| registerConfig: { | |
| ...state.registerConfig, | |
| mail: { ...state.registerConfig.mail, [key]: Number(value) || 0 }, | |
| }, | |
| } : {}); | |
| }, | |
| setRegisterHeroSmsField: (key, value) => { | |
| set((state) => { | |
| if (!state.registerConfig) return {}; | |
| const numericFields = new Set(["country", "wait_timeout", "poll_interval"]); | |
| return { | |
| registerConfig: { | |
| ...state.registerConfig, | |
| hero_sms: { | |
| ...state.registerConfig.hero_sms, | |
| [key]: numericFields.has(String(key)) ? Number(value) || 0 : value, | |
| }, | |
| }, | |
| }; | |
| }); | |
| }, | |
| addRegisterProvider: () => { | |
| set((state) => state.registerConfig ? { | |
| registerConfig: { | |
| ...state.registerConfig, | |
| mail: { | |
| ...state.registerConfig.mail, | |
| providers: [ | |
| ...(state.registerConfig.mail.providers || []), | |
| { enable: true, type: "tempmail_lol", api_key: "", domain: [] }, | |
| ], | |
| }, | |
| }, | |
| } : {}); | |
| }, | |
| updateRegisterProvider: (index, updates) => { | |
| set((state) => { | |
| if (!state.registerConfig) return {}; | |
| const providers = [...(state.registerConfig.mail.providers || [])]; | |
| providers[index] = { ...(providers[index] || {}), ...updates }; | |
| return { registerConfig: { ...state.registerConfig, mail: { ...state.registerConfig.mail, providers } } }; | |
| }); | |
| }, | |
| deleteRegisterProvider: (index) => { | |
| set((state) => state.registerConfig ? { | |
| registerConfig: { | |
| ...state.registerConfig, | |
| mail: { | |
| ...state.registerConfig.mail, | |
| providers: (state.registerConfig.mail.providers || []).filter((_, itemIndex) => itemIndex !== index), | |
| }, | |
| }, | |
| } : {}); | |
| }, | |
| saveRegister: async () => { | |
| const { registerConfig } = get(); | |
| if (!registerConfig) return; | |
| try { | |
| set({ isSavingRegister: true }); | |
| const data = await updateRegisterConfig({ | |
| mail: registerConfig.mail, | |
| hero_sms: normalizeRegisterHeroSmsForSave(registerConfig.hero_sms), | |
| proxy: registerConfig.proxy.trim(), | |
| total: Math.max(1, Number(registerConfig.total) || 1), | |
| threads: Math.max(1, Number(registerConfig.threads) || 1), | |
| mode: registerConfig.mode, | |
| target_quota: Math.max(1, Number(registerConfig.target_quota) || 1), | |
| target_available: Math.max(1, Number(registerConfig.target_available) || 1), | |
| check_interval: Math.max(1, Number(registerConfig.check_interval) || 5), | |
| }); | |
| set({ registerConfig: data.register }); | |
| toast.success("注册配置已保存"); | |
| } catch (error) { | |
| toast.error(error instanceof Error ? error.message : "保存注册配置失败"); | |
| } finally { | |
| set({ isSavingRegister: false }); | |
| } | |
| }, | |
| toggleRegister: async () => { | |
| const { registerConfig } = get(); | |
| if (!registerConfig) return; | |
| set({ isSavingRegister: true }); | |
| try { | |
| if (!registerConfig.enabled) { | |
| await updateRegisterConfig({ | |
| mail: registerConfig.mail, | |
| hero_sms: normalizeRegisterHeroSmsForSave(registerConfig.hero_sms), | |
| proxy: registerConfig.proxy.trim(), | |
| total: Math.max(1, Number(registerConfig.total) || 1), | |
| threads: Math.max(1, Number(registerConfig.threads) || 1), | |
| mode: registerConfig.mode, | |
| target_quota: Math.max(1, Number(registerConfig.target_quota) || 1), | |
| target_available: Math.max(1, Number(registerConfig.target_available) || 1), | |
| check_interval: Math.max(1, Number(registerConfig.check_interval) || 5), | |
| }); | |
| } | |
| const data = registerConfig.enabled ? await stopRegister() : await startRegister(); | |
| set({ registerConfig: data.register }); | |
| toast.success(registerConfig.enabled ? "注册任务已停止" : "注册任务已启动"); | |
| } catch (error) { | |
| toast.error(error instanceof Error ? error.message : "切换注册状态失败"); | |
| } finally { | |
| set({ isSavingRegister: false }); | |
| } | |
| }, | |
| startCodexRegister: async () => { | |
| const { registerConfig } = get(); | |
| if (!registerConfig) return; | |
| set({ isSavingRegister: true }); | |
| try { | |
| await updateRegisterConfig({ | |
| mail: registerConfig.mail, | |
| hero_sms: normalizeRegisterHeroSmsForSave(registerConfig.hero_sms), | |
| proxy: registerConfig.proxy.trim(), | |
| total: Math.max(1, Number(registerConfig.total) || 1), | |
| threads: Math.max(1, Number(registerConfig.threads) || 1), | |
| mode: registerConfig.mode, | |
| target_quota: Math.max(1, Number(registerConfig.target_quota) || 1), | |
| target_available: Math.max(1, Number(registerConfig.target_available) || 1), | |
| check_interval: Math.max(1, Number(registerConfig.check_interval) || 5), | |
| }); | |
| const data = await startCodexRegisterApi(); | |
| set({ registerConfig: data.register }); | |
| toast.success("Codex CPA 注册任务已启动"); | |
| } catch (error) { | |
| toast.error(error instanceof Error ? error.message : "启动 Codex CPA 注册失败"); | |
| } finally { | |
| set({ isSavingRegister: false }); | |
| } | |
| }, | |
| resetRegister: async () => { | |
| set({ isSavingRegister: true }); | |
| try { | |
| const data = await resetRegisterApi(); | |
| set({ registerConfig: data.register }); | |
| toast.success("注册统计已重置"); | |
| } catch (error) { | |
| toast.error(error instanceof Error ? error.message : "重置注册统计失败"); | |
| } finally { | |
| set({ isSavingRegister: 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 }); | |
| } | |
| }, | |
| })); | |