feat: Implement DeepSeek account validation, testing, and queue status monitoring with corresponding admin API endpoints.

This commit is contained in:
CJACK
2026-02-01 03:10:26 +08:00
parent bc260899c1
commit 5ac472626e
9 changed files with 807 additions and 283 deletions

View File

@@ -1,4 +1,4 @@
import { useState } from 'react'
import { useState, useEffect } from 'react'
export default function AccountManager({ config, onRefresh, onMessage }) {
const [showAddKey, setShowAddKey] = useState(false)
@@ -6,6 +6,30 @@ export default function AccountManager({ config, onRefresh, onMessage }) {
const [newKey, setNewKey] = useState('')
const [newAccount, setNewAccount] = useState({ email: '', mobile: '', password: '' })
const [loading, setLoading] = useState(false)
const [validating, setValidating] = useState({}) // 单个账号验证状态
const [validatingAll, setValidatingAll] = useState(false)
const [testing, setTesting] = useState({}) // 单个账号测试状态
const [testingAll, setTestingAll] = useState(false)
const [queueStatus, setQueueStatus] = useState(null)
// 获取队列状态
const fetchQueueStatus = async () => {
try {
const res = await fetch('/admin/queue/status')
if (res.ok) {
const data = await res.json()
setQueueStatus(data)
}
} catch (e) {
console.error('获取队列状态失败:', e)
}
}
useEffect(() => {
fetchQueueStatus()
const interval = setInterval(fetchQueueStatus, 5000) // 每5秒刷新
return () => clearInterval(interval)
}, [])
const addKey = async () => {
if (!newKey.trim()) return
@@ -90,8 +114,115 @@ export default function AccountManager({ config, onRefresh, onMessage }) {
}
}
// 验证单个账号
const validateAccount = async (identifier) => {
setValidating(prev => ({ ...prev, [identifier]: true }))
try {
const res = await fetch('/admin/accounts/validate', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ identifier }),
})
const data = await res.json()
if (data.valid) {
onMessage('success', `${identifier}: ${data.message}`)
} else {
onMessage('error', `${identifier}: ${data.message}`)
}
onRefresh()
} catch (e) {
onMessage('error', '验证失败: ' + e.message)
} finally {
setValidating(prev => ({ ...prev, [identifier]: false }))
}
}
// 批量验证所有账号
const validateAllAccounts = async () => {
if (!confirm('确定要验证所有账号?这可能需要一些时间。')) return
setValidatingAll(true)
try {
const res = await fetch('/admin/accounts/validate-all', { method: 'POST' })
const data = await res.json()
onMessage('success', `验证完成: ${data.valid}/${data.total} 个账号有效`)
onRefresh()
} catch (e) {
onMessage('error', '批量验证失败: ' + e.message)
} finally {
setValidatingAll(false)
}
}
// 测试单个账号 API
const testAccount = async (identifier) => {
setTesting(prev => ({ ...prev, [identifier]: true }))
try {
const res = await fetch('/admin/accounts/test', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ identifier }),
})
const data = await res.json()
if (data.success) {
onMessage('success', `${identifier}: API 测试成功 (${data.response_time}ms)`)
} else {
onMessage('error', `${identifier}: ${data.message}`)
}
onRefresh()
} catch (e) {
onMessage('error', 'API 测试失败: ' + e.message)
} finally {
setTesting(prev => ({ ...prev, [identifier]: false }))
}
}
// 批量测试所有账号 API
const testAllAccounts = async () => {
if (!confirm('确定要测试所有账号的 API这可能需要较长时间。')) return
setTestingAll(true)
try {
const res = await fetch('/admin/accounts/test-all', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({}),
})
const data = await res.json()
onMessage('success', `API 测试完成: ${data.success}/${data.total} 个账号可用`)
onRefresh()
} catch (e) {
onMessage('error', '批量 API 测试失败: ' + e.message)
} finally {
setTestingAll(false)
}
}
return (
<div className="section">
{/* 队列状态监控 */}
{queueStatus && (
<div className="card">
<div className="card-header">
<span className="card-title">📊 轮询队列状态</span>
<button className="btn btn-secondary" onClick={fetchQueueStatus}>刷新</button>
</div>
<div className="queue-status">
<div className="stat-row">
<span className="stat-label">可用账号:</span>
<span className="stat-value stat-success">{queueStatus.available}</span>
<span className="stat-label" style={{ marginLeft: '20px' }}>使用中:</span>
<span className="stat-value stat-warning">{queueStatus.in_use}</span>
<span className="stat-label" style={{ marginLeft: '20px' }}>总计:</span>
<span className="stat-value">{queueStatus.total}</span>
</div>
{queueStatus.in_use > 0 && (
<div className="stat-detail">
正在使用: {queueStatus.in_use_accounts.join(', ')}
</div>
)}
</div>
</div>
)}
{/* API Keys */}
<div className="card">
<div className="card-header">
@@ -117,22 +248,57 @@ export default function AccountManager({ config, onRefresh, onMessage }) {
<div className="card">
<div className="card-header">
<span className="card-title">👤 DeepSeek 账号</span>
<button className="btn btn-primary" onClick={() => setShowAddAccount(true)}>+ 添加</button>
<div className="btn-group-inline">
<button
className="btn btn-primary btn-sm"
onClick={testAllAccounts}
disabled={testingAll || validatingAll || !config.accounts?.length}
>
{testingAll ? <span className="loading"></span> : '🧪 批量测试'}
</button>
<button
className="btn btn-secondary btn-sm"
onClick={validateAllAccounts}
disabled={validatingAll || testingAll || !config.accounts?.length}
>
{validatingAll ? <span className="loading"></span> : '✅ 批量验证'}
</button>
<button className="btn btn-primary" onClick={() => setShowAddAccount(true)}>+ 添加</button>
</div>
</div>
{config.accounts?.length > 0 ? (
<div className="list">
{config.accounts.map((acc, i) => (
<div key={i} className="list-item">
<div className="list-item-info">
<span className="list-item-text">{acc.email || acc.mobile}</span>
<span className={`badge ${acc.has_token ? 'badge-success' : 'badge-warning'}`}>
{acc.has_token ? '已登录' : '未登录'}
</span>
{config.accounts.map((acc, i) => {
const id = acc.email || acc.mobile
return (
<div key={i} className="list-item">
<div className="list-item-info">
<span className="list-item-text">{id}</span>
<span className={`badge ${acc.has_token ? 'badge-success' : 'badge-warning'}`}>
{acc.has_token ? '已登录' : '未登录'}
</span>
</div>
<div className="btn-group-inline">
<button
className="btn btn-primary btn-sm"
onClick={() => testAccount(id)}
disabled={testing[id]}
>
{testing[id] ? <span className="loading"></span> : '测试'}
</button>
<button
className="btn btn-secondary btn-sm"
onClick={() => validateAccount(id)}
disabled={validating[id]}
>
{validating[id] ? <span className="loading"></span> : '验证'}
</button>
<button className="btn btn-danger btn-sm" onClick={() => deleteAccount(id)}>删除</button>
</div>
</div>
<button className="btn btn-danger" onClick={() => deleteAccount(acc.email || acc.mobile)}>删除</button>
</div>
))}
)
})}
</div>
) : (
<div className="empty-state">暂无账号</div>

View File

@@ -1,10 +1,10 @@
import { useState } from 'react'
const MODELS = [
{ id: 'deepseek-chat', name: 'DeepSeek V3 (Chat)' },
{ id: 'deepseek-reasoner', name: 'DeepSeek R1 (Reasoner)' },
{ id: 'deepseek-chat-search', name: 'DeepSeek V3 + 搜索' },
{ id: 'deepseek-reasoner-search', name: 'DeepSeek R1 + 搜索' },
{ id: 'deepseek-chat', name: 'deepseek-chat' },
{ id: 'deepseek-reasoner', name: 'deepseek-reasoner' },
{ id: 'deepseek-chat-search', name: 'deepseek-chat-search' },
{ id: 'deepseek-reasoner-search', name: 'deepseek-reasoner-search' },
]
export default function ApiTester({ config, onMessage }) {

View File

@@ -301,7 +301,9 @@ textarea.form-input {
}
@keyframes spin {
to { transform: rotate(360deg); }
to {
transform: rotate(360deg);
}
}
.modal-overlay {
@@ -397,25 +399,72 @@ textarea.form-input {
color: var(--text-secondary);
}
/* Queue Status */
.queue-status {
padding: 0.75rem;
background: var(--bg-tertiary);
border-radius: var(--radius);
}
.stat-row {
display: flex;
align-items: center;
gap: 0.5rem;
flex-wrap: wrap;
}
.stat-value {
font-size: 1.25rem;
font-weight: 700;
margin-right: 0.5rem;
}
.stat-success {
color: var(--success);
}
.stat-warning {
color: var(--warning);
}
.stat-detail {
margin-top: 0.5rem;
font-size: 0.85rem;
color: var(--text-secondary);
font-family: 'Monaco', 'Menlo', monospace;
}
/* Button Group Inline */
.btn-group-inline {
display: flex;
gap: 0.5rem;
align-items: center;
}
.btn-sm {
padding: 0.4rem 0.8rem;
font-size: 0.8rem;
}
@media (max-width: 640px) {
.app {
padding: 1rem;
}
.tabs {
flex-direction: column;
}
.tab {
text-align: center;
}
.btn-group {
flex-direction: column;
}
.btn {
width: 100%;
justify-content: center;
}
}
}