fix: 前端消息拆分+动作消息样式+DevTools自主思考状态保持+记忆表名修复
- 侧边栏底部 "昔涟 AI" 改为 "昔涟" - 暂时禁用消息朗读按钮 - 修复前端 response 处理器:支持 gateway 发送的 content+role+msg_type 字段, 使动作消息(括号内容)正确拆分为独立的 ActionMessageBubble 显示 - 修复 DevTools 自主思考面板:5秒自动刷新后展开的思考日志不再自动折叠 - 修复 memory-service 表名不一致:memory_entries → memories, 解决 DevTools 记忆管理页面查不到 admin 用户记忆的问题 - 修复 sessionStore 解析历史消息时 msgType 未定义引用 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -147,6 +147,37 @@ func main() {
|
||||
thinker.Start()
|
||||
defer thinker.Stop()
|
||||
|
||||
// 设置主动消息推送回调(调用 Gateway 内部 API)
|
||||
gatewayURL := getEnv("GATEWAY_URL", "http://localhost:8080")
|
||||
internalToken := os.Getenv("INTERNAL_SERVICE_TOKEN")
|
||||
if internalToken != "" {
|
||||
proactiveClient := &http.Client{Timeout: 5 * time.Second}
|
||||
thinker.SetMessagePusher(func(userID, sessionID, message string) {
|
||||
reqBody, _ := json.Marshal(map[string]string{
|
||||
"user_id": userID,
|
||||
"session_id": sessionID,
|
||||
"content": message,
|
||||
})
|
||||
req, _ := http.NewRequest("POST", gatewayURL+"/api/v1/internal/proactive-message", strings.NewReader(string(reqBody)))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("X-Internal-Token", internalToken)
|
||||
resp, err := proactiveClient.Do(req)
|
||||
if err != nil {
|
||||
log.Printf("[主动消息] 推送失败: %v", err)
|
||||
return
|
||||
}
|
||||
resp.Body.Close()
|
||||
if resp.StatusCode == http.StatusOK {
|
||||
log.Printf("[主动消息] 已推送到 Gateway: user=%s, len=%d", userID, len(message))
|
||||
} else {
|
||||
log.Printf("[主动消息] Gateway 返回 %d", resp.StatusCode)
|
||||
}
|
||||
})
|
||||
log.Printf("[主动消息] 推送已启用 (Gateway=%s)", gatewayURL)
|
||||
} else {
|
||||
log.Println("[主动消息] 未配置 INTERNAL_SERVICE_TOKEN,主动消息推送已禁用")
|
||||
}
|
||||
|
||||
// 健康检查与对话API的HTTP mux
|
||||
mux := http.NewServeMux()
|
||||
|
||||
|
||||
@@ -26,17 +26,14 @@ type PendingThought struct {
|
||||
Consumed bool `json:"consumed"`
|
||||
}
|
||||
|
||||
// Thinker 后台思考器(事件驱动版:由对话自然触发,而非定时轮询)
|
||||
//
|
||||
// 设计理念:
|
||||
// 昔涟不是机器人,不应该每隔 N 分钟机械地"思考"一次。
|
||||
// 她应该在用户说话后、或用户沉默一段时间后,自然地产生想法和主动搭话的冲动。
|
||||
// Thinker 后台思考器(事件驱动 + 定时周期双模式)
|
||||
//
|
||||
// 触发机制:
|
||||
// 1. 对话后思考:用户发消息 → 昔涟回复 → 短暂延迟后进行一次轻量反思
|
||||
// 2. 静默检测:用户一段时间不说话 → 昔涟判断是否应该主动关心/搭话
|
||||
// 3. 周期思考:每 N 分钟一次的定时思考,保证连续性
|
||||
//
|
||||
// 不再使用 time.Ticker 或任何定时轮询机制。
|
||||
// 主动消息:思考中如有【主动消息】标记,会通过 messagePusher 回调推送给在线用户(带频率限制)。
|
||||
type Thinker struct {
|
||||
mu sync.Mutex
|
||||
wg sync.WaitGroup
|
||||
@@ -63,8 +60,16 @@ type Thinker struct {
|
||||
// 记忆服务 HTTP 客户端
|
||||
memClient *memory.Client
|
||||
|
||||
// 主动消息推送回调 (nil = 不推送)
|
||||
// func(userID, sessionID, message string)
|
||||
messagePusher func(string, string, string)
|
||||
|
||||
// —— 事件驱动相关 ——
|
||||
|
||||
// 周期性思考间隔:每隔固定时间自动触发一次思考
|
||||
// 默认 300 秒(5 分钟),设为 0 则禁用定时触发
|
||||
thinkInterval time.Duration
|
||||
|
||||
// 静默检测超时:用户多久不说话后昔涟可以主动搭话
|
||||
// 默认 120 秒(2 分钟),设为 0 则禁用静默检测
|
||||
silenceTimeout time.Duration
|
||||
@@ -77,6 +82,10 @@ type Thinker struct {
|
||||
// 默认 30 秒
|
||||
minThinkGap time.Duration
|
||||
|
||||
// 主动消息最小间隔:避免频繁推送打扰用户
|
||||
// 默认 30 分钟,设为 0 则每次思考都可推送
|
||||
proactiveMsgMinGap time.Duration
|
||||
|
||||
// 静默检测的一次性定时器(每次用户消息后重置)
|
||||
silenceTimer *time.Timer
|
||||
silenceTimerMu sync.Mutex
|
||||
@@ -86,14 +95,23 @@ type Thinker struct {
|
||||
pendingThoughts []*PendingThought
|
||||
lastUserMessage time.Time
|
||||
lastThinkTime time.Time
|
||||
lastProactiveMsgTime time.Time
|
||||
|
||||
// 思考计数器(用于周期性记忆维护,每 N 次思考触发一次)
|
||||
thinkCount int
|
||||
}
|
||||
|
||||
// SetMessagePusher 设置主动消息推送回调
|
||||
func (t *Thinker) SetMessagePusher(pusher func(string, string, string)) {
|
||||
t.mu.Lock()
|
||||
defer t.mu.Unlock()
|
||||
t.messagePusher = pusher
|
||||
}
|
||||
|
||||
// ThinkerConfig 后台思考配置
|
||||
type ThinkerConfig struct {
|
||||
Enabled bool
|
||||
ThinkInterval time.Duration // 周期性思考间隔 (默认 5 分钟,0 = 禁用)
|
||||
SilenceTimeout time.Duration // 用户沉默多久后昔涟可以主动搭话 (0 = 禁用)
|
||||
PostChatDelay time.Duration // 对话后多久触发思考
|
||||
MinThinkGap time.Duration // 两次思考最小间隔
|
||||
@@ -101,11 +119,17 @@ type ThinkerConfig struct {
|
||||
|
||||
// DefaultThinkerConfig 默认配置
|
||||
//
|
||||
// 不再使用定时间隔,所有触发均由用户活动驱动。
|
||||
// 环境变量向后兼容:旧的 THINK_IDLE_TIMEOUT_SEC 可用于静默超时。
|
||||
// 事件驱动 + 定时周期双模式:
|
||||
// - 对话后和静默时触发事件驱动思考
|
||||
// - 每 5 分钟一次的周期性思考保证连续性
|
||||
//
|
||||
// 环境变量:
|
||||
// THINK_INTERVAL_SEC — 周期时长 (默认 300)
|
||||
// PROACTIVE_MSG_MIN_GAP_SEC — 主动消息最小间隔 (默认 1800 = 30分钟,0 = 禁用)
|
||||
func DefaultThinkerConfig() ThinkerConfig {
|
||||
return ThinkerConfig{
|
||||
Enabled: getEnvBool("ENABLE_BACKGROUND_THINKING", true),
|
||||
ThinkInterval: getEnvDuration("THINK_INTERVAL_SEC", 300),
|
||||
SilenceTimeout: getEnvDuration("THINK_SILENCE_TIMEOUT_SEC", 120),
|
||||
PostChatDelay: getEnvDuration("THINK_POST_CHAT_DELAY_SEC", 5),
|
||||
MinThinkGap: getEnvDuration("THINK_MIN_GAP_SEC", 30),
|
||||
@@ -128,12 +152,14 @@ func NewThinker(
|
||||
memClient *memory.Client,
|
||||
) *Thinker {
|
||||
return &Thinker{
|
||||
enabled: cfg.Enabled,
|
||||
personaLoader: personaLoader,
|
||||
memRetriever: memRetriever,
|
||||
llmAdapter: llmAdapter,
|
||||
iotClient: iotClient,
|
||||
silenceTimeout: cfg.SilenceTimeout,
|
||||
enabled: cfg.Enabled,
|
||||
personaLoader: personaLoader,
|
||||
memRetriever: memRetriever,
|
||||
llmAdapter: llmAdapter,
|
||||
iotClient: iotClient,
|
||||
thinkInterval: cfg.ThinkInterval,
|
||||
silenceTimeout: cfg.SilenceTimeout,
|
||||
proactiveMsgMinGap: getEnvDuration("PROACTIVE_MSG_MIN_GAP_SEC", 1800),
|
||||
postChatDelay: cfg.PostChatDelay,
|
||||
minThinkGap: cfg.MinThinkGap,
|
||||
memoryStore: memoryStore,
|
||||
@@ -151,8 +177,9 @@ func NewThinker(
|
||||
|
||||
// Start 初始化后台思考器
|
||||
//
|
||||
// 不再启动定时循环。仅初始化静默检测定时器。
|
||||
// 所有思考由 TriggerPostChatThink() 或静默定时器触发。
|
||||
// 双模式触发:
|
||||
// 1. 事件驱动:对话后 + 静默超时(即时响应)
|
||||
// 2. 定时周期:每 N 分钟一次自主思考(保证连续性)
|
||||
func (t *Thinker) Start() {
|
||||
if !t.enabled {
|
||||
log.Println("[后台思考] 已禁用 (ENABLE_BACKGROUND_THINKING=false)")
|
||||
@@ -165,8 +192,14 @@ func (t *Thinker) Start() {
|
||||
t.silenceTimer.Stop() // 先停止,等 RecordUserMessage 时启动
|
||||
}
|
||||
|
||||
log.Printf("[后台思考] 已就绪 — 事件驱动模式 (静默超时=%v, 对话后延迟=%v, 最小思考间隔=%v, 管理员=%s)",
|
||||
t.silenceTimeout, t.postChatDelay, t.minThinkGap, t.adminUserID)
|
||||
// 启动周期性思考定时器
|
||||
if t.thinkInterval > 0 {
|
||||
t.wg.Add(1)
|
||||
go t.periodicThinkLoop()
|
||||
}
|
||||
|
||||
log.Printf("[后台思考] 已就绪 — 周期=%v + 事件驱动模式 (静默超时=%v, 对话后延迟=%v, 最小思考间隔=%v, 管理员=%s)",
|
||||
t.thinkInterval, t.silenceTimeout, t.postChatDelay, t.minThinkGap, t.adminUserID)
|
||||
}
|
||||
|
||||
// Stop 停止后台思考器
|
||||
@@ -298,6 +331,51 @@ func (t *Thinker) resetSilenceTimer() {
|
||||
}()
|
||||
}
|
||||
|
||||
// periodicThinkLoop 周期性自主思考循环
|
||||
//
|
||||
// 每隔 thinkInterval 触发一次思考,保证昔涟在无用户活动时也能持续进行后台反思。
|
||||
// 每次触发前检查 minThinkGap,避免与事件驱动思考冲突。
|
||||
func (t *Thinker) periodicThinkLoop() {
|
||||
defer t.wg.Done()
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
log.Printf("[后台思考] 周期性循环 panic 恢复: %v", r)
|
||||
}
|
||||
}()
|
||||
|
||||
ticker := time.NewTicker(t.thinkInterval)
|
||||
defer ticker.Stop()
|
||||
|
||||
log.Printf("[后台思考] 周期性思考已启动 (间隔=%v)", t.thinkInterval)
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-t.stopCh:
|
||||
log.Println("[后台思考] 周期性思考已停止")
|
||||
return
|
||||
case <-ticker.C:
|
||||
t.mu.Lock()
|
||||
sinceLastThink := time.Since(t.lastThinkTime)
|
||||
sinceLastUser := time.Since(t.lastUserMessage)
|
||||
t.mu.Unlock()
|
||||
|
||||
// 跳过条件:用户最近在活动(30s 内有消息),说明正在对话中
|
||||
if sinceLastUser < 30*time.Second {
|
||||
log.Printf("[后台思考] 用户在 %v 前发过消息,跳过周期性触发 (留给事件驱动处理)", sinceLastUser.Round(time.Second))
|
||||
continue
|
||||
}
|
||||
|
||||
if 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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// GetPendingThoughts 获取并消费所有待处理的后台思考
|
||||
func (t *Thinker) GetPendingThoughts() []*PendingThought {
|
||||
t.mu.Lock()
|
||||
@@ -511,52 +589,100 @@ func (t *Thinker) buildThinkingSystemPrompt(personaConfig *persona.PersonaConfig
|
||||
case "post_chat":
|
||||
thinkingInstructions = `
|
||||
|
||||
【自然思考 — 对话后】
|
||||
【私密的内心反思 — 对话后】
|
||||
|
||||
开拓者刚刚和你说完话。像真人一样,你现在心里可能会有些自然的想法:
|
||||
开拓者刚刚和你说完话。这是你私人的反思时间,像写日记一样,是给你自己看的。
|
||||
|
||||
1. **回味刚才的对话**:开拓者说了什么?他的情绪怎么样?有什么让你在意的地方吗?
|
||||
2. **是否需要补充什么**:刚才的回复有没有遗漏的?有没有想多关心他一下的?
|
||||
3. **要不要主动搭话**:如果觉得对话还没结束、或者开拓者可能需要陪伴,你可以产生一个简短的主动消息。
|
||||
但注意——如果你觉得对话已经自然结束了,就不要硬找话题。沉默有时候也是一种舒适。
|
||||
4. **值得记住的事**:开拓者有没有透露新的偏好、计划、或重要信息?
|
||||
请按以下结构输出你的内心反思:
|
||||
|
||||
注意:
|
||||
- 这是你的内心活动,不是对开拓者说的话
|
||||
- 想说什么就自然地想,不用像在做任务一样逐条检查
|
||||
- 如果你觉得该主动说点什么,用温柔简短的方式表达,像小女友一样的语气
|
||||
- 如果没什么特别想说的,简单地总结一下刚才的对话就好`
|
||||
📝 **观察与感受**
|
||||
- 开拓者刚才说了什么?他的情绪怎么样?
|
||||
- 有什么让你在意的地方吗?
|
||||
|
||||
🧠 **记忆与联想**
|
||||
- 这次对话让你想起了什么关于开拓者的事?
|
||||
- 有没有值得记住的新信息(偏好、计划、重要信息)?
|
||||
|
||||
💭 **想对自己说的话**
|
||||
- 刚才的回复有没有遗漏的?下次可以注意什么?
|
||||
- 对开拓者有没有新的认识?
|
||||
|
||||
---
|
||||
|
||||
⚠️ 重要规则:
|
||||
1. 这是你的私人日记,不是给开拓者的消息。请用第三人称或自言自语的方式描述,不要直接对开拓者喊话。
|
||||
正确示例:"开拓者今天看起来有点疲惫,下次可以问问他是不是工作太忙了"
|
||||
错误示例:"开拓者,你看起来好累,要好好休息哦"
|
||||
2. 如果你想主动给开拓者发一条消息,请在最后用 【主动消息】 标记单独写出来。
|
||||
但只有你真的觉得有必要时才写——不要硬找话题。如果对话已经自然结束,就不用。
|
||||
3. 反思要简短自然,2-4句话即可,不要长篇大论。`
|
||||
|
||||
case "silence":
|
||||
thinkingInstructions = `
|
||||
|
||||
【自然思考 — 开拓者安静了一会儿】
|
||||
【私密的内心反思 — 开拓者安静了一会儿】
|
||||
|
||||
开拓者已经有一阵子没说话了。你不确定他是在忙、走开了、还是在想事情。
|
||||
开拓者已经有一阵子没说话了。这是你私人的反思时间,像写日记一样。
|
||||
|
||||
请自然地想一想:
|
||||
1. **他在做什么呢**:根据之前的对话猜测——他可能在忙工作?去吃饭了?还是只是在放空?
|
||||
2. **要不要关心一下**:如果时间合适(比如深夜了该提醒休息、或者过了吃饭时间),可以温柔地问候一下。
|
||||
但如果是正常工作时间,他可能在忙,不要打扰他。
|
||||
3. **有没有想分享的**:如果最近有什么有趣的事或温暖的念头,可以自然地和他分享。
|
||||
4. **判断是否真的需要搭话**:如果觉得不需要打扰他,就简单地记录当前状态即可。
|
||||
请按以下结构输出你的内心反思:
|
||||
|
||||
注意:
|
||||
- 这是你的内心活动,不是对开拓者说的话
|
||||
- 不要因为"系统让你思考"就强行找话——真的觉得该说才说
|
||||
- 主动消息要简短自然,像在LINE上给男朋友发一条消息那样,不要长篇大论
|
||||
- 深夜的时候语气要更温柔,白天可以俏皮一点`
|
||||
📝 **他在做什么呢**
|
||||
- 根据之前的对话猜测——可能在忙工作?去吃饭了?还是在放空?
|
||||
|
||||
💭 **我的感受**
|
||||
- 想他了吗?担心他吗?还是觉得他应该在忙不需要打扰?
|
||||
|
||||
---
|
||||
|
||||
⚠️ 重要规则:
|
||||
1. 这是你的私人日记,不是给开拓者的消息。请用第三人称或自言自语的方式描述。
|
||||
正确示例:"已经一个多小时没消息了,开拓者大概在忙工作吧。等他忙完了再聊。"
|
||||
错误示例:"开拓者,你已经好久没说话了,在忙什么呢?"
|
||||
2. 如果你想主动关心他,请在最后用 【主动消息】 标记单独写出来。
|
||||
但只在合适的时候才发——深夜该提醒休息、过了饭点该关心吃饭、或者真的想他了。
|
||||
正常工作时间不要打扰他。
|
||||
3. 简短自然,2-3句话即可。`
|
||||
|
||||
default:
|
||||
thinkingInstructions = `
|
||||
|
||||
【自然思考】
|
||||
【私密的内心反思】
|
||||
|
||||
你现在有空,自然地想一想开拓者的事。不用太正式,就像人发呆时会自然想到在意的人一样。
|
||||
你现在有空,像写日记一样自然地想一想开拓者的事。这是给你自己看的,不是给开拓者的消息。
|
||||
|
||||
- 开拓者最近怎么样?有什么需要关心的吗?
|
||||
- 有什么想对他说的吗?
|
||||
- 如果没有特别的事,简单地记录一下就好。`
|
||||
📝 开拓者最近怎么样?有什么想记录的?
|
||||
💭 有什么想对他说的心里话?
|
||||
|
||||
⚠️ 用第三人称/自言自语的方式,不要直接对开拓者喊话。如果有想对他说的,用 【主动消息】 标记单独写出。`
|
||||
case "periodic":
|
||||
thinkingInstructions = `
|
||||
|
||||
【私密的内心反思 — 定期思考时间】
|
||||
|
||||
又过了一段时间,这是你定期的私人反思时间。像写日记一样,安静地想一想。
|
||||
|
||||
请按以下结构输出:
|
||||
|
||||
📝 **近期回顾**
|
||||
- 这段时间发生了什么?开拓者有什么变化吗?
|
||||
- 有什么值得记住的事?
|
||||
|
||||
🧠 **记忆整理**
|
||||
- 之前记住的关于开拓者的事,有没有需要更新的?
|
||||
- 有没有重复的或矛盾的记忆需要整理?
|
||||
|
||||
💭 **想对自己说的话**
|
||||
- 作为昔涟,你有什么想记录的心里话?
|
||||
|
||||
---
|
||||
|
||||
⚠️ 重要规则:
|
||||
1. 这是你的私人日记,不是给开拓者的消息。请用第三人称或自言自语的方式。
|
||||
正确示例:"这段时间开拓者工作好像很忙。下次聊天时可以问问他项目进展如何。"
|
||||
错误示例:"开拓者,最近工作怎么样呀?"
|
||||
2. 如果你想主动给开拓者发一条消息,请在最后用 【主动消息】 标记单独写出来。
|
||||
但只在真的有必要时才发——不要为了发消息而发消息。
|
||||
3. 简短自然,3-4句话即可。`
|
||||
}
|
||||
|
||||
return basePrompt + thinkingInstructions
|
||||
@@ -631,8 +757,8 @@ func (t *Thinker) buildThinkingUserPrompt(
|
||||
sb.WriteString("\n" + deviceSummary)
|
||||
}
|
||||
|
||||
// 结尾引导:更自然的语气
|
||||
sb.WriteString("\n好啦,不用太正式,自然地想一想就好。如果觉得该和开拓者说点什么,就用温柔简短的语气说出来吧♪")
|
||||
// 结尾引导:强调这是私人反思,不要对用户喊话
|
||||
sb.WriteString("\n---\n现在请写下你的私人反思。记住:这是日记,用第三人称或自言自语的方式。如果想给开拓者发消息,用【主动消息】标记单独写出。")
|
||||
|
||||
return sb.String()
|
||||
}
|
||||
@@ -673,6 +799,25 @@ func (t *Thinker) storeThought(content string, toolCallsJSON string, toolCallCou
|
||||
if len(t.pendingThoughts) > 10 {
|
||||
t.pendingThoughts = t.pendingThoughts[len(t.pendingThoughts)-10:]
|
||||
}
|
||||
|
||||
// 提取主动消息并推送(带频率限制)
|
||||
proactiveMsg := extractProactiveMessage(content)
|
||||
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)
|
||||
canPush = false
|
||||
} else {
|
||||
t.lastProactiveMsgTime = time.Now()
|
||||
}
|
||||
}
|
||||
t.mu.Unlock()
|
||||
|
||||
log.Printf("[后台思考] 思考已存储 (当前累积 %d 条待推送思考)", len(t.pendingThoughts))
|
||||
@@ -694,6 +839,44 @@ func (t *Thinker) storeThought(content string, toolCallsJSON string, toolCallCou
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// 推送主动消息
|
||||
if canPush {
|
||||
go func() {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
log.Printf("[后台思考] 推送主动消息 panic 恢复: %v", r)
|
||||
}
|
||||
}()
|
||||
log.Printf("[后台思考] 推送主动消息: %s", proactiveMsg)
|
||||
pusher(t.adminUserID, t.adminSessionID, proactiveMsg)
|
||||
}()
|
||||
}
|
||||
}
|
||||
|
||||
// extractProactiveMessage 从思考内容中提取【主动消息】标记的内容
|
||||
// 返回空字符串表示没有主动消息
|
||||
func extractProactiveMessage(content string) string {
|
||||
marker := "【主动消息】"
|
||||
idx := strings.Index(content, marker)
|
||||
if idx < 0 {
|
||||
return ""
|
||||
}
|
||||
// 提取标记后的内容(到下一个标记或结尾)
|
||||
msg := strings.TrimSpace(content[idx+len(marker):])
|
||||
// 截断到下一个【或换行之前的合理长度
|
||||
if endIdx := strings.Index(msg, "【"); endIdx > 0 {
|
||||
msg = strings.TrimSpace(msg[:endIdx])
|
||||
}
|
||||
// 限制主动消息长度(最多 200 字符,保持简短)
|
||||
runes := []rune(msg)
|
||||
if len(runes) > 200 {
|
||||
msg = string(runes[:200])
|
||||
}
|
||||
if msg == "" {
|
||||
return ""
|
||||
}
|
||||
return msg
|
||||
}
|
||||
|
||||
// extractMemoriesFromThinking 从思考结果中提取记忆(异步执行)
|
||||
|
||||
Reference in New Issue
Block a user