Skip to content

Commit 1a03366

Browse files
authored
Merge pull request #14 from handsomezhuzhu/siliconcloud-balance-feature
feat: 新增硅基流动API余额检测功能
2 parents 9489227 + 53dbc97 commit 1a03366

File tree

7 files changed

+187
-2
lines changed

7 files changed

+187
-2
lines changed
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import React, { useState, useEffect } from 'react';
2+
import { getApiBalance } from '../../../services/api/base';
3+
import { useLanguage } from '../../../hooks/useLanguage';
4+
5+
const KeyBalanceDisplay = ({ apiKey, apiType, proxyUrl }) => {
6+
const { t } = useLanguage();
7+
const [balanceInfo, setBalanceInfo] = useState(null);
8+
const [loading, setLoading] = useState(false);
9+
const [error, setError] = useState(null);
10+
11+
const fetchBalance = async () => {
12+
if (!apiKey || !apiType) return;
13+
14+
setLoading(true);
15+
setError(null);
16+
17+
try {
18+
const result = await getApiBalance(apiKey, apiType, proxyUrl);
19+
if (result.success) {
20+
setBalanceInfo(result);
21+
setError(null);
22+
} else {
23+
setError(result.error);
24+
setBalanceInfo(null);
25+
}
26+
} catch (err) {
27+
setError(err.message);
28+
setBalanceInfo(null);
29+
} finally {
30+
setLoading(false);
31+
}
32+
};
33+
34+
useEffect(() => {
35+
// 只有当apiType支持余额查询时才自动获取
36+
if (apiType === 'siliconcloud' && apiKey) {
37+
fetchBalance();
38+
}
39+
// eslint-disable-next-line react-hooks/exhaustive-deps
40+
}, [apiKey, apiType, proxyUrl]);
41+
42+
// 如果不是支持余额查询的API类型,则不显示组件
43+
if (apiType !== 'siliconcloud') {
44+
return null;
45+
}
46+
47+
// 根据状态返回相应的显示内容
48+
if (loading) {
49+
return <div className="key-model">{t('balance.title')}: {t('balance.refreshing')}</div>;
50+
}
51+
52+
if (error) {
53+
return <div className="key-model">{t('balance.title')}: {t('balance.fetchFailed')}</div>;
54+
}
55+
56+
if (balanceInfo) {
57+
return <div className="key-model">{t('balance.title')}: ¥{Number(balanceInfo.balance).toFixed(2)}</div>;
58+
}
59+
60+
return null;
61+
};
62+
63+
export default KeyBalanceDisplay;

src/components/features/Results/VirtualizedList.jsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { VariableSizeList as List } from 'react-window';
33
import { useLanguage } from '../../../hooks/useLanguage';
44
import { useAppState } from '../../../contexts/AppStateContext';
55
import { useVirtualization } from '../../../hooks/useVirtualization';
6+
import KeyBalanceDisplay from '../BalanceDisplay/KeyBalanceDisplay';
67

78
const KeyItem = ({ index, style, data }) => {
89
const { t } = useLanguage();
@@ -111,6 +112,14 @@ const KeyItem = ({ index, style, data }) => {
111112
{getKeyStatusInfo()}
112113
</div>
113114
)}
115+
{/* 余额显示 - 只在有效的siliconcloud key时显示 */}
116+
{(keyData.status === 'valid' || keyData.status === 'paid') && state.apiType === 'siliconcloud' && (
117+
<KeyBalanceDisplay
118+
apiKey={keyData.key}
119+
apiType={state.apiType}
120+
proxyUrl={state.proxyUrl}
121+
/>
122+
)}
114123
</div>
115124
<div className={`key-status ${getStatusClass(keyData.status)}`}>
116125
{getStatusText(keyData.status)}

src/locales/en.json

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,5 +203,15 @@
203203
},
204204
"results": {
205205
"logTooltip": "Tip: Click a test result card to view the log for that key."
206+
},
207+
"balance": {
208+
"title": "Balance",
209+
"refresh": "Refresh",
210+
"refreshing": "Querying...",
211+
"accountBalance": "Account Balance",
212+
"userNickname": "User Nickname",
213+
"email": "Email",
214+
"fetchFailed": "Query Failed",
215+
"clickToRefresh": "Click refresh button to query balance information"
206216
}
207217
}

src/locales/zh.json

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,5 +203,15 @@
203203
},
204204
"results": {
205205
"logTooltip": "提示:点击测试结果卡片即可查看对应 key 的测试日志。"
206+
},
207+
"balance": {
208+
"title": "余额",
209+
"refresh": "刷新",
210+
"refreshing": "查询中...",
211+
"accountBalance": "账户余额",
212+
"userNickname": "用户昵称",
213+
"email": "邮箱",
214+
"fetchFailed": "查询失败",
215+
"clickToRefresh": "点击刷新按钮查询余额信息"
206216
}
207217
}

