feat: add param override audit modal for usage logs
This commit is contained in:
@@ -21,10 +21,21 @@ var negativeIndexRegexp = regexp.MustCompile(`\.(-\d+)`)
|
||||
const (
|
||||
paramOverrideContextRequestHeaders = "request_headers"
|
||||
paramOverrideContextHeaderOverride = "header_override"
|
||||
paramOverrideContextAuditRecorder = "__param_override_audit_recorder"
|
||||
)
|
||||
|
||||
var errSourceHeaderNotFound = errors.New("source header does not exist")
|
||||
|
||||
var paramOverrideKeyAuditPaths = map[string]struct{}{
|
||||
"model": {},
|
||||
"service_tier": {},
|
||||
"inference_geo": {},
|
||||
}
|
||||
|
||||
type paramOverrideAuditRecorder struct {
|
||||
lines []string
|
||||
}
|
||||
|
||||
type ConditionOperation struct {
|
||||
Path string `json:"path"` // JSON路径
|
||||
Mode string `json:"mode"` // full, prefix, suffix, contains, gt, gte, lt, lte
|
||||
@@ -118,6 +129,7 @@ func ApplyParamOverride(jsonData []byte, paramOverride map[string]interface{}, c
|
||||
if len(paramOverride) == 0 {
|
||||
return jsonData, nil
|
||||
}
|
||||
auditRecorder := getParamOverrideAuditRecorder(conditionContext)
|
||||
|
||||
// 尝试断言为操作格式
|
||||
if operations, ok := tryParseOperations(paramOverride); ok {
|
||||
@@ -125,7 +137,7 @@ func ApplyParamOverride(jsonData []byte, paramOverride map[string]interface{}, c
|
||||
workingJSON := jsonData
|
||||
var err error
|
||||
if len(legacyOverride) > 0 {
|
||||
workingJSON, err = applyOperationsLegacy(workingJSON, legacyOverride)
|
||||
workingJSON, err = applyOperationsLegacy(workingJSON, legacyOverride, auditRecorder)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -137,7 +149,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{} {
|
||||
@@ -161,14 +173,133 @@ func ApplyParamOverrideWithRelayInfo(jsonData []byte, info *RelayInfo) ([]byte,
|
||||
}
|
||||
|
||||
overrideCtx := BuildParamOverrideContext(info)
|
||||
var recorder *paramOverrideAuditRecorder
|
||||
if shouldEnableParamOverrideAudit(paramOverride) {
|
||||
recorder = ¶mOverrideAuditRecorder{}
|
||||
overrideCtx[paramOverrideContextAuditRecorder] = recorder
|
||||
}
|
||||
result, err := ApplyParamOverride(jsonData, paramOverride, overrideCtx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
syncRuntimeHeaderOverrideFromContext(info, overrideCtx)
|
||||
if info != nil {
|
||||
if recorder != nil {
|
||||
info.ParamOverrideAudit = recorder.lines
|
||||
} else {
|
||||
info.ParamOverrideAudit = 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) record(path string, beforeExists bool, beforeValue interface{}, afterExists bool, afterValue interface{}) {
|
||||
if r == nil {
|
||||
return
|
||||
}
|
||||
path = strings.TrimSpace(path)
|
||||
if path == "" {
|
||||
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) {
|
||||
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 applyAuditedPathMutation(result, path string, auditRecorder *paramOverrideAuditRecorder, mutate func(string) (string, error)) (string, error) {
|
||||
needAudit := auditRecorder != nil && shouldAuditParamPath(path)
|
||||
var beforeResult gjson.Result
|
||||
if needAudit {
|
||||
beforeResult = gjson.Get(result, path)
|
||||
}
|
||||
|
||||
next, err := mutate(result)
|
||||
if err != nil {
|
||||
return next, err
|
||||
}
|
||||
|
||||
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 {
|
||||
switch typed := value.(type) {
|
||||
case nil:
|
||||
return "<empty>"
|
||||
case string:
|
||||
return typed
|
||||
default:
|
||||
return common.GetJsonString(typed)
|
||||
}
|
||||
}
|
||||
|
||||
func getParamOverrideMap(info *RelayInfo) map[string]interface{} {
|
||||
if info == nil || info.ChannelMeta == nil {
|
||||
return nil
|
||||
@@ -455,7 +586,7 @@ func compareNumeric(jsonValue, targetValue gjson.Result, operator string) (bool,
|
||||
}
|
||||
|
||||
// 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{})
|
||||
err := common.Unmarshal(jsonData, &reqMap)
|
||||
if err != nil {
|
||||
@@ -463,7 +594,9 @@ func applyOperationsLegacy(jsonData []byte, paramOverride map[string]interface{}
|
||||
}
|
||||
|
||||
for key, value := range paramOverride {
|
||||
beforeValue, beforeExists := reqMap[key]
|
||||
reqMap[key] = value
|
||||
auditRecorder.record(key, beforeExists, beforeValue, true, value)
|
||||
}
|
||||
|
||||
return common.Marshal(reqMap)
|
||||
@@ -471,6 +604,7 @@ func applyOperationsLegacy(jsonData []byte, paramOverride map[string]interface{}
|
||||
|
||||
func applyOperations(jsonStr string, operations []ParamOperation, conditionContext map[string]interface{}) (string, error) {
|
||||
context := ensureContextMap(conditionContext)
|
||||
auditRecorder := getParamOverrideAuditRecorder(context)
|
||||
contextJSON, err := marshalContextJSON(context)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to marshal condition context: %v", err)
|
||||
@@ -502,7 +636,9 @@ func applyOperations(jsonStr string, operations []ParamOperation, conditionConte
|
||||
switch op.Mode {
|
||||
case "delete":
|
||||
for _, path := range opPaths {
|
||||
result, err = deleteValue(result, path)
|
||||
result, err = applyAuditedPathMutation(result, path, auditRecorder, func(current string) (string, error) {
|
||||
return deleteValue(current, path)
|
||||
})
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
@@ -512,7 +648,9 @@ func applyOperations(jsonStr string, operations []ParamOperation, conditionConte
|
||||
if op.KeepOrigin && gjson.Get(result, path).Exists() {
|
||||
continue
|
||||
}
|
||||
result, err = sjson.Set(result, path, op.Value)
|
||||
result, err = applyAuditedPathMutation(result, path, auditRecorder, func(current string) (string, error) {
|
||||
return sjson.Set(current, path, op.Value)
|
||||
})
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
@@ -520,87 +658,137 @@ func applyOperations(jsonStr string, operations []ParamOperation, conditionConte
|
||||
case "move":
|
||||
opFrom := processNegativeIndex(result, op.From)
|
||||
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)
|
||||
if err == nil {
|
||||
if needAuditTo {
|
||||
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":
|
||||
if op.From == "" || op.To == "" {
|
||||
return "", fmt.Errorf("copy from/to is required")
|
||||
}
|
||||
opFrom := processNegativeIndex(result, op.From)
|
||||
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)
|
||||
if err == nil && needAudit {
|
||||
afterResult := gjson.Get(result, opTo)
|
||||
auditRecorder.record(opTo, beforeResult.Exists(), beforeResult.Value(), afterResult.Exists(), afterResult.Value())
|
||||
}
|
||||
case "prepend":
|
||||
for _, path := range opPaths {
|
||||
result, err = modifyValue(result, path, op.Value, op.KeepOrigin, true)
|
||||
result, err = applyAuditedPathMutation(result, path, auditRecorder, func(current string) (string, error) {
|
||||
return modifyValue(current, path, op.Value, op.KeepOrigin, true)
|
||||
})
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
case "append":
|
||||
for _, path := range opPaths {
|
||||
result, err = modifyValue(result, path, op.Value, op.KeepOrigin, false)
|
||||
result, err = applyAuditedPathMutation(result, path, auditRecorder, func(current string) (string, error) {
|
||||
return modifyValue(current, path, op.Value, op.KeepOrigin, false)
|
||||
})
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
case "trim_prefix":
|
||||
for _, path := range opPaths {
|
||||
result, err = trimStringValue(result, path, op.Value, true)
|
||||
result, err = applyAuditedPathMutation(result, path, auditRecorder, func(current string) (string, error) {
|
||||
return trimStringValue(current, path, op.Value, true)
|
||||
})
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
case "trim_suffix":
|
||||
for _, path := range opPaths {
|
||||
result, err = trimStringValue(result, path, op.Value, false)
|
||||
result, err = applyAuditedPathMutation(result, path, auditRecorder, func(current string) (string, error) {
|
||||
return trimStringValue(current, path, op.Value, false)
|
||||
})
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
case "ensure_prefix":
|
||||
for _, path := range opPaths {
|
||||
result, err = ensureStringAffix(result, path, op.Value, true)
|
||||
result, err = applyAuditedPathMutation(result, path, auditRecorder, func(current string) (string, error) {
|
||||
return ensureStringAffix(current, path, op.Value, true)
|
||||
})
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
case "ensure_suffix":
|
||||
for _, path := range opPaths {
|
||||
result, err = ensureStringAffix(result, path, op.Value, false)
|
||||
result, err = applyAuditedPathMutation(result, path, auditRecorder, func(current string) (string, error) {
|
||||
return ensureStringAffix(current, path, op.Value, false)
|
||||
})
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
case "trim_space":
|
||||
for _, path := range opPaths {
|
||||
result, err = transformStringValue(result, path, strings.TrimSpace)
|
||||
result, err = applyAuditedPathMutation(result, path, auditRecorder, func(current string) (string, error) {
|
||||
return transformStringValue(current, path, strings.TrimSpace)
|
||||
})
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
case "to_lower":
|
||||
for _, path := range opPaths {
|
||||
result, err = transformStringValue(result, path, strings.ToLower)
|
||||
result, err = applyAuditedPathMutation(result, path, auditRecorder, func(current string) (string, error) {
|
||||
return transformStringValue(current, path, strings.ToLower)
|
||||
})
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
case "to_upper":
|
||||
for _, path := range opPaths {
|
||||
result, err = transformStringValue(result, path, strings.ToUpper)
|
||||
result, err = applyAuditedPathMutation(result, path, auditRecorder, func(current string) (string, error) {
|
||||
return transformStringValue(current, path, strings.ToUpper)
|
||||
})
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
case "replace":
|
||||
for _, path := range opPaths {
|
||||
result, err = replaceStringValue(result, path, op.From, op.To)
|
||||
result, err = applyAuditedPathMutation(result, path, auditRecorder, func(current string) (string, error) {
|
||||
return replaceStringValue(current, path, op.From, op.To)
|
||||
})
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
case "regex_replace":
|
||||
for _, path := range opPaths {
|
||||
result, err = regexReplaceStringValue(result, path, op.From, op.To)
|
||||
result, err = applyAuditedPathMutation(result, path, auditRecorder, func(current string) (string, error) {
|
||||
return regexReplaceStringValue(current, path, op.From, op.To)
|
||||
})
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
@@ -797,6 +985,7 @@ func setHeaderOverrideInContext(context map[string]interface{}, headerName strin
|
||||
}
|
||||
|
||||
rawHeaders := ensureMapKeyInContext(context, paramOverrideContextHeaderOverride)
|
||||
beforeRaw, beforeExists := rawHeaders[headerName]
|
||||
if keepOrigin {
|
||||
if existing, ok := rawHeaders[headerName]; ok {
|
||||
existingValue := strings.TrimSpace(fmt.Sprintf("%v", existing))
|
||||
@@ -812,10 +1001,12 @@ func setHeaderOverrideInContext(context map[string]interface{}, headerName strin
|
||||
}
|
||||
if !hasValue {
|
||||
delete(rawHeaders, headerName)
|
||||
getParamOverrideAuditRecorder(context).record("header."+headerName, beforeExists, beforeRaw, false, nil)
|
||||
return nil
|
||||
}
|
||||
|
||||
rawHeaders[headerName] = headerValue
|
||||
getParamOverrideAuditRecorder(context).record("header."+headerName, beforeExists, beforeRaw, true, headerValue)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -987,7 +1178,9 @@ func deleteHeaderOverrideInContext(context map[string]interface{}, headerName st
|
||||
return fmt.Errorf("header name is required")
|
||||
}
|
||||
rawHeaders := ensureMapKeyInContext(context, paramOverrideContextHeaderOverride)
|
||||
beforeRaw, beforeExists := rawHeaders[headerName]
|
||||
delete(rawHeaders, headerName)
|
||||
getParamOverrideAuditRecorder(context).record("header."+headerName, beforeExists, beforeRaw, false, nil)
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -149,6 +149,7 @@ type RelayInfo struct {
|
||||
LastError *types.NewAPIError
|
||||
RuntimeHeadersOverride map[string]interface{}
|
||||
UseRuntimeHeadersOverride bool
|
||||
ParamOverrideAudit []string
|
||||
|
||||
PriceData types.PriceData
|
||||
|
||||
|
||||
@@ -74,9 +74,17 @@ func GenerateTextOtherInfo(ctx *gin.Context, relayInfo *relaycommon.RelayInfo, m
|
||||
appendRequestPath(ctx, relayInfo, other)
|
||||
appendRequestConversionChain(relayInfo, other)
|
||||
appendBillingInfo(relayInfo, other)
|
||||
appendParamOverrideInfo(relayInfo, 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{}) {
|
||||
if relayInfo == nil || other == nil {
|
||||
return
|
||||
|
||||
@@ -25,6 +25,7 @@ import LogsFilters from './UsageLogsFilters';
|
||||
import ColumnSelectorModal from './modals/ColumnSelectorModal';
|
||||
import UserInfoModal from './modals/UserInfoModal';
|
||||
import ChannelAffinityUsageCacheModal from './modals/ChannelAffinityUsageCacheModal';
|
||||
import ParamOverrideModal from './modals/ParamOverrideModal';
|
||||
import { useLogsData } from '../../../hooks/usage-logs/useUsageLogsData';
|
||||
import { useIsMobile } from '../../../hooks/common/useIsMobile';
|
||||
import { createCardProPagination } from '../../../helpers/utils';
|
||||
@@ -39,6 +40,7 @@ const LogsPage = () => {
|
||||
<ColumnSelectorModal {...logsData} />
|
||||
<UserInfoModal {...logsData} />
|
||||
<ChannelAffinityUsageCacheModal {...logsData} />
|
||||
<ParamOverrideModal {...logsData} />
|
||||
|
||||
{/* Main Content */}
|
||||
<CardPro
|
||||
|
||||
@@ -0,0 +1,278 @@
|
||||
/*
|
||||
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,
|
||||
Space,
|
||||
Tag,
|
||||
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 colonIndex = line.indexOf(': ');
|
||||
const arrowIndex = line.indexOf(' -> ', colonIndex + 2);
|
||||
if (colonIndex <= 0 || arrowIndex <= colonIndex) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
field: line.slice(0, colonIndex),
|
||||
before: line.slice(colonIndex + 2, arrowIndex),
|
||||
after: line.slice(arrowIndex + 4),
|
||||
raw: line,
|
||||
};
|
||||
};
|
||||
|
||||
const ValuePanel = ({ label, value, tone }) => (
|
||||
<div
|
||||
style={{
|
||||
flex: 1,
|
||||
minWidth: 0,
|
||||
padding: 12,
|
||||
borderRadius: 12,
|
||||
border: '1px solid var(--semi-color-border)',
|
||||
background:
|
||||
tone === 'after'
|
||||
? 'rgba(var(--semi-blue-5), 0.08)'
|
||||
: 'var(--semi-color-fill-0)',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
marginBottom: 6,
|
||||
fontSize: 12,
|
||||
fontWeight: 600,
|
||||
color: 'var(--semi-color-text-2)',
|
||||
}}
|
||||
>
|
||||
{label}
|
||||
</div>
|
||||
<Text
|
||||
style={{
|
||||
display: 'block',
|
||||
fontFamily:
|
||||
'ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas, monospace',
|
||||
fontSize: 13,
|
||||
lineHeight: 1.65,
|
||||
whiteSpace: 'pre-wrap',
|
||||
wordBreak: 'break-word',
|
||||
}}
|
||||
>
|
||||
{value}
|
||||
</Text>
|
||||
</div>
|
||||
);
|
||||
|
||||
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={760}
|
||||
>
|
||||
<div style={{ padding: 20 }}>
|
||||
<div
|
||||
style={{
|
||||
marginBottom: 16,
|
||||
padding: 16,
|
||||
borderRadius: 16,
|
||||
background:
|
||||
'linear-gradient(135deg, rgba(var(--semi-blue-5), 0.08), rgba(var(--semi-teal-5), 0.12))',
|
||||
border: '1px solid rgba(var(--semi-blue-5), 0.16)',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
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>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
{lines.length === 0 ? (
|
||||
<Empty
|
||||
description={t('暂无参数覆盖记录')}
|
||||
style={{ padding: '32px 0 12px' }}
|
||||
/>
|
||||
) : (
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 12,
|
||||
maxHeight: '60vh',
|
||||
overflowY: 'auto',
|
||||
paddingRight: 4,
|
||||
}}
|
||||
>
|
||||
{parsedLines.map((item, index) => {
|
||||
if (!item) {
|
||||
return (
|
||||
<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 (
|
||||
<div
|
||||
key={`${item.field}-${index}`}
|
||||
style={{
|
||||
padding: 14,
|
||||
borderRadius: 16,
|
||||
border: '1px solid var(--semi-color-border)',
|
||||
background: 'var(--semi-color-bg-0)',
|
||||
boxShadow: '0 8px 24px rgba(15, 23, 42, 0.04)',
|
||||
}}
|
||||
>
|
||||
<div style={{ marginBottom: 12 }}>
|
||||
<Tag color='blue' shape='circle' size='large'>
|
||||
{item.field}
|
||||
</Tag>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
gap: 12,
|
||||
flexWrap: 'wrap',
|
||||
alignItems: 'stretch',
|
||||
}}
|
||||
>
|
||||
<ValuePanel label={t('变更前')} value={item.before} />
|
||||
<ValuePanel label={t('变更后')} value={item.after} tone='after' />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default ParamOverrideModal;
|
||||
53
web/src/hooks/usage-logs/useUsageLogsData.jsx
vendored
53
web/src/hooks/usage-logs/useUsageLogsData.jsx
vendored
@@ -19,7 +19,7 @@ For commercial licensing, please contact support@quantumnous.com
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Modal } from '@douyinfe/semi-ui';
|
||||
import { Modal, Button, Tag } from '@douyinfe/semi-ui';
|
||||
import {
|
||||
API,
|
||||
getTodayStartTimestamp,
|
||||
@@ -181,6 +181,8 @@ export const useLogsData = () => {
|
||||
] = useState(false);
|
||||
const [channelAffinityUsageCacheTarget, setChannelAffinityUsageCacheTarget] =
|
||||
useState(null);
|
||||
const [showParamOverrideModal, setShowParamOverrideModal] = useState(false);
|
||||
const [paramOverrideTarget, setParamOverrideTarget] = useState(null);
|
||||
|
||||
// Initialize default column visibility
|
||||
const initDefaultColumns = () => {
|
||||
@@ -345,6 +347,20 @@ export const useLogsData = () => {
|
||||
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
|
||||
const setLogsFormat = (logs) => {
|
||||
const requestConversionDisplayValue = (conversionChain) => {
|
||||
@@ -584,6 +600,37 @@ export const useLogsData = () => {
|
||||
value: other.request_path,
|
||||
});
|
||||
}
|
||||
if (Array.isArray(other?.po) && other.po.length > 0) {
|
||||
expandDataLocal.push({
|
||||
key: t('参数覆盖'),
|
||||
value: (
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 10,
|
||||
flexWrap: 'wrap',
|
||||
}}
|
||||
>
|
||||
<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>
|
||||
),
|
||||
});
|
||||
}
|
||||
if (other?.billing_source === 'subscription') {
|
||||
const planId = other?.subscription_plan_id;
|
||||
const planTitle = other?.subscription_plan_title || '';
|
||||
@@ -811,6 +858,9 @@ export const useLogsData = () => {
|
||||
setShowChannelAffinityUsageCacheModal,
|
||||
channelAffinityUsageCacheTarget,
|
||||
openChannelAffinityUsageCacheModal,
|
||||
showParamOverrideModal,
|
||||
setShowParamOverrideModal,
|
||||
paramOverrideTarget,
|
||||
|
||||
// Functions
|
||||
loadLogs,
|
||||
@@ -822,6 +872,7 @@ export const useLogsData = () => {
|
||||
setLogsFormat,
|
||||
hasExpandableRows,
|
||||
setLogType,
|
||||
openParamOverrideModal,
|
||||
|
||||
// Translation
|
||||
t,
|
||||
|
||||
Reference in New Issue
Block a user