package ctxmgr import ( "context" "encoding/json" "errors" "fmt" "log/slog" "sync" "time" "chat-agent-system/internal/replyexport" "chat-agent-system/internal/storage" "github.com/jackc/pgx/v5/pgxpool" ) // ContextManager 多级与多类型上下文总管理器 type ContextManager struct { pool *pgxpool.Pool cas *storage.CAS reflector *PreferenceReflector decayCfg TimeDecayConfig embedder VectorEmbedder // 离线降级内存缓存 mu sync.RWMutex users map[int64]*UserPreference projects map[int64]*ProjectCollectiveContext messages map[string][]ChatMessageRecord // session_id -> messages sessions map[string]*ChatSessionSummary // session_id -> summary activities map[int64][]ActivityItem // project_id -> activities memoryChunks []MemoryChunkItem // 语义记忆切片缓存 summaries map[string]*ChatSummary // session_id -> rolling summary digests map[string]map[string]*DailyDigest // session_id -> day_key -> DailyDigest readPositions map[string]*ChatReadPosition // session_id:user_id -> read position turns map[string]*ChatTurnRecord // sessionID|clientTurnID -> 幂等回合(仅内存降级) cache *MultiLevelCache // 多级统一缓存系统 (C1-C5) semanticSummarizer SemanticSummarizer // no-tool summarizer; nil → heuristic default shadowJobs []SemanticShadowJob // in-process durable fallback queue shadowWorkerRunning bool shadowReclaimStarted bool nextActivityID int64 nextMessageID int64 nextChunkID int64 // message scope column probe (information_schema); cached once per manager messageScopeOnce sync.Once messageScopeOK bool } // NewContextManager 创建上下文管理器 func NewContextManager(pool *pgxpool.Pool, cas *storage.CAS, decayCfg TimeDecayConfig) *ContextManager { if decayCfg.HalfLifeDays <= 0 { decayCfg = DefaultTimeDecayConfig() } return &ContextManager{ pool: pool, cas: cas, reflector: NewPreferenceReflector(), decayCfg: decayCfg, embedder: NewDeterministicEmbedder(1024), users: make(map[int64]*UserPreference), projects: make(map[int64]*ProjectCollectiveContext), messages: make(map[string][]ChatMessageRecord), sessions: make(map[string]*ChatSessionSummary), activities: make(map[int64][]ActivityItem), memoryChunks: make([]MemoryChunkItem, 0), summaries: make(map[string]*ChatSummary), digests: make(map[string]map[string]*DailyDigest), readPositions: make(map[string]*ChatReadPosition), turns: make(map[string]*ChatTurnRecord), cache: NewMultiLevelCache(), nextActivityID: 1, nextMessageID: 1, nextChunkID: 1, } } // Cache 获取上下文管理器的多级缓存实例 func (m *ContextManager) Cache() *MultiLevelCache { if m == nil { return GetGlobalCache() } if m.cache == nil { m.cache = GetGlobalCache() } return m.cache } // SetEmbedder 设置自定义向量嵌入器 (如生产环境 bge-m3) func (m *ContextManager) SetEmbedder(emb VectorEmbedder) { if emb != nil { m.embedder = emb } } // RecordUserMessage 记录单轮对话消息并自动驱动工作习惯偏好更新 func (m *ContextManager) RecordUserMessage(ctx context.Context, sessionID string, userID int64, role, content, intent, branch string, meta map[string]any) error { _, err := m.RecordUserMessageWithID(ctx, sessionID, userID, role, content, intent, branch, meta) return err } // RecordUserMessageWithID 记录单轮对话消息并返回生成的消息真实自增 ID func (m *ContextManager) RecordUserMessageWithID(ctx context.Context, sessionID string, userID int64, role, content, intent, branch string, meta map[string]any) (int64, error) { if sessionID == "" || content == "" { return 0, errors.New("sessionID and content required") } shanghaiTZ := time.FixedZone("CST", 8*3600) now := time.Now() dayKey := now.In(shanghaiTZ).Format("2006-01-02") if meta != nil { if customTime, ok := meta["created_at"].(time.Time); ok && !customTime.IsZero() { now = customTime dayKey = now.In(shanghaiTZ).Format("2006-01-02") } else if customTimeStr, ok := meta["created_at"].(string); ok && customTimeStr != "" { if tParsed, err := time.Parse(time.RFC3339, customTimeStr); err == nil { now = tParsed dayKey = now.In(shanghaiTZ).Format("2006-01-02") } } if customDayKey, ok := meta["day_key"].(string); ok && customDayKey != "" { dayKey = customDayKey } } category := DeriveMessageCategory(role, intent, branch, meta) contentKind := "" exportPolicy := "none" contentRevision := 1 contentSha256 := "" if meta != nil { if k, ok := meta["content_kind"].(string); ok && k != "" { contentKind = k } if p, ok := meta["export_policy"].(string); ok && p != "" { exportPolicy = p } if rev, ok := meta["content_revision"].(int); ok && rev > 0 { contentRevision = rev } else if revF, ok := meta["content_revision"].(float64); ok && revF > 0 { contentRevision = int(revF) } if hash, ok := meta["content_sha256"].(string); ok && hash != "" { contentSha256 = hash } } if contentKind == "" || exportPolicy == "none" { hasCard := false if meta != nil { if _, ok := meta["interactive_card"]; ok { hasCard = true } } evalKind, evalCap := replyexport.EvaluatePolicy(replyexport.PolicyOptions{ Role: role, Status: "completed", Content: content, HasInteractiveCard: hasCard, Revision: contentRevision, }) contentKind = string(evalKind) exportPolicy = string(evalCap.Policy) contentSha256 = evalCap.ContentSha256 } rec := ChatMessageRecord{ SessionID: sessionID, UserID: userID, Role: role, Content: content, Intent: intent, Branch: branch, Metadata: meta, DayKey: dayKey, Category: category, ContentKind: contentKind, ExportPolicy: exportPolicy, ContentRevision: contentRevision, ContentSha256: contentSha256, CreatedAt: now, } var messageID int64 if m.pool != nil { var uid *int64 if userID > 0 { uid = &userID } var pid *int64 if meta != nil { if pVal, ok := meta["project_id"].(int64); ok && pVal > 0 { pid = &pVal } else if pValFloat, ok := meta["project_id"].(float64); ok && pValFloat > 0 { p := int64(pValFloat) pid = &p } } // 确保会话存在(user_id 已有值优先,防止跨用户覆盖归属) _, _ = m.pool.Exec(ctx, ` INSERT INTO chat_sessions (session_id, user_id, project_id, title, created_at, updated_at) VALUES ($1, $2, $3, '对话协同会话', now(), now()) ON CONFLICT (session_id) DO UPDATE SET updated_at = now(), user_id = COALESCE(chat_sessions.user_id, $2), project_id = COALESCE($3, chat_sessions.project_id) `, sessionID, uid, pid) wfInstID, stInstID, stageKey, scopeKind := scopeFieldsFromMeta(meta) rec.WorkflowInstanceID = wfInstID rec.StageInstanceID = stInstID rec.StageKey = stageKey rec.ScopeKind = scopeKind metaJSON, _ := json.Marshal(meta) var err error if m.hasMessageScopeColumns(ctx) { err = m.pool.QueryRow(ctx, ` INSERT INTO chat_messages ( session_id, user_id, role, content, intent, branch, metadata, day_key, category, content_kind, export_policy, content_revision, content_sha256, workflow_instance_id, stage_instance_id, stage_key, scope_kind, created_at ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18) RETURNING id `, sessionID, uid, role, content, intent, branch, metaJSON, dayKey, category, contentKind, exportPolicy, contentRevision, contentSha256, nullScopeInt64(wfInstID), nullScopeInt64(stInstID), nullScopeText(stageKey), scopeKind, now).Scan(&messageID) } else { err = m.pool.QueryRow(ctx, ` INSERT INTO chat_messages (session_id, user_id, role, content, intent, branch, metadata, day_key, category, content_kind, export_policy, content_revision, content_sha256, created_at) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14) RETURNING id `, sessionID, uid, role, content, intent, branch, metaJSON, dayKey, category, contentKind, exportPolicy, contentRevision, contentSha256, now).Scan(&messageID) } if err != nil { return 0, fmt.Errorf("insert chat message failed: %w", err) } rec.ID = messageID } else { m.mu.Lock() rec.ID = m.nextMessageID messageID = rec.ID m.nextMessageID++ m.messages[sessionID] = append(m.messages[sessionID], rec) sessSum, exists := m.sessions[sessionID] if !exists { sessSum = &ChatSessionSummary{ SessionID: sessionID, Title: "对话协同会话", UserID: userID, UpdatedAt: time.Now(), } m.sessions[sessionID] = sessSum } if meta != nil { if pVal, ok := meta["project_id"].(int64); ok && pVal > 0 { sessSum.ProjectID = pVal } else if pValFloat, ok := meta["project_id"].(float64); ok && pValFloat > 0 { sessSum.ProjectID = int64(pValFloat) } } sessSum.MessageCount = len(m.messages[sessionID]) sessSum.LastMessage = content sessSum.UpdatedAt = time.Now() m.mu.Unlock() } // 若为用户输入,触发工作习惯偏好异步更新 if role == "user" && userID > 0 { _ = m.ReflectUserPreference(ctx, userID, sessionID) } return messageID, nil } // GetMessageByID 根据真实 ID 查询单条消息记录并填充导出能力 func (m *ContextManager) GetMessageByID(ctx context.Context, messageID int64) (*ChatMessageRecord, error) { if messageID <= 0 { return nil, errors.New("invalid message_id") } if m.pool != nil { scoped := m.hasMessageScopeColumns(ctx) r, err := scanChatMessageRow(m.pool.QueryRow(ctx, ` SELECT `+m.chatMessageSelectColumns(ctx)+` FROM chat_messages WHERE id = $1 AND deleted_at IS NULL `, messageID), scoped) if err != nil { return nil, err } return &r, nil } m.mu.RLock() defer m.mu.RUnlock() for _, msgs := range m.messages { for _, msg := range msgs { if msg.ID == messageID { cp := msg enrichMessageExportCapability(&cp) return &cp, nil } } } return nil, errors.New("message not found") } // RecordActivity 记录项目协同操作/决策/上传/下载活动 func (m *ContextManager) RecordActivity(ctx context.Context, projectID, userID int64, userName, actType, title string, detail map[string]any, weight float64, isMilestone bool) (*ActivityItem, error) { if projectID <= 0 || title == "" { return nil, errors.New("projectID and title required") } if weight <= 0 { weight = 1.00 } item := &ActivityItem{ ProjectID: projectID, UserID: userID, UserName: userName, ActivityType: actType, Title: title, Detail: detail, RawWeight: weight, IsMilestone: isMilestone, CreatedAt: time.Now(), } if m.pool != nil { detailJSON, _ := json.Marshal(detail) var actID int64 var createdAt time.Time err := m.pool.QueryRow(ctx, ` INSERT INTO project_activities (project_id, user_id, user_name, activity_type, title, detail, weight, is_milestone, created_at) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, now()) RETURNING id, created_at `, projectID, userID, userName, actType, title, detailJSON, weight, isMilestone).Scan(&actID, &createdAt) if err != nil { return nil, fmt.Errorf("insert activity failed: %w", err) } item.ID = actID item.CreatedAt = createdAt } else { m.mu.Lock() item.ID = m.nextActivityID m.nextActivityID++ m.activities[projectID] = append(m.activities[projectID], *item) m.mu.Unlock() } item.DecayedWeight = CalculateDecayedWeight(item.RawWeight, item.CreatedAt, item.IsMilestone, m.decayCfg) return item, nil } // GetUserContext 装配单用户私有上下文 (包含个人偏好画像、近期会话、个人操作与经时间衰减的文件资料) func (m *ContextManager) GetUserContext(ctx context.Context, projectID, userID int64, sessionID string) (*SingleUserContext, error) { pref, _ := m.GetUserPreference(ctx, userID) userCtx := &SingleUserContext{ UserID: userID, Preference: pref, SessionID: sessionID, RecentChats: make([]ChatMessageRecord, 0), MyActivities: make([]ActivityItem, 0), MyFiles: make([]*storage.FileReference, 0), AssembledAt: time.Now(), } // 1. 获取近期会话(FIND-SEC-01 防御纵深:SQL 层限定会话归属目标用户, // 即使未来新增调用方遗漏 handler 校验也不会泄露他人会话正文) if sessionID != "" { if m.pool != nil { rows, err := m.pool.Query(ctx, ` SELECT id, session_id, COALESCE(user_id, 0), role, content, intent, branch, metadata, created_at FROM chat_messages WHERE session_id = $1 AND EXISTS ( SELECT 1 FROM chat_sessions cs WHERE cs.session_id = $1 AND COALESCE(cs.user_id, 0) = $2 ) ORDER BY created_at ASC LIMIT 30 `, sessionID, userID) if err == nil { defer rows.Close() for rows.Next() { var r ChatMessageRecord var metaJSON []byte if err := rows.Scan(&r.ID, &r.SessionID, &r.UserID, &r.Role, &r.Content, &r.Intent, &r.Branch, &metaJSON, &r.CreatedAt); err == nil { _ = json.Unmarshal(metaJSON, &r.Metadata) userCtx.RecentChats = append(userCtx.RecentChats, r) } } if err := rows.Err(); err != nil { slog.Warn("ctxmgr_recent_chats_scan_incomplete", "error", err) reportBackground(err) } } } else { m.mu.RLock() if sess, ok := m.sessions[sessionID]; ok && sess.UserID == userID { userCtx.RecentChats = append(userCtx.RecentChats, m.messages[sessionID]...) } m.mu.RUnlock() } } // 2. 获取用户个人在项目内的活动并应用时间衰减 var rawActs []ActivityItem if m.pool != nil { rows, err := m.pool.Query(ctx, ` SELECT id, project_id, user_id, user_name, activity_type, title, detail, weight, is_milestone, created_at FROM project_activities WHERE project_id = $1 AND user_id = $2 ORDER BY created_at DESC LIMIT 50 `, projectID, userID) if err == nil { defer rows.Close() for rows.Next() { var it ActivityItem var detJSON []byte if err := rows.Scan(&it.ID, &it.ProjectID, &it.UserID, &it.UserName, &it.ActivityType, &it.Title, &detJSON, &it.RawWeight, &it.IsMilestone, &it.CreatedAt); err == nil { _ = json.Unmarshal(detJSON, &it.Detail) rawActs = append(rawActs, it) } } if err := rows.Err(); err != nil { slog.Warn("ctxmgr_user_activities_scan_incomplete", "error", err) reportBackground(err) } } } else { m.mu.RLock() for _, it := range m.activities[projectID] { if it.UserID == userID { rawActs = append(rawActs, it) } } m.mu.RUnlock() } userCtx.MyActivities = ApplyTimeDecayToActivities(rawActs, m.decayCfg) return userCtx, nil } // GetProjectContext 装配项目级多方协同上下文 (黑板事实、里程碑决策、团队活动流与共享文件) func (m *ContextManager) GetProjectContext(ctx context.Context, projectID int64) (*ProjectCollectiveContext, error) { pCtx := &ProjectCollectiveContext{ ProjectID: projectID, BlackboardFacts: make(map[string]string), AllActivities: make([]ActivityItem, 0), MilestoneDecisions: make([]ActivityItem, 0), SharedFiles: make([]*storage.FileReference, 0), ParticipatingUsers: make([]UserSummary, 0), AssembledAt: time.Now(), } if m.pool != nil { // 1. 获取项目基准与黑板事实 var pCode, pName, cStage, actWf string var creatorID int64 var wfVer int _ = m.pool.QueryRow(ctx, ` SELECT code, name, current_stage, COALESCE(creator_user_id, 1), COALESCE(active_workflow_key, 'main'), COALESCE(workflow_selection_version, 1) FROM projects WHERE id = $1 `, projectID).Scan(&pCode, &pName, &cStage, &creatorID, &actWf, &wfVer) pCtx.ProjectCode = pCode pCtx.ProjectName = pName pCtx.CurrentStage = cStage pCtx.CreatorUserID = creatorID pCtx.ActiveWorkflowKey = actWf pCtx.WorkflowSelectionVersion = wfVer var factsJSON []byte err := m.pool.QueryRow(ctx, "SELECT facts FROM project_blackboards WHERE project_id = $1", projectID).Scan(&factsJSON) if err == nil { _ = json.Unmarshal(factsJSON, &pCtx.BlackboardFacts) } // 2. 获取全项目活动 rows, err := m.pool.Query(ctx, ` SELECT id, project_id, user_id, user_name, activity_type, title, detail, weight, is_milestone, created_at FROM project_activities WHERE project_id = $1 ORDER BY created_at DESC LIMIT 100 `, projectID) if err == nil { defer rows.Close() var rawActs []ActivityItem for rows.Next() { var it ActivityItem var detJSON []byte if err := rows.Scan(&it.ID, &it.ProjectID, &it.UserID, &it.UserName, &it.ActivityType, &it.Title, &detJSON, &it.RawWeight, &it.IsMilestone, &it.CreatedAt); err == nil { _ = json.Unmarshal(detJSON, &it.Detail) rawActs = append(rawActs, it) if it.IsMilestone || it.ActivityType == "decision" { pCtx.MilestoneDecisions = append(pCtx.MilestoneDecisions, it) } } } if err := rows.Err(); err != nil { slog.Warn("ctxmgr_project_activities_scan_incomplete", "error", err) reportBackground(err) } pCtx.AllActivities = ApplyTimeDecayToActivities(rawActs, m.decayCfg) } } else { m.mu.RLock() if existing, ok := m.projects[projectID]; ok { pCtx.ProjectName = existing.ProjectName pCtx.CurrentStage = existing.CurrentStage for k, v := range existing.BlackboardFacts { pCtx.BlackboardFacts[k] = v } } rawActs := m.activities[projectID] for _, it := range rawActs { if it.IsMilestone || it.ActivityType == "decision" { pCtx.MilestoneDecisions = append(pCtx.MilestoneDecisions, it) } } pCtx.AllActivities = ApplyTimeDecayToActivities(rawActs, m.decayCfg) m.mu.RUnlock() } return pCtx, nil } // UpdateProjectBlackboard 更新并持久化项目事实黑板数据 func (m *ContextManager) UpdateProjectBlackboard(ctx context.Context, projectID int64, facts map[string]string) error { if projectID <= 0 { return errors.New("invalid projectID") } m.mu.Lock() proj, ok := m.projects[projectID] if !ok { proj = &ProjectCollectiveContext{ ProjectID: projectID, BlackboardFacts: make(map[string]string), } m.projects[projectID] = proj } if proj.BlackboardFacts == nil { proj.BlackboardFacts = make(map[string]string) } for k, v := range facts { proj.BlackboardFacts[k] = v } m.mu.Unlock() if m.pool != nil { factsJSON, _ := json.Marshal(facts) _, err := m.pool.Exec(ctx, ` INSERT INTO project_blackboards (project_id, facts, version, updated_at) VALUES ($1, $2, 1, now()) ON CONFLICT (project_id) DO UPDATE SET facts = project_blackboards.facts || EXCLUDED.facts, version = project_blackboards.version + 1, updated_at = now() `, projectID, factsJSON) if err != nil { return fmt.Errorf("upsert project blackboard failed: %w", err) } } m.Cache().InvalidateLayer(LayerC4Context) return nil } // UpdateProjectStage 更新项目当前成熟度阶段 func (m *ContextManager) UpdateProjectStage(ctx context.Context, projectID int64, stage string) error { if projectID <= 0 || stage == "" { return errors.New("invalid projectID or stage") } m.mu.Lock() if proj, ok := m.projects[projectID]; ok { proj.CurrentStage = stage } else { m.projects[projectID] = &ProjectCollectiveContext{ ProjectID: projectID, CurrentStage: stage, BlackboardFacts: make(map[string]string), } } m.mu.Unlock() if m.pool != nil { _, err := m.pool.Exec(ctx, ` UPDATE projects SET current_stage = $2, updated_at = now() WHERE id = $1 `, projectID, stage) if err != nil { return fmt.Errorf("update project stage failed: %w", err) } } return nil } // BuildBlendedSquadContext 混合上下文装配引擎 (Context Blender) // 将项目事实、里程碑决策、触发用户的工作习惯偏好合成最适合目标 Agent 角色执行的混合上下文 func (m *ContextManager) BuildBlendedSquadContext(ctx context.Context, projectID, triggerUserID int64, taskKey, role string) (*BlendedSquadContext, error) { projCtx, err := m.GetProjectContext(ctx, projectID) if err != nil { return nil, fmt.Errorf("get project context failed: %w", err) } return m.BuildBlendedSquadContextFromProject(ctx, projCtx, triggerUserID, taskKey, role) } // BuildBlendedSquadContextFromProject 基于已过滤的项目上下文装配混合 Agent 上下文(供 ABAC gate 注入后使用)。 func (m *ContextManager) BuildBlendedSquadContextFromProject(ctx context.Context, projCtx *ProjectCollectiveContext, triggerUserID int64, taskKey, role string) (*BlendedSquadContext, error) { if projCtx == nil { return nil, nil } userPref, _ := m.GetUserPreference(ctx, triggerUserID) guidance := "" if m.reflector != nil { guidance = m.reflector.GeneratePromptGuidance(userPref) } decisions := make([]string, 0, len(projCtx.MilestoneDecisions)) for _, d := range projCtx.MilestoneDecisions { decisions = append(decisions, fmt.Sprintf("[%s] %s (%s)", d.ActivityType, d.Title, d.UserName)) } return &BlendedSquadContext{ ProjectName: projCtx.ProjectName, CurrentStage: projCtx.CurrentStage, TaskKey: taskKey, AgentRole: role, BlackboardFacts: projCtx.BlackboardFacts, KeyDecisions: decisions, TriggerUserPreference: userPref, PersonalizedGuidance: guidance, AssembledAt: time.Now(), }, nil } // CompilePromptGuidance 编译指定用户的个性化偏好指导语 (供对话 LLM 或智能体 Prompt 注入) func (m *ContextManager) CompilePromptGuidance(ctx context.Context, userID int64) (string, error) { if userID <= 0 { return "", errors.New("invalid userID") } pref, err := m.GetUserPreference(ctx, userID) if err != nil { return "", err } if pref == nil || m.reflector == nil { return "", nil } return m.reflector.GeneratePromptGuidance(pref), nil } // GetUserPreference 获取用户偏好画像 func (m *ContextManager) GetUserPreference(ctx context.Context, userID int64) (*UserPreference, error) { if userID <= 0 { return nil, errors.New("invalid userID") } if m.pool != nil { var pref UserPreference var focusArr []string var factsJSON []byte err := m.pool.QueryRow(ctx, ` SELECT u.id, u.account_id, u.name, u.role, p.communication_style, p.focus_areas, p.output_format_pref, p.custom_instructions, p.memory_facts, p.updated_at FROM users u LEFT JOIN user_preferences p ON u.id = p.user_id WHERE u.id = $1 `, userID).Scan(&pref.UserID, &pref.AccountID, &pref.Name, &pref.Role, &pref.CommunicationStyle, &focusArr, &pref.OutputFormatPref, &pref.CustomInstructions, &factsJSON, &pref.UpdatedAt) if err == nil { pref.FocusAreas = focusArr _ = json.Unmarshal(factsJSON, &pref.MemoryFacts) if pref.CommunicationStyle == "" { pref.CommunicationStyle = StyleConciseStructured } return &pref, nil } } m.mu.RLock() defer m.mu.RUnlock() if pref, ok := m.users[userID]; ok { return pref, nil } return &UserPreference{ UserID: userID, CommunicationStyle: StyleConciseStructured, FocusAreas: []string{"reg_compliance", "timeline"}, OutputFormatPref: "standard_report", MemoryFacts: make(map[string]string), UpdatedAt: time.Now(), }, nil } // UpdateUserPreference 保存用户偏好画像 func (m *ContextManager) UpdateUserPreference(ctx context.Context, pref *UserPreference) error { if pref == nil || pref.UserID <= 0 { return errors.New("invalid user preference") } pref.UpdatedAt = time.Now() if m.pool != nil { factsJSON, _ := json.Marshal(pref.MemoryFacts) _, err := m.pool.Exec(ctx, ` INSERT INTO user_preferences (user_id, communication_style, focus_areas, output_format_pref, custom_instructions, memory_facts, updated_at) VALUES ($1, $2, $3, $4, $5, $6, now()) ON CONFLICT (user_id) DO UPDATE SET communication_style = EXCLUDED.communication_style, focus_areas = EXCLUDED.focus_areas, output_format_pref = EXCLUDED.output_format_pref, custom_instructions = EXCLUDED.custom_instructions, memory_facts = EXCLUDED.memory_facts, updated_at = now() `, pref.UserID, string(pref.CommunicationStyle), pref.FocusAreas, pref.OutputFormatPref, pref.CustomInstructions, factsJSON) if err != nil { return fmt.Errorf("upsert user preference failed: %w", err) } } m.mu.Lock() m.users[pref.UserID] = pref m.mu.Unlock() return nil } // ReflectUserPreference 根据用户在指定会话的历史记录智能总结工作习惯与偏好 func (m *ContextManager) ReflectUserPreference(ctx context.Context, userID int64, sessionID string) error { var msgs []ChatMessageRecord if m.pool != nil { rows, err := m.pool.Query(ctx, ` SELECT id, session_id, user_id, role, content, intent, branch, metadata, created_at FROM chat_messages WHERE user_id = $1 OR session_id = $2 ORDER BY created_at DESC LIMIT 20 `, userID, sessionID) if err == nil { defer rows.Close() for rows.Next() { var r ChatMessageRecord var metaJSON []byte if err := rows.Scan(&r.ID, &r.SessionID, &r.UserID, &r.Role, &r.Content, &r.Intent, &r.Branch, &metaJSON, &r.CreatedAt); err == nil { _ = json.Unmarshal(metaJSON, &r.Metadata) msgs = append(msgs, r) } } if err := rows.Err(); err != nil { slog.Warn("ctxmgr_reflect_msgs_scan_incomplete", "error", err) reportBackground(err) } } } else { m.mu.RLock() msgs = append(msgs, m.messages[sessionID]...) m.mu.RUnlock() } if len(msgs) == 0 { return nil } currentPref, _ := m.GetUserPreference(ctx, userID) updatedPref := m.reflector.ReflectFromMessages(msgs, currentPref) return m.UpdateUserPreference(ctx, updatedPref) }