dsh-plugin-image-workbench 1.1.5 → 1.1.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (173) 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 +1371 -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/server/static/index.html +15 -0
  25. package/vendor/chatgpt2api/source/.env.example +82 -82
  26. package/vendor/chatgpt2api/source/.python-version +1 -1
  27. package/vendor/chatgpt2api/source/LICENSE +21 -21
  28. package/vendor/chatgpt2api/source/README.md +391 -391
  29. package/vendor/chatgpt2api/source/VERSION +1 -1
  30. package/vendor/chatgpt2api/source/api/__init__.py +2 -2
  31. package/vendor/chatgpt2api/source/api/accounts.py +537 -537
  32. package/vendor/chatgpt2api/source/api/ai.py +217 -217
  33. package/vendor/chatgpt2api/source/api/app.py +62 -62
  34. package/vendor/chatgpt2api/source/api/errors.py +46 -46
  35. package/vendor/chatgpt2api/source/api/image_inputs.py +309 -309
  36. package/vendor/chatgpt2api/source/api/image_tasks.py +120 -120
  37. package/vendor/chatgpt2api/source/api/support.py +134 -134
  38. package/vendor/chatgpt2api/source/api/system.py +365 -365
  39. package/vendor/chatgpt2api/source/assets/kunrouai.svg +50 -50
  40. package/vendor/chatgpt2api/source/config.default.json +108 -108
  41. package/vendor/chatgpt2api/source/main.py +9 -9
  42. package/vendor/chatgpt2api/source/pyproject.toml +27 -27
  43. package/vendor/chatgpt2api/source/scripts/init_proxy_config.py +186 -186
  44. package/vendor/chatgpt2api/source/scripts/migrate_storage.py +163 -163
  45. package/vendor/chatgpt2api/source/scripts/privoxy-warp.conf +13 -13
  46. package/vendor/chatgpt2api/source/scripts/test_outlook_token_mailbox.py +385 -385
  47. package/vendor/chatgpt2api/source/scripts/test_storage.py +129 -129
  48. package/vendor/chatgpt2api/source/scripts/verify_oauth_refresh.py +92 -92
  49. package/vendor/chatgpt2api/source/services/account_service.py +1757 -1757
  50. package/vendor/chatgpt2api/source/services/auth_service.py +253 -253
  51. package/vendor/chatgpt2api/source/services/backup_service.py +671 -671
  52. package/vendor/chatgpt2api/source/services/config.py +667 -667
  53. package/vendor/chatgpt2api/source/services/content_filter.py +242 -242
  54. package/vendor/chatgpt2api/source/services/cpa_service.py +315 -315
  55. package/vendor/chatgpt2api/source/services/editable_file_task_service.py +260 -260
  56. package/vendor/chatgpt2api/source/services/image_service.py +376 -376
  57. package/vendor/chatgpt2api/source/services/image_storage_service.py +405 -405
  58. package/vendor/chatgpt2api/source/services/image_tags_service.py +76 -76
  59. package/vendor/chatgpt2api/source/services/image_task_service.py +550 -550
  60. package/vendor/chatgpt2api/source/services/log_service.py +337 -337
  61. package/vendor/chatgpt2api/source/services/model_service.py +182 -182
  62. package/vendor/chatgpt2api/source/services/oauth_login_service.py +267 -267
  63. package/vendor/chatgpt2api/source/services/openai_backend_api.py +2763 -2763
  64. package/vendor/chatgpt2api/source/services/openai_oauth.py +32 -32
  65. package/vendor/chatgpt2api/source/services/protocol/__init__.py +2 -2
  66. package/vendor/chatgpt2api/source/services/protocol/anthropic_v1_messages.py +307 -307
  67. package/vendor/chatgpt2api/source/services/protocol/chat_completion_cache.py +238 -238
  68. package/vendor/chatgpt2api/source/services/protocol/conversation.py +1644 -1644
  69. package/vendor/chatgpt2api/source/services/protocol/error_response.py +124 -124
  70. package/vendor/chatgpt2api/source/services/protocol/openai_search.py +19 -19
  71. package/vendor/chatgpt2api/source/services/protocol/openai_v1_chat_complete.py +316 -316
  72. package/vendor/chatgpt2api/source/services/protocol/openai_v1_image_edit.py +86 -86
  73. package/vendor/chatgpt2api/source/services/protocol/openai_v1_image_generations.py +42 -42
  74. package/vendor/chatgpt2api/source/services/protocol/openai_v1_models.py +53 -53
  75. package/vendor/chatgpt2api/source/services/protocol/openai_v1_response.py +453 -453
  76. package/vendor/chatgpt2api/source/services/protocol/web_search_tool.py +160 -160
  77. package/vendor/chatgpt2api/source/services/proxy_service.py +648 -648
  78. package/vendor/chatgpt2api/source/services/storage/__init__.py +5 -5
  79. package/vendor/chatgpt2api/source/services/storage/base.py +38 -38
  80. package/vendor/chatgpt2api/source/services/storage/database_storage.py +194 -194
  81. package/vendor/chatgpt2api/source/services/storage/factory.py +104 -104
  82. package/vendor/chatgpt2api/source/services/storage/git_storage.py +183 -183
  83. package/vendor/chatgpt2api/source/services/storage/json_storage.py +95 -95
  84. package/vendor/chatgpt2api/source/services/sub2api_service.py +531 -531
  85. package/vendor/chatgpt2api/source/utils/helper.py +469 -469
  86. package/vendor/chatgpt2api/source/utils/image_tokens.py +346 -346
  87. package/vendor/chatgpt2api/source/utils/log.py +110 -110
  88. package/vendor/chatgpt2api/source/utils/pkce.py +19 -19
  89. package/vendor/chatgpt2api/source/utils/pow.py +207 -207
  90. package/vendor/chatgpt2api/source/utils/sentinel.py +159 -159
  91. package/vendor/chatgpt2api/source/utils/turnstile.py +184 -184
  92. package/vendor/chatgpt2api/source/uv.lock +904 -904
  93. package/vendor/chatgpt2api/source/web/bun.lock +1501 -1501
  94. package/vendor/chatgpt2api/source/web/components.json +24 -24
  95. package/vendor/chatgpt2api/source/web/eslint.config.mjs +17 -17
  96. package/vendor/chatgpt2api/source/web/next.config.ts +40 -40
  97. package/vendor/chatgpt2api/source/web/package.json +58 -58
  98. package/vendor/chatgpt2api/source/web/postcss.config.mjs +5 -5
  99. package/vendor/chatgpt2api/source/web/src/app/accounts/components/account-import-dialog.tsx +925 -925
  100. package/vendor/chatgpt2api/source/web/src/app/accounts/page.tsx +1307 -1307
  101. package/vendor/chatgpt2api/source/web/src/app/debug/components/chat-panel.tsx +224 -224
  102. package/vendor/chatgpt2api/source/web/src/app/debug/components/editable-file-panel.tsx +444 -444
  103. package/vendor/chatgpt2api/source/web/src/app/debug/components/ppt-panel.tsx +9 -9
  104. package/vendor/chatgpt2api/source/web/src/app/debug/components/psd-panel.tsx +9 -9
  105. package/vendor/chatgpt2api/source/web/src/app/debug/components/search-panel.tsx +172 -172
  106. package/vendor/chatgpt2api/source/web/src/app/debug/components/skill-panel.tsx +169 -169
  107. package/vendor/chatgpt2api/source/web/src/app/debug/components/types.ts +39 -39
  108. package/vendor/chatgpt2api/source/web/src/app/debug/page.tsx +59 -59
  109. package/vendor/chatgpt2api/source/web/src/app/globals.css +230 -230
  110. package/vendor/chatgpt2api/source/web/src/app/image/components/image-composer.tsx +542 -542
  111. package/vendor/chatgpt2api/source/web/src/app/image/components/image-results.tsx +554 -554
  112. package/vendor/chatgpt2api/source/web/src/app/image/components/image-sidebar.tsx +176 -176
  113. package/vendor/chatgpt2api/source/web/src/app/image/page.tsx +1799 -1799
  114. package/vendor/chatgpt2api/source/web/src/app/image-manager/page.tsx +732 -732
  115. package/vendor/chatgpt2api/source/web/src/app/layout.tsx +50 -50
  116. package/vendor/chatgpt2api/source/web/src/app/login/page.tsx +101 -101
  117. package/vendor/chatgpt2api/source/web/src/app/logs/page.tsx +356 -356
  118. package/vendor/chatgpt2api/source/web/src/app/page.tsx +30 -30
  119. package/vendor/chatgpt2api/source/web/src/app/settings/components/api-docs-card.tsx +341 -341
  120. package/vendor/chatgpt2api/source/web/src/app/settings/components/backup-settings-card.tsx +447 -447
  121. package/vendor/chatgpt2api/source/web/src/app/settings/components/base-url-card.tsx +71 -71
  122. package/vendor/chatgpt2api/source/web/src/app/settings/components/config-card.tsx +491 -491
  123. package/vendor/chatgpt2api/source/web/src/app/settings/components/cpa-pool-dialog.tsx +108 -108
  124. package/vendor/chatgpt2api/source/web/src/app/settings/components/cpa-pools-card.tsx +169 -169
  125. package/vendor/chatgpt2api/source/web/src/app/settings/components/import-browser-dialog.tsx +185 -185
  126. package/vendor/chatgpt2api/source/web/src/app/settings/components/proxy-runtime-card.tsx +379 -379
  127. package/vendor/chatgpt2api/source/web/src/app/settings/components/proxy-settings-card.tsx +127 -127
  128. package/vendor/chatgpt2api/source/web/src/app/settings/components/proxy-settings.tsx +200 -200
  129. package/vendor/chatgpt2api/source/web/src/app/settings/components/settings-header.tsx +12 -12
  130. package/vendor/chatgpt2api/source/web/src/app/settings/components/sub2api-connections.tsx +886 -886
  131. package/vendor/chatgpt2api/source/web/src/app/settings/components/third-party-apps-card.tsx +81 -81
  132. package/vendor/chatgpt2api/source/web/src/app/settings/components/user-keys-card.tsx +422 -422
  133. package/vendor/chatgpt2api/source/web/src/app/settings/page.tsx +135 -135
  134. package/vendor/chatgpt2api/source/web/src/app/settings/store.ts +1069 -1069
  135. package/vendor/chatgpt2api/source/web/src/components/date-range-filter.tsx +49 -49
  136. package/vendor/chatgpt2api/source/web/src/components/header-actions.tsx +24 -24
  137. package/vendor/chatgpt2api/source/web/src/components/image-lightbox.tsx +434 -434
  138. package/vendor/chatgpt2api/source/web/src/components/image-thumbnail.tsx +46 -46
  139. package/vendor/chatgpt2api/source/web/src/components/theme-script.tsx +18 -18
  140. package/vendor/chatgpt2api/source/web/src/components/theme-toggle.tsx +14 -14
  141. package/vendor/chatgpt2api/source/web/src/components/top-nav.tsx +263 -263
  142. package/vendor/chatgpt2api/source/web/src/components/ui/animated-theme-toggler.tsx +256 -256
  143. package/vendor/chatgpt2api/source/web/src/components/ui/badge.tsx +41 -41
  144. package/vendor/chatgpt2api/source/web/src/components/ui/button.tsx +59 -59
  145. package/vendor/chatgpt2api/source/web/src/components/ui/calendar.tsx +46 -46
  146. package/vendor/chatgpt2api/source/web/src/components/ui/card.tsx +61 -61
  147. package/vendor/chatgpt2api/source/web/src/components/ui/checkbox.tsx +27 -27
  148. package/vendor/chatgpt2api/source/web/src/components/ui/dia-text-reveal.tsx +267 -267
  149. package/vendor/chatgpt2api/source/web/src/components/ui/dialog.tsx +132 -132
  150. package/vendor/chatgpt2api/source/web/src/components/ui/field.tsx +13 -13
  151. package/vendor/chatgpt2api/source/web/src/components/ui/input.tsx +19 -19
  152. package/vendor/chatgpt2api/source/web/src/components/ui/label.tsx +24 -24
  153. package/vendor/chatgpt2api/source/web/src/components/ui/morphing-text.tsx +149 -149
  154. package/vendor/chatgpt2api/source/web/src/components/ui/popover.tsx +38 -38
  155. package/vendor/chatgpt2api/source/web/src/components/ui/select.tsx +175 -175
  156. package/vendor/chatgpt2api/source/web/src/components/ui/sheet.tsx +143 -143
  157. package/vendor/chatgpt2api/source/web/src/components/ui/table.tsx +29 -29
  158. package/vendor/chatgpt2api/source/web/src/components/ui/tabs.tsx +91 -91
  159. package/vendor/chatgpt2api/source/web/src/components/ui/text-3d-flip.tsx +269 -269
  160. package/vendor/chatgpt2api/source/web/src/components/ui/textarea.tsx +18 -18
  161. package/vendor/chatgpt2api/source/web/src/components/version-release-dialog.tsx +127 -127
  162. package/vendor/chatgpt2api/source/web/src/constants/common-env.ts +6 -6
  163. package/vendor/chatgpt2api/source/web/src/hooks/use-version-check.ts +85 -85
  164. package/vendor/chatgpt2api/source/web/src/lib/api.ts +926 -926
  165. package/vendor/chatgpt2api/source/web/src/lib/auth-session.ts +26 -26
  166. package/vendor/chatgpt2api/source/web/src/lib/release.ts +25 -25
  167. package/vendor/chatgpt2api/source/web/src/lib/request.ts +93 -93
  168. package/vendor/chatgpt2api/source/web/src/lib/use-auth-guard.ts +89 -89
  169. package/vendor/chatgpt2api/source/web/src/lib/utils.ts +6 -6
  170. package/vendor/chatgpt2api/source/web/src/store/auth.ts +112 -112
  171. package/vendor/chatgpt2api/source/web/src/store/editable-file-history.ts +33 -33
  172. package/vendor/chatgpt2api/source/web/src/store/image-conversations.ts +318 -318
  173. package/vendor/chatgpt2api/source/web/tsconfig.json +42 -42
