fix anwser token and database issus

This commit is contained in:
tamakyi
2026-09-15 11:06:09 +08:00
parent e683e798aa
commit adcc42cc8c
33 changed files with 1011 additions and 802 deletions
+30 -33
View File
@@ -24,13 +24,10 @@ var (
warnNoCensorCredentialsOnce sync.Once
)
// censorChain 返回按配置选定的审核通道列表,顺序即优先级:
// 靠前的通道判定「通过」即返回;未通过则继续交给后面的通道复核。
//
// 取值来源 [security] text_censor_source(见 conf.CensorSource*):
// - qiniu → 只用七牛(凭据齐备性已由 conf.Init 启动校验)
// - aliyun → 只用阿里云(同上)
// - 其它(含留空 / auto)→ 七牛优先、阿里云兜底,与引入该配置前完全一致
// censorChain 返回按 [security] text_censor_source 选定的通道列表,顺序即优先级:
// 靠前通道判定通过即返回,未通过则交给后面的通道复核。
// qiniu / aliyun 只用对应通道(凭据齐备性已由 conf.Init 启动校验);
// 其它取值(含留空 / auto)为七牛优先、阿里云兜底,与引入该配置前一致。
func censorChain() []TextCensor {
qiniu := NewQiniuTextCensor(conf.App.QiniuAccessKey, conf.App.QiniuAccessSecret)
aliyun := NewAliyunTextCensor(conf.App.AliyunAccessKey, conf.App.AliyunAccessKeySecret)
@@ -45,11 +42,9 @@ func censorChain() []TextCensor {
}
}
// warnIfNoCensorCredentials 在「配置里一条可用凭据都没有」时告警一次。
//
// 这种情况下审核调用必然失败,而调用方是 fail-closed(拒绝提交),表现为
// 全站发不出内容/私信——现象隐蔽、只在日志可见,因此第一次触发就留下明确线索。
// 显式选定通道但缺凭据的情况已由 conf.Init 直接挡住,这里兜的是 auto 的漏网情况。
// warnIfNoCensorCredentials 在一条可用凭据都没有时告警一次:此时审核必然失败,
// 而调用方是 fail-closed(拒绝提交),表现为全站发不出内容/私信且只在日志可见。
// 显式选定通道但缺凭据已由 conf.Init 挡住,这里兜的是 auto 的漏网情况。
func warnIfNoCensorCredentials(ctx context.Context) {
qiniuReady := conf.App.QiniuAccessKey != "" && conf.App.QiniuAccessSecret != ""
aliyunReady := conf.App.AliyunAccessKey != "" && conf.App.AliyunAccessKeySecret != ""
@@ -62,35 +57,35 @@ func warnIfNoCensorCredentials(ctx context.Context) {
})
}
// Text checks the text for sensitive content.
// It will save the censor log to the database and invoke the callback function.
// Text 检查文本是否含敏感内容,并把审核结果落库(31 天内可回放复用)。
func Text(ctx context.Context, text string) (*TextCensorResponse, error) {
if !conf.Security.EnableTextCensor {
return &TextCensorResponse{Pass: true}, nil
}
// 缓存键与送审内容统一用归一化文本(已剔除可信域名链接):否则缓存按原文命中、
// 实际送审的却是归一化结果 —— 审计对不上,且归一化后相同的原文会各占一条缓存、重复调 API。
normalized := RemoveTrustedURL(text)
var responses []*TextCensorResponse
warnIfNoCensorCredentials(ctx)
for _, censor := range censorChain() {
sourceName := censor.String()
// Try to get the censor from the database log.
censorLog, err := db.CensorLogs.GetByText(ctx, sourceName, text, time.Now().Add(-censorCacheNoMoreThan))
censorLog, err := db.CensorLogs.GetByText(ctx, sourceName, normalized, time.Now().Add(-censorCacheNoMoreThan))
if err != nil {
if !errors.Is(err, db.ErrCensorLogsNotFound) {
logrus.WithContext(ctx).WithError(err).Error("Failed to get censor log")
}
} else {
raw := censorLog.RawResponse
// We got the previous censor log cache, seems like we saved money for the API call.
switch censorLog.SourceName {
case "qiniu":
// 七牛缓存命中:只有「通过」才可直接复用(省一次 API 调用)。
// 未通过时不在此下最终结论,而是交给链上的下一个通道复核(auto 模式下即阿里云,
// 这是引入通道配置前就有的行为)。
// text_censor_source = qiniu 时链上只有七牛:continue 后循环结束,
// 仍会由函数末尾返回这份七牛结果,判定不会丢失。
// 只有「通过」才直接复用(省一次 API 调用);未通过则交给链上下一个通道复核
// (auto 模式下即阿里云,引入通道配置前就是如此)。
// text_censor_source=qiniu 时链上只有七牛,continue 后循环结束,
// 仍由函数末尾返回这份结果,判定不会丢失。
response, err := QiniuTextCensorParser(raw)
if err == nil {
if response.Pass {
@@ -102,14 +97,19 @@ func Text(ctx context.Context, text string) (*TextCensorResponse, error) {
}
case "aliyun":
// 阿里云缓存命中:能解析就直接复用(省一次 API 调用)。
// 解析失败必须退回一次实时调用,而不是把错误往上抛:
// 最典型的失败源是「迁移到内容安全 2.0 之前落库的 1.0 缓存」,
// 它无法按 2.0 语义解释;若在此返回错误,fail-closed 会让这些
// 历史文本在缓存过期(31 天)前一直发不出去。
// 能解析就复用;解析失败必须退回实时调用而不是把错误往上抛:
// 最典型的失败源是迁到内容安全 2.0 前落库的 1.0 缓存,无法按 2.0 语义解释,
// 若在此报错,fail-closed 会让这些历史文本在缓存过期前一直发不出去。
// 与七牛一致:仅「通过」才直接返回,未通过则交给链上下一个通道复核
// (阿里云现为链尾,continue 后循环结束,仍由函数末尾返回同一份结果)。
response, err := AliyunTextCensorParser(raw)
if err == nil {
return response, nil
if response.Pass {
return response, nil
}
responses = append(responses, response)
continue
}
logrus.WithContext(ctx).WithError(err).WithField("censor_source", sourceName).
Warn("Failed to replay cached aliyun censor result; falling back to a live call")
@@ -119,19 +119,16 @@ func Text(ctx context.Context, text string) (*TextCensorResponse, error) {
}
}
// response, err := censor.Censor(ctx, text)
censorText := RemoveTrustedURL(text)
response, err := censor.Censor(ctx, censorText)
response, err := censor.Censor(ctx, normalized)
if err != nil {
logrus.WithContext(ctx).WithError(err).WithField("censor_source", sourceName).Error("Failed to censor text")
continue
}
responses = append(responses, response)
// Save the censor log to the database.
if err := db.CensorLogs.Create(ctx, db.CreateCensorLogOptions{
SourceName: sourceName,
Input: text,
Input: normalized,
Pass: response.Pass,
RawResponse: response.RawResponse,
}); err != nil {