dsh-plugin-image-workbench 1.1.5 → 1.1.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -21
- package/README.en.md +5 -3
- package/README.md +7 -3
- package/client.js +241 -241
- package/cordis.patch.yml +14 -14
- package/host.js +51 -0
- package/install.mjs +333 -387
- package/package.json +3 -2
- package/scripts/fetch-style-images.mjs +104 -0
- package/scripts/vendor-snapshot.py +154 -154
- package/server/access.py +26 -26
- package/server/app.py +1371 -1329
- package/server/catalog.py +147 -147
- package/server/composer.py +391 -391
- package/server/drafts.py +158 -158
- package/server/history.py +575 -575
- package/server/imaging.py +172 -172
- package/server/pool.py +1335 -1335
- package/server/pool_admin.py +921 -921
- package/server/pool_service.py +1073 -1073
- package/server/requirements.txt +6 -6
- package/server/smoke_check.py +402 -402
- package/server/static/access.html +202 -202
- package/server/static/index.html +15 -0
- package/vendor/chatgpt2api/source/.env.example +82 -82
- package/vendor/chatgpt2api/source/.python-version +1 -1
- package/vendor/chatgpt2api/source/LICENSE +21 -21
- package/vendor/chatgpt2api/source/README.md +391 -391
- package/vendor/chatgpt2api/source/VERSION +1 -1
- package/vendor/chatgpt2api/source/api/__init__.py +2 -2
- package/vendor/chatgpt2api/source/api/accounts.py +537 -537
- package/vendor/chatgpt2api/source/api/ai.py +217 -217
- package/vendor/chatgpt2api/source/api/app.py +62 -62
- package/vendor/chatgpt2api/source/api/errors.py +46 -46
- package/vendor/chatgpt2api/source/api/image_inputs.py +309 -309
- package/vendor/chatgpt2api/source/api/image_tasks.py +120 -120
- package/vendor/chatgpt2api/source/api/support.py +134 -134
- package/vendor/chatgpt2api/source/api/system.py +365 -365
- package/vendor/chatgpt2api/source/assets/kunrouai.svg +50 -50
- package/vendor/chatgpt2api/source/config.default.json +108 -108
- package/vendor/chatgpt2api/source/main.py +9 -9
- package/vendor/chatgpt2api/source/pyproject.toml +27 -27
- package/vendor/chatgpt2api/source/scripts/init_proxy_config.py +186 -186
- package/vendor/chatgpt2api/source/scripts/migrate_storage.py +163 -163
- package/vendor/chatgpt2api/source/scripts/privoxy-warp.conf +13 -13
- package/vendor/chatgpt2api/source/scripts/test_outlook_token_mailbox.py +385 -385
- package/vendor/chatgpt2api/source/scripts/test_storage.py +129 -129
- package/vendor/chatgpt2api/source/scripts/verify_oauth_refresh.py +92 -92
- package/vendor/chatgpt2api/source/services/account_service.py +1757 -1757
- package/vendor/chatgpt2api/source/services/auth_service.py +253 -253
- package/vendor/chatgpt2api/source/services/backup_service.py +671 -671
- package/vendor/chatgpt2api/source/services/config.py +667 -667
- package/vendor/chatgpt2api/source/services/content_filter.py +242 -242
- package/vendor/chatgpt2api/source/services/cpa_service.py +315 -315
- package/vendor/chatgpt2api/source/services/editable_file_task_service.py +260 -260
- package/vendor/chatgpt2api/source/services/image_service.py +376 -376
- package/vendor/chatgpt2api/source/services/image_storage_service.py +405 -405
- package/vendor/chatgpt2api/source/services/image_tags_service.py +76 -76
- package/vendor/chatgpt2api/source/services/image_task_service.py +550 -550
- package/vendor/chatgpt2api/source/services/log_service.py +337 -337
- package/vendor/chatgpt2api/source/services/model_service.py +182 -182
- package/vendor/chatgpt2api/source/services/oauth_login_service.py +267 -267
- package/vendor/chatgpt2api/source/services/openai_backend_api.py +2763 -2763
- package/vendor/chatgpt2api/source/services/openai_oauth.py +32 -32
- package/vendor/chatgpt2api/source/services/protocol/__init__.py +2 -2
- package/vendor/chatgpt2api/source/services/protocol/anthropic_v1_messages.py +307 -307
- package/vendor/chatgpt2api/source/services/protocol/chat_completion_cache.py +238 -238
- package/vendor/chatgpt2api/source/services/protocol/conversation.py +1644 -1644
- package/vendor/chatgpt2api/source/services/protocol/error_response.py +124 -124
- package/vendor/chatgpt2api/source/services/protocol/openai_search.py +19 -19
- package/vendor/chatgpt2api/source/services/protocol/openai_v1_chat_complete.py +316 -316
- package/vendor/chatgpt2api/source/services/protocol/openai_v1_image_edit.py +86 -86
- package/vendor/chatgpt2api/source/services/protocol/openai_v1_image_generations.py +42 -42
- package/vendor/chatgpt2api/source/services/protocol/openai_v1_models.py +53 -53
- package/vendor/chatgpt2api/source/services/protocol/openai_v1_response.py +453 -453
- package/vendor/chatgpt2api/source/services/protocol/web_search_tool.py +160 -160
- package/vendor/chatgpt2api/source/services/proxy_service.py +648 -648
- package/vendor/chatgpt2api/source/services/storage/__init__.py +5 -5
- package/vendor/chatgpt2api/source/services/storage/base.py +38 -38
- package/vendor/chatgpt2api/source/services/storage/database_storage.py +194 -194
- package/vendor/chatgpt2api/source/services/storage/factory.py +104 -104
- package/vendor/chatgpt2api/source/services/storage/git_storage.py +183 -183
- package/vendor/chatgpt2api/source/services/storage/json_storage.py +95 -95
- package/vendor/chatgpt2api/source/services/sub2api_service.py +531 -531
- package/vendor/chatgpt2api/source/utils/helper.py +469 -469
- package/vendor/chatgpt2api/source/utils/image_tokens.py +346 -346
- package/vendor/chatgpt2api/source/utils/log.py +110 -110
- package/vendor/chatgpt2api/source/utils/pkce.py +19 -19
- package/vendor/chatgpt2api/source/utils/pow.py +207 -207
- package/vendor/chatgpt2api/source/utils/sentinel.py +159 -159
- package/vendor/chatgpt2api/source/utils/turnstile.py +184 -184
- package/vendor/chatgpt2api/source/uv.lock +904 -904
- package/vendor/chatgpt2api/source/web/bun.lock +1501 -1501
- package/vendor/chatgpt2api/source/web/components.json +24 -24
- package/vendor/chatgpt2api/source/web/eslint.config.mjs +17 -17
- package/vendor/chatgpt2api/source/web/next.config.ts +40 -40
- package/vendor/chatgpt2api/source/web/package.json +58 -58
- package/vendor/chatgpt2api/source/web/postcss.config.mjs +5 -5
- package/vendor/chatgpt2api/source/web/src/app/accounts/components/account-import-dialog.tsx +925 -925
- package/vendor/chatgpt2api/source/web/src/app/accounts/page.tsx +1307 -1307
- package/vendor/chatgpt2api/source/web/src/app/debug/components/chat-panel.tsx +224 -224
- package/vendor/chatgpt2api/source/web/src/app/debug/components/editable-file-panel.tsx +444 -444
- package/vendor/chatgpt2api/source/web/src/app/debug/components/ppt-panel.tsx +9 -9
- package/vendor/chatgpt2api/source/web/src/app/debug/components/psd-panel.tsx +9 -9
- package/vendor/chatgpt2api/source/web/src/app/debug/components/search-panel.tsx +172 -172
- package/vendor/chatgpt2api/source/web/src/app/debug/components/skill-panel.tsx +169 -169
- package/vendor/chatgpt2api/source/web/src/app/debug/components/types.ts +39 -39
- package/vendor/chatgpt2api/source/web/src/app/debug/page.tsx +59 -59
- package/vendor/chatgpt2api/source/web/src/app/globals.css +230 -230
- package/vendor/chatgpt2api/source/web/src/app/image/components/image-composer.tsx +542 -542
- package/vendor/chatgpt2api/source/web/src/app/image/components/image-results.tsx +554 -554
- package/vendor/chatgpt2api/source/web/src/app/image/components/image-sidebar.tsx +176 -176
- package/vendor/chatgpt2api/source/web/src/app/image/page.tsx +1799 -1799
- package/vendor/chatgpt2api/source/web/src/app/image-manager/page.tsx +732 -732
- package/vendor/chatgpt2api/source/web/src/app/layout.tsx +50 -50
- package/vendor/chatgpt2api/source/web/src/app/login/page.tsx +101 -101
- package/vendor/chatgpt2api/source/web/src/app/logs/page.tsx +356 -356
- package/vendor/chatgpt2api/source/web/src/app/page.tsx +30 -30
- package/vendor/chatgpt2api/source/web/src/app/settings/components/api-docs-card.tsx +341 -341
- package/vendor/chatgpt2api/source/web/src/app/settings/components/backup-settings-card.tsx +447 -447
- package/vendor/chatgpt2api/source/web/src/app/settings/components/base-url-card.tsx +71 -71
- package/vendor/chatgpt2api/source/web/src/app/settings/components/config-card.tsx +491 -491
- package/vendor/chatgpt2api/source/web/src/app/settings/components/cpa-pool-dialog.tsx +108 -108
- package/vendor/chatgpt2api/source/web/src/app/settings/components/cpa-pools-card.tsx +169 -169
- package/vendor/chatgpt2api/source/web/src/app/settings/components/import-browser-dialog.tsx +185 -185
- package/vendor/chatgpt2api/source/web/src/app/settings/components/proxy-runtime-card.tsx +379 -379
- package/vendor/chatgpt2api/source/web/src/app/settings/components/proxy-settings-card.tsx +127 -127
- package/vendor/chatgpt2api/source/web/src/app/settings/components/proxy-settings.tsx +200 -200
- package/vendor/chatgpt2api/source/web/src/app/settings/components/settings-header.tsx +12 -12
- package/vendor/chatgpt2api/source/web/src/app/settings/components/sub2api-connections.tsx +886 -886
- package/vendor/chatgpt2api/source/web/src/app/settings/components/third-party-apps-card.tsx +81 -81
- package/vendor/chatgpt2api/source/web/src/app/settings/components/user-keys-card.tsx +422 -422
- package/vendor/chatgpt2api/source/web/src/app/settings/page.tsx +135 -135
- package/vendor/chatgpt2api/source/web/src/app/settings/store.ts +1069 -1069
- package/vendor/chatgpt2api/source/web/src/components/date-range-filter.tsx +49 -49
- package/vendor/chatgpt2api/source/web/src/components/header-actions.tsx +24 -24
- package/vendor/chatgpt2api/source/web/src/components/image-lightbox.tsx +434 -434
- package/vendor/chatgpt2api/source/web/src/components/image-thumbnail.tsx +46 -46
- package/vendor/chatgpt2api/source/web/src/components/theme-script.tsx +18 -18
- package/vendor/chatgpt2api/source/web/src/components/theme-toggle.tsx +14 -14
- package/vendor/chatgpt2api/source/web/src/components/top-nav.tsx +263 -263
- package/vendor/chatgpt2api/source/web/src/components/ui/animated-theme-toggler.tsx +256 -256
- package/vendor/chatgpt2api/source/web/src/components/ui/badge.tsx +41 -41
- package/vendor/chatgpt2api/source/web/src/components/ui/button.tsx +59 -59
- package/vendor/chatgpt2api/source/web/src/components/ui/calendar.tsx +46 -46
- package/vendor/chatgpt2api/source/web/src/components/ui/card.tsx +61 -61
- package/vendor/chatgpt2api/source/web/src/components/ui/checkbox.tsx +27 -27
- package/vendor/chatgpt2api/source/web/src/components/ui/dia-text-reveal.tsx +267 -267
- package/vendor/chatgpt2api/source/web/src/components/ui/dialog.tsx +132 -132
- package/vendor/chatgpt2api/source/web/src/components/ui/field.tsx +13 -13
- package/vendor/chatgpt2api/source/web/src/components/ui/input.tsx +19 -19
- package/vendor/chatgpt2api/source/web/src/components/ui/label.tsx +24 -24
- package/vendor/chatgpt2api/source/web/src/components/ui/morphing-text.tsx +149 -149
- package/vendor/chatgpt2api/source/web/src/components/ui/popover.tsx +38 -38
- package/vendor/chatgpt2api/source/web/src/components/ui/select.tsx +175 -175
- package/vendor/chatgpt2api/source/web/src/components/ui/sheet.tsx +143 -143
- package/vendor/chatgpt2api/source/web/src/components/ui/table.tsx +29 -29
- package/vendor/chatgpt2api/source/web/src/components/ui/tabs.tsx +91 -91
- package/vendor/chatgpt2api/source/web/src/components/ui/text-3d-flip.tsx +269 -269
- package/vendor/chatgpt2api/source/web/src/components/ui/textarea.tsx +18 -18
- package/vendor/chatgpt2api/source/web/src/components/version-release-dialog.tsx +127 -127
- package/vendor/chatgpt2api/source/web/src/constants/common-env.ts +6 -6
- package/vendor/chatgpt2api/source/web/src/hooks/use-version-check.ts +85 -85
- package/vendor/chatgpt2api/source/web/src/lib/api.ts +926 -926
- package/vendor/chatgpt2api/source/web/src/lib/auth-session.ts +26 -26
- package/vendor/chatgpt2api/source/web/src/lib/release.ts +25 -25
- package/vendor/chatgpt2api/source/web/src/lib/request.ts +93 -93
- package/vendor/chatgpt2api/source/web/src/lib/use-auth-guard.ts +89 -89
- package/vendor/chatgpt2api/source/web/src/lib/utils.ts +6 -6
- package/vendor/chatgpt2api/source/web/src/store/auth.ts +112 -112
- package/vendor/chatgpt2api/source/web/src/store/editable-file-history.ts +33 -33
- package/vendor/chatgpt2api/source/web/src/store/image-conversations.ts +318 -318
- package/vendor/chatgpt2api/source/web/tsconfig.json +42 -42
|
@@ -1,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
|
+
}
|