Files
Agent-System/internal/ctxmgr/project_dialogue.go
T
weichen d6f5e0e28a feat(projectcontext): bizdev 注入项目对话并完成 PG 治理 E2E
新增 ctxmgr 项目对话采集并接入 PM/TE BuildSnapshot;PG 测试统一 TEST_DATABASE_URL 并使用三正式工作流 key。
2026-09-13 02:50:23 +00:00

170 lines
4.6 KiB
Go

package ctxmgr
import (
"context"
"encoding/json"
"fmt"
"strings"
"chat-agent-system/internal/replyexport"
)
const defaultProjectDialogueLimit = 120
// ProjectDialogueCollectResult 项目级对话采集结果(供上下文快照使用)。
type ProjectDialogueCollectResult struct {
Lines []string
ExcludedPrivate int
ExcludedNoAccess int
AllowedStageKey string
StageFilteredCount int
}
// CollectProjectDialogueOptions 采集选项。
type CollectProjectDialogueOptions struct {
Limit int
AllowedStageKey string // 非空时仅保留无 stage_key 或匹配的对话
}
// CollectProjectDialogue 收集进入项目知识边界的共享对话行。
func (m *ContextManager) CollectProjectDialogue(ctx context.Context, projectID int64, opts CollectProjectDialogueOptions) (*ProjectDialogueCollectResult, error) {
out := &ProjectDialogueCollectResult{AllowedStageKey: opts.AllowedStageKey}
if m == nil || projectID <= 0 {
return out, nil
}
limit := opts.Limit
if limit <= 0 {
limit = defaultProjectDialogueLimit
}
var msgs []ChatMessageRecord
if m.pool != nil {
rows, err := m.pool.Query(ctx, `
SELECT cm.id, cm.session_id, COALESCE(cm.user_id, 0), cm.role, cm.content,
cm.intent, cm.branch, cm.metadata, COALESCE(cm.day_key::text, ''),
COALESCE(cm.category, ''), COALESCE(cm.content_kind, ''),
COALESCE(cm.export_policy, 'none'), cm.created_at
FROM chat_messages cm
JOIN chat_sessions cs ON cs.session_id = cm.session_id
WHERE cs.project_id = $1 AND cm.deleted_at IS NULL
ORDER BY cm.created_at ASC
LIMIT $2
`, projectID, limit)
if err != nil {
return nil, fmt.Errorf("collect project dialogue: %w", err)
}
defer rows.Close()
for rows.Next() {
var rec ChatMessageRecord
var metaJSON []byte
if err := rows.Scan(
&rec.ID, &rec.SessionID, &rec.UserID, &rec.Role, &rec.Content,
&rec.Intent, &rec.Branch, &metaJSON, &rec.DayKey, &rec.Category,
&rec.ContentKind, &rec.ExportPolicy, &rec.CreatedAt,
); err != nil {
return nil, err
}
if len(metaJSON) > 0 {
_ = json.Unmarshal(metaJSON, &rec.Metadata)
}
msgs = append(msgs, rec)
}
} else {
m.mu.RLock()
for sessionID, sessionMsgs := range m.messages {
sess := m.sessions[sessionID]
if sess != nil && sess.ProjectID != projectID {
continue
}
for _, rec := range sessionMsgs {
if sess == nil || sess.ProjectID == 0 {
if pid := messageProjectID(rec); pid != projectID {
continue
}
}
msgs = append(msgs, rec)
}
}
m.mu.RUnlock()
}
for _, rec := range msgs {
line, include, excludedPrivate, excludedNoAccess := classifyDialogueLine(rec, opts.AllowedStageKey)
if excludedPrivate {
out.ExcludedPrivate++
}
if excludedNoAccess {
out.ExcludedNoAccess++
out.StageFilteredCount++
}
if !include || line == "" {
continue
}
out.Lines = append(out.Lines, line)
}
return out, nil
}
func messageProjectID(rec ChatMessageRecord) int64 {
if rec.Metadata == nil {
return 0
}
if pVal, ok := rec.Metadata["project_id"].(int64); ok {
return pVal
}
if pVal, ok := rec.Metadata["project_id"].(float64); ok {
return int64(pVal)
}
return 0
}
func classifyDialogueLine(rec ChatMessageRecord, allowedStageKey string) (line string, include bool, excludedPrivate bool, excludedNoAccess bool) {
content := strings.TrimSpace(rec.Content)
if content == "" {
return "", false, true, false
}
if strings.HasPrefix(content, "[private]") || strings.HasPrefix(content, "[draft]") {
return "", false, true, false
}
if vis, _ := rec.Metadata["visibility"].(string); vis == "private" {
return "", false, true, false
}
stageKey := MessageStageKey(rec.Metadata)
if allowedStageKey != "" && stageKey != "" && stageKey != allowedStageKey {
return "", false, false, true
}
switch rec.Role {
case "user":
line = fmt.Sprintf("用户: %s", content)
return line, true, false, false
case "assistant":
switch rec.ContentKind {
case string(replyexport.KindActionCard), string(replyexport.KindStatusUpdate),
string(replyexport.KindErrorNotice), string(replyexport.KindSystemNotice):
return "", false, true, false
}
switch rec.ExportPolicy {
case string(replyexport.PolicyFull), string(replyexport.PolicyCopyOnly):
line = fmt.Sprintf("助手: %s", content)
return line, true, false, false
default:
return "", false, true, false
}
default:
return "", false, true, false
}
}
// MessageStageKey 从消息 metadata 提取阶段键。
func MessageStageKey(metadata map[string]any) string {
if metadata == nil {
return ""
}
if sk, ok := metadata["stage_key"].(string); ok {
return sk
}
return ""
}