fix: 第四轮调试 — 回复去重/消息时序/UI布局/自主思考深度优化 + 文档重整

后端修复:
- main.go: 恢复 /api/v1/chat 路由中丢失的 handleChat 调用 (空响应回归)
- orchestrator.go: splitChatByLines 改为双换行分割, 避免单换行误拆
- chat_handler.go: multi_message 增加 !hasReview 守卫, 消息延迟 200→800ms
- thinker.go: RecordUserMessage 追踪活跃会话ID, 推送主动消息到正确会话
- thinker.go: 增强思考提示词 — 禁止在用户休息/离开时发送主动消息

前端修复:
- useWebSocket.ts: stream_segments 不再创建消息气泡, 消除重复回复
- MessageBubble.tsx: 动作消息居左对齐无头像, 时间戳移至气泡外侧 hover 显示
- ChatInput.tsx: 昔涟输入提示移至输入框上方, 波点动画效果
- MessageList/TypingIndicator/ChatContainer: 清理冗余 isTyping 传递
- MemoryPanel.tsx: 新增记忆面板组件

文档重整:
- docs/debug/ → docs/debug_log/ 重命名统一
- 新增 debug_log/README.md 索引
- .gitignore: 新增 android/ 排除规则

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-23 13:09:18 +08:00
parent 0c1bbff7b4
commit b123a36aae
37 changed files with 580 additions and 174 deletions
@@ -131,16 +131,15 @@ func (o *Orchestrator) ProcessInput(
Nickname: userName,
}
// 对于 simple greeting,跳过子会话分派,直接合成回复
var resultCh <-chan model.SubSessionResult
skipSubSessions := intent.Primary == "greeting" ||
(intent.Primary == "chat" && !intent.NeedsIoT && !intent.NeedsMemory)
if skipSubSessions {
log.Printf("[orchestrator] 快速通道: 简单消息(primary=%s),跳过子会话分派", intent.Primary)
// 创建一个已关闭的空通道
emptyCh := make(chan model.SubSessionResult)
close(emptyCh)
resultCh = emptyCh
// 只有明确的关键词问候才跳过子会话分派,日常闲聊也需要检索记忆
// 因为 LLM 容易将日常闲聊误判为 needs_memory=false,导致回复缺乏上下文
var resultCh <-chan model.SubSessionResult
skipSubSessions := intent.Primary == "greeting" && !intent.NeedsMemory
if skipSubSessions {
log.Printf("[orchestrator] 快速通道: 简单问候(primary=%s),跳过子会话分派", intent.Primary)
emptyCh := make(chan model.SubSessionResult)
close(emptyCh)
resultCh = emptyCh
} else {
resultCh = o.subManager.Dispatch(subCtx, intent, params.Message, createParams)
}
@@ -369,7 +368,7 @@ func parseReviewMessages(text string) []model.ReviewMessage {
if actionStart > 0 {
prefix := strings.TrimSpace(remaining[:actionStart])
if prefix != "" {
messages = append(messages, splitReviewLongMessage(model.ReviewMessageChat, prefix)...)
messages = append(messages, splitChatByLines(model.ReviewMessageChat, prefix)...)
}
}
// 括号内作为 action
@@ -385,7 +384,7 @@ func parseReviewMessages(text string) []model.ReviewMessage {
// 没有括号,剩余全部作为 chat
remaining = strings.TrimSpace(remaining)
if remaining != "" {
messages = append(messages, splitReviewLongMessage(model.ReviewMessageChat, remaining)...)
messages = append(messages, splitChatByLines(model.ReviewMessageChat, remaining)...)
}
break
}
@@ -409,6 +408,26 @@ func splitReviewLongMessage(msgType model.ReviewMessageType, text string) []mode
if len(runes) <= maxLen {
return []model.ReviewMessage{{Type: msgType, Content: text}}
}
// ... split by sentence boundaries for long messages
return splitLongText(msgType, runes, maxLen)
}
// splitChatByLines 将聊天文本按双换行(段落分隔)拆分为多条消息,每条再检查是否需要按长度拆分
func splitChatByLines(msgType model.ReviewMessageType, text string) []model.ReviewMessage {
lines := strings.Split(text, "\n\n")
var msgs []model.ReviewMessage
for _, line := range lines {
line = strings.TrimSpace(line)
if line == "" {
continue
}
msgs = append(msgs, splitReviewLongMessage(msgType, line)...)
}
return msgs
}
// splitLongText 将文本按句子边界分割
func splitLongText(msgType model.ReviewMessageType, runes []rune, maxLen int) []model.ReviewMessage {
var messages []model.ReviewMessage
start := 0
@@ -459,7 +478,7 @@ func splitReviewLongMessage(msgType model.ReviewMessageType, text string) []mode
if len(messages) == 0 {
messages = append(messages, model.ReviewMessage{
Type: msgType,
Content: text,
Content: string(runes),
})
}