Download web/src/app/settings/store.ts from zlcy/study: direct link, hf CLI and curl.
- Browser
- Download file 78.9 kB
-
https://huggingface.co/spaces/zlcy/study/resolve/main/web/src/app/settings/store.ts
- Command line
-
hf download hf://spaces/zlcy/study/web/src/app/settings/store.ts
-
curl -L -o store.ts https://huggingface.co/spaces/zlcy/study/resolve/main/web/src/app/settings/store.ts
78.9 kB
| "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<string>(); | |
| value.forEach((raw, index) => { | |
| if (!raw || typeof raw !== "object") { | |
| return; | |
| } | |
| const item = raw as Partial<ErrorMessageRule>; | |
| 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<SettingsConfig["local_review"]> { | |
| const excludedTerms = new Set(["换脸", "丝袜", "裤袜", "连裤袜"]); | |
| const cleanLegacyTerm = (term: unknown) => String(term || "").trim().replace(/^[\s,,、;;。]+|[\s,,、;;。]+$/g, ""); | |
| const ruleNames: Record<string, string> = { | |
| "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<PoolPricingConfig> : {}; | |
| 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<ProxyRuntimeSettings> : {}; | |
| const clearanceSource = typeof source.clearance === "object" && source.clearance !== null | |
| ? source.clearance as Partial<ProxyRuntimeSettings["clearance"]> | |
| : {}; | |
| 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<FreeAccountCleanupSettings> : {}; | |
| 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<RedeemCodeCleanupSettings> : {}; | |
| 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<string>(); | |
| const rows: ImageModelDailyLimit[] = []; | |
| for (const item of value) { | |
| if (typeof item !== "object" || item === null) { | |
| continue; | |
| } | |
| const source = item as Partial<ImageModelDailyLimit>; | |
| 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<NavigationItem>; | |
| 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<Announcement> => 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<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; | |
| } | |
| 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<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; | |
| 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<CheckinContent>) => 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: <K extends keyof FreeAccountCleanupSettings>(key: K, value: FreeAccountCleanupSettings[K]) => void; | |
| setRedeemCodeCleanupField: <K extends keyof RedeemCodeCleanupSettings>(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<SettingsConfig["local_review"]>["categories"] | NonNullable<SettingsConfig["local_review"]>["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: <K extends keyof ProxyRuntimeSettings>(key: K, value: ProxyRuntimeSettings[K]) => void; | |
| setProxyRuntimeClearanceField: <K extends keyof ProxyRuntimeSettings["clearance"]>(key: K, value: ProxyRuntimeSettings["clearance"][K]) => void; | |
| setProxyRuntimeStatusCodesText: (value: string) => void; | |
| setInfiniteCanvasField: (key: "enabled" | "url", value: string | boolean) => void; | |
| testImageStorage: () => Promise<void>; | |
| syncImagesToWebDAV: () => Promise<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; | |
| 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<string, unknown>) => void; | |
| deleteRegisterProvider: (index: number) => void; | |
| saveRegister: () => Promise<void>; | |
| toggleRegister: () => 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, | |
| 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 }); | |
| } | |
| }, | |
| })); | |