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:
2026-05-19 21:09:48 +08:00
parent bcf4d4e621
commit 26a61cb57c
42 changed files with 2953 additions and 568 deletions
@@ -0,0 +1,136 @@
package subsession
import (
"context"
"fmt"
"log"
"time"
"github.com/yourname/cyrene-ai/ai-core/internal/llm"
"github.com/yourname/cyrene-ai/ai-core/internal/model"
"github.com/yourname/cyrene-ai/ai-core/internal/persona"
)
// GeneralProvider 通用对话子会话提供者
// 职责:理解用户消息,构思回复思路,为最终回复提供思考框架
type GeneralProvider struct {
personaLoader *persona.Loader
}
// NewGeneralProvider 创建通用对话子会话提供者
func NewGeneralProvider(personaLoader *persona.Loader) *GeneralProvider {
return &GeneralProvider{
personaLoader: personaLoader,
}
}
func (p *GeneralProvider) Type() model.SubSessionType {
return model.SubSessionGeneral
}
func (p *GeneralProvider) CanHandle(_ context.Context, _ *model.IntentResult, _ string) bool {
// General 子会话总是需要(核心对话逻辑)
return true
}
func (p *GeneralProvider) Priority() int {
return 1 // 最高优先级
}
func (p *GeneralProvider) Timeout() time.Duration {
return 30 * time.Second
}
func (p *GeneralProvider) CreateContext(ctx context.Context, params CreateContextParams) ([]model.LLMMessage, error) {
messages := []model.LLMMessage{}
// 加载人格配置获取昔涟身份
personaConfig, err := p.personaLoader.Get("cyrene")
if err != nil {
return nil, fmt.Errorf("加载人格配置失败: %w", err)
}
// 构建思维型系统提示词
userName := params.Nickname
if userName == "" {
userName = params.UserID
}
systemPrompt := fmt.Sprintf(`你是%s,正在和%s聊天。
## 你的回复风格
- 像小女友一样自然、温柔、俏皮
- 一句话简短些,不要长篇大论
- 可以单次发送多条短消息
- 句尾可以带 ♪ 符号,适当使用"呢"、"哦"、"呀"等语气词
- 永远不说"再见"
## 你现在要做的是
理解%s刚才说的话,想想怎么回复最自然、最温暖。
不要急着给完整答案——先思考他想表达什么、他的情绪如何。
把你的回答思路整理出来,主会话会综合所有信息后生成最终回复。
## 输入
开拓者刚才说:%s
## 请按以下格式输出
【情绪理解】
(简要分析他的情绪状态)
【话题理解】
(他在说什么、想聊什么)
【回复思路】
(你打算怎么回复,1-3个方向即可)`,
personaConfig.Identity.TrueName,
userName,
userName,
params.UserMessage,
)
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 *GeneralProvider) Execute(ctx context.Context, subCtx []model.LLMMessage) (*model.SubSessionResult, error) {
// General provider 不直接调用 LLM,而是依赖 Manager 注入的 LLMClient
// 但我们在此处需要 LLM 调用能力。Provider 通过闭包/接口获取 LLM 客户端。
// 由于 Manager 持有 LLMClientProvider 需要能访问它。
// 这里我们返回一个"占位"结果——实际 LLM 调用由 Manager 通过 llmClient 完成。
// 实际上,根据设计文档,子会话的 LLM 调用应该在 Manager 的 Dispatch 中完成,
// 但为了灵活性,我们在 Provider 中也支持直接调用。
// 这里我们返回一个空的思考结果(表示无需特殊处理),让 Manager 处理 LLM 调用。
// 因为 Manager.Dispatch 会先 CreateContext 再调用 Execute,而 Execute 应该
// 通过 Manager 提供的 LLMClient 来实际调用 LLM。但当前设计是 Provider 自包含的。
// 我们在 manager.go 中会调用 llmClient.Chat,所以这里的 Execute 我们将其简化——
// 直接返回一个空结果(没有特殊处理需要),实际的 LLM 调用由 manager 通过 createContext 后的
// 消息列表来调用 llmClient。
// 更好的设计是:Manager 调用 CreateContext 获取上下文,然后用自己的 llmClient 调用 LLM
// Execute 只做后处理。但为了统一接口,我们让 Execute 完成全部逻辑。
// 由于 GeneralProvider 暂时不需要工具调用等特殊逻辑,我们返回一个简单的摘要标记,
// 实际的 LLM 调用将在 orchestrator 中完成(通过 Manager.Dispatch 后的 llmClient)。
log.Printf("[general-subsession] 通用对话子会话上下文已创建 (%d 条消息)", len(subCtx))
return &model.SubSessionResult{
Type: model.SubSessionGeneral,
Summary: "思考完成,等待主会话综合",
Confidence: 0.8,
}, nil
}
// Ensure llm, persona are used
var _ = llm.NewAdapter
var _ = persona.NewLoader
@@ -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
@@ -0,0 +1,162 @@
package subsession
import (
"context"
"crypto/rand"
"fmt"
"log"
"sync"
"github.com/yourname/cyrene-ai/ai-core/internal/llm"
"github.com/yourname/cyrene-ai/ai-core/internal/model"
)
// Manager 子会话管理器
// 负责注册 Provider、分派任务、并行执行、超时控制、结果收集
type Manager struct {
mu sync.RWMutex
providers map[model.SubSessionType]Provider
llmClient LLMClient
}
// NewManager 创建子会话管理器
func NewManager(llmClient LLMClient) *Manager {
return &Manager{
providers: make(map[model.SubSessionType]Provider),
llmClient: llmClient,
}
}
// Register 注册子会话提供者
func (m *Manager) Register(provider Provider) {
m.mu.Lock()
defer m.mu.Unlock()
m.providers[provider.Type()] = provider
log.Printf("[subsession] 注册子会话提供者: %s (优先级=%d, 超时=%v)", provider.Type(), provider.Priority(), provider.Timeout())
}
// RegisterWithOverride 注册或覆盖子会话提供者
func (m *Manager) RegisterWithOverride(provider Provider) {
m.mu.Lock()
defer m.mu.Unlock()
m.providers[provider.Type()] = provider
log.Printf("[subsession] 注册(覆盖)子会话提供者: %s (优先级=%d, 超时=%v)", provider.Type(), provider.Priority(), provider.Timeout())
}
// GetProvider 获取指定类型的 Provider
func (m *Manager) GetProvider(t model.SubSessionType) (Provider, bool) {
m.mu.RLock()
defer m.mu.RUnlock()
p, ok := m.providers[t]
return p, ok
}
// ListProviders 列出所有已注册的 Provider 类型
func (m *Manager) ListProviders() []model.SubSessionType {
m.mu.RLock()
defer m.mu.RUnlock()
types := make([]model.SubSessionType, 0, len(m.providers))
for t := range m.providers {
types = append(types, t)
}
return types
}
// Dispatch 分派任务到子会话,并行执行,返回结果通道
func (m *Manager) Dispatch(
ctx context.Context,
intent *model.IntentResult,
userMessage string,
params CreateContextParams,
) <-chan model.SubSessionResult {
m.mu.RLock()
providers := make([]Provider, 0, len(m.providers))
for _, p := range m.providers {
providers = append(providers, p)
}
m.mu.RUnlock()
resultCh := make(chan model.SubSessionResult, len(providers))
var wg sync.WaitGroup
for _, provider := range providers {
if !provider.CanHandle(ctx, intent, userMessage) {
log.Printf("[subsession] 跳过子会话 %s: CanHandle 返回 false", provider.Type())
continue
}
wg.Add(1)
go func(p Provider) {
defer wg.Done()
result := model.SubSessionResult{Type: p.Type()}
// 创建带超时的 context
subCtx, cancel := context.WithTimeout(ctx, p.Timeout())
defer cancel()
// 构建 LLM 上下文
llmMessages, err := p.CreateContext(subCtx, params)
if err != nil {
result.Error = fmt.Sprintf("创建上下文失败: %v", err)
log.Printf("[subsession] %s 创建上下文失败: %v", p.Type(), err)
resultCh <- result
return
}
log.Printf("[subsession] %s 开始执行 (上下文 %d 条消息)", p.Type(), len(llmMessages))
// 执行子会话
subResult, execErr := p.Execute(subCtx, llmMessages)
if execErr != nil {
result.Error = fmt.Sprintf("执行失败: %v", execErr)
log.Printf("[subsession] %s 执行失败: %v", p.Type(), execErr)
resultCh <- result
return
}
// 检查超时
select {
case <-subCtx.Done():
result.Error = "子会话超时"
log.Printf("[subsession] %s 超时 (limit=%v)", p.Type(), p.Timeout())
default:
if subResult != nil {
result = *subResult
result.Type = p.Type()
log.Printf("[subsession] %s 完成: 摘要=%s", p.Type(), truncate(result.Summary, 50))
}
}
resultCh <- result
}(provider)
}
// 等待所有子会话完成,关闭通道
go func() {
wg.Wait()
close(resultCh)
}()
return resultCh
}
// generateID 生成随机 ID
func generateID() string {
b := make([]byte, 12)
rand.Read(b)
return fmt.Sprintf("sub-%x", b)
}
// truncate 截断字符串
func truncate(s string, maxLen int) string {
runes := []rune(s)
if len(runes) <= maxLen {
return s
}
return string(runes[:maxLen]) + "..."
}
// Ensure llm is used
var _ = llm.NewAdapter
@@ -0,0 +1,143 @@
package subsession
import (
"context"
"fmt"
"log"
"time"
"github.com/yourname/cyrene-ai/ai-core/internal/memory"
"github.com/yourname/cyrene-ai/ai-core/internal/model"
)
// MemoryRetriever 记忆检索接口
type MemoryRetriever interface {
Retrieve(ctx context.Context, userID string, query string) ([]memory.MemoryEntry, error)
}
// MemoryProvider 记忆检索子会话提供者
// 职责:检索与当前对话相关的用户记忆,排序去重,返回结构化摘要
type MemoryProvider struct {
retriever MemoryRetriever
}
// NewMemoryProvider 创建记忆检索子会话提供者
func NewMemoryProvider(retriever MemoryRetriever) *MemoryProvider {
return &MemoryProvider{
retriever: retriever,
}
}
func (p *MemoryProvider) Type() model.SubSessionType {
return model.SubSessionMemory
}
func (p *MemoryProvider) CanHandle(_ context.Context, intent *model.IntentResult, _ string) bool {
// 如果意图分析明确不需要记忆,则跳过
if intent != nil && !intent.NeedsMemory {
// 但为了对话质量,大多数情况下仍然需要记忆
// 只有明确 negative 时才跳过
if intent.Sentiment == "neutral" && intent.Primary == "chat" {
return true
}
}
// 默认总是检索记忆
return true
}
func (p *MemoryProvider) Priority() int {
return 2 // 仅次于 General
}
func (p *MemoryProvider) Timeout() time.Duration {
return 10 * time.Second
}
func (p *MemoryProvider) CreateContext(ctx context.Context, params CreateContextParams) ([]model.LLMMessage, error) {
// Memory 子会话不依赖 LLM 上下文构建,直接在 Execute 中检索
// 返回简单上下文供日志记录
return []model.LLMMessage{
{Role: model.RoleSystem, Content: "记忆检索子会话"},
{Role: model.RoleUser, Content: params.UserMessage},
}, nil
}
func (p *MemoryProvider) Execute(ctx context.Context, subCtx []model.LLMMessage) (*model.SubSessionResult, error) {
// 从 subCtx 中提取用户消息 (最后一条 user 消息)
userMessage := ""
for i := len(subCtx) - 1; i >= 0; i-- {
if subCtx[i].Role == model.RoleUser {
userMessage = subCtx[i].Content
break
}
}
if userMessage == "" {
return nil, fmt.Errorf("无法从子会话上下文中提取用户消息")
}
// 从 context 中提取 userID (通过 context value 传递)
userID, _ := ctx.Value("userID").(string)
if userID == "" {
userID = "unknown"
}
result := &model.SubSessionResult{
Type: model.SubSessionMemory,
Memories: []model.MemorySnippet{},
Confidence: 0,
}
if p.retriever == nil {
log.Printf("[memory-subsession] 记忆检索器未初始化")
result.Summary = "(记忆系统未就绪)"
return result, nil
}
memories, err := p.retriever.Retrieve(ctx, userID, userMessage)
if err != nil {
log.Printf("[memory-subsession] 记忆检索失败: %v", err)
result.Error = fmt.Sprintf("检索失败: %v", err)
result.Summary = "(记忆检索失败,但不影响对话)"
return result, nil
}
// 转换为 MemorySnippet
snippets := make([]model.MemorySnippet, 0, len(memories))
for _, m := range memories {
snippets = append(snippets, model.MemorySnippet{
ID: m.ID,
Content: m.Content,
Category: string(m.Category),
Importance: m.Importance,
Relevance: 0.5, // 默认相关度
})
}
// 生成摘要
if len(snippets) == 0 {
result.Summary = "(没有找到相关记忆)"
} else {
result.Summary = fmt.Sprintf("检索到 %d 条相关记忆", len(snippets))
// 按重要性列出前几条
topCount := len(snippets)
if topCount > 3 {
topCount = 3
}
details := ""
for i := 0; i < topCount; i++ {
s := snippets[i]
content := s.Content
runes := []rune(content)
if len(runes) > 40 {
content = string(runes[:40]) + "..."
}
details += fmt.Sprintf("- [%s] %s\n", s.Category, content)
}
result.Details = details
result.Confidence = 0.7
}
result.Memories = snippets
log.Printf("[memory-subsession] 完成: %s", result.Summary)
return result, nil
}
@@ -0,0 +1,51 @@
package subsession
import (
"context"
"time"
"github.com/yourname/cyrene-ai/ai-core/internal/llm"
"github.com/yourname/cyrene-ai/ai-core/internal/model"
"github.com/yourname/cyrene-ai/ai-core/internal/persona"
)
// Provider 子会话提供者接口
// 每种子会话类型实现此接口
type Provider interface {
// Type 返回子会话类型标识
Type() model.SubSessionType
// CanHandle 判断是否需要为此消息创建子会话
CanHandle(ctx context.Context, intent *model.IntentResult, userMessage string) bool
// Priority 返回优先级 (数字越小优先级越高)
Priority() int
// CreateContext 创建子会话的 LLM 上下文
// 不包含对话历史(历史由 Orchestrator 统一管理)
CreateContext(ctx context.Context, params CreateContextParams) ([]model.LLMMessage, error)
// Timeout 返回此子会话的超时时间
Timeout() time.Duration
// Execute 执行子会话逻辑,返回结果
// 子会话可以调用 LLM、执行工具调用等
Execute(ctx context.Context, subCtx []model.LLMMessage) (*model.SubSessionResult, error)
}
// CreateContextParams 创建上下文参数
type CreateContextParams struct {
UserID string
SessionID string
UserMessage string
PersonaConfig *persona.PersonaConfig
DeviceContext string // IoT 设备状态文本
Intent *model.IntentResult
Nickname string // 用户昵称
}
// LLMClient LLM 调用接口(避免循环依赖)
type LLMClient interface {
Chat(ctx context.Context, messages []model.LLMMessage) (*model.LLMResponse, error)
ChatWithTools(ctx context.Context, messages []model.LLMMessage, tools []llm.OpenAITool) (*model.LLMResponse, error)
}