170 lines
4.6 KiB
Go
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 ""
|
|
}
|