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,200 @@
|
||||
package background
|
||||
|
||||
import (
|
||||
"log"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// ProactiveDecision represents a decision about whether to send a proactive message.
|
||||
type ProactiveDecision struct {
|
||||
ShouldSend bool `json:"should_send"`
|
||||
Urgency string `json:"urgency"` // low, medium, high
|
||||
Reason string `json:"reason"`
|
||||
}
|
||||
|
||||
// ProactiveGuard evaluates whether a proactive message should be sent
|
||||
// based on time-of-day, urgency, rate limits, and user state.
|
||||
type ProactiveGuard struct {
|
||||
// Quiet hours: no non-urgent messages during this window
|
||||
QuietHoursStart int // 0-23, default 23
|
||||
QuietHoursEnd int // 0-23, default 7
|
||||
|
||||
// Min gap between proactive messages, by urgency
|
||||
MinGapByUrgency map[string]time.Duration
|
||||
|
||||
// Max proactive messages per hour
|
||||
MaxMessagesPerHour int
|
||||
|
||||
// Track recent send times for rate limiting
|
||||
recentSends []time.Time
|
||||
}
|
||||
|
||||
// DefaultProactiveGuard returns a guard with sensible defaults.
|
||||
func DefaultProactiveGuard() *ProactiveGuard {
|
||||
return &ProactiveGuard{
|
||||
QuietHoursStart: 23,
|
||||
QuietHoursEnd: 7,
|
||||
MinGapByUrgency: map[string]time.Duration{
|
||||
"low": 30 * time.Minute,
|
||||
"medium": 10 * time.Minute,
|
||||
"high": 2 * time.Minute,
|
||||
},
|
||||
MaxMessagesPerHour: 3,
|
||||
}
|
||||
}
|
||||
|
||||
// IsQuietHour returns true if the given time falls within quiet hours.
|
||||
func (g *ProactiveGuard) IsQuietHour(now time.Time) bool {
|
||||
hour := now.Hour()
|
||||
if g.QuietHoursStart < g.QuietHoursEnd {
|
||||
return hour >= g.QuietHoursStart && hour < g.QuietHoursEnd
|
||||
}
|
||||
// Overnight quiet hours (e.g., 23:00 - 07:00)
|
||||
return hour >= g.QuietHoursStart || hour < g.QuietHoursEnd
|
||||
}
|
||||
|
||||
// Evaluate checks whether a proactive message should be sent.
|
||||
func (g *ProactiveGuard) Evaluate(now time.Time, lastProactiveTime time.Time, urgency string, userState string) ProactiveDecision {
|
||||
// 1. Quiet hours: only high urgency messages pass
|
||||
if g.IsQuietHour(now) && urgency != "high" {
|
||||
return ProactiveDecision{
|
||||
ShouldSend: false,
|
||||
Urgency: urgency,
|
||||
Reason: "当前处于安静时段(23:00-07:00),仅紧急消息可推送",
|
||||
}
|
||||
}
|
||||
|
||||
// 2. User state check: don't disturb if user is resting/busy
|
||||
if userState == "resting" || userState == "busy" || userState == "sleeping" {
|
||||
if urgency != "high" {
|
||||
return ProactiveDecision{
|
||||
ShouldSend: false,
|
||||
Urgency: urgency,
|
||||
Reason: "开拓者正在休息/忙碌,不打扰",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Rate limit by urgency
|
||||
minGap, ok := g.MinGapByUrgency[urgency]
|
||||
if !ok {
|
||||
minGap = g.MinGapByUrgency["low"]
|
||||
}
|
||||
if !lastProactiveTime.IsZero() && now.Sub(lastProactiveTime) < minGap {
|
||||
return ProactiveDecision{
|
||||
ShouldSend: false,
|
||||
Urgency: urgency,
|
||||
Reason: "距上次主动消息时间过短(" + minGap.String() + " 最小间隔)",
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Hourly rate limit
|
||||
g.pruneOldSends(now)
|
||||
if len(g.recentSends) >= g.MaxMessagesPerHour {
|
||||
return ProactiveDecision{
|
||||
ShouldSend: false,
|
||||
Urgency: urgency,
|
||||
Reason: "本小时主动消息已达上限",
|
||||
}
|
||||
}
|
||||
|
||||
// 5. Content length validation (caller should also check)
|
||||
return ProactiveDecision{
|
||||
ShouldSend: true,
|
||||
Urgency: urgency,
|
||||
Reason: "",
|
||||
}
|
||||
}
|
||||
|
||||
// RecordSend records a proactive message send for rate limiting.
|
||||
func (g *ProactiveGuard) RecordSend(now time.Time) {
|
||||
g.recentSends = append(g.recentSends, now)
|
||||
g.pruneOldSends(now)
|
||||
}
|
||||
|
||||
// pruneOldSends removes sends older than 1 hour.
|
||||
func (g *ProactiveGuard) pruneOldSends(now time.Time) {
|
||||
cutoff := now.Add(-1 * time.Hour)
|
||||
valid := g.recentSends[:0]
|
||||
for _, t := range g.recentSends {
|
||||
if t.After(cutoff) {
|
||||
valid = append(valid, t)
|
||||
}
|
||||
}
|
||||
g.recentSends = valid
|
||||
}
|
||||
|
||||
// ExtractUrgencyFromContent tries to infer urgency from the proactive message content.
|
||||
func ExtractUrgencyFromContent(content string) string {
|
||||
lower := strings.ToLower(content)
|
||||
|
||||
// High urgency indicators
|
||||
highIndicators := []string{"紧急", "立刻", "马上", "危险", "警告", "报警", "异常", "urgent", "alert"}
|
||||
for _, kw := range highIndicators {
|
||||
if strings.Contains(lower, kw) {
|
||||
return "high"
|
||||
}
|
||||
}
|
||||
|
||||
// Medium urgency indicators
|
||||
mediumIndicators := []string{"建议", "提醒", "注意", "该", "要", "应该", "记得", "别忘了"}
|
||||
for _, kw := range mediumIndicators {
|
||||
if strings.Contains(lower, kw) {
|
||||
return "medium"
|
||||
}
|
||||
}
|
||||
|
||||
return "low"
|
||||
}
|
||||
|
||||
// ValidateProactiveMessage performs post-extraction validation on a message.
|
||||
func ValidateProactiveMessage(content string) (valid bool, reason string) {
|
||||
runes := []rune(content)
|
||||
if len(runes) == 0 {
|
||||
return false, "消息为空"
|
||||
}
|
||||
if len(runes) > 500 {
|
||||
return false, "消息过长(>500字符)"
|
||||
}
|
||||
|
||||
// Check for prohibited patterns (should not tell user they're resting when they're active)
|
||||
prohibited := []string{
|
||||
"系统检测到", "根据分析", "经检测", "后台监控",
|
||||
}
|
||||
for _, p := range prohibited {
|
||||
if strings.Contains(content, p) {
|
||||
return false, "包含机械语言: " + p
|
||||
}
|
||||
}
|
||||
|
||||
return true, ""
|
||||
}
|
||||
|
||||
// DetermineUserState checks conversation history for user state indicators.
|
||||
func DetermineUserState(lastUserMsg string) string {
|
||||
lower := strings.ToLower(lastUserMsg)
|
||||
restIndicators := []string{"睡", "休息", "躺", "困", "累", "晚安", "午安", "小憩"}
|
||||
busyIndicators := []string{"忙", "工作", "开会", "出去", "走了", "拜拜", "再见", "回头", "晚点"}
|
||||
|
||||
for _, kw := range restIndicators {
|
||||
if strings.Contains(lower, kw) {
|
||||
return "resting"
|
||||
}
|
||||
}
|
||||
for _, kw := range busyIndicators {
|
||||
if strings.Contains(lower, kw) {
|
||||
return "busy"
|
||||
}
|
||||
}
|
||||
return "active"
|
||||
}
|
||||
|
||||
// logDecision logs the proactive decision for debugging.
|
||||
func logDecision(d ProactiveDecision) {
|
||||
if d.ShouldSend {
|
||||
log.Printf("[主动消息决策] 允许推送 (紧急程度=%s)", d.Urgency)
|
||||
} else {
|
||||
log.Printf("[主动消息决策] 阻止推送 (紧急程度=%s, 原因=%s)", d.Urgency, d.Reason)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user