feat: 全链路优化 — 死锁修复、MD3主题、上下文持久化、群聊自然化、打字状态、知识库
**死锁根因修复** - periodicThinkLoop:1015 orphaned lock → 删除(字段已原子化) - RecordUserMessage 隔离为 recordMu - atomic.Int64 替换 lastUserMessage/lastThinkTime 等 **MD3 / Android 17 主题** - 毛玻璃卡片 (backdrop-filter) - MD3 色彩令牌 (pink primary #f472b6) - icons.js 独立矢量图标库 + 运行时 emoji 替换 - 无边框卡片、圆角按钮、阴影层次 **上下文持久化** - AddMessage → saveToDB 异步写 PostgreSQL - LoadFromDB 恢复 (admin-session-main + 懒加载) - LLMMessage.Timestamp 字段 **群聊与适配器** - group_ambient 模式: 非@消息让 LLM 自己判断是否插话 - 戳一戳动作消息总是回复 - NapCat 打字状态 (set_input_status, 最小3秒显示) - HTTP API 配置 (http_url/http_token) **知识库 & 防编造** - knowledge.CanHandle 对 chat 意图也触发 - 关键词预筛选避免无关 embedding 调用 - persona + synthesizer 三重诚实规则 - 工具结果持久化到会话历史 **平台桥接器** - detached:true Go进程独立存活 - ethend 重启自动接管已运行服务 - stop() 接管模式 taskkill/F/ PID - Windows netstat 替代 fuser 获取 PID - 重复适配器种子逻辑修复 - 失败转发日志 Direction: error **崩溃诊断** - crashlog 包 (Recover + WrapHTTP + LLMCall) - /api/v1/debug/goroutines 端点 - thinker 操作日志 + 30s stats - 日志写入 logs/ 目录持久化 Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -7,9 +7,11 @@ import (
|
||||
"log"
|
||||
"os"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
ctxbuild "git.yeij.top/AskaEth/Cyrene/ai-core/internal/context"
|
||||
@@ -124,8 +126,11 @@ func ParsePlatformChannels(raw string) []PlatformChannel {
|
||||
//
|
||||
// 主动消息:思考中如有【主动消息】标记,会通过 messagePusher 回调推送给在线用户(带频率限制)。
|
||||
type Thinker struct {
|
||||
mu sync.Mutex
|
||||
wg sync.WaitGroup
|
||||
mu sync.Mutex
|
||||
recordMu sync.Mutex
|
||||
muLockedBy string // debug: file:line of last Lock()
|
||||
muLockedAt time.Time // debug: when Lock() was acquired // RecordUserMessage专用,隔离于mu死锁
|
||||
wg sync.WaitGroup
|
||||
stopCh chan struct{}
|
||||
|
||||
enabled bool
|
||||
@@ -198,11 +203,11 @@ type Thinker struct {
|
||||
pendingThoughts []*PendingThought
|
||||
lastUserMessage time.Time
|
||||
lastThinkTime time.Time
|
||||
lastProactiveMsgTime time.Time
|
||||
lastProactiveMsgTime time.Time // deprecated, use lastProactiveTime()
|
||||
thinkCancel context.CancelFunc // 取消当前思考,让步于前台
|
||||
|
||||
// 思考计数器(用于周期性记忆维护,每 N 次思考触发一次)
|
||||
thinkCount int
|
||||
thinkCountAtomic atomic.Int64
|
||||
|
||||
// Phase 1 Step 4: 思考链 + 自主工具安全策略
|
||||
chain *ThinkChain
|
||||
@@ -218,7 +223,10 @@ type Thinker struct {
|
||||
scheduleLoader *ScheduleLoader
|
||||
|
||||
// Phase 2: 在线状态追踪
|
||||
userOnline bool
|
||||
userOnlineAtomic atomic.Bool
|
||||
lastUserMsgNs atomic.Int64 // UnixNano (replaces lastUserMessage)
|
||||
lastThinkNs atomic.Int64 // UnixNano (replaces lastThinkTime)
|
||||
lastProactiveNs atomic.Int64 // UnixNano (replaces lastProactiveMsgTime)
|
||||
lastOnlineChange time.Time
|
||||
userSessionID string // 当前活跃的 session ID (用于重连)
|
||||
|
||||
@@ -284,28 +292,28 @@ func timePeriod(now time.Time) (string, string) {
|
||||
// SetMessagePusher 设置主动消息推送回调
|
||||
// SetScheduleLoader sets the dynamic schedule loader for interval calculation.
|
||||
func (t *Thinker) SetScheduleLoader(loader *ScheduleLoader) {
|
||||
t.mu.Lock()
|
||||
defer t.mu.Unlock()
|
||||
t.muLock()
|
||||
defer t.muUnlock()
|
||||
t.scheduleLoader = loader
|
||||
}
|
||||
|
||||
func (t *Thinker) SetMessagePusher(pusher func(string, string, string)) {
|
||||
t.mu.Lock()
|
||||
defer t.mu.Unlock()
|
||||
t.muLock()
|
||||
defer t.muUnlock()
|
||||
t.messagePusher = pusher
|
||||
}
|
||||
|
||||
// SetPlatformMessagePusher sets the callback for pushing proactive messages to platform adapters (OBv11, etc.).
|
||||
func (t *Thinker) SetPlatformMessagePusher(pusher func(ProactiveTarget, string)) {
|
||||
t.mu.Lock()
|
||||
defer t.mu.Unlock()
|
||||
t.muLock()
|
||||
defer t.muUnlock()
|
||||
t.platformMessagePusher = pusher
|
||||
}
|
||||
|
||||
// SetBotUID sets the bot's own platform UID (e.g., OBv11 account).
|
||||
func (t *Thinker) SetBotUID(platform, uid string) {
|
||||
t.mu.Lock()
|
||||
defer t.mu.Unlock()
|
||||
t.muLock()
|
||||
defer t.muUnlock()
|
||||
if t.botUIDs == nil {
|
||||
t.botUIDs = make(map[string]string)
|
||||
}
|
||||
@@ -316,8 +324,8 @@ func (t *Thinker) SetBotUID(platform, uid string) {
|
||||
|
||||
// AddOrUpdatePlatformChannel adds or updates a platform channel with resolved display name.
|
||||
func (t *Thinker) AddOrUpdatePlatformChannel(platform, channelType, channelID, channelName, adapterID, adapterName string) {
|
||||
t.mu.Lock()
|
||||
defer t.mu.Unlock()
|
||||
t.muLock()
|
||||
defer t.muUnlock()
|
||||
|
||||
for i, ch := range t.platformChannels {
|
||||
if ch.Platform == platform && ch.ChannelType == channelType && ch.ChannelID == channelID {
|
||||
@@ -346,23 +354,23 @@ func (t *Thinker) AddOrUpdatePlatformChannel(platform, channelType, channelID, c
|
||||
|
||||
// SetEmotionTracker sets the emotion tracker.
|
||||
func (t *Thinker) SetEmotionTracker(et *persona.EmotionTracker) {
|
||||
t.mu.Lock()
|
||||
defer t.mu.Unlock()
|
||||
t.muLock()
|
||||
defer t.muUnlock()
|
||||
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.muLock()
|
||||
wasOffline := !t.isUserOnline()
|
||||
t.setUserOnline(online)
|
||||
t.lastOnlineChange = time.Now()
|
||||
if sessionID != "" {
|
||||
t.userSessionID = sessionID
|
||||
t.activeSessionID = sessionID
|
||||
}
|
||||
t.mu.Unlock()
|
||||
t.muUnlock()
|
||||
|
||||
if online && wasOffline {
|
||||
log.Printf("[后台思考] 用户上线 (session=%s),触发重连思考", sessionID)
|
||||
@@ -381,7 +389,7 @@ func (t *Thinker) UpdatePresence(online bool, sessionID string) {
|
||||
// TriggerReminderMessage pushes a reminder message generated by LLM to the user.
|
||||
// Called by the internal reminder-trigger endpoint when a reminder fires.
|
||||
func (t *Thinker) TriggerReminderMessage(userID, sessionID, message string) {
|
||||
t.mu.Lock()
|
||||
t.muLock()
|
||||
pusher := t.messagePusher
|
||||
pushSessionID := sessionID
|
||||
if pushSessionID == "" {
|
||||
@@ -390,7 +398,7 @@ func (t *Thinker) TriggerReminderMessage(userID, sessionID, message string) {
|
||||
if pushSessionID == "" {
|
||||
pushSessionID = t.adminSessionID
|
||||
}
|
||||
t.mu.Unlock()
|
||||
t.muUnlock()
|
||||
|
||||
if pusher != nil && message != "" {
|
||||
log.Printf("[提醒推送] 推送LLM提醒: user=%s session=%s msg=%s", userID, pushSessionID, message)
|
||||
@@ -400,9 +408,9 @@ func (t *Thinker) TriggerReminderMessage(userID, sessionID, message string) {
|
||||
|
||||
// PushPlatformMessage pushes a message to a platform channel via the platform pusher.
|
||||
func (t *Thinker) PushPlatformMessage(target ProactiveTarget, message string) {
|
||||
t.mu.Lock()
|
||||
t.muLock()
|
||||
pusher := t.platformMessagePusher
|
||||
t.mu.Unlock()
|
||||
t.muUnlock()
|
||||
if pusher != nil {
|
||||
log.Printf("[平台推送] target=%s/%s group=%s msg=%s", target.Platform, target.ChatType, target.GroupID, message)
|
||||
pusher(target, message)
|
||||
@@ -413,9 +421,9 @@ func (t *Thinker) PushPlatformMessage(target ProactiveTarget, message string) {
|
||||
|
||||
// IsUserRecentlyActive returns true if the user has been active within the given duration.
|
||||
func (t *Thinker) IsUserRecentlyActive(d time.Duration) bool {
|
||||
t.mu.Lock()
|
||||
defer t.mu.Unlock()
|
||||
return time.Since(t.lastUserMessage) < d
|
||||
t.muLock()
|
||||
defer t.muUnlock()
|
||||
return time.Since(t.lastUserTime()) < d
|
||||
}
|
||||
|
||||
|
||||
@@ -532,7 +540,7 @@ func (t *Thinker) restoreContext() {
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
defer func() { t.mu.Lock(); t.thinkCancel = nil; t.mu.Unlock() }()
|
||||
defer func() { t.muLock(); t.thinkCancel = nil; t.muUnlock() }()
|
||||
|
||||
memories, err := t.memClient.Query(ctx, model.MemoryQuery{
|
||||
UserID: t.adminUserID,
|
||||
@@ -551,9 +559,10 @@ func (t *Thinker) restoreContext() {
|
||||
}
|
||||
// Only use if reasonably recent (within 24h). Old memories don't represent user activity.
|
||||
if !latest.IsZero() && time.Since(latest) < 24*time.Hour {
|
||||
t.mu.Lock()
|
||||
t.muLock()
|
||||
t.lastUserMessage = latest
|
||||
t.mu.Unlock()
|
||||
t.setLastUser(latest)
|
||||
t.muUnlock()
|
||||
log.Printf("[后台思考] 上下文已恢复: 最近活动 %v 前", time.Since(latest).Round(time.Second))
|
||||
} else if !latest.IsZero() {
|
||||
log.Printf("[后台思考] 上下文恢复跳过: 最近记忆 %v 前(>24h),使用默认值", time.Since(latest).Round(time.Second))
|
||||
@@ -573,6 +582,10 @@ func (t *Thinker) Start() {
|
||||
|
||||
// 恢复上下文:从持久化存储查询最近活动时间,避免重启后"失忆"
|
||||
t.restoreContext()
|
||||
// 同步原子字段(restoreContext 写入 lastUserMessage 后需要同步)
|
||||
t.setLastUser(t.lastUserMessage)
|
||||
t.setLastThink(t.lastThinkTime)
|
||||
t.setLastProactive(t.lastProactiveMsgTime)
|
||||
|
||||
// 初始化静默检测定时器(但不启动,等第一次用户消息后启动)
|
||||
if t.silenceTimeout > 0 {
|
||||
@@ -628,14 +641,16 @@ func (t *Thinker) Stop() {
|
||||
// 2. 记录当前活跃的前端会话 ID(用于对话上下文检索和主动消息推送)
|
||||
// 3. 重置静默检测的一次性定时器(如果启用)
|
||||
func (t *Thinker) RecordUserMessage(sessionID string) {
|
||||
t.mu.Lock()
|
||||
t.lastUserMessage = time.Now()
|
||||
now := time.Now()
|
||||
t.recordMu.Lock()
|
||||
t.lastUserMessage = now
|
||||
t.setLastUser(now)
|
||||
if sessionID != "" {
|
||||
t.activeSessionID = sessionID
|
||||
}
|
||||
// 用户主动发消息时重置主动消息推送冷却——活跃对话中应允许昔涟回复
|
||||
t.lastProactiveMsgTime = time.Time{}
|
||||
t.mu.Unlock()
|
||||
t.setLastProactive(time.Time{})
|
||||
t.recordMu.Unlock()
|
||||
if t.thinkCancel != nil {
|
||||
t.thinkCancel()
|
||||
t.thinkCancel = nil
|
||||
@@ -656,12 +671,12 @@ func (t *Thinker) TriggerPostChatThink() {
|
||||
return
|
||||
}
|
||||
|
||||
t.mu.Lock()
|
||||
canThink := time.Since(t.lastThinkTime) >= t.minThinkGap
|
||||
t.mu.Unlock()
|
||||
t.muLock()
|
||||
canThink := time.Since(t.lastThinkTimeAtomic()) >= t.minThinkGap
|
||||
t.muUnlock()
|
||||
|
||||
if !canThink {
|
||||
log.Printf("[后台思考] 距上次思考仅 %v,跳过 (最小间隔=%v)", time.Since(t.lastThinkTime), t.minThinkGap)
|
||||
log.Printf("[后台思考] 距上次思考仅 %v,跳过 (最小间隔=%v)", time.Since(t.lastThinkTimeAtomic()), t.minThinkGap)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -725,10 +740,10 @@ func (t *Thinker) resetSilenceTimer() {
|
||||
return
|
||||
case <-t.silenceTimer.C:
|
||||
// 再次检查:用户是否真的沉默了足够久
|
||||
t.mu.Lock()
|
||||
silenceDuration := time.Since(t.lastUserMessage)
|
||||
canThink := time.Since(t.lastThinkTime) >= t.minThinkGap
|
||||
t.mu.Unlock()
|
||||
t.muLock()
|
||||
silenceDuration := time.Since(t.lastUserTime())
|
||||
canThink := time.Since(t.lastThinkTimeAtomic()) >= t.minThinkGap
|
||||
t.muUnlock()
|
||||
|
||||
if silenceDuration < t.silenceTimeout {
|
||||
log.Printf("[后台思考] 静默检测触发但用户已活动,跳过 (实际静默=%v)", silenceDuration)
|
||||
@@ -842,7 +857,7 @@ func (t *Thinker) performPlatformObservation() {
|
||||
}
|
||||
|
||||
observationContent := fmt.Sprintf("[平台观察 %s]\n%s", time.Now().In(t.timeLocation).Format("15:04"), result.Summary)
|
||||
t.mu.Lock()
|
||||
t.muLock()
|
||||
t.pendingThoughts = append(t.pendingThoughts, &PendingThought{
|
||||
Content: observationContent,
|
||||
CreatedAt: time.Now(),
|
||||
@@ -851,7 +866,7 @@ func (t *Thinker) performPlatformObservation() {
|
||||
if len(t.pendingThoughts) > 10 {
|
||||
t.pendingThoughts = t.pendingThoughts[len(t.pendingThoughts)-10:]
|
||||
}
|
||||
t.mu.Unlock()
|
||||
t.muUnlock()
|
||||
|
||||
log.Printf("[后台思考] 平台观察摘要已生成 (长度=%d, 需要关注=%v)", len(result.Summary), result.NeedsAttention)
|
||||
}
|
||||
@@ -874,10 +889,10 @@ func (t *Thinker) lightThinkLoop() {
|
||||
log.Println("[后台思考] 轻量思考已停止")
|
||||
return
|
||||
case <-time.After(t.lightThinkInterval):
|
||||
t.mu.Lock()
|
||||
sinceLastUser := time.Since(t.lastUserMessage)
|
||||
sinceLastThink := time.Since(t.lastThinkTime)
|
||||
t.mu.Unlock()
|
||||
t.muLock()
|
||||
sinceLastUser := time.Since(t.lastUserTime())
|
||||
sinceLastThink := time.Since(t.lastThinkTimeAtomic())
|
||||
t.muUnlock()
|
||||
|
||||
// Skip if user was active recently (last 30s).
|
||||
if sinceLastUser < 30*time.Second {
|
||||
@@ -895,6 +910,8 @@ func (t *Thinker) lightThinkLoop() {
|
||||
|
||||
// performLightThink runs a single lightweight thinking cycle using the fast model.
|
||||
func (t *Thinker) performLightThink() {
|
||||
log.Printf("[debug] lightThink ENTER")
|
||||
defer func() { log.Printf("[debug] lightThink EXIT") }()
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
|
||||
defer cancel()
|
||||
|
||||
@@ -933,9 +950,9 @@ func (t *Thinker) performLightThink() {
|
||||
// Check if deep think should be woken.
|
||||
if strings.Contains(content, "【需要深思】") {
|
||||
log.Println("[轻量思考] 检测到【需要深思】,唤醒深度思考...")
|
||||
t.mu.Lock()
|
||||
canDeep := time.Since(t.lastThinkTime) >= t.minThinkGap
|
||||
t.mu.Unlock()
|
||||
t.muLock()
|
||||
canDeep := time.Since(t.lastThinkTimeAtomic()) >= t.minThinkGap
|
||||
t.muUnlock()
|
||||
if canDeep {
|
||||
t.performThink("light_wake")
|
||||
} else {
|
||||
@@ -948,14 +965,14 @@ func (t *Thinker) performLightThink() {
|
||||
topic := strings.TrimSpace(content[idx+len("【话题发起】"):])
|
||||
if topic != "" {
|
||||
log.Printf("[轻量思考] 话题发起: %s", topic)
|
||||
t.mu.Lock()
|
||||
t.muLock()
|
||||
pusher := t.messagePusher
|
||||
sessionID := t.activeSessionID
|
||||
if sessionID == "" {
|
||||
sessionID = t.adminSessionID
|
||||
}
|
||||
canPush := time.Since(t.lastProactiveMsgTime) >= t.proactiveMsgMinGap
|
||||
t.mu.Unlock()
|
||||
canPush := time.Since(t.lastProactiveTime()) >= t.proactiveMsgMinGap
|
||||
t.muUnlock()
|
||||
if pusher != nil && canPush {
|
||||
go pusher(t.adminUserID, sessionID, topic)
|
||||
if t.convStore != nil && sessionID != "" {
|
||||
@@ -995,15 +1012,10 @@ func (t *Thinker) periodicThinkLoop() {
|
||||
log.Println("[后台思考] 周期性思考已停止")
|
||||
return
|
||||
case <-time.After(interval):
|
||||
t.mu.Lock()
|
||||
sinceLastThink := time.Since(t.lastThinkTime)
|
||||
sinceLastUser := time.Since(t.lastUserMessage)
|
||||
t.mu.Unlock()
|
||||
|
||||
// 离线时降低思考频率(可配置,默认 10 分钟)
|
||||
t.mu.Lock()
|
||||
isOffline := !t.userOnline
|
||||
t.mu.Unlock()
|
||||
// (atomic fields, no lock needed — all reads are lock-free)
|
||||
sinceLastThink := time.Since(t.lastThinkTimeAtomic())
|
||||
sinceLastUser := time.Since(t.lastUserTime())
|
||||
isOffline := !t.isUserOnline()
|
||||
offlineMinGap := t.offlineThinkGap
|
||||
|
||||
// 跳过条件:用户最近在活动(30s 内有消息),说明正在对话中
|
||||
@@ -1022,7 +1034,7 @@ func (t *Thinker) periodicThinkLoop() {
|
||||
continue
|
||||
}
|
||||
|
||||
log.Printf("[后台思考] 周期性触发 (间隔=%v, 上次思考=%v前, 上次用户消息=%v前)", interval, sinceLastThink.Round(time.Second), sinceLastUser.Round(time.Second))
|
||||
log.Printf("[debug] periodicThink TRIGGER interval=%v lastThink=%v lastUser=%v", interval, sinceLastThink.Round(time.Second), sinceLastUser.Round(time.Second))
|
||||
t.performThink("periodic")
|
||||
}
|
||||
}
|
||||
@@ -1030,8 +1042,8 @@ func (t *Thinker) periodicThinkLoop() {
|
||||
|
||||
// GetPendingThoughts 获取并消费所有待处理的后台思考
|
||||
func (t *Thinker) GetPendingThoughts() []*PendingThought {
|
||||
t.mu.Lock()
|
||||
defer t.mu.Unlock()
|
||||
t.muLock()
|
||||
defer t.muUnlock()
|
||||
|
||||
if len(t.pendingThoughts) == 0 {
|
||||
return nil
|
||||
@@ -1048,8 +1060,8 @@ func (t *Thinker) GetPendingThoughts() []*PendingThought {
|
||||
|
||||
// HasPendingThoughts 检查是否有待处理的思考
|
||||
func (t *Thinker) HasPendingThoughts() bool {
|
||||
t.mu.Lock()
|
||||
defer t.mu.Unlock()
|
||||
t.muLock()
|
||||
defer t.muUnlock()
|
||||
return len(t.pendingThoughts) > 0
|
||||
}
|
||||
|
||||
@@ -1060,34 +1072,35 @@ func (t *Thinker) HasPendingThoughts() bool {
|
||||
// 防御性速率限制:即使调用方未检查 minThinkGap,performThink 自身也会
|
||||
// 强制执行最小间隔,防止并发调用或 bug 导致 LLM 配额被快速消耗。
|
||||
func (t *Thinker) performThink(triggerReason string) {
|
||||
t.mu.Lock()
|
||||
gapSinceLast := time.Since(t.lastThinkTime)
|
||||
t.muLock()
|
||||
gapSinceLast := time.Since(t.lastThinkTimeAtomic())
|
||||
minGap := t.minThinkGap
|
||||
if minGap <= 0 {
|
||||
minGap = 5 * time.Second // 默认最小间隔 5 秒
|
||||
}
|
||||
if gapSinceLast < minGap {
|
||||
t.mu.Unlock()
|
||||
t.muUnlock()
|
||||
log.Printf("[后台思考] 距上次思考仅 %v,跳过 (最小间隔=%v, 触发原因=%s)", gapSinceLast.Round(time.Second), minGap, triggerReason)
|
||||
return
|
||||
}
|
||||
|
||||
t.lastThinkTime = time.Now()
|
||||
t.thinkCount++
|
||||
currentCount := t.thinkCount
|
||||
t.mu.Unlock()
|
||||
t.setLastThink(t.lastThinkTime)
|
||||
t.incThinkCount()
|
||||
currentCount := t.thinkCount()
|
||||
t.muUnlock()
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second)
|
||||
defer cancel()
|
||||
t.mu.Lock()
|
||||
t.muLock()
|
||||
t.thinkCancel = cancel
|
||||
t.mu.Unlock()
|
||||
t.muUnlock()
|
||||
|
||||
// 0. 让步于前台——如果用户最近有活动(非post_chat),跳过本次思考。
|
||||
if triggerReason != "post_chat" {
|
||||
t.mu.Lock()
|
||||
sinceLastUser := time.Since(t.lastUserMessage)
|
||||
t.mu.Unlock()
|
||||
t.muLock()
|
||||
sinceLastUser := time.Since(t.lastUserTime())
|
||||
t.muUnlock()
|
||||
if sinceLastUser < 10*time.Second {
|
||||
log.Printf("[后台思考] 用户 %v 前有活动,让步于前台回复,跳过思考", sinceLastUser.Round(time.Second))
|
||||
return
|
||||
@@ -1098,9 +1111,9 @@ func (t *Thinker) performThink(triggerReason string) {
|
||||
|
||||
// 0. 让步于前台——如果用户最近有活动(非post_chat),跳过本次思考。
|
||||
if triggerReason != "post_chat" {
|
||||
t.mu.Lock()
|
||||
sinceLastUser := time.Since(t.lastUserMessage)
|
||||
t.mu.Unlock()
|
||||
t.muLock()
|
||||
sinceLastUser := time.Since(t.lastUserTime())
|
||||
t.muUnlock()
|
||||
if sinceLastUser < 10*time.Second {
|
||||
log.Printf("[后台思考] 用户 %v 前有活动,让步于前台回复,跳过思考", sinceLastUser.Round(time.Second))
|
||||
return
|
||||
@@ -1117,12 +1130,12 @@ func (t *Thinker) performThink(triggerReason string) {
|
||||
// 2. 获取当前活跃会话的对话历史(优先活跃会话,回退到管理员主会话)
|
||||
var convHistory []model.LLMMessage
|
||||
if t.convStore != nil {
|
||||
t.mu.Lock()
|
||||
t.muLock()
|
||||
sessionID := t.activeSessionID
|
||||
if sessionID == "" {
|
||||
sessionID = t.adminSessionID
|
||||
}
|
||||
t.mu.Unlock()
|
||||
t.muUnlock()
|
||||
if sessionID != "" {
|
||||
convHistory = t.convStore.GetHistory(sessionID, 30)
|
||||
if len(convHistory) > 0 {
|
||||
@@ -1234,14 +1247,14 @@ func (t *Thinker) performThink(triggerReason string) {
|
||||
// 4.5 获取最近平台观察(定期触发和对话后触发时注入)
|
||||
var platformObservation string
|
||||
if triggerReason == "periodic" || triggerReason == "post_chat" {
|
||||
t.mu.Lock()
|
||||
t.muLock()
|
||||
for i := len(t.pendingThoughts) - 1; i >= 0; i-- {
|
||||
if strings.HasPrefix(t.pendingThoughts[i].Content, "[平台观察") {
|
||||
platformObservation = t.pendingThoughts[i].Content
|
||||
break
|
||||
}
|
||||
}
|
||||
t.mu.Unlock()
|
||||
t.muUnlock()
|
||||
}
|
||||
|
||||
// 5. 构建思考提示词(根据触发原因调整)
|
||||
@@ -1384,7 +1397,7 @@ func (t *Thinker) performThink(triggerReason string) {
|
||||
if len(parts) == 2 {
|
||||
adapterName, groupID := parts[0], parts[1]
|
||||
// Find the channel to get the correct platform type.
|
||||
t.mu.Lock()
|
||||
t.muLock()
|
||||
var platform string
|
||||
for _, ch := range t.platformChannels {
|
||||
if ch.AdapterName == adapterName && ch.ChannelID == groupID && ch.ChannelType == "group" {
|
||||
@@ -1392,7 +1405,7 @@ func (t *Thinker) performThink(triggerReason string) {
|
||||
break
|
||||
}
|
||||
}
|
||||
t.mu.Unlock()
|
||||
t.muUnlock()
|
||||
if platform == "" {
|
||||
platform = "obv11" // fallback
|
||||
}
|
||||
@@ -1466,7 +1479,7 @@ func (t *Thinker) performThink(triggerReason string) {
|
||||
log.Printf("[后台思考] 完成 (触发原因=%s, 轮数=%d, 内容长度=%d, 工具调用=%d次)", triggerReason, len(allContents), len(finalContent), totalToolCalls)
|
||||
|
||||
// 9. 记忆维护:机械合并(每10次) + LLM整理(每次)
|
||||
t.maybeMaintainMemories(currentCount)
|
||||
t.maybeMaintainMemories(int(currentCount))
|
||||
t.performMemoryConsolidation(ctx)
|
||||
}
|
||||
|
||||
@@ -1637,9 +1650,9 @@ func (t *Thinker) buildThinkingUserPrompt(
|
||||
case "post_chat":
|
||||
sb.WriteString("刚有人和你聊完天。你想自然地在心里回味一下刚才的对话……\n")
|
||||
case "silence":
|
||||
t.mu.Lock()
|
||||
silenceDuration := time.Since(t.lastUserMessage)
|
||||
t.mu.Unlock()
|
||||
t.muLock()
|
||||
silenceDuration := time.Since(t.lastUserTime())
|
||||
t.muUnlock()
|
||||
sb.WriteString(fmt.Sprintf("已经大约 %s 没有说话了。你有点想知道大家在做什么……\n",
|
||||
formatDurationHuman(silenceDuration)))
|
||||
default:
|
||||
@@ -1736,12 +1749,12 @@ func (t *Thinker) buildThinkingUserPrompt(
|
||||
}
|
||||
|
||||
// OBv11 platform identity and available channels for proactive messaging.
|
||||
t.mu.Lock()
|
||||
t.muLock()
|
||||
qqChannels := t.platformChannels
|
||||
botUIDs := t.botUIDs
|
||||
activeSID := t.activeSessionID
|
||||
lastMsgTime := t.lastUserMessage
|
||||
t.mu.Unlock()
|
||||
lastMsgTime := t.lastUserTime()
|
||||
t.muUnlock()
|
||||
if len(qqChannels) > 0 {
|
||||
sb.WriteString("\n\n【你的平台身份与可用频道】\n")
|
||||
|
||||
@@ -1884,7 +1897,7 @@ func (t *Thinker) buildOpenAITools() []llm.OpenAITool {
|
||||
|
||||
// storeThought 存储思考结果到待推送队列,并异步持久化到 memory-service
|
||||
func (t *Thinker) storeThought(content string, toolCallsJSON string, toolCallCount int) {
|
||||
t.mu.Lock()
|
||||
t.muLock()
|
||||
t.pendingThoughts = append(t.pendingThoughts, &PendingThought{
|
||||
Content: content,
|
||||
CreatedAt: time.Now(),
|
||||
@@ -1915,21 +1928,23 @@ func (t *Thinker) storeThought(content string, toolCallsJSON string, toolCallCou
|
||||
canPush = false
|
||||
}
|
||||
if canPush && t.proactiveGuard != nil {
|
||||
decision := t.proactiveGuard.Evaluate(time.Now(), t.lastProactiveMsgTime, urgency, "active")
|
||||
decision := t.proactiveGuard.Evaluate(time.Now(), t.lastProactiveTime(), urgency, "active")
|
||||
logDecision(decision)
|
||||
if !decision.ShouldSend {
|
||||
canPush = false
|
||||
} else {
|
||||
t.lastProactiveMsgTime = time.Now()
|
||||
t.setLastProactive(t.lastProactiveMsgTime)
|
||||
t.proactiveGuard.RecordSend(time.Now())
|
||||
}
|
||||
} else if canPush {
|
||||
gapSinceLast := time.Since(t.lastProactiveMsgTime)
|
||||
gapSinceLast := time.Since(t.lastProactiveTime())
|
||||
if gapSinceLast < 30*time.Minute {
|
||||
log.Printf("[后台思考] 主动消息距上次仅 %v,跳过推送", gapSinceLast.Round(time.Second))
|
||||
canPush = false
|
||||
} else {
|
||||
t.lastProactiveMsgTime = time.Now()
|
||||
t.setLastProactive(t.lastProactiveMsgTime)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1939,7 +1954,7 @@ func (t *Thinker) storeThought(content string, toolCallsJSON string, toolCallCou
|
||||
copy := *proactiveTarget
|
||||
targetCopy = ©
|
||||
}
|
||||
t.mu.Unlock()
|
||||
t.muUnlock()
|
||||
|
||||
log.Printf("[后台思考] 思考已存储 (当前累积 %d 条待推送思考)", len(t.pendingThoughts))
|
||||
|
||||
@@ -2037,7 +2052,7 @@ func (t *Thinker) extractProactiveMessage(content string) (string, *ProactiveTar
|
||||
channelID = "private_" + m[2]
|
||||
}
|
||||
adapterName := platform // fallback to format key
|
||||
t.mu.Lock()
|
||||
t.muLock()
|
||||
for _, ch := range t.platformChannels {
|
||||
if ch.Platform == platform && ch.ChannelType == chatType && ch.ChannelID == channelID {
|
||||
if ch.AdapterName != "" {
|
||||
@@ -2046,7 +2061,7 @@ func (t *Thinker) extractProactiveMessage(content string) (string, *ProactiveTar
|
||||
break
|
||||
}
|
||||
}
|
||||
t.mu.Unlock()
|
||||
t.muUnlock()
|
||||
|
||||
target = &ProactiveTarget{
|
||||
Platform: adapterName,
|
||||
@@ -2594,3 +2609,53 @@ func getEnvDuration(key string, fallbackSec int) time.Duration {
|
||||
}
|
||||
return time.Duration(sec) * time.Second
|
||||
}
|
||||
|
||||
// muLock acquires t.mu with caller tracking.
|
||||
func (t *Thinker) muLock() {
|
||||
t.mu.Lock()
|
||||
_, file, line, _ := runtime.Caller(1)
|
||||
if idx := strings.LastIndex(file, "Cyrene/"); idx >= 0 {
|
||||
file = file[idx+len("Cyrene/"):]
|
||||
}
|
||||
t.muLockedBy = fmt.Sprintf("%s:%d", file, line)
|
||||
t.muLockedAt = time.Now()
|
||||
}
|
||||
|
||||
// muUnlock releases t.mu and clears the tracking.
|
||||
func (t *Thinker) muUnlock() {
|
||||
t.muLockedBy = ""
|
||||
t.mu.Unlock()
|
||||
}
|
||||
|
||||
// ========== lock-free atomic accessors ==========
|
||||
func (t *Thinker) lastUserTime() time.Time { return time.Unix(0, t.lastUserMsgNs.Load()) }
|
||||
func (t *Thinker) setLastUser(ts time.Time) { t.lastUserMsgNs.Store(ts.UnixNano()) }
|
||||
func (t *Thinker) lastThinkTimeAtomic() time.Time { return time.Unix(0, t.lastThinkNs.Load()) }
|
||||
func (t *Thinker) setLastThink(ts time.Time) { t.lastThinkNs.Store(ts.UnixNano()) }
|
||||
func (t *Thinker) thinkCount() int64 { return t.thinkCountAtomic.Add(0) }
|
||||
func (t *Thinker) incThinkCount() int64 { return t.thinkCountAtomic.Add(1) }
|
||||
func (t *Thinker) isUserOnline() bool { return t.userOnlineAtomic.Load() }
|
||||
func (t *Thinker) setUserOnline(v bool) { t.userOnlineAtomic.Store(v) }
|
||||
func (t *Thinker) lastProactiveTime() time.Time { return time.Unix(0, t.lastProactiveNs.Load()) }
|
||||
func (t *Thinker) setLastProactive(ts time.Time) { t.lastProactiveNs.Store(ts.UnixNano()) }
|
||||
|
||||
// DeadlockDetected tries to acquire t.mu with a timeout. Returns true if the lock appears orphaned.
|
||||
func (t *Thinker) DeadlockDetected(timeout time.Duration) bool {
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
t.muLock()
|
||||
t.muUnlock()
|
||||
close(done)
|
||||
}()
|
||||
select {
|
||||
case <-done:
|
||||
return false
|
||||
case <-time.After(timeout):
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// LockHolder returns info about who currently holds t.mu (for debugging).
|
||||
func (t *Thinker) LockHolder() (string, time.Time) {
|
||||
return t.muLockedBy, t.muLockedAt
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user