|
|
|
@@ -2,7 +2,6 @@ package subsession
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"context"
|
|
|
|
|
"encoding/json"
|
|
|
|
|
"fmt"
|
|
|
|
|
"log"
|
|
|
|
|
"strings"
|
|
|
|
@@ -25,13 +24,15 @@ type IoTDeviceProvider interface {
|
|
|
|
|
// IoTProvider IoT 控制子会话提供者
|
|
|
|
|
// 职责:处理 IoT 设备查询和控制请求
|
|
|
|
|
type IoTProvider struct {
|
|
|
|
|
iotClient IoTDeviceProvider
|
|
|
|
|
iotClient IoTDeviceProvider
|
|
|
|
|
personaDir string
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// NewIoTProvider 创建 IoT 控制子会话提供者
|
|
|
|
|
func NewIoTProvider(iotClient IoTDeviceProvider) *IoTProvider {
|
|
|
|
|
func NewIoTProvider(iotClient IoTDeviceProvider, personaDir string) *IoTProvider {
|
|
|
|
|
return &IoTProvider{
|
|
|
|
|
iotClient: iotClient,
|
|
|
|
|
iotClient: iotClient,
|
|
|
|
|
personaDir: personaDir,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@@ -114,7 +115,11 @@ func (p *IoTProvider) CreateContext(ctx context.Context, params CreateContextPar
|
|
|
|
|
|
|
|
|
|
// 加载人格配置
|
|
|
|
|
trueName := "昔涟"
|
|
|
|
|
loader, err := persona.NewLoader("")
|
|
|
|
|
personaPath := p.personaDir
|
|
|
|
|
if personaPath == "" {
|
|
|
|
|
personaPath = "./internal/persona"
|
|
|
|
|
}
|
|
|
|
|
loader, err := persona.NewLoader(personaPath)
|
|
|
|
|
if err != nil {
|
|
|
|
|
log.Printf("[iot-provider] 加载人格配置失败: %v", err)
|
|
|
|
|
}
|
|
|
|
@@ -190,7 +195,6 @@ func (p *IoTProvider) Execute(ctx context.Context, subCtx []model.LLMMessage) (*
|
|
|
|
|
Summary: "(未执行 IoT 操作)",
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 提取用户消息
|
|
|
|
|
userMessage := ""
|
|
|
|
|
for i := len(subCtx) - 1; i >= 0; i-- {
|
|
|
|
|
if subCtx[i].Role == model.RoleUser {
|
|
|
|
@@ -207,98 +211,129 @@ func (p *IoTProvider) Execute(ctx context.Context, subCtx []model.LLMMessage) (*
|
|
|
|
|
return result, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 简单的关键词匹配来执行设备操作(不依赖 LLM 解析)
|
|
|
|
|
// 这是作为降级方案,当 LLM 不可用时仍然可以处理基本 IoT 命令
|
|
|
|
|
msgLower := strings.ToLower(userMessage)
|
|
|
|
|
|
|
|
|
|
// 尝试获取设备列表进行匹配
|
|
|
|
|
devices := p.iotClient.GetDevicesForContext(ctx)
|
|
|
|
|
log.Printf("[iot-provider] 📋 获取到 %d 个设备用于匹配", len(devices))
|
|
|
|
|
|
|
|
|
|
msgLower := strings.ToLower(userMessage)
|
|
|
|
|
userName := extractUserName(subCtx)
|
|
|
|
|
|
|
|
|
|
// 收集所有匹配的设备-操作对,支持多设备命令
|
|
|
|
|
type deviceAction struct {
|
|
|
|
|
dev tools.IoTDevice
|
|
|
|
|
operation string // "on" | "off" | "query"
|
|
|
|
|
}
|
|
|
|
|
var actions []deviceAction
|
|
|
|
|
|
|
|
|
|
for _, dev := range devices {
|
|
|
|
|
devNameLower := strings.ToLower(dev.Name)
|
|
|
|
|
|
|
|
|
|
if !strings.Contains(msgLower, devNameLower) {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 匹配到了设备名称
|
|
|
|
|
if strings.Contains(msgLower, "打开") || strings.Contains(msgLower, "开") {
|
|
|
|
|
if dev.Status != "on" && dev.Status != "open" {
|
|
|
|
|
if dev.Type == "curtain" {
|
|
|
|
|
// 窗帘使用 set 而非 toggle
|
|
|
|
|
_ = p.iotClient.SetDeviceProperty(dev.ID, "status", "open")
|
|
|
|
|
} else {
|
|
|
|
|
_ = p.iotClient.ToggleDevice(dev.ID)
|
|
|
|
|
// 判断此设备的操作:检查设备名附近的意图词
|
|
|
|
|
devIdx := strings.Index(msgLower, devNameLower)
|
|
|
|
|
contextStart := devIdx - 15
|
|
|
|
|
if contextStart < 0 {
|
|
|
|
|
contextStart = 0
|
|
|
|
|
}
|
|
|
|
|
contextEnd := devIdx + len(devNameLower) + 15
|
|
|
|
|
if contextEnd > len(msgLower) {
|
|
|
|
|
contextEnd = len(msgLower)
|
|
|
|
|
}
|
|
|
|
|
nearbyContext := msgLower[contextStart:contextEnd]
|
|
|
|
|
|
|
|
|
|
if strings.Contains(nearbyContext, "打开") || strings.Contains(nearbyContext, "开") {
|
|
|
|
|
actions = append(actions, deviceAction{dev: dev, operation: "on"})
|
|
|
|
|
} else if strings.Contains(nearbyContext, "关闭") || strings.Contains(nearbyContext, "关掉") || strings.Contains(nearbyContext, "关上") {
|
|
|
|
|
actions = append(actions, deviceAction{dev: dev, operation: "off"})
|
|
|
|
|
} else {
|
|
|
|
|
actions = append(actions, deviceAction{dev: dev, operation: "query"})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 如果没有匹配到具体设备,可能是查询所有设备状态
|
|
|
|
|
if len(actions) == 0 {
|
|
|
|
|
if strings.Contains(msgLower, "设备") && (strings.Contains(msgLower, "状态") || strings.Contains(msgLower, "怎么样") || strings.Contains(msgLower, "看看")) {
|
|
|
|
|
if len(devices) > 0 {
|
|
|
|
|
var sb strings.Builder
|
|
|
|
|
sb.WriteString("家里设备状态:\n")
|
|
|
|
|
for _, d := range devices {
|
|
|
|
|
sb.WriteString(fmt.Sprintf("- %s: %s\n", d.Name, d.Status))
|
|
|
|
|
}
|
|
|
|
|
result.Summary = fmt.Sprintf("已帮%s打开%s♪", extractUserName(subCtx), dev.Name)
|
|
|
|
|
result.Confidence = 0.9
|
|
|
|
|
result.ToolCalls = []model.ToolCallRecord{{
|
|
|
|
|
Name: "iot_control",
|
|
|
|
|
Arguments: map[string]any{"device_id": dev.ID, "operation": "toggle"},
|
|
|
|
|
Result: "success",
|
|
|
|
|
}}
|
|
|
|
|
log.Printf("[iot-subsession] 执行操作: 打开 %s (%s)", dev.Name, dev.ID)
|
|
|
|
|
return result, nil
|
|
|
|
|
} else {
|
|
|
|
|
result.Summary = fmt.Sprintf("%s已经是打开状态啦~", dev.Name)
|
|
|
|
|
result.Confidence = 0.9
|
|
|
|
|
result.Summary = sb.String()
|
|
|
|
|
result.Confidence = 0.7
|
|
|
|
|
return result, nil
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if strings.Contains(msgLower, "关闭") || strings.Contains(msgLower, "关") {
|
|
|
|
|
if dev.Status == "on" || dev.Status == "open" {
|
|
|
|
|
if dev.Type == "curtain" {
|
|
|
|
|
_ = p.iotClient.SetDeviceProperty(dev.ID, "status", "closed")
|
|
|
|
|
} else {
|
|
|
|
|
_ = p.iotClient.ToggleDevice(dev.ID)
|
|
|
|
|
}
|
|
|
|
|
result.Summary = fmt.Sprintf("已帮%s关闭%s~", extractUserName(subCtx), dev.Name)
|
|
|
|
|
result.Confidence = 0.9
|
|
|
|
|
result.ToolCalls = []model.ToolCallRecord{{
|
|
|
|
|
Name: "iot_control",
|
|
|
|
|
Arguments: map[string]any{"device_id": dev.ID, "operation": "toggle"},
|
|
|
|
|
Result: "success",
|
|
|
|
|
}}
|
|
|
|
|
log.Printf("[iot-subsession] 执行操作: 关闭 %s (%s)", dev.Name, dev.ID)
|
|
|
|
|
return result, nil
|
|
|
|
|
} else {
|
|
|
|
|
result.Summary = fmt.Sprintf("%s已经是关闭状态啦~", dev.Name)
|
|
|
|
|
result.Confidence = 0.9
|
|
|
|
|
return result, nil
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 查询设备状态
|
|
|
|
|
deviceStatus := fmt.Sprintf("%s当前状态: %s", dev.Name, dev.Status)
|
|
|
|
|
if dev.Type == "light" && dev.Status == "on" {
|
|
|
|
|
deviceStatus += fmt.Sprintf(" (亮度%d%%, 颜色%s)", dev.Brightness, dev.Color)
|
|
|
|
|
} else if dev.Type == "ac" && dev.Status == "on" {
|
|
|
|
|
deviceStatus += fmt.Sprintf(" (模式%s, 温度%.0f°C)", dev.Mode, dev.Temperature)
|
|
|
|
|
}
|
|
|
|
|
result.Summary = deviceStatus
|
|
|
|
|
result.Confidence = 0.8
|
|
|
|
|
log.Printf("[iot-provider] ❌ 未匹配到 IoT 操作: userMessage=%s", truncateStr(userMessage, 80))
|
|
|
|
|
result.Summary = "(未匹配到 IoT 操作)"
|
|
|
|
|
result.Confidence = 0.5
|
|
|
|
|
return result, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 没有匹配到设备,可能只是查询所有设备状态
|
|
|
|
|
if strings.Contains(msgLower, "设备") && (strings.Contains(msgLower, "状态") || strings.Contains(msgLower, "怎么样") || strings.Contains(msgLower, "看看")) {
|
|
|
|
|
if len(devices) > 0 {
|
|
|
|
|
var sb strings.Builder
|
|
|
|
|
sb.WriteString("家里设备状态:\n")
|
|
|
|
|
for _, d := range devices {
|
|
|
|
|
sb.WriteString(fmt.Sprintf("- %s: %s\n", d.Name, d.Status))
|
|
|
|
|
// 执行所有匹配到的操作
|
|
|
|
|
var summaries []string
|
|
|
|
|
var allToolCalls []model.ToolCallRecord
|
|
|
|
|
executedCount := 0
|
|
|
|
|
|
|
|
|
|
for _, action := range actions {
|
|
|
|
|
switch action.operation {
|
|
|
|
|
case "on":
|
|
|
|
|
if action.dev.Status != "on" && action.dev.Status != "open" {
|
|
|
|
|
if action.dev.Type == "curtain" {
|
|
|
|
|
_ = p.iotClient.SetDeviceProperty(action.dev.ID, "status", "open")
|
|
|
|
|
} else {
|
|
|
|
|
_ = p.iotClient.ToggleDevice(action.dev.ID)
|
|
|
|
|
}
|
|
|
|
|
summaries = append(summaries, fmt.Sprintf("已帮%s打开%s♪", userName, action.dev.Name))
|
|
|
|
|
allToolCalls = append(allToolCalls, model.ToolCallRecord{
|
|
|
|
|
Name: "iot_control",
|
|
|
|
|
Arguments: map[string]any{"device_id": action.dev.ID, "operation": "toggle"},
|
|
|
|
|
Result: "success",
|
|
|
|
|
})
|
|
|
|
|
log.Printf("[iot-subsession] 执行操作: 打开 %s (%s)", action.dev.Name, action.dev.ID)
|
|
|
|
|
executedCount++
|
|
|
|
|
} else {
|
|
|
|
|
summaries = append(summaries, fmt.Sprintf("%s已经是打开状态啦~", action.dev.Name))
|
|
|
|
|
}
|
|
|
|
|
result.Summary = sb.String()
|
|
|
|
|
result.Confidence = 0.7
|
|
|
|
|
return result, nil
|
|
|
|
|
case "off":
|
|
|
|
|
if action.dev.Status == "on" || action.dev.Status == "open" {
|
|
|
|
|
if action.dev.Type == "curtain" {
|
|
|
|
|
_ = p.iotClient.SetDeviceProperty(action.dev.ID, "status", "closed")
|
|
|
|
|
} else {
|
|
|
|
|
_ = p.iotClient.ToggleDevice(action.dev.ID)
|
|
|
|
|
}
|
|
|
|
|
summaries = append(summaries, fmt.Sprintf("已帮%s关闭%s~", userName, action.dev.Name))
|
|
|
|
|
allToolCalls = append(allToolCalls, model.ToolCallRecord{
|
|
|
|
|
Name: "iot_control",
|
|
|
|
|
Arguments: map[string]any{"device_id": action.dev.ID, "operation": "toggle"},
|
|
|
|
|
Result: "success",
|
|
|
|
|
})
|
|
|
|
|
log.Printf("[iot-subsession] 执行操作: 关闭 %s (%s)", action.dev.Name, action.dev.ID)
|
|
|
|
|
executedCount++
|
|
|
|
|
} else {
|
|
|
|
|
summaries = append(summaries, fmt.Sprintf("%s已经是关闭状态啦~", action.dev.Name))
|
|
|
|
|
}
|
|
|
|
|
case "query":
|
|
|
|
|
deviceStatus := fmt.Sprintf("%s当前状态: %s", action.dev.Name, action.dev.Status)
|
|
|
|
|
if action.dev.Type == "light" && action.dev.Status == "on" {
|
|
|
|
|
deviceStatus += fmt.Sprintf(" (亮度%d%%, 颜色%s)", action.dev.Brightness, action.dev.Color)
|
|
|
|
|
} else if action.dev.Type == "ac" && action.dev.Status == "on" {
|
|
|
|
|
deviceStatus += fmt.Sprintf(" (模式%s, 温度%.0f°C)", action.dev.Mode, action.dev.Temperature)
|
|
|
|
|
}
|
|
|
|
|
summaries = append(summaries, deviceStatus)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
log.Printf("[iot-provider] ❌ 未匹配到 IoT 操作: userMessage=%s", truncateStr(userMessage, 80))
|
|
|
|
|
result.Summary = "(未匹配到 IoT 操作)"
|
|
|
|
|
result.Confidence = 0.5
|
|
|
|
|
result.Summary = strings.Join(summaries, "; ")
|
|
|
|
|
result.Confidence = 0.9
|
|
|
|
|
if len(allToolCalls) > 0 {
|
|
|
|
|
result.ToolCalls = allToolCalls
|
|
|
|
|
}
|
|
|
|
|
if executedCount == 0 {
|
|
|
|
|
result.Confidence = 0.8
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return result, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@@ -336,5 +371,3 @@ func truncateStr(s string, maxLen int) string {
|
|
|
|
|
return string(runes[:maxLen]) + "..."
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Ensure json is used
|
|
|
|
|
var _ = json.Marshal
|
|
|
|
|