feat: 🎸 export csv from web and support hourly TTL of key

This commit is contained in:
yaogdu
2025-09-27 13:36:50 +08:00
committed by duyaoguang
parent c8b72b4eaa
commit d1bbc71796
7 changed files with 429 additions and 28 deletions

View File

@@ -84,16 +84,214 @@ function sanitizeData(data, type) {
return sanitized
}
// CSV 字段映射配置
const CSV_FIELD_MAPPING = {
// 基本信息
id: 'ID',
name: '名称',
description: '描述',
isActive: '状态',
createdAt: '创建时间',
lastUsedAt: '最后使用时间',
createdBy: '创建者',
// API Key 信息
apiKey: 'API密钥',
tokenLimit: '令牌限制',
// 过期设置
expirationMode: '过期模式',
expiresAt: '过期时间',
activationDays: '激活天数',
activationUnit: '激活单位',
isActivated: '已激活',
activatedAt: '激活时间',
// 权限设置
permissions: '服务权限',
// 限制设置
rateLimitWindow: '速率窗口(分钟)',
rateLimitRequests: '请求次数限制',
rateLimitCost: '费用限制(美元)',
concurrencyLimit: '并发限制',
dailyCostLimit: '日费用限制(美元)',
totalCostLimit: '总费用限制(美元)',
weeklyOpusCostLimit: '周Opus费用限制(美元)',
// 账户绑定
claudeAccountId: 'Claude专属账户',
claudeConsoleAccountId: 'Claude控制台账户',
geminiAccountId: 'Gemini专属账户',
openaiAccountId: 'OpenAI专属账户',
azureOpenaiAccountId: 'Azure OpenAI专属账户',
bedrockAccountId: 'Bedrock专属账户',
// 限制配置
enableModelRestriction: '启用模型限制',
restrictedModels: '限制的模型',
enableClientRestriction: '启用客户端限制',
allowedClients: '允许的客户端',
// 标签和用户
tags: '标签',
userId: '用户ID',
userUsername: '用户名',
// 其他信息
icon: '图标'
}
// 数据格式化函数
function formatCSVValue(key, value, shouldSanitize = false) {
if (!value || value === '' || value === 'null' || value === 'undefined') {
return ''
}
switch (key) {
case 'apiKey':
if (shouldSanitize && value.length > 10) {
return `${value.substring(0, 10)}...[已脱敏]`
}
return value
case 'isActive':
case 'isActivated':
case 'enableModelRestriction':
case 'enableClientRestriction':
return value === 'true' ? '是' : '否'
case 'expirationMode':
return value === 'activation' ? '首次使用后激活' : value === 'fixed' ? '固定时间' : value
case 'activationUnit':
return value === 'hours' ? '小时' : value === 'days' ? '天' : value
case 'permissions':
switch (value) {
case 'all':
return '全部服务'
case 'claude':
return '仅Claude'
case 'gemini':
return '仅Gemini'
case 'openai':
return '仅OpenAI'
default:
return value
}
case 'restrictedModels':
case 'allowedClients':
case 'tags':
try {
const parsed = JSON.parse(value)
return Array.isArray(parsed) ? parsed.join('; ') : value
} catch {
return value
}
case 'createdAt':
case 'lastUsedAt':
case 'activatedAt':
case 'expiresAt':
if (value) {
try {
return new Date(value).toLocaleString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
})
} catch {
return value
}
}
return ''
case 'rateLimitWindow':
case 'rateLimitRequests':
case 'concurrencyLimit':
case 'activationDays':
case 'tokenLimit':
return value === '0' || value === 0 ? '无限制' : value
case 'rateLimitCost':
case 'dailyCostLimit':
case 'totalCostLimit':
case 'weeklyOpusCostLimit':
return value === '0' || value === 0 ? '无限制' : `$${value}`
default:
return value
}
}
// 转义 CSV 字段
function escapeCSVField(field) {
if (field === null || field === undefined) {
return ''
}
const str = String(field)
// 如果包含逗号、引号或换行符,需要用引号包围
if (str.includes(',') || str.includes('"') || str.includes('\n') || str.includes('\r')) {
// 先转义引号(双引号变成两个双引号)
const escaped = str.replace(/"/g, '""')
return `"${escaped}"`
}
return str
}
// 转换数据为 CSV 格式
function convertToCSV(exportDataObj, shouldSanitize = false) {
if (!exportDataObj.data.apiKeys || exportDataObj.data.apiKeys.length === 0) {
throw new Error('CSV format only supports API Keys export. Please use --types=apikeys')
}
const { apiKeys } = exportDataObj.data
const fields = Object.keys(CSV_FIELD_MAPPING)
const headers = Object.values(CSV_FIELD_MAPPING)
// 生成标题行
const csvLines = [headers.map(escapeCSVField).join(',')]
// 生成数据行
for (const apiKey of apiKeys) {
const row = fields.map((field) => {
const value = formatCSVValue(field, apiKey[field], shouldSanitize)
return escapeCSVField(value)
})
csvLines.push(row.join(','))
}
return csvLines.join('\n')
}
// 导出数据
async function exportData() {
try {
const outputFile = params.output || `backup-${new Date().toISOString().split('T')[0]}.json`
const format = params.format || 'json'
const fileExtension = format === 'csv' ? '.csv' : '.json'
const defaultFileName = `backup-${new Date().toISOString().split('T')[0]}${fileExtension}`
const outputFile = params.output || defaultFileName
const types = params.types ? params.types.split(',') : ['all']
const shouldSanitize = params.sanitize === true
// CSV 格式验证
if (format === 'csv' && !types.includes('apikeys') && !types.includes('all')) {
logger.error('❌ CSV format only supports API Keys export. Please use --types=apikeys')
process.exit(1)
}
logger.info('🔄 Starting data export...')
logger.info(`📁 Output file: ${outputFile}`)
logger.info(`📋 Data types: ${types.join(', ')}`)
logger.info(`📄 Output format: ${format.toUpperCase()}`)
logger.info(`🔒 Sanitize sensitive data: ${shouldSanitize ? 'YES' : 'NO'}`)
// 连接 Redis
@@ -203,8 +401,16 @@ async function exportData() {
logger.success(`✅ Exported ${admins.length} admins`)
}
// 写入文件
await fs.writeFile(outputFile, JSON.stringify(exportData, null, 2))
// 根据格式写入文件
let fileContent
if (format === 'csv') {
fileContent = convertToCSV(exportDataObj, shouldSanitize)
// 添加 UTF-8 BOM 以便 Excel 正确识别中文
fileContent = `\ufeff${fileContent}`
await fs.writeFile(outputFile, fileContent, 'utf8')
} else {
await fs.writeFile(outputFile, JSON.stringify(exportDataObj, null, 2))
}
// 显示导出摘要
console.log(`\n${'='.repeat(60)}`)
@@ -471,8 +677,9 @@ Commands:
import Import data from a JSON file to Redis
Export Options:
--output=FILE Output filename (default: backup-YYYY-MM-DD.json)
--output=FILE Output filename (default: backup-YYYY-MM-DD.json/.csv)
--types=TYPE,... Data types to export: apikeys,accounts,admins,all (default: all)
--format=FORMAT Output format: json,csv (default: json)
--sanitize Remove sensitive data from export
Import Options:
@@ -492,6 +699,12 @@ Examples:
# Export specific data types
node scripts/data-transfer.js export --types=apikeys,accounts --output=prod-data.json
# Export API keys to CSV format
node scripts/data-transfer.js export --types=apikeys --format=csv --sanitize
# Export to CSV with custom filename
node scripts/data-transfer.js export --types=apikeys --format=csv --output=api-keys.csv
`)
}