Files
sub2api/backend/internal/service/openai_cyber_policy.go
T
李建琦 6d655c9903
Release / update-version (push) Has been cancelled
Release / build-frontend (push) Has been cancelled
Release / release (push) Has been cancelled
Release / sync-version-file (push) Has been cancelled
CI / shell (push) Canceled after 0s
CI / test (push) Canceled after 0s
CI / frontend (push) Canceled after 0s
CI / golangci-lint (push) Canceled after 0s
Security Scan / backend-security (push) Canceled after 0s
Security Scan / frontend-security (push) Canceled after 0s
Sub2API v1.0 - AI API 网关(二开初始版本,基于上游 Wei-Shaw/sub2api)
2026-08-21 18:30:13 +08:00

89 lines
3.5 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.
package service
import (
"errors"
"strings"
"github.com/gin-gonic/gin"
"github.com/tidwall/gjson"
)
// opsCyberPolicyKey 在 gin context 中携带 cyber_policy 命中标记。
// 由 gateway 服务层在检测到上游 error.code=="cyber_policy" 时设置,
// handler 在 Forward 返回后读取以触发风控记录、邮件与 tokens=0 用量行。
const opsCyberPolicyKey = "ops_cyber_policy"
// errOpenAICyberPolicyForwarded 表示 cyber_policy 已按当前端点格式透传给客户端
// error 已写出/下发)。compat 路径 ForwardAsChatCompletions / ForwardAsAnthropic 出口
// 据此丢弃 result 并返回该哨兵,使 handler 落入 tokens=0 免费用量行(对齐 /v1/responses),
// 既不计费、也不 failover、不重复写响应。
var errOpenAICyberPolicyForwarded = errors.New("openai cyber_policy forwarded to client")
// CyberPolicyMark 记录一次 cyber_policy 硬阻断的上游证据。
type CyberPolicyMark struct {
Code string // 固定 "cyber_policy"
Message string // 上游 error.message
Body string // 上游 response.failed / 400 原始 body(已截断;未脱敏,ops_error 落库由 sanitizeErrorBodyForStorage、风控日志由 redactContentModerationSecrets 统一脱敏)
UpstreamStatus int // 上游 HTTP 状态(流式=200,非流式=400)
UpstreamInTok int // 上游已报 input tokens(如有)
UpstreamOutTok int // 上游已报 output tokens(如有)
}
// MarkOpsCyberPolicy 记录 cyber 标记;首个写入生效,后续忽略(同一 turn 只记一次)。
// WS 多轮场景由 handler 在每个 turn 结束后调用 ClearOpsCyberPolicy 重置。
func MarkOpsCyberPolicy(c *gin.Context, mark CyberPolicyMark) {
if c == nil {
return
}
if GetOpsCyberPolicy(c) != nil {
return
}
mark.Code = "cyber_policy"
mark.Message = strings.TrimSpace(mark.Message)
mark.Body = strings.TrimSpace(mark.Body)
c.Set(opsCyberPolicyKey, &mark)
}
// GetOpsCyberPolicy 返回 cyber 标记,未命中(或已被 Clear)返回 nil。
func GetOpsCyberPolicy(c *gin.Context) *CyberPolicyMark {
if c == nil {
return nil
}
if v, ok := c.Get(opsCyberPolicyKey); ok {
if m, ok := v.(*CyberPolicyMark); ok && m != nil {
return m
}
}
return nil
}
// ClearOpsCyberPolicy 清除 cyber 标记(typed-nil 覆盖;gin context 无并发安全的
// 删除原语,Set 走内部锁,与异步 GetOpsCyberPolicy 不构成 data race)。
// 仅 WS 多轮路径在 turn 收尾调用;HTTP 单请求路径不调用(context 随请求销毁,
// 且中间件 shouldSkipOpsErrorLogForCyber 依赖标记防双写)。
// WS 路径 clear 发生在中间件收尾之前,连接响应状态为 101,不触发中间件 status>=400
// 落库分支,故无双写/漏写。
func ClearOpsCyberPolicy(c *gin.Context) {
if c == nil {
return
}
c.Set(opsCyberPolicyKey, (*CyberPolicyMark)(nil))
}
// detectOpenAICyberPolicy 精确识别 cyber_policy(对齐 codex api_bridge.rs:145 /
// sse/responses.rs:529)。命中返回 (true, "cyber_policy", message)。
func detectOpenAICyberPolicy(payload []byte) (bool, string, string) {
code := gjson.GetBytes(payload, "error.code").String()
if code == "" {
code = gjson.GetBytes(payload, "response.error.code").String()
}
if !strings.EqualFold(strings.TrimSpace(code), "cyber_policy") {
return false, "", ""
}
msg := gjson.GetBytes(payload, "error.message").String()
if msg == "" {
msg = gjson.GetBytes(payload, "response.error.message").String()
}
return true, "cyber_policy", strings.TrimSpace(msg)
}