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:
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user