|
@@ -3,6 +3,7 @@ package api
|
|
|
import (
|
|
import (
|
|
|
"encoding/json"
|
|
"encoding/json"
|
|
|
"net/http"
|
|
"net/http"
|
|
|
|
|
+ "sort"
|
|
|
"strconv"
|
|
"strconv"
|
|
|
"strings"
|
|
"strings"
|
|
|
"sync"
|
|
"sync"
|
|
@@ -12,14 +13,24 @@ import (
|
|
|
|
|
|
|
|
"AI-Status-Light/internal/database"
|
|
"AI-Status-Light/internal/database"
|
|
|
"AI-Status-Light/internal/logger"
|
|
"AI-Status-Light/internal/logger"
|
|
|
|
|
+ "AI-Status-Light/internal/web"
|
|
|
)
|
|
)
|
|
|
|
|
|
|
|
|
|
+type ClientStatus struct {
|
|
|
|
|
+ Port int `json:"port"`
|
|
|
|
|
+ Status string `json:"status"`
|
|
|
|
|
+ Code string `json:"code"`
|
|
|
|
|
+ Timestamp string `json:"timestamp"`
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
type Server struct {
|
|
type Server struct {
|
|
|
db *database.DB
|
|
db *database.DB
|
|
|
server *http.Server
|
|
server *http.Server
|
|
|
clients map[*websocket.Conn]bool
|
|
clients map[*websocket.Conn]bool
|
|
|
clientsMu sync.Mutex
|
|
clientsMu sync.Mutex
|
|
|
upgrader websocket.Upgrader
|
|
upgrader websocket.Upgrader
|
|
|
|
|
+ statusMap map[int]*ClientStatus
|
|
|
|
|
+ statusMu sync.RWMutex
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
type Response struct {
|
|
type Response struct {
|
|
@@ -30,8 +41,9 @@ type Response struct {
|
|
|
|
|
|
|
|
func New(db *database.DB, addr string) *Server {
|
|
func New(db *database.DB, addr string) *Server {
|
|
|
s := &Server{
|
|
s := &Server{
|
|
|
- db: db,
|
|
|
|
|
- clients: make(map[*websocket.Conn]bool),
|
|
|
|
|
|
|
+ db: db,
|
|
|
|
|
+ clients: make(map[*websocket.Conn]bool),
|
|
|
|
|
+ statusMap: make(map[int]*ClientStatus),
|
|
|
upgrader: websocket.Upgrader{
|
|
upgrader: websocket.Upgrader{
|
|
|
CheckOrigin: func(r *http.Request) bool {
|
|
CheckOrigin: func(r *http.Request) bool {
|
|
|
return true
|
|
return true
|
|
@@ -40,11 +52,12 @@ func New(db *database.DB, addr string) *Server {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
mux := http.NewServeMux()
|
|
mux := http.NewServeMux()
|
|
|
|
|
+ mux.HandleFunc("/api/clients", s.handleClients)
|
|
|
mux.HandleFunc("/api/mqtt", s.handleMQTT)
|
|
mux.HandleFunc("/api/mqtt", s.handleMQTT)
|
|
|
mux.HandleFunc("/api/mqtt/", s.handleMQTTByID)
|
|
mux.HandleFunc("/api/mqtt/", s.handleMQTTByID)
|
|
|
mux.HandleFunc("/api/health", s.handleHealth)
|
|
mux.HandleFunc("/api/health", s.handleHealth)
|
|
|
mux.HandleFunc("/ws", s.handleWebSocket)
|
|
mux.HandleFunc("/ws", s.handleWebSocket)
|
|
|
- mux.HandleFunc("/", s.handleIndex)
|
|
|
|
|
|
|
+ mux.HandleFunc("/", web.Handler())
|
|
|
|
|
|
|
|
s.server = &http.Server{
|
|
s.server = &http.Server{
|
|
|
Addr: addr,
|
|
Addr: addr,
|
|
@@ -81,6 +94,26 @@ func (s *Server) handleHealth(w http.ResponseWriter, r *http.Request) {
|
|
|
writeJSON(w, http.StatusOK, Response{Code: 0, Message: "ok"})
|
|
writeJSON(w, http.StatusOK, Response{Code: 0, Message: "ok"})
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+func (s *Server) handleClients(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
+ if r.Method != http.MethodGet {
|
|
|
|
|
+ writeJSON(w, http.StatusMethodNotAllowed, Response{Code: -1, Message: "方法不允许"})
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ s.statusMu.RLock()
|
|
|
|
|
+ result := make([]*ClientStatus, 0, len(s.statusMap))
|
|
|
|
|
+ for _, cs := range s.statusMap {
|
|
|
|
|
+ result = append(result, cs)
|
|
|
|
|
+ }
|
|
|
|
|
+ s.statusMu.RUnlock()
|
|
|
|
|
+
|
|
|
|
|
+ sort.Slice(result, func(i, j int) bool {
|
|
|
|
|
+ return result[i].Port < result[j].Port
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ writeJSON(w, http.StatusOK, Response{Code: 0, Message: "success", Data: result})
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
func (s *Server) handleMQTT(w http.ResponseWriter, r *http.Request) {
|
|
func (s *Server) handleMQTT(w http.ResponseWriter, r *http.Request) {
|
|
|
logger.Debug("HTTP %s %s", r.Method, r.URL.Path)
|
|
logger.Debug("HTTP %s %s", r.Method, r.URL.Path)
|
|
|
switch r.Method {
|
|
switch r.Method {
|
|
@@ -263,6 +296,17 @@ func (s *Server) handleWebSocket(w http.ResponseWriter, r *http.Request) {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
func (s *Server) BroadcastStatus(port int, status string, code string) {
|
|
func (s *Server) BroadcastStatus(port int, status string, code string) {
|
|
|
|
|
+ ts := time.Now().Format(time.RFC3339)
|
|
|
|
|
+
|
|
|
|
|
+ s.statusMu.Lock()
|
|
|
|
|
+ s.statusMap[port] = &ClientStatus{
|
|
|
|
|
+ Port: port,
|
|
|
|
|
+ Status: status,
|
|
|
|
|
+ Code: code,
|
|
|
|
|
+ Timestamp: ts,
|
|
|
|
|
+ }
|
|
|
|
|
+ s.statusMu.Unlock()
|
|
|
|
|
+
|
|
|
s.clientsMu.Lock()
|
|
s.clientsMu.Lock()
|
|
|
defer s.clientsMu.Unlock()
|
|
defer s.clientsMu.Unlock()
|
|
|
|
|
|
|
@@ -274,7 +318,7 @@ func (s *Server) BroadcastStatus(port int, status string, code string) {
|
|
|
"port": port,
|
|
"port": port,
|
|
|
"status": status,
|
|
"status": status,
|
|
|
"code": code,
|
|
"code": code,
|
|
|
- "timestamp": time.Now().Format(time.RFC3339),
|
|
|
|
|
|
|
+ "timestamp": ts,
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
data, err := json.Marshal(payload)
|
|
data, err := json.Marshal(payload)
|
|
@@ -292,132 +336,3 @@ func (s *Server) BroadcastStatus(port int, status string, code string) {
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
-func (s *Server) handleIndex(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
- if r.URL.Path != "/" {
|
|
|
|
|
- http.NotFound(w, r)
|
|
|
|
|
- return
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- html := `<!DOCTYPE html>
|
|
|
|
|
-<html lang="zh-CN">
|
|
|
|
|
-<head>
|
|
|
|
|
- <meta charset="UTF-8">
|
|
|
|
|
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
|
|
|
- <title>OpenCode Monitor</title>
|
|
|
|
|
- <style>
|
|
|
|
|
- * { margin: 0; padding: 0; box-sizing: border-box; }
|
|
|
|
|
- body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #f5f5f5; padding: 20px; }
|
|
|
|
|
- .container { max-width: 1200px; margin: 0 auto; }
|
|
|
|
|
- h1 { color: #333; margin-bottom: 20px; }
|
|
|
|
|
- .status-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 20px; }
|
|
|
|
|
- .status-card { background: white; border-radius: 12px; padding: 24px; box-shadow: 0 2px 8px rgba(0,0,0,0.1); }
|
|
|
|
|
- .status-card h2 { color: #666; font-size: 14px; margin-bottom: 8px; }
|
|
|
|
|
- .status-value { font-size: 32px; font-weight: bold; margin-bottom: 8px; }
|
|
|
|
|
- .status-time { color: #999; font-size: 12px; }
|
|
|
|
|
- .status-空闲 { color: #52c41a; }
|
|
|
|
|
- .status-工作中 { color: #ff4d4f; }
|
|
|
|
|
- .status-思考中 { color: #faad14; }
|
|
|
|
|
- .status-运行中 { color: #1890ff; }
|
|
|
|
|
- .status-完成 { color: #52c41a; }
|
|
|
|
|
- .status-错误 { color: #ff4d4f; }
|
|
|
|
|
- .status-重试中 { color: #faad14; }
|
|
|
|
|
- .status-修改中 { color: #722ed1; }
|
|
|
|
|
- .log { margin-top: 20px; background: white; border-radius: 12px; padding: 24px; box-shadow: 0 2px 8px rgba(0,0,0,0.1); }
|
|
|
|
|
- .log h2 { color: #666; font-size: 14px; margin-bottom: 12px; }
|
|
|
|
|
- .log-list { max-height: 300px; overflow-y: auto; }
|
|
|
|
|
- .log-item { padding: 8px 0; border-bottom: 1px solid #f0f0f0; font-size: 14px; }
|
|
|
|
|
- .log-item:last-child { border-bottom: none; }
|
|
|
|
|
- .log-time { color: #999; margin-right: 8px; }
|
|
|
|
|
- .log-port { color: #1890ff; margin-right: 8px; }
|
|
|
|
|
- .connected { color: #52c41a; font-size: 12px; margin-left: 10px; }
|
|
|
|
|
- .disconnected { color: #ff4d4f; font-size: 12px; margin-left: 10px; }
|
|
|
|
|
- </style>
|
|
|
|
|
-</head>
|
|
|
|
|
-<body>
|
|
|
|
|
- <div class="container">
|
|
|
|
|
- <h1>OpenCode Monitor <span id="connectionStatus" class="disconnected">● 未连接</span></h1>
|
|
|
|
|
- <div class="status-grid" id="statusGrid">
|
|
|
|
|
- <div class="status-card">
|
|
|
|
|
- <h2>当前状态</h2>
|
|
|
|
|
- <div class="status-value" id="currentStatus">等待中...</div>
|
|
|
|
|
- <div class="status-time" id="statusTime"></div>
|
|
|
|
|
- </div>
|
|
|
|
|
- </div>
|
|
|
|
|
- <div class="log">
|
|
|
|
|
- <h2>状态日志</h2>
|
|
|
|
|
- <div class="log-list" id="logList"></div>
|
|
|
|
|
- </div>
|
|
|
|
|
- </div>
|
|
|
|
|
-
|
|
|
|
|
- <script>
|
|
|
|
|
- const statusGrid = document.getElementById('statusGrid');
|
|
|
|
|
- const logList = document.getElementById('logList');
|
|
|
|
|
- const currentStatus = document.getElementById('currentStatus');
|
|
|
|
|
- const statusTime = document.getElementById('statusTime');
|
|
|
|
|
- const connectionStatus = document.getElementById('connectionStatus');
|
|
|
|
|
-
|
|
|
|
|
- let ws = null;
|
|
|
|
|
- let reconnectTimer = null;
|
|
|
|
|
-
|
|
|
|
|
- function connect() {
|
|
|
|
|
- const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
|
|
|
- ws = new WebSocket(protocol + '//' + window.location.host + '/ws');
|
|
|
|
|
-
|
|
|
|
|
- ws.onopen = function() {
|
|
|
|
|
- connectionStatus.textContent = '● 已连接';
|
|
|
|
|
- connectionStatus.className = 'connected';
|
|
|
|
|
- if (reconnectTimer) {
|
|
|
|
|
- clearTimeout(reconnectTimer);
|
|
|
|
|
- reconnectTimer = null;
|
|
|
|
|
- }
|
|
|
|
|
- };
|
|
|
|
|
-
|
|
|
|
|
- ws.onmessage = function(event) {
|
|
|
|
|
- try {
|
|
|
|
|
- const data = JSON.parse(event.data);
|
|
|
|
|
- updateStatus(data);
|
|
|
|
|
- addLog(data);
|
|
|
|
|
- } catch (e) {
|
|
|
|
|
- console.error('解析消息失败:', e);
|
|
|
|
|
- }
|
|
|
|
|
- };
|
|
|
|
|
-
|
|
|
|
|
- ws.onclose = function() {
|
|
|
|
|
- connectionStatus.textContent = '● 未连接';
|
|
|
|
|
- connectionStatus.className = 'disconnected';
|
|
|
|
|
- reconnectTimer = setTimeout(connect, 3000);
|
|
|
|
|
- };
|
|
|
|
|
-
|
|
|
|
|
- ws.onerror = function() {
|
|
|
|
|
- ws.close();
|
|
|
|
|
- };
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- function updateStatus(data) {
|
|
|
|
|
- currentStatus.textContent = data.status;
|
|
|
|
|
- currentStatus.className = 'status-value status-' + data.status;
|
|
|
|
|
- statusTime.textContent = '端口: ' + data.port + ' | 更新时间: ' + new Date().toLocaleTimeString();
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- function addLog(data) {
|
|
|
|
|
- const item = document.createElement('div');
|
|
|
|
|
- item.className = 'log-item';
|
|
|
|
|
- item.innerHTML = '<span class="log-time">' + new Date().toLocaleTimeString() + '</span>' +
|
|
|
|
|
- '<span class="log-port">[:' + data.port + ']</span>' +
|
|
|
|
|
- '<span class="status-' + data.status + '">' + data.status + '</span>';
|
|
|
|
|
- logList.insertBefore(item, logList.firstChild);
|
|
|
|
|
-
|
|
|
|
|
- while (logList.children.length > 50) {
|
|
|
|
|
- logList.removeChild(logList.lastChild);
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- connect();
|
|
|
|
|
- </script>
|
|
|
|
|
-</body>
|
|
|
|
|
-</html>`
|
|
|
|
|
-
|
|
|
|
|
- w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
|
|
|
|
- w.Write([]byte(html))
|
|
|
|
|
-}
|
|
|