dsh-plugin-image-workbench 0.0.0-stage → 1.1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.en.md +258 -0
- package/README.md +260 -2
- package/client.js +241 -0
- package/cordis.patch.yml +14 -0
- package/host.js +692 -0
- package/install.mjs +333 -0
- package/package.json +58 -4
- package/scripts/fetch-style-images.mjs +104 -0
- 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,925 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useRouter } from "next/navigation";
|
|
4
|
+
import { useRef, useState, type ChangeEvent } from "react";
|
|
5
|
+
import {
|
|
6
|
+
ArrowLeft,
|
|
7
|
+
Copy,
|
|
8
|
+
ExternalLink,
|
|
9
|
+
FileJson,
|
|
10
|
+
FileText,
|
|
11
|
+
Files,
|
|
12
|
+
KeyRound,
|
|
13
|
+
LoaderCircle,
|
|
14
|
+
LogIn,
|
|
15
|
+
ServerCog,
|
|
16
|
+
Upload,
|
|
17
|
+
} from "lucide-react";
|
|
18
|
+
import { toast } from "sonner";
|
|
19
|
+
|
|
20
|
+
import { Button } from "@/components/ui/button";
|
|
21
|
+
import { Card, CardContent } from "@/components/ui/card";
|
|
22
|
+
import {
|
|
23
|
+
Dialog,
|
|
24
|
+
DialogContent,
|
|
25
|
+
DialogDescription,
|
|
26
|
+
DialogFooter,
|
|
27
|
+
DialogHeader,
|
|
28
|
+
DialogTitle,
|
|
29
|
+
} from "@/components/ui/dialog";
|
|
30
|
+
import { Textarea } from "@/components/ui/textarea";
|
|
31
|
+
import {
|
|
32
|
+
createAccounts,
|
|
33
|
+
finishOAuthLogin,
|
|
34
|
+
startOAuthLogin,
|
|
35
|
+
type Account,
|
|
36
|
+
type AccountImportPayload,
|
|
37
|
+
type OAuthLoginStartResponse,
|
|
38
|
+
} from "@/lib/api";
|
|
39
|
+
import { cn } from "@/lib/utils";
|
|
40
|
+
|
|
41
|
+
type ImportMethod = "menu" | "token" | "session" | "codex-auth" | "account-json" | "oauth";
|
|
42
|
+
|
|
43
|
+
type AccountImportDialogProps = {
|
|
44
|
+
disabled?: boolean;
|
|
45
|
+
onImported: (items: Account[]) => void;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
type PendingAccountJsonImport = {
|
|
49
|
+
tokens: string[];
|
|
50
|
+
accounts: AccountImportPayload[];
|
|
51
|
+
parsedAccountCount: number;
|
|
52
|
+
errorCount: number;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const sessionUrl = "https://chatgpt.com/api/auth/session";
|
|
56
|
+
|
|
57
|
+
function splitTokens(value: string) {
|
|
58
|
+
return value
|
|
59
|
+
.split(/\r?\n/)
|
|
60
|
+
.map((item) => item.trim())
|
|
61
|
+
.filter(Boolean);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function getSessionAccessToken(value: unknown) {
|
|
65
|
+
const token = (value as { accessToken?: unknown })?.accessToken;
|
|
66
|
+
return typeof token === "string" ? token.trim() : "";
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function getAccountJsonAccount(value: unknown): AccountImportPayload | null {
|
|
70
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
const raw = value as Record<string, unknown>;
|
|
74
|
+
const tokenValue = raw.access_token ?? raw.accessToken;
|
|
75
|
+
const token = typeof tokenValue === "string" ? tokenValue.trim() : "";
|
|
76
|
+
if (!token) {
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const payload: AccountImportPayload = {
|
|
81
|
+
...raw,
|
|
82
|
+
access_token: token,
|
|
83
|
+
source_type: "codex",
|
|
84
|
+
};
|
|
85
|
+
delete payload.accessToken;
|
|
86
|
+
if (payload.type === "codex") {
|
|
87
|
+
payload.export_type = "codex";
|
|
88
|
+
delete payload.type;
|
|
89
|
+
}
|
|
90
|
+
return payload;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function getAccountJsonAccounts(value: unknown): AccountImportPayload[] {
|
|
94
|
+
if (Array.isArray(value)) {
|
|
95
|
+
return value
|
|
96
|
+
.map((item) => getAccountJsonAccount(item))
|
|
97
|
+
.filter((item): item is AccountImportPayload => Boolean(item));
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const singleAccount = getAccountJsonAccount(value);
|
|
101
|
+
if (singleAccount) {
|
|
102
|
+
return [singleAccount];
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (value && typeof value === "object") {
|
|
106
|
+
const raw = value as Record<string, unknown>;
|
|
107
|
+
const nested = raw.accounts ?? raw.items;
|
|
108
|
+
if (Array.isArray(nested)) {
|
|
109
|
+
return nested
|
|
110
|
+
.map((item) => getAccountJsonAccount(item))
|
|
111
|
+
.filter((item): item is AccountImportPayload => Boolean(item));
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return [];
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function getCodexAuthAccount(value: unknown): AccountImportPayload | null {
|
|
119
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
const raw = value as Record<string, unknown>;
|
|
123
|
+
const tokenValue = raw.access_token ?? raw.accessToken;
|
|
124
|
+
const token = typeof tokenValue === "string" ? tokenValue.trim() : "";
|
|
125
|
+
if (!token) {
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const payload: AccountImportPayload = {
|
|
130
|
+
...raw,
|
|
131
|
+
access_token: token,
|
|
132
|
+
export_type: "codex",
|
|
133
|
+
source_type: "codex",
|
|
134
|
+
};
|
|
135
|
+
delete payload.accessToken;
|
|
136
|
+
if (payload.type === "codex") {
|
|
137
|
+
delete payload.type;
|
|
138
|
+
}
|
|
139
|
+
return payload;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function readFileAsText(file: File) {
|
|
143
|
+
return new Promise<string>((resolve, reject) => {
|
|
144
|
+
const reader = new FileReader();
|
|
145
|
+
reader.onload = () => resolve(typeof reader.result === "string" ? reader.result : "");
|
|
146
|
+
reader.onerror = () => reject(reader.error ?? new Error(`读取文件失败: ${file.name}`));
|
|
147
|
+
reader.readAsText(file);
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function MethodCard({
|
|
152
|
+
title,
|
|
153
|
+
description,
|
|
154
|
+
icon: Icon,
|
|
155
|
+
onClick,
|
|
156
|
+
}: {
|
|
157
|
+
title: string;
|
|
158
|
+
description: string;
|
|
159
|
+
icon: typeof KeyRound;
|
|
160
|
+
onClick: () => void;
|
|
161
|
+
}) {
|
|
162
|
+
return (
|
|
163
|
+
<button
|
|
164
|
+
type="button"
|
|
165
|
+
onClick={onClick}
|
|
166
|
+
className="w-full rounded-2xl border border-stone-200 bg-white p-0 text-left transition hover:border-stone-300 hover:bg-stone-50"
|
|
167
|
+
>
|
|
168
|
+
<Card className="rounded-2xl border-0 bg-transparent shadow-none">
|
|
169
|
+
<CardContent className="flex items-start gap-4 p-4">
|
|
170
|
+
<div className="rounded-xl bg-stone-100 p-3 text-stone-700">
|
|
171
|
+
<Icon className="size-5" />
|
|
172
|
+
</div>
|
|
173
|
+
<div className="space-y-1">
|
|
174
|
+
<div className="text-sm font-semibold text-stone-900">{title}</div>
|
|
175
|
+
<div className="text-sm leading-6 text-stone-500">{description}</div>
|
|
176
|
+
</div>
|
|
177
|
+
</CardContent>
|
|
178
|
+
</Card>
|
|
179
|
+
</button>
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export function AccountImportDialog({ disabled, onImported }: AccountImportDialogProps) {
|
|
184
|
+
const router = useRouter();
|
|
185
|
+
const [open, setOpen] = useState(false);
|
|
186
|
+
const [method, setMethod] = useState<ImportMethod>("menu");
|
|
187
|
+
const [tokenInput, setTokenInput] = useState("");
|
|
188
|
+
const [sessionInput, setSessionInput] = useState("");
|
|
189
|
+
const [codexAuthInput, setCodexAuthInput] = useState("");
|
|
190
|
+
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
191
|
+
const [pendingAccountJsonImport, setPendingAccountJsonImport] = useState<PendingAccountJsonImport | null>(null);
|
|
192
|
+
const [confirmOpen, setConfirmOpen] = useState(false);
|
|
193
|
+
const [oauthEmailHint, setOauthEmailHint] = useState("");
|
|
194
|
+
const [oauthSession, setOauthSession] = useState<OAuthLoginStartResponse | null>(null);
|
|
195
|
+
const [oauthCallbackInput, setOauthCallbackInput] = useState("");
|
|
196
|
+
const [oauthStarting, setOauthStarting] = useState(false);
|
|
197
|
+
|
|
198
|
+
const txtInputRef = useRef<HTMLInputElement | null>(null);
|
|
199
|
+
const accountJsonInputRef = useRef<HTMLInputElement | null>(null);
|
|
200
|
+
|
|
201
|
+
const resetState = () => {
|
|
202
|
+
setMethod("menu");
|
|
203
|
+
setTokenInput("");
|
|
204
|
+
setSessionInput("");
|
|
205
|
+
setCodexAuthInput("");
|
|
206
|
+
setPendingAccountJsonImport(null);
|
|
207
|
+
setConfirmOpen(false);
|
|
208
|
+
setOauthEmailHint("");
|
|
209
|
+
setOauthSession(null);
|
|
210
|
+
setOauthCallbackInput("");
|
|
211
|
+
setOauthStarting(false);
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
const handleOpenChange = (nextOpen: boolean) => {
|
|
215
|
+
setOpen(nextOpen);
|
|
216
|
+
if (!nextOpen) {
|
|
217
|
+
resetState();
|
|
218
|
+
}
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
const submitTokens = async (tokens: string[], successText?: string, accountPayloads: AccountImportPayload[] = []) => {
|
|
222
|
+
const normalizedTokens = tokens.map((item) => item.trim()).filter(Boolean);
|
|
223
|
+
|
|
224
|
+
if (normalizedTokens.length === 0) {
|
|
225
|
+
toast.error("请先提供至少一个可用 Token");
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
setIsSubmitting(true);
|
|
230
|
+
try {
|
|
231
|
+
const data = await createAccounts(normalizedTokens, accountPayloads);
|
|
232
|
+
onImported(data.items);
|
|
233
|
+
setOpen(false);
|
|
234
|
+
resetState();
|
|
235
|
+
|
|
236
|
+
if ((data.errors?.length ?? 0) > 0) {
|
|
237
|
+
const firstError = data.errors?.[0]?.error;
|
|
238
|
+
toast.error(
|
|
239
|
+
`${successText ?? "导入完成"},新增 ${data.added ?? 0} 个,已刷新 ${data.refreshed ?? 0} 个,失败 ${data.errors?.length ?? 0} 个${firstError ? `,首个错误:${firstError}` : ""}`,
|
|
240
|
+
);
|
|
241
|
+
} else {
|
|
242
|
+
toast.success(
|
|
243
|
+
`${successText ?? "导入完成"},新增 ${data.added ?? 0} 个,跳过 ${data.skipped ?? 0} 个重复项,已自动刷新账号信息`,
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
} catch (error) {
|
|
247
|
+
const message = error instanceof Error ? error.message : "导入账户失败";
|
|
248
|
+
toast.error(message);
|
|
249
|
+
} finally {
|
|
250
|
+
setIsSubmitting(false);
|
|
251
|
+
}
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
const handleImportTokenText = async () => {
|
|
255
|
+
await submitTokens(splitTokens(tokenInput), "Access Token 导入完成");
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
// 起授权:拿 authorize URL,立刻在新窗口打开,方便用户登录
|
|
259
|
+
const handleStartOAuth = async () => {
|
|
260
|
+
setOauthStarting(true);
|
|
261
|
+
try {
|
|
262
|
+
const data = await startOAuthLogin(oauthEmailHint.trim());
|
|
263
|
+
setOauthSession(data);
|
|
264
|
+
setOauthCallbackInput("");
|
|
265
|
+
if (typeof window !== "undefined") {
|
|
266
|
+
window.open(data.authorize_url, "_blank", "noopener,noreferrer");
|
|
267
|
+
}
|
|
268
|
+
toast.success("已打开 OpenAI 授权页面,请在登录后复制 callback URL 回来");
|
|
269
|
+
} catch (error) {
|
|
270
|
+
const message = error instanceof Error ? error.message : "OAuth 起始失败";
|
|
271
|
+
toast.error(message);
|
|
272
|
+
} finally {
|
|
273
|
+
setOauthStarting(false);
|
|
274
|
+
}
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
// 用粘贴回来的 callback URL 完成换 token + 落盘
|
|
278
|
+
const handleFinishOAuth = async () => {
|
|
279
|
+
if (!oauthSession) {
|
|
280
|
+
toast.error("请先点击\"打开授权页面\"获取 session");
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
const trimmed = oauthCallbackInput.trim();
|
|
284
|
+
if (!trimmed) {
|
|
285
|
+
toast.error("请粘贴 callback URL 或 code");
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
setIsSubmitting(true);
|
|
290
|
+
try {
|
|
291
|
+
const data = await finishOAuthLogin(oauthSession.session_id, trimmed);
|
|
292
|
+
onImported(data.items);
|
|
293
|
+
setOpen(false);
|
|
294
|
+
resetState();
|
|
295
|
+
|
|
296
|
+
if ((data.errors?.length ?? 0) > 0) {
|
|
297
|
+
const firstError = data.errors?.[0]?.error;
|
|
298
|
+
toast.error(
|
|
299
|
+
`OAuth 登录完成,新增 ${data.added ?? 0} 个,已刷新 ${data.refreshed ?? 0} 个,失败 ${data.errors?.length ?? 0} 个${firstError ? `,首个错误:${firstError}` : ""}`,
|
|
300
|
+
);
|
|
301
|
+
} else {
|
|
302
|
+
toast.success(
|
|
303
|
+
`OAuth 登录完成,新增 ${data.added ?? 0} 个,跳过 ${data.skipped ?? 0} 个重复项,已自动刷新账号信息`,
|
|
304
|
+
);
|
|
305
|
+
}
|
|
306
|
+
} catch (error) {
|
|
307
|
+
const message = error instanceof Error ? error.message : "OAuth 换 token 失败";
|
|
308
|
+
toast.error(message);
|
|
309
|
+
} finally {
|
|
310
|
+
setIsSubmitting(false);
|
|
311
|
+
}
|
|
312
|
+
};
|
|
313
|
+
|
|
314
|
+
// 复制 authorize URL 到剪贴板(适配浏览器和 fallback)
|
|
315
|
+
const handleCopyAuthorizeUrl = async () => {
|
|
316
|
+
if (!oauthSession) {
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
try {
|
|
320
|
+
if (navigator.clipboard?.writeText) {
|
|
321
|
+
await navigator.clipboard.writeText(oauthSession.authorize_url);
|
|
322
|
+
toast.success("授权 URL 已复制到剪贴板");
|
|
323
|
+
} else {
|
|
324
|
+
toast.error("当前环境不支持自动复制,请手动选择并复制");
|
|
325
|
+
}
|
|
326
|
+
} catch {
|
|
327
|
+
toast.error("复制失败,请手动选择并复制");
|
|
328
|
+
}
|
|
329
|
+
};
|
|
330
|
+
|
|
331
|
+
const handleTxtSelected = async (event: ChangeEvent<HTMLInputElement>) => {
|
|
332
|
+
const file = event.target.files?.[0];
|
|
333
|
+
event.target.value = "";
|
|
334
|
+
|
|
335
|
+
if (!file) {
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
try {
|
|
340
|
+
const content = await readFileAsText(file);
|
|
341
|
+
const tokens = splitTokens(content);
|
|
342
|
+
|
|
343
|
+
if (tokens.length === 0) {
|
|
344
|
+
toast.error("TXT 文件里没有读取到有效 Token");
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
setTokenInput((prev) => {
|
|
349
|
+
const next = [...splitTokens(prev), ...tokens];
|
|
350
|
+
return next.join("\n");
|
|
351
|
+
});
|
|
352
|
+
toast.success(`已从 ${file.name} 读取 ${tokens.length} 个 Token`);
|
|
353
|
+
} catch (error) {
|
|
354
|
+
const message = error instanceof Error ? error.message : "读取 TXT 文件失败";
|
|
355
|
+
toast.error(message);
|
|
356
|
+
}
|
|
357
|
+
};
|
|
358
|
+
|
|
359
|
+
const handleImportSessionJson = async () => {
|
|
360
|
+
if (!sessionInput.trim()) {
|
|
361
|
+
toast.error("请先粘贴完整 Session JSON");
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
try {
|
|
366
|
+
const payload = JSON.parse(sessionInput) as unknown;
|
|
367
|
+
const token = getSessionAccessToken(payload);
|
|
368
|
+
|
|
369
|
+
if (!token) {
|
|
370
|
+
toast.error("未从 Session JSON 中提取到 accessToken");
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
await submitTokens([token], "Session JSON 导入完成");
|
|
375
|
+
} catch (error) {
|
|
376
|
+
const message = error instanceof Error ? error.message : "Session JSON 解析失败";
|
|
377
|
+
toast.error(message);
|
|
378
|
+
}
|
|
379
|
+
};
|
|
380
|
+
|
|
381
|
+
const handleImportCodexAuthJson = async () => {
|
|
382
|
+
if (!codexAuthInput.trim()) {
|
|
383
|
+
toast.error("请先粘贴 Codex 认证 JSON");
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
try {
|
|
388
|
+
const payload = JSON.parse(codexAuthInput) as unknown;
|
|
389
|
+
const account = getCodexAuthAccount(payload);
|
|
390
|
+
|
|
391
|
+
if (!account) {
|
|
392
|
+
toast.error("未从 Codex 认证 JSON 中提取到 access_token");
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
await submitTokens([account.access_token], "Codex 认证 JSON 导入完成", [account]);
|
|
397
|
+
} catch (error) {
|
|
398
|
+
const message = error instanceof Error ? error.message : "Codex 认证 JSON 解析失败";
|
|
399
|
+
toast.error(message);
|
|
400
|
+
}
|
|
401
|
+
};
|
|
402
|
+
|
|
403
|
+
const handleAccountJsonSelected = async (event: ChangeEvent<HTMLInputElement>) => {
|
|
404
|
+
const files = Array.from(event.target.files ?? []);
|
|
405
|
+
event.target.value = "";
|
|
406
|
+
|
|
407
|
+
if (files.length === 0) {
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
try {
|
|
412
|
+
const results = await Promise.all(
|
|
413
|
+
files.map(async (file) => {
|
|
414
|
+
const raw = await readFileAsText(file);
|
|
415
|
+
const parsed = JSON.parse(raw) as unknown;
|
|
416
|
+
const accounts = getAccountJsonAccounts(parsed);
|
|
417
|
+
return {
|
|
418
|
+
accounts,
|
|
419
|
+
};
|
|
420
|
+
}),
|
|
421
|
+
);
|
|
422
|
+
|
|
423
|
+
const accounts = results.flatMap((item) => item.accounts);
|
|
424
|
+
const tokens = accounts.map((item) => item.access_token);
|
|
425
|
+
const parsedAccountCount = accounts.length;
|
|
426
|
+
const errorCount = results.filter((item) => item.accounts.length === 0).length;
|
|
427
|
+
|
|
428
|
+
if (parsedAccountCount === 0) {
|
|
429
|
+
toast.error("这些账号 JSON 文件里没有读取到可用 access_token");
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
setPendingAccountJsonImport({
|
|
434
|
+
tokens,
|
|
435
|
+
accounts,
|
|
436
|
+
parsedAccountCount,
|
|
437
|
+
errorCount,
|
|
438
|
+
});
|
|
439
|
+
setConfirmOpen(true);
|
|
440
|
+
} catch (error) {
|
|
441
|
+
const message = error instanceof Error ? error.message : "读取账号 JSON 文件失败";
|
|
442
|
+
toast.error(message);
|
|
443
|
+
}
|
|
444
|
+
};
|
|
445
|
+
|
|
446
|
+
const renderMethodBody = () => {
|
|
447
|
+
if (method === "token") {
|
|
448
|
+
const tokenCount = splitTokens(tokenInput).length;
|
|
449
|
+
|
|
450
|
+
return (
|
|
451
|
+
<div className="space-y-4">
|
|
452
|
+
<div className="flex items-center justify-between">
|
|
453
|
+
<button
|
|
454
|
+
type="button"
|
|
455
|
+
onClick={() => setMethod("menu")}
|
|
456
|
+
className="inline-flex items-center gap-1 text-sm text-stone-500 transition hover:text-stone-800"
|
|
457
|
+
>
|
|
458
|
+
<ArrowLeft className="size-4" />
|
|
459
|
+
返回导入方式
|
|
460
|
+
</button>
|
|
461
|
+
<span className="text-xs text-stone-400">当前识别 {tokenCount} 个 Token</span>
|
|
462
|
+
</div>
|
|
463
|
+
<div className="space-y-2">
|
|
464
|
+
<label className="text-sm font-medium text-stone-700">Access Token 列表</label>
|
|
465
|
+
<Textarea
|
|
466
|
+
placeholder="每行一个 Access Token..."
|
|
467
|
+
value={tokenInput}
|
|
468
|
+
onChange={(event) => setTokenInput(event.target.value)}
|
|
469
|
+
className="min-h-56 resize-none rounded-xl border-stone-200"
|
|
470
|
+
/>
|
|
471
|
+
</div>
|
|
472
|
+
<div className="rounded-2xl border border-dashed border-stone-200 bg-stone-50 p-4">
|
|
473
|
+
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
|
474
|
+
<div className="space-y-1">
|
|
475
|
+
<div className="text-sm font-medium text-stone-800">从 TXT 文件导入</div>
|
|
476
|
+
<div className="text-sm leading-6 text-stone-500">支持 `.txt`,文件内容也是一行一个 Token。</div>
|
|
477
|
+
</div>
|
|
478
|
+
<Button
|
|
479
|
+
type="button"
|
|
480
|
+
variant="outline"
|
|
481
|
+
className="rounded-xl border-stone-200 bg-white"
|
|
482
|
+
onClick={() => txtInputRef.current?.click()}
|
|
483
|
+
disabled={isSubmitting}
|
|
484
|
+
>
|
|
485
|
+
<FileText className="size-4" />
|
|
486
|
+
选择 TXT
|
|
487
|
+
</Button>
|
|
488
|
+
</div>
|
|
489
|
+
</div>
|
|
490
|
+
<input
|
|
491
|
+
ref={txtInputRef}
|
|
492
|
+
type="file"
|
|
493
|
+
accept=".txt,text/plain"
|
|
494
|
+
className="hidden"
|
|
495
|
+
onChange={(event) => void handleTxtSelected(event)}
|
|
496
|
+
/>
|
|
497
|
+
</div>
|
|
498
|
+
);
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
if (method === "session") {
|
|
502
|
+
return (
|
|
503
|
+
<div className="space-y-4">
|
|
504
|
+
<button
|
|
505
|
+
type="button"
|
|
506
|
+
onClick={() => setMethod("menu")}
|
|
507
|
+
className="inline-flex items-center gap-1 text-sm text-stone-500 transition hover:text-stone-800"
|
|
508
|
+
>
|
|
509
|
+
<ArrowLeft className="size-4" />
|
|
510
|
+
返回导入方式
|
|
511
|
+
</button>
|
|
512
|
+
<div className="rounded-2xl border border-stone-200 bg-stone-50 p-4 text-sm leading-6 text-stone-600">
|
|
513
|
+
打开
|
|
514
|
+
{" "}
|
|
515
|
+
<a
|
|
516
|
+
href={sessionUrl}
|
|
517
|
+
target="_blank"
|
|
518
|
+
rel="noreferrer"
|
|
519
|
+
className="inline-flex items-center gap-1 font-medium text-stone-900 underline underline-offset-4"
|
|
520
|
+
>
|
|
521
|
+
{sessionUrl}
|
|
522
|
+
<ExternalLink className="size-3.5" />
|
|
523
|
+
</a>
|
|
524
|
+
,复制页面返回的完整 JSON,系统会自动提取其中的 `accessToken` 导入。
|
|
525
|
+
</div>
|
|
526
|
+
<div className="rounded-2xl border border-amber-200 bg-amber-50 p-4 text-sm leading-6 text-amber-900">
|
|
527
|
+
<div className="font-medium">风险提示</div>
|
|
528
|
+
<div>
|
|
529
|
+
不要使用自己的大号,尽量使用不常用的小号进行导入,避免出现封号风险。本项目不承担任何封号风险责任。
|
|
530
|
+
</div>
|
|
531
|
+
</div>
|
|
532
|
+
<div className="space-y-2">
|
|
533
|
+
<label className="text-sm font-medium text-stone-700">Session JSON</label>
|
|
534
|
+
<Textarea
|
|
535
|
+
placeholder='粘贴完整 JSON,例如包含 "accessToken" 的对象...'
|
|
536
|
+
value={sessionInput}
|
|
537
|
+
onChange={(event) => setSessionInput(event.target.value)}
|
|
538
|
+
className="min-h-56 resize-none rounded-xl border-stone-200 font-mono text-xs"
|
|
539
|
+
/>
|
|
540
|
+
</div>
|
|
541
|
+
</div>
|
|
542
|
+
);
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
if (method === "oauth") {
|
|
546
|
+
return (
|
|
547
|
+
<div className="space-y-4">
|
|
548
|
+
<button
|
|
549
|
+
type="button"
|
|
550
|
+
onClick={() => setMethod("menu")}
|
|
551
|
+
className="inline-flex items-center gap-1 text-sm text-stone-500 transition hover:text-stone-800"
|
|
552
|
+
>
|
|
553
|
+
<ArrowLeft className="size-4" />
|
|
554
|
+
返回导入方式
|
|
555
|
+
</button>
|
|
556
|
+
<div className="rounded-2xl border border-stone-200 bg-stone-50 p-4 text-sm leading-6 text-stone-600 space-y-2">
|
|
557
|
+
<div className="font-medium text-stone-800">操作步骤</div>
|
|
558
|
+
<ol className="list-decimal pl-5 space-y-1">
|
|
559
|
+
<li>(可选)填写你 ChatGPT 账号的邮箱,登录页会预填。</li>
|
|
560
|
+
<li>点击下方"打开授权页面",在新标签里登录自己的 ChatGPT 账号。</li>
|
|
561
|
+
<li>登录完成后浏览器会跳到 <code className="rounded bg-stone-200 px-1">platform.openai.com/auth/callback?code=...</code>。立刻从地址栏复制整段 URL(或开 F12 在 Network 里抓到 callback 那一行,右键 Copy → Copy URL)。</li>
|
|
562
|
+
<li>把 callback URL 粘到下面输入框,点"完成导入"。</li>
|
|
563
|
+
</ol>
|
|
564
|
+
</div>
|
|
565
|
+
<div className="space-y-2">
|
|
566
|
+
<label className="text-sm font-medium text-stone-700">邮箱(可选预填)</label>
|
|
567
|
+
<input
|
|
568
|
+
type="email"
|
|
569
|
+
placeholder="you@example.com"
|
|
570
|
+
value={oauthEmailHint}
|
|
571
|
+
onChange={(event) => setOauthEmailHint(event.target.value)}
|
|
572
|
+
disabled={Boolean(oauthSession) || oauthStarting}
|
|
573
|
+
className="w-full rounded-xl border border-stone-200 bg-white px-3 py-2 text-sm outline-none focus:border-stone-400"
|
|
574
|
+
/>
|
|
575
|
+
</div>
|
|
576
|
+
{!oauthSession ? (
|
|
577
|
+
<Button
|
|
578
|
+
type="button"
|
|
579
|
+
className="h-10 rounded-xl bg-stone-950 text-white hover:bg-stone-800"
|
|
580
|
+
onClick={() => void handleStartOAuth()}
|
|
581
|
+
disabled={oauthStarting}
|
|
582
|
+
>
|
|
583
|
+
{oauthStarting ? <LoaderCircle className="size-4 animate-spin" /> : <ExternalLink className="size-4" />}
|
|
584
|
+
打开授权页面
|
|
585
|
+
</Button>
|
|
586
|
+
) : (
|
|
587
|
+
<div className="space-y-3">
|
|
588
|
+
<div className="rounded-2xl border border-stone-200 bg-white p-3 text-xs leading-6 text-stone-600 break-all font-mono">
|
|
589
|
+
{oauthSession.authorize_url}
|
|
590
|
+
</div>
|
|
591
|
+
<div className="flex flex-wrap gap-2">
|
|
592
|
+
<Button
|
|
593
|
+
type="button"
|
|
594
|
+
variant="outline"
|
|
595
|
+
className="rounded-xl border-stone-200 bg-white"
|
|
596
|
+
onClick={() => void handleCopyAuthorizeUrl()}
|
|
597
|
+
>
|
|
598
|
+
<Copy className="size-4" />
|
|
599
|
+
复制授权 URL
|
|
600
|
+
</Button>
|
|
601
|
+
<Button
|
|
602
|
+
type="button"
|
|
603
|
+
variant="outline"
|
|
604
|
+
className="rounded-xl border-stone-200 bg-white"
|
|
605
|
+
onClick={() => window.open(oauthSession.authorize_url, "_blank", "noopener,noreferrer")}
|
|
606
|
+
>
|
|
607
|
+
<ExternalLink className="size-4" />
|
|
608
|
+
再次打开
|
|
609
|
+
</Button>
|
|
610
|
+
<Button
|
|
611
|
+
type="button"
|
|
612
|
+
variant="outline"
|
|
613
|
+
className="rounded-xl border-stone-200 bg-white"
|
|
614
|
+
onClick={() => {
|
|
615
|
+
setOauthSession(null);
|
|
616
|
+
setOauthCallbackInput("");
|
|
617
|
+
}}
|
|
618
|
+
>
|
|
619
|
+
重新生成
|
|
620
|
+
</Button>
|
|
621
|
+
</div>
|
|
622
|
+
<div className="space-y-2">
|
|
623
|
+
<label className="text-sm font-medium text-stone-700">粘贴 callback URL(或仅 code)</label>
|
|
624
|
+
<Textarea
|
|
625
|
+
placeholder={"https://platform.openai.com/auth/callback?code=...&state=..."}
|
|
626
|
+
value={oauthCallbackInput}
|
|
627
|
+
onChange={(event) => setOauthCallbackInput(event.target.value)}
|
|
628
|
+
className="min-h-24 resize-none rounded-xl border-stone-200 font-mono text-xs"
|
|
629
|
+
/>
|
|
630
|
+
</div>
|
|
631
|
+
</div>
|
|
632
|
+
)}
|
|
633
|
+
<div className="rounded-2xl border border-amber-200 bg-amber-50 p-4 text-sm leading-6 text-amber-900">
|
|
634
|
+
<div className="font-medium">注意</div>
|
|
635
|
+
<div>
|
|
636
|
+
授权码(code)只能使用一次。如果浏览器的 callback 页加载完成、显示了 OpenAI 的错误页,那 code 大概率已经被消耗,
|
|
637
|
+
请点击"重新生成"再走一次。整个流程在 10 分钟内完成即可。
|
|
638
|
+
</div>
|
|
639
|
+
</div>
|
|
640
|
+
</div>
|
|
641
|
+
);
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
if (method === "account-json") {
|
|
645
|
+
return (
|
|
646
|
+
<div className="space-y-4">
|
|
647
|
+
<button
|
|
648
|
+
type="button"
|
|
649
|
+
onClick={() => setMethod("menu")}
|
|
650
|
+
className="inline-flex items-center gap-1 text-sm text-stone-500 transition hover:text-stone-800"
|
|
651
|
+
>
|
|
652
|
+
<ArrowLeft className="size-4" />
|
|
653
|
+
返回导入方式
|
|
654
|
+
</button>
|
|
655
|
+
<div className="rounded-2xl border border-dashed border-stone-200 bg-stone-50 p-5">
|
|
656
|
+
<div className="space-y-2">
|
|
657
|
+
<div className="text-sm font-medium text-stone-800">选择本地账号 JSON 文件</div>
|
|
658
|
+
<div className="text-sm leading-6 text-stone-500">
|
|
659
|
+
支持本项目导出的单账号对象或全部账号数组,也兼容每个文件一个账号对象的 CPA JSON。
|
|
660
|
+
系统会自动提取 `access_token` 或 `accessToken`。
|
|
661
|
+
</div>
|
|
662
|
+
</div>
|
|
663
|
+
<Button
|
|
664
|
+
type="button"
|
|
665
|
+
className="mt-4 rounded-xl bg-stone-950 text-white hover:bg-stone-800"
|
|
666
|
+
onClick={() => accountJsonInputRef.current?.click()}
|
|
667
|
+
disabled={isSubmitting}
|
|
668
|
+
>
|
|
669
|
+
<Files className="size-4" />
|
|
670
|
+
选择 JSON 文件
|
|
671
|
+
</Button>
|
|
672
|
+
</div>
|
|
673
|
+
<input
|
|
674
|
+
ref={accountJsonInputRef}
|
|
675
|
+
type="file"
|
|
676
|
+
accept=".json,application/json"
|
|
677
|
+
multiple
|
|
678
|
+
className="hidden"
|
|
679
|
+
onChange={(event) => void handleAccountJsonSelected(event)}
|
|
680
|
+
/>
|
|
681
|
+
{pendingAccountJsonImport ? (
|
|
682
|
+
<div className="rounded-2xl border border-stone-200 bg-white p-4 text-sm leading-6 text-stone-600">
|
|
683
|
+
最近一次读取到 {pendingAccountJsonImport.parsedAccountCount} 个 Token
|
|
684
|
+
{pendingAccountJsonImport.errorCount > 0 ? `,另有 ${pendingAccountJsonImport.errorCount} 个文件未提取成功` : ""}。
|
|
685
|
+
</div>
|
|
686
|
+
) : null}
|
|
687
|
+
</div>
|
|
688
|
+
);
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
if (method === "codex-auth") {
|
|
692
|
+
return (
|
|
693
|
+
<div className="space-y-4">
|
|
694
|
+
<button
|
|
695
|
+
type="button"
|
|
696
|
+
onClick={() => setMethod("menu")}
|
|
697
|
+
className="inline-flex items-center gap-1 text-sm text-stone-500 transition hover:text-stone-800"
|
|
698
|
+
>
|
|
699
|
+
<ArrowLeft className="size-4" />
|
|
700
|
+
返回导入方式
|
|
701
|
+
</button>
|
|
702
|
+
<div className="space-y-2">
|
|
703
|
+
<label className="text-sm font-medium text-stone-700">Codex 认证 JSON</label>
|
|
704
|
+
<Textarea
|
|
705
|
+
placeholder='粘贴包含 "access_token"、"refresh_token"、"id_token" 的 Codex 认证 JSON...'
|
|
706
|
+
value={codexAuthInput}
|
|
707
|
+
onChange={(event) => setCodexAuthInput(event.target.value)}
|
|
708
|
+
className="min-h-64 resize-none rounded-xl border-stone-200 font-mono text-xs"
|
|
709
|
+
/>
|
|
710
|
+
</div>
|
|
711
|
+
</div>
|
|
712
|
+
);
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
return (
|
|
716
|
+
<div className="space-y-3">
|
|
717
|
+
<MethodCard
|
|
718
|
+
title="OAuth 登录已有账号(带自动刷新)"
|
|
719
|
+
description="用浏览器登录自己的 ChatGPT 账号,回填 callback URL 即可拿到 refresh_token,后台会自动续期。"
|
|
720
|
+
icon={LogIn}
|
|
721
|
+
onClick={() => setMethod("oauth")}
|
|
722
|
+
/>
|
|
723
|
+
<MethodCard
|
|
724
|
+
title="导入 Access Token"
|
|
725
|
+
description="支持直接粘贴,一行一个;也支持从 TXT 文件读取,一行一个。"
|
|
726
|
+
icon={KeyRound}
|
|
727
|
+
onClick={() => setMethod("token")}
|
|
728
|
+
/>
|
|
729
|
+
<MethodCard
|
|
730
|
+
title="导入 Session JSON"
|
|
731
|
+
description="从 chatgpt.com 的 session 接口复制完整 JSON,自动提取 accessToken。"
|
|
732
|
+
icon={FileJson}
|
|
733
|
+
onClick={() => setMethod("session")}
|
|
734
|
+
/>
|
|
735
|
+
<MethodCard
|
|
736
|
+
title="导入 Codex 认证 JSON"
|
|
737
|
+
description="粘贴 Codex 认证 JSON,导入后账号来源标记为 codex。"
|
|
738
|
+
icon={FileJson}
|
|
739
|
+
onClick={() => setMethod("codex-auth")}
|
|
740
|
+
/>
|
|
741
|
+
<MethodCard
|
|
742
|
+
title="导入账号 JSON 文件"
|
|
743
|
+
description="支持本项目导出的单账号 JSON 或全部账号数组,也兼容 CPA JSON 文件。"
|
|
744
|
+
icon={Files}
|
|
745
|
+
onClick={() => setMethod("account-json")}
|
|
746
|
+
/>
|
|
747
|
+
<MethodCard
|
|
748
|
+
title="从远程 CPA 服务器导入"
|
|
749
|
+
description="前往设置页面配置远程 CPA 服务器后再执行导入。"
|
|
750
|
+
icon={Files}
|
|
751
|
+
onClick={() => {
|
|
752
|
+
setOpen(false);
|
|
753
|
+
resetState();
|
|
754
|
+
router.push("/settings");
|
|
755
|
+
}}
|
|
756
|
+
/>
|
|
757
|
+
<MethodCard
|
|
758
|
+
title="从 Sub2API 服务器导入"
|
|
759
|
+
description="前往设置页面配置 Sub2API 服务器,再选择其中的 OpenAI 账号导入。"
|
|
760
|
+
icon={ServerCog}
|
|
761
|
+
onClick={() => {
|
|
762
|
+
setOpen(false);
|
|
763
|
+
resetState();
|
|
764
|
+
router.push("/settings");
|
|
765
|
+
}}
|
|
766
|
+
/>
|
|
767
|
+
</div>
|
|
768
|
+
);
|
|
769
|
+
};
|
|
770
|
+
|
|
771
|
+
const footerDisabled = disabled || isSubmitting;
|
|
772
|
+
|
|
773
|
+
return (
|
|
774
|
+
<>
|
|
775
|
+
<Dialog open={open} onOpenChange={handleOpenChange}>
|
|
776
|
+
<Button
|
|
777
|
+
className="h-10 rounded-xl bg-stone-950 px-4 text-white hover:bg-stone-800"
|
|
778
|
+
onClick={() => setOpen(true)}
|
|
779
|
+
disabled={disabled}
|
|
780
|
+
>
|
|
781
|
+
<Upload className="size-4" />
|
|
782
|
+
导入
|
|
783
|
+
</Button>
|
|
784
|
+
<DialogContent showCloseButton={false} className="rounded-2xl p-6">
|
|
785
|
+
<DialogHeader className="gap-2">
|
|
786
|
+
<DialogTitle>
|
|
787
|
+
{method === "menu"
|
|
788
|
+
? "导入账户"
|
|
789
|
+
: method === "token"
|
|
790
|
+
? "导入 Access Token"
|
|
791
|
+
: method === "session"
|
|
792
|
+
? "导入 Session JSON"
|
|
793
|
+
: method === "codex-auth"
|
|
794
|
+
? "导入 Codex 认证 JSON"
|
|
795
|
+
: method === "oauth"
|
|
796
|
+
? "OAuth 登录已有账号"
|
|
797
|
+
: "导入账号 JSON"}
|
|
798
|
+
</DialogTitle>
|
|
799
|
+
<DialogDescription className="text-sm leading-6">
|
|
800
|
+
{method === "menu"
|
|
801
|
+
? "选择一种导入方式。导入成功后会自动拉取邮箱、类型和额度。"
|
|
802
|
+
: method === "token"
|
|
803
|
+
? "支持手动粘贴或从 TXT 文件导入,一行一个 Token。"
|
|
804
|
+
: method === "session"
|
|
805
|
+
? "粘贴完整 Session JSON,系统会自动提取 accessToken。"
|
|
806
|
+
: method === "codex-auth"
|
|
807
|
+
? "粘贴 Codex 认证 JSON,系统会按 codex 来源导入。"
|
|
808
|
+
: method === "oauth"
|
|
809
|
+
? "用浏览器跑一遍 OpenAI 标准 OAuth,拿回 refresh_token 后系统会自动续期。"
|
|
810
|
+
: "支持读取本项目导出的单账号对象或全部账号数组,并在提交前做数量确认。"}
|
|
811
|
+
</DialogDescription>
|
|
812
|
+
</DialogHeader>
|
|
813
|
+
|
|
814
|
+
{renderMethodBody()}
|
|
815
|
+
|
|
816
|
+
<DialogFooter className="pt-2">
|
|
817
|
+
<Button
|
|
818
|
+
variant="secondary"
|
|
819
|
+
className="h-10 rounded-xl bg-stone-100 px-5 text-stone-700 hover:bg-stone-200"
|
|
820
|
+
onClick={() => setOpen(false)}
|
|
821
|
+
disabled={footerDisabled}
|
|
822
|
+
>
|
|
823
|
+
取消
|
|
824
|
+
</Button>
|
|
825
|
+
{method === "token" ? (
|
|
826
|
+
<Button
|
|
827
|
+
className="h-10 rounded-xl bg-stone-950 px-5 text-white hover:bg-stone-800"
|
|
828
|
+
onClick={() => void handleImportTokenText()}
|
|
829
|
+
disabled={footerDisabled}
|
|
830
|
+
>
|
|
831
|
+
{isSubmitting ? <LoaderCircle className="size-4 animate-spin" /> : null}
|
|
832
|
+
导入 Token
|
|
833
|
+
</Button>
|
|
834
|
+
) : null}
|
|
835
|
+
{method === "session" ? (
|
|
836
|
+
<Button
|
|
837
|
+
className="h-10 rounded-xl bg-stone-950 px-5 text-white hover:bg-stone-800"
|
|
838
|
+
onClick={() => void handleImportSessionJson()}
|
|
839
|
+
disabled={footerDisabled}
|
|
840
|
+
>
|
|
841
|
+
{isSubmitting ? <LoaderCircle className="size-4 animate-spin" /> : null}
|
|
842
|
+
导入 JSON
|
|
843
|
+
</Button>
|
|
844
|
+
) : null}
|
|
845
|
+
{method === "codex-auth" ? (
|
|
846
|
+
<Button
|
|
847
|
+
className="h-10 rounded-xl bg-stone-950 px-5 text-white hover:bg-stone-800"
|
|
848
|
+
onClick={() => void handleImportCodexAuthJson()}
|
|
849
|
+
disabled={footerDisabled}
|
|
850
|
+
>
|
|
851
|
+
{isSubmitting ? <LoaderCircle className="size-4 animate-spin" /> : null}
|
|
852
|
+
导入 JSON
|
|
853
|
+
</Button>
|
|
854
|
+
) : null}
|
|
855
|
+
{method === "oauth" ? (
|
|
856
|
+
<Button
|
|
857
|
+
className={cn(
|
|
858
|
+
"h-10 rounded-xl bg-stone-950 px-5 text-white hover:bg-stone-800",
|
|
859
|
+
!oauthSession ? "hidden" : "",
|
|
860
|
+
)}
|
|
861
|
+
onClick={() => void handleFinishOAuth()}
|
|
862
|
+
disabled={footerDisabled || !oauthSession || !oauthCallbackInput.trim()}
|
|
863
|
+
>
|
|
864
|
+
{isSubmitting ? <LoaderCircle className="size-4 animate-spin" /> : null}
|
|
865
|
+
完成导入
|
|
866
|
+
</Button>
|
|
867
|
+
) : null}
|
|
868
|
+
{method === "account-json" ? (
|
|
869
|
+
<Button
|
|
870
|
+
className={cn(
|
|
871
|
+
"h-10 rounded-xl bg-stone-950 px-5 text-white hover:bg-stone-800",
|
|
872
|
+
!pendingAccountJsonImport ? "hidden" : "",
|
|
873
|
+
)}
|
|
874
|
+
onClick={() => setConfirmOpen(true)}
|
|
875
|
+
disabled={footerDisabled || !pendingAccountJsonImport}
|
|
876
|
+
>
|
|
877
|
+
查看导入确认
|
|
878
|
+
</Button>
|
|
879
|
+
) : null}
|
|
880
|
+
</DialogFooter>
|
|
881
|
+
</DialogContent>
|
|
882
|
+
</Dialog>
|
|
883
|
+
|
|
884
|
+
<Dialog open={confirmOpen} onOpenChange={setConfirmOpen}>
|
|
885
|
+
<DialogContent className="rounded-2xl p-6">
|
|
886
|
+
<DialogHeader className="gap-2">
|
|
887
|
+
<DialogTitle>确认导入账号 Token</DialogTitle>
|
|
888
|
+
<DialogDescription className="text-sm leading-6">
|
|
889
|
+
{pendingAccountJsonImport
|
|
890
|
+
? `确认识别到 ${pendingAccountJsonImport.parsedAccountCount} 个 Token,是否确认导入?`
|
|
891
|
+
: "尚未读取到可导入的 Token。"}
|
|
892
|
+
{pendingAccountJsonImport?.errorCount
|
|
893
|
+
? `,另有 ${pendingAccountJsonImport.errorCount} 个文件未提取成功。`
|
|
894
|
+
: "。"}
|
|
895
|
+
</DialogDescription>
|
|
896
|
+
</DialogHeader>
|
|
897
|
+
<DialogFooter className="pt-2">
|
|
898
|
+
<Button
|
|
899
|
+
variant="secondary"
|
|
900
|
+
className="h-10 rounded-xl bg-stone-100 px-5 text-stone-700 hover:bg-stone-200"
|
|
901
|
+
onClick={() => setConfirmOpen(false)}
|
|
902
|
+
disabled={isSubmitting}
|
|
903
|
+
>
|
|
904
|
+
返回
|
|
905
|
+
</Button>
|
|
906
|
+
<Button
|
|
907
|
+
className="h-10 rounded-xl bg-stone-950 px-5 text-white hover:bg-stone-800"
|
|
908
|
+
onClick={() =>
|
|
909
|
+
void submitTokens(
|
|
910
|
+
pendingAccountJsonImport?.tokens ?? [],
|
|
911
|
+
"账号 JSON 导入完成",
|
|
912
|
+
pendingAccountJsonImport?.accounts ?? [],
|
|
913
|
+
)
|
|
914
|
+
}
|
|
915
|
+
disabled={isSubmitting || !pendingAccountJsonImport}
|
|
916
|
+
>
|
|
917
|
+
{isSubmitting ? <LoaderCircle className="size-4 animate-spin" /> : null}
|
|
918
|
+
确认导入
|
|
919
|
+
</Button>
|
|
920
|
+
</DialogFooter>
|
|
921
|
+
</DialogContent>
|
|
922
|
+
</Dialog>
|
|
923
|
+
</>
|
|
924
|
+
);
|
|
925
|
+
}
|