fix: 修复6个bug + IoT设备控制增强 + DevTools IoT面板

问题1: 刷新后主对话历史不显示,侧边栏子对话列表为空
  - sessionStore: 修复 setCurrentSessionId 用 Map 去重消息
  - AppLayout: 修复 autoLoadNewSession 逻辑
  - useWebSocket: 修复 setMessages 调用时机

问题2: 切换到次级对话后无法切换回主对话
  - Sidebar: 为删除按钮添加 e.stopPropagation()

问题3&4: IoT设备列表展开导致输入栏消失 + 聊天消息无法滚动
  - IoTStatusBar: 从fixed定位改为inline布局
  - ChatContainer: 重构flex布局,MessageList自动撑满

问题5: AI核心无法操作IoT设备 + 无法设置温度等属性
  - 新增 IoTControlTool (iot_control_tool.go)
  - IoTClient: 新增 ToggleDevice/SetProperty/GetHistory
  - 支持 set_temperature/set_brightness/set_position/set_mode/set_color

问题6: DevTools启动时Gateway代理登录异常
  - devtools: 登录失败时静默降级,不阻塞启动

额外修复:
  - iot_tools.go: 修复fmt.Sprintf参数缺失
  - iot-debug-service: 修复并发死锁问题
  - DevTools: 新增IoT设备控制面板(API代理+前端UI)
This commit is contained in:
2026-05-17 14:37:44 +08:00
parent 5d0bb96abe
commit a80bfd12eb
20 changed files with 1299 additions and 58 deletions
+163 -1
View File
@@ -213,10 +213,146 @@ func (ds *DeviceStore) Toggle(id string) (*Device, error) {
}
cp := *d
cp.History = ds.GetHistory(id)
// 不能调用 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 "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
}
// 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()
@@ -307,6 +443,32 @@ func main() {
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 {