dsh-plugin-image-workbench 1.1.5 → 1.1.6

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 (172) hide show
  1. package/LICENSE +21 -21
  2. package/README.en.md +5 -3
  3. package/README.md +7 -3
  4. package/client.js +241 -241
  5. package/cordis.patch.yml +14 -14
  6. package/host.js +51 -0
  7. package/install.mjs +333 -387
  8. package/package.json +3 -2
  9. package/scripts/fetch-style-images.mjs +104 -0
  10. package/scripts/vendor-snapshot.py +154 -154
  11. package/server/access.py +26 -26
  12. package/server/app.py +1329 -1329
  13. package/server/catalog.py +147 -147
  14. package/server/composer.py +391 -391
  15. package/server/drafts.py +158 -158
  16. package/server/history.py +575 -575
  17. package/server/imaging.py +172 -172
  18. package/server/pool.py +1335 -1335
  19. package/server/pool_admin.py +921 -921
  20. package/server/pool_service.py +1073 -1073
  21. package/server/requirements.txt +6 -6
  22. package/server/smoke_check.py +402 -402
  23. package/server/static/access.html +202 -202
  24. package/vendor/chatgpt2api/source/.env.example +82 -82
  25. package/vendor/chatgpt2api/source/.python-version +1 -1
  26. package/vendor/chatgpt2api/source/LICENSE +21 -21
  27. package/vendor/chatgpt2api/source/README.md +391 -391
  28. package/vendor/chatgpt2api/source/VERSION +1 -1
  29. package/vendor/chatgpt2api/source/api/__init__.py +2 -2
  30. package/vendor/chatgpt2api/source/api/accounts.py +537 -537
  31. package/vendor/chatgpt2api/source/api/ai.py +217 -217
  32. package/vendor/chatgpt2api/source/api/app.py +62 -62
  33. package/vendor/chatgpt2api/source/api/errors.py +46 -46
  34. package/vendor/chatgpt2api/source/api/image_inputs.py +309 -309
  35. package/vendor/chatgpt2api/source/api/image_tasks.py +120 -120
  36. package/vendor/chatgpt2api/source/api/support.py +134 -134
  37. package/vendor/chatgpt2api/source/api/system.py +365 -365
  38. package/vendor/chatgpt2api/source/assets/kunrouai.svg +50 -50
  39. package/vendor/chatgpt2api/source/config.default.json +108 -108
  40. package/vendor/chatgpt2api/source/main.py +9 -9
  41. package/vendor/chatgpt2api/source/pyproject.toml +27 -27
  42. package/vendor/chatgpt2api/source/scripts/init_proxy_config.py +186 -186
  43. package/vendor/chatgpt2api/source/scripts/migrate_storage.py +163 -163
  44. package/vendor/chatgpt2api/source/scripts/privoxy-warp.conf +13 -13
  45. package/vendor/chatgpt2api/source/scripts/test_outlook_token_mailbox.py +385 -385
  46. package/vendor/chatgpt2api/source/scripts/test_storage.py +129 -129
  47. package/vendor/chatgpt2api/source/scripts/verify_oauth_refresh.py +92 -92
  48. package/vendor/chatgpt2api/source/services/account_service.py +1757 -1757
  49. package/vendor/chatgpt2api/source/services/auth_service.py +253 -253
  50. package/vendor/chatgpt2api/source/services/backup_service.py +671 -671
  51. package/vendor/chatgpt2api/source/services/config.py +667 -667
  52. package/vendor/chatgpt2api/source/services/content_filter.py +242 -242
  53. package/vendor/chatgpt2api/source/services/cpa_service.py +315 -315
  54. package/vendor/chatgpt2api/source/services/editable_file_task_service.py +260 -260
  55. package/vendor/chatgpt2api/source/services/image_service.py +376 -376
  56. package/vendor/chatgpt2api/source/services/image_storage_service.py +405 -405
  57. package/vendor/chatgpt2api/source/services/image_tags_service.py +76 -76
  58. package/vendor/chatgpt2api/source/services/image_task_service.py +550 -550
  59. package/vendor/chatgpt2api/source/services/log_service.py +337 -337
  60. package/vendor/chatgpt2api/source/services/model_service.py +182 -182
  61. package/vendor/chatgpt2api/source/services/oauth_login_service.py +267 -267
  62. package/vendor/chatgpt2api/source/services/openai_backend_api.py +2763 -2763
  63. package/vendor/chatgpt2api/source/services/openai_oauth.py +32 -32
  64. package/vendor/chatgpt2api/source/services/protocol/__init__.py +2 -2
  65. package/vendor/chatgpt2api/source/services/protocol/anthropic_v1_messages.py +307 -307
  66. package/vendor/chatgpt2api/source/services/protocol/chat_completion_cache.py +238 -238
  67. package/vendor/chatgpt2api/source/services/protocol/conversation.py +1644 -1644
  68. package/vendor/chatgpt2api/source/services/protocol/error_response.py +124 -124
  69. package/vendor/chatgpt2api/source/services/protocol/openai_search.py +19 -19
  70. package/vendor/chatgpt2api/source/services/protocol/openai_v1_chat_complete.py +316 -316
  71. package/vendor/chatgpt2api/source/services/protocol/openai_v1_image_edit.py +86 -86
  72. package/vendor/chatgpt2api/source/services/protocol/openai_v1_image_generations.py +42 -42
  73. package/vendor/chatgpt2api/source/services/protocol/openai_v1_models.py +53 -53
  74. package/vendor/chatgpt2api/source/services/protocol/openai_v1_response.py +453 -453
  75. package/vendor/chatgpt2api/source/services/protocol/web_search_tool.py +160 -160
  76. package/vendor/chatgpt2api/source/services/proxy_service.py +648 -648
  77. package/vendor/chatgpt2api/source/services/storage/__init__.py +5 -5
  78. package/vendor/chatgpt2api/source/services/storage/base.py +38 -38
  79. package/vendor/chatgpt2api/source/services/storage/database_storage.py +194 -194
  80. package/vendor/chatgpt2api/source/services/storage/factory.py +104 -104
  81. package/vendor/chatgpt2api/source/services/storage/git_storage.py +183 -183
  82. package/vendor/chatgpt2api/source/services/storage/json_storage.py +95 -95
  83. package/vendor/chatgpt2api/source/services/sub2api_service.py +531 -531
  84. package/vendor/chatgpt2api/source/utils/helper.py +469 -469
  85. package/vendor/chatgpt2api/source/utils/image_tokens.py +346 -346
  86. package/vendor/chatgpt2api/source/utils/log.py +110 -110
  87. package/vendor/chatgpt2api/source/utils/pkce.py +19 -19
  88. package/vendor/chatgpt2api/source/utils/pow.py +207 -207
  89. package/vendor/chatgpt2api/source/utils/sentinel.py +159 -159
  90. package/vendor/chatgpt2api/source/utils/turnstile.py +184 -184
  91. package/vendor/chatgpt2api/source/uv.lock +904 -904
  92. package/vendor/chatgpt2api/source/web/bun.lock +1501 -1501
  93. package/vendor/chatgpt2api/source/web/components.json +24 -24
  94. package/vendor/chatgpt2api/source/web/eslint.config.mjs +17 -17
  95. package/vendor/chatgpt2api/source/web/next.config.ts +40 -40
  96. package/vendor/chatgpt2api/source/web/package.json +58 -58
  97. package/vendor/chatgpt2api/source/web/postcss.config.mjs +5 -5
  98. package/vendor/chatgpt2api/source/web/src/app/accounts/components/account-import-dialog.tsx +925 -925
  99. package/vendor/chatgpt2api/source/web/src/app/accounts/page.tsx +1307 -1307
  100. package/vendor/chatgpt2api/source/web/src/app/debug/components/chat-panel.tsx +224 -224
  101. package/vendor/chatgpt2api/source/web/src/app/debug/components/editable-file-panel.tsx +444 -444
  102. package/vendor/chatgpt2api/source/web/src/app/debug/components/ppt-panel.tsx +9 -9
  103. package/vendor/chatgpt2api/source/web/src/app/debug/components/psd-panel.tsx +9 -9
  104. package/vendor/chatgpt2api/source/web/src/app/debug/components/search-panel.tsx +172 -172
  105. package/vendor/chatgpt2api/source/web/src/app/debug/components/skill-panel.tsx +169 -169
  106. package/vendor/chatgpt2api/source/web/src/app/debug/components/types.ts +39 -39
  107. package/vendor/chatgpt2api/source/web/src/app/debug/page.tsx +59 -59
  108. package/vendor/chatgpt2api/source/web/src/app/globals.css +230 -230
  109. package/vendor/chatgpt2api/source/web/src/app/image/components/image-composer.tsx +542 -542
  110. package/vendor/chatgpt2api/source/web/src/app/image/components/image-results.tsx +554 -554
  111. package/vendor/chatgpt2api/source/web/src/app/image/components/image-sidebar.tsx +176 -176
  112. package/vendor/chatgpt2api/source/web/src/app/image/page.tsx +1799 -1799
  113. package/vendor/chatgpt2api/source/web/src/app/image-manager/page.tsx +732 -732
  114. package/vendor/chatgpt2api/source/web/src/app/layout.tsx +50 -50
  115. package/vendor/chatgpt2api/source/web/src/app/login/page.tsx +101 -101
  116. package/vendor/chatgpt2api/source/web/src/app/logs/page.tsx +356 -356
  117. package/vendor/chatgpt2api/source/web/src/app/page.tsx +30 -30
  118. package/vendor/chatgpt2api/source/web/src/app/settings/components/api-docs-card.tsx +341 -341
  119. package/vendor/chatgpt2api/source/web/src/app/settings/components/backup-settings-card.tsx +447 -447
  120. package/vendor/chatgpt2api/source/web/src/app/settings/components/base-url-card.tsx +71 -71
  121. package/vendor/chatgpt2api/source/web/src/app/settings/components/config-card.tsx +491 -491
  122. package/vendor/chatgpt2api/source/web/src/app/settings/components/cpa-pool-dialog.tsx +108 -108
  123. package/vendor/chatgpt2api/source/web/src/app/settings/components/cpa-pools-card.tsx +169 -169
  124. package/vendor/chatgpt2api/source/web/src/app/settings/components/import-browser-dialog.tsx +185 -185
  125. package/vendor/chatgpt2api/source/web/src/app/settings/components/proxy-runtime-card.tsx +379 -379
  126. package/vendor/chatgpt2api/source/web/src/app/settings/components/proxy-settings-card.tsx +127 -127
  127. package/vendor/chatgpt2api/source/web/src/app/settings/components/proxy-settings.tsx +200 -200
  128. package/vendor/chatgpt2api/source/web/src/app/settings/components/settings-header.tsx +12 -12
  129. package/vendor/chatgpt2api/source/web/src/app/settings/components/sub2api-connections.tsx +886 -886
  130. package/vendor/chatgpt2api/source/web/src/app/settings/components/third-party-apps-card.tsx +81 -81
  131. package/vendor/chatgpt2api/source/web/src/app/settings/components/user-keys-card.tsx +422 -422
  132. package/vendor/chatgpt2api/source/web/src/app/settings/page.tsx +135 -135
  133. package/vendor/chatgpt2api/source/web/src/app/settings/store.ts +1069 -1069
  134. package/vendor/chatgpt2api/source/web/src/components/date-range-filter.tsx +49 -49
  135. package/vendor/chatgpt2api/source/web/src/components/header-actions.tsx +24 -24
  136. package/vendor/chatgpt2api/source/web/src/components/image-lightbox.tsx +434 -434
  137. package/vendor/chatgpt2api/source/web/src/components/image-thumbnail.tsx +46 -46
  138. package/vendor/chatgpt2api/source/web/src/components/theme-script.tsx +18 -18
  139. package/vendor/chatgpt2api/source/web/src/components/theme-toggle.tsx +14 -14
  140. package/vendor/chatgpt2api/source/web/src/components/top-nav.tsx +263 -263
  141. package/vendor/chatgpt2api/source/web/src/components/ui/animated-theme-toggler.tsx +256 -256
  142. package/vendor/chatgpt2api/source/web/src/components/ui/badge.tsx +41 -41
  143. package/vendor/chatgpt2api/source/web/src/components/ui/button.tsx +59 -59
  144. package/vendor/chatgpt2api/source/web/src/components/ui/calendar.tsx +46 -46
  145. package/vendor/chatgpt2api/source/web/src/components/ui/card.tsx +61 -61
  146. package/vendor/chatgpt2api/source/web/src/components/ui/checkbox.tsx +27 -27
  147. package/vendor/chatgpt2api/source/web/src/components/ui/dia-text-reveal.tsx +267 -267
  148. package/vendor/chatgpt2api/source/web/src/components/ui/dialog.tsx +132 -132
  149. package/vendor/chatgpt2api/source/web/src/components/ui/field.tsx +13 -13
  150. package/vendor/chatgpt2api/source/web/src/components/ui/input.tsx +19 -19
  151. package/vendor/chatgpt2api/source/web/src/components/ui/label.tsx +24 -24
  152. package/vendor/chatgpt2api/source/web/src/components/ui/morphing-text.tsx +149 -149
  153. package/vendor/chatgpt2api/source/web/src/components/ui/popover.tsx +38 -38
  154. package/vendor/chatgpt2api/source/web/src/components/ui/select.tsx +175 -175
  155. package/vendor/chatgpt2api/source/web/src/components/ui/sheet.tsx +143 -143
  156. package/vendor/chatgpt2api/source/web/src/components/ui/table.tsx +29 -29
  157. package/vendor/chatgpt2api/source/web/src/components/ui/tabs.tsx +91 -91
  158. package/vendor/chatgpt2api/source/web/src/components/ui/text-3d-flip.tsx +269 -269
  159. package/vendor/chatgpt2api/source/web/src/components/ui/textarea.tsx +18 -18
  160. package/vendor/chatgpt2api/source/web/src/components/version-release-dialog.tsx +127 -127
  161. package/vendor/chatgpt2api/source/web/src/constants/common-env.ts +6 -6
  162. package/vendor/chatgpt2api/source/web/src/hooks/use-version-check.ts +85 -85
  163. package/vendor/chatgpt2api/source/web/src/lib/api.ts +926 -926
  164. package/vendor/chatgpt2api/source/web/src/lib/auth-session.ts +26 -26
  165. package/vendor/chatgpt2api/source/web/src/lib/release.ts +25 -25
  166. package/vendor/chatgpt2api/source/web/src/lib/request.ts +93 -93
  167. package/vendor/chatgpt2api/source/web/src/lib/use-auth-guard.ts +89 -89
  168. package/vendor/chatgpt2api/source/web/src/lib/utils.ts +6 -6
  169. package/vendor/chatgpt2api/source/web/src/store/auth.ts +112 -112
  170. package/vendor/chatgpt2api/source/web/src/store/editable-file-history.ts +33 -33
  171. package/vendor/chatgpt2api/source/web/src/store/image-conversations.ts +318 -318
  172. package/vendor/chatgpt2api/source/web/tsconfig.json +42 -42
@@ -1,1069 +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
- }));
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
+ }));