mirror of
https://github.com/Wei-Shaw/claude-relay-service.git
synced 2026-01-23 21:17:30 +00:00
添加claude账号维度计算token费用
This commit is contained in:
@@ -531,7 +531,34 @@ router.post('/claude-accounts/exchange-code', authenticateAdmin, async (req, res
|
|||||||
router.get('/claude-accounts', authenticateAdmin, async (req, res) => {
|
router.get('/claude-accounts', authenticateAdmin, async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const accounts = await claudeAccountService.getAllAccounts();
|
const accounts = await claudeAccountService.getAllAccounts();
|
||||||
res.json({ success: true, data: accounts });
|
|
||||||
|
// 为每个账户添加使用统计信息
|
||||||
|
const accountsWithStats = await Promise.all(accounts.map(async (account) => {
|
||||||
|
try {
|
||||||
|
const usageStats = await redis.getAccountUsageStats(account.id);
|
||||||
|
return {
|
||||||
|
...account,
|
||||||
|
usage: {
|
||||||
|
daily: usageStats.daily,
|
||||||
|
total: usageStats.total,
|
||||||
|
averages: usageStats.averages
|
||||||
|
}
|
||||||
|
};
|
||||||
|
} catch (statsError) {
|
||||||
|
logger.warn(`⚠️ Failed to get usage stats for account ${account.id}:`, statsError.message);
|
||||||
|
// 如果获取统计失败,返回空统计
|
||||||
|
return {
|
||||||
|
...account,
|
||||||
|
usage: {
|
||||||
|
daily: { tokens: 0, requests: 0, allTokens: 0 },
|
||||||
|
total: { tokens: 0, requests: 0, allTokens: 0 },
|
||||||
|
averages: { rpm: 0, tpm: 0 }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
res.json({ success: true, data: accountsWithStats });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('❌ Failed to get Claude accounts:', error);
|
logger.error('❌ Failed to get Claude accounts:', error);
|
||||||
res.status(500).json({ error: 'Failed to get Claude accounts', message: error.message });
|
res.status(500).json({ error: 'Failed to get Claude accounts', message: error.message });
|
||||||
@@ -718,7 +745,18 @@ router.post('/gemini-accounts/exchange-code', authenticateAdmin, async (req, res
|
|||||||
router.get('/gemini-accounts', authenticateAdmin, async (req, res) => {
|
router.get('/gemini-accounts', authenticateAdmin, async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const accounts = await geminiAccountService.getAllAccounts();
|
const accounts = await geminiAccountService.getAllAccounts();
|
||||||
res.json({ success: true, data: accounts });
|
|
||||||
|
// 为Gemini账户添加空的使用统计(暂时)
|
||||||
|
const accountsWithStats = accounts.map(account => ({
|
||||||
|
...account,
|
||||||
|
usage: {
|
||||||
|
daily: { tokens: 0, requests: 0, allTokens: 0 },
|
||||||
|
total: { tokens: 0, requests: 0, allTokens: 0 },
|
||||||
|
averages: { rpm: 0, tpm: 0 }
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
res.json({ success: true, data: accountsWithStats });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('❌ Failed to get Gemini accounts:', error);
|
logger.error('❌ Failed to get Gemini accounts:', error);
|
||||||
res.status(500).json({ error: 'Failed to get accounts', message: error.message });
|
res.status(500).json({ error: 'Failed to get accounts', message: error.message });
|
||||||
|
|||||||
@@ -192,6 +192,7 @@ const app = createApp({
|
|||||||
// 账户
|
// 账户
|
||||||
accounts: [],
|
accounts: [],
|
||||||
accountsLoading: false,
|
accountsLoading: false,
|
||||||
|
accountSortBy: 'dailyTokens', // 默认按今日Token排序
|
||||||
showCreateAccountModal: false,
|
showCreateAccountModal: false,
|
||||||
createAccountLoading: false,
|
createAccountLoading: false,
|
||||||
accountForm: {
|
accountForm: {
|
||||||
@@ -1868,6 +1869,9 @@ const app = createApp({
|
|||||||
account.boundApiKeysCount = this.apiKeys.filter(key => key.geminiAccountId === account.id).length;
|
account.boundApiKeysCount = this.apiKeys.filter(key => key.geminiAccountId === account.id).length;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 加载完成后自动排序
|
||||||
|
this.sortAccounts();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to load accounts:', error);
|
console.error('Failed to load accounts:', error);
|
||||||
} finally {
|
} finally {
|
||||||
@@ -1875,6 +1879,35 @@ const app = createApp({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// 账户排序
|
||||||
|
sortAccounts() {
|
||||||
|
if (!this.accounts || this.accounts.length === 0) return;
|
||||||
|
|
||||||
|
this.accounts.sort((a, b) => {
|
||||||
|
switch (this.accountSortBy) {
|
||||||
|
case 'name':
|
||||||
|
return a.name.localeCompare(b.name);
|
||||||
|
case 'dailyTokens':
|
||||||
|
const aTokens = (a.usage && a.usage.daily && a.usage.daily.allTokens) || 0;
|
||||||
|
const bTokens = (b.usage && b.usage.daily && b.usage.daily.allTokens) || 0;
|
||||||
|
return bTokens - aTokens; // 降序
|
||||||
|
case 'dailyRequests':
|
||||||
|
const aRequests = (a.usage && a.usage.daily && a.usage.daily.requests) || 0;
|
||||||
|
const bRequests = (b.usage && b.usage.daily && b.usage.daily.requests) || 0;
|
||||||
|
return bRequests - aRequests; // 降序
|
||||||
|
case 'totalTokens':
|
||||||
|
const aTotalTokens = (a.usage && a.usage.total && a.usage.total.allTokens) || 0;
|
||||||
|
const bTotalTokens = (b.usage && b.usage.total && b.usage.total.allTokens) || 0;
|
||||||
|
return bTotalTokens - aTotalTokens; // 降序
|
||||||
|
case 'lastUsed':
|
||||||
|
const aLastUsed = a.lastUsedAt ? new Date(a.lastUsedAt) : new Date(0);
|
||||||
|
const bLastUsed = b.lastUsedAt ? new Date(b.lastUsedAt) : new Date(0);
|
||||||
|
return bLastUsed - aLastUsed; // 降序(最近使用的在前)
|
||||||
|
default:
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
async loadModelStats() {
|
async loadModelStats() {
|
||||||
this.modelStatsLoading = true;
|
this.modelStatsLoading = true;
|
||||||
|
|||||||
@@ -922,6 +922,14 @@
|
|||||||
<h3 class="text-xl font-bold text-gray-900 mb-2">账户管理</h3>
|
<h3 class="text-xl font-bold text-gray-900 mb-2">账户管理</h3>
|
||||||
<p class="text-gray-600">管理您的 Claude 和 Gemini 账户及代理配置</p>
|
<p class="text-gray-600">管理您的 Claude 和 Gemini 账户及代理配置</p>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<select v-model="accountSortBy" @change="sortAccounts()" class="form-input px-3 py-2 text-sm">
|
||||||
|
<option value="name">按名称排序</option>
|
||||||
|
<option value="dailyTokens">按今日Token排序</option>
|
||||||
|
<option value="dailyRequests">按今日请求数排序</option>
|
||||||
|
<option value="totalTokens">按总Token排序</option>
|
||||||
|
<option value="lastUsed">按最后使用排序</option>
|
||||||
|
</select>
|
||||||
<button
|
<button
|
||||||
@click.stop="openCreateAccountModal"
|
@click.stop="openCreateAccountModal"
|
||||||
class="btn btn-success px-6 py-3 flex items-center gap-2"
|
class="btn btn-success px-6 py-3 flex items-center gap-2"
|
||||||
@@ -929,6 +937,7 @@
|
|||||||
<i class="fas fa-plus"></i>添加账户
|
<i class="fas fa-plus"></i>添加账户
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div v-if="accountsLoading" class="text-center py-12">
|
<div v-if="accountsLoading" class="text-center py-12">
|
||||||
<div class="loading-spinner mx-auto mb-4"></div>
|
<div class="loading-spinner mx-auto mb-4"></div>
|
||||||
@@ -952,6 +961,7 @@
|
|||||||
<th class="px-6 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider">类型</th>
|
<th class="px-6 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider">类型</th>
|
||||||
<th class="px-6 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider">状态</th>
|
<th class="px-6 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider">状态</th>
|
||||||
<th class="px-6 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider">代理</th>
|
<th class="px-6 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider">代理</th>
|
||||||
|
<th class="px-6 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider">今日使用</th>
|
||||||
<th class="px-6 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider">最后使用</th>
|
<th class="px-6 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider">最后使用</th>
|
||||||
<th class="px-6 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider">操作</th>
|
<th class="px-6 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider">操作</th>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -1024,6 +1034,22 @@
|
|||||||
</div>
|
</div>
|
||||||
<div v-else class="text-gray-400">无代理</div>
|
<div v-else class="text-gray-400">无代理</div>
|
||||||
</td>
|
</td>
|
||||||
|
<td class="px-6 py-4 whitespace-nowrap text-sm">
|
||||||
|
<div v-if="account.usage && account.usage.daily" class="space-y-1">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<div class="w-2 h-2 bg-green-500 rounded-full"></div>
|
||||||
|
<span class="text-sm font-medium text-gray-900">{{ account.usage.daily.requests || 0 }} 次</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<div class="w-2 h-2 bg-blue-500 rounded-full"></div>
|
||||||
|
<span class="text-xs text-gray-600">{{ formatNumber(account.usage.daily.allTokens || 0) }} tokens</span>
|
||||||
|
</div>
|
||||||
|
<div v-if="account.usage.averages && account.usage.averages.rpm > 0" class="text-xs text-gray-500">
|
||||||
|
平均 {{ account.usage.averages.rpm.toFixed(2) }} RPM
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-else class="text-gray-400 text-xs">暂无数据</div>
|
||||||
|
</td>
|
||||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||||
{{ account.lastUsedAt ? new Date(account.lastUsedAt).toLocaleDateString() : '从未使用' }}
|
{{ account.lastUsedAt ? new Date(account.lastUsedAt).toLocaleDateString() : '从未使用' }}
|
||||||
</td>
|
</td>
|
||||||
|
|||||||
Reference in New Issue
Block a user