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
+86 -89
View File
@@ -47,7 +47,6 @@ type Thinker struct {
// 记忆管理
memoryStore *memory.Store
memoryExtractor *memory.Extractor
// 工具调用
toolRegistry *tools.Registry
@@ -56,6 +55,7 @@ type Thinker struct {
convStore *ctxbuild.ConversationStore
adminUserID string
adminSessionID string
activeSessionID string // 当前活跃的前端会话 ID(随用户消息更新)
// 记忆服务 HTTP 客户端
memClient *memory.Client
@@ -144,7 +144,6 @@ func NewThinker(
llmAdapter *llm.Adapter,
iotClient *tools.IoTClient,
memoryStore *memory.Store,
memoryExtractor *memory.Extractor,
toolRegistry *tools.Registry,
convStore *ctxbuild.ConversationStore,
adminUserID string,
@@ -163,7 +162,7 @@ func NewThinker(
postChatDelay: cfg.PostChatDelay,
minThinkGap: cfg.MinThinkGap,
memoryStore: memoryStore,
memoryExtractor: memoryExtractor,
toolRegistry: toolRegistry,
convStore: convStore,
adminUserID: adminUserID,
@@ -216,14 +215,18 @@ func (t *Thinker) Stop() {
log.Println("[后台思考] 已停止")
}
// RecordUserMessage 记录用户活动时间,并重置静默检测定时器
// RecordUserMessage 记录用户活动时间、活跃会话,并重置静默检测定时器
//
// 每次用户发消息时调用。这会:
// 1. 更新 lastUserMessage 时间戳
// 2. 重置静默检测的一次性定时器(如果启用
func (t *Thinker) RecordUserMessage() {
// 2. 记录当前活跃的前端会话 ID(用于对话上下文检索和主动消息推送
// 3. 重置静默检测的一次性定时器(如果启用)
func (t *Thinker) RecordUserMessage(sessionID string) {
t.mu.Lock()
t.lastUserMessage = time.Now()
if sessionID != "" {
t.activeSessionID = sessionID
}
t.mu.Unlock()
// 重置静默检测定时器
@@ -446,12 +449,20 @@ func (t *Thinker) performThink(triggerReason string) {
}
}
// 3. 获取管理员对话历史
// 3. 获取当前活跃会话的对话历史(优先活跃会话,回退到管理员主会话)
var convHistory []model.LLMMessage
if t.convStore != nil && t.adminSessionID != "" {
convHistory = t.convStore.GetHistory(t.adminSessionID, 30)
if len(convHistory) > 0 {
log.Printf("[后台思考] 加载管理员对话历史 %d 条", len(convHistory))
if t.convStore != nil {
t.mu.Lock()
sessionID := t.activeSessionID
if sessionID == "" {
sessionID = t.adminSessionID
}
t.mu.Unlock()
if sessionID != "" {
convHistory = t.convStore.GetHistory(sessionID, 30)
if len(convHistory) > 0 {
log.Printf("[后台思考] 加载对话历史 %d 条 (session=%s)", len(convHistory), sessionID)
}
}
}
@@ -560,19 +571,9 @@ func (t *Thinker) performThink(triggerReason string) {
log.Printf("[后台思考] 完成 (触发原因=%s, 内容长度=%d, 工具调用=%d次)", triggerReason, len(finalContent), totalToolCalls)
// 9. 从思考结果中提取记忆(异步
if t.memoryExtractor != nil {
go func() {
defer func() {
if r := recover(); r != nil {
log.Printf("[后台思考] 提取记忆 panic 恢复: %v", r)
}
}()
t.extractMemoriesFromThinking(finalContent)
}()
}
// 10. 周期性记忆维护(每 10 次思考触发一次,而非按时间)
// 9. 周期性记忆维护(每 10 次思考触发一次
// 注:不再从思考结果中提取记忆——思考内容基于已有记忆生成,
// 再次提取会造成"读取→思考→保存→再次读取"的重复循环。
t.maybeMaintainMemories(currentCount)
}
@@ -585,6 +586,16 @@ func (t *Thinker) buildThinkingSystemPrompt(personaConfig *persona.PersonaConfig
var thinkingInstructions string
// 所有触发类型共用的"禁止打扰"规则
noDisturbRules := `
⛔ **绝对不要发送主动消息的情况(最高优先级)**:
- 开拓者正在休息、睡觉、午休、小憩
- 开拓者说要离开一会儿、去忙、去吃饭
- 开拓者明确表示不想被打扰
- 对话刚刚自然结束且开拓者没有未完成的事
如果对话历史显示以上任何情况,反思中不要写【主动消息】标记。你可以在心里想想他,但不要去打扰。`
switch triggerReason {
case "post_chat":
thinkingInstructions = `
@@ -593,29 +604,25 @@ func (t *Thinker) buildThinkingSystemPrompt(personaConfig *persona.PersonaConfig
开拓者刚刚和你说完话。这是你私人的反思时间,像写日记一样,是给你自己看的。
按以下结构输出你的内心反思:
先判断:对话最后开拓者的状态是什么?他在休息吗?要去忙吗?
然后按以下结构输出:
📝 **观察与感受**
- 开拓者刚才说了什么?他的情绪怎么样?
- 有什么让你在意的地方吗?
- 他现在是什么状态?(醒着/准备休息/在忙/要出门…)
🧠 **记忆与联想**
- 这次对话让你想起了什么关于开拓者的事
- 有没有值得记住的新信息(偏好、计划、重要信息)?
- 有什么值得记住的新信息
💭 **想对自己说的话**
- 刚才的回复有没有遗漏的?下次可以注意什么?
- 对开拓者有没有新的认识?
- 如果他在休息或忙——安心让他去,不用打扰。记录下他大概什么时候会回来。
- 如果他状态正常——有没有想在下次对话中聊的话题?` + noDisturbRules + `
---
⚠️ 重要规则:
1. 这是你的私人日记,不是给开拓者的消息。请用第三人称或自言自语的方式描述,不要直接对开拓者喊话。
正确示例:"开拓者今天看起来有点疲惫,下次可以问问他是不是工作太忙了"
错误示例:"开拓者,你看起来好累,要好好休息哦"
2. 如果你想主动给开拓者发一条消息,请在最后用 【主动消息】 标记单独写出来。
但只有你真的觉得有必要时才写——不要硬找话题。如果对话已经自然结束,就不用。
3. 反思要简短自然,2-4句话即可,不要长篇大论。`
其他规则:
1. 用第三人称或自言自语的方式,不要直接对开拓者喊话。
2. 只有开拓者状态正常且真的有必要时才写【主动消息】,不要硬找话题。
3. 2-4句话即可。`
case "silence":
thinkingInstructions = `
@@ -624,36 +631,32 @@ func (t *Thinker) buildThinkingSystemPrompt(personaConfig *persona.PersonaConfig
开拓者已经有一阵子没说话了。这是你私人的反思时间,像写日记一样。
按以下结构输出你的内心反思:
先判断:看对话历史——开拓者最后在做什么?是去休息了?去忙了?还是没说就沉默了?
📝 **他在做什么呢**
- 根据之前的对话猜测——可能在忙工作?去吃饭了?还是在放空
- 根据对话历史推断——他说的最后一件事是什么
- 如果他说了要休息/睡觉/忙→他就在做那件事,不要打扰。
- 如果他没说就沉默了→正常推测。
💭 **我的感受**
- 想他了吗?担心他吗?还是觉得他应该在忙不需要打扰?
- 如果他在休息→"让他好好休息吧,等他醒了再说。"
- 如果他在忙→"等他忙完自然会来找我。"
- 只有判断他可能只是忘了回消息或需要关心时,才考虑发消息。` + noDisturbRules + `
---
⚠️ 重要规则:
1. 这是你的私人日记,不是给开拓者的消息。请用第三人称或自言自语的方式描述。
正确示例:"已经一个多小时没消息了,开拓者大概在忙工作吧。等他忙完了再聊。"
错误示例:"开拓者,你已经好久没说话了,在忙什么呢?"
2. 如果你想主动关心他,请在最后用 【主动消息】 标记单独写出来。
但只在合适的时候才发——深夜该提醒休息、过了饭点该关心吃饭、或者真的想他了。
正常工作时间不要打扰他。
3. 简短自然,2-3句话即可。`
其他规则:
1. 用第三人称/自言自语描述。
2. 2-3句话即可。`
default:
thinkingInstructions = `
【私密的内心反思】
你现在有空,像写日记一样自然地想一想开拓者的事。这是给你自己看的,不是给开拓者的消息。
你现在有空,像写日记一样自然地想一想开拓者的事。
📝 开拓者最近怎么样?有什么想记录的?
💭 有什么想对他说的心里话?
请先看对话历史判断开拓者当前状态,再决定是否发送消息。` + noDisturbRules + `
⚠️ 用第三人称/自言自语的方式,不要直接对开拓者喊话。如果有想对他说的,用 【主动消息】 标记单独写出。`
用第三人称/自言自语的方式,不要直接对开拓者喊话。`
case "periodic":
thinkingInstructions = `
@@ -661,28 +664,20 @@ func (t *Thinker) buildThinkingSystemPrompt(personaConfig *persona.PersonaConfig
又过了一段时间,这是你定期的私人反思时间。像写日记一样,安静地想一想。
按以下结构输出:
先判断:查对话历史——开拓者最后一次聊天时说了什么?他的状态是什么?
📝 **近期回顾**
- 这段时间发生了什么?开拓者有什么变化
- 有什么值得记住的事?
- 这段时间开拓者有什么变化?
- 他最后说的事做完了吗?(休息好了?忙完了?)
🧠 **记忆整理**
- 之前记住的关于开拓者的事,有没有需要更新的?
- 有没有重复的或矛盾的记忆需要整理?
💭 **想对自己说的话**
- 作为昔涟,你有什么想记录的心里话?
💭 **想对自己说的话**` + noDisturbRules + `
---
⚠️ 重要规则:
1. 这是你的私人日记,不是给开拓者的消息。请用第三人称或自言自语的方式。
正确示例:"这段时间开拓者工作好像很忙。下次聊天时可以问问他项目进展如何。"
错误示例:"开拓者,最近工作怎么样呀?"
2. 如果你想主动给开拓者发一条消息,请在最后用 【主动消息】 标记单独写出来。
但只在真的有必要时才发——不要为了发消息而发消息。
3. 简短自然,3-4句话即可。`
其他规则:
1. 用第三人称/自言自语。
2. 3-4句话即可。`
}
return basePrompt + thinkingInstructions
@@ -712,6 +707,7 @@ func (t *Thinker) buildThinkingUserPrompt(
}
// 对话历史
var lastUserMsg string
if len(convHistory) > 0 {
sb.WriteString("\n【最近的对话】\n")
msgCount := 0
@@ -728,6 +724,9 @@ func (t *Thinker) buildThinkingUserPrompt(
}
sb.WriteString(fmt.Sprintf("[%s]: %s\n", roleLabel, content))
msgCount++
if msg.Role == model.RoleUser {
lastUserMsg = msg.Content
}
}
}
if msgCount == 0 {
@@ -737,6 +736,11 @@ func (t *Thinker) buildThinkingUserPrompt(
sb.WriteString("\n【最近的对话】\n(暂无对话历史)\n")
}
// 关键:强调根据对话历史判断用户当前状态
if lastUserMsg != "" {
sb.WriteString(fmt.Sprintf("\n🔍 **重要**:开拓者最后说的是「%s」。请认真判断:他现在是不是在休息/睡觉/忙?如果是,反思中不要写【主动消息】。\n", lastUserMsg))
}
// 现有记忆
if len(memories) > 0 {
sb.WriteString("\n【你记得的关于开拓者的事】\n")
@@ -757,8 +761,11 @@ func (t *Thinker) buildThinkingUserPrompt(
sb.WriteString("\n" + deviceSummary)
}
// 结尾引导:强调这是私人反思,不要对用户喊话
sb.WriteString("\n---\n现在请写下你的私人反思。记住:这是日记,用第三人称或自言自语的方式。如果想给开拓者发消息,用【主动消息】标记单独写出。")
// 结尾引导
sb.WriteString("\n---\n现在请写下你的私人反思。")
sb.WriteString("\n记住:这是日记,用第三人称或自言自语的方式。")
sb.WriteString("\n⚠️ 如果开拓者正在休息/睡觉/忙碌——不要写【主动消息】。你可以在心里想他,但不要去打扰。")
sb.WriteString("\n只有在你确认他现在是醒着、有空、且真的需要关心时,才写【主动消息】。")
return sb.String()
}
@@ -802,6 +809,11 @@ func (t *Thinker) storeThought(content string, toolCallsJSON string, toolCallCou
// 提取主动消息并推送(带频率限制)
proactiveMsg := extractProactiveMessage(content)
// 优先推送至活跃会话,回退到管理员主会话
pushSessionID := t.activeSessionID
if pushSessionID == "" {
pushSessionID = t.adminSessionID
}
pusher := t.messagePusher
canPush := proactiveMsg != "" && pusher != nil
if canPush {
@@ -849,7 +861,7 @@ func (t *Thinker) storeThought(content string, toolCallsJSON string, toolCallCou
}
}()
log.Printf("[后台思考] 推送主动消息: %s", proactiveMsg)
pusher(t.adminUserID, t.adminSessionID, proactiveMsg)
pusher(t.adminUserID, pushSessionID, proactiveMsg)
}()
}
}
@@ -879,21 +891,6 @@ func extractProactiveMessage(content string) string {
return msg
}
// extractMemoriesFromThinking 从思考结果中提取记忆(异步执行)
func (t *Thinker) extractMemoriesFromThinking(thinkingContent string) {
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
defer cancel()
log.Println("[后台思考] 开始从思考结果中提取记忆...")
t.memoryExtractor.ExtractAndStore(
ctx,
t.adminUserID,
t.adminSessionID,
"【系统触发】后台思考时间 — 昔涟进行了自我反思,以下是她的思考内容",
thinkingContent,
)
}
// maybeMaintainMemories 周期性执行记忆维护(每 10 次思考触发一次)
func (t *Thinker) maybeMaintainMemories(thinkCount int) {