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
@@ -32,6 +32,7 @@ type Config struct {
// 管理员账户 (开发阶段使用)
AdminUsername string
AdminPassword string
AdminNickname string // 昔涟对用户的基本称呼
// 注册开关
RegistrationEnabled bool
@@ -94,6 +95,7 @@ func Load() *Config {
// 管理员账户 (开发阶段使用)
AdminUsername: getEnv("ADMIN_USERNAME", "admin"),
AdminPassword: getEnv("ADMIN_PASSWORD", "cyrene-dev-admin"),
AdminNickname: getEnv("ADMIN_NICKNAME", "管理员"),
// 注册开关 (开发阶段默认关闭)
RegistrationEnabled: getEnvBool("REGISTRATION_ENABLED", false),
@@ -20,7 +20,7 @@ func NewAuthHandler(cfg *config.Config) *AuthHandler {
return &AuthHandler{cfg: cfg}
}
// Register 用户注册 (需要邮箱验证码)
// Register 用户注册 (需要邮箱验证码、昵称必填)
func (h *AuthHandler) Register(c *gin.Context) {
// 检查注册开关
if !h.cfg.RegistrationEnabled {
@@ -32,6 +32,7 @@ func (h *AuthHandler) Register(c *gin.Context) {
Username string `json:"username" binding:"required,min=2,max=32"`
Password string `json:"password" binding:"required,min=6,max=64"`
Email string `json:"email" binding:"required,email"`
Nickname string `json:"nickname" binding:"required,min=1,max=32"`
// MVP阶段:验证码仅做格式校验,后续接入邮件服务
VerifyCode string `json:"verify_code" binding:"required,len=6"`
}
@@ -65,9 +66,10 @@ func (h *AuthHandler) Register(c *gin.Context) {
}
c.JSON(http.StatusCreated, gin.H{
"user_id": userID,
"token": token,
"expires": time.Now().Add(h.cfg.JWTExpiryHours).Unix(),
"user_id": userID,
"token": token,
"expires": time.Now().Add(h.cfg.JWTExpiryHours).Unix(),
"nickname": req.Nickname,
})
}
@@ -219,6 +219,7 @@ func (h *ChatHandler) streamResponse(client *ws.Client, mode string, reqBody []b
var fullText string
var msgID string
var segments []ws.VoiceSegment // 收集断句信息
for scanner.Scan() {
line := scanner.Text()
@@ -240,7 +241,13 @@ func (h *ChatHandler) streamResponse(client *ws.Client, mode string, reqBody []b
Delta string `json:"delta"`
Error string `json:"error,omitempty"`
MessageID string `json:"message_id,omitempty"`
Mode string `json:"mode,omitempty"`
Done bool `json:"done,omitempty"`
// 断句相关 (来自 AI-Core 新格式)
Segments []struct {
Index int `json:"index"`
Text string `json:"text"`
} `json:"segments,omitempty"`
}
if err := json.Unmarshal([]byte(data), &chunk); err != nil {
log.Printf("[chat] 解析 SSE delta 失败: %v, raw=%s", err, data)
@@ -270,6 +277,25 @@ func (h *ChatHandler) streamResponse(client *ws.Client, mode string, reqBody []b
break
}
// 处理断句事件 (stream_segments)
if len(chunk.Segments) > 0 {
for _, seg := range chunk.Segments {
segments = append(segments, ws.VoiceSegment{
Index: seg.Index,
Text: seg.Text,
})
}
// 发送断句事件给前端
client.SendMessage(ws.ServerMessage{
Type: "stream_segments",
MessageID: msgID,
Segments: segments,
SessionID: client.SessionID,
Timestamp: time.Now().UnixMilli(),
})
continue
}
// 逐 delta 转发
if chunk.Delta != "" {
fullText += chunk.Delta
@@ -301,6 +327,28 @@ func (h *ChatHandler) streamResponse(client *ws.Client, mode string, reqBody []b
msgID = "msg_" + generateID()
}
// 检测是否为多消息格式(包含空行分隔的多条消息)
multiParts := parseMultiMessage(fullText)
if len(multiParts) > 1 {
// 发送 multi_message 事件
var items []ws.MultiMessageItem
for i, part := range multiParts {
items = append(items, ws.MultiMessageItem{
Index: i,
Content: part,
})
}
client.SendMessage(ws.ServerMessage{
Type: "multi_message",
MessageID: msgID,
SessionID: client.SessionID,
MultiMessage: &ws.MultiMessagePayload{
Messages: items,
},
Timestamp: time.Now().UnixMilli(),
})
}
// 发送 stream_end
client.SendMessage(ws.ServerMessage{
Type: "stream_end",
@@ -393,3 +441,26 @@ func randomStr(n int) string {
return string(b)
}
// parseMultiMessage 检测并解析多消息格式
// 如果文本包含空行分隔的多条消息,拆分为多条;否则返回单条
func parseMultiMessage(text string) []string {
if text == "" {
return nil
}
// 按双换行(空行)分割
parts := strings.Split(text, "\n\n")
// 过滤空字符串并去除首尾空白
var result []string
for _, p := range parts {
p = strings.TrimSpace(p)
if p != "" {
result = append(result, p)
}
}
// 如果只有一条,返回 nil 表示不是多消息格式
if len(result) <= 1 {
return nil
}
return result
}
+3 -1
View File
@@ -133,6 +133,8 @@ func (c *Client) SendMessage(msg ServerMessage) error {
case c.Send <- data:
return nil
default:
return nil // 通道满则丢弃
// 通道满:记录警告并返回错误(避免静默丢弃
log.Printf("[WS] 发送通道已满,丢弃消息: type=%s user=%s", msg.Type, c.UserID)
return nil
}
}
+28 -16
View File
@@ -25,22 +25,34 @@ type ClientMessage struct {
// 服务端 → 客户端消息
type ServerMessage struct {
Type string `json:"type"` // response | segment | audio | error | device_update | pong | history_response | stream_chunk | stream_end | background_thinking | notification
MessageID string `json:"message_id"`
Text string `json:"text,omitempty"`
Content string `json:"content,omitempty"` // stream_chunk 的增量文本
Role string `json:"role,omitempty"` // stream 消息的角色
SessionID string `json:"session_id,omitempty"` // 会话 ID
Segments []VoiceSegment `json:"segments,omitempty"` // 断句数组
FullAudioURL string `json:"full_audio_url,omitempty"`
ResponseMode string `json:"response_mode"`
ToolCalls []ToolCall `json:"tool_calls,omitempty"`
Error string `json:"error,omitempty"`
Timestamp int64 `json:"timestamp"`
Messages []Message `json:"messages,omitempty"` // 历史消息列表
Devices []IotDeviceInfo `json:"devices,omitempty"` // IoT 设备状态
ThinkingStatus string `json:"thinking_status,omitempty"` // 后台思考状态
Notification *NotificationInfo `json:"notification,omitempty"` // 通知推送
Type string `json:"type"` // response | segment | audio | error | device_update | pong | history_response | stream_chunk | stream_end | background_thinking | notification | multi_message | stream_segments
MessageID string `json:"message_id"`
Text string `json:"text,omitempty"`
Content string `json:"content,omitempty"` // stream_chunk 的增量文本
Role string `json:"role,omitempty"` // stream 消息的角色
SessionID string `json:"session_id,omitempty"` // 会话 ID
Segments []VoiceSegment `json:"segments,omitempty"` // 断句数组
FullAudioURL string `json:"full_audio_url,omitempty"`
ResponseMode string `json:"response_mode"`
ToolCalls []ToolCall `json:"tool_calls,omitempty"`
Error string `json:"error,omitempty"`
Timestamp int64 `json:"timestamp"`
Messages []Message `json:"messages,omitempty"` // 历史消息列表
Devices []IotDeviceInfo `json:"devices,omitempty"` // IoT 设备状态
ThinkingStatus string `json:"thinking_status,omitempty"` // 后台思考状态
Notification *NotificationInfo `json:"notification,omitempty"` // 通知推送
MultiMessage *MultiMessagePayload `json:"multi_message,omitempty"` // 多条消息批量发送
}
// MultiMessagePayload 多条消息的容器 (对应昔涟的多消息回复风格)
type MultiMessagePayload struct {
Messages []MultiMessageItem `json:"messages"`
}
// MultiMessageItem 多消息中的单条
type MultiMessageItem struct {
Index int `json:"index"`
Content string `json:"content"`
}
// NotificationInfo 通知推送信息