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:
@@ -1,8 +1,10 @@
|
||||
package tools
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
@@ -147,6 +149,51 @@ func (c *IoTClient) ToggleDevice(id string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetDeviceProperty 设置设备属性(温度、亮度、位置、模式、颜色等)
|
||||
func (c *IoTClient) SetDeviceProperty(id string, field string, value interface{}) error {
|
||||
body, err := json.Marshal(map[string]interface{}{
|
||||
"field": field,
|
||||
"value": value,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("序列化请求失败: %w", err)
|
||||
}
|
||||
|
||||
req, err := http.NewRequest(http.MethodPost, c.baseURL+"/api/v1/devices/"+id+"/set", nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("创建设置请求失败: %w", err)
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Body = io.NopCloser(bytes.NewReader(body))
|
||||
|
||||
resp, err := c.client.Do(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("设置设备 %s 属性失败: %w", id, err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode == http.StatusNotFound {
|
||||
return fmt.Errorf("设备 %s 不存在", id)
|
||||
}
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
var errResp struct {
|
||||
Error string `json:"error"`
|
||||
}
|
||||
json.NewDecoder(resp.Body).Decode(&errResp)
|
||||
if errResp.Error != "" {
|
||||
return fmt.Errorf("设置设备 %s 属性失败: %s", id, errResp.Error)
|
||||
}
|
||||
return fmt.Errorf("设置设备 %s 属性返回状态码 %d", id, resp.StatusCode)
|
||||
}
|
||||
|
||||
// 修改后清除缓存
|
||||
c.mu.Lock()
|
||||
c.cache = nil
|
||||
c.mu.Unlock()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetDevicesForContext 获取设备状态摘要(供上下文注入使用,失败不报错)
|
||||
func (c *IoTClient) GetDevicesForContext() []IoTDevice {
|
||||
devices, err := c.GetAllDevices()
|
||||
|
||||
@@ -0,0 +1,425 @@
|
||||
package tools
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// IoTControlTool IoT 设备控制工具
|
||||
type IoTControlTool struct {
|
||||
iotClient *IoTClient
|
||||
}
|
||||
|
||||
// NewIoTControlTool 创建 IoT 控制工具
|
||||
func NewIoTControlTool(iotClient *IoTClient) *IoTControlTool {
|
||||
return &IoTControlTool{iotClient: iotClient}
|
||||
}
|
||||
|
||||
// Definition 返回工具定义
|
||||
func (t *IoTControlTool) Definition() ToolDefinition {
|
||||
return ToolDefinition{
|
||||
Name: "iot_control",
|
||||
Description: "控制家中智能设备。可以开关灯光、空调、窗帘、门锁等设备,也可以调节温度、亮度、位置、模式、颜色等属性。" +
|
||||
"\n支持的操作:toggle(切换开关状态)、turn_on(打开设备)、turn_off(关闭设备)、" +
|
||||
"set_temperature(设置空调温度,需要 value 参数,单位°C)、" +
|
||||
"set_brightness(设置灯光亮度,需要 value 参数,0-100)、" +
|
||||
"set_position(设置窗帘位置,需要 value 参数,0-100,0=关闭 100=全开)、" +
|
||||
"set_mode(设置空调模式,需要 value 参数,可选值: cool/heat/auto)、" +
|
||||
"set_color(设置灯光颜色,需要 value 参数,可选值: warm_white/cool_white/colorful)",
|
||||
Parameters: map[string]interface{}{
|
||||
"type": "object",
|
||||
"properties": map[string]interface{}{
|
||||
"device_id": map[string]interface{}{
|
||||
"type": "string",
|
||||
"description": "要控制的设备ID。可选值: light-livingroom, light-bedroom, ac-livingroom, ac-bedroom, curtain-livingroom, lock-door",
|
||||
},
|
||||
"action": map[string]interface{}{
|
||||
"type": "string",
|
||||
"enum": []string{"toggle", "turn_on", "turn_off", "set_temperature", "set_brightness", "set_position", "set_mode", "set_color"},
|
||||
"description": "要执行的操作。toggle:切换开关状态;turn_on:打开设备;turn_off:关闭设备;set_temperature:设置空调温度(需配合value参数);set_brightness:设置灯光亮度(需配合value参数);set_position:设置窗帘位置(需配合value参数);set_mode:设置空调模式(需配合value参数);set_color:设置灯光颜色(需配合value参数)",
|
||||
},
|
||||
"value": map[string]interface{}{
|
||||
"type": "number",
|
||||
"description": "操作的值。set_temperature 时表示目标温度(°C),set_brightness 时表示亮度百分比(0-100),set_position 时表示窗帘开合程度(0-100)。action 为 set_temperature/set_brightness/set_position 时必须提供。set_mode 时为字符串(cool/heat/auto),set_color 时为字符串(warm_white/cool_white/colorful)",
|
||||
},
|
||||
},
|
||||
"required": []string{"device_id", "action"},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Execute 执行设备控制
|
||||
func (t *IoTControlTool) Execute(ctx context.Context, arguments map[string]interface{}) (*ToolResult, error) {
|
||||
if t.iotClient == nil {
|
||||
return &ToolResult{
|
||||
ToolName: "iot_control",
|
||||
Success: false,
|
||||
Error: "IoT 客户端未初始化",
|
||||
}, nil
|
||||
}
|
||||
|
||||
// 参数别名:entity_id → device_id
|
||||
deviceID, _ := arguments["device_id"].(string)
|
||||
if deviceID == "" {
|
||||
deviceID, _ = arguments["entity_id"].(string)
|
||||
}
|
||||
|
||||
action, _ := arguments["action"].(string)
|
||||
|
||||
if deviceID == "" {
|
||||
return &ToolResult{
|
||||
ToolName: "iot_control",
|
||||
Success: false,
|
||||
Error: "缺少设备ID(请使用 device_id 参数)",
|
||||
}, nil
|
||||
}
|
||||
|
||||
if action == "" {
|
||||
action = "toggle"
|
||||
}
|
||||
|
||||
// 处理属性设置类操作
|
||||
switch action {
|
||||
case "set_temperature":
|
||||
return t.handleSetTemperature(deviceID, arguments)
|
||||
case "set_brightness":
|
||||
return t.handleSetBrightness(deviceID, arguments)
|
||||
case "set_position":
|
||||
return t.handleSetPosition(deviceID, arguments)
|
||||
case "set_mode":
|
||||
return t.handleSetMode(deviceID, arguments)
|
||||
case "set_color":
|
||||
return t.handleSetColor(deviceID, arguments)
|
||||
}
|
||||
|
||||
// 处理开关类操作:需要获取当前设备状态
|
||||
currentDevice, err := t.iotClient.GetDevice(deviceID)
|
||||
if err != nil {
|
||||
return &ToolResult{
|
||||
ToolName: "iot_control",
|
||||
Success: false,
|
||||
Error: fmt.Sprintf("获取设备状态失败: %v", err),
|
||||
}, nil
|
||||
}
|
||||
|
||||
switch action {
|
||||
case "turn_on":
|
||||
// 如果设备已经开启,不需要操作
|
||||
if currentDevice.Status == "on" || currentDevice.Status == "open" || currentDevice.Status == "unlocked" {
|
||||
return &ToolResult{
|
||||
ToolName: "iot_control",
|
||||
Success: true,
|
||||
Data: fmt.Sprintf("设备 %s (%s) 已经处于开启状态,无需操作。", currentDevice.Name, deviceID),
|
||||
}, nil
|
||||
}
|
||||
if err := t.iotClient.ToggleDevice(deviceID); err != nil {
|
||||
return &ToolResult{
|
||||
ToolName: "iot_control",
|
||||
Success: false,
|
||||
Error: fmt.Sprintf("打开设备失败: %v", err),
|
||||
}, nil
|
||||
}
|
||||
return &ToolResult{
|
||||
ToolName: "iot_control",
|
||||
Success: true,
|
||||
Data: fmt.Sprintf("已打开设备: %s", currentDevice.Name),
|
||||
}, nil
|
||||
|
||||
case "turn_off":
|
||||
// 如果设备已经关闭,不需要操作
|
||||
if currentDevice.Status == "off" || currentDevice.Status == "closed" || currentDevice.Status == "locked" {
|
||||
return &ToolResult{
|
||||
ToolName: "iot_control",
|
||||
Success: true,
|
||||
Data: fmt.Sprintf("设备 %s (%s) 已经处于关闭状态,无需操作。", currentDevice.Name, deviceID),
|
||||
}, nil
|
||||
}
|
||||
if err := t.iotClient.ToggleDevice(deviceID); err != nil {
|
||||
return &ToolResult{
|
||||
ToolName: "iot_control",
|
||||
Success: false,
|
||||
Error: fmt.Sprintf("关闭设备失败: %v", err),
|
||||
}, nil
|
||||
}
|
||||
return &ToolResult{
|
||||
ToolName: "iot_control",
|
||||
Success: true,
|
||||
Data: fmt.Sprintf("已关闭设备: %s", currentDevice.Name),
|
||||
}, nil
|
||||
|
||||
default: // "toggle"
|
||||
if err := t.iotClient.ToggleDevice(deviceID); err != nil {
|
||||
return &ToolResult{
|
||||
ToolName: "iot_control",
|
||||
Success: false,
|
||||
Error: fmt.Sprintf("操作设备失败: %v", err),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// 获取切换后的状态
|
||||
updatedDevice, err := t.iotClient.GetDevice(deviceID)
|
||||
if err != nil {
|
||||
return &ToolResult{
|
||||
ToolName: "iot_control",
|
||||
Success: true,
|
||||
Data: fmt.Sprintf("已成功切换设备 %s 的状态。", currentDevice.Name),
|
||||
}, nil
|
||||
}
|
||||
|
||||
return &ToolResult{
|
||||
ToolName: "iot_control",
|
||||
Success: true,
|
||||
Data: fmt.Sprintf("已成功操作设备: %s\n当前状态: %s", updatedDevice.Name, formatDeviceLine(*updatedDevice)),
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
// extractValue 从 arguments 中提取 value 参数(支持 value/Value 及数字/字符串类型)
|
||||
func extractValue(arguments map[string]interface{}) interface{} {
|
||||
if v, ok := arguments["value"]; ok {
|
||||
return v
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// handleSetTemperature 处理设置温度
|
||||
func (t *IoTControlTool) handleSetTemperature(deviceID string, arguments map[string]interface{}) (*ToolResult, error) {
|
||||
val := extractValue(arguments)
|
||||
if val == nil {
|
||||
return &ToolResult{
|
||||
ToolName: "iot_control",
|
||||
Success: false,
|
||||
Error: "缺少 value 参数,请指定目标温度(如 24)",
|
||||
}, nil
|
||||
}
|
||||
|
||||
// 先获取当前设备信息
|
||||
currentDevice, err := t.iotClient.GetDevice(deviceID)
|
||||
if err != nil {
|
||||
return &ToolResult{
|
||||
ToolName: "iot_control",
|
||||
Success: false,
|
||||
Error: fmt.Sprintf("获取设备状态失败: %v", err),
|
||||
}, nil
|
||||
}
|
||||
|
||||
temperature, ok := toFloat64(val)
|
||||
if !ok {
|
||||
return &ToolResult{
|
||||
ToolName: "iot_control",
|
||||
Success: false,
|
||||
Error: fmt.Sprintf("温度值无效: %v", val),
|
||||
}, nil
|
||||
}
|
||||
|
||||
if err := t.iotClient.SetDeviceProperty(deviceID, "temperature", temperature); err != nil {
|
||||
return &ToolResult{
|
||||
ToolName: "iot_control",
|
||||
Success: false,
|
||||
Error: fmt.Sprintf("设置温度失败: %v", err),
|
||||
}, nil
|
||||
}
|
||||
|
||||
return &ToolResult{
|
||||
ToolName: "iot_control",
|
||||
Success: true,
|
||||
Data: fmt.Sprintf("已将 %s 温度从 %.1f°C 调整为 %.1f°C", currentDevice.Name, currentDevice.Temperature, temperature),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// handleSetBrightness 处理设置亮度
|
||||
func (t *IoTControlTool) handleSetBrightness(deviceID string, arguments map[string]interface{}) (*ToolResult, error) {
|
||||
val := extractValue(arguments)
|
||||
if val == nil {
|
||||
return &ToolResult{
|
||||
ToolName: "iot_control",
|
||||
Success: false,
|
||||
Error: "缺少 value 参数,请指定亮度值(0-100)",
|
||||
}, nil
|
||||
}
|
||||
|
||||
// 先获取当前设备信息
|
||||
currentDevice, err := t.iotClient.GetDevice(deviceID)
|
||||
if err != nil {
|
||||
return &ToolResult{
|
||||
ToolName: "iot_control",
|
||||
Success: false,
|
||||
Error: fmt.Sprintf("获取设备状态失败: %v", err),
|
||||
}, nil
|
||||
}
|
||||
|
||||
brightness, ok := toFloat64(val)
|
||||
if !ok {
|
||||
return &ToolResult{
|
||||
ToolName: "iot_control",
|
||||
Success: false,
|
||||
Error: fmt.Sprintf("亮度值无效: %v", val),
|
||||
}, nil
|
||||
}
|
||||
|
||||
if err := t.iotClient.SetDeviceProperty(deviceID, "brightness", brightness); err != nil {
|
||||
return &ToolResult{
|
||||
ToolName: "iot_control",
|
||||
Success: false,
|
||||
Error: fmt.Sprintf("设置亮度失败: %v", err),
|
||||
}, nil
|
||||
}
|
||||
|
||||
return &ToolResult{
|
||||
ToolName: "iot_control",
|
||||
Success: true,
|
||||
Data: fmt.Sprintf("已将 %s 亮度调整为 %d%%", currentDevice.Name, int(brightness)),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// handleSetPosition 处理设置窗帘位置
|
||||
func (t *IoTControlTool) handleSetPosition(deviceID string, arguments map[string]interface{}) (*ToolResult, error) {
|
||||
val := extractValue(arguments)
|
||||
if val == nil {
|
||||
return &ToolResult{
|
||||
ToolName: "iot_control",
|
||||
Success: false,
|
||||
Error: "缺少 value 参数,请指定位置值(0=关闭, 100=全开)",
|
||||
}, nil
|
||||
}
|
||||
|
||||
currentDevice, err := t.iotClient.GetDevice(deviceID)
|
||||
if err != nil {
|
||||
return &ToolResult{
|
||||
ToolName: "iot_control",
|
||||
Success: false,
|
||||
Error: fmt.Sprintf("获取设备状态失败: %v", err),
|
||||
}, nil
|
||||
}
|
||||
|
||||
position, ok := toFloat64(val)
|
||||
if !ok {
|
||||
return &ToolResult{
|
||||
ToolName: "iot_control",
|
||||
Success: false,
|
||||
Error: fmt.Sprintf("位置值无效: %v", val),
|
||||
}, nil
|
||||
}
|
||||
|
||||
if err := t.iotClient.SetDeviceProperty(deviceID, "position", position); err != nil {
|
||||
return &ToolResult{
|
||||
ToolName: "iot_control",
|
||||
Success: false,
|
||||
Error: fmt.Sprintf("设置窗帘位置失败: %v", err),
|
||||
}, nil
|
||||
}
|
||||
|
||||
return &ToolResult{
|
||||
ToolName: "iot_control",
|
||||
Success: true,
|
||||
Data: fmt.Sprintf("已将 %s 窗帘调整为 %d%%", currentDevice.Name, int(position)),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// handleSetMode 处理设置空调模式
|
||||
func (t *IoTControlTool) handleSetMode(deviceID string, arguments map[string]interface{}) (*ToolResult, error) {
|
||||
val := extractValue(arguments)
|
||||
if val == nil {
|
||||
return &ToolResult{
|
||||
ToolName: "iot_control",
|
||||
Success: false,
|
||||
Error: "缺少 value 参数,请指定模式(cool/heat/auto)",
|
||||
}, nil
|
||||
}
|
||||
|
||||
mode, ok := val.(string)
|
||||
if !ok {
|
||||
return &ToolResult{
|
||||
ToolName: "iot_control",
|
||||
Success: false,
|
||||
Error: fmt.Sprintf("模式值无效: %v", val),
|
||||
}, nil
|
||||
}
|
||||
|
||||
currentDevice, err := t.iotClient.GetDevice(deviceID)
|
||||
if err != nil {
|
||||
return &ToolResult{
|
||||
ToolName: "iot_control",
|
||||
Success: false,
|
||||
Error: fmt.Sprintf("获取设备状态失败: %v", err),
|
||||
}, nil
|
||||
}
|
||||
|
||||
if err := t.iotClient.SetDeviceProperty(deviceID, "mode", mode); err != nil {
|
||||
return &ToolResult{
|
||||
ToolName: "iot_control",
|
||||
Success: false,
|
||||
Error: fmt.Sprintf("设置模式失败: %v", err),
|
||||
}, nil
|
||||
}
|
||||
|
||||
return &ToolResult{
|
||||
ToolName: "iot_control",
|
||||
Success: true,
|
||||
Data: fmt.Sprintf("已将 %s 模式切换为 %s", currentDevice.Name, mode),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// handleSetColor 处理设置灯光颜色
|
||||
func (t *IoTControlTool) handleSetColor(deviceID string, arguments map[string]interface{}) (*ToolResult, error) {
|
||||
val := extractValue(arguments)
|
||||
if val == nil {
|
||||
return &ToolResult{
|
||||
ToolName: "iot_control",
|
||||
Success: false,
|
||||
Error: "缺少 value 参数,请指定颜色(warm_white/cool_white/colorful)",
|
||||
}, nil
|
||||
}
|
||||
|
||||
color, ok := val.(string)
|
||||
if !ok {
|
||||
return &ToolResult{
|
||||
ToolName: "iot_control",
|
||||
Success: false,
|
||||
Error: fmt.Sprintf("颜色值无效: %v", val),
|
||||
}, nil
|
||||
}
|
||||
|
||||
currentDevice, err := t.iotClient.GetDevice(deviceID)
|
||||
if err != nil {
|
||||
return &ToolResult{
|
||||
ToolName: "iot_control",
|
||||
Success: false,
|
||||
Error: fmt.Sprintf("获取设备状态失败: %v", err),
|
||||
}, nil
|
||||
}
|
||||
|
||||
if err := t.iotClient.SetDeviceProperty(deviceID, "color", color); err != nil {
|
||||
return &ToolResult{
|
||||
ToolName: "iot_control",
|
||||
Success: false,
|
||||
Error: fmt.Sprintf("设置颜色失败: %v", err),
|
||||
}, nil
|
||||
}
|
||||
|
||||
return &ToolResult{
|
||||
ToolName: "iot_control",
|
||||
Success: true,
|
||||
Data: fmt.Sprintf("已将 %s 灯光颜色切换为 %s", currentDevice.Name, color),
|
||||
}, 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
|
||||
}
|
||||
}
|
||||
@@ -85,7 +85,7 @@ func (t *IoTQueryTool) Execute(ctx context.Context, arguments map[string]interfa
|
||||
}
|
||||
|
||||
func formatSingleDevice(d *IoTDevice) string {
|
||||
return fmt.Sprintf("设备: %s (%s)\n状态: %s", d.Name, formatDeviceLine(*d))
|
||||
return fmt.Sprintf("设备: %s (%s)\n状态: %s", d.Name, d.Type, formatDeviceLine(*d))
|
||||
}
|
||||
|
||||
func formatDeviceLine(d IoTDevice) string {
|
||||
|
||||
Reference in New Issue
Block a user