dsh-plugin-image-workbench 0.0.0-stage → 1.1.5

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.
Files changed (185) hide show
  1. package/LICENSE +21 -0
  2. package/README.en.md +256 -0
  3. package/README.md +256 -2
  4. package/client.js +241 -0
  5. package/cordis.patch.yml +14 -0
  6. package/host.js +641 -0
  7. package/install.mjs +387 -0
  8. package/package.json +57 -4
  9. package/scripts/vendor-snapshot.py +154 -0
  10. package/server/access.py +26 -0
  11. package/server/app.py +1329 -0
  12. package/server/catalog.json +1 -0
  13. package/server/catalog.py +147 -0
  14. package/server/composer.py +391 -0
  15. package/server/drafts.py +158 -0
  16. package/server/history.py +575 -0
  17. package/server/imaging.py +172 -0
  18. package/server/pool.py +1335 -0
  19. package/server/pool_admin.py +921 -0
  20. package/server/pool_service.py +1073 -0
  21. package/server/requirements.txt +6 -0
  22. package/server/smoke_check.py +402 -0
  23. package/server/static/access.html +202 -0
  24. package/server/static/index.html +4725 -0
  25. package/vendor/chatgpt2api/README.md +13 -0
  26. package/vendor/chatgpt2api/SOURCE.json +11 -0
  27. package/vendor/chatgpt2api/runtime/uv.exe +0 -0
  28. package/vendor/chatgpt2api/source/.env.example +82 -0
  29. package/vendor/chatgpt2api/source/.python-version +1 -0
  30. package/vendor/chatgpt2api/source/LICENSE +21 -0
  31. package/vendor/chatgpt2api/source/README.md +391 -0
  32. package/vendor/chatgpt2api/source/VERSION +1 -0
  33. package/vendor/chatgpt2api/source/api/__init__.py +2 -0
  34. package/vendor/chatgpt2api/source/api/accounts.py +537 -0
  35. package/vendor/chatgpt2api/source/api/ai.py +217 -0
  36. package/vendor/chatgpt2api/source/api/app.py +62 -0
  37. package/vendor/chatgpt2api/source/api/errors.py +46 -0
  38. package/vendor/chatgpt2api/source/api/image_inputs.py +309 -0
  39. package/vendor/chatgpt2api/source/api/image_tasks.py +120 -0
  40. package/vendor/chatgpt2api/source/api/support.py +134 -0
  41. package/vendor/chatgpt2api/source/api/system.py +365 -0
  42. package/vendor/chatgpt2api/source/assets/atlascloud.svg +1 -0
  43. package/vendor/chatgpt2api/source/assets/ipwo.png +0 -0
  44. package/vendor/chatgpt2api/source/assets/kunrouai.svg +50 -0
  45. package/vendor/chatgpt2api/source/config.default.json +108 -0
  46. package/vendor/chatgpt2api/source/main.py +9 -0
  47. package/vendor/chatgpt2api/source/pyproject.toml +27 -0
  48. package/vendor/chatgpt2api/source/scripts/init_proxy_config.py +186 -0
  49. package/vendor/chatgpt2api/source/scripts/migrate_storage.py +163 -0
  50. package/vendor/chatgpt2api/source/scripts/privoxy-warp.conf +13 -0
  51. package/vendor/chatgpt2api/source/scripts/test_outlook_token_mailbox.py +385 -0
  52. package/vendor/chatgpt2api/source/scripts/test_storage.py +129 -0
  53. package/vendor/chatgpt2api/source/scripts/verify_oauth_refresh.py +92 -0
  54. package/vendor/chatgpt2api/source/services/__init__.py +0 -0
  55. package/vendor/chatgpt2api/source/services/account_service.py +1757 -0
  56. package/vendor/chatgpt2api/source/services/auth_service.py +253 -0
  57. package/vendor/chatgpt2api/source/services/backup_service.py +671 -0
  58. package/vendor/chatgpt2api/source/services/config.py +667 -0
  59. package/vendor/chatgpt2api/source/services/content_filter.py +242 -0
  60. package/vendor/chatgpt2api/source/services/cpa_service.py +315 -0
  61. package/vendor/chatgpt2api/source/services/editable_file_task_service.py +260 -0
  62. package/vendor/chatgpt2api/source/services/image_service.py +376 -0
  63. package/vendor/chatgpt2api/source/services/image_storage_service.py +405 -0
  64. package/vendor/chatgpt2api/source/services/image_tags_service.py +76 -0
  65. package/vendor/chatgpt2api/source/services/image_task_service.py +550 -0
  66. package/vendor/chatgpt2api/source/services/log_service.py +337 -0
  67. package/vendor/chatgpt2api/source/services/model_service.py +182 -0
  68. package/vendor/chatgpt2api/source/services/oauth_login_service.py +267 -0
  69. package/vendor/chatgpt2api/source/services/openai_backend_api.py +2763 -0
  70. package/vendor/chatgpt2api/source/services/openai_oauth.py +32 -0
  71. package/vendor/chatgpt2api/source/services/protocol/__init__.py +2 -0
  72. package/vendor/chatgpt2api/source/services/protocol/anthropic_v1_messages.py +307 -0
  73. package/vendor/chatgpt2api/source/services/protocol/chat_completion_cache.py +238 -0
  74. package/vendor/chatgpt2api/source/services/protocol/conversation.py +1644 -0
  75. package/vendor/chatgpt2api/source/services/protocol/error_response.py +124 -0
  76. package/vendor/chatgpt2api/source/services/protocol/openai_search.py +19 -0
  77. package/vendor/chatgpt2api/source/services/protocol/openai_v1_chat_complete.py +316 -0
  78. package/vendor/chatgpt2api/source/services/protocol/openai_v1_image_edit.py +86 -0
  79. package/vendor/chatgpt2api/source/services/protocol/openai_v1_image_generations.py +42 -0
  80. package/vendor/chatgpt2api/source/services/protocol/openai_v1_models.py +53 -0
  81. package/vendor/chatgpt2api/source/services/protocol/openai_v1_response.py +453 -0
  82. package/vendor/chatgpt2api/source/services/protocol/web_search_tool.py +160 -0
  83. package/vendor/chatgpt2api/source/services/proxy_service.py +648 -0
  84. package/vendor/chatgpt2api/source/services/storage/__init__.py +5 -0
  85. package/vendor/chatgpt2api/source/services/storage/base.py +38 -0
  86. package/vendor/chatgpt2api/source/services/storage/database_storage.py +194 -0
  87. package/vendor/chatgpt2api/source/services/storage/factory.py +104 -0
  88. package/vendor/chatgpt2api/source/services/storage/git_storage.py +183 -0
  89. package/vendor/chatgpt2api/source/services/storage/json_storage.py +95 -0
  90. package/vendor/chatgpt2api/source/services/sub2api_service.py +531 -0
  91. package/vendor/chatgpt2api/source/utils/__init__.py +0 -0
  92. package/vendor/chatgpt2api/source/utils/helper.py +469 -0
  93. package/vendor/chatgpt2api/source/utils/image_tokens.py +346 -0
  94. package/vendor/chatgpt2api/source/utils/log.py +110 -0
  95. package/vendor/chatgpt2api/source/utils/pkce.py +19 -0
  96. package/vendor/chatgpt2api/source/utils/pow.py +207 -0
  97. package/vendor/chatgpt2api/source/utils/sentinel.py +159 -0
  98. package/vendor/chatgpt2api/source/utils/turnstile.py +184 -0
  99. package/vendor/chatgpt2api/source/uv.lock +904 -0
  100. package/vendor/chatgpt2api/source/web/bun.lock +1501 -0
  101. package/vendor/chatgpt2api/source/web/components.json +24 -0
  102. package/vendor/chatgpt2api/source/web/eslint.config.mjs +17 -0
  103. package/vendor/chatgpt2api/source/web/next.config.ts +40 -0
  104. package/vendor/chatgpt2api/source/web/package.json +58 -0
  105. package/vendor/chatgpt2api/source/web/postcss.config.mjs +5 -0
  106. package/vendor/chatgpt2api/source/web/public/github.svg +1 -0
  107. package/vendor/chatgpt2api/source/web/public/next.svg +1 -0
  108. package/vendor/chatgpt2api/source/web/public/openai.svg +1 -0
  109. package/vendor/chatgpt2api/source/web/public/vercel.svg +1 -0
  110. package/vendor/chatgpt2api/source/web/src/app/accounts/components/account-import-dialog.tsx +925 -0
  111. package/vendor/chatgpt2api/source/web/src/app/accounts/page.tsx +1307 -0
  112. package/vendor/chatgpt2api/source/web/src/app/debug/components/chat-panel.tsx +224 -0
  113. package/vendor/chatgpt2api/source/web/src/app/debug/components/editable-file-panel.tsx +444 -0
  114. package/vendor/chatgpt2api/source/web/src/app/debug/components/ppt-panel.tsx +9 -0
  115. package/vendor/chatgpt2api/source/web/src/app/debug/components/psd-panel.tsx +9 -0
  116. package/vendor/chatgpt2api/source/web/src/app/debug/components/search-panel.tsx +172 -0
  117. package/vendor/chatgpt2api/source/web/src/app/debug/components/skill-panel.tsx +169 -0
  118. package/vendor/chatgpt2api/source/web/src/app/debug/components/types.ts +39 -0
  119. package/vendor/chatgpt2api/source/web/src/app/debug/page.tsx +59 -0
  120. package/vendor/chatgpt2api/source/web/src/app/favicon.ico +0 -0
  121. package/vendor/chatgpt2api/source/web/src/app/globals.css +231 -0
  122. package/vendor/chatgpt2api/source/web/src/app/image/components/image-composer.tsx +542 -0
  123. package/vendor/chatgpt2api/source/web/src/app/image/components/image-results.tsx +554 -0
  124. package/vendor/chatgpt2api/source/web/src/app/image/components/image-sidebar.tsx +176 -0
  125. package/vendor/chatgpt2api/source/web/src/app/image/page.tsx +1799 -0
  126. package/vendor/chatgpt2api/source/web/src/app/image-manager/page.tsx +732 -0
  127. package/vendor/chatgpt2api/source/web/src/app/layout.tsx +50 -0
  128. package/vendor/chatgpt2api/source/web/src/app/login/page.tsx +101 -0
  129. package/vendor/chatgpt2api/source/web/src/app/logs/page.tsx +356 -0
  130. package/vendor/chatgpt2api/source/web/src/app/page.tsx +30 -0
  131. package/vendor/chatgpt2api/source/web/src/app/settings/components/api-docs-card.tsx +341 -0
  132. package/vendor/chatgpt2api/source/web/src/app/settings/components/backup-settings-card.tsx +447 -0
  133. package/vendor/chatgpt2api/source/web/src/app/settings/components/base-url-card.tsx +71 -0
  134. package/vendor/chatgpt2api/source/web/src/app/settings/components/config-card.tsx +491 -0
  135. package/vendor/chatgpt2api/source/web/src/app/settings/components/cpa-pool-dialog.tsx +108 -0
  136. package/vendor/chatgpt2api/source/web/src/app/settings/components/cpa-pools-card.tsx +169 -0
  137. package/vendor/chatgpt2api/source/web/src/app/settings/components/import-browser-dialog.tsx +185 -0
  138. package/vendor/chatgpt2api/source/web/src/app/settings/components/proxy-runtime-card.tsx +379 -0
  139. package/vendor/chatgpt2api/source/web/src/app/settings/components/proxy-settings-card.tsx +127 -0
  140. package/vendor/chatgpt2api/source/web/src/app/settings/components/proxy-settings.tsx +200 -0
  141. package/vendor/chatgpt2api/source/web/src/app/settings/components/settings-header.tsx +12 -0
  142. package/vendor/chatgpt2api/source/web/src/app/settings/components/sub2api-connections.tsx +886 -0
  143. package/vendor/chatgpt2api/source/web/src/app/settings/components/third-party-apps-card.tsx +81 -0
  144. package/vendor/chatgpt2api/source/web/src/app/settings/components/user-keys-card.tsx +422 -0
  145. package/vendor/chatgpt2api/source/web/src/app/settings/page.tsx +135 -0
  146. package/vendor/chatgpt2api/source/web/src/app/settings/store.ts +1069 -0
  147. package/vendor/chatgpt2api/source/web/src/components/date-range-filter.tsx +49 -0
  148. package/vendor/chatgpt2api/source/web/src/components/header-actions.tsx +24 -0
  149. package/vendor/chatgpt2api/source/web/src/components/image-lightbox.tsx +434 -0
  150. package/vendor/chatgpt2api/source/web/src/components/image-thumbnail.tsx +46 -0
  151. package/vendor/chatgpt2api/source/web/src/components/theme-script.tsx +18 -0
  152. package/vendor/chatgpt2api/source/web/src/components/theme-toggle.tsx +14 -0
  153. package/vendor/chatgpt2api/source/web/src/components/top-nav.tsx +263 -0
  154. package/vendor/chatgpt2api/source/web/src/components/ui/animated-theme-toggler.tsx +256 -0
  155. package/vendor/chatgpt2api/source/web/src/components/ui/badge.tsx +41 -0
  156. package/vendor/chatgpt2api/source/web/src/components/ui/button.tsx +59 -0
  157. package/vendor/chatgpt2api/source/web/src/components/ui/calendar.tsx +46 -0
  158. package/vendor/chatgpt2api/source/web/src/components/ui/card.tsx +61 -0
  159. package/vendor/chatgpt2api/source/web/src/components/ui/checkbox.tsx +27 -0
  160. package/vendor/chatgpt2api/source/web/src/components/ui/dia-text-reveal.tsx +267 -0
  161. package/vendor/chatgpt2api/source/web/src/components/ui/dialog.tsx +132 -0
  162. package/vendor/chatgpt2api/source/web/src/components/ui/field.tsx +13 -0
  163. package/vendor/chatgpt2api/source/web/src/components/ui/input.tsx +19 -0
  164. package/vendor/chatgpt2api/source/web/src/components/ui/label.tsx +24 -0
  165. package/vendor/chatgpt2api/source/web/src/components/ui/morphing-text.tsx +149 -0
  166. package/vendor/chatgpt2api/source/web/src/components/ui/popover.tsx +38 -0
  167. package/vendor/chatgpt2api/source/web/src/components/ui/select.tsx +175 -0
  168. package/vendor/chatgpt2api/source/web/src/components/ui/sheet.tsx +143 -0
  169. package/vendor/chatgpt2api/source/web/src/components/ui/table.tsx +29 -0
  170. package/vendor/chatgpt2api/source/web/src/components/ui/tabs.tsx +91 -0
  171. package/vendor/chatgpt2api/source/web/src/components/ui/text-3d-flip.tsx +269 -0
  172. package/vendor/chatgpt2api/source/web/src/components/ui/textarea.tsx +18 -0
  173. package/vendor/chatgpt2api/source/web/src/components/version-release-dialog.tsx +127 -0
  174. package/vendor/chatgpt2api/source/web/src/constants/common-env.ts +6 -0
  175. package/vendor/chatgpt2api/source/web/src/hooks/use-version-check.ts +85 -0
  176. package/vendor/chatgpt2api/source/web/src/lib/api.ts +926 -0
  177. package/vendor/chatgpt2api/source/web/src/lib/auth-session.ts +26 -0
  178. package/vendor/chatgpt2api/source/web/src/lib/release.ts +25 -0
  179. package/vendor/chatgpt2api/source/web/src/lib/request.ts +93 -0
  180. package/vendor/chatgpt2api/source/web/src/lib/use-auth-guard.ts +89 -0
  181. package/vendor/chatgpt2api/source/web/src/lib/utils.ts +6 -0
  182. package/vendor/chatgpt2api/source/web/src/store/auth.ts +112 -0
  183. package/vendor/chatgpt2api/source/web/src/store/editable-file-history.ts +33 -0
  184. package/vendor/chatgpt2api/source/web/src/store/image-conversations.ts +318 -0
  185. package/vendor/chatgpt2api/source/web/tsconfig.json +42 -0
