moki 2 недель назад
Родитель
Сommit
93215c0933
7 измененных файлов с 370 добавлено и 138 удалено
  1. 1 1
      .idea/runConfigurations/Monitor_API_.xml
  2. 1 1
      cmd/monitor/main.go
  3. 48 133
      internal/api/api.go
  4. 33 3
      internal/monitor/monitor.go
  5. 20 0
      internal/web/embed.go
  6. 118 0
      internal/web/index.html
  7. 149 0
      logs

+ 1 - 1
.idea/runConfigurations/Monitor_API_.xml

@@ -3,7 +3,7 @@
     <module name="AI-Status-Light" />
     <working_directory value="$PROJECT_DIR$" />
     <go_parameters value="-gcflags=&quot;all=-N -l&quot;" />
-    <parameters value="monitor --api-addr :8080" />
+    <parameters value="monitor --api-addr :8045" />
     <kind value="FILE" />
     <package value="AI-Status-Light" />
     <filePath value="$PROJECT_DIR$/cmd/monitor/main.go" />

+ 1 - 1
cmd/monitor/main.go

@@ -106,7 +106,7 @@ func runMonitor(args []string) {
 	host := fs.String("host", "127.0.0.1", "主机地址")
 	portsFlag := fs.String("ports", "", "端口列表,逗号分隔 (如: 4096,4097,4098)")
 	scanFlag := fs.String("scan", "", "扫描端口范围 (如: 4096-4100)")
-	intervalFlag := fs.Int("interval", 5, "动态扫描间隔(秒), 默认5")
+	intervalFlag := fs.Int("interval", 1, "动态扫描间隔(秒), 默认1")
 	dbPath := fs.String("db", defaultDBPath, "数据库路径")
 	apiAddr := fs.String("api-addr", "", "API 服务地址 (如: :8080)")
 	logFile := fs.String("log-file", "./logs", "日志文件路径(默认 ./logs/monitor.log)")

+ 48 - 133
internal/api/api.go

@@ -3,6 +3,7 @@ package api
 import (
 	"encoding/json"
 	"net/http"
+	"sort"
 	"strconv"
 	"strings"
 	"sync"
@@ -12,14 +13,24 @@ import (
 
 	"AI-Status-Light/internal/database"
 	"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 {
 	db        *database.DB
 	server    *http.Server
 	clients   map[*websocket.Conn]bool
 	clientsMu sync.Mutex
 	upgrader  websocket.Upgrader
+	statusMap map[int]*ClientStatus
+	statusMu  sync.RWMutex
 }
 
 type Response struct {
@@ -30,8 +41,9 @@ type Response struct {
 
 func New(db *database.DB, addr string) *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{
 			CheckOrigin: func(r *http.Request) bool {
 				return true
@@ -40,11 +52,12 @@ func New(db *database.DB, addr string) *Server {
 	}
 
 	mux := http.NewServeMux()
+	mux.HandleFunc("/api/clients", s.handleClients)
 	mux.HandleFunc("/api/mqtt", s.handleMQTT)
 	mux.HandleFunc("/api/mqtt/", s.handleMQTTByID)
 	mux.HandleFunc("/api/health", s.handleHealth)
 	mux.HandleFunc("/ws", s.handleWebSocket)
-	mux.HandleFunc("/", s.handleIndex)
+	mux.HandleFunc("/", web.Handler())
 
 	s.server = &http.Server{
 		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"})
 }
 
+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) {
 	logger.Debug("HTTP %s %s", r.Method, r.URL.Path)
 	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) {
+	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()
 	defer s.clientsMu.Unlock()
 
@@ -274,7 +318,7 @@ func (s *Server) BroadcastStatus(port int, status string, code string) {
 		"port":      port,
 		"status":    status,
 		"code":      code,
-		"timestamp": time.Now().Format(time.RFC3339),
+		"timestamp": ts,
 	}
 
 	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))
-}

+ 33 - 3
internal/monitor/monitor.go

@@ -52,17 +52,46 @@ func (m *Monitor) CheckHealth() bool {
 }
 
 func (m *Monitor) Run(ctx context.Context) {
+	for {
+		if ctx.Err() != nil {
+			return
+		}
+
+		if !m.CheckHealth() {
+			logger.Debug("端口 %d 健康检查失败,等待重试", m.port)
+			select {
+			case <-ctx.Done():
+				return
+			case <-time.After(5 * time.Second):
+			}
+			continue
+		}
+
+		err := m.connectAndRead(ctx)
+		if ctx.Err() != nil {
+			return
+		}
+		logger.Warn("端口 %d 连接断开: %v, 3秒后重连", m.port, err)
+		select {
+		case <-ctx.Done():
+			return
+		case <-time.After(3 * time.Second):
+		}
+	}
+}
+
+func (m *Monitor) connectAndRead(ctx context.Context) error {
 	req, err := http.NewRequestWithContext(ctx, "GET", m.baseURL+"/event", nil)
 	if err != nil {
 		logger.Error("端口 %d 创建请求失败: %v", m.port, err)
-		return
+		return err
 	}
 	req.Header.Set("Accept", "text/event-stream")
 
 	resp, err := http.DefaultClient.Do(req)
 	if err != nil {
 		logger.Debug("端口 %d 连接事件流失败: %v", m.port, err)
-		return
+		return err
 	}
 	defer resp.Body.Close()
 
@@ -96,7 +125,7 @@ func (m *Monitor) Run(ctx context.Context) {
 		case <-ctx.Done():
 			logger.Debug("端口 %d 上下文已取消,退出事件循环", m.port)
 			close(timeoutChan)
-			return
+			return nil
 		default:
 		}
 
@@ -124,4 +153,5 @@ func (m *Monitor) Run(ctx context.Context) {
 
 	close(timeoutChan)
 	logger.Debug("端口 %d 事件流读取结束", m.port)
+	return scanner.Err()
 }

+ 20 - 0
internal/web/embed.go

@@ -0,0 +1,20 @@
+package web
+
+import (
+	_ "embed"
+	"net/http"
+)
+
+//go:embed index.html
+var indexHTML string
+
+func Handler() http.HandlerFunc {
+	return func(w http.ResponseWriter, r *http.Request) {
+		if r.URL.Path != "/" {
+			http.NotFound(w, r)
+			return
+		}
+		w.Header().Set("Content-Type", "text/html; charset=utf-8")
+		w.Write([]byte(indexHTML))
+	}
+}

+ 118 - 0
internal/web/index.html

@@ -0,0 +1,118 @@
+<!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; 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 { padding: 16px 0; }
+        .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; }
+        .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>

+ 149 - 0
logs

@@ -0,0 +1,149 @@
+[INFO] 2026/06/04 16:28:32 数据库已连接: ./data/config.db
+[INFO] 2026/06/04 16:28:33 MQTT 已连接: tcp://47.92.50.210:9883 (主题: opencode/status)
+[INFO] 2026/06/04 16:28:33 MQTT 已连接: tcp://47.92.50.210:9883
+[INFO] 2026/06/04 16:28:33 API 服务已启动: :8080
+[INFO] 2026/06/04 16:28:33 API 服务器开始监听: :8080
+[WARN] 2026/06/04 16:28:33 MQTT 连接断开: EOF
+[INFO] 2026/06/04 16:28:33 MQTT 已连接: tcp://47.92.50.210:9883
+[INFO] 2026/06/04 16:28:33 开始监控端口: 37081
+[INFO] 2026/06/04 16:28:33 开始监控端口: 4096
+[INFO] 2026/06/04 16:28:33 找到 5 个实例: [4096 37081 42597 44101 46077]
+[INFO] 2026/06/04 16:28:33 动态模式启动,每 1 秒扫描新实例
+[INFO] 2026/06/04 16:28:33 开始监控端口: 46077
+[INFO] 2026/06/04 16:28:33 开始监控端口: 42597
+[INFO] 2026/06/04 16:28:33 开始监控端口: 44101
+[INFO] 2026/06/04 16:28:33 端口 42597 已连接到事件流: http://127.0.0.1:42597/event
+[INFO] 2026/06/04 16:28:33 端口 44101 已连接到事件流: http://127.0.0.1:44101/event
+[INFO] 2026/06/04 16:28:33 端口 46077 已连接到事件流: http://127.0.0.1:46077/event
+[INFO] 2026/06/04 16:28:33 端口 4096 已连接到事件流: http://127.0.0.1:4096/event
+[INFO] 2026/06/04 16:28:33 端口 37081 已连接到事件流: http://127.0.0.1:37081/event
+[WARN] 2026/06/04 16:28:34 MQTT 连接断开: EOF
+[INFO] 2026/06/04 16:28:35 MQTT 已连接: tcp://47.92.50.210:9883
+[INFO] 2026/06/04 16:28:59 收到停止信号,正在退出
+[INFO] 2026/06/04 16:28:59 端口 4096 监控已停止,等待重新连接
+[INFO] 2026/06/04 16:28:59 端口 37081 监控已停止,等待重新连接
+[INFO] 2026/06/04 16:28:59 端口 42597 监控已停止,等待重新连接
+[INFO] 2026/06/04 16:28:59 端口 44101 监控已停止,等待重新连接
+[INFO] 2026/06/04 16:28:59 端口 46077 监控已停止,等待重新连接
+[INFO] 2026/06/04 16:28:59 数据库已连接: ./data/config.db
+[INFO] 2026/06/04 16:28:59 MQTT 已连接: tcp://47.92.50.210:9883 (主题: opencode/status)
+[INFO] 2026/06/04 16:28:59 MQTT 已连接: tcp://47.92.50.210:9883
+[INFO] 2026/06/04 16:28:59 API 服务已启动: :8045
+[INFO] 2026/06/04 16:28:59 API 服务器开始监听: :8045
+[INFO] 2026/06/04 16:29:00 找到 5 个实例: [4096 37081 42597 44101 46077]
+[INFO] 2026/06/04 16:29:00 动态模式启动,每 1 秒扫描新实例
+[INFO] 2026/06/04 16:29:00 开始监控端口: 4096
+[INFO] 2026/06/04 16:29:00 开始监控端口: 37081
+[INFO] 2026/06/04 16:29:00 开始监控端口: 42597
+[INFO] 2026/06/04 16:29:00 开始监控端口: 44101
+[INFO] 2026/06/04 16:29:00 开始监控端口: 46077
+[INFO] 2026/06/04 16:29:00 端口 4096 已连接到事件流: http://127.0.0.1:4096/event
+[INFO] 2026/06/04 16:29:00 端口 42597 已连接到事件流: http://127.0.0.1:42597/event
+[INFO] 2026/06/04 16:29:00 端口 44101 已连接到事件流: http://127.0.0.1:44101/event
+[INFO] 2026/06/04 16:29:00 端口 37081 已连接到事件流: http://127.0.0.1:37081/event
+[INFO] 2026/06/04 16:29:00 端口 46077 已连接到事件流: http://127.0.0.1:46077/event
+[INFO] 2026/06/04 16:29:05 WebSocket 客户端已连接,当前连接数: 1
+[INFO] 2026/06/04 16:29:08 WebSocket 客户端已连接,当前连接数: 2
+[INFO] 2026/06/04 16:29:08 WebSocket 客户端已断开,当前连接数: 1
+[INFO] 2026/06/04 16:29:08 WebSocket 客户端已连接,当前连接数: 2
+[INFO] 2026/06/04 16:29:24 WebSocket 客户端已断开,当前连接数: 1
+[INFO] 2026/06/04 16:29:28 WebSocket 客户端已断开,当前连接数: 0
+[INFO] 2026/06/04 16:29:28 WebSocket 客户端已连接,当前连接数: 1
+[INFO] 2026/06/04 16:30:01 WebSocket 客户端已连接,当前连接数: 2
+[INFO] 2026/06/04 16:30:07 WebSocket 客户端已断开,当前连接数: 1
+[INFO] 2026/06/04 16:30:07 WebSocket 客户端已连接,当前连接数: 2
+[INFO] 2026/06/04 16:30:52 收到停止信号,正在退出
+[INFO] 2026/06/04 16:30:52 端口 42597 监控已停止,等待重新连接
+[INFO] 2026/06/04 16:30:52 端口 4096 监控已停止,等待重新连接
+[INFO] 2026/06/04 16:30:52 端口 37081 监控已停止,等待重新连接
+[INFO] 2026/06/04 16:30:52 数据库已连接: ./data/config.db
+[INFO] 2026/06/04 16:30:52 MQTT 已连接: tcp://47.92.50.210:9883 (主题: opencode/status)
+[INFO] 2026/06/04 16:30:52 MQTT 已连接: tcp://47.92.50.210:9883
+[INFO] 2026/06/04 16:30:52 API 服务已启动: :8045
+[INFO] 2026/06/04 16:30:52 API 服务器开始监听: :8045
+[INFO] 2026/06/04 16:30:53 开始监控端口: 37081
+[INFO] 2026/06/04 16:30:53 找到 5 个实例: [4096 37081 42597 44101 46077]
+[INFO] 2026/06/04 16:30:53 动态模式启动,每 1 秒扫描新实例
+[INFO] 2026/06/04 16:30:53 开始监控端口: 4096
+[INFO] 2026/06/04 16:30:53 开始监控端口: 46077
+[INFO] 2026/06/04 16:30:53 开始监控端口: 44101
+[INFO] 2026/06/04 16:30:53 开始监控端口: 42597
+[INFO] 2026/06/04 16:30:53 端口 44101 已连接到事件流: http://127.0.0.1:44101/event
+[INFO] 2026/06/04 16:30:53 端口 4096 已连接到事件流: http://127.0.0.1:4096/event
+[INFO] 2026/06/04 16:30:53 端口 37081 已连接到事件流: http://127.0.0.1:37081/event
+[INFO] 2026/06/04 16:30:53 端口 42597 已连接到事件流: http://127.0.0.1:42597/event
+[INFO] 2026/06/04 16:30:53 端口 46077 已连接到事件流: http://127.0.0.1:46077/event
+[INFO] 2026/06/04 16:30:53 WebSocket 客户端已连接,当前连接数: 1
+[INFO] 2026/06/04 16:30:53 WebSocket 客户端已连接,当前连接数: 2
+[INFO] 2026/06/04 16:31:12 端口 44101 监控已停止,等待重新连接
+[INFO] 2026/06/04 16:31:12 WebSocket 客户端已断开,当前连接数: 1
+[INFO] 2026/06/04 16:31:12 发现新实例端口: 44101,开始监控
+[INFO] 2026/06/04 16:35:07 收到停止信号,正在退出
+[INFO] 2026/06/04 16:35:07 数据库已连接: ./data/config.db
+[INFO] 2026/06/04 16:35:07 MQTT 已连接: tcp://47.92.50.210:9883 (主题: opencode/status)
+[INFO] 2026/06/04 16:35:07 MQTT 已连接: tcp://47.92.50.210:9883
+[INFO] 2026/06/04 16:35:07 API 服务已启动: :8045
+[INFO] 2026/06/04 16:35:07 API 服务器开始监听: :8045
+[INFO] 2026/06/04 16:35:08 开始监控端口: 37081
+[INFO] 2026/06/04 16:35:08 找到 5 个实例: [4096 37081 42597 44101 46077]
+[INFO] 2026/06/04 16:35:08 动态模式启动,每 1 秒扫描新实例
+[INFO] 2026/06/04 16:35:08 开始监控端口: 42597
+[INFO] 2026/06/04 16:35:08 开始监控端口: 4096
+[INFO] 2026/06/04 16:35:08 开始监控端口: 44101
+[INFO] 2026/06/04 16:35:08 开始监控端口: 46077
+[INFO] 2026/06/04 16:35:08 端口 46077 已连接到事件流: http://127.0.0.1:46077/event
+[INFO] 2026/06/04 16:35:08 端口 44101 已连接到事件流: http://127.0.0.1:44101/event
+[INFO] 2026/06/04 16:35:08 端口 37081 已连接到事件流: http://127.0.0.1:37081/event
+[INFO] 2026/06/04 16:35:08 端口 4096 已连接到事件流: http://127.0.0.1:4096/event
+[INFO] 2026/06/04 16:35:08 端口 42597 已连接到事件流: http://127.0.0.1:42597/event
+[INFO] 2026/06/04 16:35:08 WebSocket 客户端已连接,当前连接数: 1
+[INFO] 2026/06/04 16:35:12 WebSocket 客户端已断开,当前连接数: 0
+[INFO] 2026/06/04 16:35:12 WebSocket 客户端已连接,当前连接数: 1
+[INFO] 2026/06/04 16:36:06 端口 44101 监控已停止,等待重新连接
+[INFO] 2026/06/04 16:36:06 发现新实例端口: 44101,开始监控
+[INFO] 2026/06/04 16:36:28 WebSocket 客户端已连接,当前连接数: 2
+[INFO] 2026/06/04 16:36:28 WebSocket 客户端已断开,当前连接数: 1
+[INFO] 2026/06/04 16:36:28 WebSocket 客户端已连接,当前连接数: 2
+[INFO] 2026/06/04 16:39:11 WebSocket 客户端已断开,当前连接数: 1
+[INFO] 2026/06/04 16:39:50 收到停止信号,正在退出
+[INFO] 2026/06/04 16:39:50 数据库已连接: ./data/config.db
+[INFO] 2026/06/04 16:39:50 MQTT 已连接: tcp://47.92.50.210:9883
+[INFO] 2026/06/04 16:39:50 MQTT 已连接: tcp://47.92.50.210:9883 (主题: opencode/status)
+[INFO] 2026/06/04 16:39:50 API 服务已启动: :8045
+[INFO] 2026/06/04 16:39:50 API 服务器开始监听: :8045
+[INFO] 2026/06/04 16:39:51 找到 5 个实例: [4096 37081 42597 44101 46077]
+[INFO] 2026/06/04 16:39:51 动态模式启动,每 1 秒扫描新实例
+[INFO] 2026/06/04 16:39:51 开始监控端口: 4096
+[INFO] 2026/06/04 16:39:51 开始监控端口: 44101
+[INFO] 2026/06/04 16:39:51 开始监控端口: 37081
+[INFO] 2026/06/04 16:39:51 开始监控端口: 42597
+[INFO] 2026/06/04 16:39:51 开始监控端口: 46077
+[INFO] 2026/06/04 16:39:51 端口 46077 已连接到事件流: http://127.0.0.1:46077/event
+[INFO] 2026/06/04 16:39:51 端口 44101 已连接到事件流: http://127.0.0.1:44101/event
+[INFO] 2026/06/04 16:39:51 端口 42597 已连接到事件流: http://127.0.0.1:42597/event
+[INFO] 2026/06/04 16:39:51 端口 4096 已连接到事件流: http://127.0.0.1:4096/event
+[INFO] 2026/06/04 16:39:51 端口 37081 已连接到事件流: http://127.0.0.1:37081/event
+[INFO] 2026/06/04 16:39:51 WebSocket 客户端已连接,当前连接数: 1
+[INFO] 2026/06/04 16:40:22 端口 44101 监控已停止,等待重新连接
+[INFO] 2026/06/04 16:40:22 发现新实例端口: 44101,开始监控
+[INFO] 2026/06/04 16:41:20 收到停止信号,正在退出
+[INFO] 2026/06/04 16:41:21 数据库已连接: ./data/config.db
+[INFO] 2026/06/04 16:41:21 MQTT 已连接: tcp://47.92.50.210:9883
+[INFO] 2026/06/04 16:41:21 MQTT 已连接: tcp://47.92.50.210:9883 (主题: opencode/status)
+[INFO] 2026/06/04 16:41:21 API 服务已启动: :8045
+[INFO] 2026/06/04 16:41:21 API 服务器开始监听: :8045
+[INFO] 2026/06/04 16:41:21 找到 5 个实例: [4096 37081 42597 44101 46077]
+[INFO] 2026/06/04 16:41:21 动态模式启动,每 1 秒扫描新实例
+[INFO] 2026/06/04 16:41:21 开始监控端口: 37081
+[INFO] 2026/06/04 16:41:21 开始监控端口: 4096
+[INFO] 2026/06/04 16:41:21 开始监控端口: 42597
+[INFO] 2026/06/04 16:41:21 开始监控端口: 44101
+[INFO] 2026/06/04 16:41:21 开始监控端口: 46077
+[INFO] 2026/06/04 16:41:21 端口 46077 已连接到事件流: http://127.0.0.1:46077/event
+[INFO] 2026/06/04 16:41:21 端口 37081 已连接到事件流: http://127.0.0.1:37081/event
+[INFO] 2026/06/04 16:41:21 端口 4096 已连接到事件流: http://127.0.0.1:4096/event
+[INFO] 2026/06/04 16:41:21 端口 44101 已连接到事件流: http://127.0.0.1:44101/event
+[INFO] 2026/06/04 16:41:21 端口 42597 已连接到事件流: http://127.0.0.1:42597/event
+[INFO] 2026/06/04 16:41:21 WebSocket 客户端已连接,当前连接数: 1
+[INFO] 2026/06/04 16:41:39 端口 44101 监控已停止,等待重新连接
+[INFO] 2026/06/04 16:41:39 发现新实例端口: 44101,开始监控