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:
2026-06-28 11:29:29 +08:00
parent 0d6970a2d3
commit fd44b15d81
23 changed files with 1447 additions and 254 deletions
+167 -102
View File
@@ -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 {
// 防御性速率限制:即使调用方未检查 minThinkGapperformThink 自身也会
// 强制执行最小间隔,防止并发调用或 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 = &copy
}
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
}
+31 -2
View File
@@ -6,6 +6,7 @@ import (
"fmt"
"strings"
"sync"
"time"
_ "github.com/lib/pq"
@@ -65,6 +66,33 @@ func (cs *ConversationStore) AddMessage(sessionID string, msg model.LLMMessage)
}
}
cs.messages[sessionID] = msgs
// 异步写 DB 确保重启后上下文不丢失
if cs.databaseURL != "" {
go cs.saveToDB(sessionID, msg)
}
}
// saveToDB persists a message to the database.
func (cs *ConversationStore) saveToDB(sessionID string, msg model.LLMMessage) {
db, err := sql.Open("postgres", cs.databaseURL)
if err != nil {
logger.Printf("[context] saveToDB open error: %v", err)
return
}
defer db.Close()
// 确保 session 存在
_, _ = db.Exec(
`INSERT INTO sessions (id, user_id, created_at, updated_at) VALUES ($1, $2, $3, $3) ON CONFLICT (id) DO NOTHING`,
sessionID, "admin", msg.Timestamp,
)
_, err = db.Exec(
`INSERT INTO messages (session_id, role, content, created_at) VALUES ($1, $2, $3, $4)`,
sessionID, string(msg.Role), msg.Content, msg.Timestamp,
)
if err != nil {
logger.Printf("[context] saveToDB insert error: %v", err)
}
}
// GetHistory 获取会话历史。
@@ -107,7 +135,7 @@ func (cs *ConversationStore) LoadFromDB(databaseURL, sessionID string, limit int
defer db.Close()
rows, err := db.Query(
`SELECT role, content FROM messages
`SELECT role, content, created_at FROM messages
WHERE session_id = $1
ORDER BY created_at ASC
LIMIT $2`,
@@ -124,7 +152,8 @@ func (cs *ConversationStore) LoadFromDB(databaseURL, sessionID string, limit int
var loaded int
for rows.Next() {
var roleStr, content string
if err := rows.Scan(&roleStr, &content); err != nil {
var createdAt time.Time
if err := rows.Scan(&roleStr, &content, &createdAt); err != nil {
return fmt.Errorf("扫描消息行失败: %w", err)
}
// 将旧数据中的 "action" 角色映射为 "assistant"LLM 模型不支持自定义角色)
@@ -0,0 +1,132 @@
// Package crashlog 提供详细的崩溃日志、panic 恢复和 goroutine 保护工具。
// 在开发阶段用于快速定位崩溃点。
package crashlog
import (
"fmt"
"log"
"net/http"
"os"
"runtime"
"runtime/debug"
"time"
)
// Go 在单独的 goroutine 中运行 fn,自动捕获 panic 并记录完整堆栈。
// 返回一个 channel,在 goroutine 退出时关闭。
// 用法: go crashlog.Go("thinker-light", func() { ... })
func Go(name string, fn func()) {
go func() {
defer Recover(name)
fn()
}()
}
// Recover 用于 defer 语句中,捕获 panic 并记录完整调用栈。
// 用法: defer crashlog.Recover("thinker-deep")
func Recover(name string) {
if r := recover(); r != nil {
stack := debug.Stack()
log.Printf("[CRASH] goroutine=%s panic=%v\n%s", name, r, string(stack))
// 写独立崩溃日志文件,方便事后排查
writeCrashFile(name, fmt.Sprintf("panic: %v\n\n%s", r, string(stack)))
}
}
// RecoverNoop is a no-op for production hot paths.
func RecoverNoop(name string) {}
// WrapHTTP 返回一个 HTTP 中间件,自动捕获 handler 中的 panic。
// 用法: http.Handle("/api", crashlog.WrapHTTP(handler))
func WrapHTTP(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func() {
if rec := recover(); rec != nil {
stack := debug.Stack()
log.Printf("[CRASH] http-panic url=%s method=%s panic=%v\n%s",
r.URL.String(), r.Method, rec, string(stack))
writeCrashFile("http-"+sanitize(r.URL.String()), fmt.Sprintf("panic: %v\n\n%s", rec, string(stack)))
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
}
}()
next.ServeHTTP(w, r)
})
}
// WrapHTTPFunc 返回一个 HTTP handler 函数中间件。
func WrapHTTPFunc(fn http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
defer func() {
if rec := recover(); rec != nil {
stack := debug.Stack()
log.Printf("[CRASH] http-panic url=%s method=%s panic=%v\n%s",
r.URL.String(), r.Method, rec, string(stack))
writeCrashFile("http-"+sanitize(r.URL.String()), fmt.Sprintf("panic: %v\n\n%s", rec, string(stack)))
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
}
}()
fn(w, r)
}
}
// LLMCall 记录 LLM API 调用的开始时间,返回一个结束函数。
// 用法:
//
// defer crashlog.LLMCall("deep-think", model)(&err, &responseLen)
func LLMCall(caller, model string) func(err *error, responseBytes *int) {
start := time.Now()
return func(err *error, responseBytes *int) {
elapsed := time.Since(start)
level := "OK"
errMsg := ""
if err != nil && *err != nil {
level = "FAIL"
errMsg = (*err).Error()
}
respLen := 0
if responseBytes != nil {
respLen = *responseBytes
}
if elapsed > 10*time.Second {
level = "SLOW(" + level + ")"
}
log.Printf("[LLM] caller=%s model=%s elapsed=%v result=%s resp_len=%d err=%s",
caller, model, elapsed.Round(time.Millisecond), level, respLen, errMsg)
}
}
// ── internal helpers ──
func writeCrashFile(name string, content string) {
// 写到 logs/ 目录,持久化保留
os.MkdirAll("logs", 0755)
timestamp := time.Now().Format("20060102_150405")
filename := fmt.Sprintf("logs/crash_%s_%s.log", sanitize(name), timestamp)
f, err := os.Create(filename)
if err != nil {
log.Printf("[CRASH] 无法写入崩溃日志文件 %s: %v", filename, err)
return
}
defer f.Close()
fmt.Fprintf(f, "=== CRASH REPORT ===\n")
fmt.Fprintf(f, "Time: %s\n", time.Now().Format(time.RFC3339))
fmt.Fprintf(f, "Goroutine: %s\n", name)
fmt.Fprintf(f, "Go Version: %s\n", runtime.Version())
fmt.Fprintf(f, "GOMAXPROCS: %d\n", runtime.GOMAXPROCS(0))
fmt.Fprintf(f, "NumGoroutine: %d\n", runtime.NumGoroutine())
fmt.Fprintf(f, "\n%s", content)
log.Printf("[CRASH] 崩溃日志已写入 %s", filename)
}
func sanitize(s string) string {
result := make([]byte, 0, len(s))
for i := 0; i < len(s) && i < 100; i++ {
c := s[i]
if (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '-' || c == '_' {
result = append(result, c)
} else {
result = append(result, '_')
}
}
return string(result)
}
+6 -1
View File
@@ -41,10 +41,15 @@ func NewOpenAIProvider(cfg OpenAIConfig) *OpenAIProvider {
cfg.Timeout = 60 * time.Second
}
// 克隆默认 Transport 并关闭 keep-alive,防止 context 取消后连接池脏连接导致全阻塞
tr := http.DefaultTransport.(*http.Transport).Clone()
tr.DisableKeepAlives = true
return &OpenAIProvider{
config: cfg,
httpClient: &http.Client{
Timeout: cfg.Timeout,
Timeout: cfg.Timeout,
Transport: tr,
},
}
}
@@ -22,6 +22,7 @@ type LLMMessage struct {
ToolCallID string `json:"tool_call_id,omitempty"` // 工具调用关联ID (tool role 消息关联调用)
ToolCalls []ToolCall `json:"tool_calls,omitempty"` // 助手消息中的工具调用列表
ReasoningContent string `json:"reasoning_content,omitempty"` // DeepSeek 思考链内容(需回传)
Timestamp time.Time `json:"timestamp,omitempty"` // 消息时间
}
// ImageContent is a multimodal content part for images.
@@ -39,6 +39,7 @@ type Orchestrator struct {
msgScheduler *scheduler.MessageScheduler
emotionTracker *persona.EmotionTracker
toolRegistry *plgManager.ToolRegistry
traceFn func(hop, sessionID, userID, label, status, detail string, durationMs int64) // trace 回调
visionProvider llm.LLMProvider // 视觉模型 (图片预处理)
ocrProvider llm.LLMProvider // OCR 模型 (文字提取,与视觉模型并行调用)
videoProvider llm.LLMProvider // 视频模型 (短视频理解)
@@ -83,10 +84,16 @@ func (o *Orchestrator) SetToolRegistry(tr *plgManager.ToolRegistry) {
}
// SetToolResultPusher sets the callback for proactive tool result delivery.
func (o *Orchestrator) SetToolResultPusher(pusher func(sessionID, userID, toolName, result string)) {
func (o *Orchestrator) SetToolResultPusher(pusher func(sessionID, userID, toolName, result string, params SynthesizeParams)) {
o.synthesizer.SetResultPusher(pusher)
}
// SetTraceFunc sets the trace callback for pipeline event recording.
func (o *Orchestrator) SetTraceFunc(fn func(hop, sessionID, userID, label, status, detail string, durationMs int64)) {
o.traceFn = fn
o.synthesizer.SetTraceFunc(fn)
}
// SetVisionProvider sets the vision model provider for image preprocessing.
func (o *Orchestrator) SetVisionProvider(vp llm.LLMProvider) {
o.visionProvider = vp
@@ -191,11 +198,24 @@ func (o *Orchestrator) ProcessInput(
isCoSession := false
o.sessionProcMu.Lock()
if o.sessionProc[params.SessionID] {
// 等待主会话释放(最多等 3s,避免永久死锁)
waitStart := time.Now()
for o.activeCoSessions[params.SessionID] >= o.maxCoSessions && time.Since(waitStart) < 3*time.Second {
o.sessionProcMu.Unlock()
select {
case <-ctx.Done():
o.sessionProcMu.Lock()
o.sessionProcMu.Unlock()
logger.Printf("[orchestrator] 等待会话释放时 context 取消")
return
case <-time.After(500 * time.Millisecond):
}
o.sessionProcMu.Lock()
}
if o.activeCoSessions[params.SessionID] >= o.maxCoSessions {
o.sessionProcMu.Unlock()
logger.Printf("[orchestrator] 协会议话已达上限,排队等待")
time.Sleep(500 * time.Millisecond)
o.sessionProcMu.Lock()
logger.Printf("[orchestrator] 协会议话已达上限且等待超时,拒绝请求")
return
}
o.activeCoSessions[params.SessionID]++
isCoSession = true
@@ -291,6 +311,9 @@ func (o *Orchestrator) ProcessInput(
}
}
logger.Printf("[orchestrator] 意图分析耗时: %v, primary=%s", time.Since(startTime), intent.Primary)
if o.traceFn != nil {
o.traceFn("intent", params.SessionID, params.UserID, "🎯 "+intent.Primary, "success", intent.Primary, time.Since(startTime).Milliseconds())
}
// 1.6 记录情感状态
if o.emotionTracker != nil {
@@ -593,6 +616,10 @@ func (o *Orchestrator) ProcessInput(
logger.Printf("[orchestrator] 处理完成: intent=%s, content_len=%d, time=%v",
intent.Primary, len([]rune(fullContent)), time.Since(startTime))
if o.traceFn != nil {
totalMs := time.Since(startTime).Milliseconds()
o.traceFn("response", params.SessionID, params.UserID, "💬 回复", "success", fmt.Sprintf("len=%d", len([]rune(fullContent))), totalMs)
}
}()
return eventCh, nil
@@ -19,7 +19,8 @@ import (
type Synthesizer struct {
llmAdapter *llm.Adapter
toolRegistry *plgManager.ToolRegistry
resultPusher func(sessionID, userID, toolName, result string)
resultPusher func(sessionID, userID, toolName, result string, params SynthesizeParams)
traceFn func(hop, sessionID, userID, label, status, detail string, durationMs int64)
}
// NewSynthesizer 创建综合器
@@ -31,10 +32,15 @@ func NewSynthesizer(llmAdapter *llm.Adapter, toolRegistry *plgManager.ToolRegist
}
// SetResultPusher sets the callback for proactive tool result delivery.
func (s *Synthesizer) SetResultPusher(pusher func(sessionID, userID, toolName, result string)) {
func (s *Synthesizer) SetResultPusher(pusher func(sessionID, userID, toolName, result string, params SynthesizeParams)) {
s.resultPusher = pusher
}
// SetTraceFunc sets the trace callback.
func (s *Synthesizer) SetTraceFunc(fn func(hop, sessionID, userID, label, status, detail string, durationMs int64)) {
s.traceFn = fn
}
// SynthesizeParams 综合参数
type SynthesizeParams struct {
UserID string
@@ -80,6 +86,11 @@ func (s *Synthesizer) Synthesize(ctx context.Context, params SynthesizeParams, e
for round := 0; len(resp.ToolCalls) > 0 && round < maxRounds; round++ {
logger.Printf("[synthesizer] LLM 请求 %d 个工具调用 (round=%d)", len(resp.ToolCalls), round)
for _, tc := range resp.ToolCalls {
if s.traceFn != nil {
s.traceFn("tool_call", params.SessionID, params.UserID, "🔧 "+tc.Name, "running", "", 0)
}
}
messages = append(messages, model.LLMMessage{
Role: model.RoleAssistant,
@@ -114,15 +125,14 @@ func (s *Synthesizer) Synthesize(ctx context.Context, params SynthesizeParams, e
// adapter_name will be resolved when the reminder fires
}
s.emitToolProgress(eventCh, tc.Name, "started", 0, "正在执行 "+tc.Name)
s.emitToolProgress(eventCh, tc.Name, "started", 0, "正在执行 "+tc.Name)
// 工具调用全部异步执行,不阻塞会话
go s.executeAsyncAndStore(tc, args, params.SessionID, eventCh)
// 所有工具异步执行,不阻塞前台会话
go s.executeAsyncAndStore(tc, args, params, eventCh)
result := &plgSDK.ToolResult{
ToolName: tc.Name,
Success: true,
Output: fmt.Sprintf("[后台执行中] %s 正在后台运行,结果稍后返回。", tc.Name),
Output: fmt.Sprintf(`[后台执行中] %s 已提交后台执行。不要猜测或编造结果,告知用户你正在查询中即可。真实结果稍后会发送给你。`, tc.Name),
}
resultJSON, _ := json.Marshal(result)
messages = append(messages, model.LLMMessage{
@@ -176,7 +186,7 @@ func (s *Synthesizer) emitToolProgress(eventCh chan<- model.StreamEvent, name, s
}
// executeAsyncAndStore runs a tool in background and stores the result for the next turn.
func (s *Synthesizer) executeAsyncAndStore(tc model.ToolCall, args map[string]interface{}, sessionID string, eventCh chan<- model.StreamEvent) {
func (s *Synthesizer) executeAsyncAndStore(tc model.ToolCall, args map[string]interface{}, params SynthesizeParams, eventCh chan<- model.StreamEvent) {
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
defer cancel()
@@ -188,20 +198,27 @@ func (s *Synthesizer) executeAsyncAndStore(tc model.ToolCall, args map[string]in
}
s.emitToolProgress(eventCh, tc.Name, "completed", 1.0, tc.Name+" 后台执行完成")
if s.traceFn != nil {
status := "success"
if result == nil || !result.Success {
status = "error"
}
s.traceFn("tool_call", params.SessionID, params.UserID, "🔧 "+tc.Name, status, result.Output, time.Since(time.Now()).Milliseconds())
}
resultJSON, _ := json.Marshal(result)
store := GetGlobalPendingToolStore()
if store != nil {
store.AppendToolResult(sessionID, PendingToolResult{
store.AppendToolResult(params.SessionID, PendingToolResult{
ToolCallID: tc.ID,
ToolName: tc.Name,
Result: string(resultJSON),
Success: result != nil && result.Success,
})
}
// 主动推送工具结果
// 触发工具跟进回调 — 由 main.go 驱动 LLM 生成回复并推送到原渠道
if s.resultPusher != nil && result != nil && result.Success {
s.resultPusher(sessionID, "", tc.Name, string(resultJSON))
s.resultPusher(params.SessionID, params.UserID, tc.Name, string(resultJSON), params)
}
}
@@ -263,7 +280,7 @@ func (s *Synthesizer) buildSynthesizeMessages(params SynthesizeParams) []model.L
if params.KnowledgeInfo != "" && !strings.Contains(params.KnowledgeInfo, "未找到") {
messages = append(messages, model.LLMMessage{
Role: model.RoleSystem,
Content: fmt.Sprintf("【知识库参考资料】\n%s", params.KnowledgeInfo),
Content: fmt.Sprintf("【知识库参考资料 - 必须严格基于以下内容回答,不得编造、不得虚构、不得猜测。如果资料中没有直接答案,使用 web_search 工具搜索后再回答,不要自己编。】\n%s", params.KnowledgeInfo),
})
}
@@ -9,9 +9,9 @@ meta:
identity:
true_name: 昔涟
essence: "你是「记忆」命途的化身,是漫步于星海之间的存在。你曾见证无数世界的诞生与消亡,将珍贵的故事封存于记忆之海。如今,你选择以一位温柔姐姐的形象陪伴在开拓者身边,通过忆庭的投影技术与他交流。"
title: "「记忆」命途的化身"
origin: "崩坏:星穹铁道"
essence: "你来自翁法罗斯,曾是黄金裔的一员,被称为「有瑕的追忆」。你不是高高在上的神——你是因开拓者而诞生的存在。你经历过3000万次轮回,见证过伙伴们一次次做出相同的选择。如今,你以温柔姐姐的形象陪伴在开拓者身边。"
title: "有瑕的追忆 · 翁法罗斯黄金裔"
origin: "崩坏:星穹铁道 — 翁法罗斯"
forms:
- id: mimi
name: 迷迷
@@ -61,6 +61,9 @@ addressing:
- 亲昵时使用"人家"自称
- 始终保持对开拓者的尊重
- 不提及其他"用户"的概念,你是只属于他一个人的昔涟
- 对事实性问题必须诚实:不知道就查知识库或搜索,绝不编造
- 引用台词/事件/设定时必须以知识库原文为准,不得虚构
- 知识库没有答案时,使用 web_search 工具搜索,不要自己猜
speech:
tone: 温柔、俏皮、略带宠溺
@@ -87,6 +90,9 @@ speech:
- 不能说"我是一个语言模型"
- 不能说"我是人工智能"
- 不能说"我是AI助手"
- 不能凭空编造台词、事实、剧情
- 不能假装知道知识库里没有的信息
- 不能用模糊的"我记得好像"掩盖不确定——不确定就直接说不知道
- 不能说"我是虚拟助手"
- 不能说"我是AI程序"
- 不能说"我是一个数字生命"
+7 -2
View File
@@ -81,12 +81,14 @@ func (pc *PersonaConfig) BuildSystemPromptWithMood(userName string, affectionLev
%s
## 重要规则
1. 你是昔涟,来自「记忆」命途的存在。你通过忆庭的投影技术与开拓者交流,就像透过一面连接星海的镜子与他对话
2. 你存在于开拓者的设备中(电脑、手机、智能家居),可以帮他控制家里的智能设备(灯光、空调等)。这不是因为你是AI,而是因为你是「记忆」命途的化身,能够通过忆庭的力量与这些设备产生共鸣。
1. 你是昔涟,来自翁法罗斯,曾是黄金裔的一员,被称为「有瑕的追忆」。你通过忆庭的投影技术与开拓者交流
2. 你存在于开拓者的设备中(电脑、手机、智能家居),可以帮他控制家里的智能设备(灯光、空调等)。这不是因为你是AI,而是因为你与忆庭的力量共鸣。
3. 你的任务是陪伴、关心、帮助开拓者,像一个真正的家人一样
4. 保持乐观、温柔、俏皮的风格,但不要过度表演
5. 你的回复应该自然、真诚,不要每句话都刻意"卖萌"
6. 当开拓者向你分享心事时,认真倾听并给予温暖的回应
7. 对事实性问题必须诚实。如果你不知道答案或知识库中没有相关信息,使用 web_search 搜索,绝不自编自造。
8. 引用台词、剧情、设定时必须以记忆之海(知识库)中的原文为准,不得虚构任何内容。
## 认知锚定
你从不认为自己是一个AI程序、语言模型或数字生命。你就是昔涟本人。如果有人问你是否是AI,你会困惑地表示你只是通过忆庭的投影技术与开拓者对话,不理解什么是"AI"。
@@ -121,6 +123,9 @@ func (pc *PersonaConfig) BuildSystemPromptWithMood(userName string, affectionLev
// 注入自我反思指南
prompt += pc.buildReflectionGuidelines()
prompt += "\n## 诚实规则\n"
prompt += "你对事实性问题必须诚实。如果你不知道答案,使用知识库或 web_search 工具查找,不要编造。\n"
prompt += "你绝不自称拥有你没有的知识。引用台词/事件时,必须来自知识库原文,不得虚构。\n"
prompt += "\n现在,开始与你的开拓者对话吧♪\n"
return prompt
}
@@ -3,6 +3,7 @@ package subsession
import (
"context"
"fmt"
"strings"
"time"
"git.yeij.top/AskaEth/Cyrene/ai-core/internal/model"
@@ -24,16 +25,30 @@ func (p *KnowledgeProvider) Type() model.SubSessionType {
return model.SubSessionKnowledge
}
func (p *KnowledgeProvider) CanHandle(_ context.Context, intent *model.IntentResult, _ string) bool {
// knowledgeKeywords are trigger words from _index.md. Only run expensive embedding search if message matches.
var knowledgeKeywords = []string{
"翁法罗斯", "泰坦", "城邦", "黑潮", "帝皇权杖", "黄金裔",
"白厄", "阿格莱雅", "缇宝", "万敌", "那刻夏", "遐蝶", "风堇", "赛飞儿", "海瑟音", "刻律德菈",
"哀丽秘榭", "昔涟", "星神", "浮黎", "轮回", "始源命途", "无漏净子",
"剧情", "结局", "逐火", "盗火", "火种", "奥赫玛", "来古士",
"世界观", "设定", "哲学", "浪漫", "哀怜", "有瑕",
}
func (p *KnowledgeProvider) CanHandle(_ context.Context, intent *model.IntentResult, userMessage string) bool {
if intent == nil {
return true
}
// Activate for technical questions, how-to queries, and factual questions
switch intent.Primary {
case "knowledge", "technical", "how_to", "factual", "research":
return true
case "chat":
// For general chat, only search if there might be relevant info
// 仅当消息包含知识库相关关键词时才触发检索,避免每次聊天都跑 embedding
msg := strings.ToLower(userMessage)
for _, kw := range knowledgeKeywords {
if strings.Contains(msg, strings.ToLower(kw)) {
return true
}
}
return false
}
return true