feat: Phase 1+2 架构进化 — 连续思考链/主动消息决策/情感状态机/离线自主思考 (86文件)

Phase 1 (基础设施):
- ThinkChain 思考链连续性 + 差异化思考提示词 (persistent)
- AutonomousToolPolicy 工具安全策略 (safe/unsafe/conditional)
- MessageScheduler 自适应消息节奏 (Idle/Available/Busy)
- SessionEnrichmentStore 渐进式上下文丰富 (5层)
- ConversationBus 事件总线 + ResponseCache (dedup)
- pkg/logger 统一日志 + 所有 handler 替换 fmt.Printf
- NPE 守卫/链路优化/数据库表修复/Go workspace

Phase 2 (人格交互):
- EmotionState/EmotionTracker 情感状态机 (5种心情, 情绪衰减)
- ProactiveGuard 主动消息多维决策 (静默时段/紧急度/频率/校验)
- Gateway↔ai-core 在线状态感知链路 (presence notification)
- 离线思考频率控制 + 重连问候 + 离线消息排队

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-23 15:25:12 +08:00
parent b123a36aae
commit 87214b9441
86 changed files with 3085 additions and 582 deletions
@@ -8,7 +8,7 @@ import (
"encoding/json"
"fmt"
"io"
"log"
"github.com/yourname/cyrene-ai/pkg/logger"
"net/http"
"strings"
"time"
@@ -88,7 +88,7 @@ func (h *ChatHandler) HandleWebSocket(c *gin.Context) {
// 升级WebSocket连接
conn, err := h.upgrader.Upgrade(c.Writer, c.Request, nil)
if err != nil {
log.Printf("[WS] 升级连接失败: %v", err)
logger.Printf("[WS] 升级连接失败: %v", err)
return
}
@@ -115,7 +115,7 @@ func (h *ChatHandler) handleMessage(client *ws.Client, msg ws.ClientMessage) {
case "history":
h.handleHistoryRequest(client, msg)
default:
log.Printf("[WS] 未知消息类型: %s from user=%s", msg.Type, client.UserID)
logger.Printf("[WS] 未知消息类型: %s from user=%s", msg.Type, client.UserID)
}
}
@@ -128,8 +128,8 @@ func (h *ChatHandler) handleChatMessage(client *ws.Client, msg ws.ClientMessage)
// 持久化用户消息到数据库(在 WebSocket 发送之前)
if h.sessionStore != nil && h.sessionStore.IsAvailable() {
if err := h.sessionStore.AddMessage(client.SessionID, "user", msg.Content); err != nil {
log.Printf("[chat] 持久化用户消息失败: %v", err)
if err := h.sessionStore.AddMessage(client.SessionID, "user", "chat", msg.Content); err != nil {
logger.Printf("[chat] 持久化用户消息失败: %v", err)
}
}
@@ -151,7 +151,7 @@ func (h *ChatHandler) handleChatMessage(client *ws.Client, msg ws.ClientMessage)
}
reqBody, err := json.Marshal(aiReq)
if err != nil {
log.Printf("[chat] 序列化请求失败: %v", err)
logger.Printf("[chat] 序列化请求失败: %v", err)
h.hub.UpdateSessionState(client.SessionID, "error")
client.SendMessage(ws.ServerMessage{
Type: "error",
@@ -183,7 +183,7 @@ func (h *ChatHandler) streamResponse(client *ws.Client, mode string, reqBody []b
aiCoreURL := h.cfg.AICoreURL + "/api/v1/chat"
httpReq, err := http.NewRequest("POST", aiCoreURL, bytes.NewReader(reqBody))
if err != nil {
log.Printf("[chat] 创建 AI-Core 请求失败: %v", err)
logger.Printf("[chat] 创建 AI-Core 请求失败: %v", err)
h.hub.UpdateSessionState(client.SessionID, "error")
client.SendMessage(ws.ServerMessage{
Type: "error",
@@ -199,7 +199,7 @@ func (h *ChatHandler) streamResponse(client *ws.Client, mode string, reqBody []b
httpClient := &http.Client{Timeout: 120 * time.Second}
resp, err := httpClient.Do(httpReq)
if err != nil {
log.Printf("[chat] AI-Core 调用失败: %v", err)
logger.Printf("[chat] AI-Core 调用失败: %v", err)
h.hub.UpdateSessionState(client.SessionID, "error")
client.SendMessage(ws.ServerMessage{
Type: "error",
@@ -213,7 +213,7 @@ func (h *ChatHandler) streamResponse(client *ws.Client, mode string, reqBody []b
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
log.Printf("[chat] AI-Core 返回错误 [%d]: %s", resp.StatusCode, string(body))
logger.Printf("[chat] AI-Core 返回错误 [%d]: %s", resp.StatusCode, string(body))
h.hub.UpdateSessionState(client.SessionID, "error")
client.SendMessage(ws.ServerMessage{
Type: "error",
@@ -273,13 +273,13 @@ func (h *ChatHandler) streamResponse(client *ws.Client, mode string, reqBody []b
ReviewMessages []ws.ReviewMessage `json:"review_messages,omitempty"`
}
if err := json.Unmarshal([]byte(data), &chunk); err != nil {
log.Printf("[chat] 解析 SSE delta 失败: %v, raw=%s", err, data)
logger.Printf("[chat] 解析 SSE delta 失败: %v, raw=%s", err, data)
continue
}
// 错误处理
if chunk.Error != "" {
log.Printf("[chat] AI-Core 流式错误: %s", chunk.Error)
logger.Printf("[chat] AI-Core 流式错误: %s", chunk.Error)
h.hub.UpdateSessionState(client.SessionID, "error")
client.SendMessage(ws.ServerMessage{
Type: "error",
@@ -312,8 +312,8 @@ func (h *ChatHandler) streamResponse(client *ws.Client, mode string, reqBody []b
reviewMsgID := fmt.Sprintf("%s_r%d", msgID, i)
// 持久化每条审查消息
if h.sessionStore != nil && h.sessionStore.IsAvailable() {
if err := h.sessionStore.AddMessage(client.SessionID, role, rm.Content); err != nil {
log.Printf("[chat] 持久化审查消息失败: %v", err)
if err := h.sessionStore.AddMessage(client.SessionID, role, msgType, rm.Content); err != nil {
logger.Printf("[chat] 持久化审查消息失败: %v", err)
}
}
h.hub.CacheMessage(client.UserID, client.SessionID, ws.Message{
@@ -331,9 +331,9 @@ func (h *ChatHandler) streamResponse(client *ws.Client, mode string, reqBody []b
SessionID: client.SessionID,
Timestamp: time.Now().UnixMilli(),
})
// 小延迟让消息逐条到达,更像真人
if i < len(chunk.ReviewMessages)-1 {
time.Sleep(800 * time.Millisecond)
// 使用 MessageScheduler 计算的 per-message 延迟
if rm.DelayMs > 0 {
time.Sleep(time.Duration(rm.DelayMs) * time.Millisecond)
}
}
hasReview = true
@@ -366,7 +366,7 @@ func (h *ChatHandler) streamResponse(client *ws.Client, mode string, reqBody []b
}
if err := scanner.Err(); err != nil {
log.Printf("[chat] SSE 读取错误: %v", err)
logger.Printf("[chat] SSE 读取错误: %v", err)
h.hub.UpdateSessionState(client.SessionID, "error")
client.SendMessage(ws.ServerMessage{
Type: "error",
@@ -416,8 +416,8 @@ func (h *ChatHandler) streamResponse(client *ws.Client, mode string, reqBody []b
// 如果有审查消息,每条已单独持久化,跳过 fullText 以避免重复
if !hasReview && fullText != "" {
if h.sessionStore != nil && h.sessionStore.IsAvailable() {
if err := h.sessionStore.AddMessage(client.SessionID, "assistant", fullText); err != nil {
log.Printf("[chat] 持久化 AI 回复失败: %v", err)
if err := h.sessionStore.AddMessage(client.SessionID, "assistant", "chat", fullText); err != nil {
logger.Printf("[chat] 持久化 AI 回复失败: %v", err)
}
}
@@ -466,18 +466,20 @@ func (h *ChatHandler) handleHistoryRequest(client *ws.Client, msg ws.ClientMessa
if len(messages) == 0 && h.sessionStore != nil && h.sessionStore.IsAvailable() {
dbMessages, err := h.sessionStore.GetMessages(sessionID, 50, 0)
if err == nil && len(dbMessages) > 0 {
log.Printf("[history] 从数据库恢复会话历史: session=%s, %d 条消息", sessionID, len(dbMessages))
logger.Printf("[history] 从数据库恢复会话历史: session=%s, %d 条消息", sessionID, len(dbMessages))
// 恢复到内存缓存
for _, dbMsg := range dbMessages {
messages = append(messages, ws.Message{
ID: fmt.Sprintf("db_%d", dbMsg.ID),
Role: dbMsg.Role,
MsgType: dbMsg.MsgType,
Content: dbMsg.Content,
Timestamp: dbMsg.CreatedAt.UnixMilli(),
})
h.hub.CacheMessage(client.UserID, sessionID, ws.Message{
ID: fmt.Sprintf("db_%d", dbMsg.ID),
Role: dbMsg.Role,
MsgType: dbMsg.MsgType,
Content: dbMsg.Content,
Timestamp: dbMsg.CreatedAt.UnixMilli(),
})
@@ -497,7 +499,7 @@ func (h *ChatHandler) handleHistoryRequest(client *ws.Client, msg ws.ClientMessa
}
if err := client.SendMessage(response); err != nil {
log.Printf("[WS] 发送历史消息失败: %v", err)
logger.Printf("[WS] 发送历史消息失败: %v", err)
}
}
@@ -535,10 +537,22 @@ func (h *ChatHandler) HandleProactiveMessage(c *gin.Context) {
// 检查用户是否在线
onlineCount := h.hub.UserClientCount(req.UserID)
if onlineCount == 0 {
// Phase 2: 离线时排队,等待用户重连后推送
data, _ := json.Marshal(ws.ServerMessage{
Type: "response",
MessageID: "proactive_" + generateID(),
Content: req.Content,
Role: "assistant",
MsgType: "proactive",
SessionID: req.SessionID,
Timestamp: time.Now().UnixMilli(),
})
h.hub.QueueProactiveMessage(req.UserID, data)
logger.Printf("[proactive] 用户离线,消息已排队: user=%s", req.UserID)
c.JSON(http.StatusOK, gin.H{
"success": false,
"reason": "user_offline",
"message": "用户不在线,消息未发送",
"success": true,
"reason": "queued",
"message": "用户线,消息已排队等待重连后推送",
})
return
}
@@ -557,7 +571,7 @@ func (h *ChatHandler) HandleProactiveMessage(c *gin.Context) {
data, err := json.Marshal(msg)
if err != nil {
log.Printf("[proactive] 序列化消息失败: %v", err)
logger.Printf("[proactive] 序列化消息失败: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "内部错误"})
return
}
@@ -577,7 +591,7 @@ func (h *ChatHandler) HandleProactiveMessage(c *gin.Context) {
})
h.hub.RecordMessage(sessionID, "assistant", req.Content)
log.Printf("[proactive] 主动消息已推送: user=%s, online=%d, content_len=%d", req.UserID, onlineCount, len(req.Content))
logger.Printf("[proactive] 主动消息已推送: user=%s, online=%d, content_len=%d", req.UserID, onlineCount, len(req.Content))
c.JSON(http.StatusOK, gin.H{
"success": true,