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,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
+ }