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:
2026-05-22 22:51:27 +08:00
parent 773f19f009
commit a67b95cbc4
18 changed files with 1186 additions and 85 deletions
@@ -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