feat: Introduce a new monitoring and configuration dashboard with backend API for autopool management and S2A integration.

This commit is contained in:
2026-02-02 07:55:22 +08:00
parent 5a3b3aa8ef
commit 3d026b2010
8 changed files with 722 additions and 68 deletions

View File

@@ -13,7 +13,9 @@ import {
Globe,
Wifi,
WifiOff,
HelpCircle
HelpCircle,
Zap,
Monitor
} from 'lucide-react'
import { Card, CardHeader, CardTitle, CardContent, Button, Input } from '../components/common'
import { useConfig } from '../hooks/useConfig'
@@ -33,6 +35,8 @@ export default function Config() {
const [teamRegProxyStatus, setTeamRegProxyStatus] = useState<'unknown' | 'success' | 'error'>('unknown')
const [proxyOriginIP, setProxyOriginIP] = useState('')
const [teamRegProxyIP, setTeamRegProxyIP] = useState('')
const [authMethod, setAuthMethod] = useState<'api' | 'browser'>('browser')
const [savingAuthMethod, setSavingAuthMethod] = useState(false)
const { toasts, toast, removeToast } = useToast()
// 加载站点名称和代理配置
@@ -61,6 +65,10 @@ export default function Config() {
if (data.data.team_reg_proxy_test_ip) {
setTeamRegProxyIP(data.data.team_reg_proxy_test_ip)
}
// 加载授权方式
if (data.data.auth_method) {
setAuthMethod(data.data.auth_method === 'api' ? 'api' : 'browser')
}
}
} catch (error) {
console.error('Failed to fetch config:', error)
@@ -204,6 +212,33 @@ export default function Config() {
}
}
// 保存授权方式
const handleSaveAuthMethod = async (method: 'api' | 'browser') => {
setSavingAuthMethod(true)
const previousMethod = authMethod
setAuthMethod(method) // 立即更新 UI
try {
const res = await fetch('/api/config', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ auth_method: method }),
})
const data = await res.json()
if (data.code === 0) {
toast.success(`授权方式已切换为 ${method === 'api' ? 'CodexAuth API' : '浏览器模拟'}`)
refreshConfig()
} else {
setAuthMethod(previousMethod) // 回滚
toast.error(data.message || '保存失败')
}
} catch {
setAuthMethod(previousMethod) // 回滚
toast.error('网络错误')
} finally {
setSavingAuthMethod(false)
}
}
const configItems = [
{
to: '/config/s2a',
@@ -311,7 +346,91 @@ export default function Config() {
</p>
</div>
{/* 代理地址配置 */}
{/* 授权方式配置 */}
<div className="pt-4 border-t border-slate-200 dark:border-slate-700">
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-3">
</label>
<div className="grid grid-cols-2 gap-3">
{/* API 模式 - CodexAuth */}
<button
onClick={() => handleSaveAuthMethod('api')}
disabled={savingAuthMethod}
className={`relative flex flex-col items-center gap-2 p-4 rounded-xl border-2 transition-all duration-200 ${authMethod === 'api'
? 'border-blue-500 bg-gradient-to-br from-blue-50 to-indigo-50 dark:from-blue-900/30 dark:to-indigo-900/30 shadow-sm'
: 'border-slate-200 dark:border-slate-700 hover:border-blue-300 dark:hover:border-blue-800 hover:bg-slate-50 dark:hover:bg-slate-800/50'
}`}
>
{authMethod === 'api' && (
<div className="absolute top-2 right-2">
<CheckCircle className="h-4 w-4 text-blue-500" />
</div>
)}
<div className={`p-3 rounded-lg ${authMethod === 'api'
? 'bg-blue-500 text-white'
: 'bg-slate-100 dark:bg-slate-800 text-slate-600 dark:text-slate-400'
}`}>
<Zap className="h-6 w-6" />
</div>
<div className="text-center">
<div className={`font-semibold ${authMethod === 'api'
? 'text-blue-700 dark:text-blue-300'
: 'text-slate-700 dark:text-slate-300'
}`}>
CodexAuth API
</div>
<div className="text-xs text-slate-500 dark:text-slate-400 mt-1">
API
</div>
</div>
</button>
{/* 浏览器模式 */}
<button
onClick={() => handleSaveAuthMethod('browser')}
disabled={savingAuthMethod}
className={`relative flex flex-col items-center gap-2 p-4 rounded-xl border-2 transition-all duration-200 ${authMethod === 'browser'
? 'border-emerald-500 bg-gradient-to-br from-emerald-50 to-green-50 dark:from-emerald-900/30 dark:to-green-900/30 shadow-sm'
: 'border-slate-200 dark:border-slate-700 hover:border-emerald-300 dark:hover:border-emerald-800 hover:bg-slate-50 dark:hover:bg-slate-800/50'
}`}
>
{authMethod === 'browser' && (
<div className="absolute top-2 right-2">
<CheckCircle className="h-4 w-4 text-emerald-500" />
</div>
)}
<div className={`p-3 rounded-lg ${authMethod === 'browser'
? 'bg-emerald-500 text-white'
: 'bg-slate-100 dark:bg-slate-800 text-slate-600 dark:text-slate-400'
}`}>
<Monitor className="h-6 w-6" />
</div>
<div className="text-center">
<div className={`font-semibold ${authMethod === 'browser'
? 'text-emerald-700 dark:text-emerald-300'
: 'text-slate-700 dark:text-slate-300'
}`}>
</div>
<div className="text-xs text-slate-500 dark:text-slate-400 mt-1">
Chromedp/Rod
</div>
</div>
</button>
</div>
{authMethod === 'api' && (
<div className="mt-3 p-3 rounded-lg bg-gradient-to-r from-blue-500 to-indigo-500 text-white shadow-sm">
<div className="flex items-center gap-2">
<Zap className="h-4 w-4" />
<span className="font-medium">CodexAuth API </span>
</div>
<p className="text-xs text-blue-100 mt-1">
使 API
</p>
</div>
)}
</div>
<div className="pt-4 border-t border-slate-200 dark:border-slate-700">
<div className="flex items-center gap-2 mb-2">
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300">

View File

@@ -13,6 +13,7 @@ import {
CheckCircle,
Clock,
Save,
Monitor as MonitorIcon,
} from 'lucide-react'
import { Card, CardHeader, CardTitle, CardContent, Button, Input, Switch } from '../components/common'
import LiveLogViewer from '../components/LiveLogViewer'
@@ -70,6 +71,7 @@ export default function Monitor() {
const [replenishUseProxy, setReplenishUseProxy] = useState(false) // 补号时使用代理
const [globalProxy, setGlobalProxy] = useState('') // 全局代理地址(只读显示)
const [browserType, setBrowserType] = useState<'chromedp' | 'rod'>('chromedp') // 授权浏览器引擎
const [authMethod, setAuthMethod] = useState<'api' | 'browser'>('browser') // 授权方式
// 倒计时状态
const [countdown, setCountdown] = useState(60)
@@ -264,6 +266,10 @@ export default function Monitor() {
const configJson = await configRes.json()
if (configJson.code === 0 && configJson.data) {
setGlobalProxy(configJson.data.default_proxy || '')
// 加载授权方式配置
if (configJson.data.auth_method) {
setAuthMethod(configJson.data.auth_method === 'api' ? 'api' : 'browser')
}
}
}
@@ -543,37 +549,36 @@ export default function Monitor() {
}
/>
</div>
{/* 浏览器选择器 */}
<div className="space-y-2">
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300">
</label>
<div className="flex rounded-lg overflow-hidden border border-slate-200 dark:border-slate-700">
<button
type="button"
onClick={() => setBrowserType('chromedp')}
disabled={!autoAdd}
className={`flex-1 px-4 py-2.5 text-sm font-medium transition-all duration-200 ${browserType === 'chromedp'
? 'bg-blue-600 text-white'
: 'bg-slate-100 dark:bg-slate-800 text-slate-600 dark:text-slate-400 hover:bg-slate-200 dark:hover:bg-slate-700'
} ${!autoAdd ? 'opacity-50 cursor-not-allowed' : ''}`}
>
Chromedp
</button>
<button
type="button"
onClick={() => setBrowserType('rod')}
disabled={!autoAdd}
className={`flex-1 px-4 py-2.5 text-sm font-medium transition-all duration-200 border-l border-slate-200 dark:border-slate-700 ${browserType === 'rod'
? 'bg-blue-600 text-white'
: 'bg-slate-100 dark:bg-slate-800 text-slate-600 dark:text-slate-400 hover:bg-slate-200 dark:hover:bg-slate-700'
} ${!autoAdd ? 'opacity-50 cursor-not-allowed' : ''}`}
>
Rod
</button>
{/* 授权方式状态显示 */}
<div className={`p-4 rounded-lg border ${authMethod === 'api'
? 'bg-gradient-to-r from-blue-50 to-indigo-50 dark:from-blue-900/20 dark:to-indigo-900/20 border-blue-200 dark:border-blue-800'
: 'bg-gradient-to-r from-emerald-50 to-green-50 dark:from-emerald-900/20 dark:to-green-900/20 border-emerald-200 dark:border-emerald-800'
}`}>
<div className="flex items-center gap-3">
{authMethod === 'api' ? (
<>
<div className="p-2 rounded-lg bg-blue-500 text-white">
<Zap className="h-5 w-5" />
</div>
<div>
<div className="font-semibold text-blue-700 dark:text-blue-300">CodexAuth API </div>
<div className="text-xs text-blue-600/70 dark:text-blue-400/70"> API </div>
</div>
</>
) : (
<>
<div className="p-2 rounded-lg bg-emerald-500 text-white">
<MonitorIcon className="h-5 w-5" />
</div>
<div>
<div className="font-semibold text-emerald-700 dark:text-emerald-300"> ({browserType})</div>
<div className="text-xs text-emerald-600/70 dark:text-emerald-400/70">使</div>
</div>
</>
)}
</div>
<p className="text-xs text-slate-500 dark:text-slate-400">
Chromedp Rod
<p className="mt-2 text-xs text-slate-500 dark:text-slate-400">
</p>
</div>
<Input

View File

@@ -10,6 +10,8 @@ import {
Activity,
CheckCircle,
RefreshCw,
Zap,
Monitor,
} from 'lucide-react'
import { FileDropzone } from '../components/upload'
import LogStream from '../components/upload/LogStream'
@@ -70,14 +72,31 @@ export default function Upload() {
const [membersPerTeam, setMembersPerTeam] = useState(4)
const [concurrentTeams, setConcurrentTeams] = useState(2)
const [concurrentS2A, setConcurrentS2A] = useState(2) // 入库并发数
const [browserType, setBrowserType] = useState<'chromedp' | 'rod'>('chromedp')
const [browserType] = useState<'chromedp' | 'rod'>('chromedp') // 浏览器引擎类型(只读)
const [useProxy, setUseProxy] = useState(false) // 是否使用全局代理
const [includeOwner, setIncludeOwner] = useState(false) // 母号也入库
const [processCount, setProcessCount] = useState(0) // 处理数量0表示全部
const [authMethod, setAuthMethod] = useState<'api' | 'browser'>('browser') // 授权方式
// 获取全局代理地址
const globalProxy = config.proxy?.default || ''
// 加载全局配置中的授权方式
useEffect(() => {
const fetchAuthMethod = async () => {
try {
const res = await fetch('/api/config')
const data = await res.json()
if (data.code === 0 && data.data?.auth_method) {
setAuthMethod(data.data.auth_method === 'api' ? 'api' : 'browser')
}
} catch (error) {
console.error('Failed to fetch auth method:', error)
}
}
fetchAuthMethod()
}, [])
const hasConfig = config.s2a.apiBase && config.s2a.adminKey
// Load stats
@@ -456,31 +475,33 @@ export default function Upload() {
/>
</div>
<div>
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">
</label>
<div className="flex gap-2">
<button
onClick={() => setBrowserType('chromedp')}
disabled={isRunning}
className={`flex-1 px-4 py-2 rounded-lg text-sm font-medium transition-colors ${browserType === 'chromedp'
? 'bg-blue-500 text-white'
: 'bg-slate-100 dark:bg-slate-800 text-slate-600 dark:text-slate-400 hover:bg-slate-200'
}`}
>
Chromedp ()
</button>
<button
onClick={() => setBrowserType('rod')}
disabled={isRunning}
className={`flex-1 px-4 py-2 rounded-lg text-sm font-medium transition-colors ${browserType === 'rod'
? 'bg-blue-500 text-white'
: 'bg-slate-100 dark:bg-slate-800 text-slate-600 dark:text-slate-400 hover:bg-slate-200'
}`}
>
Rod
</button>
{/* 授权方式状态显示 */}
<div className={`p-3 rounded-lg border ${authMethod === 'api'
? 'bg-gradient-to-r from-blue-50 to-indigo-50 dark:from-blue-900/20 dark:to-indigo-900/20 border-blue-200 dark:border-blue-800'
: 'bg-gradient-to-r from-emerald-50 to-green-50 dark:from-emerald-900/20 dark:to-green-900/20 border-emerald-200 dark:border-emerald-800'
}`}>
<div className="flex items-center gap-2">
{authMethod === 'api' ? (
<>
<div className="p-1.5 rounded bg-blue-500 text-white">
<Zap className="h-4 w-4" />
</div>
<div>
<div className="font-medium text-blue-700 dark:text-blue-300">CodexAuth API </div>
<div className="text-xs text-blue-600/70 dark:text-blue-400/70"> API </div>
</div>
</>
) : (
<>
<div className="p-1.5 rounded bg-emerald-500 text-white">
<Monitor className="h-4 w-4" />
</div>
<div>
<div className="font-medium text-emerald-700 dark:text-emerald-300"> ({browserType})</div>
<div className="text-xs text-emerald-600/70 dark:text-emerald-400/70">使</div>
</div>
</>
)}
</div>
</div>