This commit is contained in:
SunSeekerX
2026-02-09 22:06:15 +08:00
parent 29f2c4aba1
commit c21997b7f4
8 changed files with 191 additions and 8 deletions

View File

@@ -56,6 +56,7 @@ const PLATFORM_TEST_MODELS = {
'claude-console': CLAUDE_MODELS,
bedrock: BEDROCK_MODELS,
gemini: GEMINI_MODELS,
'gemini-api': GEMINI_MODELS,
'openai-responses': OPENAI_MODELS,
'azure-openai': [],
droid: CLAUDE_MODELS,

View File

@@ -2937,6 +2937,7 @@ async function handleStandardStreamGenerateContent(req, res) {
module.exports = {
// 工具函数
buildGeminiApiUrl,
generateSessionHash,
checkPermissions,
ensureGeminiPermission,

View File

@@ -452,4 +452,164 @@ router.post('/gemini-api-accounts/:id/reset-status', authenticateAdmin, async (r
}
})
// 测试 Gemini-API 账户连通性SSE 流式)
const ALLOWED_MAX_TOKENS = [100, 500, 1000, 2000, 4096]
const sanitizeMaxTokens = (value) =>
ALLOWED_MAX_TOKENS.includes(Number(value)) ? Number(value) : 500
router.post('/gemini-api-accounts/:accountId/test', authenticateAdmin, async (req, res) => {
const { accountId } = req.params
const { model = 'gemini-2.5-flash', prompt = 'hi' } = req.body
const maxTokens = sanitizeMaxTokens(req.body.maxTokens)
const { createGeminiTestPayload, extractErrorMessage } = require('../../utils/testPayloadHelper')
const { buildGeminiApiUrl } = require('../../handlers/geminiHandlers')
const ProxyHelper = require('../../utils/proxyHelper')
const axios = require('axios')
const abortController = new AbortController()
res.on('close', () => abortController.abort())
const safeWrite = (data) => {
if (!res.writableEnded && !res.destroyed) {
res.write(data)
}
}
const safeEnd = () => {
if (!res.writableEnded && !res.destroyed) {
res.end()
}
}
try {
const account = await geminiApiAccountService.getAccount(accountId)
if (!account) {
return res.status(404).json({ error: 'Account not found' })
}
if (!account.apiKey) {
return res.status(401).json({ error: 'API Key not found or decryption failed' })
}
const baseUrl = account.baseUrl || 'https://generativelanguage.googleapis.com'
const apiUrl = buildGeminiApiUrl(baseUrl, model, 'streamGenerateContent', account.apiKey, {
stream: true
})
// 设置 SSE 响应头
if (res.writableEnded || res.destroyed) {
return
}
res.writeHead(200, {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
Connection: 'keep-alive',
'X-Accel-Buffering': 'no'
})
safeWrite(`data: ${JSON.stringify({ type: 'test_start', message: 'Test started' })}\n\n`)
const payload = createGeminiTestPayload(model, { prompt, maxTokens })
const requestConfig = {
headers: { 'Content-Type': 'application/json' },
timeout: 60000,
responseType: 'stream',
validateStatus: () => true,
signal: abortController.signal
}
// 配置代理
if (account.proxy) {
const agent = ProxyHelper.createProxyAgent(account.proxy)
if (agent) {
requestConfig.httpsAgent = agent
requestConfig.httpAgent = agent
}
}
try {
const response = await axios.post(apiUrl, payload, requestConfig)
if (response.status !== 200) {
const chunks = []
response.data.on('data', (chunk) => chunks.push(chunk))
response.data.on('end', () => {
const errorData = Buffer.concat(chunks).toString()
let errorMsg = `API Error: ${response.status}`
try {
const json = JSON.parse(errorData)
errorMsg = extractErrorMessage(json, errorMsg)
} catch {
if (errorData.length < 500) {
errorMsg = errorData || errorMsg
}
}
safeWrite(
`data: ${JSON.stringify({ type: 'test_complete', success: false, error: errorMsg })}\n\n`
)
safeEnd()
})
response.data.on('error', () => {
safeWrite(
`data: ${JSON.stringify({ type: 'test_complete', success: false, error: `API Error: ${response.status}` })}\n\n`
)
safeEnd()
})
return
}
let buffer = ''
response.data.on('data', (chunk) => {
buffer += chunk.toString()
const lines = buffer.split('\n')
buffer = lines.pop() || ''
for (const line of lines) {
if (!line.startsWith('data:')) {
continue
}
const jsonStr = line.substring(5).trim()
if (!jsonStr || jsonStr === '[DONE]') {
continue
}
try {
const data = JSON.parse(jsonStr)
const text = data.candidates?.[0]?.content?.parts?.[0]?.text
if (text) {
safeWrite(`data: ${JSON.stringify({ type: 'content', text })}\n\n`)
}
} catch {
// ignore parse errors
}
}
})
response.data.on('end', () => {
safeWrite(`data: ${JSON.stringify({ type: 'test_complete', success: true })}\n\n`)
safeEnd()
})
response.data.on('error', (err) => {
safeWrite(
`data: ${JSON.stringify({ type: 'test_complete', success: false, error: err.message })}\n\n`
)
safeEnd()
})
} catch (axiosError) {
if (axiosError.name === 'CanceledError') {
return
}
safeWrite(
`data: ${JSON.stringify({ type: 'test_complete', success: false, error: axiosError.message })}\n\n`
)
safeEnd()
}
} catch (error) {
logger.error('Gemini-API account test failed:', error)
if (!res.headersSent) {
return res.status(500).json({ error: 'Test failed', message: error.message })
}
safeWrite(`data: ${JSON.stringify({ type: 'error', error: error.message })}\n\n`)
safeEnd()
}
})
module.exports = router

View File

@@ -415,6 +415,9 @@ const pricingService = require('../../services/pricingService')
// 获取所有模型价格数据
router.get('/models/pricing', authenticateAdmin, async (req, res) => {
try {
if (!pricingService.pricingData || Object.keys(pricingService.pricingData).length === 0) {
await pricingService.loadPricingData()
}
const data = pricingService.pricingData
res.json({
success: true,

View File

@@ -333,6 +333,7 @@ const platformFallbackModels = {
claude: 'claude-sonnet-4-5-20250929',
'claude-console': 'claude-sonnet-4-5-20250929',
gemini: 'gemini-2.5-pro',
'gemini-api': 'gemini-2.5-flash',
'openai-responses': 'gpt-5',
droid: 'claude-sonnet-4-5-20250929',
ccr: 'claude-sonnet-4-5-20250929'
@@ -427,6 +428,11 @@ const platformConfigs = {
icon: 'fas fa-gem',
badge: 'bg-blue-100 text-blue-700 dark:bg-blue-500/20 dark:text-blue-300'
},
'gemini-api': {
label: 'Gemini API',
icon: 'fas fa-gem',
badge: 'bg-blue-100 text-blue-700 dark:bg-blue-500/20 dark:text-blue-300'
},
'openai-responses': {
label: 'OpenAI Responses',
icon: 'fas fa-code',
@@ -520,6 +526,7 @@ const getAccountEndpoint = () => {
'claude-console': `${APP_CONFIG.apiPrefix}/admin/claude-console-accounts/${props.account.id}/test`,
bedrock: `${APP_CONFIG.apiPrefix}/admin/bedrock-accounts/${props.account.id}/test`,
gemini: `${APP_CONFIG.apiPrefix}/admin/gemini-accounts/${props.account.id}/test`,
'gemini-api': `${APP_CONFIG.apiPrefix}/admin/gemini-api-accounts/${props.account.id}/test`,
'openai-responses': `${APP_CONFIG.apiPrefix}/admin/openai-responses-accounts/${props.account.id}/test`,
'azure-openai': `${APP_CONFIG.apiPrefix}/admin/azure-openai-accounts/${props.account.id}/test`,
droid: `${APP_CONFIG.apiPrefix}/admin/droid-accounts/${props.account.id}/test`,
@@ -533,7 +540,9 @@ const startTest = () => {
const endpoint = getAccountEndpoint()
if (!endpoint) return
const authToken = localStorage.getItem('authToken')
const useSSE = ['claude', 'claude-console', 'bedrock'].includes(props.account.platform)
const useSSE = ['claude', 'claude-console', 'bedrock', 'gemini-api'].includes(
props.account.platform
)
state.sendTestRequest(
endpoint,
{ model: selectedModel.value },

View File

@@ -324,8 +324,16 @@ const loadData = async () => {
getModelPricingApi(),
getModelPricingStatusApi()
])
if (pricingResult.success) pricingData.value = pricingResult.data
if (statusResult.success) pricingStatus.value = statusResult.data
if (pricingResult.success) {
pricingData.value = pricingResult.data
} else {
showToast(pricingResult.message || '加载模型价格失败', 'error')
}
if (statusResult.success) {
pricingStatus.value = statusResult.data
} else {
showToast(statusResult.message || '获取价格状态失败', 'error')
}
loading.value = false
}

View File

@@ -2618,6 +2618,7 @@ const supportedTestPlatforms = [
'claude-console',
'bedrock',
'gemini',
'gemini-api',
'openai-responses',
'azure-openai',
'droid',

View File

@@ -1218,6 +1218,11 @@
</div>
</div>
</div>
<!-- 模型价格部分 -->
<div v-show="activeSection === 'modelPricing'">
<ModelPricingSection />
</div>
</div>
</div>
</div>
@@ -1797,11 +1802,6 @@
</div>
</div>
<!-- 模型价格部分 -->
<div v-show="activeSection === 'modelPricing'">
<ModelPricingSection />
</div>
<!-- ConfirmModal -->
<ConfirmModal
:cancel-text="confirmModalConfig.cancelText"