mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-11-12 22:42:34 +00:00
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>
This commit is contained in:
1 parent
e34ad2b194
commit
b62b573f7f
56 files changed
+3036
-184
No files matched your search
@@ -34,6 +34,7 @@ type OpenAIGatewayHandler struct {
|
||||
usageRecordWorkerPool *service.UsageRecordWorkerPool
|
||||
errorPassthroughService *service.ErrorPassthroughService
|
||||
contentModerationService *service.ContentModerationService
|
||||
opsService *service.OpsService
|
||||
concurrencyHelper *ConcurrencyHelper
|
||||
imageLimiter *imageConcurrencyLimiter
|
||||
maxAccountSwitches int
|
||||
@@ -105,6 +106,7 @@ func NewOpenAIGatewayHandler(
|
||||
usageRecordWorkerPool *service.UsageRecordWorkerPool,
|
||||
errorPassthroughService *service.ErrorPassthroughService,
|
||||
contentModerationService *service.ContentModerationService,
|
||||
opsService *service.OpsService,
|
||||
cfg *config.Config,
|
||||
) *OpenAIGatewayHandler {
|
||||
pingInterval := time.Duration(0)
|
||||
@@ -122,6 +124,7 @@ func NewOpenAIGatewayHandler(
|
||||
usageRecordWorkerPool: usageRecordWorkerPool,
|
||||
errorPassthroughService: errorPassthroughService,
|
||||
contentModerationService: contentModerationService,
|
||||
opsService: opsService,
|
||||
concurrencyHelper: NewConcurrencyHelper(concurrencyService, SSEPingFormatComment, pingInterval),
|
||||
imageLimiter: &imageConcurrencyLimiter{},
|
||||
maxAccountSwitches: maxAccountSwitches,
|
||||
@@ -305,6 +308,9 @@ func (h *OpenAIGatewayHandler) Responses(c *gin.Context) {
|
||||
|
||||
// Generate session hash (header first; fallback to prompt_cache_key)
|
||||
sessionHash := h.gatewayService.GenerateSessionHash(c, sessionHashBody)
|
||||
if h.rejectIfCyberSessionBlocked(c, apiKey, sessionHashBody, reqModel, cyberBlockFormatResponses) {
|
||||
return
|
||||
}
|
||||
requireCompact := isOpenAIRemoteCompactPath(c)
|
||||
|
||||
maxAccountSwitches := h.maxAccountSwitches
|
||||
@@ -387,6 +393,11 @@ func (h *OpenAIGatewayHandler) Responses(c *gin.Context) {
|
||||
}()
|
||||
return h.gatewayService.Forward(c.Request.Context(), c, account, forwardBody)
|
||||
}()
|
||||
cyberBlockKeyHTTP := ""
|
||||
if service.GetOpsCyberPolicy(c) != nil {
|
||||
cyberBlockKeyHTTP = service.CyberSessionBlockKey(apiKey.ID, c, sessionHashBody)
|
||||
}
|
||||
h.recordCyberPolicyIfMarked(c, apiKey, account, subscription, reqModel, err != nil, cyberBlockKeyHTTP, channelMapping.ToUsageFields(reqModel, ""), service.HashUsageRequestPayload(body))
|
||||
forwardDurationMs := time.Since(forwardStart).Milliseconds()
|
||||
upstreamLatencyMs, _ := getContextInt64(c, service.OpsUpstreamLatencyMsKey)
|
||||
responseLatencyMs := forwardDurationMs
|
||||
@@ -488,6 +499,7 @@ func (h *OpenAIGatewayHandler) Responses(c *gin.Context) {
|
||||
upstreamEndpoint := GetUpstreamEndpoint(c, account.Platform)
|
||||
|
||||
// 使用量记录通过有界 worker 池提交,避免请求热路径创建无界 goroutine。
|
||||
cyberBlocked := service.GetOpsCyberPolicy(c) != nil
|
||||
h.submitOpenAIUsageRecordTask(c.Request.Context(), result, func(ctx context.Context) {
|
||||
if err := h.gatewayService.RecordUsage(ctx, &service.OpenAIRecordUsageInput{
|
||||
Result: result,
|
||||
@@ -502,6 +514,7 @@ func (h *OpenAIGatewayHandler) Responses(c *gin.Context) {
|
||||
RequestPayloadHash: requestPayloadHash,
|
||||
APIKeyService: h.apiKeyService,
|
||||
ChannelUsageFields: channelMapping.ToUsageFields(reqModel, result.UpstreamModel),
|
||||
CyberBlocked: cyberBlocked,
|
||||
}); err != nil {
|
||||
logger.L().With(
|
||||
zap.String("component", "handler.openai_gateway.responses"),
|
||||
@@ -713,6 +726,9 @@ func (h *OpenAIGatewayHandler) Messages(c *gin.Context) {
|
||||
sessionHash := h.gatewayService.GenerateSessionHash(c, body)
|
||||
promptCacheKey := h.gatewayService.ExtractSessionID(c, body)
|
||||
sessionHash, promptCacheKey = resolveOpenAIMessagesMetadataSession(sessionHash, promptCacheKey, reqModel, body)
|
||||
if h.rejectIfCyberSessionBlocked(c, apiKey, body, reqModel, cyberBlockFormatAnthropic) {
|
||||
return
|
||||
}
|
||||
|
||||
maxAccountSwitches := h.maxAccountSwitches
|
||||
switchCount := 0
|
||||
@@ -789,7 +805,11 @@ func (h *OpenAIGatewayHandler) Messages(c *gin.Context) {
|
||||
}()
|
||||
return h.gatewayService.ForwardAsAnthropic(c.Request.Context(), c, account, forwardBody, promptCacheKey, defaultMappedModel)
|
||||
}()
|
||||
|
||||
cyberBlockKeyMsg := ""
|
||||
if service.GetOpsCyberPolicy(c) != nil {
|
||||
cyberBlockKeyMsg = service.CyberSessionBlockKey(apiKey.ID, c, body)
|
||||
}
|
||||
h.recordCyberPolicyIfMarked(c, apiKey, account, subscription, reqModel, err != nil, cyberBlockKeyMsg, channelMappingMsg.ToUsageFields(reqModel, ""), service.HashUsageRequestPayload(body))
|
||||
forwardDurationMs := time.Since(forwardStart).Milliseconds()
|
||||
upstreamLatencyMs, _ := getContextInt64(c, service.OpsUpstreamLatencyMsKey)
|
||||
responseLatencyMs := forwardDurationMs
|
||||
@@ -883,6 +903,7 @@ func (h *OpenAIGatewayHandler) Messages(c *gin.Context) {
|
||||
inboundEndpoint := GetInboundEndpoint(c)
|
||||
upstreamEndpoint := GetUpstreamEndpoint(c, account.Platform)
|
||||
|
||||
cyberBlocked := service.GetOpsCyberPolicy(c) != nil
|
||||
h.submitOpenAIUsageRecordTask(c.Request.Context(), result, func(ctx context.Context) {
|
||||
if err := h.gatewayService.RecordUsage(ctx, &service.OpenAIRecordUsageInput{
|
||||
Result: result,
|
||||
@@ -897,6 +918,7 @@ func (h *OpenAIGatewayHandler) Messages(c *gin.Context) {
|
||||
RequestPayloadHash: requestPayloadHash,
|
||||
APIKeyService: h.apiKeyService,
|
||||
ChannelUsageFields: channelMappingMsg.ToUsageFields(reqModel, result.UpstreamModel),
|
||||
CyberBlocked: cyberBlocked,
|
||||
}); err != nil {
|
||||
logger.L().With(
|
||||
zap.String("component", "handler.openai_gateway.messages"),
|
||||
@@ -1259,6 +1281,17 @@ func (h *OpenAIGatewayHandler) ResponsesWebSocket(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// F5a: 握手层会话屏蔽检查。WS 握手无 body,显式标识仅来自握手 header
|
||||
// (session_id / conversation_id);无标识则放行,连接内仍有本地 flag 兜底。
|
||||
cyberBlockKey := service.CyberSessionBlockKey(apiKey.ID, c, nil)
|
||||
if cyberBlockKey != "" && h.gatewayService.IsCyberSessionBlocked(c.Request.Context(), cyberBlockKey) {
|
||||
writeCyberSessionBlockedWSError(c.Request.Context(), wsConn)
|
||||
closeOpenAIClientWS(wsConn, coderws.StatusPolicyViolation, "session blocked by cyber-security policy")
|
||||
h.enqueueCyberSessionBlockedOpsEntry(c, apiKey, reqModel, cyberBlockKey)
|
||||
return
|
||||
}
|
||||
cyberBlockedThisConn := false
|
||||
|
||||
// 解析渠道级模型映射
|
||||
channelMappingWS, _ := h.gatewayService.ResolveChannelMappingAndRestrict(ctx, apiKey.GroupID, reqModel)
|
||||
|
||||
@@ -1430,6 +1463,10 @@ func (h *OpenAIGatewayHandler) ResponsesWebSocket(c *gin.Context) {
|
||||
return nil
|
||||
},
|
||||
BeforeTurn: func(turn int) error {
|
||||
// turn==1 的会话屏蔽已由握手层检查覆盖;连接内 flag 只拦截后续 turn。
|
||||
if cyberBlockedThisConn {
|
||||
return service.NewOpenAIWSClientCloseError(coderws.StatusPolicyViolation, cyberSessionBlockedClientMsg, nil)
|
||||
}
|
||||
if turn == 1 {
|
||||
return nil
|
||||
}
|
||||
@@ -1461,11 +1498,24 @@ func (h *OpenAIGatewayHandler) ResponsesWebSocket(c *gin.Context) {
|
||||
return nil
|
||||
},
|
||||
AfterTurn: func(turn int, result *service.OpenAIForwardResult, turnErr error) {
|
||||
// F1: cyber 标记按 turn 生命周期清理——defer 保证任意早返回路径都执行;
|
||||
// CyberBlocked 必须在 submit 前同步预捕获(task 闭包由 worker 池异步执行,
|
||||
// 届时 defer 已清除标记)。
|
||||
defer clearCyberPolicyTurnState(c)
|
||||
releaseTurnSlots()
|
||||
h.recordCyberPolicyIfMarked(c, apiKey, account, subscription, reqModel, turnErr != nil, cyberBlockKey, channelMappingWS.ToUsageFields(reqModel, ""), requestPayloadHash)
|
||||
if service.GetOpsCyberPolicy(c) != nil {
|
||||
cyberBlockedThisConn = true
|
||||
}
|
||||
if turnErr != nil {
|
||||
if result == nil || result.ImageCount <= 0 {
|
||||
return
|
||||
}
|
||||
// cyber 命中时该 turn 的用量已由 recordCyberPolicyIfMarked(forwardErrored=true)
|
||||
// 按真实 token 记录,这里不再走下方 RecordUsage,避免对同一 turn 双写/双扣费。
|
||||
if service.GetOpsCyberPolicy(c) != nil {
|
||||
return
|
||||
}
|
||||
reqLog.Warn("openai.websocket_partial_error_with_image_result",
|
||||
zap.Int64("account_id", account.ID),
|
||||
zap.Int("image_count", result.ImageCount),
|
||||
@@ -1481,6 +1531,7 @@ func (h *OpenAIGatewayHandler) ResponsesWebSocket(c *gin.Context) {
|
||||
h.gatewayService.ReportOpenAIAccountScheduleResult(account.ID, true, result.FirstTokenMs)
|
||||
inboundEndpoint := GetInboundEndpoint(c)
|
||||
upstreamEndpoint := GetUpstreamEndpoint(c, account.Platform)
|
||||
cyberBlocked := service.GetOpsCyberPolicy(c) != nil
|
||||
h.submitOpenAIUsageRecordTask(ctx, result, func(taskCtx context.Context) {
|
||||
if err := h.gatewayService.RecordUsage(taskCtx, &service.OpenAIRecordUsageInput{
|
||||
Result: result,
|
||||
@@ -1495,6 +1546,7 @@ func (h *OpenAIGatewayHandler) ResponsesWebSocket(c *gin.Context) {
|
||||
RequestPayloadHash: requestPayloadHash,
|
||||
APIKeyService: h.apiKeyService,
|
||||
ChannelUsageFields: channelMappingWS.ToUsageFields(reqModel, result.UpstreamModel),
|
||||
CyberBlocked: cyberBlocked,
|
||||
}); err != nil {
|
||||
reqLog.Error("openai.websocket_record_usage_failed",
|
||||
zap.Int64("account_id", account.ID),
|
||||
@@ -1904,6 +1956,14 @@ func openAIForwardErrorAlreadyCommunicated(c *gin.Context, writerSizeBeforeForwa
|
||||
return false
|
||||
}
|
||||
|
||||
// cyber_policy 命中时上游原始错误体已透传给客户端(非流式 c.Data 写出 400 body,
|
||||
// 流式写出 response.failed 事件),不能再让 ensureForwardErrorResponse 追加
|
||||
// fallback —— 否则在已写出的完整响应尾部追加 SSE(responses 端点尾随
|
||||
// response.failed、chat 端点尾随 event:error),污染响应体。Size 已变化证明响应确已写出。
|
||||
if service.GetOpsCyberPolicy(c) != nil {
|
||||
return true
|
||||
}
|
||||
|
||||
msg := strings.TrimSpace(err.Error())
|
||||
for _, prefix := range []string{
|
||||
"upstream response failed:",
|
||||
@@ -2017,6 +2077,364 @@ func writeContentModerationWSError(ctx context.Context, conn *coderws.Conn, deci
|
||||
_ = conn.Write(writeCtx, coderws.MessageText, payload)
|
||||
}
|
||||
|
||||
// writeCyberSessionBlockedWSError sends an error frame telling the client this
|
||||
// session is blocked by the cyber session block (F5a) before closing.
|
||||
func writeCyberSessionBlockedWSError(ctx context.Context, conn *coderws.Conn) {
|
||||
if conn == nil {
|
||||
return
|
||||
}
|
||||
if ctx == nil {
|
||||
ctx = context.Background()
|
||||
}
|
||||
payload, err := json.Marshal(gin.H{
|
||||
"event_id": "evt_cyber_session_blocked",
|
||||
"type": "error",
|
||||
"error": gin.H{
|
||||
"type": "permission_error",
|
||||
"code": "session_blocked_by_cyber_policy",
|
||||
"message": cyberSessionBlockedClientMsg,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
payload = []byte(`{"event_id":"evt_cyber_session_blocked","type":"error","error":{"type":"permission_error","code":"session_blocked_by_cyber_policy","message":"This session is blocked by cyber-security policy, please start a new session"}}`)
|
||||
}
|
||||
writeCtx, cancel := context.WithTimeout(ctx, 2*time.Second)
|
||||
defer cancel()
|
||||
_ = conn.Write(writeCtx, coderws.MessageText, payload)
|
||||
}
|
||||
|
||||
// cyberPolicyRecordedKey guards against double-firing recordCyberPolicyIfMarked
|
||||
// within one request (e.g. in a retry/failover loop).
|
||||
const cyberPolicyRecordedKey = "ops_cyber_recorded"
|
||||
|
||||
// cyberPolicyOpsErrorMeta carries request-scoped fields captured outside the
|
||||
// async goroutine for building the cyber ops_error_logs entry.
|
||||
type cyberPolicyOpsErrorMeta struct {
|
||||
RequestID string
|
||||
ClientRequestID string
|
||||
Platform string
|
||||
Model string
|
||||
RequestPath string
|
||||
Stream bool
|
||||
InboundEndpoint string
|
||||
UserAgent string
|
||||
APIKeyPrefix string
|
||||
UserID int64
|
||||
APIKeyID int64
|
||||
AccountID int64
|
||||
GroupID *int64
|
||||
ClientIP string
|
||||
CreatedAt time.Time
|
||||
SessionBlockKey string
|
||||
}
|
||||
|
||||
// buildCyberPolicyOpsErrorEntry builds the ops_error_logs entry for an upstream
|
||||
// cyber_policy hit. StatusCode mirrors what the codex client actually received
|
||||
// (400 non-stream / 200 stream), per F6.
|
||||
func buildCyberPolicyOpsErrorEntry(meta cyberPolicyOpsErrorMeta, mark *service.CyberPolicyMark) *service.OpsInsertErrorLogInput {
|
||||
rt := int16(service.RequestTypeCyberBlocked)
|
||||
entry := &service.OpsInsertErrorLogInput{
|
||||
RequestID: meta.RequestID,
|
||||
ClientRequestID: meta.ClientRequestID,
|
||||
Platform: meta.Platform,
|
||||
Model: meta.Model,
|
||||
RequestPath: meta.RequestPath,
|
||||
Stream: meta.Stream,
|
||||
InboundEndpoint: meta.InboundEndpoint,
|
||||
RequestType: &rt,
|
||||
UserAgent: meta.UserAgent,
|
||||
APIKeyPrefix: meta.APIKeyPrefix,
|
||||
ErrorPhase: "request",
|
||||
ErrorType: "cyber_policy",
|
||||
Severity: "P3",
|
||||
StatusCode: mark.UpstreamStatus,
|
||||
IsBusinessLimited: true,
|
||||
ErrorMessage: "cyber_policy: " + mark.Message,
|
||||
// 原始 body 直接入队;ops service 落库前统一走 sanitizeErrorBodyForStorage 脱敏与截断。
|
||||
ErrorBody: mark.Body,
|
||||
ErrorSource: "upstream_http",
|
||||
ErrorOwner: "provider",
|
||||
CreatedAt: meta.CreatedAt,
|
||||
}
|
||||
if meta.UserID > 0 {
|
||||
entry.UserID = &meta.UserID
|
||||
}
|
||||
if meta.APIKeyID > 0 {
|
||||
entry.APIKeyID = &meta.APIKeyID
|
||||
}
|
||||
if meta.AccountID > 0 {
|
||||
entry.AccountID = &meta.AccountID
|
||||
}
|
||||
entry.GroupID = meta.GroupID
|
||||
if meta.ClientIP != "" {
|
||||
entry.ClientIP = &meta.ClientIP
|
||||
}
|
||||
return entry
|
||||
}
|
||||
|
||||
// 双语单串:网关客户端面向中英用户,且本错误无 i18n 协商通道。
|
||||
const cyberSessionBlockedClientMsg = "该会话已被网络安全策略屏蔽,请开启新会话 / This session is blocked by cyber-security policy, please start a new session"
|
||||
|
||||
// buildCyberSessionBlockedOpsEntry builds the ops_error_logs entry for a request
|
||||
// rejected locally by the cyber session block (F5a). Distinct error_type from
|
||||
// upstream `cyber_policy`; never feeds moderation logs / violation counting
|
||||
// (the request never reached upstream — see spec).
|
||||
func buildCyberSessionBlockedOpsEntry(meta cyberPolicyOpsErrorMeta) *service.OpsInsertErrorLogInput {
|
||||
rt := int16(service.RequestTypeCyberBlocked)
|
||||
entry := &service.OpsInsertErrorLogInput{
|
||||
RequestID: meta.RequestID,
|
||||
ClientRequestID: meta.ClientRequestID,
|
||||
Platform: meta.Platform,
|
||||
Model: meta.Model,
|
||||
RequestPath: meta.RequestPath,
|
||||
Stream: meta.Stream,
|
||||
InboundEndpoint: meta.InboundEndpoint,
|
||||
RequestType: &rt,
|
||||
UserAgent: meta.UserAgent,
|
||||
APIKeyPrefix: meta.APIKeyPrefix,
|
||||
ErrorPhase: "request",
|
||||
ErrorType: "cyber_policy_session_blocked",
|
||||
Severity: "P3",
|
||||
StatusCode: http.StatusForbidden,
|
||||
IsBusinessLimited: true,
|
||||
ErrorMessage: "cyber_policy_session_blocked: request rejected locally by session block",
|
||||
ErrorSource: "gateway_local",
|
||||
ErrorOwner: "platform",
|
||||
CreatedAt: meta.CreatedAt,
|
||||
// AccountID 有意不设:请求在账号选择前即被拒绝。
|
||||
}
|
||||
if meta.SessionBlockKey != "" {
|
||||
entry.ErrorBody = "session_block_key=" + meta.SessionBlockKey
|
||||
}
|
||||
if meta.UserID > 0 {
|
||||
entry.UserID = &meta.UserID
|
||||
}
|
||||
if meta.APIKeyID > 0 {
|
||||
entry.APIKeyID = &meta.APIKeyID
|
||||
}
|
||||
entry.GroupID = meta.GroupID
|
||||
if meta.ClientIP != "" {
|
||||
entry.ClientIP = &meta.ClientIP
|
||||
}
|
||||
return entry
|
||||
}
|
||||
|
||||
// cyberSessionBlockFormat selects the per-endpoint error envelope for a locally
|
||||
// blocked session (用户决策:兼容路径各自格式).
|
||||
type cyberSessionBlockFormat int
|
||||
|
||||
const (
|
||||
cyberBlockFormatResponses cyberSessionBlockFormat = iota
|
||||
cyberBlockFormatChat
|
||||
cyberBlockFormatAnthropic
|
||||
)
|
||||
|
||||
// rejectIfCyberSessionBlocked checks the session-block table BEFORE account
|
||||
// selection. Returns true when the request was rejected (response already
|
||||
// written + ops entry enqueued). Fail-open: disabled switch / empty key /
|
||||
// store error → false.
|
||||
func (h *OpenAIGatewayHandler) rejectIfCyberSessionBlocked(c *gin.Context, apiKey *service.APIKey, body []byte, model string, format cyberSessionBlockFormat) bool {
|
||||
if h == nil || h.gatewayService == nil || apiKey == nil {
|
||||
return false
|
||||
}
|
||||
// 开关默认关:先走 ~ns 级缓存开关检查,再付出 key 派生(gjson+sha256)成本。
|
||||
if enabled, _ := h.gatewayService.CyberSessionBlockRuntime(c.Request.Context()); !enabled {
|
||||
return false
|
||||
}
|
||||
key := service.CyberSessionBlockKey(apiKey.ID, c, body)
|
||||
if key == "" {
|
||||
return false
|
||||
}
|
||||
if !h.gatewayService.IsCyberSessionBlocked(c.Request.Context(), key) {
|
||||
return false
|
||||
}
|
||||
switch format {
|
||||
case cyberBlockFormatAnthropic:
|
||||
c.JSON(http.StatusForbidden, gin.H{"type": "error", "error": gin.H{
|
||||
"type": "permission_error",
|
||||
"message": cyberSessionBlockedClientMsg,
|
||||
}})
|
||||
default: // cyberBlockFormatResponses 与 cyberBlockFormatChat:同构的 OpenAI error envelope
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": gin.H{
|
||||
"type": "permission_error",
|
||||
"code": "session_blocked_by_cyber_policy",
|
||||
"message": cyberSessionBlockedClientMsg,
|
||||
}})
|
||||
}
|
||||
h.enqueueCyberSessionBlockedOpsEntry(c, apiKey, model, key)
|
||||
return true
|
||||
}
|
||||
|
||||
// enqueueCyberSessionBlockedOpsEntry captures request meta and enqueues the
|
||||
// ops_error_logs entry for a locally blocked request.
|
||||
func (h *OpenAIGatewayHandler) enqueueCyberSessionBlockedOpsEntry(c *gin.Context, apiKey *service.APIKey, model string, sessionBlockKey string) {
|
||||
if h.opsService == nil {
|
||||
return
|
||||
}
|
||||
meta := cyberPolicyOpsErrorMeta{Model: model, InboundEndpoint: GetInboundEndpoint(c), CreatedAt: time.Now(), SessionBlockKey: sessionBlockKey}
|
||||
meta.RequestID = c.Writer.Header().Get("X-Request-Id")
|
||||
if c.Request != nil && c.Request.URL != nil {
|
||||
meta.RequestPath = c.Request.URL.Path
|
||||
}
|
||||
if v, ok := c.Get(opsStreamKey); ok {
|
||||
if b, ok := v.(bool); ok {
|
||||
meta.Stream = b
|
||||
}
|
||||
}
|
||||
meta.Platform = resolveOpsPlatform(apiKey, guessPlatformFromPath(meta.RequestPath))
|
||||
if c.Request != nil {
|
||||
meta.ClientRequestID, _ = c.Request.Context().Value(ctxkey.ClientRequestID).(string)
|
||||
meta.UserAgent = c.GetHeader("User-Agent")
|
||||
meta.ClientIP = strings.TrimSpace(ip.GetClientIP(c))
|
||||
}
|
||||
meta.APIKeyID = apiKey.ID
|
||||
meta.GroupID = apiKey.GroupID
|
||||
meta.APIKeyPrefix = keyPrefix(apiKey.Key, 8)
|
||||
if apiKey.User != nil {
|
||||
meta.UserID = apiKey.User.ID
|
||||
}
|
||||
enqueueOpsErrorLog(h.opsService, buildCyberSessionBlockedOpsEntry(meta))
|
||||
}
|
||||
|
||||
// recordCyberPolicyIfMarked 在 gateway forward 返回后检查 cyber 标记,异步写风控日志/邮件,
|
||||
// 并在 forward 返回错误时写一条 tokens=0 用量行。标记由 gateway 服务层在透传 cyber 后设置;
|
||||
// 当前请求已发给用户,本方法只做事后记录,不影响响应。forwardErrored 为 true 时才写用量行,
|
||||
// 避免与正常 RecordUsage(forward 成功路径)重复。每请求至多记录一次。
|
||||
func (h *OpenAIGatewayHandler) recordCyberPolicyIfMarked(c *gin.Context, apiKey *service.APIKey, account *service.Account, subscription *service.UserSubscription, model string, forwardErrored bool, cyberBlockKey string, channelFields service.ChannelUsageFields, requestPayloadHash string) {
|
||||
mark := service.GetOpsCyberPolicy(c)
|
||||
if mark == nil {
|
||||
return
|
||||
}
|
||||
if c.GetBool(cyberPolicyRecordedKey) {
|
||||
return
|
||||
}
|
||||
c.Set(cyberPolicyRecordedKey, true)
|
||||
|
||||
requestID := c.Writer.Header().Get("X-Request-Id")
|
||||
var userID, apiKeyID int64
|
||||
var userEmail, apiKeyName, groupName string
|
||||
var groupID *int64
|
||||
if apiKey != nil {
|
||||
apiKeyID = apiKey.ID
|
||||
apiKeyName = apiKey.Name
|
||||
groupID = apiKey.GroupID
|
||||
if apiKey.User != nil {
|
||||
userID = apiKey.User.ID
|
||||
userEmail = apiKey.User.Email
|
||||
}
|
||||
if apiKey.Group != nil {
|
||||
groupName = apiKey.Group.Name
|
||||
}
|
||||
}
|
||||
inboundEndpoint := GetInboundEndpoint(c)
|
||||
upstreamEndpoint := ""
|
||||
var accountID int64
|
||||
if account != nil {
|
||||
accountID = account.ID
|
||||
upstreamEndpoint = GetUpstreamEndpoint(c, account.Platform)
|
||||
}
|
||||
stream := false
|
||||
if v, ok := c.Get(opsStreamKey); ok {
|
||||
if b, ok := v.(bool); ok {
|
||||
stream = b
|
||||
}
|
||||
}
|
||||
cmSvc := h.contentModerationService
|
||||
gwSvc := h.gatewayService
|
||||
opsSvc := h.opsService
|
||||
apiKeySvc := h.apiKeyService
|
||||
requestPath := ""
|
||||
if c.Request != nil && c.Request.URL != nil {
|
||||
requestPath = c.Request.URL.Path
|
||||
}
|
||||
platform := resolveOpsPlatform(apiKey, guessPlatformFromPath(requestPath))
|
||||
var clientRequestID, userAgent, clientIPStr string
|
||||
if c.Request != nil {
|
||||
clientRequestID, _ = c.Request.Context().Value(ctxkey.ClientRequestID).(string)
|
||||
userAgent = c.GetHeader("User-Agent")
|
||||
clientIPStr = strings.TrimSpace(ip.GetClientIP(c))
|
||||
}
|
||||
apiKeyPrefix := ""
|
||||
if apiKey != nil {
|
||||
apiKeyPrefix = keyPrefix(apiKey.Key, 8)
|
||||
}
|
||||
opsMeta := cyberPolicyOpsErrorMeta{
|
||||
RequestID: requestID,
|
||||
ClientRequestID: clientRequestID,
|
||||
Platform: platform,
|
||||
Model: model,
|
||||
RequestPath: requestPath,
|
||||
Stream: stream,
|
||||
InboundEndpoint: inboundEndpoint,
|
||||
UserAgent: userAgent,
|
||||
APIKeyPrefix: apiKeyPrefix,
|
||||
UserID: userID,
|
||||
APIKeyID: apiKeyID,
|
||||
AccountID: accountID,
|
||||
GroupID: groupID,
|
||||
ClientIP: clientIPStr,
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
go func() {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
if cmSvc != nil {
|
||||
cmSvc.RecordCyberPolicyEvent(ctx, service.CyberPolicyRecordInput{
|
||||
RequestID: requestID,
|
||||
UserID: userID,
|
||||
UserEmail: userEmail,
|
||||
APIKeyID: apiKeyID,
|
||||
APIKeyName: apiKeyName,
|
||||
GroupID: groupID,
|
||||
GroupName: groupName,
|
||||
Endpoint: inboundEndpoint,
|
||||
Model: model,
|
||||
UpstreamMessage: mark.Message,
|
||||
UpstreamBody: mark.Body,
|
||||
UpstreamStatus: mark.UpstreamStatus,
|
||||
UpstreamInTok: mark.UpstreamInTok,
|
||||
UpstreamOutTok: mark.UpstreamOutTok,
|
||||
})
|
||||
}
|
||||
if forwardErrored && gwSvc != nil {
|
||||
gwSvc.RecordCyberPolicyUsageLog(ctx, service.CyberPolicyUsageInput{
|
||||
APIKey: apiKey,
|
||||
Account: account,
|
||||
Subscription: subscription,
|
||||
RequestID: requestID,
|
||||
Model: model,
|
||||
Stream: stream,
|
||||
InputTokens: mark.UpstreamInTok,
|
||||
OutputTokens: mark.UpstreamOutTok,
|
||||
InboundEndpoint: inboundEndpoint,
|
||||
UpstreamEndpoint: upstreamEndpoint,
|
||||
UserAgent: userAgent,
|
||||
IPAddress: clientIPStr,
|
||||
RequestPayloadHash: requestPayloadHash,
|
||||
APIKeyService: apiKeySvc,
|
||||
ChannelUsageFields: channelFields,
|
||||
})
|
||||
}
|
||||
if gwSvc != nil && cyberBlockKey != "" {
|
||||
gwSvc.MarkCyberSessionBlocked(ctx, cyberBlockKey)
|
||||
}
|
||||
if opsSvc != nil {
|
||||
enqueueOpsErrorLog(opsSvc, buildCyberPolicyOpsErrorEntry(opsMeta, mark))
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// clearCyberPolicyTurnState resets the cyber mark and the per-request recorded
|
||||
// guard. WS-only: called at the END of AfterTurn, after recordCyberPolicyIfMarked
|
||||
// and RecordUsage (which reads CyberBlocked) have both consumed the mark.
|
||||
func clearCyberPolicyTurnState(c *gin.Context) {
|
||||
if c == nil {
|
||||
return
|
||||
}
|
||||
service.ClearOpsCyberPolicy(c)
|
||||
c.Set(cyberPolicyRecordedKey, false)
|
||||
}
|
||||
|
||||
func summarizeWSCloseErrorForLog(err error) (string, string) {
|
||||
if err == nil {
|
||||
return "-", "-"
|
||||
|
||||
Reference in new issue
Block a user