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:
2026-05-31 09:37:18 +08:00
parent 71f0a1abdb
commit 47dce276a4
22 changed files with 2375 additions and 313 deletions
+190 -9
View File
@@ -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记住:这是日记,用第三人称或自言自语的方式。")