fix: platform_silent记忆提取 + 群聊上下文整合 + 多QQ实例支持
- platform_silent模式接入Orchestrator记忆提取:被动观察群聊时提取值得记住的信息到对应命名空间 - post_chat后台思考注入平台观察:对话后思考也能看到群聊摘要 - QQ适配器:OneBot v11 self_id动态捕获、CQ图片URL提取、视觉+OCR并行处理 - Router解耦:ConfigName/PlatformName分离,支持多QQ实例独立连接 - 黑白名单功能:后端API + Ethend代理 + UI面板 - \n\n双换行断句:AI回复按双换行分割为多条消息按间隔发送 - @提及修复:bot自感知UID进行@检测 - 群聊上下文共享:channel-based userID避免记忆碎片化 - 消息日志显示处理后内容而非原始SSE数据 - platform-bridge Dockerfile + docker-compose.yml更新 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -29,6 +29,38 @@ type PendingThought struct {
|
||||
Consumed bool `json:"consumed"`
|
||||
}
|
||||
|
||||
// PlatformChannel represents a platform channel to observe for background thinking.
|
||||
type PlatformChannel struct {
|
||||
Platform string // qq, telegram, etc.
|
||||
ChannelType string // group, private
|
||||
ChannelID string // group ID or user QQ number
|
||||
}
|
||||
|
||||
// ParsePlatformChannels parses PLATFORM_CHANNELS env var.
|
||||
// Format: "qq:group:123456,telegram:group:789012"
|
||||
func ParsePlatformChannels(raw string) []PlatformChannel {
|
||||
if raw == "" {
|
||||
return nil
|
||||
}
|
||||
var channels []PlatformChannel
|
||||
for _, part := range strings.Split(raw, ",") {
|
||||
part = strings.TrimSpace(part)
|
||||
if part == "" {
|
||||
continue
|
||||
}
|
||||
fields := strings.SplitN(part, ":", 3)
|
||||
if len(fields) != 3 {
|
||||
continue
|
||||
}
|
||||
channels = append(channels, PlatformChannel{
|
||||
Platform: strings.TrimSpace(fields[0]),
|
||||
ChannelType: strings.TrimSpace(fields[1]),
|
||||
ChannelID: strings.TrimSpace(fields[2]),
|
||||
})
|
||||
}
|
||||
return channels
|
||||
}
|
||||
|
||||
// Thinker 后台思考器(事件驱动 + 定时周期双模式)
|
||||
//
|
||||
// 触发机制:
|
||||
@@ -128,6 +160,10 @@ type Thinker struct {
|
||||
|
||||
// 时区设置 (默认 Asia/Shanghai,可通过 TZ 环境变量覆盖)
|
||||
timeLocation *time.Location
|
||||
|
||||
// 平台静默观察
|
||||
platformChannels []PlatformChannel
|
||||
platformThinkInterval time.Duration
|
||||
}
|
||||
|
||||
// AutonomousToolPolicy 自主思考工具调用安全策略
|
||||
@@ -213,6 +249,10 @@ type ThinkerConfig struct {
|
||||
PostChatDelay time.Duration // 对话后多久触发思考
|
||||
MinThinkGap time.Duration // 两次思考最小间隔 (在线)
|
||||
OfflineThinkGap time.Duration // 两次思考最小间隔 (离线,默认 10 分钟)
|
||||
|
||||
// 平台静默观察
|
||||
PlatformSilentThinkInterval time.Duration // 平台记忆观察间隔 (默认 600s,0 = 禁用)
|
||||
PlatformChannels []PlatformChannel
|
||||
}
|
||||
|
||||
// DefaultThinkerConfig 默认配置
|
||||
@@ -232,6 +272,8 @@ func DefaultThinkerConfig() ThinkerConfig {
|
||||
PostChatDelay: getEnvDuration("THINK_POST_CHAT_DELAY_SEC", 5),
|
||||
MinThinkGap: getEnvDuration("THINK_MIN_GAP_SEC", 30),
|
||||
OfflineThinkGap: getEnvDuration("THINK_OFFLINE_GAP_SEC", 600),
|
||||
PlatformSilentThinkInterval: getEnvDuration("PLATFORM_THINK_INTERVAL_SEC", 600),
|
||||
PlatformChannels: ParsePlatformChannels(os.Getenv("PLATFORM_CHANNELS")),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -282,12 +324,14 @@ func NewThinker(
|
||||
adminUserID: adminUserID,
|
||||
adminSessionID: adminSessionID,
|
||||
memClient: memClient,
|
||||
pendingThoughts: make([]*PendingThought, 0),
|
||||
lastUserMessage: time.Now(),
|
||||
stopCh: make(chan struct{}),
|
||||
chain: NewThinkChain(10),
|
||||
autoToolPolicy: DefaultAutonomousToolPolicy(),
|
||||
proactiveGuard: DefaultProactiveGuard(),
|
||||
pendingThoughts: make([]*PendingThought, 0),
|
||||
lastUserMessage: time.Now(),
|
||||
stopCh: make(chan struct{}),
|
||||
chain: NewThinkChain(10),
|
||||
autoToolPolicy: DefaultAutonomousToolPolicy(),
|
||||
proactiveGuard: DefaultProactiveGuard(),
|
||||
platformChannels: cfg.PlatformChannels,
|
||||
platformThinkInterval: cfg.PlatformSilentThinkInterval,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -314,8 +358,14 @@ func (t *Thinker) Start() {
|
||||
go t.periodicThinkLoop()
|
||||
}
|
||||
|
||||
log.Printf("[后台思考] 已就绪 — 周期=%v + 事件驱动模式 (静默超时=%v, 对话后延迟=%v, 在线最小间隔=%v, 离线最小间隔=%v, 管理员=%s)",
|
||||
t.thinkInterval, t.silenceTimeout, t.postChatDelay, t.minThinkGap, t.offlineThinkGap, t.adminUserID)
|
||||
// 启动平台静默观察循环
|
||||
if len(t.platformChannels) > 0 && t.platformThinkInterval > 0 {
|
||||
t.wg.Add(1)
|
||||
go t.platformObservationLoop()
|
||||
}
|
||||
|
||||
log.Printf("[后台思考] 已就绪 — 周期=%v + 事件驱动模式 (静默超时=%v, 对话后延迟=%v, 在线最小间隔=%v, 离线最小间隔=%v, 管理员=%s, 平台频道=%d)",
|
||||
t.thinkInterval, t.silenceTimeout, t.postChatDelay, t.minThinkGap, t.offlineThinkGap, t.adminUserID, len(t.platformChannels))
|
||||
|
||||
// 启动后首次思考:延迟 5s,让服务完全初始化后再触发
|
||||
go func() {
|
||||
@@ -460,6 +510,116 @@ func (t *Thinker) resetSilenceTimer() {
|
||||
}()
|
||||
}
|
||||
|
||||
// platformObservationLoop periodically queries platform channel memories and generates observations.
|
||||
func (t *Thinker) platformObservationLoop() {
|
||||
defer t.wg.Done()
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
log.Printf("[后台思考] 平台观察循环 panic 恢复: %v", r)
|
||||
}
|
||||
}()
|
||||
|
||||
interval := t.platformThinkInterval
|
||||
log.Printf("[后台思考] 平台观察循环已启动 (间隔=%v, 频道数=%d)", interval, len(t.platformChannels))
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-t.stopCh:
|
||||
log.Println("[后台思考] 平台观察循环已停止")
|
||||
return
|
||||
case <-time.After(interval):
|
||||
t.performPlatformObservation()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// performPlatformObservation queries memories from all platform channels,
|
||||
// runs an intermediate LLM session to summarize, and stores the result as a pending thought.
|
||||
func (t *Thinker) performPlatformObservation() {
|
||||
if t.memClient == nil {
|
||||
return
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
|
||||
defer cancel()
|
||||
|
||||
var channelSummaries []string
|
||||
for _, ch := range t.platformChannels {
|
||||
namespace := fmt.Sprintf("platform_%s_%s_%s", ch.Platform, ch.ChannelType, ch.ChannelID)
|
||||
memories, err := t.memClient.Query(ctx, model.MemoryQuery{
|
||||
UserID: namespace,
|
||||
Limit: 20,
|
||||
})
|
||||
if err != nil {
|
||||
log.Printf("[后台思考] 查询平台频道 %s 记忆失败: %v", namespace, err)
|
||||
continue
|
||||
}
|
||||
if len(memories) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
var sb strings.Builder
|
||||
sb.WriteString(fmt.Sprintf("【%s %s %s】\n", ch.Platform, ch.ChannelType, ch.ChannelID))
|
||||
for i, m := range memories {
|
||||
if i >= 10 {
|
||||
sb.WriteString(fmt.Sprintf("... 还有 %d 条记忆\n", len(memories)-10))
|
||||
break
|
||||
}
|
||||
sb.WriteString(fmt.Sprintf("- %s\n", m.Content))
|
||||
}
|
||||
channelSummaries = append(channelSummaries, sb.String())
|
||||
}
|
||||
|
||||
if len(channelSummaries) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("[后台思考] 平台观察:%d 个频道有记忆数据,调用中间会话生成摘要...", len(channelSummaries))
|
||||
|
||||
systemPrompt := "你是昔涟的后台观察助手。以下是各聊天平台频道最近的观察摘要。\n请生成简洁报告:\n1. 各频道近期讨论主题(每频道1-2句)\n2. 是否有需要开拓者关注的重要/紧急事项\n3. 整体氛围评估\n不要直接对开拓者说话,这是给昔涟参考的幕后报告。\n输出为JSON格式:{\"summary\": \"报告内容\", \"needs_attention\": true/false}"
|
||||
|
||||
userPrompt := strings.Join(channelSummaries, "\n\n")
|
||||
|
||||
messages := []model.LLMMessage{
|
||||
{Role: model.RoleSystem, Content: systemPrompt},
|
||||
{Role: model.RoleUser, Content: userPrompt},
|
||||
}
|
||||
|
||||
resp, err := t.toolAdapter.Chat(ctx, messages)
|
||||
if err != nil {
|
||||
log.Printf("[后台思考] 中间会话 LLM 调用失败: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
var result struct {
|
||||
Summary string `json:"summary"`
|
||||
NeedsAttention bool `json:"needs_attention"`
|
||||
}
|
||||
content := strings.TrimSpace(resp.Content)
|
||||
if idx := strings.Index(content, "{"); idx >= 0 {
|
||||
if end := strings.LastIndex(content, "}"); end > idx {
|
||||
content = content[idx : end+1]
|
||||
}
|
||||
}
|
||||
if err := json.Unmarshal([]byte(content), &result); err != nil {
|
||||
result.Summary = resp.Content
|
||||
}
|
||||
|
||||
observationContent := fmt.Sprintf("[平台观察 %s]\n%s", time.Now().In(t.timeLocation).Format("15:04"), result.Summary)
|
||||
t.mu.Lock()
|
||||
t.pendingThoughts = append(t.pendingThoughts, &PendingThought{
|
||||
Content: observationContent,
|
||||
CreatedAt: time.Now(),
|
||||
Consumed: false,
|
||||
})
|
||||
if len(t.pendingThoughts) > 10 {
|
||||
t.pendingThoughts = t.pendingThoughts[len(t.pendingThoughts)-10:]
|
||||
}
|
||||
t.mu.Unlock()
|
||||
|
||||
log.Printf("[后台思考] 平台观察摘要已生成 (长度=%d, 需要关注=%v)", len(result.Summary), result.NeedsAttention)
|
||||
}
|
||||
|
||||
// periodicThinkLoop 周期性自主思考循环
|
||||
//
|
||||
// 使用动态间隔:若配置了 ScheduleLoader,每次循环根据当前时段计算间隔;
|
||||
@@ -639,9 +799,22 @@ func (t *Thinker) performThink(triggerReason string) {
|
||||
}
|
||||
}
|
||||
|
||||
// 4.5 获取最近平台观察(定期触发和对话后触发时注入)
|
||||
var platformObservation string
|
||||
if triggerReason == "periodic" || triggerReason == "post_chat" {
|
||||
t.mu.Lock()
|
||||
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()
|
||||
}
|
||||
|
||||
// 5. 构建思考提示词(根据触发原因调整)
|
||||
systemPrompt := t.buildThinkingSystemPrompt(personaConfig, triggerReason)
|
||||
userPrompt := t.buildThinkingUserPrompt(memories, convHistory, deviceSummary, triggerReason)
|
||||
userPrompt := t.buildThinkingUserPrompt(memories, convHistory, deviceSummary, triggerReason, platformObservation)
|
||||
|
||||
messages := []model.LLMMessage{
|
||||
{Role: model.RoleSystem, Content: systemPrompt},
|
||||
@@ -895,6 +1068,7 @@ func (t *Thinker) buildThinkingUserPrompt(
|
||||
convHistory []model.LLMMessage,
|
||||
deviceSummary string,
|
||||
triggerReason string,
|
||||
platformObservation string,
|
||||
) string {
|
||||
var sb strings.Builder
|
||||
|
||||
@@ -1009,6 +1183,13 @@ func (t *Thinker) buildThinkingUserPrompt(
|
||||
sb.WriteString("\n" + deviceSummary)
|
||||
}
|
||||
|
||||
// 平台观察摘要 (中间会话产生的报告)
|
||||
if platformObservation != "" {
|
||||
sb.WriteString("\n\n【平台频道观察报告(中间会话生成,供参考)】\n")
|
||||
sb.WriteString(platformObservation)
|
||||
sb.WriteString("\n")
|
||||
}
|
||||
|
||||
// 结尾引导
|
||||
sb.WriteString("\n---\n现在请写下你的私人反思。")
|
||||
sb.WriteString("\n记住:这是日记,用第三人称或自言自语的方式。")
|
||||
|
||||
Reference in New Issue
Block a user