mirror of
https://github.com/CJackHwang/ds2api.git
synced 2026-05-04 00:15:28 +08:00
feat: Implement DeepSeek account validation, testing, and queue status monitoring with corresponding admin API endpoints.
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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 }) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user