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
This commit is contained in:
@@ -0,0 +1,439 @@
|
||||
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-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"},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user