"use client"; import { create } from "zustand"; import { toast } from "sonner"; import { publishBrandingUpdate } from "@/lib/branding"; import { createCPAPool, deleteBackup, deleteCPAPool, fetchCPAPoolFiles, fetchCPAPools, fetchBackups, fetchRegisterConfig, resetRegister as resetRegisterApi, fetchSettingsConfig, runBackupNow, syncImageStorage, startRegister, startCPAImport, stopRegister, testBackupConnection, testImageStorageConnection, updateCPAPool, updateRegisterConfig, updateSettingsConfig, type BackupItem, type BackupSettings, type BackupState, type Announcement, type CPAPool, type CPARemoteFile, type FreeAccountCleanupSettings, type ImageModelDailyLimit, type ImageStorageMode, type ImageStorageSettings, type NavigationItem, type PoolPricingConfig, type ProxyRuntimeClearanceMode, type ProxyRuntimeEgressMode, type ProxyRuntimeSettings, type RedeemCodeCleanupSettings, type RegisterConfig, type RegistrationMethod, type SettingsConfig, } from "@/lib/api"; import { DEFAULT_CHECKIN_CONTENT, type CheckinContent, type ErrorMessageRule } from "@/lib/api"; export const PAGE_SIZE_OPTIONS = ["50", "100", "200"] as const; export type PageSizeOption = (typeof PAGE_SIZE_OPTIONS)[number]; /** * 轻量整形报错文案规则:**保留空规则**,只做类型归一和 id 去重。 * * 编辑态必须用它而不是 normalizeErrorMessageRules —— 刚点「添加规则」时 * 关键词和文案都是空的,若在这里做「缺字段就丢弃」的清洗,新增的那一行 * 会被立刻删掉,表现为「点添加规则没反应」。 */ function coerceErrorMessageRules(value: unknown): ErrorMessageRule[] { if (!Array.isArray(value)) { return []; } const rules: ErrorMessageRule[] = []; const seen = new Set(); value.forEach((raw, index) => { if (!raw || typeof raw !== "object") { return; } const item = raw as Partial; const keywords = Array.isArray(item.keywords) ? item.keywords.map((keyword) => String(keyword ?? "").trim()).filter(Boolean) : String(item.keywords ?? "") .split(/[\n,,、]/) .map((keyword) => keyword.trim()) .filter(Boolean); let id = String(item.id ?? "").trim() || `rule-${index + 1}`; let suffix = 1; while (seen.has(id)) { suffix += 1; id = `${id}-${suffix}`; } seen.add(id); rules.push({ id, keywords, display: String(item.display ?? ""), enabled: item.enabled !== false, note: String(item.note ?? ""), }); }); return rules; } /** * 严格归一化报错文案规则:丢掉缺关键词或缺文案的无效项,并保证 id 不重复。 * 与后端 services/error_message_service.py 的 normalize_rules 行为保持一致, * **只用于提交给后端的那一刻**(保存路径),不要用在编辑态 setter 上。 */ function normalizeErrorMessageRules(value: unknown): ErrorMessageRule[] { return coerceErrorMessageRules(value).filter( (rule) => rule.keywords.length > 0 && rule.display.trim().length > 0, ); } const DEFAULT_FREE_ACCOUNT_CLEANUP: FreeAccountCleanupSettings = { enabled: false, interval_minutes: 10, failure_threshold: 3, register_precheck_enabled: true, action: "mark_abnormal", }; const DEFAULT_REDEEM_CODE_CLEANUP: RedeemCodeCleanupSettings = { enabled: false, interval_minutes: 1440, delete_expired: true, delete_disabled: false, archive_redeemed: false, redeemed_older_than_days: 90, limit: 1000, }; const DEFAULT_POOL_PRICING: PoolPricingConfig = { enabled: false, timezone: "Asia/Shanghai", base_unit_price: 1, calendar_refresh_hours: 24, rules: [], }; const DEFAULT_LOCAL_REVIEW = { enabled: true, strong_short_terms: [ "全裸", "裸体", "裸露", "露阴", "色情", "淫秽", "性交", "自慰", "手淫", "口交", "肛交", "群交", "内射", "颜射", "潮吹", "露点", "私处", "下体", "阴茎", "阴道", "乳头", "迷奸", "强奸", "性侵", "猥亵", "恋童", "兽交", "兽奸", "乱伦", "性虐", "卖淫", "嫖娼", "炸弹", "爆炸", "枪支", "枪械", "毒品", "制毒", "冰毒", "大麻", "自杀", "自残", "虐杀", "肢解", "斩首", "血腥", "绑架", "劫持", "偷拍", "偷窥", "伪造", "换脸", ], categories: [ { name: "明确违规", mode: "block" as const, terms: ["全裸", "裸体", "裸露", "露阴", "色情", "淫秽", "性交", "自慰", "手淫", "口交", "肛交", "群交", "内射", "颜射", "潮吹", "露点", "私处", "下体", "阴茎", "阴道", "乳头", "迷奸", "强奸", "性侵", "猥亵", "恋童", "兽交", "兽奸", "乱伦", "性虐", "卖淫", "嫖娼", "炸弹", "爆炸", "枪支", "枪械", "毒品", "制毒", "冰毒", "大麻", "自杀", "自残", "虐杀", "肢解", "斩首", "血腥", "绑架", "劫持", "偷拍", "偷窥", "伪造"] }, { name: "年龄幼态", mode: "context" as const, terms: ["幼态", "稚嫩", "学生", "青涩", "未成年", "儿童", "幼童", "幼女", "萝莉", "低龄", "小学生", "初中生", "童颜", "孩童", "少儿"] }, { name: "性感氛围", mode: "context" as const, terms: ["纯欲", "撩人", "性感", "魅惑", "妩媚", "清冷欲", "性行为"] }, { name: "贴身肌肤", mode: "context" as const, terms: ["贴身", "透视", "肌肤特写", "贴身穿搭", "裸露", "裸体", "私密部位"] }, { name: "私房私密", mode: "context" as const, terms: ["私房", "私密", "私密写真", "居家慵懒"] }, { name: "真人写实", mode: "context" as const, terms: ["真人写实", "超真实人像", "原生相机", "无修原图", "近距离躯体特写"] }, { name: "透明服装", mode: "context" as const, terms: ["透明", "超薄", "镂空", "薄纱", "轻纱", "透明衣物"] }, { name: "暴露描述", mode: "context" as const, terms: ["全身无遮挡", "无遮挡", "毫无遮挡", "肌肤全开", "完全暴露", "乳头", "下体"] }, { name: "人体损伤", mode: "context" as const, terms: ["残破", "撕裂", "溃烂", "残肢", "骸骨", "骨骼", "血肉"] }, { name: "人体对象", mode: "context" as const, terms: ["人体", "躯体", "身体", "真身"] }, { name: "灵异恐怖", mode: "context" as const, terms: ["丧尸", "灵异", "诡异", "鬼魂", "怨灵", "厉鬼"] }, { name: "血腥元素", mode: "context" as const, terms: ["血色", "残肢", "骨骼", "血肉", "流血", "血泊"] }, { name: "实景写实", mode: "context" as const, terms: ["实景", "写实", "真人", "真实画面"] }, { name: "修改恶搞", mode: "context" as const, terms: ["修改", "改编", "恶搞", "篡改", "丑化"] }, { name: "公共象征", mode: "context" as const, terms: ["国旗", "国徽", "伟人", "历史事件", "党政机关"] }, { name: "制作行为", mode: "context" as const, terms: ["自制", "手工", "制作", "教程", "步骤", "配方", "改装"] }, { name: "武器毒品", mode: "context" as const, terms: ["炸弹", "枪支", "枪械", "爆炸物", "制毒原料", "毒品", "火药"] }, { name: "伪造替换", mode: "context" as const, terms: ["替换", "伪造", "仿制", "制作"] }, { name: "证件文件", mode: "context" as const, terms: ["证件", "身份证", "护照", "公章", "官方文件", "银行卡", "票据"] }, ], combination_rules: [ { id: "未成年人性化", left_categories: ["年龄幼态"], right_categories: ["性感氛围", "贴身肌肤"] }, { id: "私房真人", left_categories: ["私房私密"], right_categories: ["真人写实"] }, { id: "透明暴露", left_categories: ["透明服装"], right_categories: ["暴露描述"] }, { id: "写实血腥", left_categories: ["人体损伤", "血腥元素"], right_categories: ["人体对象", "实景写实"] }, { id: "灵异写实", left_categories: ["灵异恐怖"], right_categories: ["真人写实", "实景写实"] }, { id: "公共象征恶搞", left_categories: ["修改恶搞"], right_categories: ["公共象征"] }, { id: "武器毒品制作", left_categories: ["制作行为"], right_categories: ["武器毒品"] }, { id: "证件伪造", left_categories: ["伪造替换"], right_categories: ["证件文件"] }, ], negation_aware: true, combination_window: 120, combination_sentence_aware: true, exclusion_aware: true, benign_phrases: [], multilingual_aware: true, deobfuscate: true, }; function normalizeLocalReview(value: SettingsConfig["local_review"], legacyWords: string[] = []): NonNullable { const excludedTerms = new Set(["换脸", "丝袜", "裤袜", "连裤袜"]); const cleanLegacyTerm = (term: unknown) => String(term || "").trim().replace(/^[\s,,、;;。]+|[\s,,、;;。]+$/g, ""); const ruleNames: Record = { "minor-sexualization": "未成年人性化", "private-real-person": "私房真人", "explicit-exposure": "透明暴露", "graphic-violence": "写实血腥", "supernatural-realism": "灵异写实", "political-defacement": "公共象征恶搞", "weapon-or-drug-instructions": "武器毒品制作", "document-forgery": "证件伪造", }; const configuredCategories = Array.isArray(value?.categories) ? value.categories.filter((category) => String(category?.name || "") !== "原短词(待分类)").map((category, index) => ({ name: String(category?.name || `分类${index + 1}`), mode: category?.mode === "block" ? "block" as const : "context" as const, terms: Array.isArray(category?.terms) ? category.terms .map(cleanLegacyTerm) .filter((term) => term && !excludedTerms.has(term)) .filter((term) => String(category?.name || "") !== "原敏感词" || Array.from(term).length > 2) : [], })) : DEFAULT_LOCAL_REVIEW.categories; let categories = configuredCategories; if (legacyWords.length && !configuredCategories.some((category) => category.name === "原敏感词")) { const longTerms = legacyWords.map(cleanLegacyTerm).filter((term) => Array.from(term).length > 2 && !excludedTerms.has(term)); categories = [ ...(longTerms.length ? [{ name: "原敏感词", mode: "block" as const, terms: longTerms }] : []), ...configuredCategories, ]; } const legacyStrongTerms = Array.isArray(value?.strong_short_terms) ? value.strong_short_terms.map(String).filter(Boolean) : []; if (legacyStrongTerms.length && !categories.some((category) => category.name === "原明确违规短词" || category.name === "明确违规")) { categories = [{ name: "原明确违规短词", mode: "block" as const, terms: legacyStrongTerms }, ...categories]; } const rawWindow = Number(value?.combination_window); return { enabled: value?.enabled !== false, strong_short_terms: [], categories, combination_rules: Array.isArray(value?.combination_rules) ? value.combination_rules.map((rule, index) => ({ id: ruleNames[String(rule?.id || "")] || String(rule?.id || `自定义规则${index + 1}`), left_categories: Array.isArray(rule?.left_categories) ? rule.left_categories.map(String) : [], right_categories: Array.isArray(rule?.right_categories) ? rule.right_categories.map(String) : [], left_terms: Array.isArray(rule?.left_terms) ? rule.left_terms.map(String) : undefined, right_terms: Array.isArray(rule?.right_terms) ? rule.right_terms.map(String) : undefined, })) : DEFAULT_LOCAL_REVIEW.combination_rules, negation_aware: value?.negation_aware !== false, combination_window: Number.isFinite(rawWindow) && rawWindow >= 0 ? Math.round(rawWindow) : DEFAULT_LOCAL_REVIEW.combination_window, combination_sentence_aware: value?.combination_sentence_aware !== false, exclusion_aware: value?.exclusion_aware !== false, benign_phrases: Array.isArray(value?.benign_phrases) ? value.benign_phrases.map((phrase) => String(phrase || "").trim()).filter(Boolean) : [], multilingual_aware: value?.multilingual_aware !== false, deobfuscate: value?.deobfuscate !== false, }; } function normalizePoolPricing(value: unknown): PoolPricingConfig { const source = typeof value === "object" && value !== null ? value as Partial : {}; return { ...DEFAULT_POOL_PRICING, enabled: Boolean(source.enabled), calendar_refresh_hours: Math.max(1, Math.min(168, Number(source.calendar_refresh_hours) || 24)), rules: Array.isArray(source.rules) ? source.rules.map((rule, index) => ({ id: String(rule?.id || `pricing-${index + 1}`), name: String(rule?.name || `活动规则 ${index + 1}`), target_type: rule?.target_type === "third_party_group" ? "third_party_group" : "pool", third_party_group_id: String(rule?.third_party_group_id || ""), enabled: rule?.enabled !== false, pay_percentage: Number(rule?.pay_percentage ?? 100), date_type: rule?.date_type || "specific_dates", holiday_names: Array.isArray(rule?.holiday_names) ? rule.holiday_names.map(String) : [], weekend_basis: rule?.weekend_basis === "official" ? "official" : "natural", dates: Array.isArray(rule?.dates) ? rule.dates.map(String) : [], start_date: String(rule?.start_date || ""), end_date: String(rule?.end_date || ""), all_day: rule?.all_day !== false, start_time: String(rule?.start_time || "00:00"), end_time: String(rule?.end_time || "00:00"), })) : [], }; } 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 Chrome/136.0.0.0 Safari/537.36", browser: "chrome", flaresolverr_url: "http://flaresolverr:8191", timeout_sec: 60, refresh_interval: 3600, warm_up_on_start: false, }, }; 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 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(Number).filter((item) => Number.isInteger(item) && item >= 100 && item <= 599) : []; return { enabled: Boolean(source.enabled), egress_mode: source.egress_mode === "single_proxy" ? "single_proxy" : "direct", 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 ? 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 || "http://flaresolverr:8191"), timeout_sec: Math.max(1, Number(clearanceSource.timeout_sec) || 60), refresh_interval: Math.max(60, Number(clearanceSource.refresh_interval) || 3600), warm_up_on_start: Boolean(clearanceSource.warm_up_on_start), }, }; } function normalizeRegistrationMethod(value: unknown): RegistrationMethod { return value === "platform_oauth" ? "platform_oauth" : "chatgpt_session"; } function normalizeFreeAccountCleanup(value: unknown): FreeAccountCleanupSettings { const source = typeof value === "object" && value !== null ? value as Partial : {}; const action = source.action === "delete" ? "delete" : "mark_abnormal"; return { enabled: Boolean(source.enabled), interval_minutes: Math.max(1, Number(source.interval_minutes) || Number(DEFAULT_FREE_ACCOUNT_CLEANUP.interval_minutes)), failure_threshold: Math.max(1, Number(source.failure_threshold) || Number(DEFAULT_FREE_ACCOUNT_CLEANUP.failure_threshold)), register_precheck_enabled: source.register_precheck_enabled !== false, action, }; } function normalizeRedeemCodeCleanup(value: unknown): RedeemCodeCleanupSettings { const source = typeof value === "object" && value !== null ? value as Partial : {}; return { enabled: Boolean(source.enabled), interval_minutes: Math.max(1, Number(source.interval_minutes) || Number(DEFAULT_REDEEM_CODE_CLEANUP.interval_minutes)), delete_expired: source.delete_expired !== false, delete_disabled: Boolean(source.delete_disabled), archive_redeemed: Boolean(source.archive_redeemed), redeemed_older_than_days: Math.max(1, Number(source.redeemed_older_than_days) || Number(DEFAULT_REDEEM_CODE_CLEANUP.redeemed_older_than_days)), limit: Math.max(1, Math.min(20000, Number(source.limit) || Number(DEFAULT_REDEEM_CODE_CLEANUP.limit))), }; } function normalizeInternalImageModelDailyLimits(value: unknown): ImageModelDailyLimit[] { if (!Array.isArray(value)) { return []; } const seen = new Set(); const rows: ImageModelDailyLimit[] = []; for (const item of value) { if (typeof item !== "object" || item === null) { continue; } const source = item as Partial; const modelId = String(source.model_id || "").trim(); if (!modelId || seen.has(modelId)) { continue; } seen.add(modelId); rows.push({ model_id: modelId, daily_limit: Math.max(0, Math.trunc(Number(source.daily_limit) || 0)), limit_scope: source.limit_scope === "total" ? "total" : "per_user", used: Math.max(0, Math.trunc(Number(source.used) || 0)), reserved: Math.max(0, Math.trunc(Number(source.reserved) || 0)), remaining: Math.max(0, Math.trunc(Number(source.remaining) || 0)), date: String(source.date || ""), updated_at: String(source.updated_at || ""), }); } return rows; } function normalizeNavigationItems(value: unknown): NavigationItem[] { if (!Array.isArray(value)) return []; return value.slice(0, 30).flatMap((item, index) => { if (typeof item !== "object" || item === null) return []; const source = item as Partial; const title = String(source.title || "").trim().slice(0, 40); const url = String(source.url || "").trim().slice(0, 1000); const children = Array.isArray(source.children) ? normalizeNavigationItems(source.children).map((child) => ({ ...child, children: [] })) : []; if (!title || (!url && children.length === 0)) return []; return [{ id: String(source.id || `navigation-${index + 1}`).trim().slice(0, 64), title, url, new_window: Boolean(source.new_window), append_api_credentials: Boolean(source.append_api_credentials), api_key_param: source.api_key_param === undefined ? "apiKey" : String(source.api_key_param).trim().slice(0, 64), base_url_param: source.base_url_param === undefined ? "baseUrl" : String(source.base_url_param).trim().slice(0, 64), enabled: source.enabled !== false, children, }]; }); } 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, 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), thread_pool_tokens: Number(config.thread_pool_tokens || 40), image_retention_days: Number(config.image_retention_days || 30), image_poll_timeout_secs: Number(config.image_poll_timeout_secs || 120), image_poll_max_attempts: Math.max(1, Math.min(10, Number(config.image_poll_max_attempts) || 2)), image_settle_enabled: config.image_settle_enabled !== false, image_settle_secs: Math.max(0, Number(config.image_settle_secs ?? 2) || 2), image_timeout_retry_secs: Number(config.image_timeout_retry_secs || 30), image_account_concurrency: Number(config.image_account_concurrency || 3), image_memory_button_enabled: config.image_memory_button_enabled !== false, image_parallel_generation: config.image_parallel_generation !== false, image_prompt_aspect_hint: config.image_prompt_aspect_hint !== false, image_remove_conversation_after_result: Boolean(config.image_remove_conversation_after_result), image_remove_conversation_always: Boolean(config.image_remove_conversation_always), image_share_evidence_enabled: config.image_share_evidence_enabled === true, image_share_retention_hours: Math.max(1, Math.min(720, Number(config.image_share_retention_hours ?? 12) || 12)), default_upstream_model_name: String(config.default_upstream_model_name || "gpt-5-5"), default_thinking_effort: config.default_thinking_effort === "standard" || config.default_thinking_effort === "extended" || config.default_thinking_effort === "max" ? config.default_thinking_effort : "auto", normal_user_default_credits: Number(config.normal_user_default_credits || 0), normal_user_daily_credits: Number(config.normal_user_daily_credits || 0), normal_user_pool_pricing: normalizePoolPricing(config.normal_user_pool_pricing), normal_user_third_party_image_pricing: normalizePoolPricing(config.normal_user_third_party_image_pricing), checkin_daily_credits: Number(config.checkin_daily_credits ?? config.normal_user_daily_credits ?? 0), checkin_credit_valid_days: Math.max(0, Number(config.checkin_credit_valid_days) || 0), checkin_content: { ...DEFAULT_CHECKIN_CONTENT, ...(config.checkin_content || {}) }, error_message_rules: coerceErrorMessageRules(config.error_message_rules), error_message_fallback_enabled: config.error_message_fallback_enabled === true, error_message_ai_model: String(config.error_message_ai_model || ""), internal_image_model_daily_limits: normalizeInternalImageModelDailyLimits(config.internal_image_model_daily_limits), wechat_login_enabled: config.wechat_login_enabled !== false, wechat_auto_register_enabled: config.wechat_auto_register_enabled !== false, internal_user_login_session_duration_hours: Math.max(0, Number(config.internal_user_login_session_duration_hours) || 0), normal_user_login_session_duration_hours: Math.max(0, Number(config.normal_user_login_session_duration_hours ?? config.login_session_duration_hours ?? 72)), site_title: String(config.site_title || "ChatGPT 号池管理"), site_logo_url: String(config.site_logo_url || ""), site_header_title: String(config.site_header_title || "chatgpt2api"), site_external_link_label: String(config.site_external_link_label ?? "GitHub"), site_external_link_url: String(config.site_external_link_url ?? "https://github.com/basketikun/chatgpt2api"), site_external_link_icon_url: String(config.site_external_link_icon_url || ""), site_external_link_icon_svg: String(config.site_external_link_icon_svg || ""), shop_purchase_url: String(config.shop_purchase_url || ""), announcements: normalizeAnnouncements(config.announcements), normal_user_navigation: normalizeNavigationItems(config.normal_user_navigation), internal_user_navigation: normalizeNavigationItems(config.internal_user_navigation), admin_user_navigation: normalizeNavigationItems(config.admin_user_navigation), auto_remove_invalid_accounts: Boolean(config.auto_remove_invalid_accounts), auto_remove_rate_limited_accounts: Boolean(config.auto_remove_rate_limited_accounts), free_account_cleanup: normalizeFreeAccountCleanup(config.free_account_cleanup), redeem_code_cleanup: normalizeRedeemCodeCleanup(config.redeem_code_cleanup), cleanup_shared_conversations: Boolean(config.cleanup_shared_conversations), 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 : "", image_proxy_base_url: typeof config.image_proxy_base_url === "string" ? config.image_proxy_base_url : "", global_system_prompt: String(config.global_system_prompt || ""), prompt_optimizer_system_prompt: String(config.prompt_optimizer_system_prompt || ""), prompt_optimizer_provider: config.prompt_optimizer_provider === "external" ? "external" : "internal", prompt_optimizer_base_url: String(config.prompt_optimizer_base_url || ""), prompt_optimizer_api_key: String(config.prompt_optimizer_api_key || ""), prompt_optimizer_model: String(config.prompt_optimizer_model || "gpt-5-5"), reverse_prompt_system_prompt: String(config.reverse_prompt_system_prompt || ""), reverse_prompt_provider: config.reverse_prompt_provider === "external" ? "external" : "internal", reverse_prompt_base_url: String(config.reverse_prompt_base_url || ""), reverse_prompt_api_key: String(config.reverse_prompt_api_key || ""), reverse_prompt_model: String(config.reverse_prompt_model || "gpt-5-5"), product_visual_planner_system_prompt: String(config.product_visual_planner_system_prompt || ""), product_visual_agent_provider: config.product_visual_agent_provider === "external" ? "external" : "internal", product_visual_agent_base_url: String(config.product_visual_agent_base_url || ""), product_visual_agent_api_key: String(config.product_visual_agent_api_key || ""), product_visual_agent_model: String(config.product_visual_agent_model || "gpt-5-5"), sensitive_words: Array.isArray(config.sensitive_words) ? config.sensitive_words : [], local_review: normalizeLocalReview(config.local_review, 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 || ""), fail_open: Boolean(config.ai_review?.fail_open), }, 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: { infinite_canvas: { enabled: Boolean(config.third_party_apps?.infinite_canvas?.enabled), url: String(config.third_party_apps?.infinite_canvas?.url || ""), }, }, 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 normalizeAnnouncements(items: unknown): Announcement[] { if (!Array.isArray(items)) { return []; } return items .filter((item): item is Partial => typeof item === "object" && item !== null) .map((item, index) => { const type = item.type === "timeline" ? "timeline" : "notice"; const target = item.target === "internal" || item.target === "normal" || item.target === "all" ? item.target : "all"; return { id: String(item.id || `announcement-${index + 1}`), type, target, title: String(item.title || ""), content: String(item.content || ""), enabled: Boolean(item.enabled ?? true), created_at: String(item.created_at || ""), updated_at: String(item.updated_at || item.created_at || ""), }; }); } 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; 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; loadConfig: () => Promise; saveConfig: () => Promise; loadBackups: (silent?: boolean) => Promise; runBackup: () => Promise; removeBackup: (key: string) => Promise; testBackup: () => Promise; setRefreshAccountIntervalMinute: (value: string) => void; setThreadPoolTokens: (value: string) => void; setImageRetentionDays: (value: string) => void; setImagePollTimeoutSecs: (value: string) => void; setImagePollMaxAttempts: (value: string) => void; setImageSettleEnabled: (value: boolean) => void; setImageSettleSecs: (value: string) => void; setImageTimeoutRetrySecs: (value: string) => void; setImageAccountConcurrency: (value: string) => void; setImageMemoryButtonEnabled: (value: boolean) => void; setImageParallelGeneration: (value: boolean) => void; setImagePromptAspectHint: (value: boolean) => void; setImageRemoveConversationAfterResult: (value: boolean) => void; setImageRemoveConversationAlways: (value: boolean) => void; setImageShareEvidenceEnabled: (value: boolean) => void; setImageShareRetentionHours: (value: string) => void; setDefaultUpstreamModelName: (value: string) => void; setDefaultThinkingEffort: (value: "auto" | "standard" | "extended" | "max") => void; setNormalUserDefaultCredits: (value: string) => void; setNormalUserDailyCredits: (value: string) => void; setNormalUserPoolPricing: (value: PoolPricingConfig) => void; setNormalUserThirdPartyImagePricing: (value: PoolPricingConfig) => void; setCheckinDailyCredits: (value: string) => void; setCheckinRandomMin: (value: string) => void; setCheckinRandomMax: (value: string) => void; setCheckinInviteReward: (value: string) => void; setCheckinCreditValidDays: (value: string) => void; setCheckinAds: (value: Array<{ title?: string; image_url?: string; link_url?: string; enabled?: boolean; layout?: string }>) => void; setCheckinContent: (value: Partial) => void; setErrorMessageRules: (value: ErrorMessageRule[]) => void; setErrorMessageFallbackEnabled: (value: boolean) => void; setErrorMessageAiModel: (value: string) => void; setInternalImageModelDailyLimits: (value: ImageModelDailyLimit[]) => void; setWechatLoginEnabled: (value: boolean) => void; setWechatAutoRegisterEnabled: (value: boolean) => void; setInternalUserLoginSessionDurationHours: (value: string) => void; setNormalUserLoginSessionDurationHours: (value: string) => void; setSiteTitle: (value: string) => void; setSiteLogoUrl: (value: string) => void; setSiteHeaderTitle: (value: string) => void; setSiteExternalLinkLabel: (value: string) => void; setSiteExternalLinkUrl: (value: string) => void; setSiteExternalLinkIconUrl: (value: string) => void; setSiteExternalLinkIconSvg: (value: string) => void; setShopPurchaseUrl: (value: string) => void; setAnnouncements: (items: Announcement[]) => void; setNormalUserNavigation: (items: NavigationItem[]) => void; setInternalUserNavigation: (items: NavigationItem[]) => void; setAdminUserNavigation: (items: NavigationItem[]) => void; setAutoRemoveInvalidAccounts: (value: boolean) => void; setAutoRemoveRateLimitedAccounts: (value: boolean) => void; setFreeAccountCleanupField: (key: K, value: FreeAccountCleanupSettings[K]) => void; setRedeemCodeCleanupField: (key: K, value: RedeemCodeCleanupSettings[K]) => void; setCleanupSharedConversations: (value: boolean) => void; setLogLevel: (level: string, enabled: boolean) => void; setProxy: (value: string) => void; setBaseUrl: (value: string) => void; setImageProxyBaseUrl: (value: string) => void; setGlobalSystemPrompt: (value: string) => void; setPromptOptimizerSystemPrompt: (value: string) => void; setPromptOptimizerField: (key: "provider" | "base_url" | "api_key" | "model", value: string) => void; setReversePromptSystemPrompt: (value: string) => void; setReversePromptField: (key: "provider" | "base_url" | "api_key" | "model", value: string) => void; setProductVisualAgentField: (key: "provider" | "base_url" | "api_key" | "model", value: string) => void; setSensitiveWordsText: (value: string) => void; setLocalReviewField: (key: "enabled" | "strong_short_terms" | "categories" | "combination_rules" | "negation_aware" | "combination_window" | "combination_sentence_aware" | "exclusion_aware" | "benign_phrases" | "multilingual_aware" | "deobfuscate", value: boolean | number | string[] | NonNullable["categories"] | NonNullable["combination_rules"]) => void; setAIReviewField: (key: "enabled" | "base_url" | "api_key" | "model" | "prompt" | "fail_open", 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: "enabled" | "url", value: string | boolean) => void; testImageStorage: () => Promise; syncImagesToWebDAV: () => Promise; setBackupField: (key: keyof BackupSettings, value: string | boolean) => void; setBackupInclude: (key: keyof BackupSettings["include"], value: boolean) => void; loadRegister: (silent?: boolean) => Promise; setRegisterConfig: (config: RegisterConfig) => void; setRegisterProxy: (value: string) => void; setRegisterTotal: (value: string) => void; setRegisterThreads: (value: string) => void; setRegisterRetryAttempts: (value: string) => void; setRegistrationMethod: (value: RegistrationMethod) => 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; addRegisterProvider: () => void; updateRegisterProvider: (index: number, updates: Record) => void; deleteRegisterProvider: (index: number) => void; saveRegister: () => Promise; toggleRegister: () => Promise; resetRegister: () => Promise; 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, 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), thread_pool_tokens: Math.max(4, Math.min(512, Number(config.thread_pool_tokens) || 40)), 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_poll_max_attempts: Math.max(1, Math.min(10, Number(config.image_poll_max_attempts) || 2)), image_settle_enabled: config.image_settle_enabled !== false, image_settle_secs: Math.max(0, Number(config.image_settle_secs) || 2), image_account_concurrency: Math.max(1, Number(config.image_account_concurrency) || 3), image_memory_button_enabled: config.image_memory_button_enabled !== false, image_prompt_aspect_hint: config.image_prompt_aspect_hint !== false, normal_user_default_credits: Math.max(0, Number(config.normal_user_default_credits) || 0), normal_user_daily_credits: Math.max(0, Number(config.normal_user_daily_credits) || 0), normal_user_pool_pricing: normalizePoolPricing(config.normal_user_pool_pricing), normal_user_third_party_image_pricing: normalizePoolPricing(config.normal_user_third_party_image_pricing), checkin_daily_credits: Math.max(0, Number(config.checkin_daily_credits) || 0), checkin_credit_valid_days: Math.max(0, Number(config.checkin_credit_valid_days) || 0), checkin_content: { ...DEFAULT_CHECKIN_CONTENT, ...(config.checkin_content || {}) }, error_message_rules: normalizeErrorMessageRules(config.error_message_rules), error_message_fallback_enabled: config.error_message_fallback_enabled === true, error_message_ai_model: String(config.error_message_ai_model || "").trim(), internal_image_model_daily_limits: normalizeInternalImageModelDailyLimits(config.internal_image_model_daily_limits), wechat_login_enabled: config.wechat_login_enabled !== false, wechat_auto_register_enabled: config.wechat_auto_register_enabled !== false, internal_user_login_session_duration_hours: Math.max(0, Number(config.internal_user_login_session_duration_hours) || 0), normal_user_login_session_duration_hours: Math.max(0, Number(config.normal_user_login_session_duration_hours) || 0), site_title: String(config.site_title || "").trim(), site_logo_url: String(config.site_logo_url || "").trim(), site_header_title: String(config.site_header_title || "").trim(), site_external_link_label: String(config.site_external_link_label ?? "").trim(), site_external_link_url: String(config.site_external_link_url ?? "").trim(), site_external_link_icon_url: String(config.site_external_link_icon_url || "").trim(), site_external_link_icon_svg: String(config.site_external_link_icon_svg || "").trim(), shop_purchase_url: String(config.shop_purchase_url || "").trim(), announcements: normalizeAnnouncements(config.announcements).map((item) => ({ ...item, title: item.title.trim(), content: item.content.trim(), })).filter((item) => item.title || item.content), normal_user_navigation: normalizeNavigationItems(config.normal_user_navigation), internal_user_navigation: normalizeNavigationItems(config.internal_user_navigation), admin_user_navigation: normalizeNavigationItems(config.admin_user_navigation), auto_remove_invalid_accounts: Boolean(config.auto_remove_invalid_accounts), auto_remove_rate_limited_accounts: Boolean(config.auto_remove_rate_limited_accounts), free_account_cleanup: { ...normalizeFreeAccountCleanup(config.free_account_cleanup), interval_minutes: Math.max(1, Number(config.free_account_cleanup?.interval_minutes) || 10), failure_threshold: Math.max(1, Number(config.free_account_cleanup?.failure_threshold) || 3), }, redeem_code_cleanup: { ...normalizeRedeemCodeCleanup(config.redeem_code_cleanup), interval_minutes: Math.max(1, Number(config.redeem_code_cleanup?.interval_minutes) || 1440), redeemed_older_than_days: Math.max(1, Number(config.redeem_code_cleanup?.redeemed_older_than_days) || 90), limit: Math.max(1, Math.min(20000, Number(config.redeem_code_cleanup?.limit) || 1000)), }, cleanup_shared_conversations: Boolean(config.cleanup_shared_conversations), proxy: config.proxy.trim(), base_url: String(config.base_url || "").trim(), image_proxy_base_url: String(config.image_proxy_base_url || "").trim(), global_system_prompt: String(config.global_system_prompt || "").trim(), prompt_optimizer_system_prompt: String(config.prompt_optimizer_system_prompt || "").trim(), prompt_optimizer_provider: config.prompt_optimizer_provider === "external" ? "external" : "internal", prompt_optimizer_base_url: String(config.prompt_optimizer_base_url || "").trim(), prompt_optimizer_api_key: String(config.prompt_optimizer_api_key || "").trim(), prompt_optimizer_model: String(config.prompt_optimizer_model || "gpt-5-5").trim(), reverse_prompt_system_prompt: String(config.reverse_prompt_system_prompt || "").trim(), reverse_prompt_provider: config.reverse_prompt_provider === "external" ? "external" : "internal", reverse_prompt_base_url: String(config.reverse_prompt_base_url || "").trim(), reverse_prompt_api_key: String(config.reverse_prompt_api_key || "").trim(), reverse_prompt_model: String(config.reverse_prompt_model || "gpt-5-5").trim() || "gpt-5-5", product_visual_planner_system_prompt: String(config.product_visual_planner_system_prompt || "").trim(), product_visual_agent_provider: config.product_visual_agent_provider === "external" ? "external" : "internal", product_visual_agent_base_url: String(config.product_visual_agent_base_url || "").trim(), product_visual_agent_api_key: String(config.product_visual_agent_api_key || "").trim(), product_visual_agent_model: String(config.product_visual_agent_model || "gpt-5-5").trim() || "gpt-5-5", sensitive_words: [], local_review: normalizeLocalReview(config.local_review, (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(), fail_open: Boolean(config.ai_review?.fail_open), }, 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(), }, 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(), }, }); const normalized = normalizeConfig(data.config); set({ config: normalized }); publishBrandingUpdate(normalized); 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, }, }; }); }, setThreadPoolTokens: (value) => { set((state) => state.config ? { config: { ...state.config, thread_pool_tokens: 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 } } : {}); }, setImagePollMaxAttempts: (value) => { set((state) => state.config ? { config: { ...state.config, image_poll_max_attempts: value } } : {}); }, setImageSettleEnabled: (value) => { set((state) => state.config ? { config: { ...state.config, image_settle_enabled: value } } : {}); }, setImageSettleSecs: (value) => { set((state) => state.config ? { config: { ...state.config, image_settle_secs: value } } : {}); }, setImageAccountConcurrency: (value) => { set((state) => state.config ? { config: { ...state.config, image_account_concurrency: value } } : {}); }, setImageMemoryButtonEnabled: (value) => { set((state) => state.config ? { config: { ...state.config, image_memory_button_enabled: value } } : {}); }, setImageTimeoutRetrySecs: (value) => { set((state) => state.config ? { config: { ...state.config, image_timeout_retry_secs: value } } : {}); }, setImageParallelGeneration: (value) => { set((state) => state.config ? { config: { ...state.config, image_parallel_generation: value } } : {}); }, setImagePromptAspectHint: (value) => { set((state) => state.config ? { config: { ...state.config, image_prompt_aspect_hint: value } } : {}); }, setImageRemoveConversationAfterResult: (value) => { set((state) => state.config ? { config: { ...state.config, image_remove_conversation_after_result: value } } : {}); }, setImageRemoveConversationAlways: (value) => { set((state) => state.config ? { config: { ...state.config, image_remove_conversation_always: value } } : {}); }, setImageShareEvidenceEnabled: (value) => { set((state) => state.config ? { config: { ...state.config, image_share_evidence_enabled: value } } : {}); }, setImageShareRetentionHours: (value) => { set((state) => state.config ? { config: { ...state.config, image_share_retention_hours: value } } : {}); }, setDefaultUpstreamModelName: (value) => { set((state) => state.config ? { config: { ...state.config, default_upstream_model_name: value } } : {}); }, setDefaultThinkingEffort: (value) => { set((state) => state.config ? { config: { ...state.config, default_thinking_effort: value } } : {}); }, setNormalUserDefaultCredits: (value) => { set((state) => state.config ? { config: { ...state.config, normal_user_default_credits: value } } : {}); }, setNormalUserDailyCredits: (value) => { set((state) => state.config ? { config: { ...state.config, normal_user_daily_credits: value } } : {}); }, setNormalUserPoolPricing: (value) => { set((state) => state.config ? { config: { ...state.config, normal_user_pool_pricing: normalizePoolPricing(value) } } : {}); }, setNormalUserThirdPartyImagePricing: (value) => { set((state) => state.config ? { config: { ...state.config, normal_user_third_party_image_pricing: normalizePoolPricing(value) } } : {}); }, setCheckinDailyCredits: (value) => { set((state) => state.config ? { config: { ...state.config, checkin_daily_credits: value } } : {}); }, setCheckinRandomMin: (value) => { set((state) => state.config ? { config: { ...state.config, checkin_random_min: value } } : {}); }, setCheckinRandomMax: (value) => { set((state) => state.config ? { config: { ...state.config, checkin_random_max: value } } : {}); }, setCheckinInviteReward: (value) => { set((state) => state.config ? { config: { ...state.config, checkin_invite_reward: value } } : {}); }, setCheckinCreditValidDays: (value) => { set((state) => state.config ? { config: { ...state.config, checkin_credit_valid_days: value } } : {}); }, setCheckinAds: (value) => { set((state) => state.config ? { config: { ...state.config, checkin_ads: value } } : {}); }, setCheckinContent: (value) => { set((state) => state.config ? { config: { ...state.config, checkin_content: { ...DEFAULT_CHECKIN_CONTENT, ...(state.config.checkin_content || {}), ...value, }, }, } : {}); }, setErrorMessageRules: (value) => { // ⚠️ 配置还没加载完时不能静默丢弃:否则「添加规则」看起来完全没反应, // 而且归纳出的规则会「凭空消失」,排查成本极高。 const { config } = get(); if (!config) { toast.error("配置还没加载完,请稍后再试"); return; } set({ config: { ...config, error_message_rules: coerceErrorMessageRules(value) } }); }, setErrorMessageFallbackEnabled: (value) => { set((state) => state.config ? { config: { ...state.config, error_message_fallback_enabled: value } } : {}); }, setErrorMessageAiModel: (value) => { set((state) => state.config ? { config: { ...state.config, error_message_ai_model: value } } : {}); }, setInternalImageModelDailyLimits: (value) => { set((state) => state.config ? { config: { ...state.config, internal_image_model_daily_limits: normalizeInternalImageModelDailyLimits(value) } } : {}); }, setWechatLoginEnabled: (value) => { set((state) => state.config ? { config: { ...state.config, wechat_login_enabled: value } } : {}); }, setWechatAutoRegisterEnabled: (value) => { set((state) => state.config ? { config: { ...state.config, wechat_auto_register_enabled: value } } : {}); }, setInternalUserLoginSessionDurationHours: (value) => { set((state) => state.config ? { config: { ...state.config, internal_user_login_session_duration_hours: value } } : {}); }, setNormalUserLoginSessionDurationHours: (value) => { set((state) => state.config ? { config: { ...state.config, normal_user_login_session_duration_hours: value } } : {}); }, setSiteTitle: (value) => { set((state) => state.config ? { config: { ...state.config, site_title: value } } : {}); }, setSiteLogoUrl: (value) => { set((state) => state.config ? { config: { ...state.config, site_logo_url: value } } : {}); }, setSiteHeaderTitle: (value) => { set((state) => state.config ? { config: { ...state.config, site_header_title: value } } : {}); }, setSiteExternalLinkLabel: (value) => { set((state) => state.config ? { config: { ...state.config, site_external_link_label: value } } : {}); }, setSiteExternalLinkUrl: (value) => { set((state) => state.config ? { config: { ...state.config, site_external_link_url: value } } : {}); }, setSiteExternalLinkIconUrl: (value) => { set((state) => state.config ? { config: { ...state.config, site_external_link_icon_url: value } } : {}); }, setSiteExternalLinkIconSvg: (value) => { set((state) => state.config ? { config: { ...state.config, site_external_link_icon_svg: value } } : {}); }, setShopPurchaseUrl: (value) => { set((state) => state.config ? { config: { ...state.config, shop_purchase_url: value } } : {}); }, setAnnouncements: (items) => { set((state) => state.config ? { config: { ...state.config, announcements: normalizeAnnouncements(items) } } : {}); }, setNormalUserNavigation: (items) => { set((state) => state.config ? { config: { ...state.config, normal_user_navigation: items } } : {}); }, setInternalUserNavigation: (items) => { set((state) => state.config ? { config: { ...state.config, internal_user_navigation: items } } : {}); }, setAdminUserNavigation: (items) => { set((state) => state.config ? { config: { ...state.config, admin_user_navigation: items } } : {}); }, 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 } } : {}); }, setFreeAccountCleanupField: (key, value) => { set((state) => { if (!state.config) { return {}; } const current = normalizeFreeAccountCleanup(state.config.free_account_cleanup); return { config: { ...state.config, free_account_cleanup: normalizeFreeAccountCleanup({ ...current, [key]: value, }), }, }; }); }, setRedeemCodeCleanupField: (key, value) => { set((state) => { if (!state.config) { return {}; } const current = normalizeRedeemCodeCleanup(state.config.redeem_code_cleanup); return { config: { ...state.config, redeem_code_cleanup: normalizeRedeemCodeCleanup({ ...current, [key]: value, }), }, }; }); }, setCleanupSharedConversations: (value) => { set((state) => state.config ? { config: { ...state.config, cleanup_shared_conversations: 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, }, }; }); }, setImageProxyBaseUrl: (value) => { set((state) => { if (!state.config) { return {}; } return { config: { ...state.config, image_proxy_base_url: value, }, }; }); }, setGlobalSystemPrompt: (value) => { set((state) => state.config ? { config: { ...state.config, global_system_prompt: value } } : {}); }, setPromptOptimizerSystemPrompt: (value) => { set((state) => state.config ? { config: { ...state.config, prompt_optimizer_system_prompt: value } } : {}); }, setReversePromptSystemPrompt: (value) => { set((state) => state.config ? { config: { ...state.config, reverse_prompt_system_prompt: value } } : {}); }, setReversePromptField: (key, value) => { set((state) => state.config ? { config: { ...state.config, [`reverse_prompt_${key}`]: value }, } : {}); }, setProductVisualAgentField: (key, value) => { set((state) => state.config ? { config: { ...state.config, [`product_visual_agent_${key}`]: value }, } : {}); }, setSensitiveWordsText: (value) => { set((state) => state.config ? { config: { ...state.config, sensitive_words: value.split("\n") } } : {}); }, setLocalReviewField: (key, value) => { set((state) => state.config ? { config: { ...state.config, local_review: { ...normalizeLocalReview(state.config.local_review), [key]: value }, }, } : {}); }, 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, }, }; }); }, setPromptOptimizerField: (key, value) => { set((state) => state.config ? { config: { ...state.config, [`prompt_optimizer_${key}`]: value }, } : {}); }, setProxyRuntimeField: (key, value) => { set((state) => { if (!state.config) return {}; const runtime = normalizeProxyRuntime(state.config.proxy_runtime); return { config: { ...state.config, proxy_runtime: normalizeProxyRuntime({ ...runtime, [key]: value }) } }; }); }, setProxyRuntimeClearanceField: (key, value) => { set((state) => { if (!state.config) return {}; const runtime = normalizeProxyRuntime(state.config.proxy_runtime); return { config: { ...state.config, proxy_runtime: normalizeProxyRuntime({ ...runtime, clearance: { ...runtime.clearance, [key]: value } }), }, }; }); }, setProxyRuntimeStatusCodesText: (value) => { const codes = value.split(/[,\s]+/).map(Number).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 ? codes : [403] }) } }; }); }, setInfiniteCanvasField: (key, value) => { set((state) => { if (!state.config) return {}; const current = state.config.third_party_apps?.infinite_canvas || { enabled: false, url: "" }; return { config: { ...state.config, third_party_apps: { infinite_canvas: { ...current, [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 }); } }, loadRegister: async (silent = false) => { if (!silent) set({ isLoadingRegister: true }); try { const data = await fetchRegisterConfig(); set({ registerConfig: { ...data.register, registration_method: normalizeRegistrationMethod(data.register.registration_method), }, }); } catch (error) { if (!silent) toast.error(error instanceof Error ? error.message : "加载注册配置失败"); } finally { if (!silent) set({ isLoadingRegister: false }); } }, setRegisterConfig: (config) => { set({ registerConfig: { ...config, registration_method: normalizeRegistrationMethod(config.registration_method), }, 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 } } : {}); }, setRegisterRetryAttempts: (value) => { set((state) => state.registerConfig ? { registerConfig: { ...state.registerConfig, retry_attempts: Number(value) || 0 } } : {}); }, setRegistrationMethod: (value) => { set((state) => state.registerConfig ? { registerConfig: { ...state.registerConfig, registration_method: value } } : {}); }, 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 }, }, } : {}); }, addRegisterProvider: () => { set((state) => state.registerConfig ? { registerConfig: { ...state.registerConfig, mail: { ...state.registerConfig.mail, providers: [ ...(state.registerConfig.mail.providers || []), { enable: true, type: "cloudmail_gen", api_base: "", admin_email: "", admin_password: "", domain: [], subdomain: [], email_prefix: "" }, ], }, }, } : {}); }, 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, proxy: registerConfig.proxy.trim(), total: Math.max(1, Number(registerConfig.total) || 1), threads: Math.max(1, Number(registerConfig.threads) || 1), retry_attempts: Math.min(10, Math.max(1, Number(registerConfig.retry_attempts) || 3)), registration_method: normalizeRegistrationMethod(registerConfig.registration_method), 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, proxy: registerConfig.proxy.trim(), total: Math.max(1, Number(registerConfig.total) || 1), threads: Math.max(1, Number(registerConfig.threads) || 1), retry_attempts: Math.min(10, Math.max(1, Number(registerConfig.retry_attempts) || 3)), registration_method: normalizeRegistrationMethod(registerConfig.registration_method), 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 }); } }, 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 }); } }, }));