Files
Cyrene/backend/iot-debug-service/cmd/main.go
T
AskaEth 78e3f450c2 feat: Round 5 - Memory Service, Tool Engine, Call Records, Thinking Logs
- Fix: Session history flash (race condition + WS guard)
- Fix: Chat background overlay + sidebar transparency
- Fix: IoT device control (Chinese action names, status field)
- Feat: Independent memory-service (port 8091, 13 endpoints)
- Feat: Independent tool-engine service (port 8092, 13 tools)
- Feat: Tool call logs with paginated DevTools panel
- Feat: Thinking log records with DevTools panel
- Feat: Future development roadmap document
- Chore: Updated .gitignore, go.work, DevTools config
- Chore: 5-service health check, project review docs
2026-05-18 20:05:14 +08:00

626 lines
16 KiB
Go

package main
import (
"encoding/json"
"fmt"
"log"
"math/rand"
"net/http"
"os"
"strings"
"sync"
"time"
)
// DeviceType 设备类型
type DeviceType string
const (
TypeLight DeviceType = "light"
TypeAC DeviceType = "ac"
TypeCurtain DeviceType = "curtain"
TypeSensor DeviceType = "sensor"
TypeLock DeviceType = "lock"
)
// Device 设备状态
type Device struct {
ID string `json:"id"`
Name string `json:"name"`
Type DeviceType `json:"type"`
Status string `json:"status"` // on/off/closed/open/locked/unlocked
Brightness int `json:"brightness,omitempty"`
Color string `json:"color,omitempty"`
Temperature float64 `json:"temperature,omitempty"`
Mode string `json:"mode,omitempty"` // cool/heat/auto
Position int `json:"position,omitempty"` // curtain position 0-100
Value float64 `json:"value,omitempty"` // sensor value
Unit string `json:"unit,omitempty"` // sensor unit
Battery int `json:"battery,omitempty"` // lock battery
LastUpdated string `json:"last_updated"`
History []HistoryEntry `json:"history,omitempty"`
}
// HistoryEntry 设备状态历史
type HistoryEntry struct {
Timestamp string `json:"timestamp"`
Field string `json:"field"`
OldValue string `json:"old_value"`
NewValue string `json:"new_value"`
}
// DeviceStore 设备存储(线程安全)
type DeviceStore struct {
mu sync.RWMutex
devices map[string]*Device
history map[string][]HistoryEntry
}
func NewDeviceStore() *DeviceStore {
ds := &DeviceStore{
devices: make(map[string]*Device),
history: make(map[string][]HistoryEntry),
}
ds.initDevices()
return ds
}
func (ds *DeviceStore) initDevices() {
now := time.Now().UTC().Format(time.RFC3339)
devices := []*Device{
{ID: "light-livingroom", Name: "客厅灯", Type: TypeLight, Status: "on", Brightness: 80, Color: "warm_white", LastUpdated: now},
{ID: "light-bedroom", Name: "卧室灯", Type: TypeLight, Status: "off", Brightness: 0, Color: "warm_white", LastUpdated: now},
{ID: "ac-livingroom", Name: "客厅空调", Type: TypeAC, Status: "on", Temperature: 26, Mode: "cool", LastUpdated: now},
{ID: "ac-bedroom", Name: "卧室空调", Type: TypeAC, Status: "off", Temperature: 24, Mode: "auto", LastUpdated: now},
{ID: "curtain-livingroom", Name: "客厅窗帘", Type: TypeCurtain, Status: "closed", Position: 0, LastUpdated: now},
{ID: "sensor-temperature", Name: "温度传感器", Type: TypeSensor, Value: 25.5, Unit: "celsius", LastUpdated: now},
{ID: "sensor-humidity", Name: "湿度传感器", Type: TypeSensor, Value: 60, Unit: "percent", LastUpdated: now},
{ID: "lock-door", Name: "智能门锁", Type: TypeLock, Status: "locked", Battery: 85, LastUpdated: now},
}
for _, d := range devices {
ds.devices[d.ID] = d
ds.history[d.ID] = make([]HistoryEntry, 0)
}
}
// GetAll 获取所有设备
func (ds *DeviceStore) GetAll() []*Device {
ds.mu.RLock()
defer ds.mu.RUnlock()
result := make([]*Device, 0, len(ds.devices))
for _, d := range ds.devices {
cp := *d
cp.History = nil // 列表不返回历史
result = append(result, &cp)
}
return result
}
// Get 获取单个设备
func (ds *DeviceStore) Get(id string) *Device {
ds.mu.RLock()
defer ds.mu.RUnlock()
d, ok := ds.devices[id]
if !ok {
return nil
}
cp := *d
// 包含最近10条历史(RLock 可重入)
if h, ok := ds.history[id]; ok && len(h) > 0 {
start := 0
if len(h) > 10 {
start = len(h) - 10
}
cp.History = make([]HistoryEntry, len(h)-start)
copy(cp.History, h[start:])
} else {
cp.History = []HistoryEntry{}
}
return &cp
}
// GetHistory 获取设备状态历史(最近10条)
func (ds *DeviceStore) GetHistory(id string) []HistoryEntry {
ds.mu.RLock()
defer ds.mu.RUnlock()
h, ok := ds.history[id]
if !ok || len(h) == 0 {
return []HistoryEntry{}
}
start := 0
if len(h) > 10 {
start = len(h) - 10
}
result := make([]HistoryEntry, len(h[start:]))
copy(result, h[start:])
return result
}
// addHistory 添加历史记录
func (ds *DeviceStore) addHistory(id, field, oldVal, newVal string) {
entry := HistoryEntry{
Timestamp: time.Now().UTC().Format(time.RFC3339),
Field: field,
OldValue: oldVal,
NewValue: newVal,
}
ds.history[id] = append(ds.history[id], entry)
}
// Toggle 切换设备开关状态
func (ds *DeviceStore) Toggle(id string) (*Device, error) {
ds.mu.Lock()
defer ds.mu.Unlock()
d, ok := ds.devices[id]
if !ok {
return nil, fmt.Errorf("设备 %s 不存在", id)
}
switch d.Type {
case TypeLight:
oldStatus := d.Status
if d.Status == "on" {
d.Status = "off"
d.Brightness = 0
} else {
d.Status = "on"
d.Brightness = 80
}
d.LastUpdated = time.Now().UTC().Format(time.RFC3339)
ds.addHistory(id, "status", oldStatus, d.Status)
case TypeAC:
oldStatus := d.Status
if d.Status == "on" {
d.Status = "off"
} else {
d.Status = "on"
}
d.LastUpdated = time.Now().UTC().Format(time.RFC3339)
ds.addHistory(id, "status", oldStatus, d.Status)
case TypeCurtain:
oldStatus := d.Status
if d.Status == "closed" {
d.Status = "open"
d.Position = 100
} else {
d.Status = "closed"
d.Position = 0
}
d.LastUpdated = time.Now().UTC().Format(time.RFC3339)
ds.addHistory(id, "status", oldStatus, d.Status)
case TypeLock:
oldStatus := d.Status
if d.Status == "locked" {
d.Status = "unlocked"
} else {
d.Status = "locked"
}
d.LastUpdated = time.Now().UTC().Format(time.RFC3339)
ds.addHistory(id, "status", oldStatus, d.Status)
default:
return nil, fmt.Errorf("设备类型 %s 不支持切换", d.Type)
}
cp := *d
// 不能调用 ds.GetHistory(id),因为当前已持有写锁,Go 的 RWMutex 不允许写锁重入读锁
if h, ok := ds.history[id]; ok && len(h) > 0 {
start := 0
if len(h) > 10 {
start = len(h) - 10
}
cp.History = make([]HistoryEntry, len(h)-start)
copy(cp.History, h[start:])
} else {
cp.History = []HistoryEntry{}
}
return &cp, nil
}
// SetProperty 设置设备属性(状态、温度、亮度、位置、模式、颜色等)
func (ds *DeviceStore) SetProperty(id, field string, value interface{}) (*Device, error) {
ds.mu.Lock()
defer ds.mu.Unlock()
d, ok := ds.devices[id]
if !ok {
return nil, fmt.Errorf("设备 %s 不存在", id)
}
now := time.Now().UTC().Format(time.RFC3339)
switch field {
case "status", "power":
// 声明式电源控制:支持 "on"/"off"/"open"/"closed"/"locked"/"unlocked"
// 同时支持中文值: "开"/"关"/"打开"/"关闭"
// 支持布尔值: true/false → on/off
var newStatus string
switch v := value.(type) {
case string:
newStatus = normalizeStatus(v, d.Type)
if newStatus == "" {
return nil, fmt.Errorf("无效的状态值: %s (设备类型 %s 不支持此状态)", v, d.Type)
}
case bool:
if v {
newStatus = "on"
} else {
newStatus = "off"
}
case float64:
if v == 0 {
newStatus = "off"
} else {
newStatus = "on"
}
default:
return nil, fmt.Errorf("status 需要字符串、布尔值或数字")
}
// 对于非 on/off 设备类型(curtain/lock),转换为对应状态
switch d.Type {
case TypeCurtain:
if newStatus == "on" {
newStatus = "open"
} else if newStatus == "off" {
newStatus = "closed"
}
case TypeLock:
if newStatus == "on" {
newStatus = "unlocked"
} else if newStatus == "off" {
newStatus = "locked"
}
}
if d.Status == newStatus {
// 状态未变化,直接返回
cp := *d
return &cp, nil
}
oldStatus := d.Status
d.Status = newStatus
// 根据设备类型设置关联属性
switch d.Type {
case TypeLight:
if newStatus == "off" {
d.Brightness = 0
} else if d.Brightness == 0 {
d.Brightness = 80
}
case TypeCurtain:
if newStatus == "closed" {
d.Position = 0
} else {
d.Position = 100
}
}
d.LastUpdated = now
ds.addHistory(id, "status", oldStatus, newStatus)
case "temperature":
v, ok := toFloat64(value)
if !ok {
return nil, fmt.Errorf("temperature 需要数字值")
}
if d.Type != TypeAC {
return nil, fmt.Errorf("设备 %s (类型 %s) 不支持温度调节", d.Name, d.Type)
}
oldVal := fmt.Sprintf("%.1f", d.Temperature)
d.Temperature = v
d.LastUpdated = now
ds.addHistory(id, "temperature", oldVal, fmt.Sprintf("%.1f", v))
case "brightness":
v, ok := toFloat64(value)
if !ok {
return nil, fmt.Errorf("brightness 需要数字值")
}
if d.Type != TypeLight {
return nil, fmt.Errorf("设备 %s (类型 %s) 不支持亮度调节", d.Name, d.Type)
}
oldVal := fmt.Sprintf("%d", d.Brightness)
d.Brightness = clampInt(int(v), 0, 100)
d.LastUpdated = now
ds.addHistory(id, "brightness", oldVal, fmt.Sprintf("%d", d.Brightness))
case "position":
v, ok := toFloat64(value)
if !ok {
return nil, fmt.Errorf("position 需要数字值")
}
if d.Type != TypeCurtain {
return nil, fmt.Errorf("设备 %s (类型 %s) 不支持位置调节", d.Name, d.Type)
}
oldVal := fmt.Sprintf("%d", d.Position)
d.Position = clampInt(int(v), 0, 100)
d.LastUpdated = now
ds.addHistory(id, "position", oldVal, fmt.Sprintf("%d", d.Position))
case "mode":
v, ok := value.(string)
if !ok {
return nil, fmt.Errorf("mode 需要字符串值")
}
if d.Type != TypeAC {
return nil, fmt.Errorf("设备 %s (类型 %s) 不支持模式切换", d.Name, d.Type)
}
oldVal := d.Mode
d.Mode = v
d.LastUpdated = now
ds.addHistory(id, "mode", oldVal, v)
case "color":
v, ok := value.(string)
if !ok {
return nil, fmt.Errorf("color 需要字符串值")
}
if d.Type != TypeLight {
return nil, fmt.Errorf("设备 %s (类型 %s) 不支持颜色调节", d.Name, d.Type)
}
oldVal := d.Color
d.Color = v
d.LastUpdated = now
ds.addHistory(id, "color", oldVal, v)
default:
return nil, fmt.Errorf("不支持的属性: %s", field)
}
cp := *d
if h, ok := ds.history[id]; ok && len(h) > 0 {
start := 0
if len(h) > 10 {
start = len(h) - 10
}
cp.History = make([]HistoryEntry, len(h)-start)
copy(cp.History, h[start:])
} else {
cp.History = []HistoryEntry{}
}
return &cp, nil
}
// normalizeStatus 标准化电源状态值
// 支持英文: "on"/"off"/"open"/"closed"/"locked"/"unlocked"
// 支持中文: "开"/"关"/"打开"/"关闭"/"开启"/"解锁"/"锁定"/"上锁"
// 支持布尔兼容: "true"/"false"
// 返回空字符串表示无效值
func normalizeStatus(v string, dType DeviceType) string {
switch strings.ToLower(strings.TrimSpace(v)) {
case "on", "true", "开", "打开", "开启":
return "on"
case "off", "false", "关", "关闭":
return "off"
case "open":
if dType == TypeCurtain {
return "open"
}
return "on"
case "closed":
if dType == TypeCurtain {
return "closed"
}
return "off"
case "locked", "锁定", "上锁":
return "locked"
case "unlocked", "解锁":
return "unlocked"
default:
return ""
}
}
// toFloat64 将 interface{} 转换为 float64
func toFloat64(v interface{}) (float64, bool) {
switch val := v.(type) {
case float64:
return val, true
case float32:
return float64(val), true
case int:
return float64(val), true
case int64:
return float64(val), true
case json.Number:
f, err := val.Float64()
return f, err == nil
default:
return 0, false
}
}
// clampInt 限制整数在 [min, max] 范围内
func clampInt(v, min, max int) int {
if v < min {
return min
}
if v > max {
return max
}
return v
}
// SimulateFluctuation 模拟传感器随机波动
func (ds *DeviceStore) SimulateFluctuation() {
ds.mu.Lock()
defer ds.mu.Unlock()
now := time.Now().UTC().Format(time.RFC3339)
// 温度传感器: ±0.2°C
if t, ok := ds.devices["sensor-temperature"]; ok {
oldVal := t.Value
t.Value += (rand.Float64()*0.4 - 0.2)
t.Value = float64(int(t.Value*10)) / 10 // 保留一位小数
t.LastUpdated = now
ds.addHistory("sensor-temperature", "value", fmt.Sprintf("%.1f", oldVal), fmt.Sprintf("%.1f", t.Value))
}
// 湿度传感器: ±1%
if h, ok := ds.devices["sensor-humidity"]; ok {
oldVal := h.Value
h.Value += float64(rand.Intn(3) - 1) // -1, 0, +1
if h.Value < 0 {
h.Value = 0
}
if h.Value > 100 {
h.Value = 100
}
h.LastUpdated = now
ds.addHistory("sensor-humidity", "value", fmt.Sprintf("%.0f", oldVal), fmt.Sprintf("%.0f", h.Value))
}
}
func main() {
port := getEnv("IOT_DEBUG_PORT", "8083")
store := NewDeviceStore()
// 启动传感器波动模拟(每30秒)
go func() {
ticker := time.NewTicker(30 * time.Second)
defer ticker.Stop()
for range ticker.C {
store.SimulateFluctuation()
}
}()
mux := http.NewServeMux()
// GET /api/v1/devices - 列出所有设备
mux.HandleFunc("/api/v1/devices", func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
devices := store.GetAll()
writeJSON(w, http.StatusOK, map[string]interface{}{
"devices": devices,
"total": len(devices),
})
})
// GET /api/v1/devices/{id} - 获取单个设备
// POST /api/v1/devices/{id}/toggle - 切换设备
mux.HandleFunc("/api/v1/devices/", func(w http.ResponseWriter, r *http.Request) {
path := strings.TrimPrefix(r.URL.Path, "/api/v1/devices/")
parts := strings.Split(path, "/")
if len(parts) == 0 || parts[0] == "" {
http.Error(w, "缺少设备ID", http.StatusBadRequest)
return
}
deviceID := parts[0]
// POST /api/v1/devices/{id}/toggle
if len(parts) == 2 && parts[1] == "toggle" {
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
device, err := store.Toggle(deviceID)
if err != nil {
writeJSON(w, http.StatusNotFound, map[string]string{"error": err.Error()})
return
}
writeJSON(w, http.StatusOK, map[string]interface{}{
"device": device,
"action": "toggled",
})
return
}
// POST /api/v1/devices/{id}/set - 设置设备属性
if len(parts) == 2 && parts[1] == "set" {
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
var req struct {
Field string `json:"field"`
Value interface{} `json:"value"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "请求格式错误"})
return
}
device, err := store.SetProperty(deviceID, req.Field, req.Value)
if err != nil {
writeJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
return
}
writeJSON(w, http.StatusOK, map[string]interface{}{
"device": device,
"action": "set_" + req.Field,
})
return
}
// GET /api/v1/devices/{id}/history
if len(parts) == 2 && parts[1] == "history" {
if r.Method != http.MethodGet {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
history := store.GetHistory(deviceID)
writeJSON(w, http.StatusOK, map[string]interface{}{
"device_id": deviceID,
"history": history,
})
return
}
// GET /api/v1/devices/{id}
if r.Method != http.MethodGet {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
device := store.Get(deviceID)
if device == nil {
writeJSON(w, http.StatusNotFound, map[string]string{"error": fmt.Sprintf("设备 %s 不存在", deviceID)})
return
}
writeJSON(w, http.StatusOK, map[string]interface{}{
"device": device,
})
})
// 健康检查
mux.HandleFunc("/api/v1/health", func(w http.ResponseWriter, r *http.Request) {
writeJSON(w, http.StatusOK, map[string]string{
"status": "ok",
"service": "iot-debug-service",
})
})
log.Printf("🔌 IoT 调试服务启动在端口 %s", port)
log.Printf(" 模拟设备数: %d", len(store.GetAll()))
if err := http.ListenAndServe(":"+port, mux); err != nil {
log.Fatalf("服务启动失败: %v", err)
}
}
func writeJSON(w http.ResponseWriter, status int, data interface{}) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
json.NewEncoder(w).Encode(data)
}
func getEnv(key, fallback string) string {
if v := os.Getenv(key); v != "" {
return v
}
return fallback
}