@@ -0,0 +1,1069 @@
1
+ "use client";
2
+
3
+ import { create } from "zustand";
4
+ import { toast } from "sonner";
5
+
6
+ import {
7
+ createCPAPool,
8
+ deleteBackup,
9
+ deleteCPAPool,
10
+ fetchCPAPoolFiles,
11
+ fetchCPAPools,
12
+ fetchBackups,
13
+ fetchSettingsConfig,
14
+ runBackupNow,
15
+ syncImageStorage,
16
+ startCPAImport,
17
+ testBackupConnection,
18
+ testImageStorageConnection,
19
+ updateCPAPool,
20
+ updateSettingsConfig,
21
+ type BackupItem,
22
+ type BackupSettings,
23
+ type BackupState,
24
+ type CPAPool,
25
+ type CPARemoteFile,
26
+ type ImageStorageMode,
27
+ type ImageStorageSettings,
28
+ type ProxyRuntimeClearanceMode,
29
+ type ProxyRuntimeEgressMode,
30
+ type ProxyRuntimeSettings,
31
+ type SettingsConfig,
32
+ type ThirdPartyAppsSettings,
33
+ } from "@/lib/api";
34
+
35
+ export const PAGE_SIZE_OPTIONS = ["50", "100", "200"] as const;
36
+
37
+ export type PageSizeOption = (typeof PAGE_SIZE_OPTIONS)[number];
38
+
39
+ const DEFAULT_PROXY_RUNTIME: ProxyRuntimeSettings = {
40
+ enabled: false,
41
+ egress_mode: "direct",
42
+ proxy_url: "",
43
+ resource_proxy_url: "",
44
+ skip_ssl_verify: false,
45
+ reset_session_status_codes: [403],
46
+ clearance: {
47
+ enabled: false,
48
+ mode: "none",
49
+ cf_cookies: "",
50
+ cf_clearance: "",
51
+ 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",
52
+ browser: "chrome",
53
+ flaresolverr_url: "",
54
+ timeout_sec: 60,
55
+ refresh_interval: 3600,
56
+ warm_up_on_start: false,
57
+ has_cf_cookies: false,
58
+ has_cf_clearance: false,
59
+ },
60
+ };
61
+
62
+ const DEFAULT_THIRD_PARTY_APPS: ThirdPartyAppsSettings = {
63
+ infinite_canvas: {
64
+ enabled: false,
65
+ url: "https://canvas.best",
66
+ },
67
+ };
68
+
69
+ function normalizeProxyRuntime(value: unknown): ProxyRuntimeSettings {
70
+ const source = typeof value === "object" && value !== null ? value as Partial<ProxyRuntimeSettings> : {};
71
+ const clearanceSource = typeof source.clearance === "object" && source.clearance !== null
72
+ ? source.clearance as Partial<ProxyRuntimeSettings["clearance"]>
73
+ : {};
74
+ const egressMode = source.egress_mode === "single_proxy" ? "single_proxy" : "direct";
75
+ const clearanceMode: ProxyRuntimeClearanceMode = clearanceSource.mode === "manual" || clearanceSource.mode === "flaresolverr"
76
+ ? clearanceSource.mode
77
+ : "none";
78
+ const statusCodes = Array.isArray(source.reset_session_status_codes)
79
+ ? source.reset_session_status_codes
80
+ .map((item) => Number(item))
81
+ .filter((item) => Number.isInteger(item) && item >= 100 && item <= 599)
82
+ : [];
83
+ return {
84
+ ...DEFAULT_PROXY_RUNTIME,
85
+ ...source,
86
+ enabled: Boolean(source.enabled),
87
+ egress_mode: egressMode as ProxyRuntimeEgressMode,
88
+ proxy_url: String(source.proxy_url || ""),
89
+ resource_proxy_url: String(source.resource_proxy_url || ""),
90
+ skip_ssl_verify: Boolean(source.skip_ssl_verify),
91
+ reset_session_status_codes: statusCodes.length > 0 ? statusCodes : [403],
92
+ clearance: {
93
+ ...DEFAULT_PROXY_RUNTIME.clearance,
94
+ ...clearanceSource,
95
+ enabled: Boolean(clearanceSource.enabled),
96
+ mode: clearanceMode,
97
+ cf_cookies: String(clearanceSource.cf_cookies || ""),
98
+ cf_clearance: String(clearanceSource.cf_clearance || ""),
99
+ user_agent: String(clearanceSource.user_agent || DEFAULT_PROXY_RUNTIME.clearance.user_agent),
100
+ browser: String(clearanceSource.browser || "chrome"),
101
+ flaresolverr_url: String(clearanceSource.flaresolverr_url || ""),
102
+ timeout_sec: Number(clearanceSource.timeout_sec || 60),
103
+ refresh_interval: Number(clearanceSource.refresh_interval || 3600),
104
+ warm_up_on_start: Boolean(clearanceSource.warm_up_on_start),
105
+ has_cf_cookies: Boolean(clearanceSource.has_cf_cookies),
106
+ has_cf_clearance: Boolean(clearanceSource.has_cf_clearance),
107
+ },
108
+ };
109
+ }
110
+
111
+ function normalizeThirdPartyApps(value: unknown): ThirdPartyAppsSettings {
112
+ const source = typeof value === "object" && value !== null ? value as Partial<ThirdPartyAppsSettings> : {};
113
+ const canvas: Partial<ThirdPartyAppsSettings["infinite_canvas"]> = typeof source.infinite_canvas === "object" && source.infinite_canvas
114
+ ? source.infinite_canvas
115
+ : {};
116
+ return {
117
+ infinite_canvas: {
118
+ enabled: Boolean(canvas.enabled),
119
+ url: String(canvas.url || DEFAULT_THIRD_PARTY_APPS.infinite_canvas.url),
120
+ },
121
+ };
122
+ }
123
+
124
+ function normalizeConfig(config: SettingsConfig): SettingsConfig {
125
+ const defaultThinkingEffort = ["standard", "extended", "max"].includes(String(config.default_thinking_effort))
126
+ ? config.default_thinking_effort as "standard" | "extended" | "max"
127
+ : "auto";
128
+ const imageStorage = typeof config.image_storage === "object" && config.image_storage
129
+ ? config.image_storage as ImageStorageSettings
130
+ : {
131
+ enabled: false,
132
+ mode: "local",
133
+ webdav_url: "",
134
+ webdav_username: "",
135
+ webdav_password: "",
136
+ webdav_root_path: "chatgpt2api/images",
137
+ public_base_url: "",
138
+ };
139
+ const imageStorageMode: ImageStorageMode = imageStorage.enabled && imageStorage.mode === "both"
140
+ ? "both"
141
+ : imageStorage.enabled && imageStorage.mode === "webdav"
142
+ ? "webdav"
143
+ : "local";
144
+ const backup = typeof config.backup === "object" && config.backup
145
+ ? config.backup as BackupSettings
146
+ : {
147
+ enabled: false,
148
+ provider: "cloudflare_r2",
149
+ account_id: "",
150
+ access_key_id: "",
151
+ secret_access_key: "",
152
+ bucket: "",
153
+ prefix: "backups",
154
+ interval_minutes: 360,
155
+ rotation_keep: 10,
156
+ encrypt: false,
157
+ passphrase: "",
158
+ include: {
159
+ config: true,
160
+ cpa: true,
161
+ sub2api: true,
162
+ logs: true,
163
+ image_tasks: true,
164
+ accounts_snapshot: true,
165
+ auth_keys_snapshot: true,
166
+ images: false,
167
+ },
168
+ };
169
+ return {
170
+ ...config,
171
+ refresh_account_interval_minute: Number(config.refresh_account_interval_minute || 5),
172
+ image_retention_days: Number(config.image_retention_days || 30),
173
+ image_poll_timeout_secs: Number(config.image_poll_timeout_secs || 120),
174
+ image_account_concurrency: Number(config.image_account_concurrency || 3),
175
+ image_settle_enabled: Boolean(config.image_settle_enabled !== false),
176
+ image_check_before_hit_enabled: Boolean(config.image_check_before_hit_enabled !== false),
177
+ image_remove_conversation_after_result: Boolean(config.image_remove_conversation_after_result),
178
+ image_remove_conversation_always: Boolean(config.image_remove_conversation_always),
179
+ image_settle_secs: Number(config.image_settle_secs || 2.0),
180
+ image_timeout_retry_secs: Number(config.image_timeout_retry_secs || 30),
181
+ auto_remove_invalid_accounts: Boolean(config.auto_remove_invalid_accounts),
182
+ auto_remove_rate_limited_accounts: Boolean(config.auto_remove_rate_limited_accounts),
183
+ auto_relogin_after_refresh: Boolean(config.auto_relogin_after_refresh),
184
+ log_levels: Array.isArray(config.log_levels) ? config.log_levels : [],
185
+ proxy: typeof config.proxy === "string" ? config.proxy : "",
186
+ base_url: typeof config.base_url === "string" ? config.base_url : "",
187
+ global_system_prompt: String(config.global_system_prompt || ""),
188
+ default_upstream_model_name: String(config.default_upstream_model_name || "gpt-5-5"),
189
+ default_thinking_effort: defaultThinkingEffort,
190
+ sensitive_words: Array.isArray(config.sensitive_words) ? config.sensitive_words : [],
191
+ ai_review: {
192
+ enabled: Boolean(config.ai_review?.enabled),
193
+ base_url: String(config.ai_review?.base_url || ""),
194
+ api_key: String(config.ai_review?.api_key || ""),
195
+ model: String(config.ai_review?.model || ""),
196
+ prompt: String(config.ai_review?.prompt || ""),
197
+ },
198
+ image_storage: {
199
+ enabled: Boolean(imageStorage.enabled),
200
+ mode: imageStorageMode,
201
+ webdav_url: String(imageStorage.webdav_url || ""),
202
+ webdav_username: String(imageStorage.webdav_username || ""),
203
+ webdav_password: String(imageStorage.webdav_password || ""),
204
+ webdav_root_path: String(imageStorage.webdav_root_path || "chatgpt2api/images"),
205
+ public_base_url: String(imageStorage.public_base_url || ""),
206
+ },
207
+ proxy_runtime: normalizeProxyRuntime(config.proxy_runtime),
208
+ third_party_apps: normalizeThirdPartyApps(config.third_party_apps),
209
+ backup: {
210
+ ...backup,
211
+ enabled: Boolean(backup.enabled),
212
+ account_id: String(backup.account_id || ""),
213
+ access_key_id: String(backup.access_key_id || ""),
214
+ secret_access_key: String(backup.secret_access_key || ""),
215
+ bucket: String(backup.bucket || ""),
216
+ prefix: String(backup.prefix || "backups"),
217
+ interval_minutes: Number(backup.interval_minutes || 360),
218
+ rotation_keep: Number(backup.rotation_keep ?? 10),
219
+ encrypt: Boolean(backup.encrypt),
220
+ passphrase: String(backup.passphrase || ""),
221
+ include: {
222
+ config: Boolean(backup.include?.config ?? true),
223
+ cpa: Boolean(backup.include?.cpa ?? true),
224
+ sub2api: Boolean(backup.include?.sub2api ?? true),
225
+ logs: Boolean(backup.include?.logs ?? true),
226
+ image_tasks: Boolean(backup.include?.image_tasks ?? true),
227
+ accounts_snapshot: Boolean(backup.include?.accounts_snapshot ?? true),
228
+ auth_keys_snapshot: Boolean(backup.include?.auth_keys_snapshot ?? true),
229
+ images: Boolean(backup.include?.images ?? false),
230
+ },
231
+ },
232
+ };
233
+ }
234
+
235
+ function normalizeFiles(items: CPARemoteFile[]) {
236
+ const seen = new Set<string>();
237
+ const files: CPARemoteFile[] = [];
238
+ for (const item of items) {
239
+ const name = String(item.name || "").trim();
240
+ if (!name || seen.has(name)) {
241
+ continue;
242
+ }
243
+ seen.add(name);
244
+ files.push({
245
+ name,
246
+ email: String(item.email || "").trim(),
247
+ });
248
+ }
249
+ return files;
250
+ }
251
+
252
+ type SettingsStore = {
253
+ config: SettingsConfig | null;
254
+ isLoadingConfig: boolean;
255
+ isSavingConfig: boolean;
256
+ backups: BackupItem[];
257
+ backupState: BackupState | null;
258
+ isLoadingBackups: boolean;
259
+ isRunningBackup: boolean;
260
+ deletingBackupKey: string | null;
261
+ isTestingBackup: boolean;
262
+ isTestingImageStorage: boolean;
263
+ isSyncingImageStorage: boolean;
264
+
265
+ pools: CPAPool[];
266
+ isLoadingPools: boolean;
267
+ deletingId: string | null;
268
+ loadingFilesId: string | null;
269
+
270
+ dialogOpen: boolean;
271
+ editingPool: CPAPool | null;
272
+ formName: string;
273
+ formBaseUrl: string;
274
+ formSecretKey: string;
275
+ showSecret: boolean;
276
+ isSavingPool: boolean;
277
+
278
+ browserOpen: boolean;
279
+ browserPool: CPAPool | null;
280
+ remoteFiles: CPARemoteFile[];
281
+ selectedNames: string[];
282
+ fileQuery: string;
283
+ filePage: number;
284
+ pageSize: PageSizeOption;
285
+ isStartingImport: boolean;
286
+
287
+ initialize: () => Promise<void>;
288
+ loadConfig: () => Promise<void>;
289
+ saveConfig: () => Promise<boolean>;
290
+ loadBackups: (silent?: boolean) => Promise<void>;
291
+ runBackup: () => Promise<void>;
292
+ removeBackup: (key: string) => Promise<void>;
293
+ testBackup: () => Promise<void>;
294
+ setRefreshAccountIntervalMinute: (value: string) => void;
295
+ setImageRetentionDays: (value: string) => void;
296
+ setImagePollTimeoutSecs: (value: string) => void;
297
+ setImageAccountConcurrency: (value: string) => void;
298
+ setImageSettleEnabled: (value: boolean) => void;
299
+ setImageCheckBeforeHitEnabled: (value: boolean) => void;
300
+ setImageRemoveConversationAfterResult: (value: boolean) => void;
301
+ setImageRemoveConversationAlways: (value: boolean) => void;
302
+ setImageSettleSecs: (value: string) => void;
303
+ setImageTimeoutRetrySecs: (value: string) => void;
304
+ setAutoRemoveInvalidAccounts: (value: boolean) => void;
305
+ setAutoRemoveRateLimitedAccounts: (value: boolean) => void;
306
+ setAutoReloginAfterRefresh: (value: boolean) => void;
307
+ setLogLevel: (level: string, enabled: boolean) => void;
308
+ setProxy: (value: string) => void;
309
+ setBaseUrl: (value: string) => void;
310
+ setGlobalSystemPrompt: (value: string) => void;
311
+ setDefaultUpstreamModelName: (value: string) => void;
312
+ setDefaultThinkingEffort: (value: "auto" | "standard" | "extended" | "max") => void;
313
+ setSensitiveWordsText: (value: string) => void;
314
+ setAIReviewField: (key: "enabled" | "base_url" | "api_key" | "model" | "prompt", value: string | boolean) => void;
315
+ setImageStorageField: (key: keyof ImageStorageSettings, value: string | boolean) => void;
316
+ setProxyRuntimeField: <K extends keyof ProxyRuntimeSettings>(key: K, value: ProxyRuntimeSettings[K]) => void;
317
+ setProxyRuntimeClearanceField: <K extends keyof ProxyRuntimeSettings["clearance"]>(key: K, value: ProxyRuntimeSettings["clearance"][K]) => void;
318
+ setProxyRuntimeStatusCodesText: (value: string) => void;
319
+ setInfiniteCanvasField: <K extends keyof ThirdPartyAppsSettings["infinite_canvas"]>(key: K, value: ThirdPartyAppsSettings["infinite_canvas"][K]) => void;
320
+ testImageStorage: () => Promise<void>;
321
+ syncImagesToWebDAV: () => Promise<void>;
322
+ setBackupField: (key: keyof BackupSettings, value: string | boolean) => void;
323
+ setBackupInclude: (key: keyof BackupSettings["include"], value: boolean) => void;
324
+
325
+ loadPools: (silent?: boolean) => Promise<void>;
326
+ openAddDialog: () => void;
327
+ openEditDialog: (pool: CPAPool) => void;
328
+ setDialogOpen: (open: boolean) => void;
329
+ setFormName: (value: string) => void;
330
+ setFormBaseUrl: (value: string) => void;
331
+ setFormSecretKey: (value: string) => void;
332
+ setShowSecret: (checked: boolean) => void;
333
+ savePool: () => Promise<void>;
334
+ deletePool: (pool: CPAPool) => Promise<void>;
335
+
336
+ browseFiles: (pool: CPAPool) => Promise<void>;
337
+ setBrowserOpen: (open: boolean) => void;
338
+ toggleFile: (name: string, checked: boolean) => void;
339
+ replaceSelectedNames: (names: string[]) => void;
340
+ setFileQuery: (value: string) => void;
341
+ setFilePage: (page: number) => void;
342
+ setPageSize: (value: PageSizeOption) => void;
343
+ startImport: () => Promise<void>;
344
+ };
345
+
346
+ export const useSettingsStore = create<SettingsStore>((set, get) => ({
347
+ config: null,
348
+ isLoadingConfig: true,
349
+ isSavingConfig: false,
350
+ backups: [],
351
+ backupState: null,
352
+ isLoadingBackups: true,
353
+ isRunningBackup: false,
354
+ deletingBackupKey: null,
355
+ isTestingBackup: false,
356
+ isTestingImageStorage: false,
357
+ isSyncingImageStorage: false,
358
+
359
+ pools: [],
360
+ isLoadingPools: true,
361
+ deletingId: null,
362
+ loadingFilesId: null,
363
+
364
+ dialogOpen: false,
365
+ editingPool: null,
366
+ formName: "",
367
+ formBaseUrl: "",
368
+ formSecretKey: "",
369
+ showSecret: false,
370
+ isSavingPool: false,
371
+
372
+ browserOpen: false,
373
+ browserPool: null,
374
+ remoteFiles: [],
375
+ selectedNames: [],
376
+ fileQuery: "",
377
+ filePage: 1,
378
+ pageSize: "100",
379
+ isStartingImport: false,
380
+
381
+ initialize: async () => {
382
+ await Promise.allSettled([get().loadConfig(), get().loadPools()]);
383
+ const backup = get().config?.backup;
384
+ const isConfigured = Boolean(
385
+ String(backup?.account_id || "").trim()
386
+ && String(backup?.access_key_id || "").trim()
387
+ && String(backup?.secret_access_key || "").trim()
388
+ && String(backup?.bucket || "").trim(),
389
+ );
390
+ if (isConfigured) {
391
+ await get().loadBackups();
392
+ } else {
393
+ set({ backups: [], isLoadingBackups: false });
394
+ }
395
+ },
396
+
397
+ loadConfig: async () => {
398
+ set({ isLoadingConfig: true });
399
+ try {
400
+ const data = await fetchSettingsConfig();
401
+ const normalized = normalizeConfig(data.config);
402
+ set({
403
+ config: normalized,
404
+ });
405
+ } catch (error) {
406
+ toast.error(error instanceof Error ? error.message : "加载系统配置失败");
407
+ } finally {
408
+ set({ isLoadingConfig: false });
409
+ }
410
+ },
411
+
412
+ saveConfig: async () => {
413
+ const { config } = get();
414
+ if (!config) {
415
+ return false;
416
+ }
417
+
418
+ set({ isSavingConfig: true });
419
+ try {
420
+ const data = await updateSettingsConfig({
421
+ ...config,
422
+ refresh_account_interval_minute: Math.max(1, Number(config.refresh_account_interval_minute) || 1),
423
+ image_retention_days: Math.max(1, Number(config.image_retention_days) || 30),
424
+ image_poll_timeout_secs: Math.max(1, Number(config.image_poll_timeout_secs) || 120),
425
+ image_account_concurrency: Math.max(1, Number(config.image_account_concurrency) || 3),
426
+ image_settle_enabled: Boolean(config.image_settle_enabled !== false),
427
+ image_check_before_hit_enabled: Boolean(config.image_check_before_hit_enabled !== false),
428
+ image_remove_conversation_after_result: Boolean(config.image_remove_conversation_after_result),
429
+ image_remove_conversation_always: Boolean(config.image_remove_conversation_always),
430
+ image_settle_secs: Math.max(0.5, Number(config.image_settle_secs) || 2.0),
431
+ image_timeout_retry_secs: Math.max(1, Number(config.image_timeout_retry_secs) || 30),
432
+ auto_remove_invalid_accounts: Boolean(config.auto_remove_invalid_accounts),
433
+ auto_remove_rate_limited_accounts: Boolean(config.auto_remove_rate_limited_accounts),
434
+ auto_relogin_after_refresh: Boolean(config.auto_relogin_after_refresh),
435
+ proxy: config.proxy.trim(),
436
+ base_url: String(config.base_url || "").trim(),
437
+ global_system_prompt: String(config.global_system_prompt || "").trim(),
438
+ default_upstream_model_name: String(config.default_upstream_model_name || "gpt-5-5").trim() || "gpt-5-5",
439
+ default_thinking_effort: ["standard", "extended", "max"].includes(String(config.default_thinking_effort))
440
+ ? config.default_thinking_effort
441
+ : "auto",
442
+ sensitive_words: (config.sensitive_words || []).map((item) => String(item).trim()).filter(Boolean),
443
+ ai_review: {
444
+ enabled: Boolean(config.ai_review?.enabled),
445
+ base_url: String(config.ai_review?.base_url || "").trim(),
446
+ api_key: String(config.ai_review?.api_key || "").trim(),
447
+ model: String(config.ai_review?.model || "").trim(),
448
+ prompt: String(config.ai_review?.prompt || "").trim(),
449
+ },
450
+ image_storage: {
451
+ enabled: Boolean(config.image_storage?.enabled),
452
+ mode: config.image_storage?.enabled && ["webdav", "both"].includes(String(config.image_storage?.mode)) ? config.image_storage.mode : "local",
453
+ webdav_url: String(config.image_storage?.webdav_url || "").trim(),
454
+ webdav_username: String(config.image_storage?.webdav_username || "").trim(),
455
+ webdav_password: String(config.image_storage?.webdav_password || "").trim(),
456
+ webdav_root_path: String(config.image_storage?.webdav_root_path || "chatgpt2api/images").trim(),
457
+ public_base_url: String(config.image_storage?.public_base_url || "").trim(),
458
+ },
459
+ proxy_runtime: {
460
+ ...normalizeProxyRuntime(config.proxy_runtime),
461
+ proxy_url: String(config.proxy_runtime?.proxy_url || "").trim(),
462
+ resource_proxy_url: String(config.proxy_runtime?.resource_proxy_url || "").trim(),
463
+ reset_session_status_codes: normalizeProxyRuntime({
464
+ reset_session_status_codes: (config.proxy_runtime?.reset_session_status_codes || [403])
465
+ .map((item) => Number(item))
466
+ .filter((item) => Number.isInteger(item) && item >= 100 && item <= 599),
467
+ }).reset_session_status_codes,
468
+ clearance: {
469
+ ...normalizeProxyRuntime(config.proxy_runtime).clearance,
470
+ cf_cookies: String(config.proxy_runtime?.clearance?.cf_cookies || "").trim(),
471
+ cf_clearance: String(config.proxy_runtime?.clearance?.cf_clearance || "").trim(),
472
+ user_agent: String(config.proxy_runtime?.clearance?.user_agent || DEFAULT_PROXY_RUNTIME.clearance.user_agent).trim(),
473
+ browser: String(config.proxy_runtime?.clearance?.browser || "chrome").trim(),
474
+ flaresolverr_url: String(config.proxy_runtime?.clearance?.flaresolverr_url || "").trim(),
475
+ timeout_sec: Math.max(1, Number(config.proxy_runtime?.clearance?.timeout_sec) || 60),
476
+ refresh_interval: Math.max(60, Number(config.proxy_runtime?.clearance?.refresh_interval) || 3600),
477
+ },
478
+ },
479
+ third_party_apps: {
480
+ infinite_canvas: {
481
+ enabled: Boolean(config.third_party_apps?.infinite_canvas?.enabled),
482
+ url: String(config.third_party_apps?.infinite_canvas?.url || DEFAULT_THIRD_PARTY_APPS.infinite_canvas.url).trim(),
483
+ },
484
+ },
485
+ backup: {
486
+ ...(config.backup as BackupSettings),
487
+ account_id: String(config.backup?.account_id || "").trim(),
488
+ access_key_id: String(config.backup?.access_key_id || "").trim(),
489
+ secret_access_key: String(config.backup?.secret_access_key || "").trim(),
490
+ bucket: String(config.backup?.bucket || "").trim(),
491
+ prefix: String(config.backup?.prefix || "backups").trim(),
492
+ interval_minutes: Math.max(1, Number(config.backup?.interval_minutes) || 360),
493
+ rotation_keep: Math.max(0, Number(config.backup?.rotation_keep) || 0),
494
+ passphrase: String(config.backup?.passphrase || "").trim(),
495
+ },
496
+ });
497
+ set({
498
+ config: normalizeConfig(data.config),
499
+ });
500
+ window.dispatchEvent(new Event("third-party-apps-updated"));
501
+ toast.success("配置已保存");
502
+ return true;
503
+ } catch (error) {
504
+ toast.error(error instanceof Error ? error.message : "保存系统配置失败");
505
+ return false;
506
+ } finally {
507
+ set({ isSavingConfig: false });
508
+ }
509
+ },
510
+
511
+ setRefreshAccountIntervalMinute: (value) => {
512
+ set((state) => {
513
+ if (!state.config) {
514
+ return {};
515
+ }
516
+ return {
517
+ config: {
518
+ ...state.config,
519
+ refresh_account_interval_minute: value,
520
+ },
521
+ };
522
+ });
523
+ },
524
+
525
+ setImageRetentionDays: (value) => {
526
+ set((state) => state.config ? { config: { ...state.config, image_retention_days: value } } : {});
527
+ },
528
+
529
+ setImagePollTimeoutSecs: (value) => {
530
+ set((state) => state.config ? { config: { ...state.config, image_poll_timeout_secs: value } } : {});
531
+ },
532
+
533
+ setImageAccountConcurrency: (value) => {
534
+ set((state) => state.config ? { config: { ...state.config, image_account_concurrency: value } } : {});
535
+ },
536
+
537
+ setImageSettleEnabled: (value) => {
538
+ set((state) => state.config ? { config: { ...state.config, image_settle_enabled: value, image_check_before_hit_enabled: value } } : {});
539
+ },
540
+
541
+ setImageCheckBeforeHitEnabled: (value) => {
542
+ set((state) => state.config ? { config: { ...state.config, image_check_before_hit_enabled: value } } : {});
543
+ },
544
+
545
+ setImageRemoveConversationAfterResult: (value) => {
546
+ set((state) => state.config ? { config: { ...state.config, image_remove_conversation_after_result: value } } : {});
547
+ },
548
+
549
+ setImageRemoveConversationAlways: (value) => {
550
+ set((state) => state.config ? { config: { ...state.config, image_remove_conversation_always: value } } : {});
551
+ },
552
+
553
+ setImageSettleSecs: (value) => {
554
+ set((state) => state.config ? { config: { ...state.config, image_settle_secs: value } } : {});
555
+ },
556
+
557
+ setImageTimeoutRetrySecs: (value) => {
558
+ set((state) => state.config ? { config: { ...state.config, image_timeout_retry_secs: value } } : {});
559
+ },
560
+
561
+ setAutoRemoveInvalidAccounts: (value) => {
562
+ set((state) => state.config ? { config: { ...state.config, auto_remove_invalid_accounts: value } } : {});
563
+ },
564
+
565
+ setAutoRemoveRateLimitedAccounts: (value) => {
566
+ set((state) => state.config ? { config: { ...state.config, auto_remove_rate_limited_accounts: value } } : {});
567
+ },
568
+
569
+ setAutoReloginAfterRefresh: (value) => {
570
+ set((state) => state.config ? { config: { ...state.config, auto_relogin_after_refresh: value } } : {});
571
+ },
572
+
573
+ setLogLevel: (level, enabled) => {
574
+ set((state) => {
575
+ if (!state.config) return {};
576
+ const levels = new Set(state.config.log_levels || []);
577
+ if (enabled) levels.add(level);
578
+ else levels.delete(level);
579
+ return { config: { ...state.config, log_levels: Array.from(levels) } };
580
+ });
581
+ },
582
+
583
+ setProxy: (value) => {
584
+ set((state) => {
585
+ if (!state.config) {
586
+ return {};
587
+ }
588
+ return {
589
+ config: {
590
+ ...state.config,
591
+ proxy: value,
592
+ },
593
+ };
594
+ });
595
+ },
596
+
597
+ setBaseUrl: (value) => {
598
+ set((state) => {
599
+ if (!state.config) {
600
+ return {};
601
+ }
602
+ return {
603
+ config: {
604
+ ...state.config,
605
+ base_url: value,
606
+ },
607
+ };
608
+ });
609
+ },
610
+
611
+ setGlobalSystemPrompt: (value) => {
612
+ set((state) => state.config ? { config: { ...state.config, global_system_prompt: value } } : {});
613
+ },
614
+
615
+ setDefaultUpstreamModelName: (value) => {
616
+ set((state) => state.config ? { config: { ...state.config, default_upstream_model_name: value } } : {});
617
+ },
618
+
619
+ setDefaultThinkingEffort: (value) => {
620
+ set((state) => state.config ? { config: { ...state.config, default_thinking_effort: value } } : {});
621
+ },
622
+
623
+ setSensitiveWordsText: (value) => {
624
+ set((state) => state.config ? { config: { ...state.config, sensitive_words: value.split("\n") } } : {});
625
+ },
626
+
627
+ setAIReviewField: (key, value) => {
628
+ set((state) => state.config ? { config: { ...state.config, ai_review: { ...(state.config.ai_review || {}), [key]: value } } } : {});
629
+ },
630
+
631
+ setImageStorageField: (key, value) => {
632
+ set((state) => {
633
+ if (!state.config?.image_storage) {
634
+ return {};
635
+ }
636
+ const next = {
637
+ ...state.config.image_storage,
638
+ [key]: value,
639
+ };
640
+ if (key === "enabled" && !value) {
641
+ next.mode = "local";
642
+ }
643
+ if (key === "enabled" && value && next.mode === "local") {
644
+ next.mode = "webdav";
645
+ }
646
+ return {
647
+ config: {
648
+ ...state.config,
649
+ image_storage: next,
650
+ },
651
+ };
652
+ });
653
+ },
654
+
655
+ setProxyRuntimeField: (key, value) => {
656
+ set((state) => {
657
+ if (!state.config) {
658
+ return {};
659
+ }
660
+ const runtime = normalizeProxyRuntime(state.config.proxy_runtime);
661
+ const nextRuntime = normalizeProxyRuntime({
662
+ ...runtime,
663
+ [key]: value,
664
+ });
665
+ return {
666
+ config: {
667
+ ...state.config,
668
+ proxy_runtime: nextRuntime,
669
+ },
670
+ };
671
+ });
672
+ },
673
+
674
+ setProxyRuntimeClearanceField: (key, value) => {
675
+ set((state) => {
676
+ if (!state.config) {
677
+ return {};
678
+ }
679
+ const runtime = normalizeProxyRuntime(state.config.proxy_runtime);
680
+ const nextRuntime = normalizeProxyRuntime({
681
+ ...runtime,
682
+ clearance: {
683
+ ...runtime.clearance,
684
+ [key]: value,
685
+ },
686
+ });
687
+ return {
688
+ config: {
689
+ ...state.config,
690
+ proxy_runtime: nextRuntime,
691
+ },
692
+ };
693
+ });
694
+ },
695
+
696
+ setProxyRuntimeStatusCodesText: (value) => {
697
+ const codes = value
698
+ .split(/[,\s]+/)
699
+ .map((item) => Number(item.trim()))
700
+ .filter((item) => Number.isInteger(item) && item >= 100 && item <= 599);
701
+ set((state) => {
702
+ if (!state.config) {
703
+ return {};
704
+ }
705
+ const runtime = normalizeProxyRuntime(state.config.proxy_runtime);
706
+ return {
707
+ config: {
708
+ ...state.config,
709
+ proxy_runtime: normalizeProxyRuntime({
710
+ ...runtime,
711
+ reset_session_status_codes: codes.length > 0 ? codes : [403],
712
+ }),
713
+ },
714
+ };
715
+ });
716
+ },
717
+
718
+ setInfiniteCanvasField: (key, value) => {
719
+ set((state) => {
720
+ if (!state.config) {
721
+ return {};
722
+ }
723
+ const apps = normalizeThirdPartyApps(state.config.third_party_apps);
724
+ return {
725
+ config: {
726
+ ...state.config,
727
+ third_party_apps: {
728
+ ...apps,
729
+ infinite_canvas: {
730
+ ...apps.infinite_canvas,
731
+ [key]: value,
732
+ },
733
+ },
734
+ },
735
+ };
736
+ });
737
+ },
738
+
739
+ testImageStorage: async () => {
740
+ set({ isTestingImageStorage: true });
741
+ try {
742
+ const saved = await get().saveConfig();
743
+ if (!saved) {
744
+ return;
745
+ }
746
+ const data = await testImageStorageConnection();
747
+ if (data.result.ok) {
748
+ toast.success(`WebDAV 连接可用:HTTP ${data.result.status}`);
749
+ } else {
750
+ toast.error(`WebDAV 连接失败:${data.result.error ?? `HTTP ${data.result.status}`}`);
751
+ }
752
+ } catch (error) {
753
+ toast.error(error instanceof Error ? error.message : "测试 WebDAV 失败");
754
+ } finally {
755
+ set({ isTestingImageStorage: false });
756
+ }
757
+ },
758
+
759
+ syncImagesToWebDAV: async () => {
760
+ set({ isSyncingImageStorage: true });
761
+ try {
762
+ const saved = await get().saveConfig();
763
+ if (!saved) {
764
+ return;
765
+ }
766
+ const data = await syncImageStorage();
767
+ toast.success(`同步完成:上传 ${data.result.uploaded},跳过 ${data.result.skipped},失败 ${data.result.failed}`);
768
+ } catch (error) {
769
+ toast.error(error instanceof Error ? error.message : "同步图片失败");
770
+ } finally {
771
+ set({ isSyncingImageStorage: false });
772
+ }
773
+ },
774
+
775
+ setBackupField: (key, value) => {
776
+ set((state) => {
777
+ if (!state.config?.backup) {
778
+ return {};
779
+ }
780
+ return {
781
+ config: {
782
+ ...state.config,
783
+ backup: {
784
+ ...state.config.backup,
785
+ [key]: value,
786
+ },
787
+ },
788
+ };
789
+ });
790
+ },
791
+
792
+ setBackupInclude: (key, value) => {
793
+ set((state) => {
794
+ if (!state.config?.backup) {
795
+ return {};
796
+ }
797
+ return {
798
+ config: {
799
+ ...state.config,
800
+ backup: {
801
+ ...state.config.backup,
802
+ include: {
803
+ ...state.config.backup.include,
804
+ [key]: value,
805
+ },
806
+ },
807
+ },
808
+ };
809
+ });
810
+ },
811
+
812
+ loadBackups: async (silent = false) => {
813
+ if (!silent) {
814
+ set({ isLoadingBackups: true });
815
+ }
816
+ try {
817
+ const data = await fetchBackups();
818
+ set({
819
+ backups: data.items,
820
+ backupState: data.state,
821
+ });
822
+ } catch (error) {
823
+ if (!silent) {
824
+ toast.error(error instanceof Error ? error.message : "加载备份列表失败");
825
+ }
826
+ } finally {
827
+ if (!silent) {
828
+ set({ isLoadingBackups: false });
829
+ }
830
+ }
831
+ },
832
+
833
+ runBackup: async () => {
834
+ set({ isRunningBackup: true });
835
+ try {
836
+ const saved = await get().saveConfig();
837
+ if (!saved) {
838
+ return;
839
+ }
840
+ const data = await runBackupNow();
841
+ toast.success(`备份已完成:${data.result.key}`);
842
+ await get().loadBackups(true);
843
+ } catch (error) {
844
+ toast.error(error instanceof Error ? error.message : "执行备份失败");
845
+ } finally {
846
+ set({ isRunningBackup: false });
847
+ }
848
+ },
849
+
850
+ removeBackup: async (key) => {
851
+ set({ deletingBackupKey: key });
852
+ try {
853
+ await deleteBackup(key);
854
+ toast.success("备份已删除");
855
+ await get().loadBackups(true);
856
+ } catch (error) {
857
+ toast.error(error instanceof Error ? error.message : "删除备份失败");
858
+ } finally {
859
+ set({ deletingBackupKey: null });
860
+ }
861
+ },
862
+
863
+ testBackup: async () => {
864
+ set({ isTestingBackup: true });
865
+ try {
866
+ const saved = await get().saveConfig();
867
+ if (!saved) {
868
+ return;
869
+ }
870
+ const data = await testBackupConnection();
871
+ toast.success(`R2 连接正常(HTTP ${data.result.status})`);
872
+ } catch (error) {
873
+ toast.error(error instanceof Error ? error.message : "测试备份连接失败");
874
+ } finally {
875
+ set({ isTestingBackup: false });
876
+ }
877
+ },
878
+
879
+ loadPools: async (silent = false) => {
880
+ if (!silent) {
881
+ set({ isLoadingPools: true });
882
+ }
883
+ try {
884
+ const data = await fetchCPAPools();
885
+ set({ pools: data.pools });
886
+ } catch (error) {
887
+ if (!silent) {
888
+ toast.error(error instanceof Error ? error.message : "加载 CPA 连接失败");
889
+ }
890
+ } finally {
891
+ if (!silent) {
892
+ set({ isLoadingPools: false });
893
+ }
894
+ }
895
+ },
896
+
897
+ openAddDialog: () => {
898
+ set({
899
+ editingPool: null,
900
+ formName: "",
901
+ formBaseUrl: "",
902
+ formSecretKey: "",
903
+ showSecret: false,
904
+ dialogOpen: true,
905
+ });
906
+ },
907
+
908
+ openEditDialog: (pool) => {
909
+ set({
910
+ editingPool: pool,
911
+ formName: pool.name,
912
+ formBaseUrl: pool.base_url,
913
+ formSecretKey: "",
914
+ showSecret: false,
915
+ dialogOpen: true,
916
+ });
917
+ },
918
+
919
+ setDialogOpen: (open) => {
920
+ set({ dialogOpen: open });
921
+ },
922
+
923
+ setFormName: (value) => {
924
+ set({ formName: value });
925
+ },
926
+
927
+ setFormBaseUrl: (value) => {
928
+ set({ formBaseUrl: value });
929
+ },
930
+
931
+ setFormSecretKey: (value) => {
932
+ set({ formSecretKey: value });
933
+ },
934
+
935
+ setShowSecret: (checked) => {
936
+ set({ showSecret: checked });
937
+ },
938
+
939
+ savePool: async () => {
940
+ const { editingPool, formName, formBaseUrl, formSecretKey } = get();
941
+ if (!formBaseUrl.trim()) {
942
+ toast.error("请输入 CPA 地址");
943
+ return;
944
+ }
945
+ if (!editingPool && !formSecretKey.trim()) {
946
+ toast.error("请输入 Secret Key");
947
+ return;
948
+ }
949
+
950
+ set({ isSavingPool: true });
951
+ try {
952
+ if (editingPool) {
953
+ const data = await updateCPAPool(editingPool.id, {
954
+ name: formName.trim(),
955
+ base_url: formBaseUrl.trim(),
956
+ secret_key: formSecretKey.trim() || undefined,
957
+ });
958
+ set({ pools: data.pools, dialogOpen: false });
959
+ toast.success("连接已更新");
960
+ } else {
961
+ const data = await createCPAPool({
962
+ name: formName.trim(),
963
+ base_url: formBaseUrl.trim(),
964
+ secret_key: formSecretKey.trim(),
965
+ });
966
+ set({ pools: data.pools, dialogOpen: false });
967
+ toast.success("连接已添加");
968
+ }
969
+ } catch (error) {
970
+ toast.error(error instanceof Error ? error.message : "保存失败");
971
+ } finally {
972
+ set({ isSavingPool: false });
973
+ }
974
+ },
975
+
976
+ deletePool: async (pool) => {
977
+ set({ deletingId: pool.id });
978
+ try {
979
+ const data = await deleteCPAPool(pool.id);
980
+ set({ pools: data.pools });
981
+ toast.success("连接已删除");
982
+ } catch (error) {
983
+ toast.error(error instanceof Error ? error.message : "删除失败");
984
+ } finally {
985
+ set({ deletingId: null });
986
+ }
987
+ },
988
+
989
+ browseFiles: async (pool) => {
990
+ set({ loadingFilesId: pool.id });
991
+ try {
992
+ const data = await fetchCPAPoolFiles(pool.id);
993
+ const files = normalizeFiles(data.files);
994
+ set({
995
+ browserPool: pool,
996
+ remoteFiles: files,
997
+ selectedNames: [],
998
+ fileQuery: "",
999
+ filePage: 1,
1000
+ browserOpen: true,
1001
+ });
1002
+ toast.success(`读取成功,共 ${files.length} 个远程账号`);
1003
+ } catch (error) {
1004
+ toast.error(error instanceof Error ? error.message : "读取远程账号失败");
1005
+ } finally {
1006
+ set({ loadingFilesId: null });
1007
+ }
1008
+ },
1009
+
1010
+ setBrowserOpen: (open) => {
1011
+ set({ browserOpen: open });
1012
+ },
1013
+
1014
+ toggleFile: (name, checked) => {
1015
+ set((state) => {
1016
+ if (checked) {
1017
+ return {
1018
+ selectedNames: Array.from(new Set([...state.selectedNames, name])),
1019
+ };
1020
+ }
1021
+ return {
1022
+ selectedNames: state.selectedNames.filter((item) => item !== name),
1023
+ };
1024
+ });
1025
+ },
1026
+
1027
+ replaceSelectedNames: (names) => {
1028
+ set({ selectedNames: Array.from(new Set(names)) });
1029
+ },
1030
+
1031
+ setFileQuery: (value) => {
1032
+ set({ fileQuery: value, filePage: 1 });
1033
+ },
1034
+
1035
+ setFilePage: (page) => {
1036
+ set({ filePage: page });
1037
+ },
1038
+
1039
+ setPageSize: (value) => {
1040
+ set({ pageSize: value, filePage: 1 });
1041
+ },
1042
+
1043
+ startImport: async () => {
1044
+ const { browserPool, selectedNames, pools } = get();
1045
+ if (!browserPool) {
1046
+ return;
1047
+ }
1048
+ if (selectedNames.length === 0) {
1049
+ toast.error("请先选择要导入的账号");
1050
+ return;
1051
+ }
1052
+
1053
+ set({ isStartingImport: true });
1054
+ try {
1055
+ const result = await startCPAImport(browserPool.id, selectedNames);
1056
+ set({
1057
+ pools: pools.map((pool) =>
1058
+ pool.id === browserPool.id ? { ...pool, import_job: result.import_job } : pool,
1059
+ ),
1060
+ browserOpen: false,
1061
+ });
1062
+ toast.success("导入任务已启动");
1063
+ } catch (error) {
1064
+ toast.error(error instanceof Error ? error.message : "启动导入失败");
1065
+ } finally {
1066
+ set({ isStartingImport: false });
1067
+ }
1068
+ },
1069
+ }));