Spaces:
Running
Running
| from __future__ import annotations | |
| import copy | |
| from dataclasses import dataclass | |
| import json | |
| import os | |
| import sys | |
| from pathlib import Path | |
| import time | |
| from services.storage.base import StorageBackend | |
| BASE_DIR = Path(__file__).resolve().parents[1] | |
| DATA_DIR = BASE_DIR / "data" | |
| CONFIG_FILE = BASE_DIR / "config.json" | |
| VERSION_FILE = BASE_DIR / "VERSION" | |
| BACKUP_STATE_FILE = DATA_DIR / "backup_state.json" | |
| DEFAULT_BACKUP_INCLUDE = { | |
| "config": True, | |
| "cpa": True, | |
| "sub2api": True, | |
| "logs": True, | |
| "image_tasks": True, | |
| "accounts_snapshot": True, | |
| "auth_keys_snapshot": True, | |
| "images": False, | |
| } | |
| DEFAULT_IMAGE_STORAGE = { | |
| "enabled": False, | |
| "mode": "local", | |
| "webdav_url": "", | |
| "webdav_username": "", | |
| "webdav_password": "", | |
| "webdav_root_path": "chatgpt2api/images", | |
| "public_base_url": "", | |
| } | |
| DEFAULT_CHAT_COMPLETION_CACHE = { | |
| "enabled": True, | |
| "ttl_seconds": 60, | |
| "max_entries": 256, | |
| "dedupe_inflight": True, | |
| "stream_cache": True, | |
| "normalize_messages": True, | |
| "drop_adjacent_duplicates": True, | |
| "drop_assistant_history": False, | |
| } | |
| DEFAULT_PROXY_RUNTIME_USER_AGENT = ( | |
| "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " | |
| "AppleWebKit/537.36 (KHTML, like Gecko) " | |
| "Chrome/145.0.0.0 Safari/537.36" | |
| ) | |
| DEFAULT_PROXY_RUNTIME = { | |
| "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": DEFAULT_PROXY_RUNTIME_USER_AGENT, | |
| "browser": "chrome", | |
| "flaresolverr_url": "", | |
| "timeout_sec": 60, | |
| "refresh_interval": 3600, | |
| "warm_up_on_start": False, | |
| }, | |
| } | |
| DEFAULT_THIRD_PARTY_APPS = { | |
| "infinite_canvas": { | |
| "enabled": False, | |
| "url": "https://canvas.best", | |
| }, | |
| } | |
| def _normalize_bool(value: object, default: bool = False) -> bool: | |
| if isinstance(value, str): | |
| lowered = value.strip().lower() | |
| if lowered in {"1", "true", "yes", "on"}: | |
| return True | |
| if lowered in {"0", "false", "no", "off"}: | |
| return False | |
| return default | |
| if value is None: | |
| return default | |
| return bool(value) | |
| def _normalize_positive_int(value: object, default: int, minimum: int = 0) -> int: | |
| try: | |
| normalized = int(value) | |
| except (OverflowError, TypeError, ValueError): | |
| normalized = default | |
| return max(minimum, normalized) | |
| def _normalize_backup_include(value: object) -> dict[str, bool]: | |
| source = value if isinstance(value, dict) else {} | |
| normalized = dict(DEFAULT_BACKUP_INCLUDE) | |
| for key in normalized: | |
| normalized[key] = _normalize_bool(source.get(key), normalized[key]) | |
| return normalized | |
| def _normalize_backup_settings(value: object) -> dict[str, object]: | |
| source = value if isinstance(value, dict) else {} | |
| return { | |
| "enabled": _normalize_bool(source.get("enabled"), False), | |
| "provider": "cloudflare_r2", | |
| "account_id": str(source.get("account_id") or "").strip(), | |
| "access_key_id": str(source.get("access_key_id") or "").strip(), | |
| "secret_access_key": str(source.get("secret_access_key") or "").strip(), | |
| "bucket": str(source.get("bucket") or "").strip(), | |
| "prefix": str(source.get("prefix") or "backups").strip().strip("/") or "backups", | |
| "interval_minutes": _normalize_positive_int(source.get("interval_minutes"), 360, 1), | |
| "rotation_keep": _normalize_positive_int(source.get("rotation_keep"), 10, 0), | |
| "encrypt": _normalize_bool(source.get("encrypt"), False), | |
| "passphrase": str(source.get("passphrase") or "").strip(), | |
| "include": _normalize_backup_include(source.get("include")), | |
| } | |
| def _normalize_backup_state(value: object) -> dict[str, object]: | |
| source = value if isinstance(value, dict) else {} | |
| return { | |
| "last_started_at": str(source.get("last_started_at") or "").strip() or None, | |
| "last_finished_at": str(source.get("last_finished_at") or "").strip() or None, | |
| "last_status": str(source.get("last_status") or "idle").strip() or "idle", | |
| "last_error": str(source.get("last_error") or "").strip() or None, | |
| "last_object_key": str(source.get("last_object_key") or "").strip() or None, | |
| } | |
| def _normalize_image_storage_settings(value: object) -> dict[str, object]: | |
| source = value if isinstance(value, dict) else {} | |
| mode = str(source.get("mode") or "local").strip().lower() | |
| if mode not in {"local", "webdav", "both"}: | |
| mode = "local" | |
| enabled = _normalize_bool(source.get("enabled"), False) | |
| if not enabled: | |
| mode = "local" | |
| root_path = str(source.get("webdav_root_path") or DEFAULT_IMAGE_STORAGE["webdav_root_path"]).strip().strip("/") | |
| return { | |
| "enabled": enabled, | |
| "mode": mode, | |
| "webdav_url": str(source.get("webdav_url") or "").strip().rstrip("/"), | |
| "webdav_username": str(source.get("webdav_username") or "").strip(), | |
| "webdav_password": str(source.get("webdav_password") or "").strip(), | |
| "webdav_root_path": root_path or str(DEFAULT_IMAGE_STORAGE["webdav_root_path"]), | |
| "public_base_url": str(source.get("public_base_url") or "").strip().rstrip("/"), | |
| } | |
| def _normalize_chat_completion_cache_settings(value: object) -> dict[str, object]: | |
| source = value if isinstance(value, dict) else {} | |
| return { | |
| "enabled": _normalize_bool(source.get("enabled"), DEFAULT_CHAT_COMPLETION_CACHE["enabled"]), | |
| "ttl_seconds": _normalize_positive_int( | |
| source.get("ttl_seconds"), | |
| int(DEFAULT_CHAT_COMPLETION_CACHE["ttl_seconds"]), | |
| 0, | |
| ), | |
| "max_entries": _normalize_positive_int( | |
| source.get("max_entries"), | |
| int(DEFAULT_CHAT_COMPLETION_CACHE["max_entries"]), | |
| 1, | |
| ), | |
| "dedupe_inflight": _normalize_bool( | |
| source.get("dedupe_inflight"), | |
| bool(DEFAULT_CHAT_COMPLETION_CACHE["dedupe_inflight"]), | |
| ), | |
| "stream_cache": _normalize_bool( | |
| source.get("stream_cache"), | |
| bool(DEFAULT_CHAT_COMPLETION_CACHE["stream_cache"]), | |
| ), | |
| "normalize_messages": _normalize_bool( | |
| source.get("normalize_messages"), | |
| bool(DEFAULT_CHAT_COMPLETION_CACHE["normalize_messages"]), | |
| ), | |
| "drop_adjacent_duplicates": _normalize_bool( | |
| source.get("drop_adjacent_duplicates"), | |
| bool(DEFAULT_CHAT_COMPLETION_CACHE["drop_adjacent_duplicates"]), | |
| ), | |
| "drop_assistant_history": _normalize_bool( | |
| source.get("drop_assistant_history"), | |
| bool(DEFAULT_CHAT_COMPLETION_CACHE["drop_assistant_history"]), | |
| ), | |
| } | |
| def _normalize_status_codes(value: object) -> list[int]: | |
| items = value if isinstance(value, list) else DEFAULT_PROXY_RUNTIME["reset_session_status_codes"] | |
| normalized: list[int] = [] | |
| for item in items: | |
| if isinstance(item, bool): | |
| continue | |
| try: | |
| status = int(item) | |
| except (OverflowError, TypeError, ValueError): | |
| continue | |
| if 100 <= status <= 599 and status not in normalized: | |
| normalized.append(status) | |
| if not normalized: | |
| return list(DEFAULT_PROXY_RUNTIME["reset_session_status_codes"]) | |
| return normalized | |
| def _normalize_proxy_runtime_settings(value: object) -> dict[str, object]: | |
| source = value if isinstance(value, dict) else {} | |
| default_clearance = DEFAULT_PROXY_RUNTIME["clearance"] | |
| clearance_source = source.get("clearance") if isinstance(source.get("clearance"), dict) else {} | |
| egress_mode = str(source.get("egress_mode") or DEFAULT_PROXY_RUNTIME["egress_mode"]).strip().lower() | |
| if egress_mode not in {"direct", "single_proxy"}: | |
| egress_mode = str(DEFAULT_PROXY_RUNTIME["egress_mode"]) | |
| clearance_mode = str(clearance_source.get("mode") or default_clearance["mode"]).strip().lower() | |
| if clearance_mode not in {"none", "manual", "flaresolverr"}: | |
| clearance_mode = str(default_clearance["mode"]) | |
| user_agent = str(clearance_source.get("user_agent") or default_clearance["user_agent"]).strip() | |
| browser = str(clearance_source.get("browser") or default_clearance["browser"]).strip() | |
| existing_clearance_cookies = str(source.get("_existing_cf_cookies") or "").strip() | |
| existing_cf_clearance = str(source.get("_existing_cf_clearance") or "").strip() | |
| cf_cookies = str(clearance_source.get("cf_cookies") or "").strip() | |
| cf_clearance = str(clearance_source.get("cf_clearance") or "").strip() | |
| if not cf_cookies and _normalize_bool(clearance_source.get("has_cf_cookies"), False): | |
| cf_cookies = existing_clearance_cookies | |
| if not cf_clearance and _normalize_bool(clearance_source.get("has_cf_clearance"), False): | |
| cf_clearance = existing_cf_clearance | |
| return { | |
| "enabled": _normalize_bool(source.get("enabled"), bool(DEFAULT_PROXY_RUNTIME["enabled"])), | |
| "egress_mode": egress_mode, | |
| "proxy_url": str(source.get("proxy_url") or "").strip(), | |
| "resource_proxy_url": str(source.get("resource_proxy_url") or "").strip(), | |
| "skip_ssl_verify": _normalize_bool( | |
| source.get("skip_ssl_verify"), | |
| bool(DEFAULT_PROXY_RUNTIME["skip_ssl_verify"]), | |
| ), | |
| "reset_session_status_codes": _normalize_status_codes(source.get("reset_session_status_codes")), | |
| "clearance": { | |
| "enabled": _normalize_bool(clearance_source.get("enabled"), bool(default_clearance["enabled"])), | |
| "mode": clearance_mode, | |
| "cf_cookies": cf_cookies, | |
| "cf_clearance": cf_clearance, | |
| "user_agent": user_agent or str(default_clearance["user_agent"]), | |
| "browser": browser or str(default_clearance["browser"]), | |
| "flaresolverr_url": str(clearance_source.get("flaresolverr_url") or "").strip(), | |
| "timeout_sec": _normalize_positive_int( | |
| clearance_source.get("timeout_sec"), | |
| int(default_clearance["timeout_sec"]), | |
| 1, | |
| ), | |
| "refresh_interval": _normalize_positive_int( | |
| clearance_source.get("refresh_interval"), | |
| int(default_clearance["refresh_interval"]), | |
| 60, | |
| ), | |
| "warm_up_on_start": _normalize_bool( | |
| clearance_source.get("warm_up_on_start"), | |
| bool(default_clearance["warm_up_on_start"]), | |
| ), | |
| }, | |
| } | |
| def _normalize_third_party_apps_settings(value: object) -> dict[str, object]: | |
| source = value if isinstance(value, dict) else {} | |
| canvas_source = source.get("infinite_canvas") if isinstance(source.get("infinite_canvas"), dict) else {} | |
| return { | |
| "infinite_canvas": { | |
| "enabled": _normalize_bool(canvas_source.get("enabled"), False), | |
| "url": str(canvas_source.get("url") or DEFAULT_THIRD_PARTY_APPS["infinite_canvas"]["url"]).strip(), | |
| }, | |
| } | |
| def _validate_image_storage_settings(settings: dict[str, object]) -> None: | |
| if not _normalize_bool(settings.get("enabled"), False): | |
| return | |
| if not str(settings.get("webdav_url") or "").strip(): | |
| raise ValueError("启用 WebDAV 图片存储后必须填写 WebDAV URL") | |
| if not str(settings.get("webdav_password") or "").strip(): | |
| raise ValueError("启用 WebDAV 图片存储后必须填写 WebDAV 密码") | |
| class LoadedSettings: | |
| auth_key: str | |
| refresh_account_interval_minute: int | |
| def _normalize_auth_key(value: object) -> str: | |
| return str(value or "").strip() | |
| def _is_invalid_auth_key(value: object) -> bool: | |
| return _normalize_auth_key(value) == "" | |
| def _read_json_object(path: Path, *, name: str) -> dict[str, object]: | |
| if not path.exists(): | |
| return {} | |
| if path.is_dir(): | |
| print( | |
| f"Warning: {name} at '{path}' is a directory, ignoring it and falling back to other configuration sources.", | |
| file=sys.stderr, | |
| ) | |
| return {} | |
| try: | |
| data = json.loads(path.read_text(encoding="utf-8")) | |
| except Exception: | |
| return {} | |
| return data if isinstance(data, dict) else {} | |
| def _load_settings() -> LoadedSettings: | |
| DATA_DIR.mkdir(parents=True, exist_ok=True) | |
| raw_config = _read_json_object(CONFIG_FILE, name="config.json") | |
| auth_key = _normalize_auth_key(os.getenv("CHATGPT2API_AUTH_KEY") or raw_config.get("auth-key")) | |
| if _is_invalid_auth_key(auth_key): | |
| raise ValueError( | |
| "❌ auth-key 未设置!\n" | |
| "请在环境变量 CHATGPT2API_AUTH_KEY 中设置,或者在 config.json 中填写 auth-key。" | |
| ) | |
| try: | |
| refresh_interval = int(raw_config.get("refresh_account_interval_minute", 5)) | |
| except (TypeError, ValueError): | |
| refresh_interval = 5 | |
| return LoadedSettings( | |
| auth_key=auth_key, | |
| refresh_account_interval_minute=refresh_interval, | |
| ) | |
| class ConfigStore: | |
| def __init__(self, path: Path): | |
| self.path = path | |
| DATA_DIR.mkdir(parents=True, exist_ok=True) | |
| self.data = self._load() | |
| self._storage_backend: StorageBackend | None = None | |
| if _is_invalid_auth_key(self.auth_key): | |
| raise ValueError( | |
| "❌ auth-key 未设置!\n" | |
| "请按以下任意一种方式解决:\n" | |
| "1. 在 Render 的 Environment 变量中添加:\n" | |
| " CHATGPT2API_AUTH_KEY = your_real_auth_key\n" | |
| "2. 或者在 config.json 中填写:\n" | |
| ' "auth-key": "your_real_auth_key"' | |
| ) | |
| def _load(self) -> dict[str, object]: | |
| return _read_json_object(self.path, name="config.json") | |
| def _save(self) -> None: | |
| self.path.write_text(json.dumps(self.data, ensure_ascii=False, indent=2) + "\n", encoding="utf-8") | |
| def auth_key(self) -> str: | |
| return _normalize_auth_key(os.getenv("CHATGPT2API_AUTH_KEY") or self.data.get("auth-key")) | |
| def accounts_file(self) -> Path: | |
| return DATA_DIR / "accounts.json" | |
| def refresh_account_interval_minute(self) -> int: | |
| try: | |
| return int(self.data.get("refresh_account_interval_minute", 5)) | |
| except (TypeError, ValueError): | |
| return 5 | |
| def image_retention_days(self) -> int: | |
| try: | |
| return max(1, int(self.data.get("image_retention_days", 30))) | |
| except (TypeError, ValueError): | |
| return 30 | |
| def image_poll_timeout_secs(self) -> int: | |
| try: | |
| return max(1, int(self.data.get("image_poll_timeout_secs", 120))) | |
| except (TypeError, ValueError): | |
| return 120 | |
| def image_poll_interval_secs(self) -> float: | |
| try: | |
| return max(0.5, float(self.data.get("image_poll_interval_secs", 10.0))) | |
| except (TypeError, ValueError): | |
| return 10.0 | |
| def image_poll_initial_wait_secs(self) -> float: | |
| """Image generation upstream takes ~30s; polling immediately wastes requests | |
| and trips a transient 429. Default 10s gives the conversation document time | |
| to commit before the first poll.""" | |
| try: | |
| return max(0.0, float(self.data.get("image_poll_initial_wait_secs", 10.0))) | |
| except (TypeError, ValueError): | |
| return 10.0 | |
| def image_account_concurrency(self) -> int: | |
| try: | |
| return max(1, int(self.data.get("image_account_concurrency", 3))) | |
| except (TypeError, ValueError): | |
| return 3 | |
| def image_parallel_generation(self) -> bool: | |
| value = self.data.get("image_parallel_generation", True) | |
| if isinstance(value, str): | |
| return value.strip().lower() in {"1", "true", "yes", "on"} | |
| return bool(value) | |
| def image_settle_enabled(self) -> bool: | |
| """图片二次确认机制:找到 file_ids 后等待一段时间再次确认。""" | |
| value = self.data.get("image_settle_enabled", True) | |
| if isinstance(value, str): | |
| return value.strip().lower() in {"1", "true", "yes", "on"} | |
| return bool(value) | |
| def image_check_before_hit_enabled(self) -> bool: | |
| """先check再hit:通过轮询确认 file_ids 存在后再返回,而非仅依赖 SSE 事件。""" | |
| value = self.data.get("image_check_before_hit_enabled", True) | |
| if isinstance(value, str): | |
| return value.strip().lower() in {"1", "true", "yes", "on"} | |
| return bool(value) | |
| def image_remove_conversation_after_result(self) -> bool: | |
| """出图成功后异步隐藏 ChatGPT 本地对话记录。""" | |
| value = self.data.get("image_remove_conversation_after_result", False) | |
| if isinstance(value, str): | |
| return value.strip().lower() in {"1", "true", "yes", "on"} | |
| return bool(value) | |
| def image_settle_secs(self) -> float: | |
| """二次确认等待时间(秒)。""" | |
| try: | |
| return max(0.5, float(self.data.get("image_settle_secs", 2.0))) | |
| except (TypeError, ValueError): | |
| return 2.0 | |
| def auto_remove_invalid_accounts(self) -> bool: | |
| value = self.data.get("auto_remove_invalid_accounts", False) | |
| if isinstance(value, str): | |
| return value.strip().lower() in {"1", "true", "yes", "on"} | |
| return bool(value) | |
| def auto_remove_rate_limited_accounts(self) -> bool: | |
| value = self.data.get("auto_remove_rate_limited_accounts", False) | |
| if isinstance(value, str): | |
| return value.strip().lower() in {"1", "true", "yes", "on"} | |
| return bool(value) | |
| def auto_relogin_after_refresh(self) -> bool: | |
| value = self.data.get("auto_relogin_after_refresh", False) | |
| if isinstance(value, str): | |
| return value.strip().lower() in {"1", "true", "yes", "on"} | |
| return bool(value) | |
| def log_levels(self) -> list[str]: | |
| levels = self.data.get("log_levels") | |
| if not isinstance(levels, list): | |
| return [] | |
| allowed = {"debug", "info", "warning", "error"} | |
| return [level for item in levels if (level := str(item or "").strip().lower()) in allowed] | |
| def sensitive_words(self) -> list[str]: | |
| words = self.data.get("sensitive_words") | |
| return [word for item in words if (word := str(item or "").strip())] if isinstance(words, list) else [] | |
| def ai_review(self) -> dict[str, object]: | |
| value = self.data.get("ai_review") | |
| return value if isinstance(value, dict) else {} | |
| def global_system_prompt(self) -> str: | |
| return str(self.data.get("global_system_prompt") or "").strip() | |
| def images_dir(self) -> Path: | |
| path = DATA_DIR / "images" | |
| path.mkdir(parents=True, exist_ok=True) | |
| return path | |
| def image_thumbnails_dir(self) -> Path: | |
| path = DATA_DIR / "image_thumbnails" | |
| path.mkdir(parents=True, exist_ok=True) | |
| return path | |
| def cleanup_old_images(self) -> int: | |
| cutoff = time.time() - self.image_retention_days * 86400 | |
| removed = 0 | |
| for path in self.images_dir.rglob("*"): | |
| if path.is_file() and path.stat().st_mtime < cutoff: | |
| path.unlink() | |
| removed += 1 | |
| for path in sorted((p for p in self.images_dir.rglob("*") if p.is_dir()), key=lambda p: len(p.parts), reverse=True): | |
| try: | |
| path.rmdir() | |
| except OSError: | |
| pass | |
| return removed | |
| def base_url(self) -> str: | |
| return str( | |
| os.getenv("CHATGPT2API_BASE_URL") | |
| or self.data.get("base_url") | |
| or "" | |
| ).strip().rstrip("/") | |
| def app_version(self) -> str: | |
| try: | |
| value = VERSION_FILE.read_text(encoding="utf-8").strip() | |
| except FileNotFoundError: | |
| return "0.0.0" | |
| return value or "0.0.0" | |
| def get(self) -> dict[str, object]: | |
| data = dict(self.data) | |
| data["refresh_account_interval_minute"] = self.refresh_account_interval_minute | |
| data["image_retention_days"] = self.image_retention_days | |
| data["image_poll_timeout_secs"] = self.image_poll_timeout_secs | |
| data["image_poll_interval_secs"] = self.image_poll_interval_secs | |
| data["image_poll_initial_wait_secs"] = self.image_poll_initial_wait_secs | |
| data["image_account_concurrency"] = self.image_account_concurrency | |
| data["image_parallel_generation"] = self.image_parallel_generation | |
| data["image_remove_conversation_after_result"] = self.image_remove_conversation_after_result | |
| data["auto_remove_invalid_accounts"] = self.auto_remove_invalid_accounts | |
| data["auto_remove_rate_limited_accounts"] = self.auto_remove_rate_limited_accounts | |
| data["auto_relogin_after_refresh"] = self.auto_relogin_after_refresh | |
| data["log_levels"] = self.log_levels | |
| data["sensitive_words"] = self.sensitive_words | |
| data["ai_review"] = self.ai_review | |
| data["global_system_prompt"] = self.global_system_prompt | |
| data["backup"] = self.get_backup_settings() | |
| data["image_storage"] = self.get_image_storage_settings() | |
| data["chat_completion_cache"] = self.get_chat_completion_cache_settings() | |
| data["proxy_runtime"] = self.get_public_proxy_runtime_settings() | |
| data["third_party_apps"] = self.get_third_party_apps_settings() | |
| data.pop("auth-key", None) | |
| return data | |
| def get_proxy_settings(self) -> str: | |
| return str(self.data.get("proxy") or "").strip() | |
| def get_proxy_runtime_settings(self) -> dict[str, object]: | |
| return _normalize_proxy_runtime_settings(self.data.get("proxy_runtime")) | |
| def get_public_proxy_runtime_settings(self) -> dict[str, object]: | |
| runtime = copy.deepcopy(self.get_proxy_runtime_settings()) | |
| clearance = runtime.get("clearance") if isinstance(runtime.get("clearance"), dict) else {} | |
| if isinstance(clearance, dict): | |
| cf_cookies = str(clearance.get("cf_cookies") or "").strip() | |
| cf_clearance = str(clearance.get("cf_clearance") or "").strip() | |
| clearance["cf_cookies"] = "" | |
| clearance["cf_clearance"] = "" | |
| clearance["has_cf_cookies"] = bool(cf_cookies) | |
| clearance["has_cf_clearance"] = bool(cf_clearance) | |
| return runtime | |
| def get_third_party_apps_settings(self) -> dict[str, object]: | |
| return _normalize_third_party_apps_settings(self.data.get("third_party_apps")) | |
| def update(self, data: dict[str, object]) -> dict[str, object]: | |
| next_data = dict(self.data) | |
| next_data.update(dict(data or {})) | |
| if "backup" in next_data: | |
| next_data["backup"] = _normalize_backup_settings(next_data.get("backup")) | |
| if "image_storage" in next_data: | |
| next_data["image_storage"] = _normalize_image_storage_settings(next_data.get("image_storage")) | |
| _validate_image_storage_settings(next_data["image_storage"]) | |
| if "chat_completion_cache" in next_data: | |
| next_data["chat_completion_cache"] = _normalize_chat_completion_cache_settings( | |
| next_data.get("chat_completion_cache") | |
| ) | |
| if "third_party_apps" in next_data: | |
| next_data["third_party_apps"] = _normalize_third_party_apps_settings(next_data.get("third_party_apps")) | |
| if "proxy_runtime" in next_data: | |
| incoming_runtime = next_data.get("proxy_runtime") | |
| if isinstance(incoming_runtime, dict): | |
| previous_clearance = self.get_proxy_runtime_settings().get("clearance") | |
| if isinstance(previous_clearance, dict): | |
| incoming_runtime = dict(incoming_runtime) | |
| incoming_runtime["_existing_cf_cookies"] = previous_clearance.get("cf_cookies") | |
| incoming_runtime["_existing_cf_clearance"] = previous_clearance.get("cf_clearance") | |
| next_data["proxy_runtime"] = _normalize_proxy_runtime_settings(incoming_runtime) | |
| next_data.pop("backup_state", None) | |
| self.data = next_data | |
| self._save() | |
| return self.get() | |
| def get_backup_settings(self) -> dict[str, object]: | |
| return _normalize_backup_settings(self.data.get("backup")) | |
| def get_image_storage_settings(self) -> dict[str, object]: | |
| return _normalize_image_storage_settings(self.data.get("image_storage")) | |
| def get_chat_completion_cache_settings(self) -> dict[str, object]: | |
| return _normalize_chat_completion_cache_settings(self.data.get("chat_completion_cache")) | |
| def get_storage_backend(self) -> StorageBackend: | |
| """获取存储后端实例(单例)""" | |
| if self._storage_backend is None: | |
| from services.storage.factory import create_storage_backend | |
| self._storage_backend = create_storage_backend(DATA_DIR) | |
| return self._storage_backend | |
| def load_backup_state() -> dict[str, object]: | |
| return _normalize_backup_state(_read_json_object(BACKUP_STATE_FILE, name="backup_state.json")) | |
| def save_backup_state(state: dict[str, object]) -> dict[str, object]: | |
| normalized = _normalize_backup_state(state) | |
| BACKUP_STATE_FILE.write_text(json.dumps(normalized, ensure_ascii=False, indent=2) + "\n", encoding="utf-8") | |
| return normalized | |
| config = ConfigStore(CONFIG_FILE) | |