Add dialoguectx packaging, conflictresolve adjudication with HITL routing, workspace context/dialogue/ injection (default on), and goose 00071.
123 lines
3.2 KiB
Go
123 lines
3.2 KiB
Go
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 ""
|
||
}
|