src/services/api/base.js

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { testOpenAIKey, getOpenAIModels } from './openai';
22
import { testClaudeKey, getClaudeModels } from './claude';
33
import { testGeminiKey, getGeminiModels } from './gemini';
44
import { testDeepSeekKey, getDeepSeekModels } from './deepseek';
5-
import { testSiliconCloudKey, getSiliconCloudModels } from './siliconcloud';
5+
import { testSiliconCloudKey, getSiliconCloudModels, getSiliconCloudBalance } from './siliconcloud';
66
import { testXAIKey, getXAIModels } from './xai';
77
import { testOpenRouterKey, getOpenRouterModels } from './openrouter';
88

@@ -78,3 +78,36 @@ export const getAvailableModels = async (apiKey, apiType, proxyUrl) => {
7878
return [];
7979
}
8080
};
81+
82+
export const getApiBalance = async (apiKey, apiType, proxyUrl) => {
83+
try {
84+
switch (apiType) {
85+
case 'siliconcloud':
86+
return await getSiliconCloudBalance(apiKey, proxyUrl);
87+
case 'openai':
88+
case 'claude':
89+
case 'gemini':
90+
case 'deepseek':
91+
case 'xai':
92+
case 'openrouter':
93+
return {
94+
success: false,
95+
error: '该API类型暂不支持余额查询',
96+
balance: null
97+
};
98+
default:
99+
return {
100+
success: false,
101+
error: '不支持的API类型',
102+
balance: null
103+
};
104+
}
105+
} catch (error) {
106+
console.error('获取API余额失败:', error);
107+
return {
108+
success: false,
109+
error: error.message,
110+
balance: null
111+
};
112+
}
113+
};

src/services/api/siliconcloud.js

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,3 +59,43 @@ export const getSiliconCloudModels = async (apiKey, proxyUrl) => {
5959
return [];
6060
}
6161
};
62+
63+
export const getSiliconCloudBalance = async (apiKey, proxyUrl) => {
64+
try {
65+
const url = getApiUrl('siliconcloud', '/user/info', proxyUrl);
66+
67+
const response = await fetch(url, {
68+
method: 'GET',
69+
headers: {
70+
'Authorization': `Bearer ${apiKey}`,
71+
'Content-Type': 'application/json'
72+
}
73+
});
74+
75+
if (!response.ok) {
76+
const errorData = await response.json().catch(() => null);
77+
throw new Error(errorData?.error?.message || `HTTP ${response.status}`);
78+
}
79+
80+
const data = await response.json();
81+
82+
// 返回格式化的余额信息
83+
return {
84+
success: true,
85+
balance: data.data?.balance || 0,
86+
currency: data.data?.currency || 'CNY',
87+
userInfo: {
88+
userId: data.data?.user_id,
89+
email: data.data?.email,
90+
nickname: data.data?.nickname
91+
}
92+
};
93+
} catch (error) {
94+
console.error('获取SiliconCloud余额失败:', error);
95+
return {
96+
success: false,
97+
error: error.message,
98+
balance: null
99+
};
100+
}
101+
};

src/styles/variables.css

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,15 +49,27 @@
4949

5050
/* 颜色别名 */
5151
--primary-color: var(--color-primary);
52+
--primary-hover: #0056b3;
5253
--success-color: var(--color-success);
5354
--error-color: var(--color-danger);
5455
--warning-color: var(--color-warning);
5556
--info-color: var(--color-info);
5657

5758
/* 背景色别名 */
58-
--bg-primary: var(--color-primary);
59+
--bg-primary: var(--bg-input);
60+
--bg-secondary: var(--bg-card);
5961
--bg-tertiary: #f1f3f4;
6062

63+
/* 错误样式 */
64+
--error-bg: #f8d7da;
65+
--error-border: #f5c6cb;
66+
67+
/* 边框样式 */
68+
--border-light: rgba(0, 0, 0, 0.1);
69+
70+
/* 字体 */
71+
--font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
72+
6173
/* 过渡动画 */
6274
--transition-fast: 0.15s ease;
6375
--transition-normal: 0.25s ease;
@@ -79,4 +91,12 @@
7991
--border-focus: #60a5fa;
8092

8193
--color-primary: #60a5fa;
94+
95+
/* 深色主题的特殊变量 */
96+
--dark-bg-primary: #1a1a1a;
97+
--dark-bg-secondary: #2a2a2a;
98+
--dark-text-primary: #ffffff;
99+
--dark-text-secondary: #b0b0b0;
100+
--dark-border-color: #444444;
101+
--dark-border-light: rgba(255, 255, 255, 0.1);
82102
}

0 commit comments

Comments
 (0)