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:
2026-05-18 20:05:14 +08:00
parent b6ec36886c
commit 78e3f450c2
54 changed files with 7846 additions and 106 deletions
@@ -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-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
}
}