5325eaca3f
- 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>
201 lines
5.7 KiB
Go
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)
|
|
}
|
|
}
|