#!/usr/bin/env node /** * 增强版数据导出/导入工具 * 支持加密数据的处理 */ const fs = require('fs').promises const crypto = require('crypto') const redis = require('../src/models/redis') const logger = require('../src/utils/logger') const readline = require('readline') const config = require('../config/config') // 解析命令行参数 const args = process.argv.slice(2) const command = args[0] const params = {} args.slice(1).forEach((arg) => { const [key, value] = arg.split('=') params[key.replace('--', '')] = value || true }) // 创建 readline 接口 const rl = readline.createInterface({ input: process.stdin, output: process.stdout }) async function askConfirmation(question) { return new Promise((resolve) => { rl.question(`${question} (yes/no): `, (answer) => { resolve(answer.toLowerCase() === 'yes' || answer.toLowerCase() === 'y') }) }) } // Claude 账户解密函数 function decryptClaudeData(encryptedData) { if (!encryptedData || !config.security.encryptionKey) { return encryptedData } try { if (encryptedData.includes(':')) { const parts = encryptedData.split(':') const key = crypto.scryptSync(config.security.encryptionKey, 'salt', 32) const iv = Buffer.from(parts[0], 'hex') const encrypted = parts[1] const decipher = crypto.createDecipheriv('aes-256-cbc', key, iv) let decrypted = decipher.update(encrypted, 'hex', 'utf8') decrypted += decipher.final('utf8') return decrypted } return encryptedData } catch (error) { logger.warn(`⚠️ Failed to decrypt data: ${error.message}`) return encryptedData } } // Gemini 账户解密函数 function decryptGeminiData(encryptedData) { if (!encryptedData || !config.security.encryptionKey) { return encryptedData } try { if (encryptedData.includes(':')) { const parts = encryptedData.split(':') const key = crypto.scryptSync(config.security.encryptionKey, 'gemini-account-salt', 32) const iv = Buffer.from(parts[0], 'hex') const encrypted = parts[1] const decipher = crypto.createDecipheriv('aes-256-cbc', key, iv) let decrypted = decipher.update(encrypted, 'hex', 'utf8') decrypted += decipher.final('utf8') return decrypted } return encryptedData } catch (error) { logger.warn(`⚠️ Failed to decrypt data: ${error.message}`) return encryptedData } } // API Key 哈希函数(与apiKeyService保持一致) function hashApiKey(apiKey) { if (!apiKey || !config.security.encryptionKey) { return apiKey } return crypto .createHash('sha256') .update(apiKey + config.security.encryptionKey) .digest('hex') } // 检查是否为明文API Key(通过格式判断,不依赖前缀) function isPlaintextApiKey(apiKey) { if (!apiKey || typeof apiKey !== 'string') { return false } // SHA256哈希值固定为64个十六进制字符,如果是哈希值则返回false if (apiKey.length === 64 && /^[a-f0-9]+$/i.test(apiKey)) { return false // 已经是哈希值 } // 其他情况都认为是明文API Key(包括sk-ant-、cr_、自定义前缀等) return true } // 数据加密函数(用于导入) function encryptClaudeData(data) { if (!data || !config.security.encryptionKey) { return data } const key = crypto.scryptSync(config.security.encryptionKey, 'salt', 32) const iv = crypto.randomBytes(16) const cipher = crypto.createCipheriv('aes-256-cbc', key, iv) let encrypted = cipher.update(data, 'utf8', 'hex') encrypted += cipher.final('hex') return `${iv.toString('hex')}:${encrypted}` } function encryptGeminiData(data) { if (!data || !config.security.encryptionKey) { return data } const key = crypto.scryptSync(config.security.encryptionKey, 'gemini-account-salt', 32) const iv = crypto.randomBytes(16) const cipher = crypto.createCipheriv('aes-256-cbc', key, iv) let encrypted = cipher.update(data, 'utf8', 'hex') encrypted += cipher.final('hex') return `${iv.toString('hex')}:${encrypted}` } // 导出使用统计数据 async function exportUsageStats(keyId) { try { const stats = { total: {}, daily: {}, monthly: {}, hourly: {}, models: {} } // 导出总统计 const totalKey = `usage:${keyId}` const totalData = await redis.client.hgetall(totalKey) if (totalData && Object.keys(totalData).length > 0) { stats.total = totalData } // 导出每日统计(最近30天) const today = new Date() for (let i = 0; i < 30; i++) { const date = new Date(today) date.setDate(date.getDate() - i) const dateStr = date.toISOString().split('T')[0] const dailyKey = `usage:daily:${keyId}:${dateStr}` const dailyData = await redis.client.hgetall(dailyKey) if (dailyData && Object.keys(dailyData).length > 0) { stats.daily[dateStr] = dailyData } } // 导出每月统计(最近12个月) for (let i = 0; i < 12; i++) { const date = new Date(today) date.setMonth(date.getMonth() - i) const monthStr = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}` const monthlyKey = `usage:monthly:${keyId}:${monthStr}` const monthlyData = await redis.client.hgetall(monthlyKey) if (monthlyData && Object.keys(monthlyData).length > 0) { stats.monthly[monthStr] = monthlyData } } // 导出小时统计(最近24小时) for (let i = 0; i < 24; i++) { const date = new Date(today) date.setHours(date.getHours() - i) const dateStr = date.toISOString().split('T')[0] const hour = String(date.getHours()).padStart(2, '0') const hourKey = `${dateStr}:${hour}` const hourlyKey = `usage:hourly:${keyId}:${hourKey}` const hourlyData = await redis.client.hgetall(hourlyKey) if (hourlyData && Object.keys(hourlyData).length > 0) { stats.hourly[hourKey] = hourlyData } } // 导出模型统计 // 每日模型统计 const modelDailyPattern = `usage:${keyId}:model:daily:*` const modelDailyKeys = await redis.client.keys(modelDailyPattern) for (const key of modelDailyKeys) { const match = key.match(/usage:.+:model:daily:(.+):(\d{4}-\d{2}-\d{2})$/) if (match) { const model = match[1] const date = match[2] const data = await redis.client.hgetall(key) if (data && Object.keys(data).length > 0) { if (!stats.models[model]) { stats.models[model] = { daily: {}, monthly: {} } } stats.models[model].daily[date] = data } } } // 每月模型统计 const modelMonthlyPattern = `usage:${keyId}:model:monthly:*` const modelMonthlyKeys = await redis.client.keys(modelMonthlyPattern) for (const key of modelMonthlyKeys) { const match = key.match(/usage:.+:model:monthly:(.+):(\d{4}-\d{2})$/) if (match) { const model = match[1] const month = match[2] const data = await redis.client.hgetall(key) if (data && Object.keys(data).length > 0) { if (!stats.models[model]) { stats.models[model] = { daily: {}, monthly: {} } } stats.models[model].monthly[month] = data } } } return stats } catch (error) { logger.warn(`⚠️ Failed to export usage stats for ${keyId}: ${error.message}`) return null } } // 导入使用统计数据 async function importUsageStats(keyId, stats) { try { if (!stats) { return } const pipeline = redis.client.pipeline() let importCount = 0 // 导入总统计 if (stats.total && Object.keys(stats.total).length > 0) { for (const [field, value] of Object.entries(stats.total)) { pipeline.hset(`usage:${keyId}`, field, value) } importCount++ } // 导入每日统计 if (stats.daily) { for (const [date, data] of Object.entries(stats.daily)) { for (const [field, value] of Object.entries(data)) { pipeline.hset(`usage:daily:${keyId}:${date}`, field, value) } importCount++ } } // 导入每月统计 if (stats.monthly) { for (const [month, data] of Object.entries(stats.monthly)) { for (const [field, value] of Object.entries(data)) { pipeline.hset(`usage:monthly:${keyId}:${month}`, field, value) } importCount++ } } // 导入小时统计 if (stats.hourly) { for (const [hour, data] of Object.entries(stats.hourly)) { for (const [field, value] of Object.entries(data)) { pipeline.hset(`usage:hourly:${keyId}:${hour}`, field, value) } importCount++ } } // 导入模型统计 if (stats.models) { for (const [model, modelStats] of Object.entries(stats.models)) { // 每日模型统计 if (modelStats.daily) { for (const [date, data] of Object.entries(modelStats.daily)) { for (const [field, value] of Object.entries(data)) { pipeline.hset(`usage:${keyId}:model:daily:${model}:${date}`, field, value) } importCount++ } } // 每月模型统计 if (modelStats.monthly) { for (const [month, data] of Object.entries(modelStats.monthly)) { for (const [field, value] of Object.entries(data)) { pipeline.hset(`usage:${keyId}:model:monthly:${model}:${month}`, field, value) } importCount++ } } } } await pipeline.exec() logger.info(` 📊 Imported ${importCount} usage stat entries for API Key ${keyId}`) } catch (error) { logger.warn(`⚠️ Failed to import usage stats for ${keyId}: ${error.message}`) } } // 数据脱敏函数 function sanitizeData(data, type) { const sanitized = { ...data } switch (type) { case 'apikey': if (sanitized.apiKey) { sanitized.apiKey = `${sanitized.apiKey.substring(0, 10)}...[REDACTED]` } break case 'claude_account': if (sanitized.email) { sanitized.email = '[REDACTED]' } if (sanitized.password) { sanitized.password = '[REDACTED]' } if (sanitized.accessToken) { sanitized.accessToken = '[REDACTED]' } if (sanitized.refreshToken) { sanitized.refreshToken = '[REDACTED]' } if (sanitized.claudeAiOauth) { sanitized.claudeAiOauth = '[REDACTED]' } if (sanitized.proxyPassword) { sanitized.proxyPassword = '[REDACTED]' } break case 'gemini_account': if (sanitized.geminiOauth) { sanitized.geminiOauth = '[REDACTED]' } if (sanitized.accessToken) { sanitized.accessToken = '[REDACTED]' } if (sanitized.refreshToken) { sanitized.refreshToken = '[REDACTED]' } if (sanitized.proxyPassword) { sanitized.proxyPassword = '[REDACTED]' } break case 'admin': if (sanitized.password) { sanitized.password = '[REDACTED]' } break } return sanitized } // 导出数据 async function exportData() { try { const outputFile = params.output || `backup-${new Date().toISOString().split('T')[0]}.json` const types = params.types ? params.types.split(',') : ['all'] const shouldSanitize = params.sanitize === true const shouldDecrypt = params.decrypt !== false // 默认解密 logger.info('🔄 Starting data export...') logger.info(`📁 Output file: ${outputFile}`) logger.info(`📋 Data types: ${types.join(', ')}`) logger.info(`🔒 Sanitize sensitive data: ${shouldSanitize ? 'YES' : 'NO'}`) logger.info(`🔓 Decrypt data: ${shouldDecrypt ? 'YES' : 'NO'}`) await redis.connect() logger.success('✅ Connected to Redis') const exportDataObj = { metadata: { version: '2.0', exportDate: new Date().toISOString(), sanitized: shouldSanitize, decrypted: shouldDecrypt, types }, data: {} } // 导出 API Keys if (types.includes('all') || types.includes('apikeys')) { logger.info('📤 Exporting API Keys...') const keys = await redis.client.keys('apikey:*') const apiKeys = [] for (const key of keys) { if (key === 'apikey:hash_map') { continue } const data = await redis.client.hgetall(key) if (data && Object.keys(data).length > 0) { // 获取该 API Key 的 ID const keyId = data.id // 导出使用统计数据 if (keyId && (types.includes('all') || types.includes('stats'))) { data.usageStats = await exportUsageStats(keyId) } apiKeys.push(shouldSanitize ? sanitizeData(data, 'apikey') : data) } } exportDataObj.data.apiKeys = apiKeys logger.success(`✅ Exported ${apiKeys.length} API Keys`) } // 导出 Claude 账户 if (types.includes('all') || types.includes('accounts')) { logger.info('📤 Exporting Claude accounts...') const keys = await redis.client.keys('claude:account:*') logger.info(`Found ${keys.length} Claude account keys in Redis`) const accounts = [] for (const key of keys) { const data = await redis.client.hgetall(key) if (data && Object.keys(data).length > 0) { // 解密敏感字段 if (shouldDecrypt && !shouldSanitize) { if (data.email) { data.email = decryptClaudeData(data.email) } if (data.password) { data.password = decryptClaudeData(data.password) } if (data.accessToken) { data.accessToken = decryptClaudeData(data.accessToken) } if (data.refreshToken) { data.refreshToken = decryptClaudeData(data.refreshToken) } if (data.claudeAiOauth) { const decrypted = decryptClaudeData(data.claudeAiOauth) try { data.claudeAiOauth = JSON.parse(decrypted) } catch (e) { data.claudeAiOauth = decrypted } } } accounts.push(shouldSanitize ? sanitizeData(data, 'claude_account') : data) } } exportDataObj.data.claudeAccounts = accounts logger.success(`✅ Exported ${accounts.length} Claude accounts`) // 导出 Gemini 账户 logger.info('📤 Exporting Gemini accounts...') const geminiKeys = await redis.client.keys('gemini_account:*') logger.info(`Found ${geminiKeys.length} Gemini account keys in Redis`) const geminiAccounts = [] for (const key of geminiKeys) { const data = await redis.client.hgetall(key) if (data && Object.keys(data).length > 0) { // 解密敏感字段 if (shouldDecrypt && !shouldSanitize) { if (data.geminiOauth) { const decrypted = decryptGeminiData(data.geminiOauth) try { data.geminiOauth = JSON.parse(decrypted) } catch (e) { data.geminiOauth = decrypted } } if (data.accessToken) { data.accessToken = decryptGeminiData(data.accessToken) } if (data.refreshToken) { data.refreshToken = decryptGeminiData(data.refreshToken) } } geminiAccounts.push(shouldSanitize ? sanitizeData(data, 'gemini_account') : data) } } exportDataObj.data.geminiAccounts = geminiAccounts logger.success(`✅ Exported ${geminiAccounts.length} Gemini accounts`) } // 导出管理员 if (types.includes('all') || types.includes('admins')) { logger.info('📤 Exporting admins...') const keys = await redis.client.keys('admin:*') const admins = [] for (const key of keys) { if (key.includes('admin_username:')) { continue } const data = await redis.client.hgetall(key) if (data && Object.keys(data).length > 0) { admins.push(shouldSanitize ? sanitizeData(data, 'admin') : data) } } exportDataObj.data.admins = admins logger.success(`✅ Exported ${admins.length} admins`) } // 导出全局模型统计(如果需要) if (types.includes('all') || types.includes('stats')) { logger.info('📤 Exporting global model statistics...') const globalStats = { daily: {}, monthly: {}, hourly: {} } // 导出全局每日模型统计 const globalDailyPattern = 'usage:model:daily:*' const globalDailyKeys = await redis.client.keys(globalDailyPattern) for (const key of globalDailyKeys) { const match = key.match(/usage:model:daily:(.+):(\d{4}-\d{2}-\d{2})$/) if (match) { const model = match[1] const date = match[2] const data = await redis.client.hgetall(key) if (data && Object.keys(data).length > 0) { if (!globalStats.daily[date]) { globalStats.daily[date] = {} } globalStats.daily[date][model] = data } } } // 导出全局每月模型统计 const globalMonthlyPattern = 'usage:model:monthly:*' const globalMonthlyKeys = await redis.client.keys(globalMonthlyPattern) for (const key of globalMonthlyKeys) { const match = key.match(/usage:model:monthly:(.+):(\d{4}-\d{2})$/) if (match) { const model = match[1] const month = match[2] const data = await redis.client.hgetall(key) if (data && Object.keys(data).length > 0) { if (!globalStats.monthly[month]) { globalStats.monthly[month] = {} } globalStats.monthly[month][model] = data } } } // 导出全局每小时模型统计 const globalHourlyPattern = 'usage:model:hourly:*' const globalHourlyKeys = await redis.client.keys(globalHourlyPattern) for (const key of globalHourlyKeys) { const match = key.match(/usage:model:hourly:(.+):(\d{4}-\d{2}-\d{2}:\d{2})$/) if (match) { const model = match[1] const hour = match[2] const data = await redis.client.hgetall(key) if (data && Object.keys(data).length > 0) { if (!globalStats.hourly[hour]) { globalStats.hourly[hour] = {} } globalStats.hourly[hour][model] = data } } } exportDataObj.data.globalModelStats = globalStats logger.success('✅ Exported global model statistics') } // 写入文件 await fs.writeFile(outputFile, JSON.stringify(exportDataObj, null, 2)) // 显示导出摘要 console.log(`\n${'='.repeat(60)}`) console.log('✅ Export Complete!') console.log('='.repeat(60)) console.log(`Output file: ${outputFile}`) console.log(`File size: ${(await fs.stat(outputFile)).size} bytes`) if (exportDataObj.data.apiKeys) { console.log(`API Keys: ${exportDataObj.data.apiKeys.length}`) } if (exportDataObj.data.claudeAccounts) { console.log(`Claude Accounts: ${exportDataObj.data.claudeAccounts.length}`) } if (exportDataObj.data.geminiAccounts) { console.log(`Gemini Accounts: ${exportDataObj.data.geminiAccounts.length}`) } if (exportDataObj.data.admins) { console.log(`Admins: ${exportDataObj.data.admins.length}`) } console.log('='.repeat(60)) if (shouldSanitize) { logger.warn('⚠️ Sensitive data has been sanitized in this export.') } if (shouldDecrypt) { logger.info('🔓 Encrypted data has been decrypted for portability.') } } catch (error) { logger.error('💥 Export failed:', error) process.exit(1) } finally { await redis.disconnect() rl.close() } } // 显示帮助信息 function showHelp() { console.log(` Enhanced Data Transfer Tool for Claude Relay Service This tool handles encrypted data export/import between environments. Usage: node scripts/data-transfer-enhanced.js [options] Commands: export Export data from Redis to a JSON file import Import data from a JSON file to Redis Export Options: --output=FILE Output filename (default: backup-YYYY-MM-DD.json) --types=TYPE,... Data types: apikeys,accounts,admins,stats,all (default: all) stats: Include usage statistics with API keys --sanitize Remove sensitive data from export --decrypt=false Keep data encrypted (default: true - decrypt for portability) Import Options: --input=FILE Input filename (required) --force Overwrite existing data without asking --skip-conflicts Skip conflicting data without asking Important Notes: - The tool automatically handles encryption/decryption during import - If importing decrypted data, it will be re-encrypted automatically - If importing encrypted data, it will be stored as-is - Sanitized exports cannot be properly imported (missing sensitive data) - Automatic handling of plaintext API Keys * Uses your configured API_KEY_PREFIX from config (sk-, cr_, etc.) * Automatically detects plaintext vs hashed API Keys by format * Plaintext API Keys are automatically hashed during import * Hash mappings are created correctly for plaintext keys * Supports custom prefixes and legacy format detection * No manual conversion needed - just import your backup file Examples: # Export all data with decryption (for migration) node scripts/data-transfer-enhanced.js export # Export without decrypting (for backup) node scripts/data-transfer-enhanced.js export --decrypt=false # Import data (auto-handles encryption and plaintext API keys) node scripts/data-transfer-enhanced.js import --input=backup.json # Import with force overwrite node scripts/data-transfer-enhanced.js import --input=backup.json --force `) } // 导入数据 async function importData() { try { const inputFile = params.input if (!inputFile) { logger.error('❌ Please specify input file with --input=filename.json') process.exit(1) } const forceOverwrite = params.force === true const skipConflicts = params['skip-conflicts'] === true logger.info('🔄 Starting data import...') logger.info(`📁 Input file: ${inputFile}`) logger.info( `⚡ Mode: ${forceOverwrite ? 'FORCE OVERWRITE' : skipConflicts ? 'SKIP CONFLICTS' : 'ASK ON CONFLICT'}` ) // 读取文件 const fileContent = await fs.readFile(inputFile, 'utf8') const importDataObj = JSON.parse(fileContent) // 验证文件格式 if (!importDataObj.metadata || !importDataObj.data) { logger.error('❌ Invalid backup file format') process.exit(1) } logger.info(`📅 Backup date: ${importDataObj.metadata.exportDate}`) logger.info(`🔒 Sanitized: ${importDataObj.metadata.sanitized ? 'YES' : 'NO'}`) logger.info(`🔓 Decrypted: ${importDataObj.metadata.decrypted ? 'YES' : 'NO'}`) if (importDataObj.metadata.sanitized) { logger.warn('⚠️ This backup contains sanitized data. Sensitive fields will be missing!') const proceed = await askConfirmation('Continue with sanitized data?') if (!proceed) { logger.info('❌ Import cancelled') return } } // 显示导入摘要 console.log(`\n${'='.repeat(60)}`) console.log('📋 Import Summary:') console.log('='.repeat(60)) if (importDataObj.data.apiKeys) { console.log(`API Keys to import: ${importDataObj.data.apiKeys.length}`) } if (importDataObj.data.claudeAccounts) { console.log(`Claude Accounts to import: ${importDataObj.data.claudeAccounts.length}`) } if (importDataObj.data.geminiAccounts) { console.log(`Gemini Accounts to import: ${importDataObj.data.geminiAccounts.length}`) } if (importDataObj.data.admins) { console.log(`Admins to import: ${importDataObj.data.admins.length}`) } console.log(`${'='.repeat(60)}\n`) // 确认导入 const confirmed = await askConfirmation('⚠️ Proceed with import?') if (!confirmed) { logger.info('❌ Import cancelled') return } // 连接 Redis await redis.connect() logger.success('✅ Connected to Redis') const stats = { imported: 0, skipped: 0, errors: 0 } // 导入 API Keys if (importDataObj.data.apiKeys) { logger.info('\n📥 Importing API Keys...') for (const apiKey of importDataObj.data.apiKeys) { try { const exists = await redis.client.exists(`apikey:${apiKey.id}`) if (exists && !forceOverwrite) { if (skipConflicts) { logger.warn(`⏭️ Skipped existing API Key: ${apiKey.name} (${apiKey.id})`) stats.skipped++ continue } else { const overwrite = await askConfirmation( `API Key "${apiKey.name}" (${apiKey.id}) exists. Overwrite?` ) if (!overwrite) { stats.skipped++ continue } } } // 保存使用统计数据以便单独导入 const { usageStats } = apiKey // 从apiKey对象中删除usageStats字段,避免存储到主键中 const apiKeyData = { ...apiKey } delete apiKeyData.usageStats // 检查并处理API Key哈希 let plainTextApiKey = null let hashedApiKey = null if (apiKeyData.apiKey && isPlaintextApiKey(apiKeyData.apiKey)) { // 如果是明文API Key,保存明文并计算哈希 plainTextApiKey = apiKeyData.apiKey hashedApiKey = hashApiKey(plainTextApiKey) logger.info(`🔐 Detected plaintext API Key for: ${apiKey.name} (${apiKey.id})`) } else if (apiKeyData.apiKey) { // 如果已经是哈希值,直接使用 hashedApiKey = apiKeyData.apiKey logger.info(`🔍 Using existing hashed API Key for: ${apiKey.name} (${apiKey.id})`) } // API Key字段始终存储哈希值 if (hashedApiKey) { apiKeyData.apiKey = hashedApiKey } // 使用 hset 存储到哈希表 const pipeline = redis.client.pipeline() for (const [field, value] of Object.entries(apiKeyData)) { pipeline.hset(`apikey:${apiKey.id}`, field, value) } await pipeline.exec() // 更新哈希映射:hash_map的key必须是哈希值 if (!importDataObj.metadata.sanitized && hashedApiKey) { await redis.client.hset('apikey:hash_map', hashedApiKey, apiKey.id) logger.info( `📝 Updated hash mapping: ${hashedApiKey.substring(0, 8)}... -> ${apiKey.id}` ) } // 导入使用统计数据 if (usageStats) { await importUsageStats(apiKey.id, usageStats) } logger.success(`✅ Imported API Key: ${apiKey.name} (${apiKey.id})`) stats.imported++ } catch (error) { logger.error(`❌ Failed to import API Key ${apiKey.id}:`, error.message) stats.errors++ } } } // 导入 Claude 账户 if (importDataObj.data.claudeAccounts) { logger.info('\n📥 Importing Claude accounts...') for (const account of importDataObj.data.claudeAccounts) { try { const exists = await redis.client.exists(`claude:account:${account.id}`) if (exists && !forceOverwrite) { if (skipConflicts) { logger.warn(`⏭️ Skipped existing Claude account: ${account.name} (${account.id})`) stats.skipped++ continue } else { const overwrite = await askConfirmation( `Claude account "${account.name}" (${account.id}) exists. Overwrite?` ) if (!overwrite) { stats.skipped++ continue } } } // 复制账户数据以避免修改原始数据 const accountData = { ...account } // 如果数据已解密且不是脱敏数据,需要重新加密 if (importDataObj.metadata.decrypted && !importDataObj.metadata.sanitized) { logger.info(`🔐 Re-encrypting sensitive data for Claude account: ${account.name}`) if (accountData.email) { accountData.email = encryptClaudeData(accountData.email) } if (accountData.password) { accountData.password = encryptClaudeData(accountData.password) } if (accountData.accessToken) { accountData.accessToken = encryptClaudeData(accountData.accessToken) } if (accountData.refreshToken) { accountData.refreshToken = encryptClaudeData(accountData.refreshToken) } if (accountData.claudeAiOauth) { // 如果是对象,先序列化再加密 const oauthStr = typeof accountData.claudeAiOauth === 'object' ? JSON.stringify(accountData.claudeAiOauth) : accountData.claudeAiOauth accountData.claudeAiOauth = encryptClaudeData(oauthStr) } } // 使用 hset 存储到哈希表 const pipeline = redis.client.pipeline() for (const [field, value] of Object.entries(accountData)) { if (field === 'claudeAiOauth' && typeof value === 'object') { // 确保对象被序列化 pipeline.hset(`claude:account:${account.id}`, field, JSON.stringify(value)) } else { pipeline.hset(`claude:account:${account.id}`, field, value) } } await pipeline.exec() logger.success(`✅ Imported Claude account: ${account.name} (${account.id})`) stats.imported++ } catch (error) { logger.error(`❌ Failed to import Claude account ${account.id}:`, error.message) stats.errors++ } } } // 导入 Gemini 账户 if (importDataObj.data.geminiAccounts) { logger.info('\n📥 Importing Gemini accounts...') for (const account of importDataObj.data.geminiAccounts) { try { const exists = await redis.client.exists(`gemini_account:${account.id}`) if (exists && !forceOverwrite) { if (skipConflicts) { logger.warn(`⏭️ Skipped existing Gemini account: ${account.name} (${account.id})`) stats.skipped++ continue } else { const overwrite = await askConfirmation( `Gemini account "${account.name}" (${account.id}) exists. Overwrite?` ) if (!overwrite) { stats.skipped++ continue } } } // 复制账户数据以避免修改原始数据 const accountData = { ...account } // 如果数据已解密且不是脱敏数据,需要重新加密 if (importDataObj.metadata.decrypted && !importDataObj.metadata.sanitized) { logger.info(`🔐 Re-encrypting sensitive data for Gemini account: ${account.name}`) if (accountData.geminiOauth) { const oauthStr = typeof accountData.geminiOauth === 'object' ? JSON.stringify(accountData.geminiOauth) : accountData.geminiOauth accountData.geminiOauth = encryptGeminiData(oauthStr) } if (accountData.accessToken) { accountData.accessToken = encryptGeminiData(accountData.accessToken) } if (accountData.refreshToken) { accountData.refreshToken = encryptGeminiData(accountData.refreshToken) } } // 使用 hset 存储到哈希表 const pipeline = redis.client.pipeline() for (const [field, value] of Object.entries(accountData)) { pipeline.hset(`gemini_account:${account.id}`, field, value) } await pipeline.exec() logger.success(`✅ Imported Gemini account: ${account.name} (${account.id})`) stats.imported++ } catch (error) { logger.error(`❌ Failed to import Gemini account ${account.id}:`, error.message) stats.errors++ } } } // 导入管理员账户 if (importDataObj.data.admins) { logger.info('\n📥 Importing admins...') for (const admin of importDataObj.data.admins) { try { const exists = await redis.client.exists(`admin:${admin.id}`) if (exists && !forceOverwrite) { if (skipConflicts) { logger.warn(`⏭️ Skipped existing admin: ${admin.username} (${admin.id})`) stats.skipped++ continue } else { const overwrite = await askConfirmation( `Admin "${admin.username}" (${admin.id}) exists. Overwrite?` ) if (!overwrite) { stats.skipped++ continue } } } // 使用 hset 存储到哈希表 const pipeline = redis.client.pipeline() for (const [field, value] of Object.entries(admin)) { pipeline.hset(`admin:${admin.id}`, field, value) } await pipeline.exec() // 更新用户名映射 await redis.client.set(`admin_username:${admin.username}`, admin.id) logger.success(`✅ Imported admin: ${admin.username} (${admin.id})`) stats.imported++ } catch (error) { logger.error(`❌ Failed to import admin ${admin.id}:`, error.message) stats.errors++ } } } // 导入全局模型统计 if (importDataObj.data.globalModelStats) { logger.info('\n📥 Importing global model statistics...') try { const globalStats = importDataObj.data.globalModelStats const pipeline = redis.client.pipeline() let globalStatCount = 0 // 导入每日统计 if (globalStats.daily) { for (const [date, models] of Object.entries(globalStats.daily)) { for (const [model, data] of Object.entries(models)) { for (const [field, value] of Object.entries(data)) { pipeline.hset(`usage:model:daily:${model}:${date}`, field, value) } globalStatCount++ } } } // 导入每月统计 if (globalStats.monthly) { for (const [month, models] of Object.entries(globalStats.monthly)) { for (const [model, data] of Object.entries(models)) { for (const [field, value] of Object.entries(data)) { pipeline.hset(`usage:model:monthly:${model}:${month}`, field, value) } globalStatCount++ } } } // 导入每小时统计 if (globalStats.hourly) { for (const [hour, models] of Object.entries(globalStats.hourly)) { for (const [model, data] of Object.entries(models)) { for (const [field, value] of Object.entries(data)) { pipeline.hset(`usage:model:hourly:${model}:${hour}`, field, value) } globalStatCount++ } } } await pipeline.exec() logger.success(`✅ Imported ${globalStatCount} global model stat entries`) stats.imported += globalStatCount } catch (error) { logger.error('❌ Failed to import global model stats:', error.message) stats.errors++ } } // 显示导入结果 console.log(`\n${'='.repeat(60)}`) console.log('✅ Import Complete!') console.log('='.repeat(60)) console.log(`Successfully imported: ${stats.imported}`) console.log(`Skipped: ${stats.skipped}`) console.log(`Errors: ${stats.errors}`) console.log('='.repeat(60)) } catch (error) { logger.error('💥 Import failed:', error) process.exit(1) } finally { await redis.disconnect() rl.close() } } // 主函数 async function main() { if (!command || command === '--help' || command === 'help') { showHelp() process.exit(0) } switch (command) { case 'export': await exportData() break case 'import': await importData() break default: logger.error(`❌ Unknown command: ${command}`) showHelp() process.exit(1) } } // 运行 main().catch((error) => { logger.error('💥 Unexpected error:', error) process.exit(1) })