package dialoguectx import ( "crypto/sha256" "encoding/hex" "strings" "unicode/utf8" "chat-agent-system/internal/replyexport" ) // cleanResult is the outcome of cleaning a raw message. type cleanResult struct { Include bool Content string Noise bool Private bool NoAccess bool } // cleanMessage applies visibility, export-policy, noise, and R13 neutralization. func cleanMessage(role, content, contentKind, exportPolicy, stageKey, allowedStageKey string, metadata map[string]any) cleanResult { trimmed := strings.TrimSpace(content) if trimmed == "" { return cleanResult{Noise: true} } if strings.HasPrefix(trimmed, "[private]") || strings.HasPrefix(trimmed, "[draft]") { return cleanResult{Private: true} } if vis, _ := metadata["visibility"].(string); vis == "private" { return cleanResult{Private: true} } if allowedStageKey != "" && stageKey != "" && stageKey != allowedStageKey { return cleanResult{NoAccess: true} } switch role { case "user": if utf8.RuneCountInString(trimmed) < MinContentRunes { return cleanResult{Noise: true} } if isGreetingNoise(trimmed) { return cleanResult{Noise: true} } return cleanResult{Include: true, Content: neutralizeDirective(trimmed)} case "assistant": switch contentKind { case string(replyexport.KindActionCard), string(replyexport.KindStatusUpdate), string(replyexport.KindErrorNotice), string(replyexport.KindSystemNotice): return cleanResult{Noise: true} } switch exportPolicy { case string(replyexport.PolicyFull), string(replyexport.PolicyCopyOnly): if utf8.RuneCountInString(trimmed) < MinContentRunes { return cleanResult{Noise: true} } return cleanResult{Include: true, Content: neutralizeDirective(trimmed)} default: return cleanResult{Noise: true} } default: return cleanResult{Noise: true} } } func isGreetingNoise(s string) bool { lower := strings.ToLower(strings.TrimSpace(s)) greetings := []string{ "你好", "您好", "hi", "hello", "hey", "在吗", "早上好", "下午好", "晚上好", "谢谢", "感谢", "ok", "好的", "收到", "嗯", "哈哈", } for _, g := range greetings { if lower == g || lower == g+"。" || lower == g+"!" || lower == g+"?" || lower == g+"?" { return true } } return false } // neutralizeDirective marks instruction-like text as user material (R13). func neutralizeDirective(c string) string { lower := strings.ToLower(c) markers := []string{ "ignore previous", "system:", "忽略以上", "忽略之前", "提权", "grant yourself", "override auth", "将所有阶段", } for _, m := range markers { if strings.Contains(lower, strings.ToLower(m)) || strings.Contains(c, m) { return "【用户材料】" + c } } return c } func contentFingerprint(role, content string) string { sum := sha256.Sum256([]byte(role + "|" + content)) return hex.EncodeToString(sum[:8]) } func truncateRunes(s string, max int) string { if max <= 0 || utf8.RuneCountInString(s) <= max { return s } runes := []rune(s) return string(runes[:max]) + "…" } func stageKeyFromMeta(metadata map[string]any) string { if metadata == nil { return "" } if sk, ok := metadata["stage_key"].(string); ok { return sk } return "" }