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:
@@ -0,0 +1,165 @@
|
||||
package background
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// ThinkRecord is a single thinking session's result.
|
||||
type ThinkRecord struct {
|
||||
ID string `json:"id"`
|
||||
Content string `json:"content"`
|
||||
Conclusions []string `json:"conclusions"` // key takeaways
|
||||
FollowUps []string `json:"follow_ups"` // questions to continue
|
||||
ToolCalls int `json:"tool_calls"`
|
||||
Trigger string `json:"trigger"` // post_chat, silence, periodic
|
||||
Timestamp time.Time `json:"timestamp"`
|
||||
}
|
||||
|
||||
// ThinkChain stores linked thinking records so each round
|
||||
// can build on previous conclusions.
|
||||
type ThinkChain struct {
|
||||
mu sync.Mutex
|
||||
records []ThinkRecord
|
||||
maxSize int
|
||||
}
|
||||
|
||||
// NewThinkChain creates a think chain with the given max size.
|
||||
func NewThinkChain(maxSize int) *ThinkChain {
|
||||
if maxSize <= 0 {
|
||||
maxSize = 10
|
||||
}
|
||||
return &ThinkChain{
|
||||
records: make([]ThinkRecord, 0, maxSize),
|
||||
maxSize: maxSize,
|
||||
}
|
||||
}
|
||||
|
||||
// Add appends a new think record, evicting oldest if at capacity.
|
||||
func (c *ThinkChain) Add(r ThinkRecord) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
if len(c.records) >= c.maxSize {
|
||||
c.records = c.records[1:]
|
||||
}
|
||||
c.records = append(c.records, r)
|
||||
}
|
||||
|
||||
// LastConclusions returns conclusions from the most recent N records.
|
||||
func (c *ThinkChain) LastConclusions(n int) []string {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
var result []string
|
||||
start := len(c.records) - n
|
||||
if start < 0 {
|
||||
start = 0
|
||||
}
|
||||
for _, r := range c.records[start:] {
|
||||
result = append(result, r.Conclusions...)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// LastFollowUps returns follow-up questions from the single most recent record.
|
||||
func (c *ThinkChain) LastFollowUps() []string {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
if len(c.records) == 0 {
|
||||
return nil
|
||||
}
|
||||
return c.records[len(c.records)-1].FollowUps
|
||||
}
|
||||
|
||||
// LastTopic attempts to infer a topic from recent conclusions.
|
||||
func (c *ThinkChain) LastTopic() string {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
if len(c.records) == 0 {
|
||||
return ""
|
||||
}
|
||||
// Use first conclusion line of the most recent record as topic
|
||||
for _, r := range c.records {
|
||||
for _, c := range r.Conclusions {
|
||||
if c != "" {
|
||||
runes := []rune(c)
|
||||
if len(runes) > 50 {
|
||||
return string(runes[:50]) + "..."
|
||||
}
|
||||
return c
|
||||
}
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// Size returns the current number of records in the chain.
|
||||
func (c *ThinkChain) Size() int {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
return len(c.records)
|
||||
}
|
||||
|
||||
// extractConclusions parses the LLM thinking output to find conclusions and follow-ups.
|
||||
// Looks for "结论" / "后续" markers in the content.
|
||||
func extractConclusions(content string) (conclusions []string, followUps []string) {
|
||||
lines := strings.Split(content, "\n")
|
||||
inConclusions := false
|
||||
inFollowUps := false
|
||||
|
||||
for _, line := range lines {
|
||||
line = strings.TrimSpace(line)
|
||||
|
||||
if strings.Contains(line, "结论") && (strings.Contains(line, "💭") || strings.Contains(line, "📝") || strings.HasPrefix(line, "-")) {
|
||||
// Heuristic: this line starts a conclusions section
|
||||
}
|
||||
|
||||
// Match bullet-point conclusions: lines starting with - or •
|
||||
if (strings.HasPrefix(line, "- ") || strings.HasPrefix(line, "• ")) && !inFollowUps {
|
||||
text := strings.TrimPrefix(line, "- ")
|
||||
text = strings.TrimPrefix(text, "• ")
|
||||
text = strings.TrimSpace(text)
|
||||
if text != "" && len([]rune(text)) > 2 {
|
||||
if inConclusions {
|
||||
conclusions = append(conclusions, text)
|
||||
} else {
|
||||
// Without explicit marker, treat all bullets as conclusions
|
||||
conclusions = append(conclusions, text)
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// Detect section transitions
|
||||
if strings.Contains(line, "后续") || strings.Contains(line, "继续思考") || strings.Contains(line, "下次") {
|
||||
inFollowUps = true
|
||||
inConclusions = false
|
||||
continue
|
||||
}
|
||||
if strings.Contains(line, "结论") || strings.Contains(line, "观察") || strings.Contains(line, "记忆") {
|
||||
inConclusions = true
|
||||
inFollowUps = false
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// If no structured markers found, treat the whole content as a single conclusion
|
||||
if len(conclusions) == 0 {
|
||||
runes := []rune(content)
|
||||
if len(runes) > 200 {
|
||||
content = string(runes[:200]) + "..."
|
||||
}
|
||||
conclusions = []string{content}
|
||||
}
|
||||
|
||||
return conclusions, followUps
|
||||
}
|
||||
|
||||
// generateID generates a short random ID.
|
||||
func generateID() string {
|
||||
b := make([]byte, 6)
|
||||
rand.Read(b)
|
||||
return fmt.Sprintf("th-%x", b)
|
||||
}
|
||||
Reference in New Issue
Block a user