fix: IoT多设备支持 + Review Pipeline审查消息 + 意图分析快速通道优化
- IoT Provider: 重写Execute()支持多设备命令批量执行,修复persona路径 - Intent Analyzer: 新增isStrongIoTCommand快速通道,跳过LLM分析节省2-3s - Orchestrator: parseReviewMessages()内联审查 + 快速通道扩展(chat/greeting跳过子会话) - Gateway: SSE review_messages解析→WebSocket结构化消息转发(action/chat) - Persona: 对话风格注入action格式指令(括号包裹动作描述) - Frontend: sessionStore历史消息msgType映射 - 新增E2E测试脚本 + 调试标准文档 + 第4轮修复报告 E2E验证: IoT设备操控✅ Review消息拆分✅ 快速通道✅ 响应时间~3.4s Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user