import { useState, useMemo } 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 KamImportDialogProps { open: boolean onOpenChange: (open: boolean) => void } // KAM 导出 JSON 中的账号结构 interface KamAccount { email?: string userId?: string | null nickname?: string credentials: { refreshToken: string clientId?: string clientSecret?: string region?: string authMethod?: string startUrl?: string } machineId?: string status?: string } interface VerificationResult { index: number status: 'pending' | 'checking' | 'verifying' | 'verified' | 'duplicate' | 'failed' | 'skipped' error?: string usage?: string email?: string credentialId?: number rollbackStatus?: 'success' | 'failed' | 'skipped' rollbackError?: string } // 校验元素是否为有效的 KAM 账号结构 function isValidKamAccount(item: unknown): item is KamAccount { if (typeof item !== 'object' || item === null) return false const obj = item as Record if (typeof obj.credentials !== 'object' || obj.credentials === null) return false const cred = obj.credentials as Record return typeof cred.refreshToken === 'string' && cred.refreshToken.trim().length > 0 } // 解析 KAM 导出 JSON,支持单账号和多账号格式 function parseKamJson(raw: string): KamAccount[] { const parsed = JSON.parse(raw) let rawItems: unknown[] // 标准 KAM 导出格式:{ version, accounts: [...] } if (parsed.accounts && Array.isArray(parsed.accounts)) { rawItems = parsed.accounts } // 兜底:如果直接是账号数组 else if (Array.isArray(parsed)) { rawItems = parsed } // 单个账号对象(有 credentials 字段) else if (parsed.credentials && typeof parsed.credentials === 'object') { rawItems = [parsed] } else { throw new Error('无法识别的 KAM JSON 格式') } const validAccounts = rawItems.filter(isValidKamAccount) if (rawItems.length > 0 && validAccounts.length === 0) { throw new Error(`共 ${rawItems.length} 条记录,但均缺少有效的 credentials.refreshToken`) } if (validAccounts.length < rawItems.length) { const skipped = rawItems.length - validAccounts.length console.warn(`KAM 导入:跳过 ${skipped} 条缺少有效 credentials.refreshToken 的记录`) } return validAccounts } export function KamImportDialog({ open, onOpenChange }: KamImportDialogProps) { const [jsonInput, setJsonInput] = useState('') const [importing, setImporting] = useState(false) const [skipErrorAccounts, setSkipErrorAccounts] = useState(true) 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 handleImport = async () => { // 先单独解析 JSON,给出精准的错误提示 let validAccounts: KamAccount[] try { const accounts = parseKamJson(jsonInput) if (accounts.length === 0) { toast.error('没有可导入的账号') return } validAccounts = accounts.filter(a => a.credentials?.refreshToken) if (validAccounts.length === 0) { toast.error('没有包含有效 refreshToken 的账号') return } } catch (error) { toast.error('JSON 格式错误: ' + extractErrorMessage(error)) return } try { setImporting(true) setProgress({ current: 0, total: validAccounts.length }) // 初始化结果,标记 error 状态的账号 const initialResults: VerificationResult[] = validAccounts.map((account, i) => { if (skipErrorAccounts && account.status === 'error') { return { index: i + 1, status: 'skipped' as const, email: account.email || account.nickname } } return { index: i + 1, status: 'pending' as const, email: account.email || account.nickname } }) setResults(initialResults) // 重复检测 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 skippedCount = 0 for (let i = 0; i < validAccounts.length; i++) { const account = validAccounts[i] // 跳过 error 状态的账号 if (skipErrorAccounts && account.status === 'error') { skippedCount++ setProgress({ current: i + 1, total: validAccounts.length }) continue } const cred = account.credentials const token = cred.refreshToken.trim() const tokenHash = await sha256Hex(token) setCurrentProcessing(`正在处理 ${account.email || account.nickname || `账号 ${i + 1}`}`) setResults(prev => { const next = [...prev] next[i] = { ...next[i], status: 'checking' } return next }) // 检查重复 if (existingTokenHashes.has(tokenHash)) { duplicateCount++ const existingCred = existingCredentials?.credentials.find(c => c.refreshTokenHash === tokenHash) setResults(prev => { const next = [...prev] next[i] = { ...next[i], status: 'duplicate', error: '该凭据已存在', email: existingCred?.email || account.email } return next }) setProgress({ current: i + 1, total: validAccounts.length }) continue } // 验活中 setResults(prev => { const next = [...prev] next[i] = { ...next[i], status: 'verifying' } return next }) 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.region?.trim() || undefined, clientId, clientSecret, machineId: account.machineId?.trim() || undefined, }) addedCredId = addedCred.credentialId await new Promise(resolve => setTimeout(resolve, 1000)) const balance = await getCredentialBalance(addedCred.credentialId) successCount++ existingTokenHashes.add(tokenHash) setCurrentProcessing(`验活成功: ${addedCred.email || account.email || `账号 ${i + 1}`}`) setResults(prev => { const next = [...prev] next[i] = { ...next[i], status: 'verified', usage: `${balance.currentUsage}/${balance.usageLimit}`, email: addedCred.email || account.email, credentialId: addedCred.credentialId, } return next }) } catch (error) { let rollbackStatus: VerificationResult['rollbackStatus'] = 'skipped' let rollbackError: string | undefined if (addedCredId) { const result = await rollbackCredential(addedCredId) if (result.success) { rollbackStatus = 'success' } else { rollbackStatus = 'failed' rollbackError = result.error } } failCount++ setResults(prev => { const next = [...prev] next[i] = { ...next[i], status: 'failed', error: extractErrorMessage(error), rollbackStatus, rollbackError, } return next }) } setProgress({ current: i + 1, total: validAccounts.length }) } // 汇总 const parts: string[] = [] if (successCount > 0) parts.push(`成功 ${successCount}`) if (duplicateCount > 0) parts.push(`重复 ${duplicateCount}`) if (failCount > 0) parts.push(`失败 ${failCount}`) if (skippedCount > 0) parts.push(`跳过 ${skippedCount}`) if (failCount === 0 && duplicateCount === 0 && skippedCount === 0) { toast.success(`成功导入并验活 ${successCount} 个凭据`) } else { toast.info(`导入完成:${parts.join(',')}`) } } 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 'skipped': 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 'skipped': return '已跳过(error 状态)' case 'failed': if (result.rollbackStatus === 'success') return '验活失败(已排除)' if (result.rollbackStatus === 'failed') return '验活失败(未排除)' return '验活失败(未创建)' } } // 预览解析结果 const { previewAccounts, parseError } = useMemo(() => { if (!jsonInput.trim()) return { previewAccounts: [] as KamAccount[], parseError: '' } try { return { previewAccounts: parseKamJson(jsonInput), parseError: '' } } catch (e) { return { previewAccounts: [] as KamAccount[], parseError: extractErrorMessage(e) } } }, [jsonInput]) const errorAccountCount = previewAccounts.filter(a => a.status === 'error').length return ( { if (!newOpen && importing) return if (!newOpen) resetForm() onOpenChange(newOpen) }} > KAM 账号导入(自动验活)