feat: add monitor admin panel for provider/server management and challenge generation
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useMemo, useState } from 'react'
|
||||||
import api from '@/services/api'
|
import api from '@/services/api'
|
||||||
|
|
||||||
interface ProviderRow {
|
interface ProviderRow {
|
||||||
@@ -38,25 +38,116 @@ interface OverviewData {
|
|||||||
generated_at: string
|
generated_at: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface AdminUser {
|
||||||
|
id: number
|
||||||
|
is_admin: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ProviderAccountItem {
|
||||||
|
id: number
|
||||||
|
provider: string
|
||||||
|
label: string
|
||||||
|
is_enabled: boolean
|
||||||
|
credential_masked: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ServerItem {
|
||||||
|
server_id: number
|
||||||
|
identifier: string
|
||||||
|
display_name: string
|
||||||
|
online: boolean
|
||||||
|
}
|
||||||
|
|
||||||
export default function MonitorPage() {
|
export default function MonitorPage() {
|
||||||
const [data, setData] = useState<OverviewData | null>(null)
|
const [data, setData] = useState<OverviewData | null>(null)
|
||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true)
|
||||||
|
const [isAdmin, setIsAdmin] = useState(false)
|
||||||
|
const [providerAccounts, setProviderAccounts] = useState<ProviderAccountItem[]>([])
|
||||||
|
const [servers, setServers] = useState<ServerItem[]>([])
|
||||||
|
|
||||||
|
const [providerForm, setProviderForm] = useState({ provider: 'openai', label: '', credential: '' })
|
||||||
|
const [providerTestMsg, setProviderTestMsg] = useState('')
|
||||||
|
const [serverForm, setServerForm] = useState({ identifier: '', display_name: '' })
|
||||||
|
|
||||||
|
const canAdmin = useMemo(() => !!localStorage.getItem('token') && isAdmin, [isAdmin])
|
||||||
|
|
||||||
const load = async () => {
|
const load = async () => {
|
||||||
try {
|
try {
|
||||||
const res = await api.get<OverviewData>('/monitor/public/overview')
|
const res = await api.get<OverviewData>('/monitor/public/overview')
|
||||||
setData(res.data)
|
setData(res.data)
|
||||||
|
const token = localStorage.getItem('token')
|
||||||
|
if (token) {
|
||||||
|
try {
|
||||||
|
const me = await api.get<AdminUser>('/auth/me')
|
||||||
|
setIsAdmin(!!me.data.is_admin)
|
||||||
|
} catch {
|
||||||
|
setIsAdmin(false)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setIsAdmin(false)
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const loadAdminData = async () => {
|
||||||
|
if (!canAdmin) return
|
||||||
|
const [p, s] = await Promise.all([
|
||||||
|
api.get<ProviderAccountItem[]>('/monitor/admin/providers/accounts'),
|
||||||
|
api.get<ServerItem[]>('/monitor/admin/servers'),
|
||||||
|
])
|
||||||
|
setProviderAccounts(p.data)
|
||||||
|
setServers(s.data)
|
||||||
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
load()
|
load()
|
||||||
const t = setInterval(load, 30000)
|
const t = setInterval(load, 30000)
|
||||||
return () => clearInterval(t)
|
return () => clearInterval(t)
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
loadAdminData()
|
||||||
|
}, [canAdmin])
|
||||||
|
|
||||||
|
const testProvider = async () => {
|
||||||
|
const r = await api.post<{ ok: boolean; message: string }>('/monitor/admin/providers/test', {
|
||||||
|
provider: providerForm.provider,
|
||||||
|
credential: providerForm.credential,
|
||||||
|
})
|
||||||
|
setProviderTestMsg((r.data.ok ? '✅ ' : '❌ ') + r.data.message)
|
||||||
|
}
|
||||||
|
|
||||||
|
const addProvider = async () => {
|
||||||
|
await api.post('/monitor/admin/providers/accounts', providerForm)
|
||||||
|
setProviderForm({ ...providerForm, label: '', credential: '' })
|
||||||
|
await loadAdminData()
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleteProvider = async (id: number) => {
|
||||||
|
await api.delete('/monitor/admin/providers/accounts/' + id)
|
||||||
|
await loadAdminData()
|
||||||
|
}
|
||||||
|
|
||||||
|
const addServer = async () => {
|
||||||
|
await api.post('/monitor/admin/servers', serverForm)
|
||||||
|
setServerForm({ identifier: '', display_name: '' })
|
||||||
|
await loadAdminData()
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleteServer = async (id: number) => {
|
||||||
|
await api.delete('/monitor/admin/servers/' + id)
|
||||||
|
await loadAdminData()
|
||||||
|
}
|
||||||
|
|
||||||
|
const createChallenge = async (id: number) => {
|
||||||
|
const r = await api.post<{ identifier: string; challenge_uuid: string; expires_at: string }>('/monitor/admin/servers/' + id + '/challenge')
|
||||||
|
alert('identifier=' + r.data.identifier + '
|
||||||
|
challenge_uuid=' + r.data.challenge_uuid + '
|
||||||
|
expires_at=' + r.data.expires_at)
|
||||||
|
}
|
||||||
|
|
||||||
if (loading) return <div className='loading'>Monitor loading...</div>
|
if (loading) return <div className='loading'>Monitor loading...</div>
|
||||||
if (!data) return <div className='loading'>Monitor load failed</div>
|
if (!data) return <div className='loading'>Monitor load failed</div>
|
||||||
|
|
||||||
@@ -80,9 +171,9 @@ export default function MonitorPage() {
|
|||||||
{data.providers.map((p) => (
|
{data.providers.map((p) => (
|
||||||
<li key={p.account_id}>
|
<li key={p.account_id}>
|
||||||
<strong>{p.provider}</strong> / {p.label} · status: {p.status}
|
<strong>{p.provider}</strong> / {p.label} · status: {p.status}
|
||||||
{p.usage_pct !== null ? : ''}
|
{p.usage_pct !== null ? (' · usage: ' + p.usage_pct + '%') : ''}
|
||||||
{p.reset_at ? : ''}
|
{p.reset_at ? (' · reset: ' + p.reset_at) : ''}
|
||||||
{p.error ? : ''}
|
{p.error ? (' · error: ' + p.error) : ''}
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
@@ -108,6 +199,47 @@ export default function MonitorPage() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
{canAdmin && (
|
||||||
|
<section style={{ marginTop: 24 }}>
|
||||||
|
<h3>Admin: Provider 管理</h3>
|
||||||
|
<div style={{ display: 'flex', gap: 8, flexWrap: 'wrap' }}>
|
||||||
|
<select value={providerForm.provider} onChange={(e) => setProviderForm({ ...providerForm, provider: e.target.value })}>
|
||||||
|
<option value='openai'>openai</option>
|
||||||
|
<option value='anthropic'>anthropic</option>
|
||||||
|
<option value='minimax'>minimax</option>
|
||||||
|
<option value='kimi'>kimi</option>
|
||||||
|
<option value='qwen'>qwen</option>
|
||||||
|
</select>
|
||||||
|
<input placeholder='label' value={providerForm.label} onChange={(e) => setProviderForm({ ...providerForm, label: e.target.value })} />
|
||||||
|
<input placeholder='credential' value={providerForm.credential} onChange={(e) => setProviderForm({ ...providerForm, credential: e.target.value })} />
|
||||||
|
<button onClick={testProvider}>测试连接</button>
|
||||||
|
<button onClick={addProvider}>添加账号</button>
|
||||||
|
</div>
|
||||||
|
{providerTestMsg && <p>{providerTestMsg}</p>}
|
||||||
|
<ul>
|
||||||
|
{providerAccounts.map((p) => (
|
||||||
|
<li key={p.id}>{p.provider} / {p.label} / {p.credential_masked} <button onClick={() => deleteProvider(p.id)}>删除</button></li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h3>Admin: 服务器管理</h3>
|
||||||
|
<div style={{ display: 'flex', gap: 8, flexWrap: 'wrap' }}>
|
||||||
|
<input placeholder='identifier' value={serverForm.identifier} onChange={(e) => setServerForm({ ...serverForm, identifier: e.target.value })} />
|
||||||
|
<input placeholder='display_name' value={serverForm.display_name} onChange={(e) => setServerForm({ ...serverForm, display_name: e.target.value })} />
|
||||||
|
<button onClick={addServer}>添加服务器</button>
|
||||||
|
</div>
|
||||||
|
<ul>
|
||||||
|
{servers.map((s) => (
|
||||||
|
<li key={s.server_id}>
|
||||||
|
{s.display_name} ({s.identifier})
|
||||||
|
<button onClick={() => createChallenge(s.server_id)} style={{ marginLeft: 8 }}>生成挑战</button>
|
||||||
|
<button onClick={() => deleteServer(s.server_id)} style={{ marginLeft: 8 }}>删除</button>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user