@@ -1,491 +1,491 @@
1
- "use client";
2
-
3
- import { Cloud, LoaderCircle, PlugZap, RefreshCw, Save } from "lucide-react";
4
- import { useState } from "react";
5
- import { toast } from "sonner";
6
-
7
- import { Button } from "@/components/ui/button";
8
- import { Card, CardContent } from "@/components/ui/card";
9
- import { Checkbox } from "@/components/ui/checkbox";
10
- import { Input } from "@/components/ui/input";
11
- import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
12
- import { Textarea } from "@/components/ui/textarea";
13
- import type { ImageStorageMode } from "@/lib/api";
14
- import { testProxy, type ProxyTestResult } from "@/lib/api";
15
-
16
- import { useSettingsStore } from "../store";
17
-
18
- export function ConfigCard() {
19
- const [isTestingProxy, setIsTestingProxy] = useState(false);
20
- const [proxyTestResult, setProxyTestResult] = useState<ProxyTestResult | null>(null);
21
- const logLevelOptions = ["debug", "info", "warning", "error"];
22
- const config = useSettingsStore((state) => state.config);
23
- const isLoadingConfig = useSettingsStore((state) => state.isLoadingConfig);
24
- const isSavingConfig = useSettingsStore((state) => state.isSavingConfig);
25
- const setRefreshAccountIntervalMinute = useSettingsStore((state) => state.setRefreshAccountIntervalMinute);
26
- const setImageRetentionDays = useSettingsStore((state) => state.setImageRetentionDays);
27
- const setImagePollTimeoutSecs = useSettingsStore((state) => state.setImagePollTimeoutSecs);
28
- const setImageAccountConcurrency = useSettingsStore((state) => state.setImageAccountConcurrency);
29
- const setImageSettleEnabled = useSettingsStore((state) => state.setImageSettleEnabled);
30
- const setImageRemoveConversationAfterResult = useSettingsStore((state) => state.setImageRemoveConversationAfterResult);
31
- const setImageRemoveConversationAlways = useSettingsStore((state) => state.setImageRemoveConversationAlways);
32
- const setImageSettleSecs = useSettingsStore((state) => state.setImageSettleSecs);
33
- const setImageTimeoutRetrySecs = useSettingsStore((state) => state.setImageTimeoutRetrySecs);
34
- const setAutoRemoveInvalidAccounts = useSettingsStore((state) => state.setAutoRemoveInvalidAccounts);
35
- const setAutoRemoveRateLimitedAccounts = useSettingsStore((state) => state.setAutoRemoveRateLimitedAccounts);
36
- const setAutoReloginAfterRefresh = useSettingsStore((state) => state.setAutoReloginAfterRefresh);
37
- const setLogLevel = useSettingsStore((state) => state.setLogLevel);
38
- const setProxy = useSettingsStore((state) => state.setProxy);
39
- const setBaseUrl = useSettingsStore((state) => state.setBaseUrl);
40
- const setGlobalSystemPrompt = useSettingsStore((state) => state.setGlobalSystemPrompt);
41
- const setDefaultUpstreamModelName = useSettingsStore((state) => state.setDefaultUpstreamModelName);
42
- const setDefaultThinkingEffort = useSettingsStore((state) => state.setDefaultThinkingEffort);
43
- const setSensitiveWordsText = useSettingsStore((state) => state.setSensitiveWordsText);
44
- const setAIReviewField = useSettingsStore((state) => state.setAIReviewField);
45
- const setImageStorageField = useSettingsStore((state) => state.setImageStorageField);
46
- const testImageStorage = useSettingsStore((state) => state.testImageStorage);
47
- const syncImagesToWebDAV = useSettingsStore((state) => state.syncImagesToWebDAV);
48
- const isTestingImageStorage = useSettingsStore((state) => state.isTestingImageStorage);
49
- const isSyncingImageStorage = useSettingsStore((state) => state.isSyncingImageStorage);
50
- const saveConfig = useSettingsStore((state) => state.saveConfig);
51
-
52
- const handleTestProxy = async () => {
53
- const candidate = String(config?.proxy || "").trim();
54
- if (!candidate) {
55
- toast.error("请先填写代理地址");
56
- return;
57
- }
58
- setIsTestingProxy(true);
59
- setProxyTestResult(null);
60
- try {
61
- const data = await testProxy(candidate);
62
- setProxyTestResult(data.result);
63
- if (data.result.ok) {
64
- toast.success(`代理可用(${data.result.latency_ms} ms,HTTP ${data.result.status})`);
65
- } else {
66
- toast.error(`代理不可用:${data.result.error ?? "未知错误"}`);
67
- }
68
- } catch (error) {
69
- toast.error(error instanceof Error ? error.message : "测试代理失败");
70
- } finally {
71
- setIsTestingProxy(false);
72
- }
73
- };
74
-
75
- if (isLoadingConfig) {
76
- return (
77
- <Card className="rounded-2xl border-white/80 bg-white/90 shadow-sm">
78
- <CardContent className="flex items-center justify-center p-10">
79
- <LoaderCircle className="size-5 animate-spin text-stone-400" />
80
- </CardContent>
81
- </Card>
82
- );
83
- }
84
-
85
- return (
86
- <Card className="rounded-2xl border-white/80 bg-white/90 shadow-sm">
87
- <CardContent className="space-y-4 p-6">
88
- <div className="rounded-xl border border-stone-200 bg-stone-50 px-4 py-3 text-sm leading-6 text-stone-600">
89
- 管理员登录密钥继续从部署配置读取,不再在此页面展示;如需分发给其他人,请在下方创建普通用户密钥。
90
- </div>
91
- <div className="grid gap-4 md:grid-cols-2">
92
- <div className="space-y-2">
93
- <label className="text-sm text-stone-700">账号刷新间隔</label>
94
- <Input
95
- value={String(config?.refresh_account_interval_minute || "")}
96
- onChange={(event) => setRefreshAccountIntervalMinute(event.target.value)}
97
- placeholder="分钟"
98
- className="h-10 rounded-xl border-stone-200 bg-white"
99
- />
100
- <p className="text-xs text-stone-500">单位分钟,控制账号自动刷新频率。</p>
101
- </div>
102
- <div className="space-y-2">
103
- <label className="text-sm text-stone-700">全局代理</label>
104
- <Input
105
- value={String(config?.proxy || "")}
106
- onChange={(event) => {
107
- setProxy(event.target.value);
108
- setProxyTestResult(null);
109
- }}
110
- placeholder="http://127.0.0.1:7890"
111
- className="h-10 rounded-xl border-stone-200 bg-white"
112
- />
113
- <p className="text-xs leading-5 text-stone-500">
114
- 留空表示不使用代理。支持协议://账号:密码@主机:端口,也可直接粘贴代理商的 主机:端口:账号:密码;示例 http://user:pass@127.0.0.1:7890、127.0.0.1:7890:user:pass。账号密码含 @/: 等特殊字符时需 URL 编码。
115
- </p>
116
- {proxyTestResult ? (
117
- <div
118
- className={`rounded-xl border px-3 py-2 text-xs leading-6 ${
119
- proxyTestResult.ok
120
- ? "border-emerald-200 bg-emerald-50 text-emerald-800"
121
- : "border-rose-200 bg-rose-50 text-rose-800"
122
- }`}
123
- >
124
- {proxyTestResult.ok
125
- ? `代理可用:HTTP ${proxyTestResult.status},用时 ${proxyTestResult.latency_ms} ms`
126
- : `代理不可用:${proxyTestResult.error ?? "未知错误"}(用时 ${proxyTestResult.latency_ms} ms)`}
127
- </div>
128
- ) : null}
129
- <div className="flex justify-end">
130
- <Button
131
- type="button"
132
- variant="outline"
133
- className="h-9 rounded-xl border-stone-200 bg-white px-4 text-stone-700"
134
- onClick={() => void handleTestProxy()}
135
- disabled={isTestingProxy}
136
- >
137
- {isTestingProxy ? <LoaderCircle className="size-4 animate-spin" /> : <PlugZap className="size-4" />}
138
- 测试代理
139
- </Button>
140
- </div>
141
- </div>
142
- <div className="space-y-2">
143
- <label className="text-sm text-stone-700">图片访问地址</label>
144
- <Input
145
- value={String(config?.base_url || "")}
146
- onChange={(event) => setBaseUrl(event.target.value)}
147
- placeholder="https://example.com"
148
- className="h-10 rounded-xl border-stone-200 bg-white"
149
- />
150
- <p className="text-xs text-stone-500">用于生成图片结果的访问前缀地址。</p>
151
- </div>
152
- <div className="space-y-2">
153
- <label className="text-sm text-stone-700">默认请求上游模型名称</label>
154
- <Input
155
- value={String(config?.default_upstream_model_name || "")}
156
- onChange={(event) => setDefaultUpstreamModelName(event.target.value)}
157
- placeholder="gpt-5-5"
158
- className="h-10 rounded-xl border-stone-200 bg-white"
159
- />
160
- <p className="text-xs text-stone-500">gpt-image-2 发起图片请求时使用的上游模型名称,默认 gpt-5-5。</p>
161
- </div>
162
- <div className="space-y-2">
163
- <label className="text-sm text-stone-700">默认思考强度</label>
164
- <Select
165
- value={String(config?.default_thinking_effort || "auto")}
166
- onValueChange={(value) => setDefaultThinkingEffort(value as "auto" | "standard" | "extended" | "max")}
167
- >
168
- <SelectTrigger className="h-10 rounded-xl border-stone-200 bg-white">
169
- <SelectValue />
170
- </SelectTrigger>
171
- <SelectContent>
172
- <SelectItem value="auto">Auto(不传递)</SelectItem>
173
- <SelectItem value="standard">Standard</SelectItem>
174
- <SelectItem value="extended">Extended</SelectItem>
175
- <SelectItem value="max">Max</SelectItem>
176
- </SelectContent>
177
- </Select>
178
- <p className="text-xs text-stone-500">模型名称以 -standard、-extended 或 -max 结尾时,模型后缀优先。</p>
179
- </div>
180
- <div className="space-y-2">
181
- <label className="text-sm text-stone-700">图片自动清理</label>
182
- <Input
183
- value={String(config?.image_retention_days || "")}
184
- onChange={(event) => setImageRetentionDays(event.target.value)}
185
- placeholder="30"
186
- className="h-10 rounded-xl border-stone-200 bg-white"
187
- />
188
- <p className="text-xs text-stone-500">自动删除多少天前的本地图片。</p>
189
- </div>
190
- <div className="space-y-2">
191
- <label className="text-sm text-stone-700">图片轮询超时</label>
192
- <Input
193
- value={String(config?.image_poll_timeout_secs || "")}
194
- onChange={(event) => setImagePollTimeoutSecs(event.target.value)}
195
- placeholder="120"
196
- className="h-10 rounded-xl border-stone-200 bg-white"
197
- />
198
- <p className="text-xs text-stone-500">单位秒,等待上游图片结果的最长时间。</p>
199
- </div>
200
- <div className="space-y-2">
201
- <label className="text-sm text-stone-700">单账号图片并发</label>
202
- <Input
203
- value={String(config?.image_account_concurrency || "")}
204
- onChange={(event) => setImageAccountConcurrency(event.target.value)}
205
- placeholder="1"
206
- className="h-10 rounded-xl border-stone-200 bg-white"
207
- />
208
- <p className="text-xs text-stone-500">限制每个账号同时处理的图片请求数量,默认 3。</p>
209
- </div>
210
- <div className="space-y-2">
211
- <label className="flex items-center gap-3 rounded-xl border border-stone-200 bg-white px-4 py-3 text-sm text-stone-700">
212
- <Checkbox
213
- checked={Boolean(config?.auto_remove_invalid_accounts)}
214
- onCheckedChange={(checked) => setAutoRemoveInvalidAccounts(Boolean(checked))}
215
- />
216
- 自动移除异常账号
217
- </label>
218
- <p className="text-xs text-stone-500">刷新时检测并移除</p>
219
- </div>
220
- <div className="space-y-2">
221
- <div className="flex items-center gap-3 rounded-xl border border-stone-200 bg-white px-4 py-3">
222
- <Checkbox
223
- checked={Boolean(config?.image_settle_enabled !== false)}
224
- onCheckedChange={(checked) => setImageSettleEnabled(Boolean(checked))}
225
- />
226
- <span className="text-sm text-stone-700">图片二次确认机制</span>
227
- </div>
228
- <p className="text-xs text-stone-500">打开后能稍微提升获取图片的成功率。</p>
229
- </div>
230
- <div className="space-y-2">
231
- <div className="flex items-center gap-3 rounded-xl border border-stone-200 bg-white px-4 py-3">
232
- <Checkbox
233
- checked={Boolean(config?.image_remove_conversation_after_result)}
234
- onCheckedChange={(checked) => setImageRemoveConversationAfterResult(Boolean(checked))}
235
- />
236
- <span className="text-sm text-stone-700">出图后移除本地对话</span>
237
- </div>
238
- <p className="text-xs text-stone-500">成功拿到图片后,异步隐藏 ChatGPT 侧对应的本地对话记录。</p>
239
- </div>
240
- <div className="space-y-2">
241
- <div className="flex items-center gap-3 rounded-xl border border-stone-200 bg-white px-4 py-3">
242
- <Checkbox
243
- checked={Boolean(config?.image_remove_conversation_always)}
244
- onCheckedChange={(checked) => setImageRemoveConversationAlways(Boolean(checked))}
245
- />
246
- <span className="text-sm text-stone-700">没出图也移除本地对话</span>
247
- </div>
248
- <p className="text-xs text-stone-500">失败、超时或只返回文本时也一并隐藏对话记录(打开后包含出图成功的情况)。</p>
249
- </div>
250
- <div className="space-y-2">
251
- <label className="text-sm text-stone-700">图片超时继续等待时间</label>
252
- <Input
253
- value={String(config?.image_timeout_retry_secs || "30")}
254
- onChange={(event) => setImageTimeoutRetrySecs(event.target.value)}
255
- placeholder="30"
256
- className="h-10 rounded-xl border-stone-200 bg-white"
257
- />
258
- <p className="text-xs text-stone-500">单位秒,超时后点击"继续等待"额外等待的时间。</p>
259
- </div>
260
- <div className="space-y-2">
261
- <label className="text-sm text-stone-700">图片二次确认等待时间</label>
262
- <Input
263
- value={String(config?.image_settle_secs || "2.0")}
264
- onChange={(event) => setImageSettleSecs(event.target.value)}
265
- placeholder="2.0"
266
- className="h-10 rounded-xl border-stone-200 bg-white disabled:cursor-not-allowed disabled:opacity-50"
267
- disabled={!config?.image_settle_enabled}
268
- />
269
- <p className="text-xs text-stone-500">单位秒,找到图片后等待多久再次确认。需配合图片二次确认机制使用。</p>
270
- </div>
271
- <div className="flex gap-4 md:col-span-2">
272
- <div className="flex-1 space-y-2">
273
- <label className="flex items-center gap-3 rounded-xl border border-stone-200 bg-white px-4 py-3 text-sm text-stone-700">
274
- <Checkbox
275
- checked={Boolean(config?.auto_relogin_after_refresh)}
276
- onCheckedChange={(checked) => setAutoReloginAfterRefresh(Boolean(checked))}
277
- />
278
- 刷新后自动尝试移除异常状态
279
- </label>
280
- <p className="text-xs text-stone-500">开启后刷新时自动尝试密码登录恢复账号。</p>
281
- </div>
282
- <div className="flex-1" aria-hidden="true" />
283
- </div>
284
- <label className="flex items-center gap-3 rounded-xl border border-stone-200 bg-white px-4 py-3 text-sm text-stone-700">
285
- <Checkbox
286
- checked={Boolean(config?.auto_remove_rate_limited_accounts)}
287
- onCheckedChange={(checked) => setAutoRemoveRateLimitedAccounts(Boolean(checked))}
288
- />
289
- 自动移除限流账号
290
- </label>
291
- <div className="space-y-3 rounded-xl border border-stone-200 bg-white px-4 py-3">
292
- <div>
293
- <label className="text-sm text-stone-700">控制台日志级别</label>
294
- <p className="mt-1 text-xs text-stone-500">不选择时使用默认 info / warning / error。</p>
295
- </div>
296
- <div className="grid grid-cols-2 gap-2">
297
- {logLevelOptions.map((level) => (
298
- <label key={level} className="flex items-center gap-2 text-sm capitalize text-stone-700">
299
- <Checkbox
300
- checked={Boolean(config?.log_levels?.includes(level))}
301
- onCheckedChange={(checked) => setLogLevel(level, Boolean(checked))}
302
- />
303
- {level}
304
- </label>
305
- ))}
306
- </div>
307
- </div>
308
- <div className="space-y-2 md:col-span-2">
309
- <label className="text-sm text-stone-700">全局附加指令</label>
310
- <Textarea
311
- value={String(config?.global_system_prompt || "")}
312
- onChange={(event) => setGlobalSystemPrompt(event.target.value)}
313
- placeholder="例如:先判断用户提示词是否合规;遇到违法、色情、暴力、仇恨等请求时拒绝回答。"
314
- className="min-h-28 rounded-xl border-stone-200 bg-white font-mono text-xs shadow-none"
315
- />
316
- <p className="text-xs text-stone-500">每次请求都会作为 system 消息注入,可用于审核用户提示词、避免违规内容、统一约束模型行为或固定角色设定。</p>
317
- </div>
318
- <div className="space-y-2 md:col-span-2">
319
- <label className="text-sm text-stone-700">敏感词</label>
320
- <Textarea
321
- value={(config?.sensitive_words || []).join("\n")}
322
- onChange={(event) => setSensitiveWordsText(event.target.value)}
323
- placeholder="一行一个,命中即拒绝"
324
- className="min-h-28 rounded-xl border-stone-200 bg-white font-mono text-xs shadow-none"
325
- />
326
- <p className="text-xs text-stone-500">只要用户请求包含任意敏感词,就直接返回拒绝。</p>
327
- </div>
328
- <div className="space-y-4 rounded-xl border border-stone-200 bg-white px-4 py-3 md:col-span-2">
329
- <div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
330
- <label className="flex items-center gap-3 text-sm text-stone-700">
331
- <Checkbox
332
- checked={Boolean(config?.image_storage?.enabled)}
333
- onCheckedChange={(checked) => setImageStorageField("enabled", Boolean(checked))}
334
- />
335
- 启用 WebDAV 图片存储
336
- </label>
337
- <div className="flex flex-wrap gap-2">
338
- <Button
339
- type="button"
340
- variant="outline"
341
- className="h-9 rounded-xl border-stone-200 bg-white px-4 text-stone-700"
342
- onClick={() => void testImageStorage()}
343
- disabled={isTestingImageStorage || !config?.image_storage?.enabled}
344
- >
345
- {isTestingImageStorage ? <LoaderCircle className="size-4 animate-spin" /> : <Cloud className="size-4" />}
346
- 测试 WebDAV
347
- </Button>
348
- <Button
349
- type="button"
350
- variant="outline"
351
- className="h-9 rounded-xl border-stone-200 bg-white px-4 text-stone-700"
352
- onClick={() => void syncImagesToWebDAV()}
353
- disabled={isSyncingImageStorage || !config?.image_storage?.enabled || config?.image_storage?.mode === "local"}
354
- >
355
- {isSyncingImageStorage ? <LoaderCircle className="size-4 animate-spin" /> : <RefreshCw className="size-4" />}
356
- 全量同步
357
- </Button>
358
- </div>
359
- </div>
360
- <p className="text-xs leading-6 text-stone-500">
361
- 生成时只处理本次新图片;全量同步用于把已有本地图片补传到 WebDAV。
362
- </p>
363
- <div className="rounded-lg border border-stone-100 bg-stone-50 px-3 py-2 text-xs text-stone-600">
364
- 当前待保存模式:
365
- <span className="ml-1 font-medium text-stone-900">
366
- {config?.image_storage?.enabled
367
- ? config.image_storage.mode === "both"
368
- ? "本机 + WebDAV"
369
- : config.image_storage.mode === "webdav"
370
- ? "仅 WebDAV"
371
- : "仅本机"
372
- : "仅本机"}
373
- </span>
374
- <span className="ml-2 text-stone-400">修改后需要点保存,或通过测试/同步按钮自动保存。</span>
375
- </div>
376
- <div className="grid gap-4 md:grid-cols-3">
377
- <div className="space-y-2">
378
- <label className="text-sm text-stone-700">保存模式</label>
379
- <Select
380
- value={String(config?.image_storage?.mode || "local")}
381
- onValueChange={(value) => setImageStorageField("mode", value as ImageStorageMode)}
382
- disabled={!config?.image_storage?.enabled}
383
- >
384
- <SelectTrigger className="h-10 rounded-xl border-stone-200 bg-white shadow-none">
385
- <SelectValue />
386
- </SelectTrigger>
387
- <SelectContent>
388
- <SelectItem value="local">仅本机</SelectItem>
389
- <SelectItem value="webdav">仅 WebDAV</SelectItem>
390
- <SelectItem value="both">本机 + WebDAV</SelectItem>
391
- </SelectContent>
392
- </Select>
393
- </div>
394
- <div className="space-y-2 md:col-span-2">
395
- <label className="text-sm text-stone-700">WebDAV URL</label>
396
- <Input
397
- value={String(config?.image_storage?.webdav_url || "")}
398
- onChange={(event) => setImageStorageField("webdav_url", event.target.value)}
399
- placeholder="https://example.com/dav"
400
- className="h-10 rounded-xl border-stone-200 bg-white"
401
- disabled={!config?.image_storage?.enabled}
402
- />
403
- </div>
404
- <div className="space-y-2">
405
- <label className="text-sm text-stone-700">用户名</label>
406
- <Input
407
- value={String(config?.image_storage?.webdav_username || "")}
408
- onChange={(event) => setImageStorageField("webdav_username", event.target.value)}
409
- className="h-10 rounded-xl border-stone-200 bg-white"
410
- disabled={!config?.image_storage?.enabled}
411
- />
412
- </div>
413
- <div className="space-y-2">
414
- <label className="text-sm text-stone-700">密码</label>
415
- <Input
416
- type="password"
417
- value={String(config?.image_storage?.webdav_password || "")}
418
- onChange={(event) => setImageStorageField("webdav_password", event.target.value)}
419
- className="h-10 rounded-xl border-stone-200 bg-white"
420
- disabled={!config?.image_storage?.enabled}
421
- />
422
- </div>
423
- <div className="space-y-2">
424
- <label className="text-sm text-stone-700">远端目录</label>
425
- <Input
426
- value={String(config?.image_storage?.webdav_root_path || "")}
427
- onChange={(event) => setImageStorageField("webdav_root_path", event.target.value)}
428
- placeholder="chatgpt2api/images"
429
- className="h-10 rounded-xl border-stone-200 bg-white"
430
- disabled={!config?.image_storage?.enabled}
431
- />
432
- </div>
433
- <div className="space-y-2 md:col-span-3">
434
- <label className="text-sm text-stone-700">公开访问前缀</label>
435
- <Input
436
- value={String(config?.image_storage?.public_base_url || "")}
437
- onChange={(event) => setImageStorageField("public_base_url", event.target.value)}
438
- placeholder="https://cdn.example.com/chatgpt2api/images"
439
- className="h-10 rounded-xl border-stone-200 bg-white"
440
- disabled={!config?.image_storage?.enabled}
441
- />
442
- <p className="text-xs text-stone-500">留空时返回本应用 /images/... 代理地址;填入后直接返回公开图片地址。</p>
443
- </div>
444
- </div>
445
- </div>
446
- <div className="space-y-4 rounded-xl border border-stone-200 bg-white px-4 py-3 md:col-span-2">
447
- <label className="flex items-center gap-3 text-sm text-stone-700">
448
- <Checkbox
449
- checked={Boolean(config?.ai_review?.enabled)}
450
- onCheckedChange={(checked) => setAIReviewField("enabled", Boolean(checked))}
451
- />
452
- 启用 AI 审核
453
- </label>
454
- <p className="text-xs leading-6 text-stone-500">
455
- 开启后会在请求进入生图账号前先调用审核模型,审核不通过会直接拒绝,减少违规提示词触达账号造成风控或封号的风险。
456
- </p>
457
- <div className="grid gap-4 md:grid-cols-3">
458
- <div className="space-y-2">
459
- <label className="text-sm text-stone-700">Base URL</label>
460
- <Input value={String(config?.ai_review?.base_url || "")} onChange={(event) => setAIReviewField("base_url", event.target.value)} placeholder="https://api.openai.com" className="h-10 rounded-xl border-stone-200 bg-white" />
461
- </div>
462
- <div className="space-y-2">
463
- <label className="text-sm text-stone-700">API Key</label>
464
- <Input value={String(config?.ai_review?.api_key || "")} onChange={(event) => setAIReviewField("api_key", event.target.value)} placeholder="sk-..." className="h-10 rounded-xl border-stone-200 bg-white" />
465
- </div>
466
- <div className="space-y-2">
467
- <label className="text-sm text-stone-700">Model</label>
468
- <Input value={String(config?.ai_review?.model || "")} onChange={(event) => setAIReviewField("model", event.target.value)} placeholder="gpt-5.4-mini" className="h-10 rounded-xl border-stone-200 bg-white" />
469
- </div>
470
- </div>
471
- <div className="space-y-2">
472
- <label className="text-sm text-stone-700">审核提示词</label>
473
- <Textarea value={String(config?.ai_review?.prompt || "")} onChange={(event) => setAIReviewField("prompt", event.target.value)} placeholder="判断用户请求是否允许。只回答 ALLOW 或 REJECT。" className="min-h-24 rounded-xl border-stone-200 bg-white text-xs shadow-none" />
474
- </div>
475
- </div>
476
- </div>
477
-
478
- <div className="flex justify-end">
479
- <Button
480
- className="h-10 rounded-xl bg-stone-950 px-5 text-white hover:bg-stone-800"
481
- onClick={() => void saveConfig()}
482
- disabled={isSavingConfig}
483
- >
484
- {isSavingConfig ? <LoaderCircle className="size-4 animate-spin" /> : <Save className="size-4" />}
485
- 保存
486
- </Button>
487
- </div>
488
- </CardContent>
489
- </Card>
490
- );
491
- }
1
+ "use client";
2
+
3
+ import { Cloud, LoaderCircle, PlugZap, RefreshCw, Save } from "lucide-react";
4
+ import { useState } from "react";
5
+ import { toast } from "sonner";
6
+
7
+ import { Button } from "@/components/ui/button";
8
+ import { Card, CardContent } from "@/components/ui/card";
9
+ import { Checkbox } from "@/components/ui/checkbox";
10
+ import { Input } from "@/components/ui/input";
11
+ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
12
+ import { Textarea } from "@/components/ui/textarea";
13
+ import type { ImageStorageMode } from "@/lib/api";
14
+ import { testProxy, type ProxyTestResult } from "@/lib/api";
15
+
16
+ import { useSettingsStore } from "../store";
17
+
18
+ export function ConfigCard() {
19
+ const [isTestingProxy, setIsTestingProxy] = useState(false);
20
+ const [proxyTestResult, setProxyTestResult] = useState<ProxyTestResult | null>(null);
21
+ const logLevelOptions = ["debug", "info", "warning", "error"];
22
+ const config = useSettingsStore((state) => state.config);
23
+ const isLoadingConfig = useSettingsStore((state) => state.isLoadingConfig);
24
+ const isSavingConfig = useSettingsStore((state) => state.isSavingConfig);
25
+ const setRefreshAccountIntervalMinute = useSettingsStore((state) => state.setRefreshAccountIntervalMinute);
26
+ const setImageRetentionDays = useSettingsStore((state) => state.setImageRetentionDays);
27
+ const setImagePollTimeoutSecs = useSettingsStore((state) => state.setImagePollTimeoutSecs);
28
+ const setImageAccountConcurrency = useSettingsStore((state) => state.setImageAccountConcurrency);
29
+ const setImageSettleEnabled = useSettingsStore((state) => state.setImageSettleEnabled);
30
+ const setImageRemoveConversationAfterResult = useSettingsStore((state) => state.setImageRemoveConversationAfterResult);
31
+ const setImageRemoveConversationAlways = useSettingsStore((state) => state.setImageRemoveConversationAlways);
32
+ const setImageSettleSecs = useSettingsStore((state) => state.setImageSettleSecs);
33
+ const setImageTimeoutRetrySecs = useSettingsStore((state) => state.setImageTimeoutRetrySecs);
34
+ const setAutoRemoveInvalidAccounts = useSettingsStore((state) => state.setAutoRemoveInvalidAccounts);
35
+ const setAutoRemoveRateLimitedAccounts = useSettingsStore((state) => state.setAutoRemoveRateLimitedAccounts);
36
+ const setAutoReloginAfterRefresh = useSettingsStore((state) => state.setAutoReloginAfterRefresh);
37
+ const setLogLevel = useSettingsStore((state) => state.setLogLevel);
38
+ const setProxy = useSettingsStore((state) => state.setProxy);
39
+ const setBaseUrl = useSettingsStore((state) => state.setBaseUrl);
40
+ const setGlobalSystemPrompt = useSettingsStore((state) => state.setGlobalSystemPrompt);
41
+ const setDefaultUpstreamModelName = useSettingsStore((state) => state.setDefaultUpstreamModelName);
42
+ const setDefaultThinkingEffort = useSettingsStore((state) => state.setDefaultThinkingEffort);
43
+ const setSensitiveWordsText = useSettingsStore((state) => state.setSensitiveWordsText);
44
+ const setAIReviewField = useSettingsStore((state) => state.setAIReviewField);
45
+ const setImageStorageField = useSettingsStore((state) => state.setImageStorageField);
46
+ const testImageStorage = useSettingsStore((state) => state.testImageStorage);
47
+ const syncImagesToWebDAV = useSettingsStore((state) => state.syncImagesToWebDAV);
48
+ const isTestingImageStorage = useSettingsStore((state) => state.isTestingImageStorage);
49
+ const isSyncingImageStorage = useSettingsStore((state) => state.isSyncingImageStorage);
50
+ const saveConfig = useSettingsStore((state) => state.saveConfig);
51
+
52
+ const handleTestProxy = async () => {
53
+ const candidate = String(config?.proxy || "").trim();
54
+ if (!candidate) {
55
+ toast.error("请先填写代理地址");
56
+ return;
57
+ }
58
+ setIsTestingProxy(true);
59
+ setProxyTestResult(null);
60
+ try {
61
+ const data = await testProxy(candidate);
62
+ setProxyTestResult(data.result);
63
+ if (data.result.ok) {
64
+ toast.success(`代理可用(${data.result.latency_ms} ms,HTTP ${data.result.status})`);
65
+ } else {
66
+ toast.error(`代理不可用:${data.result.error ?? "未知错误"}`);
67
+ }
68
+ } catch (error) {
69
+ toast.error(error instanceof Error ? error.message : "测试代理失败");
70
+ } finally {
71
+ setIsTestingProxy(false);
72
+ }
73
+ };
74
+
75
+ if (isLoadingConfig) {
76
+ return (
77
+ <Card className="rounded-2xl border-white/80 bg-white/90 shadow-sm">
78
+ <CardContent className="flex items-center justify-center p-10">
79
+ <LoaderCircle className="size-5 animate-spin text-stone-400" />
80
+ </CardContent>
81
+ </Card>
82
+ );
83
+ }
84
+
85
+ return (
86
+ <Card className="rounded-2xl border-white/80 bg-white/90 shadow-sm">
87
+ <CardContent className="space-y-4 p-6">
88
+ <div className="rounded-xl border border-stone-200 bg-stone-50 px-4 py-3 text-sm leading-6 text-stone-600">
89
+ 管理员登录密钥继续从部署配置读取,不再在此页面展示;如需分发给其他人,请在下方创建普通用户密钥。
90
+ </div>
91
+ <div className="grid gap-4 md:grid-cols-2">
92
+ <div className="space-y-2">
93
+ <label className="text-sm text-stone-700">账号刷新间隔</label>
94
+ <Input
95
+ value={String(config?.refresh_account_interval_minute || "")}
96
+ onChange={(event) => setRefreshAccountIntervalMinute(event.target.value)}
97
+ placeholder="分钟"
98
+ className="h-10 rounded-xl border-stone-200 bg-white"
99
+ />
100
+ <p className="text-xs text-stone-500">单位分钟,控制账号自动刷新频率。</p>
101
+ </div>
102
+ <div className="space-y-2">
103
+ <label className="text-sm text-stone-700">全局代理</label>
104
+ <Input
105
+ value={String(config?.proxy || "")}
106
+ onChange={(event) => {
107
+ setProxy(event.target.value);
108
+ setProxyTestResult(null);
109
+ }}
110
+ placeholder="http://127.0.0.1:7890"
111
+ className="h-10 rounded-xl border-stone-200 bg-white"
112
+ />
113
+ <p className="text-xs leading-5 text-stone-500">
114
+ 留空表示不使用代理。支持协议://账号:密码@主机:端口,也可直接粘贴代理商的 主机:端口:账号:密码;示例 http://user:pass@127.0.0.1:7890、127.0.0.1:7890:user:pass。账号密码含 @/: 等特殊字符时需 URL 编码。
115
+ </p>
116
+ {proxyTestResult ? (
117
+ <div
118
+ className={`rounded-xl border px-3 py-2 text-xs leading-6 ${
119
+ proxyTestResult.ok
120
+ ? "border-emerald-200 bg-emerald-50 text-emerald-800"
121
+ : "border-rose-200 bg-rose-50 text-rose-800"
122
+ }`}
123
+ >
124
+ {proxyTestResult.ok
125
+ ? `代理可用:HTTP ${proxyTestResult.status},用时 ${proxyTestResult.latency_ms} ms`
126
+ : `代理不可用:${proxyTestResult.error ?? "未知错误"}(用时 ${proxyTestResult.latency_ms} ms)`}
127
+ </div>
128
+ ) : null}
129
+ <div className="flex justify-end">
130
+ <Button
131
+ type="button"
132
+ variant="outline"
133
+ className="h-9 rounded-xl border-stone-200 bg-white px-4 text-stone-700"
134
+ onClick={() => void handleTestProxy()}
135
+ disabled={isTestingProxy}
136
+ >
137
+ {isTestingProxy ? <LoaderCircle className="size-4 animate-spin" /> : <PlugZap className="size-4" />}
138
+ 测试代理
139
+ </Button>
140
+ </div>
141
+ </div>
142
+ <div className="space-y-2">
143
+ <label className="text-sm text-stone-700">图片访问地址</label>
144
+ <Input
145
+ value={String(config?.base_url || "")}
146
+ onChange={(event) => setBaseUrl(event.target.value)}
147
+ placeholder="https://example.com"
148
+ className="h-10 rounded-xl border-stone-200 bg-white"
149
+ />
150
+ <p className="text-xs text-stone-500">用于生成图片结果的访问前缀地址。</p>
151
+ </div>
152
+ <div className="space-y-2">
153
+ <label className="text-sm text-stone-700">默认请求上游模型名称</label>
154
+ <Input
155
+ value={String(config?.default_upstream_model_name || "")}
156
+ onChange={(event) => setDefaultUpstreamModelName(event.target.value)}
157
+ placeholder="gpt-5-5"
158
+ className="h-10 rounded-xl border-stone-200 bg-white"
159
+ />
160
+ <p className="text-xs text-stone-500">gpt-image-2 发起图片请求时使用的上游模型名称,默认 gpt-5-5。</p>
161
+ </div>
162
+ <div className="space-y-2">
163
+ <label className="text-sm text-stone-700">默认思考强度</label>
164
+ <Select
165
+ value={String(config?.default_thinking_effort || "auto")}
166
+ onValueChange={(value) => setDefaultThinkingEffort(value as "auto" | "standard" | "extended" | "max")}
167
+ >
168
+ <SelectTrigger className="h-10 rounded-xl border-stone-200 bg-white">
169
+ <SelectValue />
170
+ </SelectTrigger>
171
+ <SelectContent>
172
+ <SelectItem value="auto">Auto(不传递)</SelectItem>
173
+ <SelectItem value="standard">Standard</SelectItem>
174
+ <SelectItem value="extended">Extended</SelectItem>
175
+ <SelectItem value="max">Max</SelectItem>
176
+ </SelectContent>
177
+ </Select>
178
+ <p className="text-xs text-stone-500">模型名称以 -standard、-extended 或 -max 结尾时,模型后缀优先。</p>
179
+ </div>
180
+ <div className="space-y-2">
181
+ <label className="text-sm text-stone-700">图片自动清理</label>
182
+ <Input
183
+ value={String(config?.image_retention_days || "")}
184
+ onChange={(event) => setImageRetentionDays(event.target.value)}
185
+ placeholder="30"
186
+ className="h-10 rounded-xl border-stone-200 bg-white"
187
+ />
188
+ <p className="text-xs text-stone-500">自动删除多少天前的本地图片。</p>
189
+ </div>
190
+ <div className="space-y-2">
191
+ <label className="text-sm text-stone-700">图片轮询超时</label>
192
+ <Input
193
+ value={String(config?.image_poll_timeout_secs || "")}
194
+ onChange={(event) => setImagePollTimeoutSecs(event.target.value)}
195
+ placeholder="120"
196
+ className="h-10 rounded-xl border-stone-200 bg-white"
197
+ />
198
+ <p className="text-xs text-stone-500">单位秒,等待上游图片结果的最长时间。</p>
199
+ </div>
200
+ <div className="space-y-2">
201
+ <label className="text-sm text-stone-700">单账号图片并发</label>
202
+ <Input
203
+ value={String(config?.image_account_concurrency || "")}
204
+ onChange={(event) => setImageAccountConcurrency(event.target.value)}
205
+ placeholder="1"
206
+ className="h-10 rounded-xl border-stone-200 bg-white"
207
+ />
208
+ <p className="text-xs text-stone-500">限制每个账号同时处理的图片请求数量,默认 3。</p>
209
+ </div>
210
+ <div className="space-y-2">
211
+ <label className="flex items-center gap-3 rounded-xl border border-stone-200 bg-white px-4 py-3 text-sm text-stone-700">
212
+ <Checkbox
213
+ checked={Boolean(config?.auto_remove_invalid_accounts)}
214
+ onCheckedChange={(checked) => setAutoRemoveInvalidAccounts(Boolean(checked))}
215
+ />
216
+ 自动移除异常账号
217
+ </label>
218
+ <p className="text-xs text-stone-500">刷新时检测并移除</p>
219
+ </div>
220
+ <div className="space-y-2">
221
+ <div className="flex items-center gap-3 rounded-xl border border-stone-200 bg-white px-4 py-3">
222
+ <Checkbox
223
+ checked={Boolean(config?.image_settle_enabled !== false)}
224
+ onCheckedChange={(checked) => setImageSettleEnabled(Boolean(checked))}
225
+ />
226
+ <span className="text-sm text-stone-700">图片二次确认机制</span>
227
+ </div>
228
+ <p className="text-xs text-stone-500">打开后能稍微提升获取图片的成功率。</p>
229
+ </div>
230
+ <div className="space-y-2">
231
+ <div className="flex items-center gap-3 rounded-xl border border-stone-200 bg-white px-4 py-3">
232
+ <Checkbox
233
+ checked={Boolean(config?.image_remove_conversation_after_result)}
234
+ onCheckedChange={(checked) => setImageRemoveConversationAfterResult(Boolean(checked))}
235
+ />
236
+ <span className="text-sm text-stone-700">出图后移除本地对话</span>
237
+ </div>
238
+ <p className="text-xs text-stone-500">成功拿到图片后,异步隐藏 ChatGPT 侧对应的本地对话记录。</p>
239
+ </div>
240
+ <div className="space-y-2">
241
+ <div className="flex items-center gap-3 rounded-xl border border-stone-200 bg-white px-4 py-3">
242
+ <Checkbox
243
+ checked={Boolean(config?.image_remove_conversation_always)}
244
+ onCheckedChange={(checked) => setImageRemoveConversationAlways(Boolean(checked))}
245
+ />
246
+ <span className="text-sm text-stone-700">没出图也移除本地对话</span>
247
+ </div>
248
+ <p className="text-xs text-stone-500">失败、超时或只返回文本时也一并隐藏对话记录(打开后包含出图成功的情况)。</p>
249
+ </div>
250
+ <div className="space-y-2">
251
+ <label className="text-sm text-stone-700">图片超时继续等待时间</label>
252
+ <Input
253
+ value={String(config?.image_timeout_retry_secs || "30")}
254
+ onChange={(event) => setImageTimeoutRetrySecs(event.target.value)}
255
+ placeholder="30"
256
+ className="h-10 rounded-xl border-stone-200 bg-white"
257
+ />
258
+ <p className="text-xs text-stone-500">单位秒,超时后点击"继续等待"额外等待的时间。</p>
259
+ </div>
260
+ <div className="space-y-2">
261
+ <label className="text-sm text-stone-700">图片二次确认等待时间</label>
262
+ <Input
263
+ value={String(config?.image_settle_secs || "2.0")}
264
+ onChange={(event) => setImageSettleSecs(event.target.value)}
265
+ placeholder="2.0"
266
+ className="h-10 rounded-xl border-stone-200 bg-white disabled:cursor-not-allowed disabled:opacity-50"
267
+ disabled={!config?.image_settle_enabled}
268
+ />
269
+ <p className="text-xs text-stone-500">单位秒,找到图片后等待多久再次确认。需配合图片二次确认机制使用。</p>
270
+ </div>
271
+ <div className="flex gap-4 md:col-span-2">
272
+ <div className="flex-1 space-y-2">
273
+ <label className="flex items-center gap-3 rounded-xl border border-stone-200 bg-white px-4 py-3 text-sm text-stone-700">
274
+ <Checkbox
275
+ checked={Boolean(config?.auto_relogin_after_refresh)}
276
+ onCheckedChange={(checked) => setAutoReloginAfterRefresh(Boolean(checked))}
277
+ />
278
+ 刷新后自动尝试移除异常状态
279
+ </label>
280
+ <p className="text-xs text-stone-500">开启后刷新时自动尝试密码登录恢复账号。</p>
281
+ </div>
282
+ <div className="flex-1" aria-hidden="true" />
283
+ </div>
284
+ <label className="flex items-center gap-3 rounded-xl border border-stone-200 bg-white px-4 py-3 text-sm text-stone-700">
285
+ <Checkbox
286
+ checked={Boolean(config?.auto_remove_rate_limited_accounts)}
287
+ onCheckedChange={(checked) => setAutoRemoveRateLimitedAccounts(Boolean(checked))}
288
+ />
289
+ 自动移除限流账号
290
+ </label>
291
+ <div className="space-y-3 rounded-xl border border-stone-200 bg-white px-4 py-3">
292
+ <div>
293
+ <label className="text-sm text-stone-700">控制台日志级别</label>
294
+ <p className="mt-1 text-xs text-stone-500">不选择时使用默认 info / warning / error。</p>
295
+ </div>
296
+ <div className="grid grid-cols-2 gap-2">
297
+ {logLevelOptions.map((level) => (
298
+ <label key={level} className="flex items-center gap-2 text-sm capitalize text-stone-700">
299
+ <Checkbox
300
+ checked={Boolean(config?.log_levels?.includes(level))}
301
+ onCheckedChange={(checked) => setLogLevel(level, Boolean(checked))}
302
+ />
303
+ {level}
304
+ </label>
305
+ ))}
306
+ </div>
307
+ </div>
308
+ <div className="space-y-2 md:col-span-2">
309
+ <label className="text-sm text-stone-700">全局附加指令</label>
310
+ <Textarea
311
+ value={String(config?.global_system_prompt || "")}
312
+ onChange={(event) => setGlobalSystemPrompt(event.target.value)}
313
+ placeholder="例如:先判断用户提示词是否合规;遇到违法、色情、暴力、仇恨等请求时拒绝回答。"
314
+ className="min-h-28 rounded-xl border-stone-200 bg-white font-mono text-xs shadow-none"
315
+ />
316
+ <p className="text-xs text-stone-500">每次请求都会作为 system 消息注入,可用于审核用户提示词、避免违规内容、统一约束模型行为或固定角色设定。</p>
317
+ </div>
318
+ <div className="space-y-2 md:col-span-2">
319
+ <label className="text-sm text-stone-700">敏感词</label>
320
+ <Textarea
321
+ value={(config?.sensitive_words || []).join("\n")}
322
+ onChange={(event) => setSensitiveWordsText(event.target.value)}
323
+ placeholder="一行一个,命中即拒绝"
324
+ className="min-h-28 rounded-xl border-stone-200 bg-white font-mono text-xs shadow-none"
325
+ />
326
+ <p className="text-xs text-stone-500">只要用户请求包含任意敏感词,就直接返回拒绝。</p>
327
+ </div>
328
+ <div className="space-y-4 rounded-xl border border-stone-200 bg-white px-4 py-3 md:col-span-2">
329
+ <div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
330
+ <label className="flex items-center gap-3 text-sm text-stone-700">
331
+ <Checkbox
332
+ checked={Boolean(config?.image_storage?.enabled)}
333
+ onCheckedChange={(checked) => setImageStorageField("enabled", Boolean(checked))}
334
+ />
335
+ 启用 WebDAV 图片存储
336
+ </label>
337
+ <div className="flex flex-wrap gap-2">
338
+ <Button
339
+ type="button"
340
+ variant="outline"
341
+ className="h-9 rounded-xl border-stone-200 bg-white px-4 text-stone-700"
342
+ onClick={() => void testImageStorage()}
343
+ disabled={isTestingImageStorage || !config?.image_storage?.enabled}
344
+ >
345
+ {isTestingImageStorage ? <LoaderCircle className="size-4 animate-spin" /> : <Cloud className="size-4" />}
346
+ 测试 WebDAV
347
+ </Button>
348
+ <Button
349
+ type="button"
350
+ variant="outline"
351
+ className="h-9 rounded-xl border-stone-200 bg-white px-4 text-stone-700"
352
+ onClick={() => void syncImagesToWebDAV()}
353
+ disabled={isSyncingImageStorage || !config?.image_storage?.enabled || config?.image_storage?.mode === "local"}
354
+ >
355
+ {isSyncingImageStorage ? <LoaderCircle className="size-4 animate-spin" /> : <RefreshCw className="size-4" />}
356
+ 全量同步
357
+ </Button>
358
+ </div>
359
+ </div>
360
+ <p className="text-xs leading-6 text-stone-500">
361
+ 生成时只处理本次新图片;全量同步用于把已有本地图片补传到 WebDAV。
362
+ </p>
363
+ <div className="rounded-lg border border-stone-100 bg-stone-50 px-3 py-2 text-xs text-stone-600">
364
+ 当前待保存模式:
365
+ <span className="ml-1 font-medium text-stone-900">
366
+ {config?.image_storage?.enabled
367
+ ? config.image_storage.mode === "both"
368
+ ? "本机 + WebDAV"
369
+ : config.image_storage.mode === "webdav"
370
+ ? "仅 WebDAV"
371
+ : "仅本机"
372
+ : "仅本机"}
373
+ </span>
374
+ <span className="ml-2 text-stone-400">修改后需要点保存,或通过测试/同步按钮自动保存。</span>
375
+ </div>
376
+ <div className="grid gap-4 md:grid-cols-3">
377
+ <div className="space-y-2">
378
+ <label className="text-sm text-stone-700">保存模式</label>
379
+ <Select
380
+ value={String(config?.image_storage?.mode || "local")}
381
+ onValueChange={(value) => setImageStorageField("mode", value as ImageStorageMode)}
382
+ disabled={!config?.image_storage?.enabled}
383
+ >
384
+ <SelectTrigger className="h-10 rounded-xl border-stone-200 bg-white shadow-none">
385
+ <SelectValue />
386
+ </SelectTrigger>
387
+ <SelectContent>
388
+ <SelectItem value="local">仅本机</SelectItem>
389
+ <SelectItem value="webdav">仅 WebDAV</SelectItem>
390
+ <SelectItem value="both">本机 + WebDAV</SelectItem>
391
+ </SelectContent>
392
+ </Select>
393
+ </div>
394
+ <div className="space-y-2 md:col-span-2">
395
+ <label className="text-sm text-stone-700">WebDAV URL</label>
396
+ <Input
397
+ value={String(config?.image_storage?.webdav_url || "")}
398
+ onChange={(event) => setImageStorageField("webdav_url", event.target.value)}
399
+ placeholder="https://example.com/dav"
400
+ className="h-10 rounded-xl border-stone-200 bg-white"
401
+ disabled={!config?.image_storage?.enabled}
402
+ />
403
+ </div>
404
+ <div className="space-y-2">
405
+ <label className="text-sm text-stone-700">用户名</label>
406
+ <Input
407
+ value={String(config?.image_storage?.webdav_username || "")}
408
+ onChange={(event) => setImageStorageField("webdav_username", event.target.value)}
409
+ className="h-10 rounded-xl border-stone-200 bg-white"
410
+ disabled={!config?.image_storage?.enabled}
411
+ />
412
+ </div>
413
+ <div className="space-y-2">
414
+ <label className="text-sm text-stone-700">密码</label>
415
+ <Input
416
+ type="password"
417
+ value={String(config?.image_storage?.webdav_password || "")}
418
+ onChange={(event) => setImageStorageField("webdav_password", event.target.value)}
419
+ className="h-10 rounded-xl border-stone-200 bg-white"
420
+ disabled={!config?.image_storage?.enabled}
421
+ />
422
+ </div>
423
+ <div className="space-y-2">
424
+ <label className="text-sm text-stone-700">远端目录</label>
425
+ <Input
426
+ value={String(config?.image_storage?.webdav_root_path || "")}
427
+ onChange={(event) => setImageStorageField("webdav_root_path", event.target.value)}
428
+ placeholder="chatgpt2api/images"
429
+ className="h-10 rounded-xl border-stone-200 bg-white"
430
+ disabled={!config?.image_storage?.enabled}
431
+ />
432
+ </div>
433
+ <div className="space-y-2 md:col-span-3">
434
+ <label className="text-sm text-stone-700">公开访问前缀</label>
435
+ <Input
436
+ value={String(config?.image_storage?.public_base_url || "")}
437
+ onChange={(event) => setImageStorageField("public_base_url", event.target.value)}
438
+ placeholder="https://cdn.example.com/chatgpt2api/images"
439
+ className="h-10 rounded-xl border-stone-200 bg-white"
440
+ disabled={!config?.image_storage?.enabled}
441
+ />
442
+ <p className="text-xs text-stone-500">留空时返回本应用 /images/... 代理地址;填入后直接返回公开图片地址。</p>
443
+ </div>
444
+ </div>
445
+ </div>
446
+ <div className="space-y-4 rounded-xl border border-stone-200 bg-white px-4 py-3 md:col-span-2">
447
+ <label className="flex items-center gap-3 text-sm text-stone-700">
448
+ <Checkbox
449
+ checked={Boolean(config?.ai_review?.enabled)}
450
+ onCheckedChange={(checked) => setAIReviewField("enabled", Boolean(checked))}
451
+ />
452
+ 启用 AI 审核
453
+ </label>
454
+ <p className="text-xs leading-6 text-stone-500">
455
+ 开启后会在请求进入生图账号前先调用审核模型,审核不通过会直接拒绝,减少违规提示词触达账号造成风控或封号的风险。
456
+ </p>
457
+ <div className="grid gap-4 md:grid-cols-3">
458
+ <div className="space-y-2">
459
+ <label className="text-sm text-stone-700">Base URL</label>
460
+ <Input value={String(config?.ai_review?.base_url || "")} onChange={(event) => setAIReviewField("base_url", event.target.value)} placeholder="https://api.openai.com" className="h-10 rounded-xl border-stone-200 bg-white" />
461
+ </div>
462
+ <div className="space-y-2">
463
+ <label className="text-sm text-stone-700">API Key</label>
464
+ <Input value={String(config?.ai_review?.api_key || "")} onChange={(event) => setAIReviewField("api_key", event.target.value)} placeholder="sk-..." className="h-10 rounded-xl border-stone-200 bg-white" />
465
+ </div>
466
+ <div className="space-y-2">
467
+ <label className="text-sm text-stone-700">Model</label>
468
+ <Input value={String(config?.ai_review?.model || "")} onChange={(event) => setAIReviewField("model", event.target.value)} placeholder="gpt-5.4-mini" className="h-10 rounded-xl border-stone-200 bg-white" />
469
+ </div>
470
+ </div>
471
+ <div className="space-y-2">
472
+ <label className="text-sm text-stone-700">审核提示词</label>
473
+ <Textarea value={String(config?.ai_review?.prompt || "")} onChange={(event) => setAIReviewField("prompt", event.target.value)} placeholder="判断用户请求是否允许。只回答 ALLOW 或 REJECT。" className="min-h-24 rounded-xl border-stone-200 bg-white text-xs shadow-none" />
474
+ </div>
475
+ </div>
476
+ </div>
477
+
478
+ <div className="flex justify-end">
479
+ <Button
480
+ className="h-10 rounded-xl bg-stone-950 px-5 text-white hover:bg-stone-800"
481
+ onClick={() => void saveConfig()}
482
+ disabled={isSavingConfig}
483
+ >
484
+ {isSavingConfig ? <LoaderCircle className="size-4 animate-spin" /> : <Save className="size-4" />}
485
+ 保存
486
+ </Button>
487
+ </div>
488
+ </CardContent>
489
+ </Card>
490
+ );
491
+ }