Files
Cyrene/backend/ai-core/internal/background/proactive_decision.go
T
AskaEth 5325eaca3f fix: 后台思考使用深度思考模型 + 主动消息推送冷却优化
- thinker.go: Round 0 优先调用 llmAdapter(deepseek-v4-pro),失败回退 toolAdapter
- thinker.go: RecordUserMessage 重置 lastProactiveMsgTime,活跃对话中允许推送
- proactive_decision.go: MinGap low 30→15min, medium 10→5min, high 2→1min; 小时上限 3→5

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-24 17:10:28 +08:00

201 lines
5.7 KiB
Go

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": 15 * time.Minute,
"medium": 5 * time.Minute,
"high": 1 * time.Minute,
},
MaxMessagesPerHour: 5,
}
}
// 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)
}
}