diff --git a/VERSION b/VERSION index f6adb66c..e6b36d1b 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.1.86 +1.1.87 diff --git a/src/app.js b/src/app.js index 982ebe43..2a466496 100644 --- a/src/app.js +++ b/src/app.js @@ -470,6 +470,14 @@ class Application { this.server.close(async () => { logger.info('🚪 HTTP server closed'); + // 清理 pricing service 的文件监听器 + try { + pricingService.cleanup(); + logger.info('💰 Pricing service cleaned up'); + } catch (error) { + logger.error('❌ Error cleaning up pricing service:', error); + } + try { await redis.disconnect(); logger.info('👋 Redis disconnected'); diff --git a/src/services/geminiAccountService.js b/src/services/geminiAccountService.js index d11fea1f..086b886e 100644 --- a/src/services/geminiAccountService.js +++ b/src/services/geminiAccountService.js @@ -320,7 +320,18 @@ async function createAccount(accountData) { } logger.info(`Created Gemini account: ${id}`); - return account; + + // 返回时解析代理配置 + const returnAccount = { ...account }; + if (returnAccount.proxy) { + try { + returnAccount.proxy = JSON.parse(returnAccount.proxy); + } catch (e) { + returnAccount.proxy = null; + } + } + + return returnAccount; } // 获取账户 @@ -343,6 +354,16 @@ async function getAccount(accountId) { accountData.refreshToken = decrypt(accountData.refreshToken); } + // 解析代理配置 + if (accountData.proxy) { + try { + accountData.proxy = JSON.parse(accountData.proxy); + } catch (e) { + // 如果解析失败,保持原样或设置为null + accountData.proxy = null; + } + } + return accountData; } @@ -361,6 +382,11 @@ async function updateAccount(accountId, updates) { const oldRefreshToken = existingAccount.refreshToken || ''; let needUpdateExpiry = false; + // 处理代理设置 + if (updates.proxy !== undefined) { + updates.proxy = updates.proxy ? JSON.stringify(updates.proxy) : ''; + } + // 加密敏感字段 if (updates.geminiOauth) { updates.geminiOauth = encrypt( @@ -423,7 +449,20 @@ async function updateAccount(accountId, updates) { ); logger.info(`Updated Gemini account: ${accountId}`); - return { ...existingAccount, ...updates }; + + // 合并更新后的账户数据 + const updatedAccount = { ...existingAccount, ...updates }; + + // 返回时解析代理配置 + if (updatedAccount.proxy && typeof updatedAccount.proxy === 'string') { + try { + updatedAccount.proxy = JSON.parse(updatedAccount.proxy); + } catch (e) { + updatedAccount.proxy = null; + } + } + + return updatedAccount; } // 删除账户 @@ -464,6 +503,16 @@ async function getAllAccounts() { for (const key of keys) { const accountData = await client.hgetall(key); if (accountData && Object.keys(accountData).length > 0) { + // 解析代理配置 + if (accountData.proxy) { + try { + accountData.proxy = JSON.parse(accountData.proxy); + } catch (e) { + // 如果解析失败,设置为null + accountData.proxy = null; + } + } + // 不解密敏感字段,只返回基本信息 accounts.push({ ...accountData, diff --git a/src/services/pricingService.js b/src/services/pricingService.js index 76d5a1d9..1b4466b6 100644 --- a/src/services/pricingService.js +++ b/src/services/pricingService.js @@ -12,6 +12,8 @@ class PricingService { this.pricingData = null; this.lastUpdated = null; this.updateInterval = 24 * 60 * 60 * 1000; // 24小时 + this.fileWatcher = null; // 文件监听器 + this.reloadDebounceTimer = null; // 防抖定时器 } // 初始化价格服务 @@ -31,6 +33,9 @@ class PricingService { this.checkAndUpdatePricing(); }, this.updateInterval); + // 设置文件监听器 + this.setupFileWatcher(); + logger.success('💰 Pricing service initialized successfully'); } catch (error) { logger.error('❌ Failed to initialize pricing service:', error); @@ -111,6 +116,10 @@ class PricingService { this.lastUpdated = new Date(); logger.success(`💰 Downloaded pricing data for ${Object.keys(jsonData).length} models`); + + // 设置或重新设置文件监听器 + this.setupFileWatcher(); + resolve(); } catch (error) { reject(new Error(`Failed to parse pricing data: ${error.message}`)); @@ -167,6 +176,9 @@ class PricingService { this.pricingData = jsonData; this.lastUpdated = new Date(); + // 设置或重新设置文件监听器 + this.setupFileWatcher(); + logger.warn(`⚠️ Using fallback pricing data for ${Object.keys(jsonData).length} models`); logger.info('💡 Note: This fallback data may be outdated. The system will try to update from the remote source on next check.'); } else { @@ -276,6 +288,120 @@ class PricingService { }; } } + + // 设置文件监听器 + setupFileWatcher() { + try { + // 如果已有监听器,先关闭 + if (this.fileWatcher) { + this.fileWatcher.close(); + this.fileWatcher = null; + } + + // 只有文件存在时才设置监听器 + if (!fs.existsSync(this.pricingFile)) { + logger.debug('💰 Pricing file does not exist yet, skipping file watcher setup'); + return; + } + + // 使用 fs.watchFile 作为更可靠的文件监听方式 + // 它使用轮询,虽然性能稍差,但更可靠 + const watchOptions = { + persistent: true, + interval: 60000 // 每60秒检查一次 + }; + + // 记录初始的修改时间 + let lastMtime = fs.statSync(this.pricingFile).mtimeMs; + + fs.watchFile(this.pricingFile, watchOptions, (curr, prev) => { + // 检查文件是否真的被修改了(不仅仅是访问) + if (curr.mtimeMs !== lastMtime) { + lastMtime = curr.mtimeMs; + logger.debug(`💰 Detected change in pricing file (mtime: ${new Date(curr.mtime).toISOString()})`); + this.handleFileChange(); + } + }); + + // 保存引用以便清理 + this.fileWatcher = { + close: () => fs.unwatchFile(this.pricingFile) + }; + + logger.info('👁️ File watcher set up for model_pricing.json (polling every 60s)'); + } catch (error) { + logger.error('❌ Failed to setup file watcher:', error); + } + } + + // 处理文件变化(带防抖) + handleFileChange() { + // 清除之前的定时器 + if (this.reloadDebounceTimer) { + clearTimeout(this.reloadDebounceTimer); + } + + // 设置新的定时器(防抖500ms) + this.reloadDebounceTimer = setTimeout(async () => { + logger.info('🔄 Reloading pricing data due to file change...'); + await this.reloadPricingData(); + }, 500); + } + + // 重新加载价格数据 + async reloadPricingData() { + try { + // 验证文件是否存在 + if (!fs.existsSync(this.pricingFile)) { + logger.warn('💰 Pricing file was deleted, using fallback'); + await this.useFallbackPricing(); + // 重新设置文件监听器(fallback会创建新文件) + this.setupFileWatcher(); + return; + } + + // 读取文件内容 + const data = fs.readFileSync(this.pricingFile, 'utf8'); + + // 尝试解析JSON + const jsonData = JSON.parse(data); + + // 验证数据结构 + if (typeof jsonData !== 'object' || Object.keys(jsonData).length === 0) { + throw new Error('Invalid pricing data structure'); + } + + // 更新内存中的数据 + this.pricingData = jsonData; + this.lastUpdated = new Date(); + + const modelCount = Object.keys(jsonData).length; + logger.success(`💰 Reloaded pricing data for ${modelCount} models from file`); + + // 显示一些统计信息 + const claudeModels = Object.keys(jsonData).filter(k => k.includes('claude')).length; + const gptModels = Object.keys(jsonData).filter(k => k.includes('gpt')).length; + const geminiModels = Object.keys(jsonData).filter(k => k.includes('gemini')).length; + + logger.debug(`💰 Model breakdown: Claude=${claudeModels}, GPT=${gptModels}, Gemini=${geminiModels}`); + } catch (error) { + logger.error('❌ Failed to reload pricing data:', error); + logger.warn('💰 Keeping existing pricing data in memory'); + } + } + + // 清理资源 + cleanup() { + if (this.fileWatcher) { + this.fileWatcher.close(); + this.fileWatcher = null; + logger.debug('💰 File watcher closed'); + } + if (this.reloadDebounceTimer) { + clearTimeout(this.reloadDebounceTimer); + this.reloadDebounceTimer = null; + } + } } module.exports = new PricingService(); \ No newline at end of file diff --git a/web/admin-spa/src/components/accounts/AccountForm.vue b/web/admin-spa/src/components/accounts/AccountForm.vue index fee143f8..dee9a03b 100644 --- a/web/admin-spa/src/components/accounts/AccountForm.vue +++ b/web/admin-spa/src/components/accounts/AccountForm.vue @@ -1619,9 +1619,13 @@ const handleGroupRefresh = async () => { } // 监听平台变化,重置表单 -watch(() => form.value.platform, (newPlatform) => { +watch(() => form.value.platform, (newPlatform, oldPlatform) => { + // 处理添加方式的自动切换 if (newPlatform === 'claude-console' || newPlatform === 'bedrock') { form.value.addType = 'manual' // Claude Console 和 Bedrock 只支持手动模式 + } else if (oldPlatform === 'claude-console' && (newPlatform === 'claude' || newPlatform === 'gemini')) { + // 从 Claude Console 切换到其他平台时,恢复为 OAuth + form.value.addType = 'oauth' } // 平台变化时,清空分组选择 diff --git a/web/admin-spa/src/views/ApiKeysView.vue b/web/admin-spa/src/views/ApiKeysView.vue index 0a57e716..a5d37926 100644 --- a/web/admin-spa/src/views/ApiKeysView.vue +++ b/web/admin-spa/src/views/ApiKeysView.vue @@ -343,18 +343,18 @@ {{ new Date(key.createdAt).toLocaleDateString() }} -
+
已过期 {{ formatExpireDate(key.expiresAt) }} @@ -368,17 +368,19 @@ 永不过期
@@ -793,11 +795,22 @@ 创建时间 {{ formatDate(key.createdAt) }}
-
+
过期时间 - - {{ key.expiresAt ? formatDate(key.expiresAt) : '永不过期' }} - +
+ + {{ key.expiresAt ? formatDate(key.expiresAt) : '永不过期' }} + + +