feat: add monitoring feature with backend API and frontend UI for settings.
This commit is contained in:
@@ -114,6 +114,10 @@ func startServer(cfg *config.Config) {
|
||||
mux.HandleFunc("/api/team/status", api.CORS(api.HandleTeamProcessStatus))
|
||||
mux.HandleFunc("/api/team/stop", api.CORS(api.HandleTeamProcessStop))
|
||||
|
||||
// 监控设置 API
|
||||
mux.HandleFunc("/api/monitor/settings", api.CORS(api.HandleGetMonitorSettings))
|
||||
mux.HandleFunc("/api/monitor/settings/save", api.CORS(api.HandleSaveMonitorSettings))
|
||||
|
||||
// 嵌入的前端静态文件
|
||||
if web.IsEmbedded() {
|
||||
webFS := web.GetFileSystem()
|
||||
|
||||
94
backend/internal/api/monitor.go
Normal file
94
backend/internal/api/monitor.go
Normal file
@@ -0,0 +1,94 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"codex-pool/internal/database"
|
||||
)
|
||||
|
||||
// MonitorSettings 监控设置
|
||||
type MonitorSettings struct {
|
||||
Target int `json:"target"`
|
||||
AutoAdd bool `json:"auto_add"`
|
||||
MinInterval int `json:"min_interval"`
|
||||
PollingEnabled bool `json:"polling_enabled"`
|
||||
PollingInterval int `json:"polling_interval"`
|
||||
}
|
||||
|
||||
// HandleGetMonitorSettings 获取监控设置
|
||||
func HandleGetMonitorSettings(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
Error(w, http.StatusMethodNotAllowed, "仅支持 GET")
|
||||
return
|
||||
}
|
||||
|
||||
if database.Instance == nil {
|
||||
Error(w, http.StatusInternalServerError, "数据库未初始化")
|
||||
return
|
||||
}
|
||||
|
||||
settings := MonitorSettings{
|
||||
Target: 50,
|
||||
AutoAdd: false,
|
||||
MinInterval: 300,
|
||||
PollingEnabled: false,
|
||||
PollingInterval: 60,
|
||||
}
|
||||
|
||||
if val, _ := database.Instance.GetConfig("monitor_target"); val != "" {
|
||||
if v, err := strconv.Atoi(val); err == nil {
|
||||
settings.Target = v
|
||||
}
|
||||
}
|
||||
if val, _ := database.Instance.GetConfig("monitor_auto_add"); val == "true" {
|
||||
settings.AutoAdd = true
|
||||
}
|
||||
if val, _ := database.Instance.GetConfig("monitor_min_interval"); val != "" {
|
||||
if v, err := strconv.Atoi(val); err == nil {
|
||||
settings.MinInterval = v
|
||||
}
|
||||
}
|
||||
if val, _ := database.Instance.GetConfig("monitor_polling_enabled"); val == "true" {
|
||||
settings.PollingEnabled = true
|
||||
}
|
||||
if val, _ := database.Instance.GetConfig("monitor_polling_interval"); val != "" {
|
||||
if v, err := strconv.Atoi(val); err == nil {
|
||||
settings.PollingInterval = v
|
||||
}
|
||||
}
|
||||
|
||||
Success(w, settings)
|
||||
}
|
||||
|
||||
// HandleSaveMonitorSettings 保存监控设置
|
||||
func HandleSaveMonitorSettings(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
Error(w, http.StatusMethodNotAllowed, "仅支持 POST")
|
||||
return
|
||||
}
|
||||
|
||||
if database.Instance == nil {
|
||||
Error(w, http.StatusInternalServerError, "数据库未初始化")
|
||||
return
|
||||
}
|
||||
|
||||
var settings MonitorSettings
|
||||
if err := json.NewDecoder(r.Body).Decode(&settings); err != nil {
|
||||
Error(w, http.StatusBadRequest, "解析请求失败")
|
||||
return
|
||||
}
|
||||
|
||||
// 保存到数据库
|
||||
database.Instance.SetConfig("monitor_target", strconv.Itoa(settings.Target))
|
||||
database.Instance.SetConfig("monitor_auto_add", strconv.FormatBool(settings.AutoAdd))
|
||||
database.Instance.SetConfig("monitor_min_interval", strconv.Itoa(settings.MinInterval))
|
||||
database.Instance.SetConfig("monitor_polling_enabled", strconv.FormatBool(settings.PollingEnabled))
|
||||
database.Instance.SetConfig("monitor_polling_interval", strconv.Itoa(settings.PollingInterval))
|
||||
|
||||
Success(w, map[string]interface{}{
|
||||
"message": "设置已保存",
|
||||
"settings": settings,
|
||||
})
|
||||
}
|
||||
@@ -58,13 +58,16 @@ export default function Monitor() {
|
||||
const [checkingHealth, setCheckingHealth] = useState(false)
|
||||
const [autoPauseEnabled, setAutoPauseEnabled] = useState(false)
|
||||
|
||||
// 配置表单状态
|
||||
// 配置表单状态 - 从后端 SQLite 加载
|
||||
const [targetInput, setTargetInput] = useState(50)
|
||||
const [autoAdd, setAutoAdd] = useState(false)
|
||||
const [minInterval, setMinInterval] = useState(300)
|
||||
const [pollingEnabled, setPollingEnabled] = useState(false)
|
||||
const [pollingInterval, setPollingInterval] = useState(60)
|
||||
|
||||
// 倒计时状态
|
||||
const [countdown, setCountdown] = useState(60)
|
||||
|
||||
// 使用后端 S2A 代理访问 S2A 服务器
|
||||
const proxyBase = '/api/s2a/proxy'
|
||||
|
||||
@@ -128,10 +131,29 @@ export default function Monitor() {
|
||||
setRefreshing(false)
|
||||
}, [targetInput, autoAdd, minInterval, pollingEnabled, pollingInterval])
|
||||
|
||||
// 设置目标 - 本地状态管理(S2A 没有此 API)
|
||||
// 设置目标 - 保存到后端 SQLite
|
||||
const handleSetTarget = async () => {
|
||||
setLoading(true)
|
||||
// 由于 S2A 没有 pool/target API,这里只更新本地状态
|
||||
try {
|
||||
// 保存到后端数据库
|
||||
const res = await fetch('/api/monitor/settings/save', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
target: targetInput,
|
||||
auto_add: autoAdd,
|
||||
min_interval: minInterval,
|
||||
polling_enabled: pollingEnabled,
|
||||
polling_interval: pollingInterval,
|
||||
}),
|
||||
})
|
||||
if (!res.ok) {
|
||||
console.error('保存设置失败:', res.status)
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('保存设置失败:', e)
|
||||
}
|
||||
// 更新本地状态
|
||||
setPoolStatus(prev => prev ? {
|
||||
...prev,
|
||||
target: targetInput,
|
||||
@@ -144,13 +166,29 @@ export default function Monitor() {
|
||||
setLoading(false)
|
||||
}
|
||||
|
||||
// 控制轮询 - 本地状态管理
|
||||
// 控制轮询 - 保存到后端 SQLite
|
||||
const handleTogglePolling = async () => {
|
||||
setLoading(true)
|
||||
setPollingEnabled(!pollingEnabled)
|
||||
const newPollingEnabled = !pollingEnabled
|
||||
setPollingEnabled(newPollingEnabled)
|
||||
try {
|
||||
await fetch('/api/monitor/settings/save', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
target: targetInput,
|
||||
auto_add: autoAdd,
|
||||
min_interval: minInterval,
|
||||
polling_enabled: newPollingEnabled,
|
||||
polling_interval: pollingInterval,
|
||||
}),
|
||||
})
|
||||
} catch (e) {
|
||||
console.error('保存轮询设置失败:', e)
|
||||
}
|
||||
setPoolStatus(prev => prev ? {
|
||||
...prev,
|
||||
polling_enabled: !pollingEnabled,
|
||||
polling_enabled: newPollingEnabled,
|
||||
polling_interval: pollingInterval,
|
||||
} : null)
|
||||
setLoading(false)
|
||||
@@ -186,13 +224,55 @@ export default function Monitor() {
|
||||
setAutoAddLogs([])
|
||||
}
|
||||
|
||||
// 从后端加载监控设置
|
||||
const loadMonitorSettings = async () => {
|
||||
try {
|
||||
const res = await fetch('/api/monitor/settings')
|
||||
if (res.ok) {
|
||||
const json = await res.json()
|
||||
if (json.code === 0 && json.data) {
|
||||
const s = json.data
|
||||
setTargetInput(s.target || 50)
|
||||
setAutoAdd(s.auto_add || false)
|
||||
setMinInterval(s.min_interval || 300)
|
||||
setPollingEnabled(s.polling_enabled || false)
|
||||
setPollingInterval(s.polling_interval || 60)
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('加载监控设置失败:', e)
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化
|
||||
useEffect(() => {
|
||||
loadMonitorSettings()
|
||||
fetchPoolStatus()
|
||||
refreshStats()
|
||||
fetchAutoAddLogs()
|
||||
}, [fetchPoolStatus, refreshStats])
|
||||
|
||||
// 倒计时定时器 - 当启用轮询时
|
||||
useEffect(() => {
|
||||
// 初始化倒计时
|
||||
setCountdown(pollingInterval)
|
||||
|
||||
if (!pollingEnabled) return
|
||||
|
||||
const timer = setInterval(() => {
|
||||
setCountdown(prev => {
|
||||
if (prev <= 1) {
|
||||
// 倒计时结束,刷新数据并重置
|
||||
refreshStats()
|
||||
return pollingInterval
|
||||
}
|
||||
return prev - 1
|
||||
})
|
||||
}, 1000)
|
||||
|
||||
return () => clearInterval(timer)
|
||||
}, [pollingEnabled, pollingInterval, refreshStats])
|
||||
|
||||
// 计算健康状态
|
||||
const healthySummary = healthResults.reduce(
|
||||
(acc, r) => {
|
||||
@@ -309,7 +389,7 @@ export default function Monitor() {
|
||||
{pollingEnabled && (
|
||||
<p className="mt-2 text-xs text-slate-500 flex items-center gap-1">
|
||||
<Clock className="h-3 w-3" />
|
||||
每 {pollingInterval} 秒刷新
|
||||
<span className="font-mono text-green-500">{countdown}s</span> 后刷新 (每 {pollingInterval} 秒)
|
||||
</p>
|
||||
)}
|
||||
</CardContent>
|
||||
|
||||
Reference in New Issue
Block a user