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:
+89
-178
@@ -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()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user