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/status", api.CORS(api.HandleTeamProcessStatus))
|
||||||
mux.HandleFunc("/api/team/stop", api.CORS(api.HandleTeamProcessStop))
|
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() {
|
if web.IsEmbedded() {
|
||||||
webFS := web.GetFileSystem()
|
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 [checkingHealth, setCheckingHealth] = useState(false)
|
||||||
const [autoPauseEnabled, setAutoPauseEnabled] = useState(false)
|
const [autoPauseEnabled, setAutoPauseEnabled] = useState(false)
|
||||||
|
|
||||||
// 配置表单状态
|
// 配置表单状态 - 从后端 SQLite 加载
|
||||||
const [targetInput, setTargetInput] = useState(50)
|
const [targetInput, setTargetInput] = useState(50)
|
||||||
const [autoAdd, setAutoAdd] = useState(false)
|
const [autoAdd, setAutoAdd] = useState(false)
|
||||||
const [minInterval, setMinInterval] = useState(300)
|
const [minInterval, setMinInterval] = useState(300)
|
||||||
const [pollingEnabled, setPollingEnabled] = useState(false)
|
const [pollingEnabled, setPollingEnabled] = useState(false)
|
||||||
const [pollingInterval, setPollingInterval] = useState(60)
|
const [pollingInterval, setPollingInterval] = useState(60)
|
||||||
|
|
||||||
|
// 倒计时状态
|
||||||
|
const [countdown, setCountdown] = useState(60)
|
||||||
|
|
||||||
// 使用后端 S2A 代理访问 S2A 服务器
|
// 使用后端 S2A 代理访问 S2A 服务器
|
||||||
const proxyBase = '/api/s2a/proxy'
|
const proxyBase = '/api/s2a/proxy'
|
||||||
|
|
||||||
@@ -128,10 +131,29 @@ export default function Monitor() {
|
|||||||
setRefreshing(false)
|
setRefreshing(false)
|
||||||
}, [targetInput, autoAdd, minInterval, pollingEnabled, pollingInterval])
|
}, [targetInput, autoAdd, minInterval, pollingEnabled, pollingInterval])
|
||||||
|
|
||||||
// 设置目标 - 本地状态管理(S2A 没有此 API)
|
// 设置目标 - 保存到后端 SQLite
|
||||||
const handleSetTarget = async () => {
|
const handleSetTarget = async () => {
|
||||||
setLoading(true)
|
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 ? {
|
setPoolStatus(prev => prev ? {
|
||||||
...prev,
|
...prev,
|
||||||
target: targetInput,
|
target: targetInput,
|
||||||
@@ -144,13 +166,29 @@ export default function Monitor() {
|
|||||||
setLoading(false)
|
setLoading(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 控制轮询 - 本地状态管理
|
// 控制轮询 - 保存到后端 SQLite
|
||||||
const handleTogglePolling = async () => {
|
const handleTogglePolling = async () => {
|
||||||
setLoading(true)
|
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 ? {
|
setPoolStatus(prev => prev ? {
|
||||||
...prev,
|
...prev,
|
||||||
polling_enabled: !pollingEnabled,
|
polling_enabled: newPollingEnabled,
|
||||||
polling_interval: pollingInterval,
|
polling_interval: pollingInterval,
|
||||||
} : null)
|
} : null)
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
@@ -186,13 +224,55 @@ export default function Monitor() {
|
|||||||
setAutoAddLogs([])
|
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(() => {
|
useEffect(() => {
|
||||||
|
loadMonitorSettings()
|
||||||
fetchPoolStatus()
|
fetchPoolStatus()
|
||||||
refreshStats()
|
refreshStats()
|
||||||
fetchAutoAddLogs()
|
fetchAutoAddLogs()
|
||||||
}, [fetchPoolStatus, refreshStats])
|
}, [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(
|
const healthySummary = healthResults.reduce(
|
||||||
(acc, r) => {
|
(acc, r) => {
|
||||||
@@ -309,7 +389,7 @@ export default function Monitor() {
|
|||||||
{pollingEnabled && (
|
{pollingEnabled && (
|
||||||
<p className="mt-2 text-xs text-slate-500 flex items-center gap-1">
|
<p className="mt-2 text-xs text-slate-500 flex items-center gap-1">
|
||||||
<Clock className="h-3 w-3" />
|
<Clock className="h-3 w-3" />
|
||||||
每 {pollingInterval} 秒刷新
|
<span className="font-mono text-green-500">{countdown}s</span> 后刷新 (每 {pollingInterval} 秒)
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|||||||
Reference in New Issue
Block a user