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
89 lines
3.5 KiB
Go
89 lines
3.5 KiB
Go
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)
|
||
}
|