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
+183 -26
View File
@@ -99,6 +99,43 @@ type Thinker struct {
// 思考计数器(用于周期性记忆维护,每 N 次思考触发一次)
thinkCount int
// Phase 1 Step 4: 思考链 + 自主工具安全策略
chain *ThinkChain
autoToolPolicy *AutonomousToolPolicy
// Phase 2: 情感追踪
emotionTracker *persona.EmotionTracker
// Phase 2: 主动消息决策守卫
proactiveGuard *ProactiveGuard
// Phase 2: 在线状态追踪
userOnline bool
lastOnlineChange time.Time
userSessionID string // 当前活跃的 session ID (用于重连)
}
// AutonomousToolPolicy 自主思考工具调用安全策略
type AutonomousToolPolicy struct {
// 允许在自主思考中使用的工具白名单
AllowedTools []string // iot_query, memory_search, web_search, calculator, datetime
// 每轮最大工具调用次数
MaxToolCallsPerRound int // 默认 5
// 高风险操作每小时最大次数 (如 iot_control)
MaxHighRiskPerHour int // 默认 10
// 高风险工具列表
HighRiskTools []string // iot_control
}
// DefaultAutonomousToolPolicy 默认安全策略
func DefaultAutonomousToolPolicy() *AutonomousToolPolicy {
return &AutonomousToolPolicy{
AllowedTools: []string{"iot_query", "iot_control", "memory_search", "web_search", "calculator", "datetime", "web_fetch"},
MaxToolCallsPerRound: 5,
MaxHighRiskPerHour: 10,
HighRiskTools: []string{"iot_control"},
}
}
// SetMessagePusher 设置主动消息推送回调
@@ -108,6 +145,40 @@ func (t *Thinker) SetMessagePusher(pusher func(string, string, string)) {
t.messagePusher = pusher
}
// SetEmotionTracker sets the emotion tracker.
func (t *Thinker) SetEmotionTracker(et *persona.EmotionTracker) {
t.mu.Lock()
defer t.mu.Unlock()
t.emotionTracker = et
}
// UpdatePresence updates the user online status.
// Called by the ai-core presence endpoint when gateway detects connect/disconnect.
func (t *Thinker) UpdatePresence(online bool, sessionID string) {
t.mu.Lock()
wasOffline := !t.userOnline
t.userOnline = online
t.lastOnlineChange = time.Now()
if sessionID != "" {
t.userSessionID = sessionID
t.activeSessionID = sessionID
}
t.mu.Unlock()
if online && wasOffline {
log.Printf("[后台思考] 用户上线 (session=%s),触发重连思考", sessionID)
// Trigger a return-thinking cycle after a short delay
time.Sleep(2 * time.Second)
t.performThink("user_returned")
// Also update emotion tracker
if t.emotionTracker != nil {
t.emotionTracker.UpdateMood("user_returned")
}
} else if !online {
log.Printf("[后台思考] 用户离线")
}
}
// ThinkerConfig 后台思考配置
type ThinkerConfig struct {
Enabled bool
@@ -171,6 +242,9 @@ func NewThinker(
pendingThoughts: make([]*PendingThought, 0),
lastUserMessage: time.Now(),
stopCh: make(chan struct{}),
chain: NewThinkChain(10),
autoToolPolicy: DefaultAutonomousToolPolicy(),
proactiveGuard: DefaultProactiveGuard(),
}
}
@@ -362,16 +436,27 @@ func (t *Thinker) periodicThinkLoop() {
sinceLastUser := time.Since(t.lastUserMessage)
t.mu.Unlock()
// 跳过条件:用户最近在活动(30s 内有消息),说明正在对话中
if sinceLastUser < 30*time.Second {
log.Printf("[后台思考] 用户在 %v 前发过消息,跳过周期性触发 (留给事件驱动处理)", sinceLastUser.Round(time.Second))
continue
}
// Phase 2: 离线时降低思考频率 (每30分钟一次,而非5分钟)
t.mu.Lock()
isOffline := !t.userOnline
t.mu.Unlock()
offlineMinGap := 30 * time.Minute
if sinceLastThink < t.minThinkGap {
log.Printf("[后台思考] 距上次思考仅 %v,跳过周期性触发", sinceLastThink.Round(time.Second))
continue
}
// 跳过条件:用户最近在活动(30s 内有消息),说明正在对话中
if sinceLastUser < 30*time.Second {
log.Printf("[后台思考] 用户在 %v 前发过消息,跳过周期性触发 (留给事件驱动处理)", sinceLastUser.Round(time.Second))
continue
}
if isOffline && sinceLastThink < offlineMinGap {
log.Printf("[后台思考] 用户离线,距上次思考仅 %v,跳过 (离线模式最小间隔=%v)", sinceLastThink.Round(time.Second), offlineMinGap)
continue
}
if !isOffline && sinceLastThink < t.minThinkGap {
log.Printf("[后台思考] 距上次思考仅 %v,跳过周期性触发", sinceLastThink.Round(time.Second))
continue
}
log.Printf("[后台思考] 周期性触发 (上次思考=%v前, 上次用户消息=%v前)", sinceLastThink.Round(time.Second), sinceLastUser.Round(time.Second))
t.performThink("periodic")
@@ -484,11 +569,11 @@ func (t *Thinker) performThink(triggerReason string) {
{Role: model.RoleUser, Content: userPrompt},
}
// 6. 准备工具定义
openAITools := t.buildOpenAITools()
// 6. 准备工具定义(通过自主工具策略过滤)
openAITools := t.filterToolsByPolicy(t.buildOpenAITools())
// 7. 调用 LLM(支持工具调用,最多 3 轮
maxToolRounds := 3
// 7. 调用 LLM(支持工具调用,策略限制轮数
maxToolRounds := t.autoToolPolicy.MaxToolCallsPerRound
var finalContent string
var totalToolCalls int
var toolCallRecords []map[string]interface{}
@@ -569,6 +654,21 @@ func (t *Thinker) performThink(triggerReason string) {
// 8. 存储思考结果
t.storeThought(finalContent, toolCallsJSON, totalToolCalls)
// 8.5 记录到思考链
if t.chain != nil {
conclusions, followUps := extractConclusions(finalContent)
t.chain.Add(ThinkRecord{
ID: generateID(),
Content: finalContent,
Conclusions: conclusions,
FollowUps: followUps,
ToolCalls: totalToolCalls,
Trigger: triggerReason,
Timestamp: time.Now(),
})
log.Printf("[后台思考] 思考链已记录 (序号=%d, 结论数=%d, 后续问题=%d)", t.chain.Size(), len(conclusions), len(followUps))
}
log.Printf("[后台思考] 完成 (触发原因=%s, 内容长度=%d, 工具调用=%d次)", triggerReason, len(finalContent), totalToolCalls)
// 9. 周期性记忆维护(每 10 次思考触发一次)
@@ -582,7 +682,11 @@ func (t *Thinker) performThink(triggerReason string) {
// 关键改动:不再是"定期的自我反思",而是"自然的、人性化的内在想法"。
// triggerReason 影响提示词的侧重点。
func (t *Thinker) buildThinkingSystemPrompt(personaConfig *persona.PersonaConfig, triggerReason string) string {
basePrompt := personaConfig.BuildSystemPrompt("开拓者", 1)
mood, moodExpr, _ := "", "", 0.0
if t.emotionTracker != nil {
mood, moodExpr, _ = t.emotionTracker.GetCurrentMood()
}
basePrompt := personaConfig.BuildSystemPromptWithMood("开拓者", 1, mood, moodExpr)
var thinkingInstructions string
@@ -620,9 +724,10 @@ func (t *Thinker) buildThinkingSystemPrompt(personaConfig *persona.PersonaConfig
- 如果他状态正常——有没有想在下次对话中聊的话题?` + noDisturbRules + `
其他规则:
1. 用第三人称或自言自语的方式,不要直接对开拓者喊话。
1. 反思部分用第三人称或自言自语的方式,不要直接对开拓者喊话。
2. 只有开拓者状态正常且真的有必要时才写【主动消息】,不要硬找话题。
3. 2-4句话即可。`
3. 【主动消息】的内容必须直接对开拓者说话(用"你"称呼他),像主动找他聊天一样。反思是给自己看的,主动消息是发给他的——语气要区分开。
4. 2-4句话即可。`
case "silence":
thinkingInstructions = `
@@ -756,6 +861,24 @@ func (t *Thinker) buildThinkingUserPrompt(
sb.WriteString("\n【你记得的关于开拓者的事】\n(暂无相关记忆)\n")
}
// 思考链:注入上一轮的结论和待续问题
if t.chain != nil {
lastConclusions := t.chain.LastConclusions(3)
if len(lastConclusions) > 0 {
sb.WriteString("\n【你上一轮思考的结论】\n")
for _, c := range lastConclusions {
sb.WriteString(fmt.Sprintf("- %s\n", c))
}
}
lastFollowUps := t.chain.LastFollowUps()
if len(lastFollowUps) > 0 {
sb.WriteString("\n【你上次想继续思考的问题】\n")
for _, f := range lastFollowUps {
sb.WriteString(fmt.Sprintf("- %s\n", f))
}
}
}
// IoT 设备状态
if deviceSummary != "" {
sb.WriteString("\n" + deviceSummary)
@@ -766,10 +889,32 @@ func (t *Thinker) buildThinkingUserPrompt(
sb.WriteString("\n记住:这是日记,用第三人称或自言自语的方式。")
sb.WriteString("\n⚠️ 如果开拓者正在休息/睡觉/忙碌——不要写【主动消息】。你可以在心里想他,但不要去打扰。")
sb.WriteString("\n只有在你确认他现在是醒着、有空、且真的需要关心时,才写【主动消息】。")
sb.WriteString("\n❗【主动消息】的内容必须直接对开拓者说话(用\"你\"来称呼他),就像你主动找他聊天一样自然。不要用第三人称或自言自语的方式写主动消息。")
return sb.String()
}
// filterToolsByPolicy 通过自主工具安全策略过滤工具列表
func (t *Thinker) filterToolsByPolicy(tools []llm.OpenAITool) []llm.OpenAITool {
if t.autoToolPolicy == nil || len(tools) == 0 {
return tools
}
allowed := make(map[string]bool)
for _, name := range t.autoToolPolicy.AllowedTools {
allowed[name] = true
}
var filtered []llm.OpenAITool
for _, tool := range tools {
if allowed[tool.Function.Name] {
filtered = append(filtered, tool)
}
}
if len(filtered) < len(tools) {
log.Printf("[后台思考] 工具策略过滤: %d/%d 工具可用", len(filtered), len(tools))
}
return filtered
}
// buildOpenAITools 将工具注册中心的定义转换为 LLM 层的 OpenAITool 格式
func (t *Thinker) buildOpenAITools() []llm.OpenAITool {
if t.toolRegistry == nil || !t.toolRegistry.IsEnabled() {
@@ -817,17 +962,29 @@ func (t *Thinker) storeThought(content string, toolCallsJSON string, toolCallCou
pusher := t.messagePusher
canPush := proactiveMsg != "" && pusher != nil
if canPush {
// 检查频率限制
gapSinceLast := time.Since(t.lastProactiveMsgTime)
minGap := t.proactiveMsgMinGap
if minGap <= 0 {
minGap = 30 * time.Minute
}
if gapSinceLast < minGap {
log.Printf("[后台思考] 主动消息距上次仅 %v,跳过推送 (最小间隔=%v)", gapSinceLast.Round(time.Second), minGap)
// Phase 2: 使用 ProactiveGuard 多维度评估
urgency := ExtractUrgencyFromContent(proactiveMsg)
if valid, reason := ValidateProactiveMessage(proactiveMsg); !valid {
log.Printf("[后台思考] 主动消息内容校验失败: %s,跳过推送", reason)
canPush = false
} else {
t.lastProactiveMsgTime = time.Now()
}
if canPush && t.proactiveGuard != nil {
decision := t.proactiveGuard.Evaluate(time.Now(), t.lastProactiveMsgTime, urgency, "active")
logDecision(decision)
if !decision.ShouldSend {
canPush = false
} else {
t.lastProactiveMsgTime = time.Now()
t.proactiveGuard.RecordSend(time.Now())
}
} else if canPush {
gapSinceLast := time.Since(t.lastProactiveMsgTime)
if gapSinceLast < 30*time.Minute {
log.Printf("[后台思考] 主动消息距上次仅 %v,跳过推送", gapSinceLast.Round(time.Second))
canPush = false
} else {
t.lastProactiveMsgTime = time.Now()
}
}
}
t.mu.Unlock()