package subsession import ( "context" "fmt" "log" "strings" "time" "github.com/yourname/cyrene-ai/ai-core/internal/model" "github.com/yourname/cyrene-ai/ai-core/internal/persona" "github.com/yourname/cyrene-ai/ai-core/internal/tools" ) // IoTDeviceProvider IoT 设备查询接口 type IoTDeviceProvider interface { GetAllDevices(ctx context.Context) ([]tools.IoTDevice, error) GetDevice(ctx context.Context, id string) (*tools.IoTDevice, error) ToggleDevice(id string) error SetDeviceProperty(id string, field string, value interface{}) error GetDevicesForContext(ctx context.Context) []tools.IoTDevice } // IoTProvider IoT 控制子会话提供者 // 职责:处理 IoT 设备查询和控制请求 type IoTProvider struct { iotClient IoTDeviceProvider personaDir string } // NewIoTProvider 创建 IoT 控制子会话提供者 func NewIoTProvider(iotClient IoTDeviceProvider, personaDir string) *IoTProvider { return &IoTProvider{ iotClient: iotClient, personaDir: personaDir, } } func (p *IoTProvider) Type() model.SubSessionType { return model.SubSessionIoT } func (p *IoTProvider) CanHandle(_ context.Context, intent *model.IntentResult, userMessage string) bool { // 意图分析明确需要 IoT if intent != nil && intent.NeedsIoT { return true } // 关键词触发(作为意图分析的补充) iotKeywords := []string{ "灯", "空调", "窗帘", "电视", "设备", "开关", "打开", "关闭", "调到", "设置", "温度", "亮度", "传感器", "门锁", "插座", "风扇", "加湿器", } msgLower := strings.ToLower(userMessage) for _, kw := range iotKeywords { if strings.Contains(msgLower, kw) { return true } } return false } func (p *IoTProvider) Priority() int { return 3 // 低于 General 和 Memory } func (p *IoTProvider) Timeout() time.Duration { return 15 * time.Second } func (p *IoTProvider) CreateContext(ctx context.Context, params CreateContextParams) ([]model.LLMMessage, error) { messages := []model.LLMMessage{} // 获取当前设备状态 var deviceStatusText string if p.iotClient != nil { devices := p.iotClient.GetDevicesForContext(ctx) if len(devices) > 0 { deviceStatusText = "当前设备状态:\n" for _, d := range devices { switch d.Type { case "light": if d.Status == "on" { deviceStatusText += fmt.Sprintf("- %s: 开启 (亮度%d%%, 颜色%s)\n", d.Name, d.Brightness, d.Color) } else { deviceStatusText += fmt.Sprintf("- %s: 关闭\n", d.Name) } case "ac": if d.Status == "on" { modeLabel := acModeLabel(d.Mode) deviceStatusText += fmt.Sprintf("- %s: 运行中 (%s %.0f°C)\n", d.Name, modeLabel, d.Temperature) } else { deviceStatusText += fmt.Sprintf("- %s: 关闭\n", d.Name) } case "curtain": if d.Status == "open" { deviceStatusText += fmt.Sprintf("- %s: 已打开\n", d.Name) } else { deviceStatusText += fmt.Sprintf("- %s: 已关闭\n", d.Name) } case "sensor": deviceStatusText += fmt.Sprintf("- %s: %.1f%s\n", d.Name, d.Value, d.Unit) default: deviceStatusText += fmt.Sprintf("- %s: %s\n", d.Name, d.Status) } } } else { deviceStatusText = "(暂无设备状态信息)" } } else { deviceStatusText = "(IoT 客户端未配置)" } // 加载人格配置 trueName := "昔涟" personaPath := p.personaDir if personaPath == "" { personaPath = "./internal/persona" } loader, err := persona.NewLoader(personaPath) if err != nil { log.Printf("[iot-provider] 加载人格配置失败: %v", err) } if loader != nil { if personaConfig, err := loader.Get("cyrene"); err == nil && personaConfig != nil { trueName = personaConfig.Identity.TrueName } } userName := params.Nickname if userName == "" { userName = params.UserID } systemPrompt := fmt.Sprintf(`你是%s,正在帮%s控制家里的智能设备。 ## 你的能力 你可以通过以下方式帮%s控制设备: - 查询设备当前状态 - 开关设备(灯、空调、窗帘等) - 调节设备参数(亮度、温度、模式等) ## 回复风格 - 用俏皮可爱的语气告诉%s操作结果 - 简短自然,像小女友一样 ## 当前设备状态 %s ## 用户请求 %s说:%s ## 你的任务 分析%s的请求,判断需要: 1. 只是查询设备状态?→ 直接基于上面的设备状态回答 2. 需要控制设备?→ 说明需要执行什么操作(开关/调节),并生成一个可爱的操作确认消息 3. 不需要IoT操作?→ 回复"无需IoT操作" 请用JSON格式输出: { "action": "query" | "control" | "none", "device_id": "设备ID (如果需要操作)", "device_name": "设备名称", "operation": "toggle" | "set" | "query", "field": "属性名 (如 brightness, temperature)", "value": "属性值", "summary": "给用户的简短操作结果" }`, trueName, userName, userName, userName, deviceStatusText, userName, params.UserMessage, userName, ) messages = append(messages, model.LLMMessage{ Role: model.RoleSystem, Content: systemPrompt, }) messages = append(messages, model.LLMMessage{ Role: model.RoleUser, Content: params.UserMessage, }) return messages, nil } func (p *IoTProvider) Execute(ctx context.Context, subCtx []model.LLMMessage) (*model.SubSessionResult, error) { result := &model.SubSessionResult{ Type: model.SubSessionIoT, Summary: "(未执行 IoT 操作)", } userMessage := "" for i := len(subCtx) - 1; i >= 0; i-- { if subCtx[i].Role == model.RoleUser { userMessage = subCtx[i].Content break } } log.Printf("[iot-provider] 📥 开始处理 IoT 子会话: userMessage=%s", truncateStr(userMessage, 80)) if p.iotClient == nil { log.Printf("[iot-provider] ⚠️ IoT 客户端未配置,无法控制设备") result.Summary = "(IoT 客户端未配置,无法控制设备)" return result, nil } 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 } // 判断此设备的操作:先检查附近上下文,再回退到全文匹配 devIdx := strings.Index(msgLower, devNameLower) contextStart := devIdx - 30 if contextStart < 0 { contextStart = 0 } contextEnd := devIdx + len(devNameLower) + 30 if contextEnd > len(msgLower) { contextEnd = len(msgLower) } nearbyContext := msgLower[contextStart:contextEnd] hasOpen := strings.Contains(nearbyContext, "打开") || strings.Contains(nearbyContext, "开") hasClose := strings.Contains(nearbyContext, "关闭") || strings.Contains(nearbyContext, "关掉") || strings.Contains(nearbyContext, "关上") || strings.Contains(nearbyContext, "关") // 附近上下文不足以判断时,回退到全文搜索 if !hasOpen && !hasClose { hasOpen = strings.Contains(msgLower, "打开") hasClose = strings.Contains(msgLower, "关闭") || strings.Contains(msgLower, "关掉") || strings.Contains(msgLower, "关上") } if hasOpen { actions = append(actions, deviceAction{dev: dev, operation: "on"}) } else if hasClose { 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 = sb.String() result.Confidence = 0.7 return result, nil } } log.Printf("[iot-provider] ❌ 未匹配到 IoT 操作: userMessage=%s", truncateStr(userMessage, 80)) result.Summary = "(未匹配到 IoT 操作)" result.Confidence = 0.5 return result, nil } // 执行所有匹配到的操作 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)) } 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) } } 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 } // extractUserName 从上下文中提取用户名 func extractUserName(subCtx []model.LLMMessage) string { for _, msg := range subCtx { if msg.Role == model.RoleSystem { // 尝试从系统提示词中提取称呼 // 简单返回"你" break } } return "你" } func acModeLabel(mode string) string { switch mode { case "cool": return "制冷" case "heat": return "制热" case "auto": return "自动" default: return mode } } // truncateStr 截断字符串用于日志 func truncateStr(s string, maxLen int) string { runes := []rune(s) if len(runes) <= maxLen { return s } return string(runes[:maxLen]) + "..." }