moki 3 日 前
コミット
22a49f49ed
4 ファイル変更106 行追加13 行削除
  1. 3 3
      cmd/monitor/main.go
  2. 34 10
      docs/api.md
  3. 15 0
      internal/api/api.go
  4. 54 0
      internal/database/database.go

+ 3 - 3
cmd/monitor/main.go

@@ -117,7 +117,7 @@ func runServe(args []string) {
 	server.SetMQTTClient(mqttClient)
 	server.SetBLEStdin(bleStdin)
 
-	// 启动定时清理任务(每10分钟清理超过2小时的历史记录)
+	// 启动定时清理任务(每10分钟清理超过24小时的历史记录)
 	go func() {
 		ticker := time.NewTicker(10 * time.Minute)
 		defer ticker.Stop()
@@ -126,13 +126,13 @@ func runServe(args []string) {
 			case <-ctx.Done():
 				return
 			case <-ticker.C:
-				if err := db.CleanOldStatusRecords(2); err != nil {
+				if err := db.CleanOldStatusRecords(24); err != nil {
 					logger.Error("清理历史记录失败: %v", err)
 				}
 			}
 		}
 	}()
-	logger.Info("定时清理任务已启动(每10分钟清理超过2小时的历史记录)")
+	logger.Info("定时清理任务已启动(每10分钟清理超过24小时的历史记录)")
 
 	if *tls {
 		if err := api.EnsureSelfSignedCert(*tlsCert, *tlsKey); err != nil {

+ 34 - 10
docs/api.md

@@ -134,7 +134,31 @@ GET /api/history
 - 默认保留最近2小时的历史记录
 - 超过2小时的记录会自动清理
 
-### 5. 获取所有 MQTT 配置
+### 5. 获取今日工作时长
+
+```
+GET /api/work-duration
+```
+
+**响应示例:**
+
+```json
+{
+  "code": 0,
+  "message": "ok",
+  "data": {
+    "duration_minutes": 125
+  }
+}
+```
+
+**说明:**
+
+- 返回今日累计工作时长(分钟)
+- 活跃状态包括:busy、reasoning、using_tool、running、pending、retry
+- 从第一个活跃状态开始计算,到下一个状态变化或当前时间
+
+### 6. 获取所有 MQTT 配置
 
 ```
 GET /api/mqtt
@@ -159,7 +183,7 @@ GET /api/mqtt
 }
 ```
 
-### 6. 创建 MQTT 配置
+### 7. 创建 MQTT 配置
 
 ```
 POST /api/mqtt
@@ -203,7 +227,7 @@ POST /api/mqtt
 }
 ```
 
-### 7. 获取单个 MQTT 配置
+### 8. 获取单个 MQTT 配置
 
 ```
 GET /api/mqtt/:id
@@ -231,7 +255,7 @@ GET /api/mqtt/:id
 }
 ```
 
-### 8. 更新 MQTT 配置
+### 9. 更新 MQTT 配置
 
 ```
 PUT /api/mqtt/:id
@@ -271,7 +295,7 @@ PUT /api/mqtt/:id
 }
 ```
 
-### 9. 删除 MQTT 配置
+### 10. 删除 MQTT 配置
 
 ```
 DELETE /api/mqtt/:id
@@ -290,7 +314,7 @@ DELETE /api/mqtt/:id
 }
 ```
 
-### 10. 获取所有 BLE 配置
+### 11. 获取所有 BLE 配置
 
 ```
 GET /api/ble
@@ -313,7 +337,7 @@ GET /api/ble
 }
 ```
 
-### 11. 创建 BLE 配置
+### 12. 创建 BLE 配置
 
 ```
 POST /api/ble
@@ -351,7 +375,7 @@ POST /api/ble
 }
 ```
 
-### 12. 获取单个 BLE 配置
+### 13. 获取单个 BLE 配置
 
 ```
 GET /api/ble/:id
@@ -362,7 +386,7 @@ GET /api/ble/:id
 |------|------|------|
 | id | integer | 配置 ID |
 
-### 13. 更新 BLE 配置
+### 14. 更新 BLE 配置
 
 ```
 PUT /api/ble/:id
@@ -383,7 +407,7 @@ PUT /api/ble/:id
 }
 ```
 
-### 14. 删除 BLE 配置
+### 15. 删除 BLE 配置
 
 ```
 DELETE /api/ble/:id

+ 15 - 0
internal/api/api.go

@@ -60,6 +60,7 @@ func New(db *database.DB, addr string) *Server {
 	mux.HandleFunc("/api/health", s.handleHealth)
 	mux.HandleFunc("/api/status", s.handleStatus)
 	mux.HandleFunc("/api/history", s.handleHistory)
+	mux.HandleFunc("/api/work-duration", s.handleWorkDuration)
 	mux.HandleFunc("/api/device/config", s.handleDeviceConfig)
 	mux.HandleFunc("/api/device/config/", s.handleDeviceConfigByID)
 	mux.HandleFunc("/api/device/config/push", s.handleDeviceConfigPush)
@@ -182,6 +183,20 @@ func (s *Server) handleHistory(w http.ResponseWriter, r *http.Request) {
 	writeJSON(w, http.StatusOK, Response{Code: 0, Message: "ok", Data: records})
 }
 
+func (s *Server) handleWorkDuration(w http.ResponseWriter, r *http.Request) {
+	if r.Method != http.MethodGet {
+		writeJSON(w, http.StatusMethodNotAllowed, Response{Code: -1, Message: "方法不允许"})
+		return
+	}
+	duration, err := s.db.GetTodayWorkDuration()
+	if err != nil {
+		logger.Error("查询今日工作时长失败: %v", err)
+		writeJSON(w, http.StatusInternalServerError, Response{Code: -1, Message: err.Error()})
+		return
+	}
+	writeJSON(w, http.StatusOK, Response{Code: 0, Message: "ok", Data: duration})
+}
+
 func (s *Server) handleEvent(w http.ResponseWriter, r *http.Request) {
 	if r.Method != http.MethodPost {
 		writeJSON(w, http.StatusMethodNotAllowed, Response{Code: -1, Message: "方法不允许"})

+ 54 - 0
internal/database/database.go

@@ -6,6 +6,7 @@ import (
 	"fmt"
 	"os"
 	"path/filepath"
+	"time"
 
 	_ "modernc.org/sqlite"
 
@@ -61,6 +62,10 @@ type StatusRecord struct {
 	Timestamp string `json:"timestamp"`
 }
 
+type WorkDuration struct {
+	DurationMinutes int `json:"duration_minutes"`
+}
+
 func New(dbPath string) (*DB, error) {
 	dir := filepath.Dir(dbPath)
 	if err := os.MkdirAll(dir, 0755); err != nil {
@@ -439,3 +444,52 @@ func (d *DB) CleanOldStatusRecords(hours int) error {
 	}
 	return nil
 }
+
+func (d *DB) GetTodayWorkDuration() (*WorkDuration, error) {
+	query := "SELECT code, timestamp FROM status_history WHERE timestamp >= date('now', 'localtime') ORDER BY timestamp ASC"
+	rows, err := d.conn.QueryContext(context.Background(), query)
+	if err != nil {
+		logger.Error("查询今日工作时长失败: %v", err)
+		return nil, err
+	}
+	defer rows.Close()
+
+	type record struct {
+		Code string
+		Time time.Time
+	}
+	var records []record
+	for rows.Next() {
+		var code, ts string
+		if err := rows.Scan(&code, &ts); err != nil {
+			continue
+		}
+		t, err := time.ParseInLocation("2006-01-02 15:04:05", ts, time.Local)
+		if err != nil {
+			logger.Warn("解析时间戳失败: %s, %v", ts, err)
+			continue
+		}
+		records = append(records, record{Code: code, Time: t})
+	}
+
+	active := map[string]bool{
+		"busy": true, "reasoning": true, "using_tool": true,
+		"running": true, "pending": true, "retry": true,
+	}
+
+	var totalMs int64
+	now := time.Now()
+	for i, r := range records {
+		if !active[r.Code] {
+			continue
+		}
+		if i+1 < len(records) {
+			totalMs += records[i+1].Time.Sub(r.Time).Milliseconds()
+		} else if active[r.Code] {
+			totalMs += now.Sub(r.Time).Milliseconds()
+		}
+	}
+
+	logger.Debug("今日工作时长: %d 分钟", int(totalMs/60000))
+	return &WorkDuration{DurationMinutes: int(totalMs / 60000)}, nil
+}