| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195 |
- package service
- import (
- "crypto/sha256"
- "encoding/hex"
- "encoding/json"
- "strconv"
- "strings"
- "github.com/gin-gonic/gin"
- "github.com/tidwall/gjson"
- )
- // maxSecurityPolicyTranscriptLookupKeys 与 cyber 一致:限定单次 Redis 查询量,
- // 保留最新的 transcript 前缀(续聊最可能命中)。
- const maxSecurityPolicyTranscriptLookupKeys = 256
- // SecurityPolicySessionScopeKey 会话封禁的 Redis HASH scope。
- // 按分组+API Key 隔离:换 Key 即新身份,管理员可按 scope 整体解封,无需扫描。
- func SecurityPolicySessionScopeKey(groupID, apiKeyID int64) string {
- return "secpol:scope:" + strconv.FormatInt(groupID, 10) + ":" + strconv.FormatInt(apiKeyID, 10)
- }
- func hashSecurityPolicySessionField(raw string) string {
- sum := sha256.Sum256([]byte(raw))
- return "k:" + hex.EncodeToString(sum[:16])
- }
- // SecurityPolicyExplicitSessionKey 显式会话标识:通用头
- // (session_id/x-session-id/conversation_id)优先;OpenAI 系协议额外复用
- // 既有 cyber 显式 key(含 responses 转录回退语义)。
- func SecurityPolicyExplicitSessionKey(protocol string, apiKeyID int64, c *gin.Context, body []byte) string {
- if c != nil && c.Request != nil {
- for _, header := range []string{"session_id", "x-session-id", "conversation_id"} {
- if id := strings.TrimSpace(c.Request.Header.Get(header)); id != "" {
- return hashSecurityPolicySessionField("secpol-explicit:v1|api_key=" + strconv.FormatInt(apiKeyID, 10) + "|" + id)
- }
- }
- }
- switch protocol {
- case ContentModerationProtocolOpenAIResponses,
- ContentModerationProtocolOpenAIChat:
- if key := CyberSessionExplicitBlockKey(apiKeyID, c, body); key != "" {
- return hashSecurityPolicySessionField(key)
- }
- }
- return ""
- }
- // SecurityPolicyTranscriptKeys 会话转录 lineage key:同一对话的后续轮次必然
- // 包含已封禁前缀,从而被拦截;新会话(新 conversation/新 transcript)自然放行。
- // 为避免“相同首句”误伤不同会话,仅当转录含模型已生成内容(即续聊)时才出 key。
- func SecurityPolicyTranscriptKeys(protocol string, apiKeyID int64, body []byte) []string {
- switch protocol {
- case ContentModerationProtocolOpenAIResponses,
- ContentModerationProtocolOpenAIChat:
- return CyberSessionTranscriptBlockKeys(apiKeyID, body)
- default:
- return securityPolicyGenericTranscriptKeys(protocol, apiKeyID, body)
- }
- }
- // SecurityPolicySessionLookupKeys 本次请求全部待查 key:显式优先,其次转录。
- func SecurityPolicySessionLookupKeys(protocol string, apiKeyID int64, c *gin.Context, body []byte) []string {
- keys := make([]string, 0, 8)
- if explicit := SecurityPolicyExplicitSessionKey(protocol, apiKeyID, c, body); explicit != "" {
- keys = append(keys, explicit)
- }
- transcript := SecurityPolicyTranscriptKeys(protocol, apiKeyID, body)
- seen := make(map[string]struct{}, len(transcript)+1)
- for _, key := range keys {
- seen[key] = struct{}{}
- }
- for _, key := range transcript {
- if key == "" {
- continue
- }
- if _, dup := seen[key]; dup {
- continue
- }
- seen[key] = struct{}{}
- keys = append(keys, key)
- if len(keys) >= maxSecurityPolicyTranscriptLookupKeys+1 {
- break
- }
- }
- return keys
- }
- func securityPolicyGenericTranscriptKeys(protocol string, apiKeyID int64, body []byte) []string {
- if len(body) == 0 {
- return nil
- }
- var root gjson.Result
- switch protocol {
- case ContentModerationProtocolAnthropicMessages:
- root = gjson.GetBytes(body, "messages")
- case ContentModerationProtocolGemini:
- root = gjson.GetBytes(body, "contents")
- default:
- return nil
- }
- if !root.IsArray() {
- return nil
- }
- h := sha256.New()
- _, _ = h.Write([]byte("secpol-transcript:v1|api_key="))
- _, _ = h.Write([]byte(strconv.FormatInt(apiKeyID, 10)))
- keys := make([]string, 0, 8)
- nextSlot := 0
- rotated := false
- lastKey := ""
- hasModelGenerated := false
- root.ForEach(func(_, item gjson.Result) bool {
- role, text := securityPolicyTranscriptItemText(protocol, item)
- if strings.TrimSpace(text) == "" {
- return true
- }
- if securityPolicyTranscriptItemIsModelGenerated(protocol, role) {
- hasModelGenerated = true
- }
- canonical, _ := json.Marshal(map[string]string{"role": role, "text": text})
- _, _ = h.Write([]byte("|item="))
- _, _ = h.Write(canonical)
- lastKey = hex.EncodeToString(h.Sum(nil))
- if len(keys) < maxSecurityPolicyTranscriptLookupKeys {
- keys = append(keys, lastKey)
- } else {
- keys[nextSlot] = lastKey
- nextSlot = (nextSlot + 1) % maxSecurityPolicyTranscriptLookupKeys
- rotated = true
- }
- return true
- })
- if !hasModelGenerated {
- // 首轮(无模型历史):不输出 lineage key,避免相同首句误伤其他会话。
- return nil
- }
- if rotated {
- ordered := make([]string, 0, len(keys))
- ordered = append(ordered, keys[nextSlot:]...)
- ordered = append(ordered, keys[:nextSlot]...)
- return ordered
- }
- return keys
- }
- // securityPolicyTranscriptItemText 提取单条消息的角色与纯文本。
- func securityPolicyTranscriptItemText(protocol string, item gjson.Result) (string, string) {
- switch protocol {
- case ContentModerationProtocolAnthropicMessages:
- role := strings.ToLower(strings.TrimSpace(item.Get("role").String()))
- content := item.Get("content")
- if content.Type == gjson.String {
- return role, content.String()
- }
- var sb strings.Builder
- content.ForEach(func(_, block gjson.Result) bool {
- if text := block.Get("text"); text.Type == gjson.String && strings.TrimSpace(text.String()) != "" {
- if sb.Len() > 0 {
- sb.WriteString("\n")
- }
- sb.WriteString(text.String())
- }
- return true
- })
- return role, sb.String()
- default: // gemini contents[].parts[].text
- role := strings.ToLower(strings.TrimSpace(item.Get("role").String()))
- var sb strings.Builder
- item.Get("parts").ForEach(func(_, part gjson.Result) bool {
- if text := part.Get("text"); text.Type == gjson.String && strings.TrimSpace(text.String()) != "" {
- if sb.Len() > 0 {
- sb.WriteString("\n")
- }
- sb.WriteString(text.String())
- }
- return true
- })
- return role, sb.String()
- }
- }
- func securityPolicyTranscriptItemIsModelGenerated(protocol, role string) bool {
- switch role {
- case "assistant", "model":
- return true
- }
- return false
- }
- // SecurityPolicySessionStoreKeyForAdmin 供管理端按分组+Key 解封时复用同一 scope。
- func SecurityPolicySessionStoreKeyForAdmin(groupID, apiKeyID int64) string {
- return SecurityPolicySessionScopeKey(groupID, apiKeyID)
- }
|