mirror of
https://github.com/Wei-Shaw/claude-relay-service.git
synced 2026-01-22 16:43:35 +00:00
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:
@@ -470,6 +470,14 @@ class Application {
|
|||||||
this.server.close(async () => {
|
this.server.close(async () => {
|
||||||
logger.info('🚪 HTTP server closed');
|
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 {
|
try {
|
||||||
await redis.disconnect();
|
await redis.disconnect();
|
||||||
logger.info('👋 Redis disconnected');
|
logger.info('👋 Redis disconnected');
|
||||||
|
|||||||
@@ -320,7 +320,18 @@ async function createAccount(accountData) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
logger.info(`Created Gemini account: ${id}`);
|
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);
|
accountData.refreshToken = decrypt(accountData.refreshToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 解析代理配置
|
||||||
|
if (accountData.proxy) {
|
||||||
|
try {
|
||||||
|
accountData.proxy = JSON.parse(accountData.proxy);
|
||||||
|
} catch (e) {
|
||||||
|
// 如果解析失败,保持原样或设置为null
|
||||||
|
accountData.proxy = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return accountData;
|
return accountData;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -361,6 +382,11 @@ async function updateAccount(accountId, updates) {
|
|||||||
const oldRefreshToken = existingAccount.refreshToken || '';
|
const oldRefreshToken = existingAccount.refreshToken || '';
|
||||||
let needUpdateExpiry = false;
|
let needUpdateExpiry = false;
|
||||||
|
|
||||||
|
// 处理代理设置
|
||||||
|
if (updates.proxy !== undefined) {
|
||||||
|
updates.proxy = updates.proxy ? JSON.stringify(updates.proxy) : '';
|
||||||
|
}
|
||||||
|
|
||||||
// 加密敏感字段
|
// 加密敏感字段
|
||||||
if (updates.geminiOauth) {
|
if (updates.geminiOauth) {
|
||||||
updates.geminiOauth = encrypt(
|
updates.geminiOauth = encrypt(
|
||||||
@@ -423,7 +449,20 @@ async function updateAccount(accountId, updates) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
logger.info(`Updated Gemini account: ${accountId}`);
|
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) {
|
for (const key of keys) {
|
||||||
const accountData = await client.hgetall(key);
|
const accountData = await client.hgetall(key);
|
||||||
if (accountData && Object.keys(accountData).length > 0) {
|
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({
|
accounts.push({
|
||||||
...accountData,
|
...accountData,
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ class PricingService {
|
|||||||
this.pricingData = null;
|
this.pricingData = null;
|
||||||
this.lastUpdated = null;
|
this.lastUpdated = null;
|
||||||
this.updateInterval = 24 * 60 * 60 * 1000; // 24小时
|
this.updateInterval = 24 * 60 * 60 * 1000; // 24小时
|
||||||
|
this.fileWatcher = null; // 文件监听器
|
||||||
|
this.reloadDebounceTimer = null; // 防抖定时器
|
||||||
}
|
}
|
||||||
|
|
||||||
// 初始化价格服务
|
// 初始化价格服务
|
||||||
@@ -31,6 +33,9 @@ class PricingService {
|
|||||||
this.checkAndUpdatePricing();
|
this.checkAndUpdatePricing();
|
||||||
}, this.updateInterval);
|
}, this.updateInterval);
|
||||||
|
|
||||||
|
// 设置文件监听器
|
||||||
|
this.setupFileWatcher();
|
||||||
|
|
||||||
logger.success('💰 Pricing service initialized successfully');
|
logger.success('💰 Pricing service initialized successfully');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('❌ Failed to initialize pricing service:', error);
|
logger.error('❌ Failed to initialize pricing service:', error);
|
||||||
@@ -111,6 +116,10 @@ class PricingService {
|
|||||||
this.lastUpdated = new Date();
|
this.lastUpdated = new Date();
|
||||||
|
|
||||||
logger.success(`💰 Downloaded pricing data for ${Object.keys(jsonData).length} models`);
|
logger.success(`💰 Downloaded pricing data for ${Object.keys(jsonData).length} models`);
|
||||||
|
|
||||||
|
// 设置或重新设置文件监听器
|
||||||
|
this.setupFileWatcher();
|
||||||
|
|
||||||
resolve();
|
resolve();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
reject(new Error(`Failed to parse pricing data: ${error.message}`));
|
reject(new Error(`Failed to parse pricing data: ${error.message}`));
|
||||||
@@ -167,6 +176,9 @@ class PricingService {
|
|||||||
this.pricingData = jsonData;
|
this.pricingData = jsonData;
|
||||||
this.lastUpdated = new Date();
|
this.lastUpdated = new Date();
|
||||||
|
|
||||||
|
// 设置或重新设置文件监听器
|
||||||
|
this.setupFileWatcher();
|
||||||
|
|
||||||
logger.warn(`⚠️ Using fallback pricing data for ${Object.keys(jsonData).length} models`);
|
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.');
|
logger.info('💡 Note: This fallback data may be outdated. The system will try to update from the remote source on next check.');
|
||||||
} else {
|
} 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();
|
module.exports = new PricingService();
|
||||||
@@ -1619,9 +1619,13 @@ const handleGroupRefresh = async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 监听平台变化,重置表单
|
// 监听平台变化,重置表单
|
||||||
watch(() => form.value.platform, (newPlatform) => {
|
watch(() => form.value.platform, (newPlatform, oldPlatform) => {
|
||||||
|
// 处理添加方式的自动切换
|
||||||
if (newPlatform === 'claude-console' || newPlatform === 'bedrock') {
|
if (newPlatform === 'claude-console' || newPlatform === 'bedrock') {
|
||||||
form.value.addType = 'manual' // Claude Console 和 Bedrock 只支持手动模式
|
form.value.addType = 'manual' // Claude Console 和 Bedrock 只支持手动模式
|
||||||
|
} else if (oldPlatform === 'claude-console' && (newPlatform === 'claude' || newPlatform === 'gemini')) {
|
||||||
|
// 从 Claude Console 切换到其他平台时,恢复为 OAuth
|
||||||
|
form.value.addType = 'oauth'
|
||||||
}
|
}
|
||||||
|
|
||||||
// 平台变化时,清空分组选择
|
// 平台变化时,清空分组选择
|
||||||
|
|||||||
@@ -343,18 +343,18 @@
|
|||||||
{{ new Date(key.createdAt).toLocaleDateString() }}
|
{{ new Date(key.createdAt).toLocaleDateString() }}
|
||||||
</td>
|
</td>
|
||||||
<td class="px-3 py-4 whitespace-nowrap text-sm">
|
<td class="px-3 py-4 whitespace-nowrap text-sm">
|
||||||
<div class="inline-flex items-center gap-1 group">
|
<div class="inline-flex items-center gap-1.5">
|
||||||
<span v-if="key.expiresAt">
|
<span v-if="key.expiresAt">
|
||||||
<span
|
<span
|
||||||
v-if="isApiKeyExpired(key.expiresAt)"
|
v-if="isApiKeyExpired(key.expiresAt)"
|
||||||
class="text-red-600"
|
class="text-red-600 inline-flex items-center"
|
||||||
>
|
>
|
||||||
<i class="fas fa-exclamation-circle mr-1" />
|
<i class="fas fa-exclamation-circle mr-1" />
|
||||||
已过期
|
已过期
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
v-else-if="isApiKeyExpiringSoon(key.expiresAt)"
|
v-else-if="isApiKeyExpiringSoon(key.expiresAt)"
|
||||||
class="text-orange-600"
|
class="text-orange-600 inline-flex items-center"
|
||||||
>
|
>
|
||||||
<i class="fas fa-clock mr-1" />
|
<i class="fas fa-clock mr-1" />
|
||||||
{{ formatExpireDate(key.expiresAt) }}
|
{{ formatExpireDate(key.expiresAt) }}
|
||||||
@@ -368,17 +368,19 @@
|
|||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
v-else
|
v-else
|
||||||
class="text-gray-400"
|
class="text-gray-400 inline-flex items-center"
|
||||||
>
|
>
|
||||||
<i class="fas fa-infinity mr-1" />
|
<i class="fas fa-infinity mr-1" />
|
||||||
永不过期
|
永不过期
|
||||||
</span>
|
</span>
|
||||||
<button
|
<button
|
||||||
class="opacity-0 group-hover:opacity-100 p-0.5 text-gray-400 hover:text-blue-600 rounded transition-all duration-200"
|
class="inline-flex items-center justify-center w-6 h-6 text-gray-300 hover:text-blue-500 hover:bg-blue-50 rounded-md transition-all duration-200"
|
||||||
title="快速修改过期时间"
|
title="编辑过期时间"
|
||||||
@click.stop="startEditExpiry(key)"
|
@click.stop="startEditExpiry(key)"
|
||||||
>
|
>
|
||||||
<i class="fas fa-pencil-alt text-xs" />
|
<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15.232 5.232l3.536 3.536m-2.036-5.036a2.5 2.5 0 113.536 3.536L6.5 21.036H3v-3.572L16.732 3.732z"></path>
|
||||||
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
@@ -793,11 +795,22 @@
|
|||||||
<span>创建时间</span>
|
<span>创建时间</span>
|
||||||
<span>{{ formatDate(key.createdAt) }}</span>
|
<span>{{ formatDate(key.createdAt) }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex justify-between">
|
<div class="flex justify-between items-center">
|
||||||
<span>过期时间</span>
|
<span>过期时间</span>
|
||||||
|
<div class="flex items-center gap-1">
|
||||||
<span :class="isApiKeyExpiringSoon(key.expiresAt) ? 'text-orange-600 font-semibold' : ''">
|
<span :class="isApiKeyExpiringSoon(key.expiresAt) ? 'text-orange-600 font-semibold' : ''">
|
||||||
{{ key.expiresAt ? formatDate(key.expiresAt) : '永不过期' }}
|
{{ key.expiresAt ? formatDate(key.expiresAt) : '永不过期' }}
|
||||||
</span>
|
</span>
|
||||||
|
<button
|
||||||
|
class="inline-flex items-center justify-center w-5 h-5 text-gray-300 hover:text-blue-500 hover:bg-blue-50 rounded transition-all duration-200"
|
||||||
|
title="编辑过期时间"
|
||||||
|
@click.stop="startEditExpiry(key)"
|
||||||
|
>
|
||||||
|
<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15.232 5.232l3.536 3.536m-2.036-5.036a2.5 2.5 0 113.536 3.536L6.5 21.036H3v-3.572L16.732 3.732z"></path>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user