study / web /src /app /settings /store.ts
xiaolongmr
zidong shibie fumian tishici quduan, wu xu shoudong jia fangxing duanyu
a3fcfd7
Raw History Blame Contribute Delete
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 });
}
},
}));