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
+89 -178
View File
@@ -21,9 +21,12 @@ import (
"github.com/yourname/cyrene-ai/ai-core/internal/model"
"github.com/yourname/cyrene-ai/ai-core/internal/orchestrator"
"github.com/yourname/cyrene-ai/ai-core/internal/persona"
"github.com/yourname/cyrene-ai/ai-core/internal/subsession"
"github.com/yourname/cyrene-ai/ai-core/internal/tools"
)
var cfg Config
func main() {
// 自动加载 .env 文件(来自 backend/.env
if err := godotenv.Load("../.env"); err != nil {
@@ -34,7 +37,7 @@ func main() {
log.Println("🧠 AI-Core 服务启动中...")
// 加载配置
cfg := loadConfig()
cfg = loadConfig()
// 初始化人格加载器
personaDir := cfg.PersonaDir
@@ -147,8 +150,29 @@ func main() {
// 健康检查与对话API的HTTP mux
mux := http.NewServeMux()
// 手动构建 orchestrator 用于处理(因为现有orchestrator结构体已定义但未导出构造函数)
orch := &orchestrator.Orchestrator{}
// 初始化子会话管理器
subManager := subsession.NewManager(llmAdapter)
// 注册子会话提供者
subManager.Register(subsession.NewGeneralProvider(personaLoader))
if memRetriever != nil {
subManager.Register(subsession.NewMemoryProvider(memRetriever))
}
if iotClient != nil {
subManager.Register(subsession.NewIoTProvider(iotClient))
}
log.Printf("子会话管理器已就绪: %d 个提供者 (%v)", len(subManager.ListProviders()), subManager.ListProviders())
// 构建新的 Orchestrator (v2.0)
orch := orchestrator.NewOrchestrator(
personaLoader,
ctxBuilder,
llmAdapter,
subManager,
memRetriever,
memExtractor,
)
log.Println("对话编排器 v2.0 已就绪")
// 注册对话API端点
mux.HandleFunc("/api/v1/chat", func(w http.ResponseWriter, r *http.Request) {
@@ -203,6 +227,7 @@ type Config struct {
LLMFallbackModel string
DatabaseURL string
IoTServiceURL string
AdminNickname string // 昔涟对管理员用户的基本称呼
}
func loadConfig() Config {
@@ -215,6 +240,7 @@ func loadConfig() Config {
LLMFallbackModel: getEnv("LLM_FALLBACK_MODEL", "gpt-4o-mini"),
DatabaseURL: buildDatabaseURL(),
IoTServiceURL: getEnv("IOT_DEBUG_SERVICE_URL", ""),
AdminNickname: getEnv("ADMIN_NICKNAME", "管理员"),
}
}
@@ -275,19 +301,19 @@ func buildOpenAITools(registry *tools.Registry) []llm.OpenAITool {
return result
}
// handleChat 处理对话请求(SSE 流式响应 + 工具调用)
// handleChat 处理对话请求(SSE 流式响应)— 使用新 Orchestrator v2.0
func handleChat(
w http.ResponseWriter,
r *http.Request,
_ *orchestrator.Orchestrator,
orch *orchestrator.Orchestrator,
ctxBuilder *ctxbuild.Builder,
llmAdapter *llm.Adapter,
personaLoader *persona.Loader,
memRetriever *memory.Retriever,
memExtractor *memory.Extractor,
_ *llm.Adapter,
_ *persona.Loader,
_ *memory.Retriever,
_ *memory.Extractor,
iotClient *tools.IoTClient,
thinker *background.Thinker,
toolRegistry *tools.Registry,
_ *tools.Registry,
) {
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
@@ -300,6 +326,7 @@ func handleChat(
SessionID string `json:"session_id"`
Message string `json:"message"`
Mode string `json:"mode"`
Nickname string `json:"nickname,omitempty"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "无效的请求体", http.StatusBadRequest)
@@ -317,82 +344,16 @@ func handleChat(
thinker.RecordUserMessage()
}
// 确定用户昵称
userNickname := req.Nickname
if userNickname == "" {
userNickname = cfg.AdminNickname
}
// 0.1 缓存用户消息到会话历史
ctxBuilder.CacheMessage(req.SessionID, model.RoleUser, req.Message)
// 1. 检索相关记忆
var memories []memory.MemoryEntry
if memRetriever != nil {
var err error
memories, err = memRetriever.Retrieve(ctx, req.UserID, req.Message)
if err != nil {
log.Printf("[chat] 记忆检索失败: %v", err)
}
}
// 2. 加载人格配置
personaConfig, err := personaLoader.Get("cyrene")
if err != nil {
http.Error(w, fmt.Sprintf("加载人格失败: %v", err), http.StatusInternalServerError)
return
}
// 2.1 始终获取 IoT 设备状态(去掉关键词门控,让昔涟始终了解家里的状态)
var deviceContext string
if iotClient != nil {
devices := iotClient.GetDevicesForContext()
if len(devices) > 0 {
deviceInfos := make([]ctxbuild.DeviceInfo, 0, len(devices))
for _, d := range devices {
deviceInfos = append(deviceInfos, ctxbuild.DeviceInfo{
Name: d.Name,
Type: d.Type,
Status: d.Status,
Brightness: d.Brightness,
Color: d.Color,
Temperature: d.Temperature,
Mode: d.Mode,
Value: d.Value,
Unit: d.Unit,
Battery: d.Battery,
})
}
deviceContext = ctxbuild.InjectDeviceContext(deviceInfos)
log.Printf("[chat] 已注入 IoT 设备状态 (%d 个设备)", len(deviceInfos))
}
}
// 2.2 获取待处理的后台思考
var pendingThoughts []string
if thinker != nil && thinker.HasPendingThoughts() {
pts := thinker.GetPendingThoughts()
for _, pt := range pts {
if pt.Content != "" {
pendingThoughts = append(pendingThoughts, pt.Content)
}
}
if len(pendingThoughts) > 0 {
log.Printf("[chat] 注入 %d 条后台思考到上下文", len(pendingThoughts))
}
}
// 3. 构建对话上下文
llmMessages, err := ctxBuilder.Build(ctx, ctxbuild.BuildParams{
UserID: req.UserID,
SessionID: req.SessionID,
UserMessage: req.Message,
Persona: personaConfig,
Memories: memories,
HistoryLimit: 20,
DeviceContext: deviceContext,
PendingThoughts: pendingThoughts,
})
if err != nil {
http.Error(w, fmt.Sprintf("构建上下文失败: %v", err), http.StatusInternalServerError)
return
}
// 4. 设置 SSE 响应头
// 1. 设置 SSE 响应头
w.Header().Set("Content-Type", "text/event-stream")
w.Header().Set("Cache-Control", "no-cache")
w.Header().Set("Connection", "keep-alive")
@@ -404,57 +365,17 @@ func handleChat(
return
}
// 5. 准备工具定义
openAITools := buildOpenAITools(toolRegistry)
// 5.1 如果启用了工具,先进行同步调用检测是否需要工具调用
if len(openAITools) > 0 {
log.Printf("[chat] 启用工具调用: %d 个工具可用", len(openAITools))
syncResp, syncErr := llmAdapter.ChatWithTools(ctx, llmMessages, openAITools)
if syncErr != nil {
log.Printf("[chat] 工具检测调用失败: %v,降级为普通对话", syncErr)
} else if len(syncResp.ToolCalls) > 0 {
log.Printf("[chat] 模型请求 %d 个工具调用", len(syncResp.ToolCalls))
// 将助手消息(含工具调用)加入上下文
assistantMsg := model.LLMMessage{
Role: model.RoleAssistant,
Content: syncResp.Content,
ToolCalls: syncResp.ToolCalls,
ReasoningContent: syncResp.ReasoningContent,
}
llmMessages = append(llmMessages, assistantMsg)
// 执行每个工具调用并将结果加入上下文
for _, tc := range syncResp.ToolCalls {
var args map[string]interface{}
if err := json.Unmarshal([]byte(tc.Arguments), &args); err != nil {
log.Printf("[chat] 工具 %s 参数解析失败: %v", tc.Name, err)
args = make(map[string]interface{})
}
result, execErr := toolRegistry.Execute(ctx, tc.Name, args)
if execErr != nil {
log.Printf("[chat] 工具 %s 执行失败: %v", tc.Name, execErr)
}
resultJSON, _ := json.Marshal(result)
llmMessages = append(llmMessages, model.LLMMessage{
Role: model.RoleTool,
Content: string(resultJSON),
ToolCallID: tc.ID,
})
}
}
// 无论是否有工具调用,继续流式输出最终回复
}
// 5.2 调用LLM流式接口(可能已附加工具结果)
chunkCh, err := llmAdapter.ChatStream(ctx, llmMessages)
// 2. 调用 Orchestrator 处理(替代原有的线性处理流程)
// Orchestrator 内部处理:意图分析 → 子会话分派 → 结果汇总 → 综合生成回复
eventCh, err := orch.ProcessInput(ctx, orchestrator.ProcessParams{
UserID: req.UserID,
SessionID: req.SessionID,
Message: req.Message,
Mode: req.Mode,
Nickname: userNickname,
})
if err != nil {
// 流式初始化失败,返回 SSE 格式错误
errData, _ := json.Marshal(map[string]string{"delta": "", "error": fmt.Sprintf("LLM调用失败: %v", err)})
errData, _ := json.Marshal(map[string]string{"delta": "", "error": fmt.Sprintf("处理失败: %v", err)})
fmt.Fprintf(w, "data: %s\n\n", errData)
flusher.Flush()
fmt.Fprintf(w, "data: [DONE]\n\n")
@@ -463,66 +384,56 @@ func handleChat(
}
messageID := fmt.Sprintf("msg-%d", time.Now().UnixNano())
// 6. 逐 token 推送 SSE
var fullContent string
var segments []llm.Segment
segmenter := llm.NewSegmenter()
for chunk := range chunkCh {
if chunk.Error != nil {
log.Printf("[chat] 流式错误: %v", chunk.Error)
errData, _ := json.Marshal(map[string]string{"delta": "", "error": chunk.Error.Error()})
// 3. 流式输出 SSE
var fullContent string
for event := range eventCh {
switch event.Type {
case model.StreamError:
log.Printf("[chat] 流式错误: %v", event.Error)
errData, _ := json.Marshal(map[string]string{"delta": "", "error": event.Error.Error()})
fmt.Fprintf(w, "data: %s\n\n", errData)
flusher.Flush()
fmt.Fprintf(w, "data: [DONE]\n\n")
flusher.Flush()
return
}
if chunk.Done {
// 流结束,flush 剩余片段
if remaining := segmenter.Flush(); remaining != nil {
segments = append(segments, *remaining)
}
break
}
if chunk.Content != "" {
fullContent += chunk.Content
// 实时断句
newSegs := segmenter.Feed(chunk.Content)
segments = append(segments, newSegs...)
case model.StreamDelta:
fullContent += event.Delta
deltaData, _ := json.Marshal(map[string]string{
"delta": chunk.Content,
"delta": event.Delta,
"message_id": messageID,
})
fmt.Fprintf(w, "data: %s\n\n", deltaData)
flusher.Flush()
case model.StreamSegments:
// 发送断句信息
segData, _ := json.Marshal(map[string]interface{}{
"message_id": messageID,
"mode": req.Mode,
"segments": event.Segments,
})
fmt.Fprintf(w, "data: %s\n\n", segData)
flusher.Flush()
case model.StreamDone:
// 下发结束标记
endData, _ := json.Marshal(map[string]interface{}{
"message_id": messageID,
"mode": req.Mode,
"done": true,
})
fmt.Fprintf(w, "data: %s\n\n", endData)
flusher.Flush()
fmt.Fprintf(w, "data: [DONE]\n\n")
flusher.Flush()
}
}
// 7. 发送结束标记(附带元数据
endData, _ := json.Marshal(map[string]interface{}{
"message_id": messageID,
"mode": req.Mode,
"segments": segments,
"done": true,
})
fmt.Fprintf(w, "data: %s\n\n", endData)
flusher.Flush()
fmt.Fprintf(w, "data: [DONE]\n\n")
flusher.Flush()
// 8. 缓存 LLM 回复到会话历史
if fullContent != "" {
ctxBuilder.CacheMessage(req.SessionID, model.RoleAssistant, fullContent)
}
// 9. 异步提取记忆
if memExtractor != nil && fullContent != "" {
go memExtractor.ExtractAndStore(context.Background(), req.UserID, req.SessionID, req.Message, fullContent)
// 4. 对话完成后触发昔涟的自主思考(事件驱动,非定时
if thinker != nil {
thinker.TriggerPostChatThink()
}
}