Files
sub2api/backend/internal/service/openai_cyber_policy.go
T
DaydreamCodingandClaude Opus 4.8 b62b573f7f feat(openai): cyber_policy 硬阻断全链路透传、审计与计费
上游对单次请求下发 error.code=cyber_policy 硬阻断时,网关在所有端点
(/v1/responses、/v1/chat/completions、/v1/messages、WebSocket)及流式/
非流式路径下,将该结果原样透传给客户端,绝不 failover、换号或同步拦截;
命中后异步完成审计与计费:

- 风控中心记录 cyber_policy 留痕并发送通知邮件,落库先于发信,SMTP 阻塞
  不影响留痕
- ops 错误请求记录,状态码对齐客户端实际接收(流式 200 / 非流式 400)
- 用量明细标记 request_type=cyber,按上游真实 token 计费,HTTP 与
  WebSocket 计费口径统一,零 token 命中不误扣
- 会话级自动屏蔽(管理员开关,默认关):命中的会话在可配 TTL 内本地拦截
  不再发往上游,仅屏蔽该会话不影响同 Key 其他会话
- 封号计数排除开关:可选让 cyber 命中不计入自动封号,命中当次不判定且
  历史行在违规计数中一并排除

WebSocket 多轮连接下 cyber 标记按 turn 生命周期管理,逐轮独立检测与记录;
透传的错误响应不被兜底逻辑追加内容污染。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-12 01:47:01 +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)
}