Merge upstream changes and resolve conflicts

- Merged latest changes from Wei-Shaw/claude-relay-service
- Resolved conflict in AccountForm.vue to support both Bedrock and improved platform switching logic
- Maintained Bedrock integration while incorporating Gemini pricing improvements

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
andersonby
2025-08-06 19:26:31 +08:00
6 changed files with 215 additions and 15 deletions

View File

@@ -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');

View File

@@ -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,

View File

@@ -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();