feat: simplify param override audit UI and operation labels

This commit is contained in:
Seefs
2026-03-17 17:00:01 +08:00
parent 5db25f47f1
commit bc80477b1a
5 changed files with 448 additions and 282 deletions
+136 -103
View File
@@ -230,28 +230,14 @@ func getParamOverrideAuditRecorder(context map[string]interface{}) *paramOverrid
return recorder return recorder
} }
func (r *paramOverrideAuditRecorder) record(path string, beforeExists bool, beforeValue interface{}, afterExists bool, afterValue interface{}) { func (r *paramOverrideAuditRecorder) recordOperation(mode, path, from, to string, value interface{}) {
if r == nil { if r == nil {
return return
} }
path = strings.TrimSpace(path) line := buildParamOverrideAuditLine(mode, path, from, to, value)
if path == "" { if line == "" {
return return
} }
if !shouldAuditParamPath(path) {
return
}
beforeText := "<empty>"
if beforeExists {
beforeText = formatParamOverrideAuditValue(beforeValue)
}
afterText := "<deleted>"
if afterExists {
afterText = formatParamOverrideAuditValue(afterValue)
}
line := fmt.Sprintf("%s: %s -> %s", path, beforeText, afterText)
if lo.Contains(r.lines, line) { if lo.Contains(r.lines, line) {
return return
} }
@@ -270,23 +256,16 @@ func shouldAuditParamPath(path string) bool {
return ok return ok
} }
func applyAuditedPathMutation(result, path string, auditRecorder *paramOverrideAuditRecorder, mutate func(string) (string, error)) (string, error) { func shouldAuditOperation(mode, path, from, to string) bool {
needAudit := auditRecorder != nil && shouldAuditParamPath(path) if common.DebugEnabled {
var beforeResult gjson.Result return true
if needAudit {
beforeResult = gjson.Get(result, path)
} }
for _, candidate := range []string{path, to} {
next, err := mutate(result) if shouldAuditParamPath(candidate) {
if err != nil { return true
return next, err }
} }
return false
if needAudit {
afterResult := gjson.Get(next, path)
auditRecorder.record(path, beforeResult.Exists(), beforeResult.Value(), afterResult.Exists(), afterResult.Value())
}
return next, nil
} }
func formatParamOverrideAuditValue(value interface{}) string { func formatParamOverrideAuditValue(value interface{}) string {
@@ -300,6 +279,94 @@ func formatParamOverrideAuditValue(value interface{}) string {
} }
} }
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
@@ -594,9 +661,8 @@ func applyOperationsLegacy(jsonData []byte, paramOverride map[string]interface{}
} }
for key, value := range paramOverride { for key, value := range paramOverride {
beforeValue, beforeExists := reqMap[key]
reqMap[key] = value reqMap[key] = value
auditRecorder.record(key, beforeExists, beforeValue, true, value) auditRecorder.recordOperation("set", key, "", "", value)
} }
return common.Marshal(reqMap) return common.Marshal(reqMap)
@@ -636,47 +702,29 @@ func applyOperations(jsonStr string, operations []ParamOperation, conditionConte
switch op.Mode { switch op.Mode {
case "delete": case "delete":
for _, path := range opPaths { for _, path := range opPaths {
result, err = applyAuditedPathMutation(result, path, auditRecorder, func(current string) (string, error) { result, err = deleteValue(result, path)
return deleteValue(current, path)
})
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 {
if op.KeepOrigin && gjson.Get(result, path).Exists() { if op.KeepOrigin && gjson.Get(result, path).Exists() {
continue continue
} }
result, err = applyAuditedPathMutation(result, path, auditRecorder, func(current string) (string, error) { result, err = sjson.Set(result, path, op.Value)
return sjson.Set(current, path, op.Value)
})
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)
needAuditTo := auditRecorder != nil && shouldAuditParamPath(opTo)
needAuditFrom := auditRecorder != nil && shouldAuditParamPath(opFrom)
var beforeResult gjson.Result
var fromResult gjson.Result
if needAuditTo {
beforeResult = gjson.Get(result, opTo)
}
if needAuditFrom {
fromResult = gjson.Get(result, opFrom)
}
result, err = moveValue(result, opFrom, opTo) result, err = moveValue(result, opFrom, opTo)
if err == nil { if err == nil {
if needAuditTo { auditRecorder.recordOperation("move", "", opFrom, opTo, nil)
afterResult := gjson.Get(result, opTo)
auditRecorder.record(opTo, beforeResult.Exists(), beforeResult.Value(), afterResult.Exists(), afterResult.Value())
}
if needAuditFrom && common.DebugEnabled {
auditRecorder.record(opFrom, fromResult.Exists(), fromResult.Value(), false, nil)
}
} }
case "copy": case "copy":
if op.From == "" || op.To == "" { if op.From == "" || op.To == "" {
@@ -684,116 +732,100 @@ 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)
needAudit := auditRecorder != nil && shouldAuditParamPath(opTo)
var beforeResult gjson.Result
if needAudit {
beforeResult = gjson.Get(result, opTo)
}
result, err = copyValue(result, opFrom, opTo) result, err = copyValue(result, opFrom, opTo)
if err == nil && needAudit { if err == nil {
afterResult := gjson.Get(result, opTo) auditRecorder.recordOperation("copy", "", opFrom, opTo, nil)
auditRecorder.record(opTo, beforeResult.Exists(), beforeResult.Value(), afterResult.Exists(), afterResult.Value())
} }
case "prepend": case "prepend":
for _, path := range opPaths { for _, path := range opPaths {
result, err = applyAuditedPathMutation(result, path, auditRecorder, func(current string) (string, error) { result, err = modifyValue(result, path, op.Value, op.KeepOrigin, true)
return modifyValue(current, 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 {
result, err = applyAuditedPathMutation(result, path, auditRecorder, func(current string) (string, error) { result, err = modifyValue(result, path, op.Value, op.KeepOrigin, false)
return modifyValue(current, path, op.Value, op.KeepOrigin, false)
})
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 {
result, err = applyAuditedPathMutation(result, path, auditRecorder, func(current string) (string, error) { result, err = trimStringValue(result, path, op.Value, true)
return trimStringValue(current, path, op.Value, true)
})
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 {
result, err = applyAuditedPathMutation(result, path, auditRecorder, func(current string) (string, error) { result, err = trimStringValue(result, path, op.Value, false)
return trimStringValue(current, path, op.Value, false)
})
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 {
result, err = applyAuditedPathMutation(result, path, auditRecorder, func(current string) (string, error) { result, err = ensureStringAffix(result, path, op.Value, true)
return ensureStringAffix(current, path, op.Value, true)
})
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 {
result, err = applyAuditedPathMutation(result, path, auditRecorder, func(current string) (string, error) { result, err = ensureStringAffix(result, path, op.Value, false)
return ensureStringAffix(current, path, op.Value, false)
})
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 {
result, err = applyAuditedPathMutation(result, path, auditRecorder, func(current string) (string, error) { result, err = transformStringValue(result, path, strings.TrimSpace)
return transformStringValue(current, path, strings.TrimSpace)
})
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 {
result, err = applyAuditedPathMutation(result, path, auditRecorder, func(current string) (string, error) { result, err = transformStringValue(result, path, strings.ToLower)
return transformStringValue(current, path, strings.ToLower)
})
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 {
result, err = applyAuditedPathMutation(result, path, auditRecorder, func(current string) (string, error) { result, err = transformStringValue(result, path, strings.ToUpper)
return transformStringValue(current, path, strings.ToUpper)
})
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 {
result, err = applyAuditedPathMutation(result, path, auditRecorder, func(current string) (string, error) { result, err = replaceStringValue(result, path, op.From, op.To)
return replaceStringValue(current, path, op.From, op.To)
})
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 {
result, err = applyAuditedPathMutation(result, path, auditRecorder, func(current string) (string, error) { result, err = regexReplaceStringValue(result, path, op.From, op.To)
return regexReplaceStringValue(current, path, op.From, op.To)
})
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
@@ -809,11 +841,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":
@@ -830,6 +864,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":
@@ -846,6 +881,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":
@@ -863,11 +899,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:
@@ -985,7 +1023,6 @@ func setHeaderOverrideInContext(context map[string]interface{}, headerName strin
} }
rawHeaders := ensureMapKeyInContext(context, paramOverrideContextHeaderOverride) rawHeaders := ensureMapKeyInContext(context, paramOverrideContextHeaderOverride)
beforeRaw, beforeExists := rawHeaders[headerName]
if keepOrigin { if keepOrigin {
if existing, ok := rawHeaders[headerName]; ok { if existing, ok := rawHeaders[headerName]; ok {
existingValue := strings.TrimSpace(fmt.Sprintf("%v", existing)) existingValue := strings.TrimSpace(fmt.Sprintf("%v", existing))
@@ -1001,12 +1038,10 @@ func setHeaderOverrideInContext(context map[string]interface{}, headerName strin
} }
if !hasValue { if !hasValue {
delete(rawHeaders, headerName) delete(rawHeaders, headerName)
getParamOverrideAuditRecorder(context).record("header."+headerName, beforeExists, beforeRaw, false, nil)
return nil return nil
} }
rawHeaders[headerName] = headerValue rawHeaders[headerName] = headerValue
getParamOverrideAuditRecorder(context).record("header."+headerName, beforeExists, beforeRaw, true, headerValue)
return nil return nil
} }
@@ -1178,9 +1213,7 @@ func deleteHeaderOverrideInContext(context map[string]interface{}, headerName st
return fmt.Errorf("header name is required") return fmt.Errorf("header name is required")
} }
rawHeaders := ensureMapKeyInContext(context, paramOverrideContextHeaderOverride) rawHeaders := ensureMapKeyInContext(context, paramOverrideContextHeaderOverride)
beforeRaw, beforeExists := rawHeaders[headerName]
delete(rawHeaders, headerName) delete(rawHeaders, headerName)
getParamOverrideAuditRecorder(context).record("header."+headerName, beforeExists, beforeRaw, false, nil)
return nil return nil
} }
+100
View File
@@ -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()
@@ -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);
@@ -22,8 +22,7 @@ import {
Modal, Modal,
Button, Button,
Empty, Empty,
Space, Divider,
Tag,
Typography, Typography,
} from '@douyinfe/semi-ui'; } from '@douyinfe/semi-ui';
import { IconCopy } from '@douyinfe/semi-icons'; import { IconCopy } from '@douyinfe/semi-icons';
@@ -35,59 +34,66 @@ const parseAuditLine = (line) => {
if (typeof line !== 'string') { if (typeof line !== 'string') {
return null; return null;
} }
const colonIndex = line.indexOf(': '); const firstSpaceIndex = line.indexOf(' ');
const arrowIndex = line.indexOf(' -> ', colonIndex + 2); if (firstSpaceIndex <= 0) {
if (colonIndex <= 0 || arrowIndex <= colonIndex) { return { action: line, content: line };
return null;
} }
return { return {
field: line.slice(0, colonIndex), action: line.slice(0, firstSpaceIndex),
before: line.slice(colonIndex + 2, arrowIndex), content: line.slice(firstSpaceIndex + 1),
after: line.slice(arrowIndex + 4),
raw: line,
}; };
}; };
const ValuePanel = ({ label, value, tone }) => ( const getActionLabel = (action, t) => {
<div switch ((action || '').toLowerCase()) {
style={{ case 'set':
flex: 1, return t('设置');
minWidth: 0, case 'delete':
padding: 12, return t('删除');
borderRadius: 12, case 'copy':
border: '1px solid var(--semi-color-border)', return t('复制');
background: case 'move':
tone === 'after' return t('移动');
? 'rgba(var(--semi-blue-5), 0.08)' case 'append':
: 'var(--semi-color-fill-0)', return t('追加');
}} case 'prepend':
> return t('前置');
<div case 'trim_prefix':
style={{ return t('去前缀');
marginBottom: 6, case 'trim_suffix':
fontSize: 12, return t('去后缀');
fontWeight: 600, case 'ensure_prefix':
color: 'var(--semi-color-text-2)', return t('保前缀');
}} case 'ensure_suffix':
> return t('保后缀');
{label} case 'trim_space':
</div> return t('去空格');
<Text case 'to_lower':
style={{ return t('转小写');
display: 'block', case 'to_upper':
fontFamily: return t('转大写');
'ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas, monospace', case 'replace':
fontSize: 13, return t('替换');
lineHeight: 1.65, case 'regex_replace':
whiteSpace: 'pre-wrap', return t('正则替换');
wordBreak: 'break-word', case 'set_header':
}} return t('设请求头');
> case 'delete_header':
{value} return t('删请求头');
</Text> case 'copy_header':
</div> 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 = ({ const ParamOverrideModal = ({
showParamOverrideModal, showParamOverrideModal,
@@ -124,147 +130,135 @@ const ParamOverrideModal = ({
centered centered
closable closable
maskClosable maskClosable
width={760} width={640}
> >
<div style={{ padding: 20 }}> <div style={{ padding: '8px 20px 20px' }}>
<div <div
style={{ style={{
marginBottom: 16, display: 'flex',
padding: 16, justifyContent: 'space-between',
borderRadius: 16, alignItems: 'flex-start',
background: gap: 12,
'linear-gradient(135deg, rgba(var(--semi-blue-5), 0.08), rgba(var(--semi-teal-5), 0.12))', marginBottom: 10,
border: '1px solid rgba(var(--semi-blue-5), 0.16)',
}} }}
> >
<div <div style={{ minWidth: 0 }}>
style={{ <div style={{ marginBottom: 4 }}>
display: 'flex', <Text style={{ fontWeight: 600 }}>
justifyContent: 'space-between', {t('{{count}} 项操作', { count: lines.length })}
gap: 12,
flexWrap: 'wrap',
alignItems: 'flex-start',
}}
>
<div>
<div
style={{
fontSize: 18,
fontWeight: 700,
color: 'var(--semi-color-text-0)',
marginBottom: 8,
}}
>
{t('已应用参数覆盖')}
</div>
<Space wrap spacing={8}>
<Tag color='blue' size='large'>
{t('{{count}} 项变更', { count: lines.length })}
</Tag>
{paramOverrideTarget?.modelName ? (
<Tag color='cyan' size='large'>
{paramOverrideTarget.modelName}
</Tag>
) : null}
{paramOverrideTarget?.requestId ? (
<Tag color='grey' size='large'>
{t('Request ID')}: {paramOverrideTarget.requestId}
</Tag>
) : null}
</Space>
</div>
<Button
icon={<IconCopy />}
theme='solid'
type='tertiary'
onClick={copyAll}
disabled={lines.length === 0}
>
{t('复制全部')}
</Button>
</div>
{paramOverrideTarget?.requestPath ? (
<div style={{ marginTop: 12 }}>
<Text type='tertiary' size='small'>
{t('请求路径')}: {paramOverrideTarget.requestPath}
</Text> </Text>
</div> </div>
) : null} <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> </div>
<Divider margin='12px' />
{lines.length === 0 ? ( {lines.length === 0 ? (
<Empty <Empty
description={t('暂无参数覆盖记录')} description={t('暂无参数覆盖记录')}
style={{ padding: '32px 0 12px' }} style={{ padding: '24px 0 8px' }}
/> />
) : ( ) : (
<div <div
style={{ style={{
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
gap: 12, gap: 8,
maxHeight: '60vh', maxHeight: '56vh',
overflowY: 'auto', overflowY: 'auto',
paddingRight: 4, paddingRight: 2,
}} }}
> >
{parsedLines.map((item, index) => { {parsedLines.map((item, index) => {
if (!item) { if (!item) {
return ( return null;
<div
key={`raw-${index}`}
style={{
padding: 14,
borderRadius: 14,
border: '1px solid var(--semi-color-border)',
background: 'var(--semi-color-fill-0)',
}}
>
<Text
style={{
fontFamily:
'ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas, monospace',
fontSize: 13,
lineHeight: 1.65,
whiteSpace: 'pre-wrap',
wordBreak: 'break-word',
}}
>
{lines[index]}
</Text>
</div>
);
} }
return ( return (
<div <div
key={`${item.field}-${index}`} key={`${item.action}-${index}`}
style={{ style={{
padding: 14, padding: '10px 12px',
borderRadius: 16, borderRadius: 10,
border: '1px solid var(--semi-color-border)', border: '1px solid var(--semi-color-border)',
background: 'var(--semi-color-bg-0)', background: 'var(--semi-color-fill-0)',
boxShadow: '0 8px 24px rgba(15, 23, 42, 0.04)', display: 'flex',
gap: 12,
alignItems: 'flex-start',
}} }}
> >
<div style={{ marginBottom: 12 }}>
<Tag color='blue' shape='circle' size='large'>
{item.field}
</Tag>
</div>
<div <div
style={{ style={{
display: 'flex', flex: '0 0 auto',
gap: 12, minWidth: 74,
flexWrap: 'wrap',
alignItems: 'stretch',
}} }}
> >
<ValuePanel label={t('变更前')} value={item.before} /> <Text
<ValuePanel label={t('变更后')} value={item.after} tone='after' /> 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> </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>
); );
})} })}
+9 -24
View File
@@ -19,7 +19,7 @@ For commercial licensing, please contact support@quantumnous.com
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { Modal, Button, Tag } from '@douyinfe/semi-ui'; import { Modal } from '@douyinfe/semi-ui';
import { import {
API, API,
getTodayStartTimestamp, getTodayStartTimestamp,
@@ -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();
@@ -604,30 +605,14 @@ export const useLogsData = () => {
expandDataLocal.push({ expandDataLocal.push({
key: t('参数覆盖'), key: t('参数覆盖'),
value: ( value: (
<div <ParamOverrideEntry
style={{ count={other.po.length}
display: 'flex', t={t}
alignItems: 'center', onOpen={(event) => {
gap: 10, event.stopPropagation();
flexWrap: 'wrap', openParamOverrideModal(logs[i], other);
}} }}
> />
<Tag color='blue' shape='circle'>
{t('{{count}} 项变更', { count: other.po.length })}
</Tag>
<Button
theme='borderless'
type='primary'
size='small'
style={{ paddingLeft: 0 }}
onClick={(event) => {
event.stopPropagation();
openParamOverrideModal(logs[i], other);
}}
>
{t('查看详情')}
</Button>
</div>
), ),
}); });
} }