Merge pull request #3285 from seefs001/feature/param-override-log
feat: params override log
This commit is contained in:
+231
-3
@@ -21,10 +21,23 @@ var negativeIndexRegexp = regexp.MustCompile(`\.(-\d+)`)
|
|||||||
const (
|
const (
|
||||||
paramOverrideContextRequestHeaders = "request_headers"
|
paramOverrideContextRequestHeaders = "request_headers"
|
||||||
paramOverrideContextHeaderOverride = "header_override"
|
paramOverrideContextHeaderOverride = "header_override"
|
||||||
|
paramOverrideContextAuditRecorder = "__param_override_audit_recorder"
|
||||||
)
|
)
|
||||||
|
|
||||||
var errSourceHeaderNotFound = errors.New("source header does not exist")
|
var errSourceHeaderNotFound = errors.New("source header does not exist")
|
||||||
|
|
||||||
|
var paramOverrideKeyAuditPaths = map[string]struct{}{
|
||||||
|
"model": {},
|
||||||
|
"original_model": {},
|
||||||
|
"upstream_model": {},
|
||||||
|
"service_tier": {},
|
||||||
|
"inference_geo": {},
|
||||||
|
}
|
||||||
|
|
||||||
|
type paramOverrideAuditRecorder struct {
|
||||||
|
lines []string
|
||||||
|
}
|
||||||
|
|
||||||
type ConditionOperation struct {
|
type ConditionOperation struct {
|
||||||
Path string `json:"path"` // JSON路径
|
Path string `json:"path"` // JSON路径
|
||||||
Mode string `json:"mode"` // full, prefix, suffix, contains, gt, gte, lt, lte
|
Mode string `json:"mode"` // full, prefix, suffix, contains, gt, gte, lt, lte
|
||||||
@@ -118,6 +131,7 @@ func ApplyParamOverride(jsonData []byte, paramOverride map[string]interface{}, c
|
|||||||
if len(paramOverride) == 0 {
|
if len(paramOverride) == 0 {
|
||||||
return jsonData, nil
|
return jsonData, nil
|
||||||
}
|
}
|
||||||
|
auditRecorder := getParamOverrideAuditRecorder(conditionContext)
|
||||||
|
|
||||||
// 尝试断言为操作格式
|
// 尝试断言为操作格式
|
||||||
if operations, ok := tryParseOperations(paramOverride); ok {
|
if operations, ok := tryParseOperations(paramOverride); ok {
|
||||||
@@ -125,7 +139,7 @@ func ApplyParamOverride(jsonData []byte, paramOverride map[string]interface{}, c
|
|||||||
workingJSON := jsonData
|
workingJSON := jsonData
|
||||||
var err error
|
var err error
|
||||||
if len(legacyOverride) > 0 {
|
if len(legacyOverride) > 0 {
|
||||||
workingJSON, err = applyOperationsLegacy(workingJSON, legacyOverride)
|
workingJSON, err = applyOperationsLegacy(workingJSON, legacyOverride, auditRecorder)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -137,7 +151,7 @@ func ApplyParamOverride(jsonData []byte, paramOverride map[string]interface{}, c
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 直接使用旧方法
|
// 直接使用旧方法
|
||||||
return applyOperationsLegacy(jsonData, paramOverride)
|
return applyOperationsLegacy(jsonData, paramOverride, auditRecorder)
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildLegacyParamOverride(paramOverride map[string]interface{}) map[string]interface{} {
|
func buildLegacyParamOverride(paramOverride map[string]interface{}) map[string]interface{} {
|
||||||
@@ -161,14 +175,200 @@ func ApplyParamOverrideWithRelayInfo(jsonData []byte, info *RelayInfo) ([]byte,
|
|||||||
}
|
}
|
||||||
|
|
||||||
overrideCtx := BuildParamOverrideContext(info)
|
overrideCtx := BuildParamOverrideContext(info)
|
||||||
|
var recorder *paramOverrideAuditRecorder
|
||||||
|
if shouldEnableParamOverrideAudit(paramOverride) {
|
||||||
|
recorder = ¶mOverrideAuditRecorder{}
|
||||||
|
overrideCtx[paramOverrideContextAuditRecorder] = recorder
|
||||||
|
}
|
||||||
result, err := ApplyParamOverride(jsonData, paramOverride, overrideCtx)
|
result, err := ApplyParamOverride(jsonData, paramOverride, overrideCtx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
syncRuntimeHeaderOverrideFromContext(info, overrideCtx)
|
syncRuntimeHeaderOverrideFromContext(info, overrideCtx)
|
||||||
|
if info != nil {
|
||||||
|
if recorder != nil {
|
||||||
|
info.ParamOverrideAudit = recorder.lines
|
||||||
|
} else {
|
||||||
|
info.ParamOverrideAudit = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func shouldEnableParamOverrideAudit(paramOverride map[string]interface{}) bool {
|
||||||
|
if common.DebugEnabled {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if len(paramOverride) == 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if operations, ok := tryParseOperations(paramOverride); ok {
|
||||||
|
for _, operation := range operations {
|
||||||
|
if shouldAuditParamPath(strings.TrimSpace(operation.Path)) ||
|
||||||
|
shouldAuditParamPath(strings.TrimSpace(operation.To)) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for key := range buildLegacyParamOverride(paramOverride) {
|
||||||
|
if shouldAuditParamPath(strings.TrimSpace(key)) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for key := range paramOverride {
|
||||||
|
if shouldAuditParamPath(strings.TrimSpace(key)) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func getParamOverrideAuditRecorder(context map[string]interface{}) *paramOverrideAuditRecorder {
|
||||||
|
if context == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
recorder, _ := context[paramOverrideContextAuditRecorder].(*paramOverrideAuditRecorder)
|
||||||
|
return recorder
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *paramOverrideAuditRecorder) recordOperation(mode, path, from, to string, value interface{}) {
|
||||||
|
if r == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
line := buildParamOverrideAuditLine(mode, path, from, to, value)
|
||||||
|
if line == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if lo.Contains(r.lines, line) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
r.lines = append(r.lines, line)
|
||||||
|
}
|
||||||
|
|
||||||
|
func shouldAuditParamPath(path string) bool {
|
||||||
|
path = strings.TrimSpace(path)
|
||||||
|
if path == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if common.DebugEnabled {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
_, ok := paramOverrideKeyAuditPaths[path]
|
||||||
|
return ok
|
||||||
|
}
|
||||||
|
|
||||||
|
func shouldAuditOperation(mode, path, from, to string) bool {
|
||||||
|
if common.DebugEnabled {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
for _, candidate := range []string{path, to} {
|
||||||
|
if shouldAuditParamPath(candidate) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func formatParamOverrideAuditValue(value interface{}) string {
|
||||||
|
switch typed := value.(type) {
|
||||||
|
case nil:
|
||||||
|
return "<empty>"
|
||||||
|
case string:
|
||||||
|
return typed
|
||||||
|
default:
|
||||||
|
return common.GetJsonString(typed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildParamOverrideAuditLine(mode, path, from, to string, value interface{}) string {
|
||||||
|
mode = strings.TrimSpace(mode)
|
||||||
|
path = strings.TrimSpace(path)
|
||||||
|
from = strings.TrimSpace(from)
|
||||||
|
to = strings.TrimSpace(to)
|
||||||
|
|
||||||
|
if !shouldAuditOperation(mode, path, from, to) {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
switch mode {
|
||||||
|
case "set":
|
||||||
|
if path == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("set %s = %s", path, formatParamOverrideAuditValue(value))
|
||||||
|
case "delete":
|
||||||
|
if path == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("delete %s", path)
|
||||||
|
case "copy":
|
||||||
|
if from == "" || to == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("copy %s -> %s", from, to)
|
||||||
|
case "move":
|
||||||
|
if from == "" || to == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("move %s -> %s", from, to)
|
||||||
|
case "prepend":
|
||||||
|
if path == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("prepend %s with %s", path, formatParamOverrideAuditValue(value))
|
||||||
|
case "append":
|
||||||
|
if path == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("append %s with %s", path, formatParamOverrideAuditValue(value))
|
||||||
|
case "trim_prefix", "trim_suffix", "ensure_prefix", "ensure_suffix":
|
||||||
|
if path == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%s %s with %s", mode, path, formatParamOverrideAuditValue(value))
|
||||||
|
case "trim_space", "to_lower", "to_upper":
|
||||||
|
if path == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%s %s", mode, path)
|
||||||
|
case "replace", "regex_replace":
|
||||||
|
if path == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%s %s from %s to %s", mode, path, from, to)
|
||||||
|
case "set_header":
|
||||||
|
if path == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("set_header %s = %s", path, formatParamOverrideAuditValue(value))
|
||||||
|
case "delete_header":
|
||||||
|
if path == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("delete_header %s", path)
|
||||||
|
case "copy_header", "move_header":
|
||||||
|
if from == "" || to == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%s %s -> %s", mode, from, to)
|
||||||
|
case "pass_headers":
|
||||||
|
return fmt.Sprintf("pass_headers %s", formatParamOverrideAuditValue(value))
|
||||||
|
case "sync_fields":
|
||||||
|
if from == "" || to == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("sync_fields %s -> %s", from, to)
|
||||||
|
case "return_error":
|
||||||
|
return fmt.Sprintf("return_error %s", formatParamOverrideAuditValue(value))
|
||||||
|
default:
|
||||||
|
if path == "" {
|
||||||
|
return mode
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%s %s", mode, path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func getParamOverrideMap(info *RelayInfo) map[string]interface{} {
|
func getParamOverrideMap(info *RelayInfo) map[string]interface{} {
|
||||||
if info == nil || info.ChannelMeta == nil {
|
if info == nil || info.ChannelMeta == nil {
|
||||||
return nil
|
return nil
|
||||||
@@ -455,7 +655,7 @@ func compareNumeric(jsonValue, targetValue gjson.Result, operator string) (bool,
|
|||||||
}
|
}
|
||||||
|
|
||||||
// applyOperationsLegacy 原参数覆盖方法
|
// applyOperationsLegacy 原参数覆盖方法
|
||||||
func applyOperationsLegacy(jsonData []byte, paramOverride map[string]interface{}) ([]byte, error) {
|
func applyOperationsLegacy(jsonData []byte, paramOverride map[string]interface{}, auditRecorder *paramOverrideAuditRecorder) ([]byte, error) {
|
||||||
reqMap := make(map[string]interface{})
|
reqMap := make(map[string]interface{})
|
||||||
err := common.Unmarshal(jsonData, &reqMap)
|
err := common.Unmarshal(jsonData, &reqMap)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -464,6 +664,7 @@ func applyOperationsLegacy(jsonData []byte, paramOverride map[string]interface{}
|
|||||||
|
|
||||||
for key, value := range paramOverride {
|
for key, value := range paramOverride {
|
||||||
reqMap[key] = value
|
reqMap[key] = value
|
||||||
|
auditRecorder.recordOperation("set", key, "", "", value)
|
||||||
}
|
}
|
||||||
|
|
||||||
return common.Marshal(reqMap)
|
return common.Marshal(reqMap)
|
||||||
@@ -471,6 +672,7 @@ func applyOperationsLegacy(jsonData []byte, paramOverride map[string]interface{}
|
|||||||
|
|
||||||
func applyOperations(jsonStr string, operations []ParamOperation, conditionContext map[string]interface{}) (string, error) {
|
func applyOperations(jsonStr string, operations []ParamOperation, conditionContext map[string]interface{}) (string, error) {
|
||||||
context := ensureContextMap(conditionContext)
|
context := ensureContextMap(conditionContext)
|
||||||
|
auditRecorder := getParamOverrideAuditRecorder(context)
|
||||||
contextJSON, err := marshalContextJSON(context)
|
contextJSON, err := marshalContextJSON(context)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("failed to marshal condition context: %v", err)
|
return "", fmt.Errorf("failed to marshal condition context: %v", err)
|
||||||
@@ -506,6 +708,7 @@ func applyOperations(jsonStr string, operations []ParamOperation, conditionConte
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
auditRecorder.recordOperation("delete", path, "", "", nil)
|
||||||
}
|
}
|
||||||
case "set":
|
case "set":
|
||||||
for _, path := range opPaths {
|
for _, path := range opPaths {
|
||||||
@@ -516,11 +719,15 @@ func applyOperations(jsonStr string, operations []ParamOperation, conditionConte
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
auditRecorder.recordOperation("set", path, "", "", op.Value)
|
||||||
}
|
}
|
||||||
case "move":
|
case "move":
|
||||||
opFrom := processNegativeIndex(result, op.From)
|
opFrom := processNegativeIndex(result, op.From)
|
||||||
opTo := processNegativeIndex(result, op.To)
|
opTo := processNegativeIndex(result, op.To)
|
||||||
result, err = moveValue(result, opFrom, opTo)
|
result, err = moveValue(result, opFrom, opTo)
|
||||||
|
if err == nil {
|
||||||
|
auditRecorder.recordOperation("move", "", opFrom, opTo, nil)
|
||||||
|
}
|
||||||
case "copy":
|
case "copy":
|
||||||
if op.From == "" || op.To == "" {
|
if op.From == "" || op.To == "" {
|
||||||
return "", fmt.Errorf("copy from/to is required")
|
return "", fmt.Errorf("copy from/to is required")
|
||||||
@@ -528,12 +735,16 @@ func applyOperations(jsonStr string, operations []ParamOperation, conditionConte
|
|||||||
opFrom := processNegativeIndex(result, op.From)
|
opFrom := processNegativeIndex(result, op.From)
|
||||||
opTo := processNegativeIndex(result, op.To)
|
opTo := processNegativeIndex(result, op.To)
|
||||||
result, err = copyValue(result, opFrom, opTo)
|
result, err = copyValue(result, opFrom, opTo)
|
||||||
|
if err == nil {
|
||||||
|
auditRecorder.recordOperation("copy", "", opFrom, opTo, nil)
|
||||||
|
}
|
||||||
case "prepend":
|
case "prepend":
|
||||||
for _, path := range opPaths {
|
for _, path := range opPaths {
|
||||||
result, err = modifyValue(result, path, op.Value, op.KeepOrigin, true)
|
result, err = modifyValue(result, path, op.Value, op.KeepOrigin, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
auditRecorder.recordOperation("prepend", path, "", "", op.Value)
|
||||||
}
|
}
|
||||||
case "append":
|
case "append":
|
||||||
for _, path := range opPaths {
|
for _, path := range opPaths {
|
||||||
@@ -541,6 +752,7 @@ func applyOperations(jsonStr string, operations []ParamOperation, conditionConte
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
auditRecorder.recordOperation("append", path, "", "", op.Value)
|
||||||
}
|
}
|
||||||
case "trim_prefix":
|
case "trim_prefix":
|
||||||
for _, path := range opPaths {
|
for _, path := range opPaths {
|
||||||
@@ -548,6 +760,7 @@ func applyOperations(jsonStr string, operations []ParamOperation, conditionConte
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
auditRecorder.recordOperation("trim_prefix", path, "", "", op.Value)
|
||||||
}
|
}
|
||||||
case "trim_suffix":
|
case "trim_suffix":
|
||||||
for _, path := range opPaths {
|
for _, path := range opPaths {
|
||||||
@@ -555,6 +768,7 @@ func applyOperations(jsonStr string, operations []ParamOperation, conditionConte
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
auditRecorder.recordOperation("trim_suffix", path, "", "", op.Value)
|
||||||
}
|
}
|
||||||
case "ensure_prefix":
|
case "ensure_prefix":
|
||||||
for _, path := range opPaths {
|
for _, path := range opPaths {
|
||||||
@@ -562,6 +776,7 @@ func applyOperations(jsonStr string, operations []ParamOperation, conditionConte
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
auditRecorder.recordOperation("ensure_prefix", path, "", "", op.Value)
|
||||||
}
|
}
|
||||||
case "ensure_suffix":
|
case "ensure_suffix":
|
||||||
for _, path := range opPaths {
|
for _, path := range opPaths {
|
||||||
@@ -569,6 +784,7 @@ func applyOperations(jsonStr string, operations []ParamOperation, conditionConte
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
auditRecorder.recordOperation("ensure_suffix", path, "", "", op.Value)
|
||||||
}
|
}
|
||||||
case "trim_space":
|
case "trim_space":
|
||||||
for _, path := range opPaths {
|
for _, path := range opPaths {
|
||||||
@@ -576,6 +792,7 @@ func applyOperations(jsonStr string, operations []ParamOperation, conditionConte
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
auditRecorder.recordOperation("trim_space", path, "", "", nil)
|
||||||
}
|
}
|
||||||
case "to_lower":
|
case "to_lower":
|
||||||
for _, path := range opPaths {
|
for _, path := range opPaths {
|
||||||
@@ -583,6 +800,7 @@ func applyOperations(jsonStr string, operations []ParamOperation, conditionConte
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
auditRecorder.recordOperation("to_lower", path, "", "", nil)
|
||||||
}
|
}
|
||||||
case "to_upper":
|
case "to_upper":
|
||||||
for _, path := range opPaths {
|
for _, path := range opPaths {
|
||||||
@@ -590,6 +808,7 @@ func applyOperations(jsonStr string, operations []ParamOperation, conditionConte
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
auditRecorder.recordOperation("to_upper", path, "", "", nil)
|
||||||
}
|
}
|
||||||
case "replace":
|
case "replace":
|
||||||
for _, path := range opPaths {
|
for _, path := range opPaths {
|
||||||
@@ -597,6 +816,7 @@ func applyOperations(jsonStr string, operations []ParamOperation, conditionConte
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
auditRecorder.recordOperation("replace", path, op.From, op.To, nil)
|
||||||
}
|
}
|
||||||
case "regex_replace":
|
case "regex_replace":
|
||||||
for _, path := range opPaths {
|
for _, path := range opPaths {
|
||||||
@@ -604,8 +824,10 @@ func applyOperations(jsonStr string, operations []ParamOperation, conditionConte
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
auditRecorder.recordOperation("regex_replace", path, op.From, op.To, nil)
|
||||||
}
|
}
|
||||||
case "return_error":
|
case "return_error":
|
||||||
|
auditRecorder.recordOperation("return_error", op.Path, "", "", op.Value)
|
||||||
returnErr, parseErr := parseParamOverrideReturnError(op.Value)
|
returnErr, parseErr := parseParamOverrideReturnError(op.Value)
|
||||||
if parseErr != nil {
|
if parseErr != nil {
|
||||||
return "", parseErr
|
return "", parseErr
|
||||||
@@ -621,11 +843,13 @@ func applyOperations(jsonStr string, operations []ParamOperation, conditionConte
|
|||||||
case "set_header":
|
case "set_header":
|
||||||
err = setHeaderOverrideInContext(context, op.Path, op.Value, op.KeepOrigin)
|
err = setHeaderOverrideInContext(context, op.Path, op.Value, op.KeepOrigin)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
|
auditRecorder.recordOperation("set_header", op.Path, "", "", op.Value)
|
||||||
contextJSON, err = marshalContextJSON(context)
|
contextJSON, err = marshalContextJSON(context)
|
||||||
}
|
}
|
||||||
case "delete_header":
|
case "delete_header":
|
||||||
err = deleteHeaderOverrideInContext(context, op.Path)
|
err = deleteHeaderOverrideInContext(context, op.Path)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
|
auditRecorder.recordOperation("delete_header", op.Path, "", "", nil)
|
||||||
contextJSON, err = marshalContextJSON(context)
|
contextJSON, err = marshalContextJSON(context)
|
||||||
}
|
}
|
||||||
case "copy_header":
|
case "copy_header":
|
||||||
@@ -642,6 +866,7 @@ func applyOperations(jsonStr string, operations []ParamOperation, conditionConte
|
|||||||
err = nil
|
err = nil
|
||||||
}
|
}
|
||||||
if err == nil {
|
if err == nil {
|
||||||
|
auditRecorder.recordOperation("copy_header", "", sourceHeader, targetHeader, nil)
|
||||||
contextJSON, err = marshalContextJSON(context)
|
contextJSON, err = marshalContextJSON(context)
|
||||||
}
|
}
|
||||||
case "move_header":
|
case "move_header":
|
||||||
@@ -658,6 +883,7 @@ func applyOperations(jsonStr string, operations []ParamOperation, conditionConte
|
|||||||
err = nil
|
err = nil
|
||||||
}
|
}
|
||||||
if err == nil {
|
if err == nil {
|
||||||
|
auditRecorder.recordOperation("move_header", "", sourceHeader, targetHeader, nil)
|
||||||
contextJSON, err = marshalContextJSON(context)
|
contextJSON, err = marshalContextJSON(context)
|
||||||
}
|
}
|
||||||
case "pass_headers":
|
case "pass_headers":
|
||||||
@@ -675,11 +901,13 @@ func applyOperations(jsonStr string, operations []ParamOperation, conditionConte
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if err == nil {
|
if err == nil {
|
||||||
|
auditRecorder.recordOperation("pass_headers", "", "", "", headerNames)
|
||||||
contextJSON, err = marshalContextJSON(context)
|
contextJSON, err = marshalContextJSON(context)
|
||||||
}
|
}
|
||||||
case "sync_fields":
|
case "sync_fields":
|
||||||
result, err = syncFieldsBetweenTargets(result, context, op.From, op.To)
|
result, err = syncFieldsBetweenTargets(result, context, op.From, op.To)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
|
auditRecorder.recordOperation("sync_fields", "", op.From, op.To, nil)
|
||||||
contextJSON, err = marshalContextJSON(context)
|
contextJSON, err = marshalContextJSON(context)
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"reflect"
|
"reflect"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
common2 "github.com/QuantumNous/new-api/common"
|
||||||
"github.com/QuantumNous/new-api/types"
|
"github.com/QuantumNous/new-api/types"
|
||||||
|
|
||||||
"github.com/QuantumNous/new-api/dto"
|
"github.com/QuantumNous/new-api/dto"
|
||||||
@@ -2066,6 +2067,105 @@ func TestRemoveDisabledFieldsAllowInferenceGeo(t *testing.T) {
|
|||||||
assertJSONEqual(t, `{"inference_geo":"eu","store":true}`, string(out))
|
assertJSONEqual(t, `{"inference_geo":"eu","store":true}`, string(out))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestApplyParamOverrideWithRelayInfoRecordsOperationAuditInDebugMode(t *testing.T) {
|
||||||
|
originalDebugEnabled := common2.DebugEnabled
|
||||||
|
common2.DebugEnabled = true
|
||||||
|
t.Cleanup(func() {
|
||||||
|
common2.DebugEnabled = originalDebugEnabled
|
||||||
|
})
|
||||||
|
|
||||||
|
info := &RelayInfo{
|
||||||
|
ChannelMeta: &ChannelMeta{
|
||||||
|
ParamOverride: map[string]interface{}{
|
||||||
|
"operations": []interface{}{
|
||||||
|
map[string]interface{}{
|
||||||
|
"mode": "copy",
|
||||||
|
"from": "metadata.target_model",
|
||||||
|
"to": "model",
|
||||||
|
},
|
||||||
|
map[string]interface{}{
|
||||||
|
"mode": "set",
|
||||||
|
"path": "service_tier",
|
||||||
|
"value": "flex",
|
||||||
|
},
|
||||||
|
map[string]interface{}{
|
||||||
|
"mode": "set",
|
||||||
|
"path": "temperature",
|
||||||
|
"value": 0.1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
out, err := ApplyParamOverrideWithRelayInfo([]byte(`{
|
||||||
|
"model":"gpt-4.1",
|
||||||
|
"temperature":0.7,
|
||||||
|
"metadata":{"target_model":"gpt-4.1-mini"}
|
||||||
|
}`), info)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ApplyParamOverrideWithRelayInfo returned error: %v", err)
|
||||||
|
}
|
||||||
|
assertJSONEqual(t, `{
|
||||||
|
"model":"gpt-4.1-mini",
|
||||||
|
"temperature":0.1,
|
||||||
|
"service_tier":"flex",
|
||||||
|
"metadata":{"target_model":"gpt-4.1-mini"}
|
||||||
|
}`, string(out))
|
||||||
|
|
||||||
|
expected := []string{
|
||||||
|
"copy metadata.target_model -> model",
|
||||||
|
"set service_tier = flex",
|
||||||
|
"set temperature = 0.1",
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(info.ParamOverrideAudit, expected) {
|
||||||
|
t.Fatalf("unexpected param override audit, got %#v", info.ParamOverrideAudit)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestApplyParamOverrideWithRelayInfoRecordsOnlyKeyOperationsWhenDebugDisabled(t *testing.T) {
|
||||||
|
originalDebugEnabled := common2.DebugEnabled
|
||||||
|
common2.DebugEnabled = false
|
||||||
|
t.Cleanup(func() {
|
||||||
|
common2.DebugEnabled = originalDebugEnabled
|
||||||
|
})
|
||||||
|
|
||||||
|
info := &RelayInfo{
|
||||||
|
ChannelMeta: &ChannelMeta{
|
||||||
|
ParamOverride: map[string]interface{}{
|
||||||
|
"operations": []interface{}{
|
||||||
|
map[string]interface{}{
|
||||||
|
"mode": "copy",
|
||||||
|
"from": "metadata.target_model",
|
||||||
|
"to": "model",
|
||||||
|
},
|
||||||
|
map[string]interface{}{
|
||||||
|
"mode": "set",
|
||||||
|
"path": "temperature",
|
||||||
|
"value": 0.1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := ApplyParamOverrideWithRelayInfo([]byte(`{
|
||||||
|
"model":"gpt-4.1",
|
||||||
|
"temperature":0.7,
|
||||||
|
"metadata":{"target_model":"gpt-4.1-mini"}
|
||||||
|
}`), info)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ApplyParamOverrideWithRelayInfo returned error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
expected := []string{
|
||||||
|
"copy metadata.target_model -> model",
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(info.ParamOverrideAudit, expected) {
|
||||||
|
t.Fatalf("unexpected param override audit, got %#v", info.ParamOverrideAudit)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func assertJSONEqual(t *testing.T, want, got string) {
|
func assertJSONEqual(t *testing.T, want, got string) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
|
|||||||
@@ -149,6 +149,7 @@ type RelayInfo struct {
|
|||||||
LastError *types.NewAPIError
|
LastError *types.NewAPIError
|
||||||
RuntimeHeadersOverride map[string]interface{}
|
RuntimeHeadersOverride map[string]interface{}
|
||||||
UseRuntimeHeadersOverride bool
|
UseRuntimeHeadersOverride bool
|
||||||
|
ParamOverrideAudit []string
|
||||||
|
|
||||||
PriceData types.PriceData
|
PriceData types.PriceData
|
||||||
|
|
||||||
|
|||||||
@@ -74,9 +74,17 @@ func GenerateTextOtherInfo(ctx *gin.Context, relayInfo *relaycommon.RelayInfo, m
|
|||||||
appendRequestPath(ctx, relayInfo, other)
|
appendRequestPath(ctx, relayInfo, other)
|
||||||
appendRequestConversionChain(relayInfo, other)
|
appendRequestConversionChain(relayInfo, other)
|
||||||
appendBillingInfo(relayInfo, other)
|
appendBillingInfo(relayInfo, other)
|
||||||
|
appendParamOverrideInfo(relayInfo, other)
|
||||||
return other
|
return other
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func appendParamOverrideInfo(relayInfo *relaycommon.RelayInfo, other map[string]interface{}) {
|
||||||
|
if relayInfo == nil || other == nil || len(relayInfo.ParamOverrideAudit) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
other["po"] = relayInfo.ParamOverrideAudit
|
||||||
|
}
|
||||||
|
|
||||||
func appendBillingInfo(relayInfo *relaycommon.RelayInfo, other map[string]interface{}) {
|
func appendBillingInfo(relayInfo *relaycommon.RelayInfo, other map[string]interface{}) {
|
||||||
if relayInfo == nil || other == nil {
|
if relayInfo == nil || other == nil {
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -0,0 +1,54 @@
|
|||||||
|
/*
|
||||||
|
Copyright (C) 2025 QuantumNous
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as
|
||||||
|
published by the Free Software Foundation, either version 3 of the
|
||||||
|
License, or (at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
For commercial licensing, please contact support@quantumnous.com
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import { Typography } from '@douyinfe/semi-ui';
|
||||||
|
|
||||||
|
const { Text } = Typography;
|
||||||
|
|
||||||
|
const ParamOverrideEntry = ({ count, onOpen, t }) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: 10,
|
||||||
|
flexWrap: 'wrap',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Text
|
||||||
|
type='tertiary'
|
||||||
|
size='small'
|
||||||
|
style={{ fontVariantNumeric: 'tabular-nums' }}
|
||||||
|
>
|
||||||
|
{t('{{count}} 项操作', { count })}
|
||||||
|
</Text>
|
||||||
|
<Text
|
||||||
|
link
|
||||||
|
size='small'
|
||||||
|
style={{ fontWeight: 600 }}
|
||||||
|
onClick={onOpen}
|
||||||
|
>
|
||||||
|
{t('查看详情')}
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default React.memo(ParamOverrideEntry);
|
||||||
@@ -25,6 +25,7 @@ import LogsFilters from './UsageLogsFilters';
|
|||||||
import ColumnSelectorModal from './modals/ColumnSelectorModal';
|
import ColumnSelectorModal from './modals/ColumnSelectorModal';
|
||||||
import UserInfoModal from './modals/UserInfoModal';
|
import UserInfoModal from './modals/UserInfoModal';
|
||||||
import ChannelAffinityUsageCacheModal from './modals/ChannelAffinityUsageCacheModal';
|
import ChannelAffinityUsageCacheModal from './modals/ChannelAffinityUsageCacheModal';
|
||||||
|
import ParamOverrideModal from './modals/ParamOverrideModal';
|
||||||
import { useLogsData } from '../../../hooks/usage-logs/useUsageLogsData';
|
import { useLogsData } from '../../../hooks/usage-logs/useUsageLogsData';
|
||||||
import { useIsMobile } from '../../../hooks/common/useIsMobile';
|
import { useIsMobile } from '../../../hooks/common/useIsMobile';
|
||||||
import { createCardProPagination } from '../../../helpers/utils';
|
import { createCardProPagination } from '../../../helpers/utils';
|
||||||
@@ -39,6 +40,7 @@ const LogsPage = () => {
|
|||||||
<ColumnSelectorModal {...logsData} />
|
<ColumnSelectorModal {...logsData} />
|
||||||
<UserInfoModal {...logsData} />
|
<UserInfoModal {...logsData} />
|
||||||
<ChannelAffinityUsageCacheModal {...logsData} />
|
<ChannelAffinityUsageCacheModal {...logsData} />
|
||||||
|
<ParamOverrideModal {...logsData} />
|
||||||
|
|
||||||
{/* Main Content */}
|
{/* Main Content */}
|
||||||
<CardPro
|
<CardPro
|
||||||
|
|||||||
@@ -0,0 +1,272 @@
|
|||||||
|
/*
|
||||||
|
Copyright (C) 2025 QuantumNous
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as
|
||||||
|
published by the Free Software Foundation, either version 3 of the
|
||||||
|
License, or (at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
For commercial licensing, please contact support@quantumnous.com
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, { useMemo } from 'react';
|
||||||
|
import {
|
||||||
|
Modal,
|
||||||
|
Button,
|
||||||
|
Empty,
|
||||||
|
Divider,
|
||||||
|
Typography,
|
||||||
|
} from '@douyinfe/semi-ui';
|
||||||
|
import { IconCopy } from '@douyinfe/semi-icons';
|
||||||
|
import { copy, showError, showSuccess } from '../../../../helpers';
|
||||||
|
|
||||||
|
const { Text } = Typography;
|
||||||
|
|
||||||
|
const parseAuditLine = (line) => {
|
||||||
|
if (typeof line !== 'string') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const firstSpaceIndex = line.indexOf(' ');
|
||||||
|
if (firstSpaceIndex <= 0) {
|
||||||
|
return { action: line, content: line };
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
action: line.slice(0, firstSpaceIndex),
|
||||||
|
content: line.slice(firstSpaceIndex + 1),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const getActionLabel = (action, t) => {
|
||||||
|
switch ((action || '').toLowerCase()) {
|
||||||
|
case 'set':
|
||||||
|
return t('设置');
|
||||||
|
case 'delete':
|
||||||
|
return t('删除');
|
||||||
|
case 'copy':
|
||||||
|
return t('复制');
|
||||||
|
case 'move':
|
||||||
|
return t('移动');
|
||||||
|
case 'append':
|
||||||
|
return t('追加');
|
||||||
|
case 'prepend':
|
||||||
|
return t('前置');
|
||||||
|
case 'trim_prefix':
|
||||||
|
return t('去前缀');
|
||||||
|
case 'trim_suffix':
|
||||||
|
return t('去后缀');
|
||||||
|
case 'ensure_prefix':
|
||||||
|
return t('保前缀');
|
||||||
|
case 'ensure_suffix':
|
||||||
|
return t('保后缀');
|
||||||
|
case 'trim_space':
|
||||||
|
return t('去空格');
|
||||||
|
case 'to_lower':
|
||||||
|
return t('转小写');
|
||||||
|
case 'to_upper':
|
||||||
|
return t('转大写');
|
||||||
|
case 'replace':
|
||||||
|
return t('替换');
|
||||||
|
case 'regex_replace':
|
||||||
|
return t('正则替换');
|
||||||
|
case 'set_header':
|
||||||
|
return t('设请求头');
|
||||||
|
case 'delete_header':
|
||||||
|
return t('删请求头');
|
||||||
|
case 'copy_header':
|
||||||
|
return t('复制请求头');
|
||||||
|
case 'move_header':
|
||||||
|
return t('移动请求头');
|
||||||
|
case 'pass_headers':
|
||||||
|
return t('透传请求头');
|
||||||
|
case 'sync_fields':
|
||||||
|
return t('同步字段');
|
||||||
|
case 'return_error':
|
||||||
|
return t('返回错误');
|
||||||
|
default:
|
||||||
|
return action;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const ParamOverrideModal = ({
|
||||||
|
showParamOverrideModal,
|
||||||
|
setShowParamOverrideModal,
|
||||||
|
paramOverrideTarget,
|
||||||
|
t,
|
||||||
|
}) => {
|
||||||
|
const lines = Array.isArray(paramOverrideTarget?.lines)
|
||||||
|
? paramOverrideTarget.lines
|
||||||
|
: [];
|
||||||
|
|
||||||
|
const parsedLines = useMemo(() => {
|
||||||
|
return lines.map(parseAuditLine);
|
||||||
|
}, [lines]);
|
||||||
|
|
||||||
|
const copyAll = async () => {
|
||||||
|
const content = lines.join('\n');
|
||||||
|
if (!content) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (await copy(content)) {
|
||||||
|
showSuccess(t('参数覆盖已复制'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
showError(t('无法复制到剪贴板,请手动复制'));
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
title={t('参数覆盖详情')}
|
||||||
|
visible={showParamOverrideModal}
|
||||||
|
onCancel={() => setShowParamOverrideModal(false)}
|
||||||
|
footer={null}
|
||||||
|
centered
|
||||||
|
closable
|
||||||
|
maskClosable
|
||||||
|
width={640}
|
||||||
|
>
|
||||||
|
<div style={{ padding: '8px 20px 20px' }}>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
alignItems: 'flex-start',
|
||||||
|
gap: 12,
|
||||||
|
marginBottom: 10,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div style={{ minWidth: 0 }}>
|
||||||
|
<div style={{ marginBottom: 4 }}>
|
||||||
|
<Text style={{ fontWeight: 600 }}>
|
||||||
|
{t('{{count}} 项操作', { count: lines.length })}
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
flexWrap: 'wrap',
|
||||||
|
gap: 8,
|
||||||
|
fontSize: 12,
|
||||||
|
color: 'var(--semi-color-text-2)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{paramOverrideTarget?.modelName ? (
|
||||||
|
<Text type='tertiary' size='small'>
|
||||||
|
{paramOverrideTarget.modelName}
|
||||||
|
</Text>
|
||||||
|
) : null}
|
||||||
|
{paramOverrideTarget?.requestId ? (
|
||||||
|
<Text type='tertiary' size='small'>
|
||||||
|
{t('Request ID')}: {paramOverrideTarget.requestId}
|
||||||
|
</Text>
|
||||||
|
) : null}
|
||||||
|
{paramOverrideTarget?.requestPath ? (
|
||||||
|
<Text type='tertiary' size='small'>
|
||||||
|
{t('请求路径')}: {paramOverrideTarget.requestPath}
|
||||||
|
</Text>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
icon={<IconCopy />}
|
||||||
|
theme='borderless'
|
||||||
|
type='tertiary'
|
||||||
|
size='small'
|
||||||
|
onClick={copyAll}
|
||||||
|
disabled={lines.length === 0}
|
||||||
|
>
|
||||||
|
{t('复制')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Divider margin='12px' />
|
||||||
|
|
||||||
|
{lines.length === 0 ? (
|
||||||
|
<Empty
|
||||||
|
description={t('暂无参数覆盖记录')}
|
||||||
|
style={{ padding: '24px 0 8px' }}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
gap: 8,
|
||||||
|
maxHeight: '56vh',
|
||||||
|
overflowY: 'auto',
|
||||||
|
paddingRight: 2,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{parsedLines.map((item, index) => {
|
||||||
|
if (!item) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={`${item.action}-${index}`}
|
||||||
|
style={{
|
||||||
|
padding: '10px 12px',
|
||||||
|
borderRadius: 10,
|
||||||
|
border: '1px solid var(--semi-color-border)',
|
||||||
|
background: 'var(--semi-color-fill-0)',
|
||||||
|
display: 'flex',
|
||||||
|
gap: 12,
|
||||||
|
alignItems: 'flex-start',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
flex: '0 0 auto',
|
||||||
|
minWidth: 74,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Text
|
||||||
|
style={{
|
||||||
|
display: 'inline-block',
|
||||||
|
fontSize: 11,
|
||||||
|
fontWeight: 700,
|
||||||
|
lineHeight: '20px',
|
||||||
|
padding: '0 8px',
|
||||||
|
borderRadius: 999,
|
||||||
|
background: 'rgba(var(--semi-blue-5), 0.12)',
|
||||||
|
color: 'var(--semi-color-primary)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{getActionLabel(item.action, t)}
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
<Text
|
||||||
|
style={{
|
||||||
|
flex: 1,
|
||||||
|
minWidth: 0,
|
||||||
|
fontFamily:
|
||||||
|
'ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas, monospace',
|
||||||
|
fontSize: 12,
|
||||||
|
lineHeight: 1.6,
|
||||||
|
whiteSpace: 'pre-wrap',
|
||||||
|
wordBreak: 'break-word',
|
||||||
|
color: 'var(--semi-color-text-0)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{item.content}
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ParamOverrideModal;
|
||||||
+36
@@ -39,6 +39,7 @@ import {
|
|||||||
} from '../../helpers';
|
} from '../../helpers';
|
||||||
import { ITEMS_PER_PAGE } from '../../constants';
|
import { ITEMS_PER_PAGE } from '../../constants';
|
||||||
import { useTableCompactMode } from '../common/useTableCompactMode';
|
import { useTableCompactMode } from '../common/useTableCompactMode';
|
||||||
|
import ParamOverrideEntry from '../../components/table/usage-logs/components/ParamOverrideEntry';
|
||||||
|
|
||||||
export const useLogsData = () => {
|
export const useLogsData = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@@ -181,6 +182,8 @@ export const useLogsData = () => {
|
|||||||
] = useState(false);
|
] = useState(false);
|
||||||
const [channelAffinityUsageCacheTarget, setChannelAffinityUsageCacheTarget] =
|
const [channelAffinityUsageCacheTarget, setChannelAffinityUsageCacheTarget] =
|
||||||
useState(null);
|
useState(null);
|
||||||
|
const [showParamOverrideModal, setShowParamOverrideModal] = useState(false);
|
||||||
|
const [paramOverrideTarget, setParamOverrideTarget] = useState(null);
|
||||||
|
|
||||||
// Initialize default column visibility
|
// Initialize default column visibility
|
||||||
const initDefaultColumns = () => {
|
const initDefaultColumns = () => {
|
||||||
@@ -345,6 +348,20 @@ export const useLogsData = () => {
|
|||||||
setShowChannelAffinityUsageCacheModal(true);
|
setShowChannelAffinityUsageCacheModal(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const openParamOverrideModal = (log, other) => {
|
||||||
|
const lines = Array.isArray(other?.po) ? other.po.filter(Boolean) : [];
|
||||||
|
if (lines.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setParamOverrideTarget({
|
||||||
|
lines,
|
||||||
|
modelName: log?.model_name || '',
|
||||||
|
requestId: log?.request_id || '',
|
||||||
|
requestPath: other?.request_path || '',
|
||||||
|
});
|
||||||
|
setShowParamOverrideModal(true);
|
||||||
|
};
|
||||||
|
|
||||||
// Format logs data
|
// Format logs data
|
||||||
const setLogsFormat = (logs) => {
|
const setLogsFormat = (logs) => {
|
||||||
const requestConversionDisplayValue = (conversionChain) => {
|
const requestConversionDisplayValue = (conversionChain) => {
|
||||||
@@ -584,6 +601,21 @@ export const useLogsData = () => {
|
|||||||
value: other.request_path,
|
value: other.request_path,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
if (Array.isArray(other?.po) && other.po.length > 0) {
|
||||||
|
expandDataLocal.push({
|
||||||
|
key: t('参数覆盖'),
|
||||||
|
value: (
|
||||||
|
<ParamOverrideEntry
|
||||||
|
count={other.po.length}
|
||||||
|
t={t}
|
||||||
|
onOpen={(event) => {
|
||||||
|
event.stopPropagation();
|
||||||
|
openParamOverrideModal(logs[i], other);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
});
|
||||||
|
}
|
||||||
if (other?.billing_source === 'subscription') {
|
if (other?.billing_source === 'subscription') {
|
||||||
const planId = other?.subscription_plan_id;
|
const planId = other?.subscription_plan_id;
|
||||||
const planTitle = other?.subscription_plan_title || '';
|
const planTitle = other?.subscription_plan_title || '';
|
||||||
@@ -811,6 +843,9 @@ export const useLogsData = () => {
|
|||||||
setShowChannelAffinityUsageCacheModal,
|
setShowChannelAffinityUsageCacheModal,
|
||||||
channelAffinityUsageCacheTarget,
|
channelAffinityUsageCacheTarget,
|
||||||
openChannelAffinityUsageCacheModal,
|
openChannelAffinityUsageCacheModal,
|
||||||
|
showParamOverrideModal,
|
||||||
|
setShowParamOverrideModal,
|
||||||
|
paramOverrideTarget,
|
||||||
|
|
||||||
// Functions
|
// Functions
|
||||||
loadLogs,
|
loadLogs,
|
||||||
@@ -822,6 +857,7 @@ export const useLogsData = () => {
|
|||||||
setLogsFormat,
|
setLogsFormat,
|
||||||
hasExpandableRows,
|
hasExpandableRows,
|
||||||
setLogType,
|
setLogType,
|
||||||
|
openParamOverrideModal,
|
||||||
|
|
||||||
// Translation
|
// Translation
|
||||||
t,
|
t,
|
||||||
|
|||||||
Reference in New Issue
Block a user