// 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 // Strictness 内容审核严格度档位。 // 审核服务调不松、不可配白名单,可控的只有「命中后拦不拦」,故把拦截从 Pass 判定剥离、改由本档位裁决。 type Strictness string const ( // StrictnessLax 宽松:仅拦底线维度,误伤最小、漏放最多。 StrictnessLax Strictness = "lax" // StrictnessStandard 标准:底线 + 高置信度的辱骂/违禁。 StrictnessStandard Strictness = "standard" // StrictnessStrict 严格:底线 + 全部非正常维度(不限置信度)+ 采信服务 block 建议。 // 严格档 ⊇ 改造前行为(改造前为 !Pass),「至少与改造前一样严」,并非完全等价。 StrictnessStrict Strictness = "strict" ) // DefaultStrictness 缺省取 strict:部署不应悄悄放宽任何账号,「调松」必须是箱主显式动作; // 想默认更少误伤可改 Standard。 const DefaultStrictness = StrictnessStrict // Suggestion 审核服务自身给出的管控建议取值。 const ( SuggestionBlock = "block" SuggestionReview = "review" SuggestionPass = "pass" ) // baselineBlockLabels 底线拦截维度:涉政/暴恐/色情/自定义词库。 // 合规底线:任何档位强制生效、不受置信度约束、不可关闭、不可绕过(见 Decide 最先判定)。 // Customized 归入底线:它是控制台显式配置的词库,要放开应改控制台词库而非调档位。 var baselineBlockLabels = map[ForbiddenType]struct{}{ ForbiddenTypePolitics: {}, ForbiddenTypeTerrorism: {}, ForbiddenTypePorn: {}, ForbiddenTypeCustomized: {}, } // abuseScoreFloor 标准档下「辱骂」生效所需最低置信度。 // 辱骂误伤最集中(吐槽常被判辱骂),故标准档要求高置信度;严格档不限。阿里云 2.0 只给 riskLevel,下语义为「只拦 high」。 const abuseScoreFloor = 0.9 // levelPolicy 单一档位的拦截规则。 type levelPolicy struct { // blockLabels 该档位额外拦截的维度 -> 生效最低置信度(0 表示不限)。底线维度不在此表,由 Decide 前置判定。 blockLabels map[ForbiddenType]float64 // blockOnSuggestion 是否采信服务自身的 block 建议:不携带维度、无法收敛置信度,误伤面最大,仅严格档启用。 blockOnSuggestion bool } var strictnessPolicies = map[Strictness]levelPolicy{ StrictnessLax: { blockLabels: map[ForbiddenType]float64{}, blockOnSuggestion: false, }, StrictnessStandard: { blockLabels: map[ForbiddenType]float64{ ForbiddenTypeAbuse: abuseScoreFloor, ForbiddenTypeContraband: 0, }, blockOnSuggestion: false, }, StrictnessStrict: { blockLabels: map[ForbiddenType]float64{ ForbiddenTypeSpam: 0, ForbiddenTypeAd: 0, ForbiddenTypeAbuse: 0, ForbiddenTypeFlood: 0, ForbiddenTypeContraband: 0, ForbiddenTypeMeaningless: 0, ForbiddenTypeHarmful: 0, }, blockOnSuggestion: true, }, } // Decision 对单条内容的最终裁决。 type Decision int const ( // DecisionAllow 放行。 DecisionAllow Decision = iota // DecisionBlock 拒绝提交。 DecisionBlock ) // Blocked 是否应当拒绝提交。 func (d Decision) Blocked() bool { return d == DecisionBlock } // IsValidStrictness 校验档位取值合法性,供后台保存前校验; // 非法值一律拒绝写入,避免脏数据把账号静默切到非预期档位。 func IsValidStrictness(s string) bool { switch Strictness(s) { case StrictnessLax, StrictnessStandard, StrictnessStrict: return true default: return false } } // ParseStrictness 归一化严格度,未知/空值回退 DefaultStrictness(最严方向); // 宁可沿用严格判定,也不要因识别失败静默放到最松档。 func ParseStrictness(s string) Strictness { if !IsValidStrictness(s) { return DefaultStrictness } return Strictness(s) } // Decide 按档位裁决一次审核结果,返回裁决与触发拦截的维度(无具体维度时 nil)。 // 判定顺序固定不可调换:底线(无条件)→ 档位维度(带置信度门槛)→ 服务 block 建议(仅严格档)。 // Details 为空时仅开启 blockOnSuggestion 的档位会拦截。 func Decide(strictness string, r *TextCensorResponse) (Decision, *LabelScore) { if r == nil { return DecisionAllow, nil } policy := strictnessPolicies[ParseStrictness(strictness)] // 1. 底线:任何档位强制生效、不可关闭。 for i := range r.Details { detail := &r.Details[i] if _, ok := baselineBlockLabels[detail.Label]; ok { return DecisionBlock, detail } } // 2. 档位维度:命中维度且达到置信度门槛。 for i := range r.Details { detail := &r.Details[i] scoreFloor, ok := policy.blockLabels[detail.Label] if !ok { continue } if scoreFloor > 0 && detail.Score < scoreFloor { continue } return DecisionBlock, detail } // 3. 服务 block 建议:仅严格档采信。 if policy.blockOnSuggestion && r.ProviderSuggestion == SuggestionBlock { return DecisionBlock, nil } return DecisionAllow, nil } // BlockedLabels 返回指定档位最终拦截的全部维度(含底线)。 // 不变量由 policy_test.go 守住(底线维度在每个档位都出现且不重复);设置页文案仍是 i18n 静态文本,未消费本函数。 func BlockedLabels(strictness string) []ForbiddenType { policy := strictnessPolicies[ParseStrictness(strictness)] labels := make([]ForbiddenType, 0, len(baselineBlockLabels)+len(policy.blockLabels)) for label := range baselineBlockLabels { labels = append(labels, label) } for label := range policy.blockLabels { // 底线维度已在上面加入,避免重复展示。 if _, ok := baselineBlockLabels[label]; ok { continue } labels = append(labels, label) } return labels }