aliyun sensor server

This commit is contained in:
tamakyi
2026-09-10 18:04:19 +08:00
parent e3e59e7c69
commit 8e2325eda5
38 changed files with 3147 additions and 344 deletions
+216
View File
@@ -0,0 +1,216 @@
// 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 建议。
//
// 这是三个档位中最严的一档,且是唯一保留「采信审核服务整体 block 建议」的档位。
// 改造前的拦截条件是 `!Pass`(即 涉政/暴恐/色情 命中,或审核服务建议为 block),
// 该条件可被本档完全覆盖,但本档会额外拦截全部带维度的非正常内容(如 review 的辱骂)。
// 换言之:严格档 ⊇ 改造前的行为,是「至少与改造前一样严」,并非完全等价。
StrictnessStrict Strictness = "strict"
)
// DefaultStrictness 缺省档位。
//
// 取 strict 的取舍:本能力只用于「让箱主把审核调松」,从不用于「在用户未选择时把审核调松」。
// 存量账号从未设置过该值,缺省落到最严档,保证部署本功能这一动作本身不会让任何账号
// 的拦截面变小(合规上更保守);想减少误伤的箱主再显式切到 标准 / 宽松。
//
// ⚠️ 注意方向:因为严格档比改造前更严(见 StrictnessStrict 注释),缺省为 strict 意味着
// 部署后默认拦截面会略有扩大 —— 这是刻意的取舍(已于 2026-09-10 确认维持 strict):
// 合规上宁紧勿松,且「调松」应当是箱主的显式动作,部署本身不应悄悄放宽任何账号。
// 若哪天希望默认更少误伤,把下面这行改成 StrictnessStandard 即可(一行常量)。
const DefaultStrictness = StrictnessStrict
// Suggestion 审核服务自身给出的管控建议取值。
const (
SuggestionBlock = "block"
SuggestionReview = "review"
SuggestionPass = "pass"
)
// baselineBlockLabels 底线拦截维度:涉政 / 暴恐 / 色情 / 自定义词库。
//
// 这是合规底线:任何档位下都强制生效、不受置信度门槛约束、不可关闭,
// 也不能通过任何用户配置绕过 —— 见 Decide 中它始终排在最先、无条件返回拦截。
//
// ForbiddenTypeCustomized 归入底线的原因:它是运营方在审核服务控制台(阿里云)
// 显式配置的词库命中,属「用户明写的规则」,与箱主自定义屏蔽词同一性质,
// 不应被档位调低后放行。运营方若要放开,正确的做法是调整控制台词库,而不是调档位。
var baselineBlockLabels = map[ForbiddenType]struct{}{
ForbiddenTypePolitics: {},
ForbiddenTypeTerrorism: {},
ForbiddenTypePorn: {},
ForbiddenTypeCustomized: {},
}
// abuseScoreFloor 标准档位下「辱骂」维度生效所需的最低置信度。
//
// 辱骂是审核服务误伤最集中的维度(普通吐槽、语气较冲的表达常被判为辱骂),
// 因此标准档要求高置信度才拦截;严格档则为任意置信度。
//
// 注:置信度的来源因服务商而异。七牛直接给 0~1 的 score;阿里云内容安全 2.0 只给
// 整条内容的 riskLevel,折算为 1 / 0.6 / 0.3(high / medium / low),因此本阈值
// 在阿里云下的实际语义是「标准档只拦 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,
},
// 严格:底线 + 全部非正常维度(不限置信度)+ 采信审核服务 block 建议。
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)。
//
// 判定顺序固定为「底线维度 → 档位维度 → 审核服务建议」,且不可调换:
// 1. 底线维度排在最前且无条件返回拦截,保证任何档位都无法把它关掉;
// 2. 档位维度带各自的置信度门槛,用于收敛误伤最集中的维度;
// 3. 审核服务自身的 block 建议排在最后,且仅严格档启用。
//
// Details 为空(例如审核服务返回了 suggestion=block 但没给出任何非 normal 维度)时,
// 只有开启 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 返回指定档位最终会拦截的全部维度(含底线维度)。
//
// 用途:策略层的自描述 API —— 目前由 policy_test.go 用来守住「底线维度在每个档位都必须出现
// 且不重复」这条不变量。设置页展示的拦截范围是 i18n 静态文案
// (templates/user/moderation-strictness.html),尚未消费本函数;若将来要让 UI 直接由策略层
// 生成文案,这里是唯一事实来源。
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
}