From d1bbc7179660f318e6ff7981253e9aedff47c033 Mon Sep 17 00:00:00 2001 From: yaogdu Date: Sat, 27 Sep 2025 13:36:50 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=F0=9F=8E=B8=20export=20csv=20from=20we?= =?UTF-8?q?b=20and=20support=20hourly=20TTL=20of=20key?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- scripts/data-transfer.js | 221 +++++++++++++++++- src/routes/admin.js | 19 +- src/services/apiKeyService.js | 22 +- .../components/apikeys/CreateApiKeyModal.vue | 69 +++++- .../components/apikeys/ExpiryEditModal.vue | 16 +- .../src/components/apistats/StatsOverview.vue | 4 +- web/admin-spa/src/views/ApiKeysView.vue | 106 ++++++++- 7 files changed, 429 insertions(+), 28 deletions(-) diff --git a/scripts/data-transfer.js b/scripts/data-transfer.js index e6f13f66..29439a4c 100644 --- a/scripts/data-transfer.js +++ b/scripts/data-transfer.js @@ -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 `) } diff --git a/src/routes/admin.js b/src/routes/admin.js index 88fbff97..947b71de 100644 --- a/src/routes/admin.js +++ b/src/routes/admin.js @@ -547,6 +547,7 @@ router.post('/api-keys', authenticateAdmin, async (req, res) => { weeklyOpusCostLimit, tags, activationDays, // 新增:激活后有效天数 + activationUnit, // 新增:激活时间单位 (hours/days) expirationMode, // 新增:过期模式 icon // 新增:图标 } = req.body @@ -643,14 +644,23 @@ router.post('/api-keys', authenticateAdmin, async (req, res) => { } if (expirationMode === 'activation') { + // 验证激活时间单位 + if (!activationUnit || !['hours', 'days'].includes(activationUnit)) { + return res.status(400).json({ + error: 'Activation unit must be either "hours" or "days" when using activation mode' + }) + } + + // 验证激活时间数值 if ( !activationDays || !Number.isInteger(Number(activationDays)) || Number(activationDays) < 1 ) { - return res - .status(400) - .json({ error: 'Activation days must be a positive integer when using activation mode' }) + const unitText = activationUnit === 'hours' ? 'hours' : 'days' + return res.status(400).json({ + error: `Activation ${unitText} must be a positive integer when using activation mode` + }) } // 激活模式下不应该设置固定过期时间 if (expiresAt) { @@ -684,6 +694,7 @@ router.post('/api-keys', authenticateAdmin, async (req, res) => { weeklyOpusCostLimit, tags, activationDays, + activationUnit, expirationMode, icon }) @@ -724,6 +735,7 @@ router.post('/api-keys/batch', authenticateAdmin, async (req, res) => { weeklyOpusCostLimit, tags, activationDays, + activationUnit, expirationMode, icon } = req.body @@ -774,6 +786,7 @@ router.post('/api-keys/batch', authenticateAdmin, async (req, res) => { weeklyOpusCostLimit, tags, activationDays, + activationUnit, expirationMode, icon }) diff --git a/src/services/apiKeyService.js b/src/services/apiKeyService.js index 3674a96e..9d0a94dd 100644 --- a/src/services/apiKeyService.js +++ b/src/services/apiKeyService.js @@ -37,6 +37,7 @@ class ApiKeyService { weeklyOpusCostLimit = 0, tags = [], activationDays = 0, // 新增:激活后有效天数(0表示不使用此功能) + activationUnit = 'days', // 新增:激活时间单位 'hours' 或 'days' expirationMode = 'fixed', // 新增:过期模式 'fixed'(固定时间) 或 'activation'(首次使用后激活) icon = '' // 新增:图标(base64编码) } = options @@ -73,6 +74,7 @@ class ApiKeyService { weeklyOpusCostLimit: String(weeklyOpusCostLimit || 0), tags: JSON.stringify(tags || []), activationDays: String(activationDays || 0), // 新增:激活后有效天数 + activationUnit: activationUnit || 'days', // 新增:激活时间单位 expirationMode: expirationMode || 'fixed', // 新增:过期模式 isActivated: expirationMode === 'fixed' ? 'true' : 'false', // 根据模式决定激活状态 activatedAt: expirationMode === 'fixed' ? new Date().toISOString() : '', // 激活时间 @@ -117,6 +119,7 @@ class ApiKeyService { weeklyOpusCostLimit: parseFloat(keyData.weeklyOpusCostLimit || 0), tags: JSON.parse(keyData.tags || '[]'), activationDays: parseInt(keyData.activationDays || 0), + activationUnit: keyData.activationUnit || 'days', expirationMode: keyData.expirationMode || 'fixed', isActivated: keyData.isActivated === 'true', activatedAt: keyData.activatedAt, @@ -152,8 +155,18 @@ class ApiKeyService { if (keyData.expirationMode === 'activation' && keyData.isActivated !== 'true') { // 首次使用,需要激活 const now = new Date() - const activationDays = parseInt(keyData.activationDays || 30) // 默认30天 - const expiresAt = new Date(now.getTime() + activationDays * 24 * 60 * 60 * 1000) + const activationPeriod = parseInt(keyData.activationDays || 30) // 默认30 + const activationUnit = keyData.activationUnit || 'days' // 默认天 + + // 根据单位计算过期时间 + let milliseconds + if (activationUnit === 'hours') { + milliseconds = activationPeriod * 60 * 60 * 1000 // 小时转毫秒 + } else { + milliseconds = activationPeriod * 24 * 60 * 60 * 1000 // 天转毫秒 + } + + const expiresAt = new Date(now.getTime() + milliseconds) // 更新激活状态和过期时间 keyData.isActivated = 'true' @@ -167,7 +180,7 @@ class ApiKeyService { logger.success( `🔓 API key activated: ${keyData.id} (${ keyData.name - }), will expire in ${activationDays} days at ${expiresAt.toISOString()}` + }), will expire in ${activationPeriod} ${activationUnit} at ${expiresAt.toISOString()}` ) } @@ -361,6 +374,7 @@ class ApiKeyService { expirationMode: keyData.expirationMode || 'fixed', isActivated: keyData.isActivated === 'true', activationDays: parseInt(keyData.activationDays || 0), + activationUnit: keyData.activationUnit || 'days', activatedAt: keyData.activatedAt || null, claudeAccountId: keyData.claudeAccountId, claudeConsoleAccountId: keyData.claudeConsoleAccountId, @@ -432,6 +446,7 @@ class ApiKeyService { key.dailyCost = (await redis.getDailyCost(key.id)) || 0 key.weeklyOpusCost = (await redis.getWeeklyOpusCost(key.id)) || 0 key.activationDays = parseInt(key.activationDays || 0) + key.activationUnit = key.activationUnit || 'days' key.expirationMode = key.expirationMode || 'fixed' key.isActivated = key.isActivated === 'true' key.activatedAt = key.activatedAt || null @@ -541,6 +556,7 @@ class ApiKeyService { 'permissions', 'expiresAt', 'activationDays', // 新增:激活后有效天数 + 'activationUnit', // 新增:激活时间单位 'expirationMode', // 新增:过期模式 'isActivated', // 新增:是否已激活 'activatedAt', // 新增:激活时间 diff --git a/web/admin-spa/src/components/apikeys/CreateApiKeyModal.vue b/web/admin-spa/src/components/apikeys/CreateApiKeyModal.vue index c3ca64c0..75c4d82c 100644 --- a/web/admin-spa/src/components/apikeys/CreateApiKeyModal.vue +++ b/web/admin-spa/src/components/apikeys/CreateApiKeyModal.vue @@ -492,11 +492,11 @@

- 固定时间模式:Key 创建后立即生效,按设定时间过期 + 固定时间模式:Key 创建后立即生效,按设定时间过期(支持小时和天数) - 激活模式:Key 首次使用时激活,激活后按设定天数过期(适合批量销售) + 激活模式:Key 首次使用时激活,激活后按设定时间过期(支持小时和天数,适合批量销售)

@@ -509,6 +509,10 @@ @change="updateExpireAt" > + + + + @@ -537,27 +541,36 @@ - +

- Key 将在首次使用后激活,激活后 {{ form.activationDays || 30 }} 天过期 + Key 将在首次使用后激活,激活后 + {{ form.activationDays || (form.activationUnit === 'hours' ? 24 : 30) }} + {{ form.activationUnit === 'hours' ? '小时' : '天' }}过期

@@ -917,6 +930,7 @@ const form = reactive({ expiresAt: null, expirationMode: 'fixed', // 过期模式:fixed(固定) 或 activation(激活) activationDays: 30, // 激活后有效天数 + activationUnit: 'days', // 激活时间单位:hours 或 days permissions: 'all', claudeAccountId: '', geminiAccountId: '', @@ -1185,6 +1199,40 @@ const removeTag = (index) => { form.tags.splice(index, 1) } +// 获取快捷时间选项 +const getQuickTimeOptions = () => { + if (form.activationUnit === 'hours') { + return [ + { value: 1, label: '1小时' }, + { value: 3, label: '3小时' }, + { value: 6, label: '6小时' }, + { value: 12, label: '12小时' } + ] + } else { + return [ + { value: 30, label: '30天' }, + { value: 90, label: '90天' }, + { value: 180, label: '180天' }, + { value: 365, label: '365天' } + ] + } +} + +// 单位变化时更新数值 +const updateActivationValue = () => { + if (form.activationUnit === 'hours') { + // 从天切换到小时,设置一个合理的默认值 + if (form.activationDays > 24) { + form.activationDays = 24 + } + } else { + // 从小时切换到天,设置一个合理的默认值 + if (form.activationDays < 1) { + form.activationDays = 1 + } + } +} + // 创建 API Key const createApiKey = async () => { // 验证表单 @@ -1260,6 +1308,7 @@ const createApiKey = async () => { expiresAt: form.expirationMode === 'fixed' ? form.expiresAt || undefined : undefined, expirationMode: form.expirationMode, activationDays: form.expirationMode === 'activation' ? form.activationDays : undefined, + activationUnit: form.expirationMode === 'activation' ? form.activationUnit : undefined, permissions: form.permissions, tags: form.tags.length > 0 ? form.tags : undefined, enableModelRestriction: form.enableModelRestriction, diff --git a/web/admin-spa/src/components/apikeys/ExpiryEditModal.vue b/web/admin-spa/src/components/apikeys/ExpiryEditModal.vue index 5509065e..1b450d11 100644 --- a/web/admin-spa/src/components/apikeys/ExpiryEditModal.vue +++ b/web/admin-spa/src/components/apikeys/ExpiryEditModal.vue @@ -46,7 +46,9 @@ 未激活 - (激活后 {{ apiKey.activationDays || 30 }} 天过期) + (激活后 + {{ apiKey.activationDays || (apiKey.activationUnit === 'hours' ? 24 : 30) }} + {{ apiKey.activationUnit === 'hours' ? '小时' : '天' }}过期) @@ -89,11 +91,15 @@ @click="handleActivateNow" > - 立即激活 (激活后 {{ apiKey.activationDays || 30 }} 天过期) + 立即激活 (激活后 + {{ apiKey.activationDays || (apiKey.activationUnit === 'hours' ? 24 : 30) }} + {{ apiKey.activationUnit === 'hours' ? '小时' : '天' }}过期)

- 点击立即激活此 API Key,激活后将在 {{ apiKey.activationDays || 30 }} 天后过期 + 点击立即激活此 API Key,激活后将在 + {{ apiKey.activationDays || (apiKey.activationUnit === 'hours' ? 24 : 30) }} + {{ apiKey.activationUnit === 'hours' ? '小时' : '天' }}后过期

@@ -400,14 +406,14 @@ const handleActivateNow = async () => { if (window.showConfirm) { confirmed = await window.showConfirm( '激活 API Key', - `确定要立即激活此 API Key 吗?激活后将在 ${props.apiKey.activationDays || 30} 天后自动过期。`, + `确定要立即激活此 API Key 吗?激活后将在 ${props.apiKey.activationDays || (props.apiKey.activationUnit === 'hours' ? 24 : 30)} ${props.apiKey.activationUnit === 'hours' ? '小时' : '天'}后自动过期。`, '确定激活', '取消' ) } else { // 降级方案 confirmed = confirm( - `确定要立即激活此 API Key 吗?激活后将在 ${props.apiKey.activationDays || 30} 天后自动过期。` + `确定要立即激活此 API Key 吗?激活后将在 ${props.apiKey.activationDays || (props.apiKey.activationUnit === 'hours' ? 24 : 30)} ${props.apiKey.activationUnit === 'hours' ? '小时' : '天'}后自动过期。` ) } diff --git a/web/admin-spa/src/components/apistats/StatsOverview.vue b/web/admin-spa/src/components/apistats/StatsOverview.vue index 5d4a9a6d..1ee5ad7f 100644 --- a/web/admin-spa/src/components/apistats/StatsOverview.vue +++ b/web/admin-spa/src/components/apistats/StatsOverview.vue @@ -123,7 +123,9 @@ 未激活 (首次使用后{{ statsData.activationDays || 30 }}天过期)(首次使用后 + {{ statsData.activationDays || (statsData.activationUnit === 'hours' ? 24 : 30) + }}{{ statsData.activationUnit === 'hours' ? '小时' : '天' }}过期) diff --git a/web/admin-spa/src/views/ApiKeysView.vue b/web/admin-spa/src/views/ApiKeysView.vue index 36ad6642..f237ad9b 100644 --- a/web/admin-spa/src/views/ApiKeysView.vue +++ b/web/admin-spa/src/views/ApiKeysView.vue @@ -661,7 +661,9 @@ style="font-size: 13px" > - 未激活 ({{ key.activationDays || 30 }}天) + 未激活 ( + {{ key.activationDays || (key.activationUnit === 'hours' ? 24 : 30) + }}{{ key.activationUnit === 'hours' ? '小时' : '天' }}) @@ -3471,7 +3473,86 @@ const exportToExcel = () => { // 基础数据 const baseData = { + ID: key.id || '', 名称: key.name || '', + 描述: key.description || '', + 状态: key.isActive ? '启用' : '禁用', + API密钥: key.apiKey || '', + + // 过期配置 + 过期模式: + key.expirationMode === 'activation' + ? '首次使用后激活' + : key.expirationMode === 'fixed' + ? '固定时间' + : '无', + 激活期限: key.activationDays || '', + 激活单位: + key.activationUnit === 'hours' ? '小时' : key.activationUnit === 'days' ? '天' : '', + 已激活: key.isActivated ? '是' : '否', + 激活时间: key.activatedAt ? formatDate(key.activatedAt) : '', + 过期时间: key.expiresAt ? formatDate(key.expiresAt) : '', + + // 权限配置 + 服务权限: + key.permissions === 'all' + ? '全部服务' + : key.permissions === 'claude' + ? '仅Claude' + : key.permissions === 'gemini' + ? '仅Gemini' + : key.permissions === 'openai' + ? '仅OpenAI' + : key.permissions || '', + + // 限制配置 + 令牌限制: key.tokenLimit === '0' || key.tokenLimit === 0 ? '无限制' : key.tokenLimit || '', + 并发限制: + key.concurrencyLimit === '0' || key.concurrencyLimit === 0 + ? '无限制' + : key.concurrencyLimit || '', + '速率窗口(分钟)': + key.rateLimitWindow === '0' || key.rateLimitWindow === 0 + ? '无限制' + : key.rateLimitWindow || '', + 速率请求限制: + key.rateLimitRequests === '0' || key.rateLimitRequests === 0 + ? '无限制' + : key.rateLimitRequests || '', + '日费用限制($)': + key.dailyCostLimit === '0' || key.dailyCostLimit === 0 + ? '无限制' + : `$${key.dailyCostLimit}` || '', + '总费用限制($)': + key.totalCostLimit === '0' || key.totalCostLimit === 0 + ? '无限制' + : `$${key.totalCostLimit}` || '', + + // 账户绑定 + Claude专属账户: key.claudeAccountId || '', + Claude控制台账户: key.claudeConsoleAccountId || '', + Gemini专属账户: key.geminiAccountId || '', + OpenAI专属账户: key.openaiAccountId || '', + 'Azure OpenAI专属账户': key.azureOpenaiAccountId || '', + Bedrock专属账户: key.bedrockAccountId || '', + + // 模型和客户端限制 + 启用模型限制: key.enableModelRestriction ? '是' : '否', + 限制的模型: + key.restrictedModels && key.restrictedModels.length > 0 + ? key.restrictedModels.join('; ') + : '', + 启用客户端限制: key.enableClientRestriction ? '是' : '否', + 允许的客户端: + key.allowedClients && key.allowedClients.length > 0 ? key.allowedClients.join('; ') : '', + + // 创建信息 + 创建时间: key.createdAt ? formatDate(key.createdAt) : '', + 创建者: key.createdBy || '', + 用户ID: key.userId || '', + 用户名: key.userUsername || '', + + // 使用统计 标签: key.tags && key.tags.length > 0 ? key.tags.join(', ') : '无', 请求总数: periodRequests, '总费用($)': periodCost.toFixed(2), @@ -3528,12 +3609,33 @@ const exportToExcel = () => { // 设置列宽 const headers = Object.keys(exportData[0] || {}) const columnWidths = headers.map((header) => { + // 基本信息字段 + if (header === 'ID') return { wch: 40 } if (header === '名称') return { wch: 25 } + if (header === '描述') return { wch: 30 } + if (header === 'API密钥') return { wch: 45 } if (header === '标签') return { wch: 20 } - if (header === '最后使用时间') return { wch: 20 } + + // 时间字段 + if (header.includes('时间')) return { wch: 20 } + + // 限制字段 + if (header.includes('限制')) return { wch: 15 } if (header.includes('费用')) return { wch: 15 } if (header.includes('Token')) return { wch: 15 } if (header.includes('请求')) return { wch: 12 } + + // 账户绑定字段 + if (header.includes('账户')) return { wch: 30 } + + // 权限配置字段 + if (header.includes('权限') || header.includes('模型') || header.includes('客户端')) + return { wch: 20 } + + // 激活配置字段 + if (header.includes('激活') || header.includes('过期')) return { wch: 18 } + + // 默认宽度 return { wch: 15 } }) ws['!cols'] = columnWidths