// Copyright 2022 E99p1ant. All rights reserved. // Use of this source code is governed by a MIT-style // license that can be found in the LICENSE file. package censor import ( "context" "encoding/json" "fmt" "strings" ) type ForbiddenType string const ( ForbiddenTypeSpam ForbiddenType = "spam" ForbiddenTypeAd ForbiddenType = "ad" ForbiddenTypePolitics ForbiddenType = "politics" ForbiddenTypeTerrorism ForbiddenType = "terrorism" ForbiddenTypeAbuse ForbiddenType = "abuse" ForbiddenTypePorn ForbiddenType = "porn" ForbiddenTypeFlood ForbiddenType = "flood" ForbiddenTypeContraband ForbiddenType = "contraband" ForbiddenTypeMeaningless ForbiddenType = "meaningless" ForbiddenTypeHarmful ForbiddenType = "harmful" // ForbiddenTypeCustomized 命中服务侧自定义词库(阿里云控制台配置,七牛无服务端词库)。 // 属运营方显式规则,在 policy.go 中归入底线集合,任何档位都不放行。 ForbiddenTypeCustomized ForbiddenType = "customized" ) func (f ForbiddenType) String() string { return map[ForbiddenType]string{ ForbiddenTypeSpam: "含垃圾信息", ForbiddenTypeAd: "广告", ForbiddenTypePolitics: "涉政", ForbiddenTypeTerrorism: "暴恐", ForbiddenTypeAbuse: "辱骂", ForbiddenTypePorn: "色情", ForbiddenTypeFlood: "灌水", ForbiddenTypeContraband: "违禁", ForbiddenTypeMeaningless: "无意义", ForbiddenTypeHarmful: "不良场景", ForbiddenTypeCustomized: "自定义词库", }[f] } // LabelScore 单个维度(label)的判定与置信度。过去只取首个非 normal 维度, // 丢掉了其余维度信息,无法按维度差异化处置(例如只拦高置信度的辱骂)。 // // ⚠️ Score 统一归一化到 0~1:七牛本身即 0~1;阿里云 2.0 无逐标签置信度,只给整条内容的 // riskLevel,由 AliyunTextCensorParser 折算(high=1 / medium=0.6 / low=0.3)后该响应内所有 // 标签共用——有损近似。policy.go 的阈值因此对两家都成立(对阿里云语义为「按风险级别分级」)。 type LabelScore struct { Label ForbiddenType `json:"label"` Score float64 `json:"score"` } type TextCensorResponse struct { SourceName string `json:"source_name"` Pass bool `json:"pass"` ForbiddenType ForbiddenType `json:"forbidden_type"` Hint string `json:"hint"` Confidence float64 `json:"confidence"` RawResponse json.RawMessage `json:"raw_response"` Details []LabelScore `json:"details,omitempty"` // 全部非 normal 维度,供严格度策略裁决;由各 parser 从 RawResponse 重建,缓存回放同样有值(故改档位对历史缓存也生效) // ProviderSuggestion 服务方管控建议(block / review / pass)。不携带维度、无法做 // 置信度收敛,因此只在严格档被采信(见 policy.go)。 ProviderSuggestion string `json:"provider_suggestion,omitempty"` } func (r *TextCensorResponse) ToJSON() []byte { jsonBytes, _ := json.Marshal(r) return jsonBytes } // ErrorMessageFor 按真正命中的维度生成拒绝提示——已移除的 ErrorMessage 固定用首个非 // normal 维度,而触发拦截的可能是另一维度(策略层按档位裁决),会导致文案对不上。 // detail 为 nil(由服务方整体 block 建议触发)时退化为不带维度的基础文案。 func (r *TextCensorResponse) ErrorMessageFor(detail *LabelScore) string { errorMessage := "内容安全检查不通过" if detail != nil && detail.Label != "" { errorMessage += fmt.Sprintf(" [%s]", detail.Label.String()) } if r.Hint != "" { errorMessage += fmt.Sprintf(",相关标签:%q", r.Hint) } return errorMessage } type TextCensor interface { Censor(ctx context.Context, text string) (*TextCensorResponse, error) String() string } var TrustedHosts = []string{ "xhslink.com", // 小红书 "weibo.com", // 微博 "b23.tv", // Bilibili "acfun.cn", "aixifan.com", "github.com", // 旧域 tama.guru 保留至 2026-12 到期,过渡期内旧分享链接仍需视为可信。 "tama.guru", "shiroko.one", "337845818.best", "x.com", "bilibili.com", } // RemoveTrustedURL 移除可信域名以免链接被判为广告。 // 仅当可信域名后紧跟路径分隔符(/ ? #)或位于串尾时才视为可信, // 避免 github.com.evil.tld 被朴素替换误判为 github.com 子域而漏审。 func RemoveTrustedURL(str string) string { for _, host := range TrustedHosts { for _, protocol := range []string{"http://", "https://"} { needle := protocol + host var b strings.Builder rest := str for { idx := strings.Index(rest, needle) if idx < 0 { b.WriteString(rest) break } b.WriteString(rest[:idx]) after := rest[idx+len(needle):] if len(after) == 0 || after[0] == '/' || after[0] == '?' || after[0] == '#' { // 可信域名边界:跳过该前缀,从域名之后继续(域名本身被移除,不进入审核)。 rest = after } else { // 非可信域名边界(如 github.com.evil):保留前缀,从其后继续扫描。 b.WriteString(needle) rest = after } } str = b.String() } } return str }