package contentpolicy import ( "bytes" "encoding/json" "strings" ) // OutputRedact 对一条模型响应 JSON(完整响应或 SSE data 载荷)应用全部 // redact 策略的规则:提取 choices[].message.content / choices[].delta.content // 文本并替换敏感信息。与输入侧不同,输出 JSON 是给客户端消费的展示数据, // 重编码可接受。返回替换后的字节与是否发生替换。 func (e *Engine) OutputRedact(payload []byte) ([]byte, bool) { if e == nil || len(payload) == 0 { return payload, false } rules := e.outputRules() if len(rules) == 0 { return payload, false } var document any decoder := json.NewDecoder(bytes.NewReader(payload)) decoder.UseNumber() if decoder.Decode(&document) != nil { return payload, false } changed := false redactTree(&document, &changed, rules) if !changed { return payload, false } encoded, err := json.Marshal(document) if err != nil { return payload, false } return encoded, true } // outputRules 返回全部启用策略的 redact 规则(输出侧不区分端点)。 func (e *Engine) outputRules() []compiledRule { current := e.current.Load() if current == nil { return nil } var rules []compiledRule for _, policy := range current.policies { if policy.Action == "redact" { rules = append(rules, policy.rules...) } } return rules } // redactTree 递归遍历 JSON,仅对 content 文本应用规则。 func redactTree(value *any, changed *bool, rules []compiledRule) { switch current := (*value).(type) { case map[string]any: for key, child := range current { local := child if strings.ToLower(key) == "content" { if text, ok := local.(string); ok { next := text for _, rule := range rules { if rule.expression.MatchString(next) { next = rule.expression.ReplaceAllString(next, rule.replacement) } } if next != text { *changed = true local = next } } } redactTree(&local, changed, rules) current[key] = local } case []any: for index, child := range current { local := child redactTree(&local, changed, rules) current[index] = local } } }