dsh-plugin-image-workbench 1.1.5 → 1.1.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -21
- package/README.en.md +5 -3
- package/README.md +7 -3
- package/client.js +241 -241
- package/cordis.patch.yml +14 -14
- package/host.js +51 -0
- package/install.mjs +333 -387
- package/package.json +3 -2
- package/scripts/fetch-style-images.mjs +104 -0
- package/scripts/vendor-snapshot.py +154 -154
- package/server/access.py +26 -26
- package/server/app.py +1371 -1329
- package/server/catalog.py +147 -147
- package/server/composer.py +391 -391
- package/server/drafts.py +158 -158
- package/server/history.py +575 -575
- package/server/imaging.py +172 -172
- package/server/pool.py +1335 -1335
- package/server/pool_admin.py +921 -921
- package/server/pool_service.py +1073 -1073
- package/server/requirements.txt +6 -6
- package/server/smoke_check.py +402 -402
- package/server/static/access.html +202 -202
- package/server/static/index.html +15 -0
- package/vendor/chatgpt2api/source/.env.example +82 -82
- package/vendor/chatgpt2api/source/.python-version +1 -1
- package/vendor/chatgpt2api/source/LICENSE +21 -21
- package/vendor/chatgpt2api/source/README.md +391 -391
- package/vendor/chatgpt2api/source/VERSION +1 -1
- package/vendor/chatgpt2api/source/api/__init__.py +2 -2
- package/vendor/chatgpt2api/source/api/accounts.py +537 -537
- package/vendor/chatgpt2api/source/api/ai.py +217 -217
- package/vendor/chatgpt2api/source/api/app.py +62 -62
- package/vendor/chatgpt2api/source/api/errors.py +46 -46
- package/vendor/chatgpt2api/source/api/image_inputs.py +309 -309
- package/vendor/chatgpt2api/source/api/image_tasks.py +120 -120
- package/vendor/chatgpt2api/source/api/support.py +134 -134
- package/vendor/chatgpt2api/source/api/system.py +365 -365
- package/vendor/chatgpt2api/source/assets/kunrouai.svg +50 -50
- package/vendor/chatgpt2api/source/config.default.json +108 -108
- package/vendor/chatgpt2api/source/main.py +9 -9
- package/vendor/chatgpt2api/source/pyproject.toml +27 -27
- package/vendor/chatgpt2api/source/scripts/init_proxy_config.py +186 -186
- package/vendor/chatgpt2api/source/scripts/migrate_storage.py +163 -163
- package/vendor/chatgpt2api/source/scripts/privoxy-warp.conf +13 -13
- package/vendor/chatgpt2api/source/scripts/test_outlook_token_mailbox.py +385 -385
- package/vendor/chatgpt2api/source/scripts/test_storage.py +129 -129
- package/vendor/chatgpt2api/source/scripts/verify_oauth_refresh.py +92 -92
- package/vendor/chatgpt2api/source/services/account_service.py +1757 -1757
- package/vendor/chatgpt2api/source/services/auth_service.py +253 -253
- package/vendor/chatgpt2api/source/services/backup_service.py +671 -671
- package/vendor/chatgpt2api/source/services/config.py +667 -667
- package/vendor/chatgpt2api/source/services/content_filter.py +242 -242
- package/vendor/chatgpt2api/source/services/cpa_service.py +315 -315
- package/vendor/chatgpt2api/source/services/editable_file_task_service.py +260 -260
- package/vendor/chatgpt2api/source/services/image_service.py +376 -376
- package/vendor/chatgpt2api/source/services/image_storage_service.py +405 -405
- package/vendor/chatgpt2api/source/services/image_tags_service.py +76 -76
- package/vendor/chatgpt2api/source/services/image_task_service.py +550 -550
- package/vendor/chatgpt2api/source/services/log_service.py +337 -337
- package/vendor/chatgpt2api/source/services/model_service.py +182 -182
- package/vendor/chatgpt2api/source/services/oauth_login_service.py +267 -267
- package/vendor/chatgpt2api/source/services/openai_backend_api.py +2763 -2763
- package/vendor/chatgpt2api/source/services/openai_oauth.py +32 -32
- package/vendor/chatgpt2api/source/services/protocol/__init__.py +2 -2
- package/vendor/chatgpt2api/source/services/protocol/anthropic_v1_messages.py +307 -307
- package/vendor/chatgpt2api/source/services/protocol/chat_completion_cache.py +238 -238
- package/vendor/chatgpt2api/source/services/protocol/conversation.py +1644 -1644
- package/vendor/chatgpt2api/source/services/protocol/error_response.py +124 -124
- package/vendor/chatgpt2api/source/services/protocol/openai_search.py +19 -19
- package/vendor/chatgpt2api/source/services/protocol/openai_v1_chat_complete.py +316 -316
- package/vendor/chatgpt2api/source/services/protocol/openai_v1_image_edit.py +86 -86
- package/vendor/chatgpt2api/source/services/protocol/openai_v1_image_generations.py +42 -42
- package/vendor/chatgpt2api/source/services/protocol/openai_v1_models.py +53 -53
- package/vendor/chatgpt2api/source/services/protocol/openai_v1_response.py +453 -453
- package/vendor/chatgpt2api/source/services/protocol/web_search_tool.py +160 -160
- package/vendor/chatgpt2api/source/services/proxy_service.py +648 -648
- package/vendor/chatgpt2api/source/services/storage/__init__.py +5 -5
- package/vendor/chatgpt2api/source/services/storage/base.py +38 -38
- package/vendor/chatgpt2api/source/services/storage/database_storage.py +194 -194
- package/vendor/chatgpt2api/source/services/storage/factory.py +104 -104
- package/vendor/chatgpt2api/source/services/storage/git_storage.py +183 -183
- package/vendor/chatgpt2api/source/services/storage/json_storage.py +95 -95
- package/vendor/chatgpt2api/source/services/sub2api_service.py +531 -531
- package/vendor/chatgpt2api/source/utils/helper.py +469 -469
- package/vendor/chatgpt2api/source/utils/image_tokens.py +346 -346
- package/vendor/chatgpt2api/source/utils/log.py +110 -110
- package/vendor/chatgpt2api/source/utils/pkce.py +19 -19
- package/vendor/chatgpt2api/source/utils/pow.py +207 -207
- package/vendor/chatgpt2api/source/utils/sentinel.py +159 -159
- package/vendor/chatgpt2api/source/utils/turnstile.py +184 -184
- package/vendor/chatgpt2api/source/uv.lock +904 -904
- package/vendor/chatgpt2api/source/web/bun.lock +1501 -1501
- package/vendor/chatgpt2api/source/web/components.json +24 -24
- package/vendor/chatgpt2api/source/web/eslint.config.mjs +17 -17
- package/vendor/chatgpt2api/source/web/next.config.ts +40 -40
- package/vendor/chatgpt2api/source/web/package.json +58 -58
- package/vendor/chatgpt2api/source/web/postcss.config.mjs +5 -5
- package/vendor/chatgpt2api/source/web/src/app/accounts/components/account-import-dialog.tsx +925 -925
- package/vendor/chatgpt2api/source/web/src/app/accounts/page.tsx +1307 -1307
- package/vendor/chatgpt2api/source/web/src/app/debug/components/chat-panel.tsx +224 -224
- package/vendor/chatgpt2api/source/web/src/app/debug/components/editable-file-panel.tsx +444 -444
- package/vendor/chatgpt2api/source/web/src/app/debug/components/ppt-panel.tsx +9 -9
- package/vendor/chatgpt2api/source/web/src/app/debug/components/psd-panel.tsx +9 -9
- package/vendor/chatgpt2api/source/web/src/app/debug/components/search-panel.tsx +172 -172
- package/vendor/chatgpt2api/source/web/src/app/debug/components/skill-panel.tsx +169 -169
- package/vendor/chatgpt2api/source/web/src/app/debug/components/types.ts +39 -39
- package/vendor/chatgpt2api/source/web/src/app/debug/page.tsx +59 -59
- package/vendor/chatgpt2api/source/web/src/app/globals.css +230 -230
- package/vendor/chatgpt2api/source/web/src/app/image/components/image-composer.tsx +542 -542
- package/vendor/chatgpt2api/source/web/src/app/image/components/image-results.tsx +554 -554
- package/vendor/chatgpt2api/source/web/src/app/image/components/image-sidebar.tsx +176 -176
- package/vendor/chatgpt2api/source/web/src/app/image/page.tsx +1799 -1799
- package/vendor/chatgpt2api/source/web/src/app/image-manager/page.tsx +732 -732
- package/vendor/chatgpt2api/source/web/src/app/layout.tsx +50 -50
- package/vendor/chatgpt2api/source/web/src/app/login/page.tsx +101 -101
- package/vendor/chatgpt2api/source/web/src/app/logs/page.tsx +356 -356
- package/vendor/chatgpt2api/source/web/src/app/page.tsx +30 -30
- package/vendor/chatgpt2api/source/web/src/app/settings/components/api-docs-card.tsx +341 -341
- package/vendor/chatgpt2api/source/web/src/app/settings/components/backup-settings-card.tsx +447 -447
- package/vendor/chatgpt2api/source/web/src/app/settings/components/base-url-card.tsx +71 -71
- package/vendor/chatgpt2api/source/web/src/app/settings/components/config-card.tsx +491 -491
- package/vendor/chatgpt2api/source/web/src/app/settings/components/cpa-pool-dialog.tsx +108 -108
- package/vendor/chatgpt2api/source/web/src/app/settings/components/cpa-pools-card.tsx +169 -169
- package/vendor/chatgpt2api/source/web/src/app/settings/components/import-browser-dialog.tsx +185 -185
- package/vendor/chatgpt2api/source/web/src/app/settings/components/proxy-runtime-card.tsx +379 -379
- package/vendor/chatgpt2api/source/web/src/app/settings/components/proxy-settings-card.tsx +127 -127
- package/vendor/chatgpt2api/source/web/src/app/settings/components/proxy-settings.tsx +200 -200
- package/vendor/chatgpt2api/source/web/src/app/settings/components/settings-header.tsx +12 -12
- package/vendor/chatgpt2api/source/web/src/app/settings/components/sub2api-connections.tsx +886 -886
- package/vendor/chatgpt2api/source/web/src/app/settings/components/third-party-apps-card.tsx +81 -81
- package/vendor/chatgpt2api/source/web/src/app/settings/components/user-keys-card.tsx +422 -422
- package/vendor/chatgpt2api/source/web/src/app/settings/page.tsx +135 -135
- package/vendor/chatgpt2api/source/web/src/app/settings/store.ts +1069 -1069
- package/vendor/chatgpt2api/source/web/src/components/date-range-filter.tsx +49 -49
- package/vendor/chatgpt2api/source/web/src/components/header-actions.tsx +24 -24
- package/vendor/chatgpt2api/source/web/src/components/image-lightbox.tsx +434 -434
- package/vendor/chatgpt2api/source/web/src/components/image-thumbnail.tsx +46 -46
- package/vendor/chatgpt2api/source/web/src/components/theme-script.tsx +18 -18
- package/vendor/chatgpt2api/source/web/src/components/theme-toggle.tsx +14 -14
- package/vendor/chatgpt2api/source/web/src/components/top-nav.tsx +263 -263
- package/vendor/chatgpt2api/source/web/src/components/ui/animated-theme-toggler.tsx +256 -256
- package/vendor/chatgpt2api/source/web/src/components/ui/badge.tsx +41 -41
- package/vendor/chatgpt2api/source/web/src/components/ui/button.tsx +59 -59
- package/vendor/chatgpt2api/source/web/src/components/ui/calendar.tsx +46 -46
- package/vendor/chatgpt2api/source/web/src/components/ui/card.tsx +61 -61
- package/vendor/chatgpt2api/source/web/src/components/ui/checkbox.tsx +27 -27
- package/vendor/chatgpt2api/source/web/src/components/ui/dia-text-reveal.tsx +267 -267
- package/vendor/chatgpt2api/source/web/src/components/ui/dialog.tsx +132 -132
- package/vendor/chatgpt2api/source/web/src/components/ui/field.tsx +13 -13
- package/vendor/chatgpt2api/source/web/src/components/ui/input.tsx +19 -19
- package/vendor/chatgpt2api/source/web/src/components/ui/label.tsx +24 -24
- package/vendor/chatgpt2api/source/web/src/components/ui/morphing-text.tsx +149 -149
- package/vendor/chatgpt2api/source/web/src/components/ui/popover.tsx +38 -38
- package/vendor/chatgpt2api/source/web/src/components/ui/select.tsx +175 -175
- package/vendor/chatgpt2api/source/web/src/components/ui/sheet.tsx +143 -143
- package/vendor/chatgpt2api/source/web/src/components/ui/table.tsx +29 -29
- package/vendor/chatgpt2api/source/web/src/components/ui/tabs.tsx +91 -91
- package/vendor/chatgpt2api/source/web/src/components/ui/text-3d-flip.tsx +269 -269
- package/vendor/chatgpt2api/source/web/src/components/ui/textarea.tsx +18 -18
- package/vendor/chatgpt2api/source/web/src/components/version-release-dialog.tsx +127 -127
- package/vendor/chatgpt2api/source/web/src/constants/common-env.ts +6 -6
- package/vendor/chatgpt2api/source/web/src/hooks/use-version-check.ts +85 -85
- package/vendor/chatgpt2api/source/web/src/lib/api.ts +926 -926
- package/vendor/chatgpt2api/source/web/src/lib/auth-session.ts +26 -26
- package/vendor/chatgpt2api/source/web/src/lib/release.ts +25 -25
- package/vendor/chatgpt2api/source/web/src/lib/request.ts +93 -93
- package/vendor/chatgpt2api/source/web/src/lib/use-auth-guard.ts +89 -89
- package/vendor/chatgpt2api/source/web/src/lib/utils.ts +6 -6
- package/vendor/chatgpt2api/source/web/src/store/auth.ts +112 -112
- package/vendor/chatgpt2api/source/web/src/store/editable-file-history.ts +33 -33
- package/vendor/chatgpt2api/source/web/src/store/image-conversations.ts +318 -318
- package/vendor/chatgpt2api/source/web/tsconfig.json +42 -42
|
@@ -1,667 +1,667 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
import copy
|
|
4
|
-
from dataclasses import dataclass
|
|
5
|
-
import json
|
|
6
|
-
import os
|
|
7
|
-
import sys
|
|
8
|
-
from pathlib import Path
|
|
9
|
-
import time
|
|
10
|
-
|
|
11
|
-
from services.storage.base import StorageBackend
|
|
12
|
-
|
|
13
|
-
BASE_DIR = Path(__file__).resolve().parents[1]
|
|
14
|
-
DATA_DIR = BASE_DIR / "data"
|
|
15
|
-
CONFIG_FILE = BASE_DIR / "config.json"
|
|
16
|
-
VERSION_FILE = BASE_DIR / "VERSION"
|
|
17
|
-
BACKUP_STATE_FILE = DATA_DIR / "backup_state.json"
|
|
18
|
-
|
|
19
|
-
DEFAULT_BACKUP_INCLUDE = {
|
|
20
|
-
"config": True,
|
|
21
|
-
"cpa": True,
|
|
22
|
-
"sub2api": True,
|
|
23
|
-
"logs": True,
|
|
24
|
-
"image_tasks": True,
|
|
25
|
-
"accounts_snapshot": True,
|
|
26
|
-
"auth_keys_snapshot": True,
|
|
27
|
-
"images": False,
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
DEFAULT_IMAGE_STORAGE = {
|
|
31
|
-
"enabled": False,
|
|
32
|
-
"mode": "local",
|
|
33
|
-
"webdav_url": "",
|
|
34
|
-
"webdav_username": "",
|
|
35
|
-
"webdav_password": "",
|
|
36
|
-
"webdav_root_path": "chatgpt2api/images",
|
|
37
|
-
"public_base_url": "",
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
DEFAULT_CHAT_COMPLETION_CACHE = {
|
|
41
|
-
"enabled": True,
|
|
42
|
-
"ttl_seconds": 60,
|
|
43
|
-
"max_entries": 256,
|
|
44
|
-
"dedupe_inflight": True,
|
|
45
|
-
"stream_cache": True,
|
|
46
|
-
"normalize_messages": True,
|
|
47
|
-
"drop_adjacent_duplicates": True,
|
|
48
|
-
"drop_assistant_history": False,
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
DEFAULT_PROXY_RUNTIME_USER_AGENT = (
|
|
52
|
-
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
|
|
53
|
-
"AppleWebKit/537.36 (KHTML, like Gecko) "
|
|
54
|
-
"Chrome/145.0.0.0 Safari/537.36"
|
|
55
|
-
)
|
|
56
|
-
|
|
57
|
-
DEFAULT_PROXY_RUNTIME = {
|
|
58
|
-
"enabled": False,
|
|
59
|
-
"egress_mode": "direct",
|
|
60
|
-
"proxy_url": "",
|
|
61
|
-
"resource_proxy_url": "",
|
|
62
|
-
"skip_ssl_verify": False,
|
|
63
|
-
"reset_session_status_codes": [403],
|
|
64
|
-
"clearance": {
|
|
65
|
-
"enabled": False,
|
|
66
|
-
"mode": "none",
|
|
67
|
-
"cf_cookies": "",
|
|
68
|
-
"cf_clearance": "",
|
|
69
|
-
"user_agent": DEFAULT_PROXY_RUNTIME_USER_AGENT,
|
|
70
|
-
"browser": "chrome",
|
|
71
|
-
"flaresolverr_url": "",
|
|
72
|
-
"timeout_sec": 60,
|
|
73
|
-
"refresh_interval": 3600,
|
|
74
|
-
"warm_up_on_start": False,
|
|
75
|
-
},
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
DEFAULT_THIRD_PARTY_APPS = {
|
|
79
|
-
"infinite_canvas": {
|
|
80
|
-
"enabled": False,
|
|
81
|
-
"url": "https://canvas.best",
|
|
82
|
-
},
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
def _normalize_bool(value: object, default: bool = False) -> bool:
|
|
87
|
-
if isinstance(value, str):
|
|
88
|
-
lowered = value.strip().lower()
|
|
89
|
-
if lowered in {"1", "true", "yes", "on"}:
|
|
90
|
-
return True
|
|
91
|
-
if lowered in {"0", "false", "no", "off"}:
|
|
92
|
-
return False
|
|
93
|
-
return default
|
|
94
|
-
if value is None:
|
|
95
|
-
return default
|
|
96
|
-
return bool(value)
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
def _normalize_positive_int(value: object, default: int, minimum: int = 0) -> int:
|
|
100
|
-
try:
|
|
101
|
-
normalized = int(value)
|
|
102
|
-
except (OverflowError, TypeError, ValueError):
|
|
103
|
-
normalized = default
|
|
104
|
-
return max(minimum, normalized)
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
def _normalize_backup_include(value: object) -> dict[str, bool]:
|
|
108
|
-
source = value if isinstance(value, dict) else {}
|
|
109
|
-
normalized = dict(DEFAULT_BACKUP_INCLUDE)
|
|
110
|
-
for key in normalized:
|
|
111
|
-
normalized[key] = _normalize_bool(source.get(key), normalized[key])
|
|
112
|
-
return normalized
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
def _normalize_backup_settings(value: object) -> dict[str, object]:
|
|
116
|
-
source = value if isinstance(value, dict) else {}
|
|
117
|
-
return {
|
|
118
|
-
"enabled": _normalize_bool(source.get("enabled"), False),
|
|
119
|
-
"provider": "cloudflare_r2",
|
|
120
|
-
"account_id": str(source.get("account_id") or "").strip(),
|
|
121
|
-
"access_key_id": str(source.get("access_key_id") or "").strip(),
|
|
122
|
-
"secret_access_key": str(source.get("secret_access_key") or "").strip(),
|
|
123
|
-
"bucket": str(source.get("bucket") or "").strip(),
|
|
124
|
-
"prefix": str(source.get("prefix") or "backups").strip().strip("/") or "backups",
|
|
125
|
-
"interval_minutes": _normalize_positive_int(source.get("interval_minutes"), 360, 1),
|
|
126
|
-
"rotation_keep": _normalize_positive_int(source.get("rotation_keep"), 10, 0),
|
|
127
|
-
"encrypt": _normalize_bool(source.get("encrypt"), False),
|
|
128
|
-
"passphrase": str(source.get("passphrase") or "").strip(),
|
|
129
|
-
"include": _normalize_backup_include(source.get("include")),
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
def _normalize_backup_state(value: object) -> dict[str, object]:
|
|
134
|
-
source = value if isinstance(value, dict) else {}
|
|
135
|
-
return {
|
|
136
|
-
"last_started_at": str(source.get("last_started_at") or "").strip() or None,
|
|
137
|
-
"last_finished_at": str(source.get("last_finished_at") or "").strip() or None,
|
|
138
|
-
"last_status": str(source.get("last_status") or "idle").strip() or "idle",
|
|
139
|
-
"last_error": str(source.get("last_error") or "").strip() or None,
|
|
140
|
-
"last_object_key": str(source.get("last_object_key") or "").strip() or None,
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
def _normalize_image_storage_settings(value: object) -> dict[str, object]:
|
|
145
|
-
source = value if isinstance(value, dict) else {}
|
|
146
|
-
mode = str(source.get("mode") or "local").strip().lower()
|
|
147
|
-
if mode not in {"local", "webdav", "both"}:
|
|
148
|
-
mode = "local"
|
|
149
|
-
enabled = _normalize_bool(source.get("enabled"), False)
|
|
150
|
-
if not enabled:
|
|
151
|
-
mode = "local"
|
|
152
|
-
root_path = str(source.get("webdav_root_path") or DEFAULT_IMAGE_STORAGE["webdav_root_path"]).strip().strip("/")
|
|
153
|
-
return {
|
|
154
|
-
"enabled": enabled,
|
|
155
|
-
"mode": mode,
|
|
156
|
-
"webdav_url": str(source.get("webdav_url") or "").strip().rstrip("/"),
|
|
157
|
-
"webdav_username": str(source.get("webdav_username") or "").strip(),
|
|
158
|
-
"webdav_password": str(source.get("webdav_password") or "").strip(),
|
|
159
|
-
"webdav_root_path": root_path or str(DEFAULT_IMAGE_STORAGE["webdav_root_path"]),
|
|
160
|
-
"public_base_url": str(source.get("public_base_url") or "").strip().rstrip("/"),
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
def _normalize_chat_completion_cache_settings(value: object) -> dict[str, object]:
|
|
165
|
-
source = value if isinstance(value, dict) else {}
|
|
166
|
-
return {
|
|
167
|
-
"enabled": _normalize_bool(source.get("enabled"), DEFAULT_CHAT_COMPLETION_CACHE["enabled"]),
|
|
168
|
-
"ttl_seconds": _normalize_positive_int(
|
|
169
|
-
source.get("ttl_seconds"),
|
|
170
|
-
int(DEFAULT_CHAT_COMPLETION_CACHE["ttl_seconds"]),
|
|
171
|
-
0,
|
|
172
|
-
),
|
|
173
|
-
"max_entries": _normalize_positive_int(
|
|
174
|
-
source.get("max_entries"),
|
|
175
|
-
int(DEFAULT_CHAT_COMPLETION_CACHE["max_entries"]),
|
|
176
|
-
1,
|
|
177
|
-
),
|
|
178
|
-
"dedupe_inflight": _normalize_bool(
|
|
179
|
-
source.get("dedupe_inflight"),
|
|
180
|
-
bool(DEFAULT_CHAT_COMPLETION_CACHE["dedupe_inflight"]),
|
|
181
|
-
),
|
|
182
|
-
"stream_cache": _normalize_bool(
|
|
183
|
-
source.get("stream_cache"),
|
|
184
|
-
bool(DEFAULT_CHAT_COMPLETION_CACHE["stream_cache"]),
|
|
185
|
-
),
|
|
186
|
-
"normalize_messages": _normalize_bool(
|
|
187
|
-
source.get("normalize_messages"),
|
|
188
|
-
bool(DEFAULT_CHAT_COMPLETION_CACHE["normalize_messages"]),
|
|
189
|
-
),
|
|
190
|
-
"drop_adjacent_duplicates": _normalize_bool(
|
|
191
|
-
source.get("drop_adjacent_duplicates"),
|
|
192
|
-
bool(DEFAULT_CHAT_COMPLETION_CACHE["drop_adjacent_duplicates"]),
|
|
193
|
-
),
|
|
194
|
-
"drop_assistant_history": _normalize_bool(
|
|
195
|
-
source.get("drop_assistant_history"),
|
|
196
|
-
bool(DEFAULT_CHAT_COMPLETION_CACHE["drop_assistant_history"]),
|
|
197
|
-
),
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
def _normalize_status_codes(value: object) -> list[int]:
|
|
202
|
-
items = value if isinstance(value, list) else DEFAULT_PROXY_RUNTIME["reset_session_status_codes"]
|
|
203
|
-
normalized: list[int] = []
|
|
204
|
-
for item in items:
|
|
205
|
-
if isinstance(item, bool):
|
|
206
|
-
continue
|
|
207
|
-
try:
|
|
208
|
-
status = int(item)
|
|
209
|
-
except (OverflowError, TypeError, ValueError):
|
|
210
|
-
continue
|
|
211
|
-
if 100 <= status <= 599 and status not in normalized:
|
|
212
|
-
normalized.append(status)
|
|
213
|
-
if not normalized:
|
|
214
|
-
return list(DEFAULT_PROXY_RUNTIME["reset_session_status_codes"])
|
|
215
|
-
return normalized
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
def _normalize_proxy_runtime_settings(value: object) -> dict[str, object]:
|
|
219
|
-
source = value if isinstance(value, dict) else {}
|
|
220
|
-
default_clearance = DEFAULT_PROXY_RUNTIME["clearance"]
|
|
221
|
-
clearance_source = source.get("clearance") if isinstance(source.get("clearance"), dict) else {}
|
|
222
|
-
|
|
223
|
-
egress_mode = str(source.get("egress_mode") or DEFAULT_PROXY_RUNTIME["egress_mode"]).strip().lower()
|
|
224
|
-
if egress_mode not in {"direct", "single_proxy"}:
|
|
225
|
-
egress_mode = str(DEFAULT_PROXY_RUNTIME["egress_mode"])
|
|
226
|
-
|
|
227
|
-
clearance_mode = str(clearance_source.get("mode") or default_clearance["mode"]).strip().lower()
|
|
228
|
-
if clearance_mode not in {"none", "manual", "flaresolverr"}:
|
|
229
|
-
clearance_mode = str(default_clearance["mode"])
|
|
230
|
-
|
|
231
|
-
user_agent = str(clearance_source.get("user_agent") or default_clearance["user_agent"]).strip()
|
|
232
|
-
browser = str(clearance_source.get("browser") or default_clearance["browser"]).strip()
|
|
233
|
-
|
|
234
|
-
existing_clearance_cookies = str(source.get("_existing_cf_cookies") or "").strip()
|
|
235
|
-
existing_cf_clearance = str(source.get("_existing_cf_clearance") or "").strip()
|
|
236
|
-
cf_cookies = str(clearance_source.get("cf_cookies") or "").strip()
|
|
237
|
-
cf_clearance = str(clearance_source.get("cf_clearance") or "").strip()
|
|
238
|
-
if not cf_cookies and _normalize_bool(clearance_source.get("has_cf_cookies"), False):
|
|
239
|
-
cf_cookies = existing_clearance_cookies
|
|
240
|
-
if not cf_clearance and _normalize_bool(clearance_source.get("has_cf_clearance"), False):
|
|
241
|
-
cf_clearance = existing_cf_clearance
|
|
242
|
-
|
|
243
|
-
return {
|
|
244
|
-
"enabled": _normalize_bool(source.get("enabled"), bool(DEFAULT_PROXY_RUNTIME["enabled"])),
|
|
245
|
-
"egress_mode": egress_mode,
|
|
246
|
-
"proxy_url": str(source.get("proxy_url") or "").strip(),
|
|
247
|
-
"resource_proxy_url": str(source.get("resource_proxy_url") or "").strip(),
|
|
248
|
-
"skip_ssl_verify": _normalize_bool(
|
|
249
|
-
source.get("skip_ssl_verify"),
|
|
250
|
-
bool(DEFAULT_PROXY_RUNTIME["skip_ssl_verify"]),
|
|
251
|
-
),
|
|
252
|
-
"reset_session_status_codes": _normalize_status_codes(source.get("reset_session_status_codes")),
|
|
253
|
-
"clearance": {
|
|
254
|
-
"enabled": _normalize_bool(clearance_source.get("enabled"), bool(default_clearance["enabled"])),
|
|
255
|
-
"mode": clearance_mode,
|
|
256
|
-
"cf_cookies": cf_cookies,
|
|
257
|
-
"cf_clearance": cf_clearance,
|
|
258
|
-
"user_agent": user_agent or str(default_clearance["user_agent"]),
|
|
259
|
-
"browser": browser or str(default_clearance["browser"]),
|
|
260
|
-
"flaresolverr_url": str(clearance_source.get("flaresolverr_url") or "").strip(),
|
|
261
|
-
"timeout_sec": _normalize_positive_int(
|
|
262
|
-
clearance_source.get("timeout_sec"),
|
|
263
|
-
int(default_clearance["timeout_sec"]),
|
|
264
|
-
1,
|
|
265
|
-
),
|
|
266
|
-
"refresh_interval": _normalize_positive_int(
|
|
267
|
-
clearance_source.get("refresh_interval"),
|
|
268
|
-
int(default_clearance["refresh_interval"]),
|
|
269
|
-
60,
|
|
270
|
-
),
|
|
271
|
-
"warm_up_on_start": _normalize_bool(
|
|
272
|
-
clearance_source.get("warm_up_on_start"),
|
|
273
|
-
bool(default_clearance["warm_up_on_start"]),
|
|
274
|
-
),
|
|
275
|
-
},
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
def _normalize_third_party_apps_settings(value: object) -> dict[str, object]:
|
|
280
|
-
source = value if isinstance(value, dict) else {}
|
|
281
|
-
canvas_source = source.get("infinite_canvas") if isinstance(source.get("infinite_canvas"), dict) else {}
|
|
282
|
-
return {
|
|
283
|
-
"infinite_canvas": {
|
|
284
|
-
"enabled": _normalize_bool(canvas_source.get("enabled"), False),
|
|
285
|
-
"url": str(canvas_source.get("url") or DEFAULT_THIRD_PARTY_APPS["infinite_canvas"]["url"]).strip(),
|
|
286
|
-
},
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
def _validate_image_storage_settings(settings: dict[str, object]) -> None:
|
|
291
|
-
if not _normalize_bool(settings.get("enabled"), False):
|
|
292
|
-
return
|
|
293
|
-
if not str(settings.get("webdav_url") or "").strip():
|
|
294
|
-
raise ValueError("启用 WebDAV 图片存储后必须填写 WebDAV URL")
|
|
295
|
-
if not str(settings.get("webdav_password") or "").strip():
|
|
296
|
-
raise ValueError("启用 WebDAV 图片存储后必须填写 WebDAV 密码")
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
@dataclass(frozen=True)
|
|
300
|
-
class LoadedSettings:
|
|
301
|
-
auth_key: str
|
|
302
|
-
refresh_account_interval_minute: int
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
def _normalize_auth_key(value: object) -> str:
|
|
306
|
-
return str(value or "").strip()
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
def _is_invalid_auth_key(value: object) -> bool:
|
|
310
|
-
return _normalize_auth_key(value) == ""
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
def _read_json_object(path: Path, *, name: str) -> dict[str, object]:
|
|
314
|
-
if not path.exists():
|
|
315
|
-
return {}
|
|
316
|
-
if path.is_dir():
|
|
317
|
-
print(
|
|
318
|
-
f"Warning: {name} at '{path}' is a directory, ignoring it and falling back to other configuration sources.",
|
|
319
|
-
file=sys.stderr,
|
|
320
|
-
)
|
|
321
|
-
return {}
|
|
322
|
-
try:
|
|
323
|
-
data = json.loads(path.read_text(encoding="utf-8"))
|
|
324
|
-
except Exception:
|
|
325
|
-
return {}
|
|
326
|
-
return data if isinstance(data, dict) else {}
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
def _load_settings() -> LoadedSettings:
|
|
330
|
-
DATA_DIR.mkdir(parents=True, exist_ok=True)
|
|
331
|
-
raw_config = _read_json_object(CONFIG_FILE, name="config.json")
|
|
332
|
-
auth_key = _normalize_auth_key(os.getenv("CHATGPT2API_AUTH_KEY") or raw_config.get("auth-key"))
|
|
333
|
-
if _is_invalid_auth_key(auth_key):
|
|
334
|
-
raise ValueError(
|
|
335
|
-
"❌ auth-key 未设置!\n"
|
|
336
|
-
"请在环境变量 CHATGPT2API_AUTH_KEY 中设置,或者在 config.json 中填写 auth-key。"
|
|
337
|
-
)
|
|
338
|
-
|
|
339
|
-
try:
|
|
340
|
-
refresh_interval = int(raw_config.get("refresh_account_interval_minute", 5))
|
|
341
|
-
except (TypeError, ValueError):
|
|
342
|
-
refresh_interval = 5
|
|
343
|
-
|
|
344
|
-
return LoadedSettings(
|
|
345
|
-
auth_key=auth_key,
|
|
346
|
-
refresh_account_interval_minute=refresh_interval,
|
|
347
|
-
)
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
class ConfigStore:
|
|
351
|
-
def __init__(self, path: Path):
|
|
352
|
-
self.path = path
|
|
353
|
-
DATA_DIR.mkdir(parents=True, exist_ok=True)
|
|
354
|
-
self.data = self._load()
|
|
355
|
-
self._storage_backend: StorageBackend | None = None
|
|
356
|
-
if _is_invalid_auth_key(self.auth_key):
|
|
357
|
-
raise ValueError(
|
|
358
|
-
"❌ auth-key 未设置!\n"
|
|
359
|
-
"请按以下任意一种方式解决:\n"
|
|
360
|
-
"1. 在 Render 的 Environment 变量中添加:\n"
|
|
361
|
-
" CHATGPT2API_AUTH_KEY = your_real_auth_key\n"
|
|
362
|
-
"2. 或者在 config.json 中填写:\n"
|
|
363
|
-
' "auth-key": "your_real_auth_key"'
|
|
364
|
-
)
|
|
365
|
-
|
|
366
|
-
def _load(self) -> dict[str, object]:
|
|
367
|
-
return _read_json_object(self.path, name="config.json")
|
|
368
|
-
|
|
369
|
-
def _save(self) -> None:
|
|
370
|
-
self.path.write_text(json.dumps(self.data, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
|
371
|
-
|
|
372
|
-
@property
|
|
373
|
-
def auth_key(self) -> str:
|
|
374
|
-
return _normalize_auth_key(os.getenv("CHATGPT2API_AUTH_KEY") or self.data.get("auth-key"))
|
|
375
|
-
|
|
376
|
-
@property
|
|
377
|
-
def accounts_file(self) -> Path:
|
|
378
|
-
return DATA_DIR / "accounts.json"
|
|
379
|
-
|
|
380
|
-
@property
|
|
381
|
-
def refresh_account_interval_minute(self) -> int:
|
|
382
|
-
try:
|
|
383
|
-
return int(self.data.get("refresh_account_interval_minute", 5))
|
|
384
|
-
except (TypeError, ValueError):
|
|
385
|
-
return 5
|
|
386
|
-
|
|
387
|
-
@property
|
|
388
|
-
def image_retention_days(self) -> int:
|
|
389
|
-
try:
|
|
390
|
-
return max(1, int(self.data.get("image_retention_days", 30)))
|
|
391
|
-
except (TypeError, ValueError):
|
|
392
|
-
return 30
|
|
393
|
-
|
|
394
|
-
@property
|
|
395
|
-
def image_poll_timeout_secs(self) -> int:
|
|
396
|
-
try:
|
|
397
|
-
return max(1, int(self.data.get("image_poll_timeout_secs", 120)))
|
|
398
|
-
except (TypeError, ValueError):
|
|
399
|
-
return 120
|
|
400
|
-
|
|
401
|
-
@property
|
|
402
|
-
def image_poll_interval_secs(self) -> float:
|
|
403
|
-
try:
|
|
404
|
-
return max(0.5, float(self.data.get("image_poll_interval_secs", 10.0)))
|
|
405
|
-
except (TypeError, ValueError):
|
|
406
|
-
return 10.0
|
|
407
|
-
|
|
408
|
-
@property
|
|
409
|
-
def image_poll_initial_wait_secs(self) -> float:
|
|
410
|
-
"""Image generation upstream takes ~30s; polling immediately wastes requests
|
|
411
|
-
and trips a transient 429. Default 10s gives the conversation document time
|
|
412
|
-
to commit before the first poll."""
|
|
413
|
-
try:
|
|
414
|
-
return max(0.0, float(self.data.get("image_poll_initial_wait_secs", 10.0)))
|
|
415
|
-
except (TypeError, ValueError):
|
|
416
|
-
return 10.0
|
|
417
|
-
|
|
418
|
-
@property
|
|
419
|
-
def image_account_concurrency(self) -> int:
|
|
420
|
-
try:
|
|
421
|
-
return max(1, int(self.data.get("image_account_concurrency", 3)))
|
|
422
|
-
except (TypeError, ValueError):
|
|
423
|
-
return 3
|
|
424
|
-
|
|
425
|
-
@property
|
|
426
|
-
def image_parallel_generation(self) -> bool:
|
|
427
|
-
value = self.data.get("image_parallel_generation", True)
|
|
428
|
-
if isinstance(value, str):
|
|
429
|
-
return value.strip().lower() in {"1", "true", "yes", "on"}
|
|
430
|
-
return bool(value)
|
|
431
|
-
|
|
432
|
-
@property
|
|
433
|
-
def image_settle_enabled(self) -> bool:
|
|
434
|
-
"""图片二次确认机制:找到 file_ids 后等待一段时间再次确认。"""
|
|
435
|
-
value = self.data.get("image_settle_enabled", True)
|
|
436
|
-
if isinstance(value, str):
|
|
437
|
-
return value.strip().lower() in {"1", "true", "yes", "on"}
|
|
438
|
-
return bool(value)
|
|
439
|
-
|
|
440
|
-
@property
|
|
441
|
-
def image_check_before_hit_enabled(self) -> bool:
|
|
442
|
-
"""先check再hit:通过轮询确认 file_ids 存在后再返回,而非仅依赖 SSE 事件。"""
|
|
443
|
-
value = self.data.get("image_check_before_hit_enabled", True)
|
|
444
|
-
if isinstance(value, str):
|
|
445
|
-
return value.strip().lower() in {"1", "true", "yes", "on"}
|
|
446
|
-
return bool(value)
|
|
447
|
-
|
|
448
|
-
@property
|
|
449
|
-
def image_remove_conversation_after_result(self) -> bool:
|
|
450
|
-
"""出图成功后异步隐藏 ChatGPT 本地对话记录。"""
|
|
451
|
-
value = self.data.get("image_remove_conversation_after_result", False)
|
|
452
|
-
if isinstance(value, str):
|
|
453
|
-
return value.strip().lower() in {"1", "true", "yes", "on"}
|
|
454
|
-
return bool(value)
|
|
455
|
-
|
|
456
|
-
@property
|
|
457
|
-
def image_remove_conversation_always(self) -> bool:
|
|
458
|
-
"""无论是否出图,画图请求结束后都异步隐藏 ChatGPT 本地对话记录。"""
|
|
459
|
-
return _normalize_bool(self.data.get("image_remove_conversation_always"), False)
|
|
460
|
-
|
|
461
|
-
@property
|
|
462
|
-
def image_settle_secs(self) -> float:
|
|
463
|
-
"""二次确认等待时间(秒)。"""
|
|
464
|
-
try:
|
|
465
|
-
return max(0.5, float(self.data.get("image_settle_secs", 2.0)))
|
|
466
|
-
except (TypeError, ValueError):
|
|
467
|
-
return 2.0
|
|
468
|
-
|
|
469
|
-
@property
|
|
470
|
-
def auto_remove_invalid_accounts(self) -> bool:
|
|
471
|
-
value = self.data.get("auto_remove_invalid_accounts", False)
|
|
472
|
-
if isinstance(value, str):
|
|
473
|
-
return value.strip().lower() in {"1", "true", "yes", "on"}
|
|
474
|
-
return bool(value)
|
|
475
|
-
|
|
476
|
-
@property
|
|
477
|
-
def auto_remove_rate_limited_accounts(self) -> bool:
|
|
478
|
-
value = self.data.get("auto_remove_rate_limited_accounts", False)
|
|
479
|
-
if isinstance(value, str):
|
|
480
|
-
return value.strip().lower() in {"1", "true", "yes", "on"}
|
|
481
|
-
return bool(value)
|
|
482
|
-
|
|
483
|
-
@property
|
|
484
|
-
def auto_relogin_after_refresh(self) -> bool:
|
|
485
|
-
value = self.data.get("auto_relogin_after_refresh", False)
|
|
486
|
-
if isinstance(value, str):
|
|
487
|
-
return value.strip().lower() in {"1", "true", "yes", "on"}
|
|
488
|
-
return bool(value)
|
|
489
|
-
|
|
490
|
-
@property
|
|
491
|
-
def log_levels(self) -> list[str]:
|
|
492
|
-
levels = self.data.get("log_levels")
|
|
493
|
-
if not isinstance(levels, list):
|
|
494
|
-
return []
|
|
495
|
-
allowed = {"debug", "info", "warning", "error"}
|
|
496
|
-
return [level for item in levels if (level := str(item or "").strip().lower()) in allowed]
|
|
497
|
-
|
|
498
|
-
@property
|
|
499
|
-
def sensitive_words(self) -> list[str]:
|
|
500
|
-
words = self.data.get("sensitive_words")
|
|
501
|
-
return [word for item in words if (word := str(item or "").strip())] if isinstance(words, list) else []
|
|
502
|
-
|
|
503
|
-
@property
|
|
504
|
-
def ai_review(self) -> dict[str, object]:
|
|
505
|
-
value = self.data.get("ai_review")
|
|
506
|
-
return value if isinstance(value, dict) else {}
|
|
507
|
-
|
|
508
|
-
@property
|
|
509
|
-
def global_system_prompt(self) -> str:
|
|
510
|
-
return str(self.data.get("global_system_prompt") or "").strip()
|
|
511
|
-
|
|
512
|
-
@property
|
|
513
|
-
def default_upstream_model_name(self) -> str:
|
|
514
|
-
return str(self.data.get("default_upstream_model_name") or "gpt-5-5").strip()
|
|
515
|
-
|
|
516
|
-
@property
|
|
517
|
-
def default_thinking_effort(self) -> str:
|
|
518
|
-
value = str(self.data.get("default_thinking_effort") or "auto").strip().lower()
|
|
519
|
-
return value if value in {"auto", "standard", "extended", "max"} else "auto"
|
|
520
|
-
|
|
521
|
-
@property
|
|
522
|
-
def images_dir(self) -> Path:
|
|
523
|
-
path = DATA_DIR / "images"
|
|
524
|
-
path.mkdir(parents=True, exist_ok=True)
|
|
525
|
-
return path
|
|
526
|
-
|
|
527
|
-
@property
|
|
528
|
-
def image_thumbnails_dir(self) -> Path:
|
|
529
|
-
path = DATA_DIR / "image_thumbnails"
|
|
530
|
-
path.mkdir(parents=True, exist_ok=True)
|
|
531
|
-
return path
|
|
532
|
-
|
|
533
|
-
def cleanup_old_images(self) -> int:
|
|
534
|
-
cutoff = time.time() - self.image_retention_days * 86400
|
|
535
|
-
removed = 0
|
|
536
|
-
for path in self.images_dir.rglob("*"):
|
|
537
|
-
if path.is_file() and path.stat().st_mtime < cutoff:
|
|
538
|
-
path.unlink()
|
|
539
|
-
removed += 1
|
|
540
|
-
for path in sorted((p for p in self.images_dir.rglob("*") if p.is_dir()), key=lambda p: len(p.parts), reverse=True):
|
|
541
|
-
try:
|
|
542
|
-
path.rmdir()
|
|
543
|
-
except OSError:
|
|
544
|
-
pass
|
|
545
|
-
return removed
|
|
546
|
-
|
|
547
|
-
@property
|
|
548
|
-
def base_url(self) -> str:
|
|
549
|
-
return str(
|
|
550
|
-
os.getenv("CHATGPT2API_BASE_URL")
|
|
551
|
-
or self.data.get("base_url")
|
|
552
|
-
or ""
|
|
553
|
-
).strip().rstrip("/")
|
|
554
|
-
|
|
555
|
-
@property
|
|
556
|
-
def app_version(self) -> str:
|
|
557
|
-
try:
|
|
558
|
-
value = VERSION_FILE.read_text(encoding="utf-8").strip()
|
|
559
|
-
except FileNotFoundError:
|
|
560
|
-
return "0.0.0"
|
|
561
|
-
return value or "0.0.0"
|
|
562
|
-
|
|
563
|
-
def get(self) -> dict[str, object]:
|
|
564
|
-
data = dict(self.data)
|
|
565
|
-
data["refresh_account_interval_minute"] = self.refresh_account_interval_minute
|
|
566
|
-
data["image_retention_days"] = self.image_retention_days
|
|
567
|
-
data["image_poll_timeout_secs"] = self.image_poll_timeout_secs
|
|
568
|
-
data["image_poll_interval_secs"] = self.image_poll_interval_secs
|
|
569
|
-
data["image_poll_initial_wait_secs"] = self.image_poll_initial_wait_secs
|
|
570
|
-
data["image_account_concurrency"] = self.image_account_concurrency
|
|
571
|
-
data["image_parallel_generation"] = self.image_parallel_generation
|
|
572
|
-
data["image_remove_conversation_after_result"] = self.image_remove_conversation_after_result
|
|
573
|
-
data["image_remove_conversation_always"] = self.image_remove_conversation_always
|
|
574
|
-
data["auto_remove_invalid_accounts"] = self.auto_remove_invalid_accounts
|
|
575
|
-
data["auto_remove_rate_limited_accounts"] = self.auto_remove_rate_limited_accounts
|
|
576
|
-
data["auto_relogin_after_refresh"] = self.auto_relogin_after_refresh
|
|
577
|
-
data["log_levels"] = self.log_levels
|
|
578
|
-
data["sensitive_words"] = self.sensitive_words
|
|
579
|
-
data["ai_review"] = self.ai_review
|
|
580
|
-
data["global_system_prompt"] = self.global_system_prompt
|
|
581
|
-
data["default_upstream_model_name"] = self.default_upstream_model_name
|
|
582
|
-
data["default_thinking_effort"] = self.default_thinking_effort
|
|
583
|
-
data["backup"] = self.get_backup_settings()
|
|
584
|
-
data["image_storage"] = self.get_image_storage_settings()
|
|
585
|
-
data["chat_completion_cache"] = self.get_chat_completion_cache_settings()
|
|
586
|
-
data["proxy_runtime"] = self.get_public_proxy_runtime_settings()
|
|
587
|
-
data["third_party_apps"] = self.get_third_party_apps_settings()
|
|
588
|
-
data.pop("auth-key", None)
|
|
589
|
-
return data
|
|
590
|
-
|
|
591
|
-
def get_proxy_settings(self) -> str:
|
|
592
|
-
return str(self.data.get("proxy") or "").strip()
|
|
593
|
-
|
|
594
|
-
def get_proxy_runtime_settings(self) -> dict[str, object]:
|
|
595
|
-
return _normalize_proxy_runtime_settings(self.data.get("proxy_runtime"))
|
|
596
|
-
|
|
597
|
-
def get_public_proxy_runtime_settings(self) -> dict[str, object]:
|
|
598
|
-
runtime = copy.deepcopy(self.get_proxy_runtime_settings())
|
|
599
|
-
clearance = runtime.get("clearance") if isinstance(runtime.get("clearance"), dict) else {}
|
|
600
|
-
if isinstance(clearance, dict):
|
|
601
|
-
cf_cookies = str(clearance.get("cf_cookies") or "").strip()
|
|
602
|
-
cf_clearance = str(clearance.get("cf_clearance") or "").strip()
|
|
603
|
-
clearance["cf_cookies"] = ""
|
|
604
|
-
clearance["cf_clearance"] = ""
|
|
605
|
-
clearance["has_cf_cookies"] = bool(cf_cookies)
|
|
606
|
-
clearance["has_cf_clearance"] = bool(cf_clearance)
|
|
607
|
-
return runtime
|
|
608
|
-
|
|
609
|
-
def get_third_party_apps_settings(self) -> dict[str, object]:
|
|
610
|
-
return _normalize_third_party_apps_settings(self.data.get("third_party_apps"))
|
|
611
|
-
|
|
612
|
-
def update(self, data: dict[str, object]) -> dict[str, object]:
|
|
613
|
-
next_data = dict(self.data)
|
|
614
|
-
next_data.update(dict(data or {}))
|
|
615
|
-
if "backup" in next_data:
|
|
616
|
-
next_data["backup"] = _normalize_backup_settings(next_data.get("backup"))
|
|
617
|
-
if "image_storage" in next_data:
|
|
618
|
-
next_data["image_storage"] = _normalize_image_storage_settings(next_data.get("image_storage"))
|
|
619
|
-
_validate_image_storage_settings(next_data["image_storage"])
|
|
620
|
-
if "chat_completion_cache" in next_data:
|
|
621
|
-
next_data["chat_completion_cache"] = _normalize_chat_completion_cache_settings(
|
|
622
|
-
next_data.get("chat_completion_cache")
|
|
623
|
-
)
|
|
624
|
-
if "third_party_apps" in next_data:
|
|
625
|
-
next_data["third_party_apps"] = _normalize_third_party_apps_settings(next_data.get("third_party_apps"))
|
|
626
|
-
if "proxy_runtime" in next_data:
|
|
627
|
-
incoming_runtime = next_data.get("proxy_runtime")
|
|
628
|
-
if isinstance(incoming_runtime, dict):
|
|
629
|
-
previous_clearance = self.get_proxy_runtime_settings().get("clearance")
|
|
630
|
-
if isinstance(previous_clearance, dict):
|
|
631
|
-
incoming_runtime = dict(incoming_runtime)
|
|
632
|
-
incoming_runtime["_existing_cf_cookies"] = previous_clearance.get("cf_cookies")
|
|
633
|
-
incoming_runtime["_existing_cf_clearance"] = previous_clearance.get("cf_clearance")
|
|
634
|
-
next_data["proxy_runtime"] = _normalize_proxy_runtime_settings(incoming_runtime)
|
|
635
|
-
next_data.pop("backup_state", None)
|
|
636
|
-
self.data = next_data
|
|
637
|
-
self._save()
|
|
638
|
-
return self.get()
|
|
639
|
-
|
|
640
|
-
def get_backup_settings(self) -> dict[str, object]:
|
|
641
|
-
return _normalize_backup_settings(self.data.get("backup"))
|
|
642
|
-
|
|
643
|
-
def get_image_storage_settings(self) -> dict[str, object]:
|
|
644
|
-
return _normalize_image_storage_settings(self.data.get("image_storage"))
|
|
645
|
-
|
|
646
|
-
def get_chat_completion_cache_settings(self) -> dict[str, object]:
|
|
647
|
-
return _normalize_chat_completion_cache_settings(self.data.get("chat_completion_cache"))
|
|
648
|
-
|
|
649
|
-
def get_storage_backend(self) -> StorageBackend:
|
|
650
|
-
"""获取存储后端实例(单例)"""
|
|
651
|
-
if self._storage_backend is None:
|
|
652
|
-
from services.storage.factory import create_storage_backend
|
|
653
|
-
self._storage_backend = create_storage_backend(DATA_DIR)
|
|
654
|
-
return self._storage_backend
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
def load_backup_state() -> dict[str, object]:
|
|
658
|
-
return _normalize_backup_state(_read_json_object(BACKUP_STATE_FILE, name="backup_state.json"))
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
def save_backup_state(state: dict[str, object]) -> dict[str, object]:
|
|
662
|
-
normalized = _normalize_backup_state(state)
|
|
663
|
-
BACKUP_STATE_FILE.write_text(json.dumps(normalized, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
|
664
|
-
return normalized
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
config = ConfigStore(CONFIG_FILE)
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import copy
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
import sys
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
import time
|
|
10
|
+
|
|
11
|
+
from services.storage.base import StorageBackend
|
|
12
|
+
|
|
13
|
+
BASE_DIR = Path(__file__).resolve().parents[1]
|
|
14
|
+
DATA_DIR = BASE_DIR / "data"
|
|
15
|
+
CONFIG_FILE = BASE_DIR / "config.json"
|
|
16
|
+
VERSION_FILE = BASE_DIR / "VERSION"
|
|
17
|
+
BACKUP_STATE_FILE = DATA_DIR / "backup_state.json"
|
|
18
|
+
|
|
19
|
+
DEFAULT_BACKUP_INCLUDE = {
|
|
20
|
+
"config": True,
|
|
21
|
+
"cpa": True,
|
|
22
|
+
"sub2api": True,
|
|
23
|
+
"logs": True,
|
|
24
|
+
"image_tasks": True,
|
|
25
|
+
"accounts_snapshot": True,
|
|
26
|
+
"auth_keys_snapshot": True,
|
|
27
|
+
"images": False,
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
DEFAULT_IMAGE_STORAGE = {
|
|
31
|
+
"enabled": False,
|
|
32
|
+
"mode": "local",
|
|
33
|
+
"webdav_url": "",
|
|
34
|
+
"webdav_username": "",
|
|
35
|
+
"webdav_password": "",
|
|
36
|
+
"webdav_root_path": "chatgpt2api/images",
|
|
37
|
+
"public_base_url": "",
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
DEFAULT_CHAT_COMPLETION_CACHE = {
|
|
41
|
+
"enabled": True,
|
|
42
|
+
"ttl_seconds": 60,
|
|
43
|
+
"max_entries": 256,
|
|
44
|
+
"dedupe_inflight": True,
|
|
45
|
+
"stream_cache": True,
|
|
46
|
+
"normalize_messages": True,
|
|
47
|
+
"drop_adjacent_duplicates": True,
|
|
48
|
+
"drop_assistant_history": False,
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
DEFAULT_PROXY_RUNTIME_USER_AGENT = (
|
|
52
|
+
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
|
|
53
|
+
"AppleWebKit/537.36 (KHTML, like Gecko) "
|
|
54
|
+
"Chrome/145.0.0.0 Safari/537.36"
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
DEFAULT_PROXY_RUNTIME = {
|
|
58
|
+
"enabled": False,
|
|
59
|
+
"egress_mode": "direct",
|
|
60
|
+
"proxy_url": "",
|
|
61
|
+
"resource_proxy_url": "",
|
|
62
|
+
"skip_ssl_verify": False,
|
|
63
|
+
"reset_session_status_codes": [403],
|
|
64
|
+
"clearance": {
|
|
65
|
+
"enabled": False,
|
|
66
|
+
"mode": "none",
|
|
67
|
+
"cf_cookies": "",
|
|
68
|
+
"cf_clearance": "",
|
|
69
|
+
"user_agent": DEFAULT_PROXY_RUNTIME_USER_AGENT,
|
|
70
|
+
"browser": "chrome",
|
|
71
|
+
"flaresolverr_url": "",
|
|
72
|
+
"timeout_sec": 60,
|
|
73
|
+
"refresh_interval": 3600,
|
|
74
|
+
"warm_up_on_start": False,
|
|
75
|
+
},
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
DEFAULT_THIRD_PARTY_APPS = {
|
|
79
|
+
"infinite_canvas": {
|
|
80
|
+
"enabled": False,
|
|
81
|
+
"url": "https://canvas.best",
|
|
82
|
+
},
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _normalize_bool(value: object, default: bool = False) -> bool:
|
|
87
|
+
if isinstance(value, str):
|
|
88
|
+
lowered = value.strip().lower()
|
|
89
|
+
if lowered in {"1", "true", "yes", "on"}:
|
|
90
|
+
return True
|
|
91
|
+
if lowered in {"0", "false", "no", "off"}:
|
|
92
|
+
return False
|
|
93
|
+
return default
|
|
94
|
+
if value is None:
|
|
95
|
+
return default
|
|
96
|
+
return bool(value)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _normalize_positive_int(value: object, default: int, minimum: int = 0) -> int:
|
|
100
|
+
try:
|
|
101
|
+
normalized = int(value)
|
|
102
|
+
except (OverflowError, TypeError, ValueError):
|
|
103
|
+
normalized = default
|
|
104
|
+
return max(minimum, normalized)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _normalize_backup_include(value: object) -> dict[str, bool]:
|
|
108
|
+
source = value if isinstance(value, dict) else {}
|
|
109
|
+
normalized = dict(DEFAULT_BACKUP_INCLUDE)
|
|
110
|
+
for key in normalized:
|
|
111
|
+
normalized[key] = _normalize_bool(source.get(key), normalized[key])
|
|
112
|
+
return normalized
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _normalize_backup_settings(value: object) -> dict[str, object]:
|
|
116
|
+
source = value if isinstance(value, dict) else {}
|
|
117
|
+
return {
|
|
118
|
+
"enabled": _normalize_bool(source.get("enabled"), False),
|
|
119
|
+
"provider": "cloudflare_r2",
|
|
120
|
+
"account_id": str(source.get("account_id") or "").strip(),
|
|
121
|
+
"access_key_id": str(source.get("access_key_id") or "").strip(),
|
|
122
|
+
"secret_access_key": str(source.get("secret_access_key") or "").strip(),
|
|
123
|
+
"bucket": str(source.get("bucket") or "").strip(),
|
|
124
|
+
"prefix": str(source.get("prefix") or "backups").strip().strip("/") or "backups",
|
|
125
|
+
"interval_minutes": _normalize_positive_int(source.get("interval_minutes"), 360, 1),
|
|
126
|
+
"rotation_keep": _normalize_positive_int(source.get("rotation_keep"), 10, 0),
|
|
127
|
+
"encrypt": _normalize_bool(source.get("encrypt"), False),
|
|
128
|
+
"passphrase": str(source.get("passphrase") or "").strip(),
|
|
129
|
+
"include": _normalize_backup_include(source.get("include")),
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _normalize_backup_state(value: object) -> dict[str, object]:
|
|
134
|
+
source = value if isinstance(value, dict) else {}
|
|
135
|
+
return {
|
|
136
|
+
"last_started_at": str(source.get("last_started_at") or "").strip() or None,
|
|
137
|
+
"last_finished_at": str(source.get("last_finished_at") or "").strip() or None,
|
|
138
|
+
"last_status": str(source.get("last_status") or "idle").strip() or "idle",
|
|
139
|
+
"last_error": str(source.get("last_error") or "").strip() or None,
|
|
140
|
+
"last_object_key": str(source.get("last_object_key") or "").strip() or None,
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def _normalize_image_storage_settings(value: object) -> dict[str, object]:
|
|
145
|
+
source = value if isinstance(value, dict) else {}
|
|
146
|
+
mode = str(source.get("mode") or "local").strip().lower()
|
|
147
|
+
if mode not in {"local", "webdav", "both"}:
|
|
148
|
+
mode = "local"
|
|
149
|
+
enabled = _normalize_bool(source.get("enabled"), False)
|
|
150
|
+
if not enabled:
|
|
151
|
+
mode = "local"
|
|
152
|
+
root_path = str(source.get("webdav_root_path") or DEFAULT_IMAGE_STORAGE["webdav_root_path"]).strip().strip("/")
|
|
153
|
+
return {
|
|
154
|
+
"enabled": enabled,
|
|
155
|
+
"mode": mode,
|
|
156
|
+
"webdav_url": str(source.get("webdav_url") or "").strip().rstrip("/"),
|
|
157
|
+
"webdav_username": str(source.get("webdav_username") or "").strip(),
|
|
158
|
+
"webdav_password": str(source.get("webdav_password") or "").strip(),
|
|
159
|
+
"webdav_root_path": root_path or str(DEFAULT_IMAGE_STORAGE["webdav_root_path"]),
|
|
160
|
+
"public_base_url": str(source.get("public_base_url") or "").strip().rstrip("/"),
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def _normalize_chat_completion_cache_settings(value: object) -> dict[str, object]:
|
|
165
|
+
source = value if isinstance(value, dict) else {}
|
|
166
|
+
return {
|
|
167
|
+
"enabled": _normalize_bool(source.get("enabled"), DEFAULT_CHAT_COMPLETION_CACHE["enabled"]),
|
|
168
|
+
"ttl_seconds": _normalize_positive_int(
|
|
169
|
+
source.get("ttl_seconds"),
|
|
170
|
+
int(DEFAULT_CHAT_COMPLETION_CACHE["ttl_seconds"]),
|
|
171
|
+
0,
|
|
172
|
+
),
|
|
173
|
+
"max_entries": _normalize_positive_int(
|
|
174
|
+
source.get("max_entries"),
|
|
175
|
+
int(DEFAULT_CHAT_COMPLETION_CACHE["max_entries"]),
|
|
176
|
+
1,
|
|
177
|
+
),
|
|
178
|
+
"dedupe_inflight": _normalize_bool(
|
|
179
|
+
source.get("dedupe_inflight"),
|
|
180
|
+
bool(DEFAULT_CHAT_COMPLETION_CACHE["dedupe_inflight"]),
|
|
181
|
+
),
|
|
182
|
+
"stream_cache": _normalize_bool(
|
|
183
|
+
source.get("stream_cache"),
|
|
184
|
+
bool(DEFAULT_CHAT_COMPLETION_CACHE["stream_cache"]),
|
|
185
|
+
),
|
|
186
|
+
"normalize_messages": _normalize_bool(
|
|
187
|
+
source.get("normalize_messages"),
|
|
188
|
+
bool(DEFAULT_CHAT_COMPLETION_CACHE["normalize_messages"]),
|
|
189
|
+
),
|
|
190
|
+
"drop_adjacent_duplicates": _normalize_bool(
|
|
191
|
+
source.get("drop_adjacent_duplicates"),
|
|
192
|
+
bool(DEFAULT_CHAT_COMPLETION_CACHE["drop_adjacent_duplicates"]),
|
|
193
|
+
),
|
|
194
|
+
"drop_assistant_history": _normalize_bool(
|
|
195
|
+
source.get("drop_assistant_history"),
|
|
196
|
+
bool(DEFAULT_CHAT_COMPLETION_CACHE["drop_assistant_history"]),
|
|
197
|
+
),
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def _normalize_status_codes(value: object) -> list[int]:
|
|
202
|
+
items = value if isinstance(value, list) else DEFAULT_PROXY_RUNTIME["reset_session_status_codes"]
|
|
203
|
+
normalized: list[int] = []
|
|
204
|
+
for item in items:
|
|
205
|
+
if isinstance(item, bool):
|
|
206
|
+
continue
|
|
207
|
+
try:
|
|
208
|
+
status = int(item)
|
|
209
|
+
except (OverflowError, TypeError, ValueError):
|
|
210
|
+
continue
|
|
211
|
+
if 100 <= status <= 599 and status not in normalized:
|
|
212
|
+
normalized.append(status)
|
|
213
|
+
if not normalized:
|
|
214
|
+
return list(DEFAULT_PROXY_RUNTIME["reset_session_status_codes"])
|
|
215
|
+
return normalized
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def _normalize_proxy_runtime_settings(value: object) -> dict[str, object]:
|
|
219
|
+
source = value if isinstance(value, dict) else {}
|
|
220
|
+
default_clearance = DEFAULT_PROXY_RUNTIME["clearance"]
|
|
221
|
+
clearance_source = source.get("clearance") if isinstance(source.get("clearance"), dict) else {}
|
|
222
|
+
|
|
223
|
+
egress_mode = str(source.get("egress_mode") or DEFAULT_PROXY_RUNTIME["egress_mode"]).strip().lower()
|
|
224
|
+
if egress_mode not in {"direct", "single_proxy"}:
|
|
225
|
+
egress_mode = str(DEFAULT_PROXY_RUNTIME["egress_mode"])
|
|
226
|
+
|
|
227
|
+
clearance_mode = str(clearance_source.get("mode") or default_clearance["mode"]).strip().lower()
|
|
228
|
+
if clearance_mode not in {"none", "manual", "flaresolverr"}:
|
|
229
|
+
clearance_mode = str(default_clearance["mode"])
|
|
230
|
+
|
|
231
|
+
user_agent = str(clearance_source.get("user_agent") or default_clearance["user_agent"]).strip()
|
|
232
|
+
browser = str(clearance_source.get("browser") or default_clearance["browser"]).strip()
|
|
233
|
+
|
|
234
|
+
existing_clearance_cookies = str(source.get("_existing_cf_cookies") or "").strip()
|
|
235
|
+
existing_cf_clearance = str(source.get("_existing_cf_clearance") or "").strip()
|
|
236
|
+
cf_cookies = str(clearance_source.get("cf_cookies") or "").strip()
|
|
237
|
+
cf_clearance = str(clearance_source.get("cf_clearance") or "").strip()
|
|
238
|
+
if not cf_cookies and _normalize_bool(clearance_source.get("has_cf_cookies"), False):
|
|
239
|
+
cf_cookies = existing_clearance_cookies
|
|
240
|
+
if not cf_clearance and _normalize_bool(clearance_source.get("has_cf_clearance"), False):
|
|
241
|
+
cf_clearance = existing_cf_clearance
|
|
242
|
+
|
|
243
|
+
return {
|
|
244
|
+
"enabled": _normalize_bool(source.get("enabled"), bool(DEFAULT_PROXY_RUNTIME["enabled"])),
|
|
245
|
+
"egress_mode": egress_mode,
|
|
246
|
+
"proxy_url": str(source.get("proxy_url") or "").strip(),
|
|
247
|
+
"resource_proxy_url": str(source.get("resource_proxy_url") or "").strip(),
|
|
248
|
+
"skip_ssl_verify": _normalize_bool(
|
|
249
|
+
source.get("skip_ssl_verify"),
|
|
250
|
+
bool(DEFAULT_PROXY_RUNTIME["skip_ssl_verify"]),
|
|
251
|
+
),
|
|
252
|
+
"reset_session_status_codes": _normalize_status_codes(source.get("reset_session_status_codes")),
|
|
253
|
+
"clearance": {
|
|
254
|
+
"enabled": _normalize_bool(clearance_source.get("enabled"), bool(default_clearance["enabled"])),
|
|
255
|
+
"mode": clearance_mode,
|
|
256
|
+
"cf_cookies": cf_cookies,
|
|
257
|
+
"cf_clearance": cf_clearance,
|
|
258
|
+
"user_agent": user_agent or str(default_clearance["user_agent"]),
|
|
259
|
+
"browser": browser or str(default_clearance["browser"]),
|
|
260
|
+
"flaresolverr_url": str(clearance_source.get("flaresolverr_url") or "").strip(),
|
|
261
|
+
"timeout_sec": _normalize_positive_int(
|
|
262
|
+
clearance_source.get("timeout_sec"),
|
|
263
|
+
int(default_clearance["timeout_sec"]),
|
|
264
|
+
1,
|
|
265
|
+
),
|
|
266
|
+
"refresh_interval": _normalize_positive_int(
|
|
267
|
+
clearance_source.get("refresh_interval"),
|
|
268
|
+
int(default_clearance["refresh_interval"]),
|
|
269
|
+
60,
|
|
270
|
+
),
|
|
271
|
+
"warm_up_on_start": _normalize_bool(
|
|
272
|
+
clearance_source.get("warm_up_on_start"),
|
|
273
|
+
bool(default_clearance["warm_up_on_start"]),
|
|
274
|
+
),
|
|
275
|
+
},
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def _normalize_third_party_apps_settings(value: object) -> dict[str, object]:
|
|
280
|
+
source = value if isinstance(value, dict) else {}
|
|
281
|
+
canvas_source = source.get("infinite_canvas") if isinstance(source.get("infinite_canvas"), dict) else {}
|
|
282
|
+
return {
|
|
283
|
+
"infinite_canvas": {
|
|
284
|
+
"enabled": _normalize_bool(canvas_source.get("enabled"), False),
|
|
285
|
+
"url": str(canvas_source.get("url") or DEFAULT_THIRD_PARTY_APPS["infinite_canvas"]["url"]).strip(),
|
|
286
|
+
},
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def _validate_image_storage_settings(settings: dict[str, object]) -> None:
|
|
291
|
+
if not _normalize_bool(settings.get("enabled"), False):
|
|
292
|
+
return
|
|
293
|
+
if not str(settings.get("webdav_url") or "").strip():
|
|
294
|
+
raise ValueError("启用 WebDAV 图片存储后必须填写 WebDAV URL")
|
|
295
|
+
if not str(settings.get("webdav_password") or "").strip():
|
|
296
|
+
raise ValueError("启用 WebDAV 图片存储后必须填写 WebDAV 密码")
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
@dataclass(frozen=True)
|
|
300
|
+
class LoadedSettings:
|
|
301
|
+
auth_key: str
|
|
302
|
+
refresh_account_interval_minute: int
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
def _normalize_auth_key(value: object) -> str:
|
|
306
|
+
return str(value or "").strip()
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def _is_invalid_auth_key(value: object) -> bool:
|
|
310
|
+
return _normalize_auth_key(value) == ""
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
def _read_json_object(path: Path, *, name: str) -> dict[str, object]:
|
|
314
|
+
if not path.exists():
|
|
315
|
+
return {}
|
|
316
|
+
if path.is_dir():
|
|
317
|
+
print(
|
|
318
|
+
f"Warning: {name} at '{path}' is a directory, ignoring it and falling back to other configuration sources.",
|
|
319
|
+
file=sys.stderr,
|
|
320
|
+
)
|
|
321
|
+
return {}
|
|
322
|
+
try:
|
|
323
|
+
data = json.loads(path.read_text(encoding="utf-8"))
|
|
324
|
+
except Exception:
|
|
325
|
+
return {}
|
|
326
|
+
return data if isinstance(data, dict) else {}
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
def _load_settings() -> LoadedSettings:
|
|
330
|
+
DATA_DIR.mkdir(parents=True, exist_ok=True)
|
|
331
|
+
raw_config = _read_json_object(CONFIG_FILE, name="config.json")
|
|
332
|
+
auth_key = _normalize_auth_key(os.getenv("CHATGPT2API_AUTH_KEY") or raw_config.get("auth-key"))
|
|
333
|
+
if _is_invalid_auth_key(auth_key):
|
|
334
|
+
raise ValueError(
|
|
335
|
+
"❌ auth-key 未设置!\n"
|
|
336
|
+
"请在环境变量 CHATGPT2API_AUTH_KEY 中设置,或者在 config.json 中填写 auth-key。"
|
|
337
|
+
)
|
|
338
|
+
|
|
339
|
+
try:
|
|
340
|
+
refresh_interval = int(raw_config.get("refresh_account_interval_minute", 5))
|
|
341
|
+
except (TypeError, ValueError):
|
|
342
|
+
refresh_interval = 5
|
|
343
|
+
|
|
344
|
+
return LoadedSettings(
|
|
345
|
+
auth_key=auth_key,
|
|
346
|
+
refresh_account_interval_minute=refresh_interval,
|
|
347
|
+
)
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
class ConfigStore:
|
|
351
|
+
def __init__(self, path: Path):
|
|
352
|
+
self.path = path
|
|
353
|
+
DATA_DIR.mkdir(parents=True, exist_ok=True)
|
|
354
|
+
self.data = self._load()
|
|
355
|
+
self._storage_backend: StorageBackend | None = None
|
|
356
|
+
if _is_invalid_auth_key(self.auth_key):
|
|
357
|
+
raise ValueError(
|
|
358
|
+
"❌ auth-key 未设置!\n"
|
|
359
|
+
"请按以下任意一种方式解决:\n"
|
|
360
|
+
"1. 在 Render 的 Environment 变量中添加:\n"
|
|
361
|
+
" CHATGPT2API_AUTH_KEY = your_real_auth_key\n"
|
|
362
|
+
"2. 或者在 config.json 中填写:\n"
|
|
363
|
+
' "auth-key": "your_real_auth_key"'
|
|
364
|
+
)
|
|
365
|
+
|
|
366
|
+
def _load(self) -> dict[str, object]:
|
|
367
|
+
return _read_json_object(self.path, name="config.json")
|
|
368
|
+
|
|
369
|
+
def _save(self) -> None:
|
|
370
|
+
self.path.write_text(json.dumps(self.data, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
|
371
|
+
|
|
372
|
+
@property
|
|
373
|
+
def auth_key(self) -> str:
|
|
374
|
+
return _normalize_auth_key(os.getenv("CHATGPT2API_AUTH_KEY") or self.data.get("auth-key"))
|
|
375
|
+
|
|
376
|
+
@property
|
|
377
|
+
def accounts_file(self) -> Path:
|
|
378
|
+
return DATA_DIR / "accounts.json"
|
|
379
|
+
|
|
380
|
+
@property
|
|
381
|
+
def refresh_account_interval_minute(self) -> int:
|
|
382
|
+
try:
|
|
383
|
+
return int(self.data.get("refresh_account_interval_minute", 5))
|
|
384
|
+
except (TypeError, ValueError):
|
|
385
|
+
return 5
|
|
386
|
+
|
|
387
|
+
@property
|
|
388
|
+
def image_retention_days(self) -> int:
|
|
389
|
+
try:
|
|
390
|
+
return max(1, int(self.data.get("image_retention_days", 30)))
|
|
391
|
+
except (TypeError, ValueError):
|
|
392
|
+
return 30
|
|
393
|
+
|
|
394
|
+
@property
|
|
395
|
+
def image_poll_timeout_secs(self) -> int:
|
|
396
|
+
try:
|
|
397
|
+
return max(1, int(self.data.get("image_poll_timeout_secs", 120)))
|
|
398
|
+
except (TypeError, ValueError):
|
|
399
|
+
return 120
|
|
400
|
+
|
|
401
|
+
@property
|
|
402
|
+
def image_poll_interval_secs(self) -> float:
|
|
403
|
+
try:
|
|
404
|
+
return max(0.5, float(self.data.get("image_poll_interval_secs", 10.0)))
|
|
405
|
+
except (TypeError, ValueError):
|
|
406
|
+
return 10.0
|
|
407
|
+
|
|
408
|
+
@property
|
|
409
|
+
def image_poll_initial_wait_secs(self) -> float:
|
|
410
|
+
"""Image generation upstream takes ~30s; polling immediately wastes requests
|
|
411
|
+
and trips a transient 429. Default 10s gives the conversation document time
|
|
412
|
+
to commit before the first poll."""
|
|
413
|
+
try:
|
|
414
|
+
return max(0.0, float(self.data.get("image_poll_initial_wait_secs", 10.0)))
|
|
415
|
+
except (TypeError, ValueError):
|
|
416
|
+
return 10.0
|
|
417
|
+
|
|
418
|
+
@property
|
|
419
|
+
def image_account_concurrency(self) -> int:
|
|
420
|
+
try:
|
|
421
|
+
return max(1, int(self.data.get("image_account_concurrency", 3)))
|
|
422
|
+
except (TypeError, ValueError):
|
|
423
|
+
return 3
|
|
424
|
+
|
|
425
|
+
@property
|
|
426
|
+
def image_parallel_generation(self) -> bool:
|
|
427
|
+
value = self.data.get("image_parallel_generation", True)
|
|
428
|
+
if isinstance(value, str):
|
|
429
|
+
return value.strip().lower() in {"1", "true", "yes", "on"}
|
|
430
|
+
return bool(value)
|
|
431
|
+
|
|
432
|
+
@property
|
|
433
|
+
def image_settle_enabled(self) -> bool:
|
|
434
|
+
"""图片二次确认机制:找到 file_ids 后等待一段时间再次确认。"""
|
|
435
|
+
value = self.data.get("image_settle_enabled", True)
|
|
436
|
+
if isinstance(value, str):
|
|
437
|
+
return value.strip().lower() in {"1", "true", "yes", "on"}
|
|
438
|
+
return bool(value)
|
|
439
|
+
|
|
440
|
+
@property
|
|
441
|
+
def image_check_before_hit_enabled(self) -> bool:
|
|
442
|
+
"""先check再hit:通过轮询确认 file_ids 存在后再返回,而非仅依赖 SSE 事件。"""
|
|
443
|
+
value = self.data.get("image_check_before_hit_enabled", True)
|
|
444
|
+
if isinstance(value, str):
|
|
445
|
+
return value.strip().lower() in {"1", "true", "yes", "on"}
|
|
446
|
+
return bool(value)
|
|
447
|
+
|
|
448
|
+
@property
|
|
449
|
+
def image_remove_conversation_after_result(self) -> bool:
|
|
450
|
+
"""出图成功后异步隐藏 ChatGPT 本地对话记录。"""
|
|
451
|
+
value = self.data.get("image_remove_conversation_after_result", False)
|
|
452
|
+
if isinstance(value, str):
|
|
453
|
+
return value.strip().lower() in {"1", "true", "yes", "on"}
|
|
454
|
+
return bool(value)
|
|
455
|
+
|
|
456
|
+
@property
|
|
457
|
+
def image_remove_conversation_always(self) -> bool:
|
|
458
|
+
"""无论是否出图,画图请求结束后都异步隐藏 ChatGPT 本地对话记录。"""
|
|
459
|
+
return _normalize_bool(self.data.get("image_remove_conversation_always"), False)
|
|
460
|
+
|
|
461
|
+
@property
|
|
462
|
+
def image_settle_secs(self) -> float:
|
|
463
|
+
"""二次确认等待时间(秒)。"""
|
|
464
|
+
try:
|
|
465
|
+
return max(0.5, float(self.data.get("image_settle_secs", 2.0)))
|
|
466
|
+
except (TypeError, ValueError):
|
|
467
|
+
return 2.0
|
|
468
|
+
|
|
469
|
+
@property
|
|
470
|
+
def auto_remove_invalid_accounts(self) -> bool:
|
|
471
|
+
value = self.data.get("auto_remove_invalid_accounts", False)
|
|
472
|
+
if isinstance(value, str):
|
|
473
|
+
return value.strip().lower() in {"1", "true", "yes", "on"}
|
|
474
|
+
return bool(value)
|
|
475
|
+
|
|
476
|
+
@property
|
|
477
|
+
def auto_remove_rate_limited_accounts(self) -> bool:
|
|
478
|
+
value = self.data.get("auto_remove_rate_limited_accounts", False)
|
|
479
|
+
if isinstance(value, str):
|
|
480
|
+
return value.strip().lower() in {"1", "true", "yes", "on"}
|
|
481
|
+
return bool(value)
|
|
482
|
+
|
|
483
|
+
@property
|
|
484
|
+
def auto_relogin_after_refresh(self) -> bool:
|
|
485
|
+
value = self.data.get("auto_relogin_after_refresh", False)
|
|
486
|
+
if isinstance(value, str):
|
|
487
|
+
return value.strip().lower() in {"1", "true", "yes", "on"}
|
|
488
|
+
return bool(value)
|
|
489
|
+
|
|
490
|
+
@property
|
|
491
|
+
def log_levels(self) -> list[str]:
|
|
492
|
+
levels = self.data.get("log_levels")
|
|
493
|
+
if not isinstance(levels, list):
|
|
494
|
+
return []
|
|
495
|
+
allowed = {"debug", "info", "warning", "error"}
|
|
496
|
+
return [level for item in levels if (level := str(item or "").strip().lower()) in allowed]
|
|
497
|
+
|
|
498
|
+
@property
|
|
499
|
+
def sensitive_words(self) -> list[str]:
|
|
500
|
+
words = self.data.get("sensitive_words")
|
|
501
|
+
return [word for item in words if (word := str(item or "").strip())] if isinstance(words, list) else []
|
|
502
|
+
|
|
503
|
+
@property
|
|
504
|
+
def ai_review(self) -> dict[str, object]:
|
|
505
|
+
value = self.data.get("ai_review")
|
|
506
|
+
return value if isinstance(value, dict) else {}
|
|
507
|
+
|
|
508
|
+
@property
|
|
509
|
+
def global_system_prompt(self) -> str:
|
|
510
|
+
return str(self.data.get("global_system_prompt") or "").strip()
|
|
511
|
+
|
|
512
|
+
@property
|
|
513
|
+
def default_upstream_model_name(self) -> str:
|
|
514
|
+
return str(self.data.get("default_upstream_model_name") or "gpt-5-5").strip()
|
|
515
|
+
|
|
516
|
+
@property
|
|
517
|
+
def default_thinking_effort(self) -> str:
|
|
518
|
+
value = str(self.data.get("default_thinking_effort") or "auto").strip().lower()
|
|
519
|
+
return value if value in {"auto", "standard", "extended", "max"} else "auto"
|
|
520
|
+
|
|
521
|
+
@property
|
|
522
|
+
def images_dir(self) -> Path:
|
|
523
|
+
path = DATA_DIR / "images"
|
|
524
|
+
path.mkdir(parents=True, exist_ok=True)
|
|
525
|
+
return path
|
|
526
|
+
|
|
527
|
+
@property
|
|
528
|
+
def image_thumbnails_dir(self) -> Path:
|
|
529
|
+
path = DATA_DIR / "image_thumbnails"
|
|
530
|
+
path.mkdir(parents=True, exist_ok=True)
|
|
531
|
+
return path
|
|
532
|
+
|
|
533
|
+
def cleanup_old_images(self) -> int:
|
|
534
|
+
cutoff = time.time() - self.image_retention_days * 86400
|
|
535
|
+
removed = 0
|
|
536
|
+
for path in self.images_dir.rglob("*"):
|
|
537
|
+
if path.is_file() and path.stat().st_mtime < cutoff:
|
|
538
|
+
path.unlink()
|
|
539
|
+
removed += 1
|
|
540
|
+
for path in sorted((p for p in self.images_dir.rglob("*") if p.is_dir()), key=lambda p: len(p.parts), reverse=True):
|
|
541
|
+
try:
|
|
542
|
+
path.rmdir()
|
|
543
|
+
except OSError:
|
|
544
|
+
pass
|
|
545
|
+
return removed
|
|
546
|
+
|
|
547
|
+
@property
|
|
548
|
+
def base_url(self) -> str:
|
|
549
|
+
return str(
|
|
550
|
+
os.getenv("CHATGPT2API_BASE_URL")
|
|
551
|
+
or self.data.get("base_url")
|
|
552
|
+
or ""
|
|
553
|
+
).strip().rstrip("/")
|
|
554
|
+
|
|
555
|
+
@property
|
|
556
|
+
def app_version(self) -> str:
|
|
557
|
+
try:
|
|
558
|
+
value = VERSION_FILE.read_text(encoding="utf-8").strip()
|
|
559
|
+
except FileNotFoundError:
|
|
560
|
+
return "0.0.0"
|
|
561
|
+
return value or "0.0.0"
|
|
562
|
+
|
|
563
|
+
def get(self) -> dict[str, object]:
|
|
564
|
+
data = dict(self.data)
|
|
565
|
+
data["refresh_account_interval_minute"] = self.refresh_account_interval_minute
|
|
566
|
+
data["image_retention_days"] = self.image_retention_days
|
|
567
|
+
data["image_poll_timeout_secs"] = self.image_poll_timeout_secs
|
|
568
|
+
data["image_poll_interval_secs"] = self.image_poll_interval_secs
|
|
569
|
+
data["image_poll_initial_wait_secs"] = self.image_poll_initial_wait_secs
|
|
570
|
+
data["image_account_concurrency"] = self.image_account_concurrency
|
|
571
|
+
data["image_parallel_generation"] = self.image_parallel_generation
|
|
572
|
+
data["image_remove_conversation_after_result"] = self.image_remove_conversation_after_result
|
|
573
|
+
data["image_remove_conversation_always"] = self.image_remove_conversation_always
|
|
574
|
+
data["auto_remove_invalid_accounts"] = self.auto_remove_invalid_accounts
|
|
575
|
+
data["auto_remove_rate_limited_accounts"] = self.auto_remove_rate_limited_accounts
|
|
576
|
+
data["auto_relogin_after_refresh"] = self.auto_relogin_after_refresh
|
|
577
|
+
data["log_levels"] = self.log_levels
|
|
578
|
+
data["sensitive_words"] = self.sensitive_words
|
|
579
|
+
data["ai_review"] = self.ai_review
|
|
580
|
+
data["global_system_prompt"] = self.global_system_prompt
|
|
581
|
+
data["default_upstream_model_name"] = self.default_upstream_model_name
|
|
582
|
+
data["default_thinking_effort"] = self.default_thinking_effort
|
|
583
|
+
data["backup"] = self.get_backup_settings()
|
|
584
|
+
data["image_storage"] = self.get_image_storage_settings()
|
|
585
|
+
data["chat_completion_cache"] = self.get_chat_completion_cache_settings()
|
|
586
|
+
data["proxy_runtime"] = self.get_public_proxy_runtime_settings()
|
|
587
|
+
data["third_party_apps"] = self.get_third_party_apps_settings()
|
|
588
|
+
data.pop("auth-key", None)
|
|
589
|
+
return data
|
|
590
|
+
|
|
591
|
+
def get_proxy_settings(self) -> str:
|
|
592
|
+
return str(self.data.get("proxy") or "").strip()
|
|
593
|
+
|
|
594
|
+
def get_proxy_runtime_settings(self) -> dict[str, object]:
|
|
595
|
+
return _normalize_proxy_runtime_settings(self.data.get("proxy_runtime"))
|
|
596
|
+
|
|
597
|
+
def get_public_proxy_runtime_settings(self) -> dict[str, object]:
|
|
598
|
+
runtime = copy.deepcopy(self.get_proxy_runtime_settings())
|
|
599
|
+
clearance = runtime.get("clearance") if isinstance(runtime.get("clearance"), dict) else {}
|
|
600
|
+
if isinstance(clearance, dict):
|
|
601
|
+
cf_cookies = str(clearance.get("cf_cookies") or "").strip()
|
|
602
|
+
cf_clearance = str(clearance.get("cf_clearance") or "").strip()
|
|
603
|
+
clearance["cf_cookies"] = ""
|
|
604
|
+
clearance["cf_clearance"] = ""
|
|
605
|
+
clearance["has_cf_cookies"] = bool(cf_cookies)
|
|
606
|
+
clearance["has_cf_clearance"] = bool(cf_clearance)
|
|
607
|
+
return runtime
|
|
608
|
+
|
|
609
|
+
def get_third_party_apps_settings(self) -> dict[str, object]:
|
|
610
|
+
return _normalize_third_party_apps_settings(self.data.get("third_party_apps"))
|
|
611
|
+
|
|
612
|
+
def update(self, data: dict[str, object]) -> dict[str, object]:
|
|
613
|
+
next_data = dict(self.data)
|
|
614
|
+
next_data.update(dict(data or {}))
|
|
615
|
+
if "backup" in next_data:
|
|
616
|
+
next_data["backup"] = _normalize_backup_settings(next_data.get("backup"))
|
|
617
|
+
if "image_storage" in next_data:
|
|
618
|
+
next_data["image_storage"] = _normalize_image_storage_settings(next_data.get("image_storage"))
|
|
619
|
+
_validate_image_storage_settings(next_data["image_storage"])
|
|
620
|
+
if "chat_completion_cache" in next_data:
|
|
621
|
+
next_data["chat_completion_cache"] = _normalize_chat_completion_cache_settings(
|
|
622
|
+
next_data.get("chat_completion_cache")
|
|
623
|
+
)
|
|
624
|
+
if "third_party_apps" in next_data:
|
|
625
|
+
next_data["third_party_apps"] = _normalize_third_party_apps_settings(next_data.get("third_party_apps"))
|
|
626
|
+
if "proxy_runtime" in next_data:
|
|
627
|
+
incoming_runtime = next_data.get("proxy_runtime")
|
|
628
|
+
if isinstance(incoming_runtime, dict):
|
|
629
|
+
previous_clearance = self.get_proxy_runtime_settings().get("clearance")
|
|
630
|
+
if isinstance(previous_clearance, dict):
|
|
631
|
+
incoming_runtime = dict(incoming_runtime)
|
|
632
|
+
incoming_runtime["_existing_cf_cookies"] = previous_clearance.get("cf_cookies")
|
|
633
|
+
incoming_runtime["_existing_cf_clearance"] = previous_clearance.get("cf_clearance")
|
|
634
|
+
next_data["proxy_runtime"] = _normalize_proxy_runtime_settings(incoming_runtime)
|
|
635
|
+
next_data.pop("backup_state", None)
|
|
636
|
+
self.data = next_data
|
|
637
|
+
self._save()
|
|
638
|
+
return self.get()
|
|
639
|
+
|
|
640
|
+
def get_backup_settings(self) -> dict[str, object]:
|
|
641
|
+
return _normalize_backup_settings(self.data.get("backup"))
|
|
642
|
+
|
|
643
|
+
def get_image_storage_settings(self) -> dict[str, object]:
|
|
644
|
+
return _normalize_image_storage_settings(self.data.get("image_storage"))
|
|
645
|
+
|
|
646
|
+
def get_chat_completion_cache_settings(self) -> dict[str, object]:
|
|
647
|
+
return _normalize_chat_completion_cache_settings(self.data.get("chat_completion_cache"))
|
|
648
|
+
|
|
649
|
+
def get_storage_backend(self) -> StorageBackend:
|
|
650
|
+
"""获取存储后端实例(单例)"""
|
|
651
|
+
if self._storage_backend is None:
|
|
652
|
+
from services.storage.factory import create_storage_backend
|
|
653
|
+
self._storage_backend = create_storage_backend(DATA_DIR)
|
|
654
|
+
return self._storage_backend
|
|
655
|
+
|
|
656
|
+
|
|
657
|
+
def load_backup_state() -> dict[str, object]:
|
|
658
|
+
return _normalize_backup_state(_read_json_object(BACKUP_STATE_FILE, name="backup_state.json"))
|
|
659
|
+
|
|
660
|
+
|
|
661
|
+
def save_backup_state(state: dict[str, object]) -> dict[str, object]:
|
|
662
|
+
normalized = _normalize_backup_state(state)
|
|
663
|
+
BACKUP_STATE_FILE.write_text(json.dumps(normalized, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
|
664
|
+
return normalized
|
|
665
|
+
|
|
666
|
+
|
|
667
|
+
config = ConfigStore(CONFIG_FILE)
|