diff --git a/.gitignore b/.gitignore index 7d21226..38f37c3 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,9 @@ /admin-ui/node_modules/ /admin-ui/dist/ +/admin-ui/pnpm-lock.yaml +/admin-ui/package-lock.json +/admin-ui/tsconfig.tsbuildinfo .claude/ !.claude/skills/ diff --git a/Cargo.lock b/Cargo.lock index a841afc..33f9f15 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -911,7 +911,7 @@ dependencies = [ [[package]] name = "kiro-rs" -version = "1.1.5" +version = "1.1.4" dependencies = [ "anyhow", "axum", diff --git a/admin-ui/src/components/batch-import-dialog.tsx b/admin-ui/src/components/batch-import-dialog.tsx deleted file mode 100644 index 407bc0c..0000000 --- a/admin-ui/src/components/batch-import-dialog.tsx +++ /dev/null @@ -1,434 +0,0 @@ -import { useState } from 'react' -import { toast } from 'sonner' -import { CheckCircle2, XCircle, AlertCircle, Loader2 } from 'lucide-react' -import { - Dialog, - DialogContent, - DialogHeader, - DialogTitle, - DialogFooter, -} from '@/components/ui/dialog' -import { Button } from '@/components/ui/button' -import { useCredentials, useAddCredential, useDeleteCredential } from '@/hooks/use-credentials' -import { getCredentialBalance, setCredentialDisabled } from '@/api/credentials' -import { extractErrorMessage, sha256Hex } from '@/lib/utils' - -interface BatchImportDialogProps { - open: boolean - onOpenChange: (open: boolean) => void -} - -interface CredentialInput { - refreshToken: string - clientId?: string - clientSecret?: string - region?: string - authRegion?: string - apiRegion?: string - priority?: number - machineId?: string -} - -interface VerificationResult { - index: number - status: 'pending' | 'checking' | 'verifying' | 'verified' | 'duplicate' | 'failed' - error?: string - usage?: string - email?: string - credentialId?: number - rollbackStatus?: 'success' | 'failed' | 'skipped' - rollbackError?: string -} - - - -export function BatchImportDialog({ open, onOpenChange }: BatchImportDialogProps) { - const [jsonInput, setJsonInput] = useState('') - const [importing, setImporting] = useState(false) - const [progress, setProgress] = useState({ current: 0, total: 0 }) - const [currentProcessing, setCurrentProcessing] = useState('') - const [results, setResults] = useState([]) - - const { data: existingCredentials } = useCredentials() - const { mutateAsync: addCredential } = useAddCredential() - const { mutateAsync: deleteCredential } = useDeleteCredential() - - const rollbackCredential = async (id: number): Promise<{ success: boolean; error?: string }> => { - try { - await setCredentialDisabled(id, true) - } catch (error) { - return { - success: false, - error: `禁用失败: ${extractErrorMessage(error)}`, - } - } - - try { - await deleteCredential(id) - return { success: true } - } catch (error) { - return { - success: false, - error: `删除失败: ${extractErrorMessage(error)}`, - } - } - } - - const resetForm = () => { - setJsonInput('') - setProgress({ current: 0, total: 0 }) - setCurrentProcessing('') - setResults([]) - } - - const handleBatchImport = async () => { - // 先单独解析 JSON,给出精准的错误提示 - let credentials: CredentialInput[] - try { - const parsed = JSON.parse(jsonInput) - credentials = Array.isArray(parsed) ? parsed : [parsed] - } catch (error) { - toast.error('JSON 格式错误: ' + extractErrorMessage(error)) - return - } - - if (credentials.length === 0) { - toast.error('没有可导入的凭据') - return - } - - try { - setImporting(true) - setProgress({ current: 0, total: credentials.length }) - - // 2. 初始化结果 - const initialResults: VerificationResult[] = credentials.map((_, i) => ({ - index: i + 1, - status: 'pending' - })) - setResults(initialResults) - - // 3. 检测重复 - const existingTokenHashes = new Set( - existingCredentials?.credentials - .map(c => c.refreshTokenHash) - .filter((hash): hash is string => Boolean(hash)) || [] - ) - - let successCount = 0 - let duplicateCount = 0 - let failCount = 0 - let rollbackSuccessCount = 0 - let rollbackFailedCount = 0 - let rollbackSkippedCount = 0 - - // 4. 导入并验活 - for (let i = 0; i < credentials.length; i++) { - const cred = credentials[i] - const token = cred.refreshToken.trim() - const tokenHash = await sha256Hex(token) - - // 更新状态为检查中 - setCurrentProcessing(`正在处理凭据 ${i + 1}/${credentials.length}`) - setResults(prev => { - const newResults = [...prev] - newResults[i] = { ...newResults[i], status: 'checking' } - return newResults - }) - - // 检查重复 - if (existingTokenHashes.has(tokenHash)) { - duplicateCount++ - const existingCred = existingCredentials?.credentials.find(c => c.refreshTokenHash === tokenHash) - setResults(prev => { - const newResults = [...prev] - newResults[i] = { - ...newResults[i], - status: 'duplicate', - error: '该凭据已存在', - email: existingCred?.email || undefined - } - return newResults - }) - setProgress({ current: i + 1, total: credentials.length }) - continue - } - - // 更新状态为验活中 - setResults(prev => { - const newResults = [...prev] - newResults[i] = { ...newResults[i], status: 'verifying' } - return newResults - }) - - let addedCredId: number | null = null - - try { - // 添加凭据 - const clientId = cred.clientId?.trim() || undefined - const clientSecret = cred.clientSecret?.trim() || undefined - const authMethod = clientId && clientSecret ? 'idc' : 'social' - - // idc 模式下必须同时提供 clientId 和 clientSecret - if (authMethod === 'social' && (clientId || clientSecret)) { - throw new Error('idc 模式需要同时提供 clientId 和 clientSecret') - } - - const addedCred = await addCredential({ - refreshToken: token, - authMethod, - region: cred.authRegion?.trim() || cred.region?.trim() || undefined, - apiRegion: cred.apiRegion?.trim() || undefined, - clientId, - clientSecret, - priority: cred.priority || 0, - machineId: cred.machineId?.trim() || undefined, - }) - - addedCredId = addedCred.credentialId - - // 延迟 1 秒 - await new Promise(resolve => setTimeout(resolve, 1000)) - - // 验活 - const balance = await getCredentialBalance(addedCred.credentialId) - - // 验活成功 - successCount++ - existingTokenHashes.add(tokenHash) - setCurrentProcessing(addedCred.email ? `验活成功: ${addedCred.email}` : `验活成功: 凭据 ${i + 1}`) - setResults(prev => { - const newResults = [...prev] - newResults[i] = { - ...newResults[i], - status: 'verified', - usage: `${balance.currentUsage}/${balance.usageLimit}`, - email: addedCred.email || undefined, - credentialId: addedCred.credentialId - } - return newResults - }) - } catch (error) { - // 验活失败,尝试回滚(先禁用再删除) - let rollbackStatus: VerificationResult['rollbackStatus'] = 'skipped' - let rollbackError: string | undefined - - if (addedCredId) { - const rollbackResult = await rollbackCredential(addedCredId) - if (rollbackResult.success) { - rollbackStatus = 'success' - rollbackSuccessCount++ - } else { - rollbackStatus = 'failed' - rollbackFailedCount++ - rollbackError = rollbackResult.error - } - } else { - rollbackSkippedCount++ - } - - failCount++ - setResults(prev => { - const newResults = [...prev] - newResults[i] = { - ...newResults[i], - status: 'failed', - error: extractErrorMessage(error), - email: undefined, - rollbackStatus, - rollbackError, - } - return newResults - }) - } - - setProgress({ current: i + 1, total: credentials.length }) - } - - // 显示结果 - if (failCount === 0 && duplicateCount === 0) { - toast.success(`成功导入并验活 ${successCount} 个凭据`) - } else { - const failureSummary = failCount > 0 - ? `,失败 ${failCount} 个(已排除 ${rollbackSuccessCount},未排除 ${rollbackFailedCount},无需排除 ${rollbackSkippedCount})` - : '' - toast.info(`验活完成:成功 ${successCount} 个,重复 ${duplicateCount} 个${failureSummary}`) - - if (rollbackFailedCount > 0) { - toast.warning(`有 ${rollbackFailedCount} 个失败凭据回滚未完成,请手动禁用并删除`) - } - } - } catch (error) { - toast.error('导入失败: ' + extractErrorMessage(error)) - } finally { - setImporting(false) - } - } - - const getStatusIcon = (status: VerificationResult['status']) => { - switch (status) { - case 'pending': - return
- case 'checking': - case 'verifying': - return - case 'verified': - return - case 'duplicate': - return - case 'failed': - return - } - } - - const getStatusText = (result: VerificationResult) => { - switch (result.status) { - case 'pending': - return '等待中' - case 'checking': - return '检查重复...' - case 'verifying': - return '验活中...' - case 'verified': - return '验活成功' - case 'duplicate': - return '重复凭据' - case 'failed': - if (result.rollbackStatus === 'success') return '验活失败(已排除)' - if (result.rollbackStatus === 'failed') return '验活失败(未排除)' - return '验活失败(未创建)' - } - } - - return ( - { - // 关闭时清空表单(但不在导入过程中清空) - if (!newOpen && !importing) { - resetForm() - } - onOpenChange(newOpen) - }} - > - - - 批量导入凭据(自动验活) - - -
-
- -