145 lines
5.0 KiB
Go
145 lines
5.0 KiB
Go
package service
|
|||
|
|
|
||
|
|
import "time"
|
||
|
|
|
||
|
|
// UserErrorRequest 是面向终端用户的"错误请求"精简脱敏视图(白名单)。
|
||
|
|
// 严禁包含 account / api_key_prefix / upstream_endpoint / user_email 等
|
||
|
|
// 敏感或内部字段。注:message(网关标准化错误描述)与 key_name
|
||
|
|
// (用户自有 API Key 名称,KeysView 中本就可见)经产品决策对该用户开放;
|
||
|
|
// client_ip / user_agent / group_name / request_type / stream 均为该用户
|
||
|
|
// 自己请求的属性,经产品决策(2026-07-03)开放,
|
||
|
|
// 与用量明细已向用户展示自身 ip_address/user_agent/分组/类型 的口径对齐;
|
||
|
|
// 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"`
|
||
|
|
ClientIP string `json:"client_ip,omitempty"`
|
||
|
|
GroupName string `json:"group_name,omitempty"`
|
||
|
|
RequestType *int16 `json:"request_type,omitempty"`
|
||
|
|
Stream bool `json:"stream"`
|
||
|
|
UserAgent string `json:"user_agent,omitempty"`
|
||
|
|
}
|
||
|
|
|
||
|
|
// 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 "account_auth", "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{"account_auth", "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
|
||
|
|
}
|
||
|
|
clientIP := ""
|
||
|
|
if e.ClientIP != nil {
|
||
|
|
clientIP = *e.ClientIP
|
||
|
|
}
|
||
|
|
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,
|
||
|
|
ClientIP: clientIP,
|
||
|
|
GroupName: e.GroupName,
|
||
|
|
RequestType: e.RequestType,
|
||
|
|
Stream: e.Stream,
|
||
|
|
UserAgent: e.UserAgent,
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// 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,
|
||
|
|
}
|
||
|
|
}
|