Files
sub2api/backend/internal/service/ops_user_error.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

128 lines
4.2 KiB
Go

package service
import "time"
// UserErrorRequest 是面向终端用户的"错误请求"精简脱敏视图(白名单)。
// 严禁包含 client_ip / user_agent / account / api_key_prefix / upstream_endpoint /
// user_email 等敏感或内部字段。注:message(网关标准化错误描述)与 key_name
// (用户自有 API Key 名称,KeysView 中本就可见)经产品决策对该用户开放;
// error_body 仅在详情接口(GetUserErrorRequestDetail)按归属校验后返回。
type UserErrorRequest struct {
ID int64 `json:"id"`
CreatedAt time.Time `json:"created_at"`
Model string `json:"model"`
InboundEndpoint string `json:"inbound_endpoint"`
StatusCode int `json:"status_code"`
Category string `json:"category"`
Platform string `json:"platform"`
Message string `json:"message"`
KeyName string `json:"key_name"`
KeyDeleted bool `json:"key_deleted"`
}
// UserErrorRequestList 是用户错误请求分页结果。
type UserErrorRequestList struct {
Items []*UserErrorRequest `json:"items"`
Total int `json:"total"`
Page int `json:"page"`
PageSize int `json:"page_size"`
}
// MapUserErrorCategory 把后端 error_phase + error_type 映射为用户侧粗分类码。
// 返回的是稳定的分类 code(前端做 i18n),不是展示文案。
func MapUserErrorCategory(phase, errType string) string {
switch phase {
case "auth":
return "auth"
case "routing":
return "service_unavailable"
case "upstream", "network":
return "upstream"
case "internal":
return "internal"
case "request":
switch errType {
case "rate_limit_error":
return "rate_limit"
case "billing_error", "subscription_error":
return "quota"
case "invalid_request_error":
return "invalid_request"
case "cyber_policy":
return "cyber"
}
}
return "other"
}
// CategoryToFilter 把用户侧分类码反向映射为后端过滤条件(plain ANY)。
// 未知分类返回两个空切片(即不施加分类过滤)。
// 注意:"other" 与未知分类都走 default 返回空切片——"other" 无对应的 phase/type 组合,无法精确反查,因此等价于不过滤。
func CategoryToFilter(category string) (phases []string, errorTypes []string) {
switch category {
case "auth":
return []string{"auth"}, nil
case "service_unavailable":
return []string{"routing"}, nil
case "upstream":
return []string{"upstream", "network"}, nil
case "internal":
return []string{"internal"}, nil
case "rate_limit":
return nil, []string{"rate_limit_error"}
case "quota":
return nil, []string{"billing_error", "subscription_error"}
case "invalid_request":
return nil, []string{"invalid_request_error"}
case "cyber":
return []string{"request"}, []string{"cyber_policy"}
default:
return nil, nil
}
}
// ToUserErrorRequest 把内部 OpsErrorLog 裁剪为用户安全视图。
func ToUserErrorRequest(e *OpsErrorLog) *UserErrorRequest {
if e == nil {
return nil
}
model := e.RequestedModel
if model == "" {
model = e.Model
}
return &UserErrorRequest{
ID: e.ID,
CreatedAt: e.CreatedAt,
Model: model,
InboundEndpoint: e.InboundEndpoint,
StatusCode: e.StatusCode,
Category: MapUserErrorCategory(e.Phase, e.Type),
Platform: e.Platform,
Message: e.Message,
KeyName: e.APIKeyName,
KeyDeleted: e.APIKeyDeleted,
}
}
// UserErrorRequestDetail 是错误请求详情的脱敏视图(点击单行查看)。
// 在 UserErrorRequest 基础上额外暴露 error_body(上游错误响应正文)与 upstream_status_code;
// 仍严禁任何内部/敏感字段。
type UserErrorRequestDetail struct {
UserErrorRequest
ErrorBody string `json:"error_body"`
UpstreamStatusCode *int `json:"upstream_status_code,omitempty"`
}
// ToUserErrorRequestDetail 把内部 OpsErrorLogDetail 裁剪为用户安全详情视图。
func ToUserErrorRequestDetail(e *OpsErrorLogDetail) *UserErrorRequestDetail {
if e == nil {
return nil
}
base := ToUserErrorRequest(&e.OpsErrorLog)
return &UserErrorRequestDetail{
UserErrorRequest: *base,
ErrorBody: e.ErrorBody,
UpstreamStatusCode: e.UpstreamStatusCode,
}
}