Files
sub2api/backend/internal/service/openai_cyber_policy.go
T

89 lines
3.5 KiB
Go
Raw Normal View History

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)
}