Files
tamabox/internal/security/moderation/moderation.go
T
2026-09-10 18:04:19 +08:00

72 lines
3.0 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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 moderation 提供跨场景统一的内容审核守卫。
//
// 存在意义:提问、投票、私聊三条链路都要对用户输入做内容审核,过去各自调用
// censor.Text 并自行解释结果,判定口径一旦调整就要改多处,极易遗漏。
// 本包收敛为唯一入口:调审核 → 按箱主「审核严格度」裁决 → 返回是否放行。
package moderation
import (
"context"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
"github.com/tamakyi/TamaBox/internal/conf"
"github.com/tamakyi/TamaBox/internal/i18n"
"github.com/tamakyi/TamaBox/internal/security/censor"
)
// StrictnessFor 解析账号最终生效的严格度档位:
// - 账号显式设置了档位(lax / standard / strict)→ 用该档位;
// - 账号未设置(空值,存量账号的默认形态)→ 跟随站点默认
// [security] default_moderation_strictness;
// - 站点默认同样为空或非法 → 回退 censor.DefaultStrictness(严格)。
//
// 回退方向刻意取「严格」:配置缺失或非法时宁可从严,也不要因为识别失败
// 把全站静默切到最松档(那等于在配置出错时悄悄放大漏放面)。
func StrictnessFor(raw string) censor.Strictness {
if raw != "" {
return censor.ParseStrictness(raw)
}
return censor.ParseStrictness(conf.Security.DefaultModerationStrictness)
}
// Check 执行一次内容审核,并按箱主严格度裁决是否放行。
//
// 这是提问 / 投票 / 私聊三类场景**唯一**应当使用的审核入口。
//
// 返回:
// - resp:审核明细,调用方用于落 content_censor_metadata 等审计字段;裁决失败时可能为 nil。
// - err != nil:应当拒绝本次提交,错误信息已按 lang 本地化,可直接展示给用户。
//
// 审核服务不可用(超时/报错)时仍为 fail-closed 拒绝提交:这是引入本能力之前就存在的
// 行为,不在本次改造范围内(相关可用性问题另见 outputs/llm-moderation-feasibility 评审)。
func Check(ctx context.Context, strictness censor.Strictness, lang, text string) (*censor.TextCensorResponse, error) {
resp, err := censor.Text(ctx, text)
if err != nil {
logrus.WithContext(ctx).WithError(err).
WithField("strictness", string(strictness)).
Error("Failed to censor text")
return nil, errors.New(i18n.T(lang, "censor.unavailable"))
}
if resp == nil {
return nil, errors.New(i18n.T(lang, "censor.unavailable"))
}
decision, detail := censor.Decide(string(strictness), resp)
if decision.Blocked() {
logrus.WithContext(ctx).
WithField("strictness", string(strictness)).
WithField("source", resp.SourceName).
WithField("forbidden_type", resp.ForbiddenType).
Info("Text blocked by moderation strictness policy")
return resp, errors.New(resp.ErrorMessageFor(detail))
}
return resp, nil
}