feat: 第四轮大版本更新 — 修复4个严重Bug、2个UI Bug,实现自主思考重构与主-子会话架构
## 🐛 Bug 修复 - 修复前端对话无响应:消除 ChatContainer 中的双重 WebSocket 连接,优化 sendMessage 失败提示 - 修复 Memory-Service 数据库迁移失败:ai-core 和 memory-service 均添加 ALTER TABLE ADD COLUMN IF NOT EXISTS 模式演化 - 修复语音/STT 不可用:添加 MediaRecorder API 降级方案,修复 whisper-cli 输出文件名错误 - 修复仪表盘数据库按钮失效:补充按钮 ID 属性,重写 controlDB() 控制逻辑 ## 🎨 UI 修复 - 修正用户消息头像位置:从 flex-row-reverse 改为 justify-end - 移除空聊天列表的 emoji 占位图标 ## ✨ 新功能 - devtools 新增 STT 处理日志面板(环形缓冲区 + WebSocket 广播 + 可视化表格) - 新增 ADMIN_NICKNAME 环境变量,支持自定义管理员昵称 ## 🔧 改进 - 注册流程增加昵称必填字段(前后端同步) ## 🏗️ 架构重构 - 重构自主思考逻辑:从定时器轮询改为事件驱动(对话后触发 + 静默检测),优化提示词使其更自然人性化 - 实现主-子会话架构:新增 4 种子会话类型(general/memory/iot/knowledge),意图分析 → 并行分发 → 结果合成流程 ## 📄 新增文档 - docs/architecture/main-session-sub-session-design.md — 子会话架构设计文档
This commit is contained in:
@@ -0,0 +1,325 @@
|
||||
package subsession
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"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() ([]tools.IoTDevice, error)
|
||||
GetDevice(id string) (*tools.IoTDevice, error)
|
||||
ToggleDevice(id string) error
|
||||
SetDeviceProperty(id string, field string, value interface{}) error
|
||||
GetDevicesForContext() []tools.IoTDevice
|
||||
}
|
||||
|
||||
// IoTProvider IoT 控制子会话提供者
|
||||
// 职责:处理 IoT 设备查询和控制请求
|
||||
type IoTProvider struct {
|
||||
iotClient IoTDeviceProvider
|
||||
}
|
||||
|
||||
// NewIoTProvider 创建 IoT 控制子会话提供者
|
||||
func NewIoTProvider(iotClient IoTDeviceProvider) *IoTProvider {
|
||||
return &IoTProvider{
|
||||
iotClient: iotClient,
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
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 客户端未配置)"
|
||||
}
|
||||
|
||||
// 加载人格配置
|
||||
loader, err := persona.NewLoader("")
|
||||
if err != nil {
|
||||
log.Printf("[iot-provider] 加载人格配置失败: %v", err)
|
||||
}
|
||||
personaConfig, _ := loader.Get("cyrene")
|
||||
trueName := "昔涟"
|
||||
if 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
|
||||
}
|
||||
}
|
||||
|
||||
if p.iotClient == nil {
|
||||
result.Summary = "(IoT 客户端未配置,无法控制设备)"
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// 简单的关键词匹配来执行设备操作(不依赖 LLM 解析)
|
||||
// 这是作为降级方案,当 LLM 不可用时仍然可以处理基本 IoT 命令
|
||||
msgLower := strings.ToLower(userMessage)
|
||||
|
||||
// 尝试获取设备列表进行匹配
|
||||
devices := p.iotClient.GetDevicesForContext()
|
||||
|
||||
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)
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
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))
|
||||
}
|
||||
result.Summary = sb.String()
|
||||
result.Confidence = 0.7
|
||||
return result, nil
|
||||
}
|
||||
}
|
||||
|
||||
result.Summary = "(未匹配到 IoT 操作)"
|
||||
result.Confidence = 0.5
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure json is used
|
||||
var _ = json.Marshal
|
||||
Reference in New Issue
Block a user