moki преди 2 седмици
родител
ревизия
f78d3827da

+ 85 - 1
cmd/monitor/main.go

@@ -17,6 +17,7 @@ import (
 	"AI-Status-Light/internal/database"
 	"AI-Status-Light/internal/discovery"
 	"AI-Status-Light/internal/event"
+	"AI-Status-Light/internal/logger"
 	"AI-Status-Light/internal/monitor"
 	mqttcli "AI-Status-Light/internal/mqtt"
 )
@@ -62,16 +63,29 @@ func runServe(args []string) {
 	fs := flag.NewFlagSet("serve", flag.ExitOnError)
 	addr := fs.String("addr", ":8080", "监听地址")
 	dbPath := fs.String("db", defaultDBPath, "数据库路径")
+	logFile := fs.String("log-file", "./logs", "日志文件路径(默认 ./logs/monitor.log)")
+	logLevel := fs.String("log-level", "info", "日志级别 (debug/info/warn/error)")
 	fs.Parse(args)
 
+	logger.SetLevel(logger.ParseLevel(*logLevel))
+	if err := logger.InitFileLog(*logFile); err != nil {
+		fmt.Printf("初始化日志文件失败: %v\n", err)
+		return
+	}
+	defer logger.Close()
+
 	db, err := database.New(*dbPath)
 	if err != nil {
+		logger.Error("打开数据库失败: %v", err)
 		fmt.Printf("打开数据库失败: %v\n", err)
 		return
 	}
 	defer db.Close()
+	logger.Info("数据库已连接: %s", *dbPath)
 
 	server := api.New(db, *addr)
+	logger.Info("API 服务已启动: %s", *addr)
+
 	fmt.Printf("API 服务已启动: %s\n", *addr)
 	fmt.Println("接口文档:")
 	fmt.Println("  GET    /api/health       - 健康检查")
@@ -82,6 +96,7 @@ func runServe(args []string) {
 	fmt.Println("  DELETE /api/mqtt/:id     - 删除配置")
 
 	if err := server.Start(); err != nil {
+		logger.Error("服务启动失败: %v", err)
 		fmt.Printf("服务启动失败: %v\n", err)
 	}
 }
@@ -94,8 +109,17 @@ func runMonitor(args []string) {
 	intervalFlag := fs.Int("interval", 5, "动态扫描间隔(秒), 默认5")
 	dbPath := fs.String("db", defaultDBPath, "数据库路径")
 	apiAddr := fs.String("api-addr", "", "API 服务地址 (如: :8080)")
+	logFile := fs.String("log-file", "./logs", "日志文件路径(默认 ./logs/monitor.log)")
+	logLevel := fs.String("log-level", "info", "日志级别 (debug/info/warn/error)")
 	fs.Parse(args)
 
+	logger.SetLevel(logger.ParseLevel(*logLevel))
+	if err := logger.InitFileLog(*logFile); err != nil {
+		fmt.Printf("初始化日志文件失败: %v\n", err)
+		return
+	}
+	defer logger.Close()
+
 	var scanRange *[2]int
 	if *scanFlag != "" {
 		parts := strings.Split(*scanFlag, "-")
@@ -116,32 +140,41 @@ func runMonitor(args []string) {
 
 	db, err := database.New(*dbPath)
 	if err != nil {
+		logger.Error("打开数据库失败: %v", err)
 		fmt.Printf("打开数据库失败: %v\n", err)
 		return
 	}
 	defer db.Close()
+	logger.Info("数据库已连接: %s", *dbPath)
 
 	var mqttClient *mqttcli.Client
 	cfg, err := db.GetMQTTConfig()
 	if err != nil {
+		logger.Error("读取 MQTT 配置失败: %v", err)
 		fmt.Printf("读取 MQTT 配置失败: %v\n", err)
 	} else if cfg != nil {
 		mqttClient = mqttcli.NewFromConfig(cfg)
 		if err := mqttClient.Connect(); err != nil {
+			logger.Error("MQTT 连接失败: %v", err)
 			fmt.Printf("MQTT 连接失败: %v\n", err)
 			mqttClient = nil
 		} else {
 			defer mqttClient.Disconnect()
+			logger.Info("MQTT 已连接: %s (主题: %s)", cfg.Broker, cfg.Topic)
 			fmt.Printf("MQTT 已连接: %s (主题: %s)\n", cfg.Broker, cfg.Topic)
 		}
+	} else {
+		logger.Info("未配置 MQTT,跳过 MQTT 连接")
 	}
 
 	var apiServer *api.Server
 	if *apiAddr != "" {
 		apiServer = api.New(db, *apiAddr)
 		go func() {
+			logger.Info("API 服务已启动: %s", *apiAddr)
 			fmt.Printf("API 服务已启动: %s\n", *apiAddr)
 			if err := apiServer.Start(); err != nil {
+				logger.Error("API 服务失败: %v", err)
 				fmt.Printf("API 服务失败: %v\n", err)
 			}
 		}()
@@ -172,24 +205,53 @@ func runConfig(args []string) {
 	}
 
 	dbPath := defaultDBPath
+	logFile := "./logs"
+	logLevel := "info"
 	for i, arg := range args {
 		if arg == "--db" && i+1 < len(args) {
 			dbPath = args[i+1]
-			break
+		}
+		if arg == "--log-file" && i+1 < len(args) {
+			logFile = args[i+1]
+		}
+		if arg == "--log-level" && i+1 < len(args) {
+			logLevel = args[i+1]
+		}
+	}
+
+	logger.SetLevel(logger.ParseLevel(logLevel))
+	if err := logger.InitFileLog(logFile); err != nil {
+		fmt.Printf("初始化日志文件失败: %v\n", err)
+		return
+	}
+	defer logger.Close()
+
+	// 过滤全局选项,避免传递给子命令的 FlagSet
+	var filtered []string
+	for i := 0; i < len(args); i++ {
+		switch args[i] {
+		case "--db", "--log-file", "--log-level":
+			i++ // 跳过值
+		default:
+			filtered = append(filtered, args[i])
 		}
 	}
+	args = filtered
 
 	db, err := database.New(dbPath)
 	if err != nil {
+		logger.Error("打开数据库失败: %v", err)
 		fmt.Printf("打开数据库失败: %v\n", err)
 		return
 	}
 	defer db.Close()
+	logger.Info("数据库已连接: %s", dbPath)
 
 	switch args[0] {
 	case "list":
 		configs, err := db.ListMQTTConfigs()
 		if err != nil {
+			logger.Error("查询配置失败: %v", err)
 			fmt.Printf("查询失败: %v\n", err)
 			return
 		}
@@ -197,6 +259,7 @@ func runConfig(args []string) {
 			fmt.Println("未配置 MQTT")
 			return
 		}
+		logger.Info("查询到 %d 条 MQTT 配置", len(configs))
 		for _, cfg := range configs {
 			status := "禁用"
 			if cfg.Enabled {
@@ -234,9 +297,11 @@ func runConfig(args []string) {
 		}
 
 		if err := db.SaveMQTTConfig(cfg); err != nil {
+			logger.Error("保存 MQTT 配置失败: %v", err)
 			fmt.Printf("保存失败: %v\n", err)
 			return
 		}
+		logger.Info("MQTT 配置已保存: %s (主题: %s)", cfg.Broker, cfg.Topic)
 		fmt.Println("配置已保存")
 
 	case "delete":
@@ -246,13 +311,16 @@ func runConfig(args []string) {
 		}
 		id, err := strconv.Atoi(args[1])
 		if err != nil {
+			logger.Warn("无效的配置 ID: %s", args[1])
 			fmt.Println("无效的 ID")
 			return
 		}
 		if err := db.DeleteMQTTConfig(id); err != nil {
+			logger.Error("删除配置失败: id=%d, %v", id, err)
 			fmt.Printf("删除失败: %v\n", err)
 			return
 		}
+		logger.Info("MQTT 配置已删除: id=%d", id)
 		fmt.Println("配置已删除")
 
 	default:
@@ -291,6 +359,7 @@ func createCallback(mqttClient *mqttcli.Client, apiServer *api.Server) monitor.E
 				"timestamp": time.Now().Format(time.RFC3339),
 			}
 			if err := mqttClient.PublishRaw(mqttClient.GetTopic(), payload); err != nil {
+				logger.Error("MQTT 发送失败: %v", err)
 				fmt.Printf("MQTT 发送失败: %v\n", err)
 			}
 		}
@@ -376,6 +445,7 @@ func runFixedMode(ctx context.Context, host string, portsFlag string, callback m
 		return
 	}
 
+	logger.Info("固定模式启动,监控端口: %v", ports)
 	fmt.Printf("监控端口: %v\n", ports)
 	fmt.Println("Ctrl+C 停止")
 	fmt.Println(strings.Repeat("-", 40))
@@ -385,14 +455,18 @@ func runFixedMode(ctx context.Context, host string, portsFlag string, callback m
 		wg.Add(1)
 		go func(p int) {
 			defer wg.Done()
+			logger.Info("开始监控端口: %d", p)
 			m := monitor.New(host, p, callback)
 			m.Run(ctx)
+			logger.Info("端口 %d 监控已停止", p)
 		}(port)
 	}
 
 	<-sigChan
+	logger.Info("收到停止信号,正在退出")
 	fmt.Println("\n已停止")
 	wg.Wait()
+	logger.Info("所有监控协程已退出")
 }
 
 func runDynamicMode(ctx context.Context, host string, scanRange *[2]int, interval int, callback monitor.EventCallback, sigChan chan os.Signal) {
@@ -413,6 +487,7 @@ func runDynamicMode(ctx context.Context, host string, scanRange *[2]int, interva
 		mu.Unlock()
 
 		go func() {
+			logger.Info("开始监控端口: %d", port)
 			fmt.Printf("开始监控端口: %d\n", port)
 			m := monitor.New(host, port, callback)
 			m.Run(monitorCtx)
@@ -421,12 +496,14 @@ func runDynamicMode(ctx context.Context, host string, scanRange *[2]int, interva
 			delete(runningMonitors, port)
 			delete(monitoredPorts, port)
 			mu.Unlock()
+			logger.Info("端口 %d 监控已停止,等待重新连接", port)
 			fmt.Printf("端口 %d 监控已停止,等待重新连接\n", port)
 		}()
 	}
 
 	initial := scanner.Discover()
 	if len(initial) == 0 {
+		logger.Info("未找到运行中的 OpenCode 实例,等待自动检测")
 		fmt.Println("未找到运行中的 OpenCode 实例")
 		fmt.Println("请先执行: opencode serve --port 4096")
 		fmt.Println("启动后会自动检测,等待中...")
@@ -443,9 +520,11 @@ func runDynamicMode(ctx context.Context, host string, scanRange *[2]int, interva
 			ports = append(ports, p)
 		}
 		sort.Ints(ports)
+		logger.Info("找到 %d 个实例: %v", len(monitoredPorts), ports)
 		fmt.Printf("找到 %d 个实例: %v\n", len(monitoredPorts), ports)
 	}
 
+	logger.Info("动态模式启动,每 %d 秒扫描新实例", interval)
 	fmt.Printf("每 %d 秒扫描新实例,Ctrl+C 停止\n", interval)
 	fmt.Println(strings.Repeat("-", 40))
 
@@ -459,10 +538,14 @@ func runDynamicMode(ctx context.Context, host string, scanRange *[2]int, interva
 				return
 			case <-scanTicker.C:
 				newPorts := scanner.Discover()
+				if len(newPorts) > 0 {
+					logger.Debug("扫描到 %d 个端口: %v", len(newPorts), newPorts)
+				}
 				for _, port := range newPorts {
 					mu.Lock()
 					if !monitoredPorts[port] {
 						monitoredPorts[port] = true
+						logger.Info("发现新实例端口: %d,开始监控", port)
 						startMonitor(port)
 					}
 					mu.Unlock()
@@ -472,5 +555,6 @@ func runDynamicMode(ctx context.Context, host string, scanRange *[2]int, interva
 	}()
 
 	<-sigChan
+	logger.Info("收到停止信号,正在退出")
 	fmt.Println("\n已停止")
 }

+ 32 - 5
internal/api/api.go

@@ -2,7 +2,6 @@ package api
 
 import (
 	"encoding/json"
-	"log"
 	"net/http"
 	"strconv"
 	"strings"
@@ -12,6 +11,7 @@ import (
 	"github.com/gorilla/websocket"
 
 	"AI-Status-Light/internal/database"
+	"AI-Status-Light/internal/logger"
 )
 
 type Server struct {
@@ -54,7 +54,12 @@ func New(db *database.DB, addr string) *Server {
 }
 
 func (s *Server) Start() error {
-	return s.server.ListenAndServe()
+	logger.Info("API 服务器开始监听: %s", s.server.Addr)
+	err := s.server.ListenAndServe()
+	if err != nil && err != http.ErrServerClosed {
+		logger.Error("API 服务器监听失败: %v", err)
+	}
+	return err
 }
 
 func corsMiddleware(next http.Handler) http.Handler {
@@ -77,20 +82,24 @@ func (s *Server) handleHealth(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)
 	switch r.Method {
 	case http.MethodGet:
 		s.listMQTTConfigs(w, r)
 	case http.MethodPost:
 		s.createMQTTConfig(w, r)
 	default:
+		logger.Warn("不支持的 HTTP 方法: %s %s", r.Method, r.URL.Path)
 		writeJSON(w, http.StatusMethodNotAllowed, Response{Code: -1, Message: "方法不允许"})
 	}
 }
 
 func (s *Server) handleMQTTByID(w http.ResponseWriter, r *http.Request) {
+	logger.Debug("HTTP %s %s", r.Method, r.URL.Path)
 	idStr := strings.TrimPrefix(r.URL.Path, "/api/mqtt/")
 	id, err := strconv.Atoi(idStr)
 	if err != nil {
+		logger.Warn("无效的配置 ID: %s", idStr)
 		writeJSON(w, http.StatusBadRequest, Response{Code: -1, Message: "无效的 ID"})
 		return
 	}
@@ -103,6 +112,7 @@ func (s *Server) handleMQTTByID(w http.ResponseWriter, r *http.Request) {
 	case http.MethodDelete:
 		s.deleteMQTTConfig(w, id)
 	default:
+		logger.Warn("不支持的 HTTP 方法: %s %s", r.Method, r.URL.Path)
 		writeJSON(w, http.StatusMethodNotAllowed, Response{Code: -1, Message: "方法不允许"})
 	}
 }
@@ -110,20 +120,24 @@ func (s *Server) handleMQTTByID(w http.ResponseWriter, r *http.Request) {
 func (s *Server) listMQTTConfigs(w http.ResponseWriter, r *http.Request) {
 	configs, err := s.db.ListMQTTConfigs()
 	if err != nil {
+		logger.Error("查询 MQTT 配置列表失败: %v", err)
 		writeJSON(w, http.StatusInternalServerError, Response{Code: -1, Message: err.Error()})
 		return
 	}
+	logger.Debug("查询 MQTT 配置列表: %d 条", len(configs))
 	writeJSON(w, http.StatusOK, Response{Code: 0, Message: "ok", Data: configs})
 }
 
 func (s *Server) createMQTTConfig(w http.ResponseWriter, r *http.Request) {
 	var cfg database.MQTTConfig
 	if err := json.NewDecoder(r.Body).Decode(&cfg); err != nil {
+		logger.Warn("创建配置请求体解析失败: %v", err)
 		writeJSON(w, http.StatusBadRequest, Response{Code: -1, Message: "无效的请求体"})
 		return
 	}
 
 	if cfg.Broker == "" {
+		logger.Warn("创建配置: broker 为空")
 		writeJSON(w, http.StatusBadRequest, Response{Code: -1, Message: "broker 不能为空"})
 		return
 	}
@@ -136,39 +150,46 @@ func (s *Server) createMQTTConfig(w http.ResponseWriter, r *http.Request) {
 	}
 
 	if err := s.db.SaveMQTTConfig(&cfg); err != nil {
+		logger.Error("创建 MQTT 配置失败: %v", err)
 		writeJSON(w, http.StatusInternalServerError, Response{Code: -1, Message: err.Error()})
 		return
 	}
 
+	logger.Info("MQTT 配置已创建: id=%d, broker=%s, topic=%s", cfg.ID, cfg.Broker, cfg.Topic)
 	writeJSON(w, http.StatusCreated, Response{Code: 0, Message: "创建成功", Data: cfg})
 }
 
 func (s *Server) getMQTTConfig(w http.ResponseWriter, id int) {
 	configs, err := s.db.ListMQTTConfigs()
 	if err != nil {
+		logger.Error("查询 MQTT 配置失败: id=%d, %v", id, err)
 		writeJSON(w, http.StatusInternalServerError, Response{Code: -1, Message: err.Error()})
 		return
 	}
 
 	for _, cfg := range configs {
 		if cfg.ID == id {
+			logger.Debug("查询 MQTT 配置: id=%d", id)
 			writeJSON(w, http.StatusOK, Response{Code: 0, Message: "ok", Data: cfg})
 			return
 		}
 	}
 
+	logger.Warn("MQTT 配置不存在: id=%d", id)
 	writeJSON(w, http.StatusNotFound, Response{Code: -1, Message: "配置不存在"})
 }
 
 func (s *Server) updateMQTTConfig(w http.ResponseWriter, r *http.Request, id int) {
 	var cfg database.MQTTConfig
 	if err := json.NewDecoder(r.Body).Decode(&cfg); err != nil {
+		logger.Warn("更新配置请求体解析失败: id=%d, %v", id, err)
 		writeJSON(w, http.StatusBadRequest, Response{Code: -1, Message: "无效的请求体"})
 		return
 	}
 
 	cfg.ID = id
 	if cfg.Broker == "" {
+		logger.Warn("更新配置: broker 为空, id=%d", id)
 		writeJSON(w, http.StatusBadRequest, Response{Code: -1, Message: "broker 不能为空"})
 		return
 	}
@@ -181,18 +202,22 @@ func (s *Server) updateMQTTConfig(w http.ResponseWriter, r *http.Request, id int
 	}
 
 	if err := s.db.SaveMQTTConfig(&cfg); err != nil {
+		logger.Error("更新 MQTT 配置失败: id=%d, %v", id, err)
 		writeJSON(w, http.StatusInternalServerError, Response{Code: -1, Message: err.Error()})
 		return
 	}
 
+	logger.Info("MQTT 配置已更新: id=%d, broker=%s, topic=%s", id, cfg.Broker, cfg.Topic)
 	writeJSON(w, http.StatusOK, Response{Code: 0, Message: "更新成功", Data: cfg})
 }
 
 func (s *Server) deleteMQTTConfig(w http.ResponseWriter, id int) {
 	if err := s.db.DeleteMQTTConfig(id); err != nil {
+		logger.Error("删除 MQTT 配置失败: id=%d, %v", id, err)
 		writeJSON(w, http.StatusInternalServerError, Response{Code: -1, Message: err.Error()})
 		return
 	}
+	logger.Info("MQTT 配置已删除: id=%d", id)
 	writeJSON(w, http.StatusOK, Response{Code: 0, Message: "删除成功"})
 }
 
@@ -209,7 +234,7 @@ func (s *Server) GetAddr() string {
 func (s *Server) handleWebSocket(w http.ResponseWriter, r *http.Request) {
 	conn, err := s.upgrader.Upgrade(w, r, nil)
 	if err != nil {
-		log.Printf("WebSocket 升级失败: %v", err)
+		logger.Error("WebSocket 升级失败: %v", err)
 		return
 	}
 
@@ -217,7 +242,7 @@ func (s *Server) handleWebSocket(w http.ResponseWriter, r *http.Request) {
 	s.clients[conn] = true
 	s.clientsMu.Unlock()
 
-	log.Printf("WebSocket 客户端已连接,当前连接数: %d", len(s.clients))
+	logger.Info("WebSocket 客户端已连接,当前连接数: %d", len(s.clients))
 
 	go func() {
 		defer func() {
@@ -225,7 +250,7 @@ func (s *Server) handleWebSocket(w http.ResponseWriter, r *http.Request) {
 			delete(s.clients, conn)
 			s.clientsMu.Unlock()
 			conn.Close()
-			log.Printf("WebSocket 客户端已断开,当前连接数: %d", len(s.clients))
+			logger.Info("WebSocket 客户端已断开,当前连接数: %d", len(s.clients))
 		}()
 
 		for {
@@ -254,12 +279,14 @@ func (s *Server) BroadcastStatus(port int, status string, code string) {
 
 	data, err := json.Marshal(payload)
 	if err != nil {
+		logger.Error("序列化广播消息失败: %v", err)
 		return
 	}
 
 	for client := range s.clients {
 		err := client.WriteMessage(websocket.TextMessage, data)
 		if err != nil {
+			logger.Debug("WebSocket 写入失败,移除客户端: %v", err)
 			client.Close()
 			delete(s.clients, client)
 		}

+ 25 - 0
internal/database/database.go

@@ -8,6 +8,8 @@ import (
 	"path/filepath"
 
 	_ "modernc.org/sqlite"
+
+	"AI-Status-Light/internal/logger"
 )
 
 type DB struct {
@@ -27,23 +29,28 @@ type MQTTConfig struct {
 func New(dbPath string) (*DB, error) {
 	dir := filepath.Dir(dbPath)
 	if err := os.MkdirAll(dir, 0755); err != nil {
+		logger.Error("创建数据库目录失败: %v", err)
 		return nil, fmt.Errorf("创建目录失败: %w", err)
 	}
 
 	db, err := sql.Open("sqlite", dbPath)
 	if err != nil {
+		logger.Error("打开数据库失败: %v", err)
 		return nil, fmt.Errorf("打开数据库失败: %w", err)
 	}
 
 	conn, err := db.Conn(context.Background())
 	if err != nil {
+		logger.Error("获取数据库连接失败: %v", err)
 		return nil, fmt.Errorf("获取连接失败: %w", err)
 	}
 
 	d := &DB{conn: conn}
 	if err := d.init(); err != nil {
+		logger.Error("初始化数据库失败: %v", err)
 		return nil, err
 	}
+	logger.Debug("数据库已打开: %s", dbPath)
 	return d, nil
 }
 
@@ -78,11 +85,14 @@ func (d *DB) GetMQTTConfig() (*MQTTConfig, error) {
 	var cfg MQTTConfig
 	err := row.Scan(&cfg.ID, &cfg.Broker, &cfg.ClientID, &cfg.Username, &cfg.Password, &cfg.Topic, &cfg.Enabled)
 	if err == sql.ErrNoRows {
+		logger.Debug("未找到启用的 MQTT 配置")
 		return nil, nil
 	}
 	if err != nil {
+		logger.Error("查询 MQTT 配置失败: %v", err)
 		return nil, fmt.Errorf("查询配置失败: %w", err)
 	}
+	logger.Debug("获取到 MQTT 配置: id=%d, broker=%s", cfg.ID, cfg.Broker)
 	return &cfg, nil
 }
 
@@ -90,16 +100,27 @@ func (d *DB) SaveMQTTConfig(cfg *MQTTConfig) error {
 	if cfg.ID == 0 {
 		query := "INSERT INTO mqtt_config (broker, client_id, username, password, topic, enabled) VALUES (?, ?, ?, ?, ?, ?)"
 		_, err := d.conn.ExecContext(context.Background(), query, cfg.Broker, cfg.ClientID, cfg.Username, cfg.Password, cfg.Topic, cfg.Enabled)
+		if err != nil {
+			logger.Error("插入 MQTT 配置失败: %v", err)
+		}
 		return err
 	}
 	query := "UPDATE mqtt_config SET broker = ?, client_id = ?, username = ?, password = ?, topic = ?, enabled = ? WHERE id = ?"
 	_, err := d.conn.ExecContext(context.Background(), query, cfg.Broker, cfg.ClientID, cfg.Username, cfg.Password, cfg.Topic, cfg.Enabled, cfg.ID)
+	if err != nil {
+		logger.Error("更新 MQTT 配置失败: id=%d, %v", cfg.ID, err)
+	}
 	return err
 }
 
 func (d *DB) DeleteMQTTConfig(id int) error {
 	query := "DELETE FROM mqtt_config WHERE id = ?"
 	_, err := d.conn.ExecContext(context.Background(), query, id)
+	if err != nil {
+		logger.Error("删除 MQTT 配置失败: id=%d, %v", id, err)
+	} else {
+		logger.Debug("MQTT 配置已删除: id=%d", id)
+	}
 	return err
 }
 
@@ -107,6 +128,7 @@ func (d *DB) ListMQTTConfigs() ([]MQTTConfig, error) {
 	query := "SELECT id, broker, client_id, username, password, topic, enabled FROM mqtt_config ORDER BY id"
 	rows, err := d.conn.QueryContext(context.Background(), query)
 	if err != nil {
+		logger.Error("查询 MQTT 配置列表失败: %v", err)
 		return nil, err
 	}
 	defer rows.Close()
@@ -115,13 +137,16 @@ func (d *DB) ListMQTTConfigs() ([]MQTTConfig, error) {
 	for rows.Next() {
 		var cfg MQTTConfig
 		if err := rows.Scan(&cfg.ID, &cfg.Broker, &cfg.ClientID, &cfg.Username, &cfg.Password, &cfg.Topic, &cfg.Enabled); err != nil {
+			logger.Warn("扫描 MQTT 配置行失败: %v", err)
 			continue
 		}
 		configs = append(configs, cfg)
 	}
+	logger.Debug("查询到 %d 条 MQTT 配置", len(configs))
 	return configs, nil
 }
 
 func (d *DB) Close() error {
+	logger.Debug("数据库连接已关闭")
 	return d.conn.Close()
 }

+ 18 - 2
internal/discovery/discovery.go

@@ -5,6 +5,8 @@ import (
 	"net/http"
 	"sort"
 	"time"
+
+	"AI-Status-Light/internal/logger"
 )
 
 type Scanner struct {
@@ -20,14 +22,27 @@ func NewScanner(host string, scanRange *[2]int) *Scanner {
 }
 
 func (s *Scanner) Discover() []int {
+	logger.Debug("开始发现 OpenCode 实例,主机: %s", s.Host)
 	ports := findByCmdline()
-	if len(ports) == 0 {
+	if len(ports) > 0 {
+		logger.Debug("通过命令行参数发现端口: %v", ports)
+	} else {
+		logger.Debug("命令行未发现端口,尝试通过 PID 查找")
 		ports = findByPID()
+		if len(ports) > 0 {
+			logger.Debug("通过 PID 发现端口: %v", ports)
+		}
 	}
 	if len(ports) == 0 && s.ScanRange != nil {
+		logger.Info("未通过系统命令发现实例,开始端口扫描 %d-%d", s.ScanRange[0], s.ScanRange[1])
 		ports = scanPorts(s.Host, s.ScanRange[0], s.ScanRange[1])
+		if len(ports) > 0 {
+			logger.Info("端口扫描发现 %d 个实例: %v", len(ports), ports)
+		}
 	}
-	return unique(ports)
+	result := unique(ports)
+	logger.Debug("发现完成,共 %d 个实例: %v", len(result), result)
+	return result
 }
 
 func scanPorts(host string, startPort, endPort int) []int {
@@ -37,6 +52,7 @@ func scanPorts(host string, startPort, endPort int) []int {
 	for port := startPort; port <= endPort; port++ {
 		resp, err := client.Get(fmt.Sprintf("http://%s:%d/global/health", host, port))
 		if err == nil && resp.StatusCode == 200 {
+			logger.Debug("端口扫描发现实例: %d", port)
 			found = append(found, port)
 			resp.Body.Close()
 		}

+ 11 - 0
internal/discovery/discovery_linux.go

@@ -9,6 +9,8 @@ import (
 	"sort"
 	"strconv"
 	"strings"
+
+	"AI-Status-Light/internal/logger"
 )
 
 func findByPID() []int {
@@ -17,6 +19,7 @@ func findByPID() []int {
 
 	out, err := exec.Command("ps", "aux").Output()
 	if err != nil {
+		logger.Debug("执行 ps aux 失败: %v", err)
 		return ports
 	}
 
@@ -32,9 +35,16 @@ func findByPID() []int {
 		}
 	}
 
+	if len(pids) == 0 {
+		logger.Debug("未找到 opencode 进程")
+		return ports
+	}
+	logger.Debug("发现 opencode 进程 PID: %v", pids)
+
 	for _, pid := range pids {
 		ssOut, err := exec.Command("ss", "-tlnp").Output()
 		if err != nil {
+			logger.Debug("执行 ss -tlnp 失败: %v", err)
 			continue
 		}
 
@@ -66,6 +76,7 @@ func findByCmdline() []int {
 
 	out, err := exec.Command("ps", "aux").Output()
 	if err != nil {
+		logger.Debug("执行 ps aux 失败: %v", err)
 		return ports
 	}
 

+ 192 - 0
internal/logger/logger.go

@@ -0,0 +1,192 @@
+package logger
+
+import (
+	"fmt"
+	"io"
+	"log"
+	"os"
+	"path/filepath"
+	"strings"
+	"sync"
+	"time"
+)
+
+type Level int
+
+const (
+	LevelDebug Level = iota
+	LevelInfo
+	LevelWarn
+	LevelError
+)
+
+const (
+	defaultDir = "./logs"
+)
+
+var (
+	mu           sync.RWMutex
+	currentLevel = LevelInfo
+	debugLog     = log.New(os.Stdout, "[DEBUG] ", log.Ldate|log.Ltime)
+	infoLog      = log.New(os.Stdout, "[INFO] ", log.Ldate|log.Ltime)
+	warnLog      = log.New(os.Stderr, "[WARN] ", log.Ldate|log.Ltime)
+	errorLog     = log.New(os.Stderr, "[ERROR] ", log.Ldate|log.Ltime)
+
+	logFile *rotatingFileWriter
+)
+
+type rotatingFileWriter struct {
+	mu      sync.Mutex
+	file    *os.File
+	path    string
+	logDate string // 当前日志日期 YYYY-MM-DD
+}
+
+func (w *rotatingFileWriter) Write(p []byte) (n int, err error) {
+	w.mu.Lock()
+	defer w.mu.Unlock()
+
+	today := time.Now().Format("2006-01-02")
+	if today != w.logDate {
+		if err := w.rotate(today); err != nil {
+			return 0, err
+		}
+	}
+
+	return w.file.Write(p)
+}
+
+func (w *rotatingFileWriter) rotate(today string) error {
+	if w.logDate != "" {
+		w.file.Close()
+		backup := w.path + "." + w.logDate
+		os.Rename(w.path, backup)
+	}
+
+	f, err := os.OpenFile(w.path, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
+	if err != nil {
+		return err
+	}
+	w.file = f
+	w.logDate = today
+	return nil
+}
+
+func (w *rotatingFileWriter) Close() error {
+	w.mu.Lock()
+	defer w.mu.Unlock()
+	if w.file != nil {
+		return w.file.Close()
+	}
+	return nil
+}
+
+func SetLevel(l Level) {
+	mu.Lock()
+	defer mu.Unlock()
+	currentLevel = l
+}
+
+func GetLevel() Level {
+	mu.RLock()
+	defer mu.RUnlock()
+	return currentLevel
+}
+
+func ParseLevel(s string) Level {
+	switch strings.ToLower(s) {
+	case "debug":
+		return LevelDebug
+	case "info":
+		return LevelInfo
+	case "warn", "warning":
+		return LevelWarn
+	case "error":
+		return LevelError
+	default:
+		return LevelInfo
+	}
+}
+
+// InitFileLog 初始化文件日志。logPath 为空时默认 ./logs/monitor.log。
+// 按天轮转,日志同时输出到控制台和文件。
+func InitFileLog(logPath string) error {
+	mu.Lock()
+	defer mu.Unlock()
+
+	if logPath == "" {
+		logPath = defaultDir
+	}
+
+	if fi, err := os.Stat(logPath); err == nil && fi.IsDir() {
+		logPath = filepath.Join(logPath, "monitor.log")
+	}
+
+	dir := filepath.Dir(logPath)
+	if err := os.MkdirAll(dir, 0755); err != nil {
+		return fmt.Errorf("创建日志目录失败: %w", err)
+	}
+
+	f, err := os.OpenFile(logPath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
+	if err != nil {
+		return fmt.Errorf("打开日志文件失败: %w", err)
+	}
+
+	logFile = &rotatingFileWriter{
+		file:    f,
+		path:    logPath,
+		logDate: time.Now().Format("2006-01-02"),
+	}
+
+	debugLog.SetOutput(io.MultiWriter(os.Stdout, logFile))
+	infoLog.SetOutput(io.MultiWriter(os.Stdout, logFile))
+	warnLog.SetOutput(io.MultiWriter(os.Stderr, logFile))
+	errorLog.SetOutput(io.MultiWriter(os.Stderr, logFile))
+
+	return nil
+}
+
+func Close() {
+	mu.Lock()
+	defer mu.Unlock()
+	if logFile != nil {
+		logFile.Close()
+		logFile = nil
+	}
+}
+
+func Debug(format string, v ...interface{}) {
+	mu.RLock()
+	l := currentLevel
+	mu.RUnlock()
+	if LevelDebug >= l {
+		debugLog.Output(2, fmt.Sprintf(format, v...))
+	}
+}
+
+func Info(format string, v ...interface{}) {
+	mu.RLock()
+	l := currentLevel
+	mu.RUnlock()
+	if LevelInfo >= l {
+		infoLog.Output(2, fmt.Sprintf(format, v...))
+	}
+}
+
+func Warn(format string, v ...interface{}) {
+	mu.RLock()
+	l := currentLevel
+	mu.RUnlock()
+	if LevelWarn >= l {
+		warnLog.Output(2, fmt.Sprintf(format, v...))
+	}
+}
+
+func Error(format string, v ...interface{}) {
+	mu.RLock()
+	l := currentLevel
+	mu.RUnlock()
+	if LevelError >= l {
+		errorLog.Output(2, fmt.Sprintf(format, v...))
+	}
+}

+ 16 - 1
internal/monitor/monitor.go

@@ -10,6 +10,7 @@ import (
 	"time"
 
 	"AI-Status-Light/internal/event"
+	"AI-Status-Light/internal/logger"
 )
 
 type EventCallback func(port int, evt *event.SSEEvent)
@@ -37,25 +38,35 @@ func New(host string, port int, callback EventCallback) *Monitor {
 func (m *Monitor) CheckHealth() bool {
 	resp, err := m.client.Get(m.baseURL + "/global/health")
 	if err != nil {
+		logger.Debug("端口 %d 健康检查失败: %v", m.port, err)
 		return false
 	}
 	defer resp.Body.Close()
-	return resp.StatusCode == 200
+	healthy := resp.StatusCode == 200
+	if healthy {
+		logger.Debug("端口 %d 健康检查通过", m.port)
+	} else {
+		logger.Debug("端口 %d 健康检查返回状态码: %d", m.port, resp.StatusCode)
+	}
+	return healthy
 }
 
 func (m *Monitor) Run(ctx context.Context) {
 	req, err := http.NewRequestWithContext(ctx, "GET", m.baseURL+"/event", nil)
 	if err != nil {
+		logger.Error("端口 %d 创建请求失败: %v", m.port, err)
 		return
 	}
 	req.Header.Set("Accept", "text/event-stream")
 
 	resp, err := http.DefaultClient.Do(req)
 	if err != nil {
+		logger.Debug("端口 %d 连接事件流失败: %v", m.port, err)
 		return
 	}
 	defer resp.Body.Close()
 
+	logger.Info("端口 %d 已连接到事件流: %s/event", m.port, m.baseURL)
 	m.lastActivity = time.Now()
 
 	// 启动超时检测 goroutine
@@ -71,6 +82,7 @@ func (m *Monitor) Run(ctx context.Context) {
 				return
 			case <-ticker.C:
 				if time.Since(m.lastActivity) > m.timeout {
+					logger.Warn("端口 %d 超时 %v,回收监控", m.port, m.timeout)
 					fmt.Printf("端口 %d 超时 %v,回收监控\n", m.port, m.timeout)
 					return
 				}
@@ -82,6 +94,7 @@ func (m *Monitor) Run(ctx context.Context) {
 	for scanner.Scan() {
 		select {
 		case <-ctx.Done():
+			logger.Debug("端口 %d 上下文已取消,退出事件循环", m.port)
 			close(timeoutChan)
 			return
 		default:
@@ -98,6 +111,7 @@ func (m *Monitor) Run(ctx context.Context) {
 
 		var evt event.SSEEvent
 		if err := json.Unmarshal([]byte(data), &evt); err != nil {
+			logger.Debug("端口 %d 解析事件失败: %v", m.port, err)
 			continue
 		}
 
@@ -109,4 +123,5 @@ func (m *Monitor) Run(ctx context.Context) {
 	}
 
 	close(timeoutChan)
+	logger.Debug("端口 %d 事件流读取结束", m.port)
 }

+ 3 - 3
internal/mqtt/mqtt.go

@@ -3,12 +3,12 @@ package mqtt
 import (
 	"encoding/json"
 	"fmt"
-	"log"
 	"time"
 
 	mqttlib "github.com/eclipse/paho.mqtt.golang"
 
 	"AI-Status-Light/internal/database"
+	"AI-Status-Light/internal/logger"
 )
 
 type Client struct {
@@ -63,10 +63,10 @@ func (c *Client) Connect() error {
 	opts.SetAutoReconnect(true)
 	opts.SetConnectTimeout(10 * time.Second)
 	opts.SetOnConnectHandler(func(client mqttlib.Client) {
-		log.Printf("MQTT 已连接: %s", c.broker)
+		logger.Info("MQTT 已连接: %s", c.broker)
 	})
 	opts.SetConnectionLostHandler(func(client mqttlib.Client, err error) {
-		log.Printf("MQTT 连接断开: %v", err)
+		logger.Warn("MQTT 连接断开: %v", err)
 	})
 
 	c.client = mqttlib.NewClient(opts)

+ 0 - 0
scripts/build.sh