dsh-plugin-image-workbench 0.0.0-stage → 1.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.en.md +256 -0
- package/README.md +256 -2
- package/client.js +241 -0
- package/cordis.patch.yml +14 -0
- package/host.js +641 -0
- package/install.mjs +387 -0
- package/package.json +57 -4
- package/scripts/vendor-snapshot.py +154 -0
- package/server/access.py +26 -0
- package/server/app.py +1329 -0
- package/server/catalog.json +1 -0
- package/server/catalog.py +147 -0
- package/server/composer.py +391 -0
- package/server/drafts.py +158 -0
- package/server/history.py +575 -0
- package/server/imaging.py +172 -0
- package/server/pool.py +1335 -0
- package/server/pool_admin.py +921 -0
- package/server/pool_service.py +1073 -0
- package/server/requirements.txt +6 -0
- package/server/smoke_check.py +402 -0
- package/server/static/access.html +202 -0
- package/server/static/index.html +4725 -0
- package/vendor/chatgpt2api/README.md +13 -0
- package/vendor/chatgpt2api/SOURCE.json +11 -0
- package/vendor/chatgpt2api/runtime/uv.exe +0 -0
- package/vendor/chatgpt2api/source/.env.example +82 -0
- package/vendor/chatgpt2api/source/.python-version +1 -0
- package/vendor/chatgpt2api/source/LICENSE +21 -0
- package/vendor/chatgpt2api/source/README.md +391 -0
- package/vendor/chatgpt2api/source/VERSION +1 -0
- package/vendor/chatgpt2api/source/api/__init__.py +2 -0
- package/vendor/chatgpt2api/source/api/accounts.py +537 -0
- package/vendor/chatgpt2api/source/api/ai.py +217 -0
- package/vendor/chatgpt2api/source/api/app.py +62 -0
- package/vendor/chatgpt2api/source/api/errors.py +46 -0
- package/vendor/chatgpt2api/source/api/image_inputs.py +309 -0
- package/vendor/chatgpt2api/source/api/image_tasks.py +120 -0
- package/vendor/chatgpt2api/source/api/support.py +134 -0
- package/vendor/chatgpt2api/source/api/system.py +365 -0
- package/vendor/chatgpt2api/source/assets/atlascloud.svg +1 -0
- package/vendor/chatgpt2api/source/assets/ipwo.png +0 -0
- package/vendor/chatgpt2api/source/assets/kunrouai.svg +50 -0
- package/vendor/chatgpt2api/source/config.default.json +108 -0
- package/vendor/chatgpt2api/source/main.py +9 -0
- package/vendor/chatgpt2api/source/pyproject.toml +27 -0
- package/vendor/chatgpt2api/source/scripts/init_proxy_config.py +186 -0
- package/vendor/chatgpt2api/source/scripts/migrate_storage.py +163 -0
- package/vendor/chatgpt2api/source/scripts/privoxy-warp.conf +13 -0
- package/vendor/chatgpt2api/source/scripts/test_outlook_token_mailbox.py +385 -0
- package/vendor/chatgpt2api/source/scripts/test_storage.py +129 -0
- package/vendor/chatgpt2api/source/scripts/verify_oauth_refresh.py +92 -0
- package/vendor/chatgpt2api/source/services/__init__.py +0 -0
- package/vendor/chatgpt2api/source/services/account_service.py +1757 -0
- package/vendor/chatgpt2api/source/services/auth_service.py +253 -0
- package/vendor/chatgpt2api/source/services/backup_service.py +671 -0
- package/vendor/chatgpt2api/source/services/config.py +667 -0
- package/vendor/chatgpt2api/source/services/content_filter.py +242 -0
- package/vendor/chatgpt2api/source/services/cpa_service.py +315 -0
- package/vendor/chatgpt2api/source/services/editable_file_task_service.py +260 -0
- package/vendor/chatgpt2api/source/services/image_service.py +376 -0
- package/vendor/chatgpt2api/source/services/image_storage_service.py +405 -0
- package/vendor/chatgpt2api/source/services/image_tags_service.py +76 -0
- package/vendor/chatgpt2api/source/services/image_task_service.py +550 -0
- package/vendor/chatgpt2api/source/services/log_service.py +337 -0
- package/vendor/chatgpt2api/source/services/model_service.py +182 -0
- package/vendor/chatgpt2api/source/services/oauth_login_service.py +267 -0
- package/vendor/chatgpt2api/source/services/openai_backend_api.py +2763 -0
- package/vendor/chatgpt2api/source/services/openai_oauth.py +32 -0
- package/vendor/chatgpt2api/source/services/protocol/__init__.py +2 -0
- package/vendor/chatgpt2api/source/services/protocol/anthropic_v1_messages.py +307 -0
- package/vendor/chatgpt2api/source/services/protocol/chat_completion_cache.py +238 -0
- package/vendor/chatgpt2api/source/services/protocol/conversation.py +1644 -0
- package/vendor/chatgpt2api/source/services/protocol/error_response.py +124 -0
- package/vendor/chatgpt2api/source/services/protocol/openai_search.py +19 -0
- package/vendor/chatgpt2api/source/services/protocol/openai_v1_chat_complete.py +316 -0
- package/vendor/chatgpt2api/source/services/protocol/openai_v1_image_edit.py +86 -0
- package/vendor/chatgpt2api/source/services/protocol/openai_v1_image_generations.py +42 -0
- package/vendor/chatgpt2api/source/services/protocol/openai_v1_models.py +53 -0
- package/vendor/chatgpt2api/source/services/protocol/openai_v1_response.py +453 -0
- package/vendor/chatgpt2api/source/services/protocol/web_search_tool.py +160 -0
- package/vendor/chatgpt2api/source/services/proxy_service.py +648 -0
- package/vendor/chatgpt2api/source/services/storage/__init__.py +5 -0
- package/vendor/chatgpt2api/source/services/storage/base.py +38 -0
- package/vendor/chatgpt2api/source/services/storage/database_storage.py +194 -0
- package/vendor/chatgpt2api/source/services/storage/factory.py +104 -0
- package/vendor/chatgpt2api/source/services/storage/git_storage.py +183 -0
- package/vendor/chatgpt2api/source/services/storage/json_storage.py +95 -0
- package/vendor/chatgpt2api/source/services/sub2api_service.py +531 -0
- package/vendor/chatgpt2api/source/utils/__init__.py +0 -0
- package/vendor/chatgpt2api/source/utils/helper.py +469 -0
- package/vendor/chatgpt2api/source/utils/image_tokens.py +346 -0
- package/vendor/chatgpt2api/source/utils/log.py +110 -0
- package/vendor/chatgpt2api/source/utils/pkce.py +19 -0
- package/vendor/chatgpt2api/source/utils/pow.py +207 -0
- package/vendor/chatgpt2api/source/utils/sentinel.py +159 -0
- package/vendor/chatgpt2api/source/utils/turnstile.py +184 -0
- package/vendor/chatgpt2api/source/uv.lock +904 -0
- package/vendor/chatgpt2api/source/web/bun.lock +1501 -0
- package/vendor/chatgpt2api/source/web/components.json +24 -0
- package/vendor/chatgpt2api/source/web/eslint.config.mjs +17 -0
- package/vendor/chatgpt2api/source/web/next.config.ts +40 -0
- package/vendor/chatgpt2api/source/web/package.json +58 -0
- package/vendor/chatgpt2api/source/web/postcss.config.mjs +5 -0
- package/vendor/chatgpt2api/source/web/public/github.svg +1 -0
- package/vendor/chatgpt2api/source/web/public/next.svg +1 -0
- package/vendor/chatgpt2api/source/web/public/openai.svg +1 -0
- package/vendor/chatgpt2api/source/web/public/vercel.svg +1 -0
- package/vendor/chatgpt2api/source/web/src/app/accounts/components/account-import-dialog.tsx +925 -0
- package/vendor/chatgpt2api/source/web/src/app/accounts/page.tsx +1307 -0
- package/vendor/chatgpt2api/source/web/src/app/debug/components/chat-panel.tsx +224 -0
- package/vendor/chatgpt2api/source/web/src/app/debug/components/editable-file-panel.tsx +444 -0
- package/vendor/chatgpt2api/source/web/src/app/debug/components/ppt-panel.tsx +9 -0
- package/vendor/chatgpt2api/source/web/src/app/debug/components/psd-panel.tsx +9 -0
- package/vendor/chatgpt2api/source/web/src/app/debug/components/search-panel.tsx +172 -0
- package/vendor/chatgpt2api/source/web/src/app/debug/components/skill-panel.tsx +169 -0
- package/vendor/chatgpt2api/source/web/src/app/debug/components/types.ts +39 -0
- package/vendor/chatgpt2api/source/web/src/app/debug/page.tsx +59 -0
- package/vendor/chatgpt2api/source/web/src/app/favicon.ico +0 -0
- package/vendor/chatgpt2api/source/web/src/app/globals.css +231 -0
- package/vendor/chatgpt2api/source/web/src/app/image/components/image-composer.tsx +542 -0
- package/vendor/chatgpt2api/source/web/src/app/image/components/image-results.tsx +554 -0
- package/vendor/chatgpt2api/source/web/src/app/image/components/image-sidebar.tsx +176 -0
- package/vendor/chatgpt2api/source/web/src/app/image/page.tsx +1799 -0
- package/vendor/chatgpt2api/source/web/src/app/image-manager/page.tsx +732 -0
- package/vendor/chatgpt2api/source/web/src/app/layout.tsx +50 -0
- package/vendor/chatgpt2api/source/web/src/app/login/page.tsx +101 -0
- package/vendor/chatgpt2api/source/web/src/app/logs/page.tsx +356 -0
- package/vendor/chatgpt2api/source/web/src/app/page.tsx +30 -0
- package/vendor/chatgpt2api/source/web/src/app/settings/components/api-docs-card.tsx +341 -0
- package/vendor/chatgpt2api/source/web/src/app/settings/components/backup-settings-card.tsx +447 -0
- package/vendor/chatgpt2api/source/web/src/app/settings/components/base-url-card.tsx +71 -0
- package/vendor/chatgpt2api/source/web/src/app/settings/components/config-card.tsx +491 -0
- package/vendor/chatgpt2api/source/web/src/app/settings/components/cpa-pool-dialog.tsx +108 -0
- package/vendor/chatgpt2api/source/web/src/app/settings/components/cpa-pools-card.tsx +169 -0
- package/vendor/chatgpt2api/source/web/src/app/settings/components/import-browser-dialog.tsx +185 -0
- package/vendor/chatgpt2api/source/web/src/app/settings/components/proxy-runtime-card.tsx +379 -0
- package/vendor/chatgpt2api/source/web/src/app/settings/components/proxy-settings-card.tsx +127 -0
- package/vendor/chatgpt2api/source/web/src/app/settings/components/proxy-settings.tsx +200 -0
- package/vendor/chatgpt2api/source/web/src/app/settings/components/settings-header.tsx +12 -0
- package/vendor/chatgpt2api/source/web/src/app/settings/components/sub2api-connections.tsx +886 -0
- package/vendor/chatgpt2api/source/web/src/app/settings/components/third-party-apps-card.tsx +81 -0
- package/vendor/chatgpt2api/source/web/src/app/settings/components/user-keys-card.tsx +422 -0
- package/vendor/chatgpt2api/source/web/src/app/settings/page.tsx +135 -0
- package/vendor/chatgpt2api/source/web/src/app/settings/store.ts +1069 -0
- package/vendor/chatgpt2api/source/web/src/components/date-range-filter.tsx +49 -0
- package/vendor/chatgpt2api/source/web/src/components/header-actions.tsx +24 -0
- package/vendor/chatgpt2api/source/web/src/components/image-lightbox.tsx +434 -0
- package/vendor/chatgpt2api/source/web/src/components/image-thumbnail.tsx +46 -0
- package/vendor/chatgpt2api/source/web/src/components/theme-script.tsx +18 -0
- package/vendor/chatgpt2api/source/web/src/components/theme-toggle.tsx +14 -0
- package/vendor/chatgpt2api/source/web/src/components/top-nav.tsx +263 -0
- package/vendor/chatgpt2api/source/web/src/components/ui/animated-theme-toggler.tsx +256 -0
- package/vendor/chatgpt2api/source/web/src/components/ui/badge.tsx +41 -0
- package/vendor/chatgpt2api/source/web/src/components/ui/button.tsx +59 -0
- package/vendor/chatgpt2api/source/web/src/components/ui/calendar.tsx +46 -0
- package/vendor/chatgpt2api/source/web/src/components/ui/card.tsx +61 -0
- package/vendor/chatgpt2api/source/web/src/components/ui/checkbox.tsx +27 -0
- package/vendor/chatgpt2api/source/web/src/components/ui/dia-text-reveal.tsx +267 -0
- package/vendor/chatgpt2api/source/web/src/components/ui/dialog.tsx +132 -0
- package/vendor/chatgpt2api/source/web/src/components/ui/field.tsx +13 -0
- package/vendor/chatgpt2api/source/web/src/components/ui/input.tsx +19 -0
- package/vendor/chatgpt2api/source/web/src/components/ui/label.tsx +24 -0
- package/vendor/chatgpt2api/source/web/src/components/ui/morphing-text.tsx +149 -0
- package/vendor/chatgpt2api/source/web/src/components/ui/popover.tsx +38 -0
- package/vendor/chatgpt2api/source/web/src/components/ui/select.tsx +175 -0
- package/vendor/chatgpt2api/source/web/src/components/ui/sheet.tsx +143 -0
- package/vendor/chatgpt2api/source/web/src/components/ui/table.tsx +29 -0
- package/vendor/chatgpt2api/source/web/src/components/ui/tabs.tsx +91 -0
- package/vendor/chatgpt2api/source/web/src/components/ui/text-3d-flip.tsx +269 -0
- package/vendor/chatgpt2api/source/web/src/components/ui/textarea.tsx +18 -0
- package/vendor/chatgpt2api/source/web/src/components/version-release-dialog.tsx +127 -0
- package/vendor/chatgpt2api/source/web/src/constants/common-env.ts +6 -0
- package/vendor/chatgpt2api/source/web/src/hooks/use-version-check.ts +85 -0
- package/vendor/chatgpt2api/source/web/src/lib/api.ts +926 -0
- package/vendor/chatgpt2api/source/web/src/lib/auth-session.ts +26 -0
- package/vendor/chatgpt2api/source/web/src/lib/release.ts +25 -0
- package/vendor/chatgpt2api/source/web/src/lib/request.ts +93 -0
- package/vendor/chatgpt2api/source/web/src/lib/use-auth-guard.ts +89 -0
- package/vendor/chatgpt2api/source/web/src/lib/utils.ts +6 -0
- package/vendor/chatgpt2api/source/web/src/store/auth.ts +112 -0
- package/vendor/chatgpt2api/source/web/src/store/editable-file-history.ts +33 -0
- package/vendor/chatgpt2api/source/web/src/store/image-conversations.ts +318 -0
- package/vendor/chatgpt2api/source/web/tsconfig.json +42 -0
|
@@ -0,0 +1,447 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { CloudUpload, Download, Eye, LoaderCircle, Play, RefreshCcw, Shield, Trash2 } from "lucide-react";
|
|
4
|
+
import { useState } from "react";
|
|
5
|
+
import { toast } from "sonner";
|
|
6
|
+
|
|
7
|
+
import { Badge } from "@/components/ui/badge";
|
|
8
|
+
import { Button } from "@/components/ui/button";
|
|
9
|
+
import { Card, CardContent } from "@/components/ui/card";
|
|
10
|
+
import { Checkbox } from "@/components/ui/checkbox";
|
|
11
|
+
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog";
|
|
12
|
+
import { Input } from "@/components/ui/input";
|
|
13
|
+
import webConfig from "@/constants/common-env";
|
|
14
|
+
import { fetchBackupDetail, getBackupDownloadUrl, type BackupDetail, type BackupInclude } from "@/lib/api";
|
|
15
|
+
import { getStoredAuthKey } from "@/store/auth";
|
|
16
|
+
import { useSettingsStore } from "../store";
|
|
17
|
+
|
|
18
|
+
function formatDateTime(value?: string | null) {
|
|
19
|
+
if (!value) {
|
|
20
|
+
return "—";
|
|
21
|
+
}
|
|
22
|
+
const date = new Date(value);
|
|
23
|
+
if (Number.isNaN(date.getTime())) {
|
|
24
|
+
return value;
|
|
25
|
+
}
|
|
26
|
+
return new Intl.DateTimeFormat("zh-CN", {
|
|
27
|
+
year: "numeric",
|
|
28
|
+
month: "2-digit",
|
|
29
|
+
day: "2-digit",
|
|
30
|
+
hour: "2-digit",
|
|
31
|
+
minute: "2-digit",
|
|
32
|
+
}).format(date);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function formatBytes(value: number) {
|
|
36
|
+
if (!Number.isFinite(value) || value <= 0) {
|
|
37
|
+
return "0 B";
|
|
38
|
+
}
|
|
39
|
+
const units = ["B", "KB", "MB", "GB"];
|
|
40
|
+
let size = value;
|
|
41
|
+
let index = 0;
|
|
42
|
+
while (size >= 1024 && index < units.length - 1) {
|
|
43
|
+
size /= 1024;
|
|
44
|
+
index += 1;
|
|
45
|
+
}
|
|
46
|
+
return `${size >= 10 || index === 0 ? size.toFixed(0) : size.toFixed(1)} ${units[index]}`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function getFilenameFromContentDisposition(value: string | null) {
|
|
50
|
+
const header = String(value || "").trim();
|
|
51
|
+
if (!header) {
|
|
52
|
+
return "";
|
|
53
|
+
}
|
|
54
|
+
const utf8Match = header.match(/filename\*\s*=\s*UTF-8''([^;]+)/i);
|
|
55
|
+
if (utf8Match?.[1]) {
|
|
56
|
+
try {
|
|
57
|
+
return decodeURIComponent(utf8Match[1]);
|
|
58
|
+
} catch {
|
|
59
|
+
return utf8Match[1];
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
const plainMatch = header.match(/filename\s*=\s*"?([^";]+)"?/i);
|
|
63
|
+
return plainMatch?.[1] || "";
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const includeLabels: Array<{ key: keyof BackupInclude; label: string }> = [
|
|
67
|
+
{ key: "config", label: "系统配置" },
|
|
68
|
+
{ key: "cpa", label: "CPA 配置" },
|
|
69
|
+
{ key: "sub2api", label: "Sub2API 配置" },
|
|
70
|
+
{ key: "logs", label: "调度与调用日志" },
|
|
71
|
+
{ key: "image_tasks", label: "图片任务记录" },
|
|
72
|
+
{ key: "accounts_snapshot", label: "账号快照" },
|
|
73
|
+
{ key: "auth_keys_snapshot", label: "用户密钥快照" },
|
|
74
|
+
{ key: "images", label: "图片文件目录" },
|
|
75
|
+
];
|
|
76
|
+
|
|
77
|
+
export function BackupSettingsCard() {
|
|
78
|
+
const [detailOpen, setDetailOpen] = useState(false);
|
|
79
|
+
const [detailLoading, setDetailLoading] = useState(false);
|
|
80
|
+
const [detail, setDetail] = useState<BackupDetail | null>(null);
|
|
81
|
+
const config = useSettingsStore((state) => state.config);
|
|
82
|
+
const backups = useSettingsStore((state) => state.backups);
|
|
83
|
+
const backupState = useSettingsStore((state) => state.backupState);
|
|
84
|
+
const isLoadingConfig = useSettingsStore((state) => state.isLoadingConfig);
|
|
85
|
+
const isSavingConfig = useSettingsStore((state) => state.isSavingConfig);
|
|
86
|
+
const isLoadingBackups = useSettingsStore((state) => state.isLoadingBackups);
|
|
87
|
+
const isRunningBackup = useSettingsStore((state) => state.isRunningBackup);
|
|
88
|
+
const deletingBackupKey = useSettingsStore((state) => state.deletingBackupKey);
|
|
89
|
+
const isTestingBackup = useSettingsStore((state) => state.isTestingBackup);
|
|
90
|
+
const saveConfig = useSettingsStore((state) => state.saveConfig);
|
|
91
|
+
const loadBackups = useSettingsStore((state) => state.loadBackups);
|
|
92
|
+
const runBackup = useSettingsStore((state) => state.runBackup);
|
|
93
|
+
const removeBackup = useSettingsStore((state) => state.removeBackup);
|
|
94
|
+
const testBackup = useSettingsStore((state) => state.testBackup);
|
|
95
|
+
const setBackupField = useSettingsStore((state) => state.setBackupField);
|
|
96
|
+
const setBackupInclude = useSettingsStore((state) => state.setBackupInclude);
|
|
97
|
+
|
|
98
|
+
if (isLoadingConfig) {
|
|
99
|
+
return (
|
|
100
|
+
<Card className="rounded-2xl border-white/80 bg-white/90 shadow-sm">
|
|
101
|
+
<CardContent className="flex items-center justify-center p-10">
|
|
102
|
+
<LoaderCircle className="size-5 animate-spin text-stone-400" />
|
|
103
|
+
</CardContent>
|
|
104
|
+
</Card>
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const backup = config?.backup;
|
|
109
|
+
if (!backup) {
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const handleOpenDetail = async (key: string) => {
|
|
114
|
+
setDetailLoading(true);
|
|
115
|
+
setDetailOpen(true);
|
|
116
|
+
try {
|
|
117
|
+
const data = await fetchBackupDetail(key);
|
|
118
|
+
setDetail(data.item);
|
|
119
|
+
} catch (error) {
|
|
120
|
+
setDetail(null);
|
|
121
|
+
toast.error(error instanceof Error ? error.message : "读取备份详情失败");
|
|
122
|
+
} finally {
|
|
123
|
+
setDetailLoading(false);
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
const handleDownload = async (key: string, name: string) => {
|
|
128
|
+
try {
|
|
129
|
+
const authKey = await getStoredAuthKey();
|
|
130
|
+
if (!authKey) {
|
|
131
|
+
toast.error("当前登录态已失效,请重新登录后再下载");
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
const response = await fetch(`${webConfig.apiUrl.replace(/\/$/, "")}${getBackupDownloadUrl(key)}`, {
|
|
135
|
+
headers: {
|
|
136
|
+
Authorization: `Bearer ${authKey}`,
|
|
137
|
+
},
|
|
138
|
+
});
|
|
139
|
+
if (!response.ok) {
|
|
140
|
+
let message = "下载备份失败";
|
|
141
|
+
try {
|
|
142
|
+
const data = await response.json() as { detail?: { error?: string }; error?: string; message?: string };
|
|
143
|
+
message = data.detail?.error || data.error || data.message || message;
|
|
144
|
+
} catch {
|
|
145
|
+
message = response.status === 401 ? "登录已失效,请重新登录后再试" : message;
|
|
146
|
+
}
|
|
147
|
+
throw new Error(message);
|
|
148
|
+
}
|
|
149
|
+
const downloadName = getFilenameFromContentDisposition(response.headers.get("Content-Disposition")) || name || "backup.bin";
|
|
150
|
+
const blob = await response.blob();
|
|
151
|
+
const url = window.URL.createObjectURL(blob);
|
|
152
|
+
const anchor = document.createElement("a");
|
|
153
|
+
anchor.href = url;
|
|
154
|
+
anchor.download = downloadName;
|
|
155
|
+
document.body.append(anchor);
|
|
156
|
+
anchor.click();
|
|
157
|
+
anchor.remove();
|
|
158
|
+
window.URL.revokeObjectURL(url);
|
|
159
|
+
toast.success("备份下载已开始");
|
|
160
|
+
} catch (error) {
|
|
161
|
+
toast.error(error instanceof Error ? error.message : "下载备份失败");
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
return (
|
|
166
|
+
<>
|
|
167
|
+
<Card className="rounded-2xl border-white/80 bg-white/90 shadow-sm">
|
|
168
|
+
<CardContent className="space-y-6 p-6">
|
|
169
|
+
<div className="flex items-start justify-between gap-4">
|
|
170
|
+
<div className="flex items-center gap-3">
|
|
171
|
+
<div className="flex size-10 items-center justify-center rounded-xl bg-stone-100">
|
|
172
|
+
<CloudUpload className="size-5 text-stone-600" />
|
|
173
|
+
</div>
|
|
174
|
+
<div>
|
|
175
|
+
<h2 className="text-lg font-semibold tracking-tight">R2 备份管理</h2>
|
|
176
|
+
<p className="text-sm text-stone-500">将关键数据定时备份到 Cloudflare R2,支持可选加密、轮替、手动执行与历史清理。</p>
|
|
177
|
+
</div>
|
|
178
|
+
</div>
|
|
179
|
+
<div className="flex items-center gap-2">
|
|
180
|
+
<Badge variant={backupState?.running ? "warning" : backupState?.last_status === "success" ? "success" : "secondary"} className="rounded-md">
|
|
181
|
+
{backupState?.running ? "备份中" : backupState?.last_status === "success" ? "最近成功" : backupState?.last_status === "error" ? "最近失败" : "未执行"}
|
|
182
|
+
</Badge>
|
|
183
|
+
</div>
|
|
184
|
+
</div>
|
|
185
|
+
|
|
186
|
+
<div className="rounded-xl border border-stone-200 bg-stone-50 px-4 py-3 text-sm leading-6 text-stone-600">
|
|
187
|
+
账号与用户密钥会从当前存储后端导出逻辑快照,不依赖底层是 `json`、`sqlite`、`postgres` 还是 `git`。图片目录默认不备份,避免备份体积过大。
|
|
188
|
+
</div>
|
|
189
|
+
|
|
190
|
+
<div className="grid gap-4 md:grid-cols-2">
|
|
191
|
+
<label className="flex items-center gap-3 rounded-xl border border-stone-200 bg-white px-4 py-3 text-sm text-stone-700">
|
|
192
|
+
<Checkbox
|
|
193
|
+
checked={Boolean(backup.enabled)}
|
|
194
|
+
onCheckedChange={(checked) => setBackupField("enabled", Boolean(checked))}
|
|
195
|
+
/>
|
|
196
|
+
启用定时备份
|
|
197
|
+
</label>
|
|
198
|
+
<label className="flex items-center gap-3 rounded-xl border border-stone-200 bg-white px-4 py-3 text-sm text-stone-700">
|
|
199
|
+
<Checkbox
|
|
200
|
+
checked={Boolean(backup.encrypt)}
|
|
201
|
+
onCheckedChange={(checked) => setBackupField("encrypt", Boolean(checked))}
|
|
202
|
+
/>
|
|
203
|
+
启用备份加密
|
|
204
|
+
</label>
|
|
205
|
+
|
|
206
|
+
<div className="space-y-2">
|
|
207
|
+
<label className="text-sm text-stone-700">Cloudflare Account ID</label>
|
|
208
|
+
<Input value={String(backup.account_id || "")} onChange={(event) => setBackupField("account_id", event.target.value)} className="h-10 rounded-xl border-stone-200 bg-white" />
|
|
209
|
+
</div>
|
|
210
|
+
<div className="space-y-2">
|
|
211
|
+
<label className="text-sm text-stone-700">Bucket 名称</label>
|
|
212
|
+
<Input value={String(backup.bucket || "")} onChange={(event) => setBackupField("bucket", event.target.value)} className="h-10 rounded-xl border-stone-200 bg-white" />
|
|
213
|
+
</div>
|
|
214
|
+
|
|
215
|
+
<div className="space-y-2">
|
|
216
|
+
<label className="text-sm text-stone-700">Access Key ID</label>
|
|
217
|
+
<Input value={String(backup.access_key_id || "")} onChange={(event) => setBackupField("access_key_id", event.target.value)} className="h-10 rounded-xl border-stone-200 bg-white" />
|
|
218
|
+
</div>
|
|
219
|
+
<div className="space-y-2">
|
|
220
|
+
<label className="text-sm text-stone-700">Secret Access Key</label>
|
|
221
|
+
<Input type="password" value={String(backup.secret_access_key || "")} onChange={(event) => setBackupField("secret_access_key", event.target.value)} className="h-10 rounded-xl border-stone-200 bg-white" />
|
|
222
|
+
</div>
|
|
223
|
+
|
|
224
|
+
<div className="space-y-2">
|
|
225
|
+
<label className="text-sm text-stone-700">备份前缀</label>
|
|
226
|
+
<Input value={String(backup.prefix || "")} onChange={(event) => setBackupField("prefix", event.target.value)} placeholder="backups" className="h-10 rounded-xl border-stone-200 bg-white" />
|
|
227
|
+
<p className="text-xs text-stone-500">R2 内对象前缀,例如 `backups/prod`。</p>
|
|
228
|
+
</div>
|
|
229
|
+
<div className="space-y-2">
|
|
230
|
+
<label className="text-sm text-stone-700">定时备份间隔</label>
|
|
231
|
+
<Input value={String(backup.interval_minutes || "")} onChange={(event) => setBackupField("interval_minutes", event.target.value)} placeholder="360" className="h-10 rounded-xl border-stone-200 bg-white" />
|
|
232
|
+
<p className="text-xs text-stone-500">单位分钟,服务启动后会按此间隔自动轮询执行。</p>
|
|
233
|
+
</div>
|
|
234
|
+
|
|
235
|
+
<div className="space-y-2">
|
|
236
|
+
<label className="text-sm text-stone-700">保留备份数量</label>
|
|
237
|
+
<Input value={String(backup.rotation_keep || "")} onChange={(event) => setBackupField("rotation_keep", event.target.value)} placeholder="10" className="h-10 rounded-xl border-stone-200 bg-white" />
|
|
238
|
+
<p className="text-xs text-stone-500">成功上传后自动删除更旧的备份。填 `0` 表示不自动轮替。</p>
|
|
239
|
+
</div>
|
|
240
|
+
<div className="space-y-2">
|
|
241
|
+
<label className="text-sm text-stone-700">加密口令</label>
|
|
242
|
+
<Input type="password" value={String(backup.passphrase || "")} onChange={(event) => setBackupField("passphrase", event.target.value)} placeholder={backup.encrypt ? "启用加密后必填" : "留空"} className="h-10 rounded-xl border-stone-200 bg-white" />
|
|
243
|
+
<p className="text-xs text-stone-500">仅在启用加密时使用。请妥善保管,否则无法解密备份内容。</p>
|
|
244
|
+
</div>
|
|
245
|
+
</div>
|
|
246
|
+
|
|
247
|
+
<div className="space-y-3 rounded-xl border border-stone-200 bg-white px-4 py-4">
|
|
248
|
+
<div>
|
|
249
|
+
<div className="text-sm font-medium text-stone-800">备份内容</div>
|
|
250
|
+
<p className="mt-1 text-xs text-stone-500">按组件勾选需要进入备份包的数据。</p>
|
|
251
|
+
</div>
|
|
252
|
+
<div className="grid gap-3 md:grid-cols-3">
|
|
253
|
+
{includeLabels.map((item) => (
|
|
254
|
+
<label key={item.key} className="flex items-center gap-3 text-sm text-stone-700">
|
|
255
|
+
<Checkbox
|
|
256
|
+
checked={Boolean(backup.include[item.key])}
|
|
257
|
+
onCheckedChange={(checked) => setBackupInclude(item.key, Boolean(checked))}
|
|
258
|
+
/>
|
|
259
|
+
{item.label}
|
|
260
|
+
</label>
|
|
261
|
+
))}
|
|
262
|
+
</div>
|
|
263
|
+
</div>
|
|
264
|
+
|
|
265
|
+
<div className="grid gap-3 rounded-xl border border-stone-200 bg-stone-50 px-4 py-4 text-sm text-stone-600 md:grid-cols-3">
|
|
266
|
+
<div>
|
|
267
|
+
<div className="text-xs text-stone-500">最近开始</div>
|
|
268
|
+
<div className="mt-1 font-medium text-stone-800">{formatDateTime(backupState?.last_started_at)}</div>
|
|
269
|
+
</div>
|
|
270
|
+
<div>
|
|
271
|
+
<div className="text-xs text-stone-500">最近完成</div>
|
|
272
|
+
<div className="mt-1 font-medium text-stone-800">{formatDateTime(backupState?.last_finished_at)}</div>
|
|
273
|
+
</div>
|
|
274
|
+
<div>
|
|
275
|
+
<div className="text-xs text-stone-500">最近对象</div>
|
|
276
|
+
<div className="mt-1 break-all font-medium text-stone-800">{backupState?.last_object_key || "—"}</div>
|
|
277
|
+
</div>
|
|
278
|
+
{backupState?.last_error ? (
|
|
279
|
+
<div className="md:col-span-3">
|
|
280
|
+
<div className="text-xs text-rose-500">最近错误</div>
|
|
281
|
+
<div className="mt-1 break-all rounded-lg border border-rose-200 bg-rose-50 px-3 py-2 text-rose-700">{backupState.last_error}</div>
|
|
282
|
+
</div>
|
|
283
|
+
) : null}
|
|
284
|
+
</div>
|
|
285
|
+
|
|
286
|
+
<div className="flex flex-wrap justify-end gap-2">
|
|
287
|
+
<Button type="button" variant="outline" className="h-9 rounded-xl border-stone-200 bg-white px-4 text-stone-700" onClick={() => void testBackup()} disabled={isTestingBackup}>
|
|
288
|
+
{isTestingBackup ? <LoaderCircle className="size-4 animate-spin" /> : <Shield className="size-4" />}
|
|
289
|
+
测试连接
|
|
290
|
+
</Button>
|
|
291
|
+
<Button type="button" variant="outline" className="h-9 rounded-xl border-stone-200 bg-white px-4 text-stone-700" onClick={() => void loadBackups()} disabled={isLoadingBackups}>
|
|
292
|
+
{isLoadingBackups ? <LoaderCircle className="size-4 animate-spin" /> : <RefreshCcw className="size-4" />}
|
|
293
|
+
刷新列表
|
|
294
|
+
</Button>
|
|
295
|
+
<Button type="button" variant="outline" className="h-9 rounded-xl border-stone-200 bg-white px-4 text-stone-700" onClick={() => void runBackup()} disabled={isRunningBackup || Boolean(backupState?.running)}>
|
|
296
|
+
{isRunningBackup || backupState?.running ? <LoaderCircle className="size-4 animate-spin" /> : <Play className="size-4" />}
|
|
297
|
+
立即备份
|
|
298
|
+
</Button>
|
|
299
|
+
<Button className="h-9 rounded-xl bg-stone-950 px-4 text-white hover:bg-stone-800" onClick={() => void saveConfig()} disabled={isSavingConfig}>
|
|
300
|
+
{isSavingConfig ? <LoaderCircle className="size-4 animate-spin" /> : <CloudUpload className="size-4" />}
|
|
301
|
+
保存配置
|
|
302
|
+
</Button>
|
|
303
|
+
</div>
|
|
304
|
+
|
|
305
|
+
<div className="space-y-3">
|
|
306
|
+
<div className="flex items-center justify-between gap-3">
|
|
307
|
+
<div>
|
|
308
|
+
<h3 className="text-sm font-medium text-stone-800">历史备份</h3>
|
|
309
|
+
<p className="text-xs text-stone-500">支持查看对象信息并直接删除远端备份。</p>
|
|
310
|
+
</div>
|
|
311
|
+
</div>
|
|
312
|
+
|
|
313
|
+
{isLoadingBackups ? (
|
|
314
|
+
<div className="flex items-center justify-center py-10">
|
|
315
|
+
<LoaderCircle className="size-5 animate-spin text-stone-400" />
|
|
316
|
+
</div>
|
|
317
|
+
) : backups.length === 0 ? (
|
|
318
|
+
<div className="rounded-xl bg-stone-50 px-6 py-10 text-center text-sm text-stone-500">
|
|
319
|
+
暂无远端备份记录。保存配置并执行一次手动备份后会出现在这里。
|
|
320
|
+
</div>
|
|
321
|
+
) : (
|
|
322
|
+
<div className="space-y-3">
|
|
323
|
+
{backups.map((item) => {
|
|
324
|
+
const isDeleting = deletingBackupKey === item.key;
|
|
325
|
+
return (
|
|
326
|
+
<div key={item.key} className="flex flex-col gap-3 rounded-xl border border-stone-200 bg-white px-4 py-4 md:flex-row md:items-center md:justify-between">
|
|
327
|
+
<div className="min-w-0 space-y-2">
|
|
328
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
329
|
+
<div className="break-all text-sm font-medium text-stone-800">{item.name}</div>
|
|
330
|
+
{item.encrypted ? <Badge variant="secondary" className="rounded-md">已加密</Badge> : null}
|
|
331
|
+
</div>
|
|
332
|
+
<div className="flex flex-wrap gap-x-4 gap-y-1 text-xs text-stone-500">
|
|
333
|
+
<span>大小 {formatBytes(item.size)}</span>
|
|
334
|
+
<span>更新时间 {formatDateTime(item.updated_at)}</span>
|
|
335
|
+
<span className="break-all">对象 key {item.key}</span>
|
|
336
|
+
</div>
|
|
337
|
+
</div>
|
|
338
|
+
|
|
339
|
+
<div className="flex items-center gap-2">
|
|
340
|
+
<Button
|
|
341
|
+
type="button"
|
|
342
|
+
variant="outline"
|
|
343
|
+
className="h-9 rounded-xl border-stone-200 bg-white px-4 text-stone-700"
|
|
344
|
+
onClick={() => void handleDownload(item.key, item.name)}
|
|
345
|
+
>
|
|
346
|
+
<Download className="size-4" />
|
|
347
|
+
下载
|
|
348
|
+
</Button>
|
|
349
|
+
<Button type="button" variant="outline" className="h-9 rounded-xl border-stone-200 bg-white px-4 text-stone-700" onClick={() => void handleOpenDetail(item.key)}>
|
|
350
|
+
<Eye className="size-4" />
|
|
351
|
+
查看详情
|
|
352
|
+
</Button>
|
|
353
|
+
<Button
|
|
354
|
+
type="button"
|
|
355
|
+
variant="outline"
|
|
356
|
+
className="h-9 rounded-xl border-rose-200 bg-white px-4 text-rose-700"
|
|
357
|
+
onClick={() => void removeBackup(item.key)}
|
|
358
|
+
disabled={isDeleting}
|
|
359
|
+
>
|
|
360
|
+
{isDeleting ? <LoaderCircle className="size-4 animate-spin" /> : <Trash2 className="size-4" />}
|
|
361
|
+
删除
|
|
362
|
+
</Button>
|
|
363
|
+
</div>
|
|
364
|
+
</div>
|
|
365
|
+
);
|
|
366
|
+
})}
|
|
367
|
+
</div>
|
|
368
|
+
)}
|
|
369
|
+
</div>
|
|
370
|
+
</CardContent>
|
|
371
|
+
</Card>
|
|
372
|
+
|
|
373
|
+
<Dialog open={detailOpen} onOpenChange={setDetailOpen}>
|
|
374
|
+
<DialogContent className="flex max-h-[85vh] max-w-3xl flex-col overflow-hidden rounded-2xl border-white/80 bg-white">
|
|
375
|
+
<DialogHeader className="shrink-0 border-b border-stone-200 pb-3">
|
|
376
|
+
<DialogTitle>备份详情</DialogTitle>
|
|
377
|
+
</DialogHeader>
|
|
378
|
+
<div className="min-h-0 flex-1 space-y-4 overflow-y-auto pr-1">
|
|
379
|
+
{detailLoading ? (
|
|
380
|
+
<div className="flex items-center justify-center py-16">
|
|
381
|
+
<LoaderCircle className="size-5 animate-spin text-stone-400" />
|
|
382
|
+
</div>
|
|
383
|
+
) : !detail ? (
|
|
384
|
+
<div className="rounded-xl bg-stone-50 px-6 py-10 text-center text-sm text-stone-500">
|
|
385
|
+
暂时无法读取备份详情;如果这是加密备份,请确认当前已填写正确的加密口令并先保存配置。
|
|
386
|
+
</div>
|
|
387
|
+
) : (
|
|
388
|
+
<>
|
|
389
|
+
<div className="grid gap-3 rounded-xl border border-stone-200 bg-stone-50 px-4 py-4 text-sm text-stone-600 md:grid-cols-2">
|
|
390
|
+
<div>
|
|
391
|
+
<div className="text-xs text-stone-500">对象名称</div>
|
|
392
|
+
<div className="mt-1 break-all font-medium text-stone-800">{detail.name}</div>
|
|
393
|
+
</div>
|
|
394
|
+
<div>
|
|
395
|
+
<div className="text-xs text-stone-500">创建时间</div>
|
|
396
|
+
<div className="mt-1 font-medium text-stone-800">{formatDateTime(detail.created_at)}</div>
|
|
397
|
+
</div>
|
|
398
|
+
<div>
|
|
399
|
+
<div className="text-xs text-stone-500">触发方式</div>
|
|
400
|
+
<div className="mt-1 font-medium text-stone-800">{detail.trigger || "—"}</div>
|
|
401
|
+
</div>
|
|
402
|
+
<div>
|
|
403
|
+
<div className="text-xs text-stone-500">应用版本</div>
|
|
404
|
+
<div className="mt-1 font-medium text-stone-800">{detail.app_version || "—"}</div>
|
|
405
|
+
</div>
|
|
406
|
+
<div className="md:col-span-2">
|
|
407
|
+
<div className="text-xs text-stone-500">存储后端</div>
|
|
408
|
+
<pre className="mt-1 overflow-x-auto rounded-lg border border-stone-200 bg-white px-3 py-2 text-xs text-stone-700">{JSON.stringify(detail.storage_backend || {}, null, 2)}</pre>
|
|
409
|
+
</div>
|
|
410
|
+
</div>
|
|
411
|
+
|
|
412
|
+
<div className="space-y-3">
|
|
413
|
+
<h4 className="text-sm font-medium text-stone-800">文件内容</h4>
|
|
414
|
+
<div className="space-y-2">
|
|
415
|
+
{detail.files.map((item) => (
|
|
416
|
+
<div key={item.name} className="rounded-xl border border-stone-200 bg-white px-4 py-3 text-sm">
|
|
417
|
+
<div className="break-all font-medium text-stone-800">{item.name}</div>
|
|
418
|
+
<div className="mt-2 flex flex-wrap gap-x-4 gap-y-1 text-xs text-stone-500">
|
|
419
|
+
<span>{item.exists ? "已包含" : "缺失"}</span>
|
|
420
|
+
<span>大小 {formatBytes(item.size)}</span>
|
|
421
|
+
<span>{item.content_type || "application/octet-stream"}</span>
|
|
422
|
+
<span className="break-all">SHA256 {item.sha256 || "—"}</span>
|
|
423
|
+
</div>
|
|
424
|
+
</div>
|
|
425
|
+
))}
|
|
426
|
+
</div>
|
|
427
|
+
</div>
|
|
428
|
+
|
|
429
|
+
<div className="space-y-3">
|
|
430
|
+
<h4 className="text-sm font-medium text-stone-800">快照内容</h4>
|
|
431
|
+
<div className="grid gap-3 md:grid-cols-2">
|
|
432
|
+
{detail.snapshots.map((item) => (
|
|
433
|
+
<div key={item.name} className="rounded-xl border border-stone-200 bg-white px-4 py-3 text-sm">
|
|
434
|
+
<div className="font-medium text-stone-800">{item.name}</div>
|
|
435
|
+
<div className="mt-2 text-xs text-stone-500">记录数 {item.count}</div>
|
|
436
|
+
</div>
|
|
437
|
+
))}
|
|
438
|
+
</div>
|
|
439
|
+
</div>
|
|
440
|
+
</>
|
|
441
|
+
)}
|
|
442
|
+
</div>
|
|
443
|
+
</DialogContent>
|
|
444
|
+
</Dialog>
|
|
445
|
+
</>
|
|
446
|
+
);
|
|
447
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { Globe, LoaderCircle, Save } from "lucide-react";
|
|
4
|
+
|
|
5
|
+
import { Badge } from "@/components/ui/badge";
|
|
6
|
+
import { Button } from "@/components/ui/button";
|
|
7
|
+
import { Card, CardContent } from "@/components/ui/card";
|
|
8
|
+
import { Input } from "@/components/ui/input";
|
|
9
|
+
|
|
10
|
+
import { useSettingsStore } from "../store";
|
|
11
|
+
|
|
12
|
+
export function BaseUrlCard() {
|
|
13
|
+
const config = useSettingsStore((state) => state.config);
|
|
14
|
+
const isLoadingConfig = useSettingsStore((state) => state.isLoadingConfig);
|
|
15
|
+
const isSavingConfig = useSettingsStore((state) => state.isSavingConfig);
|
|
16
|
+
const setBaseUrl = useSettingsStore((state) => state.setBaseUrl);
|
|
17
|
+
const saveConfig = useSettingsStore((state) => state.saveConfig);
|
|
18
|
+
|
|
19
|
+
const baseUrl = String(config?.base_url || "");
|
|
20
|
+
|
|
21
|
+
return (
|
|
22
|
+
<Card className="rounded-2xl border-white/80 bg-white/90 shadow-sm">
|
|
23
|
+
<CardContent className="space-y-6 p-6">
|
|
24
|
+
<div className="flex flex-col gap-4 lg:flex-row lg:items-start lg:justify-between">
|
|
25
|
+
<div className="flex items-center gap-3">
|
|
26
|
+
<div className="flex size-10 items-center justify-center rounded-xl bg-stone-100">
|
|
27
|
+
<Globe className="size-5 text-stone-600" />
|
|
28
|
+
</div>
|
|
29
|
+
<div>
|
|
30
|
+
<h2 className="text-lg font-semibold tracking-tight">基础地址</h2>
|
|
31
|
+
<p className="text-sm text-stone-500">设置 `CHATGPT2API_BASE_URL` 的本地配置值,保存后立即生效。</p>
|
|
32
|
+
</div>
|
|
33
|
+
</div>
|
|
34
|
+
<Badge variant={baseUrl.trim() ? "success" : "secondary"} className="w-fit rounded-md px-2.5 py-1">
|
|
35
|
+
{baseUrl.trim() ? "已配置" : "未配置"}
|
|
36
|
+
</Badge>
|
|
37
|
+
</div>
|
|
38
|
+
|
|
39
|
+
{isLoadingConfig ? (
|
|
40
|
+
<div className="flex items-center justify-center py-10">
|
|
41
|
+
<LoaderCircle className="size-5 animate-spin text-stone-400" />
|
|
42
|
+
</div>
|
|
43
|
+
) : (
|
|
44
|
+
<>
|
|
45
|
+
<div className="space-y-2">
|
|
46
|
+
<label className="text-sm font-medium text-stone-700">Base URL</label>
|
|
47
|
+
<Input
|
|
48
|
+
value={baseUrl}
|
|
49
|
+
onChange={(event) => setBaseUrl(event.target.value)}
|
|
50
|
+
placeholder="https://example.com"
|
|
51
|
+
className="h-11 rounded-xl border-stone-200 bg-white"
|
|
52
|
+
/>
|
|
53
|
+
<p className="text-sm text-stone-500">留空则使用环境变量或默认值,保存时会自动去掉首尾空格。</p>
|
|
54
|
+
</div>
|
|
55
|
+
|
|
56
|
+
<div className="flex justify-end">
|
|
57
|
+
<Button
|
|
58
|
+
className="h-10 rounded-xl bg-stone-950 px-5 text-white hover:bg-stone-800"
|
|
59
|
+
onClick={() => void saveConfig()}
|
|
60
|
+
disabled={isSavingConfig}
|
|
61
|
+
>
|
|
62
|
+
{isSavingConfig ? <LoaderCircle className="size-4 animate-spin" /> : <Save className="size-4" />}
|
|
63
|
+
保存配置
|
|
64
|
+
</Button>
|
|
65
|
+
</div>
|
|
66
|
+
</>
|
|
67
|
+
)}
|
|
68
|
+
</CardContent>
|
|
69
|
+
</Card>
|
|
70
|
+
);
|
|
71
|
+
}
|