Files
Cyrene/backend/tool-engine/internal/tools/iot_control.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

440 lines
13 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package tools
import (
"context"
"encoding/json"
"fmt"
"strings"
"github.com/yourname/cyrene-ai/tool-engine/internal/model"
)
// IoTControlTool IoT 设备控制工具
type IoTControlTool struct {
iotClient IoTClientInterface
}
// NewIoTControlTool 创建 IoT 控制工具
func NewIoTControlTool(iotClient IoTClientInterface) *IoTControlTool {
return &IoTControlTool{iotClient: iotClient}
}
// Definition 返回工具定义
func (t *IoTControlTool) Definition() model.ToolDefinition {
return model.ToolDefinition{
Name: "iot_control",
Description: "【仅当开拓者明确要求控制设备时才使用此工具】控制家中智能设备。可以开关灯光、空调、窗帘、门锁等设备,也可以调节温度、亮度、位置、模式、颜色等属性。" +
"\n⚠️ 重要约束:" +
"\n - 不要在开拓者只是询问设备状态时调用此工具(查询设备请用 iot_query" +
"\n - 不要自行决定执行操作,必须等开拓者明确说出「打开」「关闭」「调到」「设置」等控制指令" +
"\n - 不要因为之前对话中提到过某个设备就主动控制它" +
"\n支持的操作:toggle(切换开关状态)、turn_on(打开设备)、turn_off(关闭设备)、" +
"set_temperature(设置空调温度,需要 value 参数,单位°C)、" +
"set_brightness(设置灯光亮度,需要 value 参数,0-100)、" +
"set_position(设置窗帘位置,需要 value 参数,0-1000=关闭 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"},
},
}
}
// normalizeAction 标准化 action 参数,支持中文别名、power 参数等
func normalizeAction(arguments map[string]interface{}) string {
action, _ := arguments["action"].(string)
// 如果 action 为空,检查 power/status 参数
if action == "" {
// power 参数: "off"/"关"/"关闭" → turn_off, "on"/"开"/"打开" → turn_on
if pv, ok := arguments["power"]; ok {
switch v := pv.(type) {
case string:
switch strings.ToLower(strings.TrimSpace(v)) {
case "off", "false", "关", "关闭":
return "turn_off"
case "on", "true", "开", "打开", "开启":
return "turn_on"
}
case bool:
if !v {
return "turn_off"
}
return "turn_on"
}
}
// status 参数同理
if sv, ok := arguments["status"]; ok {
switch v := sv.(type) {
case string:
switch strings.ToLower(strings.TrimSpace(v)) {
case "off", "false", "关", "关闭":
return "turn_off"
case "on", "true", "开", "打开", "开启":
return "turn_on"
}
case bool:
if !v {
return "turn_off"
}
return "turn_on"
}
}
// 默认 toggle
return "toggle"
}
// 标准化中文 action 名
switch strings.ToLower(strings.TrimSpace(action)) {
case "打开", "开启", "开":
return "turn_on"
case "关闭", "关":
return "turn_off"
case "切换":
return "toggle"
case "设置温度", "调温度", "set_temp":
return "set_temperature"
case "设置亮度", "调亮度", "set_light":
return "set_brightness"
case "设置位置", "调位置":
return "set_position"
case "设置模式", "调模式", "切换模式":
return "set_mode"
case "设置颜色", "调颜色", "换颜色":
return "set_color"
}
return action
}
// Execute 执行设备控制
func (t *IoTControlTool) Execute(ctx context.Context, arguments map[string]interface{}) (*model.ToolResult, error) {
if t.iotClient == nil {
return &model.ToolResult{
Output: "",
Error: "IoT 客户端未初始化",
}, nil
}
// 参数别名:entity_id → device_id
deviceID, _ := arguments["device_id"].(string)
if deviceID == "" {
deviceID, _ = arguments["entity_id"].(string)
}
action := normalizeAction(arguments)
if deviceID == "" {
return &model.ToolResult{
Output: "",
Error: "缺少设备ID(请使用 device_id 参数)",
}, nil
}
// 先获取设备名用于友好的返回消息(失败不影响后续流程)
deviceName := deviceID
if dev, err := t.iotClient.GetDevice(deviceID); err == nil {
deviceName = dev.Name
}
// 处理属性设置类操作
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)
case "turn_off":
// 声明式关闭:使用 SetDeviceProperty status/off 而非 toggle
// 即使设备已经关闭,SetProperty 也会幂等处理
if err := t.iotClient.SetDeviceProperty(deviceID, "status", "off"); err != nil {
return &model.ToolResult{
Output: "",
Error: fmt.Sprintf("关闭设备失败: %v", err),
}, nil
}
return &model.ToolResult{
Output: fmt.Sprintf("已关闭设备: %s", deviceName),
Error: "",
}, nil
case "turn_on":
// 声明式打开:使用 SetDeviceProperty status/on 而非 toggle
if err := t.iotClient.SetDeviceProperty(deviceID, "status", "on"); err != nil {
return &model.ToolResult{
Output: "",
Error: fmt.Sprintf("打开设备失败: %v", err),
}, nil
}
return &model.ToolResult{
Output: fmt.Sprintf("已打开设备: %s", deviceName),
Error: "",
}, nil
default: // "toggle"
if err := t.iotClient.ToggleDevice(deviceID); err != nil {
return &model.ToolResult{
Output: "",
Error: fmt.Sprintf("操作设备失败: %v", err),
}, nil
}
// 获取切换后的状态
updatedDevice, err := t.iotClient.GetDevice(deviceID)
if err != nil {
return &model.ToolResult{
Output: fmt.Sprintf("已成功切换设备 %s 的状态。", deviceName),
Error: "",
}, nil
}
return &model.ToolResult{
Output: fmt.Sprintf("已成功操作设备: %s\n当前状态: %s", updatedDevice.Name, formatDeviceLine(*updatedDevice)),
Error: "",
}, 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{}) (*model.ToolResult, error) {
val := extractValue(arguments)
if val == nil {
return &model.ToolResult{
Output: "",
Error: "缺少 value 参数,请指定目标温度(如 24)",
}, nil
}
// 先获取当前设备信息
currentDevice, err := t.iotClient.GetDevice(deviceID)
if err != nil {
return &model.ToolResult{
Output: "",
Error: fmt.Sprintf("获取设备状态失败: %v", err),
}, nil
}
temperature, ok := toFloat64(val)
if !ok {
return &model.ToolResult{
Output: "",
Error: fmt.Sprintf("温度值无效: %v", val),
}, nil
}
if err := t.iotClient.SetDeviceProperty(deviceID, "temperature", temperature); err != nil {
return &model.ToolResult{
Output: "",
Error: fmt.Sprintf("设置温度失败: %v", err),
}, nil
}
return &model.ToolResult{
Output: fmt.Sprintf("已将 %s 温度从 %.1f°C 调整为 %.1f°C", currentDevice.Name, currentDevice.Temperature, temperature),
Error: "",
}, nil
}
// handleSetBrightness 处理设置亮度
func (t *IoTControlTool) handleSetBrightness(deviceID string, arguments map[string]interface{}) (*model.ToolResult, error) {
val := extractValue(arguments)
if val == nil {
return &model.ToolResult{
Output: "",
Error: "缺少 value 参数,请指定亮度值(0-100",
}, nil
}
// 先获取当前设备信息
currentDevice, err := t.iotClient.GetDevice(deviceID)
if err != nil {
return &model.ToolResult{
Output: "",
Error: fmt.Sprintf("获取设备状态失败: %v", err),
}, nil
}
brightness, ok := toFloat64(val)
if !ok {
return &model.ToolResult{
Output: "",
Error: fmt.Sprintf("亮度值无效: %v", val),
}, nil
}
if err := t.iotClient.SetDeviceProperty(deviceID, "brightness", brightness); err != nil {
return &model.ToolResult{
Output: "",
Error: fmt.Sprintf("设置亮度失败: %v", err),
}, nil
}
return &model.ToolResult{
Output: fmt.Sprintf("已将 %s 亮度调整为 %d%%", currentDevice.Name, int(brightness)),
Error: "",
}, nil
}
// handleSetPosition 处理设置窗帘位置
func (t *IoTControlTool) handleSetPosition(deviceID string, arguments map[string]interface{}) (*model.ToolResult, error) {
val := extractValue(arguments)
if val == nil {
return &model.ToolResult{
Output: "",
Error: "缺少 value 参数,请指定位置值(0=关闭, 100=全开)",
}, nil
}
currentDevice, err := t.iotClient.GetDevice(deviceID)
if err != nil {
return &model.ToolResult{
Output: "",
Error: fmt.Sprintf("获取设备状态失败: %v", err),
}, nil
}
position, ok := toFloat64(val)
if !ok {
return &model.ToolResult{
Output: "",
Error: fmt.Sprintf("位置值无效: %v", val),
}, nil
}
if err := t.iotClient.SetDeviceProperty(deviceID, "position", position); err != nil {
return &model.ToolResult{
Output: "",
Error: fmt.Sprintf("设置窗帘位置失败: %v", err),
}, nil
}
return &model.ToolResult{
Output: fmt.Sprintf("已将 %s 窗帘调整为 %d%%", currentDevice.Name, int(position)),
Error: "",
}, nil
}
// handleSetMode 处理设置空调模式
func (t *IoTControlTool) handleSetMode(deviceID string, arguments map[string]interface{}) (*model.ToolResult, error) {
val := extractValue(arguments)
if val == nil {
return &model.ToolResult{
Output: "",
Error: "缺少 value 参数,请指定模式(cool/heat/auto",
}, nil
}
mode, ok := val.(string)
if !ok {
return &model.ToolResult{
Output: "",
Error: fmt.Sprintf("模式值无效: %v", val),
}, nil
}
currentDevice, err := t.iotClient.GetDevice(deviceID)
if err != nil {
return &model.ToolResult{
Output: "",
Error: fmt.Sprintf("获取设备状态失败: %v", err),
}, nil
}
if err := t.iotClient.SetDeviceProperty(deviceID, "mode", mode); err != nil {
return &model.ToolResult{
Output: "",
Error: fmt.Sprintf("设置模式失败: %v", err),
}, nil
}
return &model.ToolResult{
Output: fmt.Sprintf("已将 %s 模式切换为 %s", currentDevice.Name, mode),
Error: "",
}, nil
}
// handleSetColor 处理设置灯光颜色
func (t *IoTControlTool) handleSetColor(deviceID string, arguments map[string]interface{}) (*model.ToolResult, error) {
val := extractValue(arguments)
if val == nil {
return &model.ToolResult{
Output: "",
Error: "缺少 value 参数,请指定颜色(warm_white/cool_white/colorful",
}, nil
}
color, ok := val.(string)
if !ok {
return &model.ToolResult{
Output: "",
Error: fmt.Sprintf("颜色值无效: %v", val),
}, nil
}
currentDevice, err := t.iotClient.GetDevice(deviceID)
if err != nil {
return &model.ToolResult{
Output: "",
Error: fmt.Sprintf("获取设备状态失败: %v", err),
}, nil
}
if err := t.iotClient.SetDeviceProperty(deviceID, "color", color); err != nil {
return &model.ToolResult{
Output: "",
Error: fmt.Sprintf("设置颜色失败: %v", err),
}, nil
}
return &model.ToolResult{
Output: fmt.Sprintf("已将 %s 灯光颜色切换为 %s", currentDevice.Name, color),
Error: "",
}, 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
}
}