mirror of
https://github.com/QuantumNous/new-api.git
synced 2026-03-30 00:46:42 +00:00
feat: add wildcard path support and improve param override templates/editor
This commit is contained in:
@@ -5,6 +5,7 @@ import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
@@ -487,15 +488,35 @@ func applyOperations(jsonStr string, operations []ParamOperation, conditionConte
|
||||
}
|
||||
// 处理路径中的负数索引
|
||||
opPath := processNegativeIndex(result, op.Path)
|
||||
var opPaths []string
|
||||
if isPathBasedOperation(op.Mode) {
|
||||
opPaths, err = resolveOperationPaths(result, opPath)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if len(opPaths) == 0 {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
switch op.Mode {
|
||||
case "delete":
|
||||
result, err = sjson.Delete(result, opPath)
|
||||
case "set":
|
||||
if op.KeepOrigin && gjson.Get(result, opPath).Exists() {
|
||||
continue
|
||||
for _, path := range opPaths {
|
||||
result, err = deleteValue(result, path)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
case "set":
|
||||
for _, path := range opPaths {
|
||||
if op.KeepOrigin && gjson.Get(result, path).Exists() {
|
||||
continue
|
||||
}
|
||||
result, err = sjson.Set(result, path, op.Value)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
result, err = sjson.Set(result, opPath, op.Value)
|
||||
case "move":
|
||||
opFrom := processNegativeIndex(result, op.From)
|
||||
opTo := processNegativeIndex(result, op.To)
|
||||
@@ -508,27 +529,82 @@ func applyOperations(jsonStr string, operations []ParamOperation, conditionConte
|
||||
opTo := processNegativeIndex(result, op.To)
|
||||
result, err = copyValue(result, opFrom, opTo)
|
||||
case "prepend":
|
||||
result, err = modifyValue(result, opPath, op.Value, op.KeepOrigin, true)
|
||||
for _, path := range opPaths {
|
||||
result, err = modifyValue(result, path, op.Value, op.KeepOrigin, true)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
case "append":
|
||||
result, err = modifyValue(result, opPath, op.Value, op.KeepOrigin, false)
|
||||
for _, path := range opPaths {
|
||||
result, err = modifyValue(result, path, op.Value, op.KeepOrigin, false)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
case "trim_prefix":
|
||||
result, err = trimStringValue(result, opPath, op.Value, true)
|
||||
for _, path := range opPaths {
|
||||
result, err = trimStringValue(result, path, op.Value, true)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
case "trim_suffix":
|
||||
result, err = trimStringValue(result, opPath, op.Value, false)
|
||||
for _, path := range opPaths {
|
||||
result, err = trimStringValue(result, path, op.Value, false)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
case "ensure_prefix":
|
||||
result, err = ensureStringAffix(result, opPath, op.Value, true)
|
||||
for _, path := range opPaths {
|
||||
result, err = ensureStringAffix(result, path, op.Value, true)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
case "ensure_suffix":
|
||||
result, err = ensureStringAffix(result, opPath, op.Value, false)
|
||||
for _, path := range opPaths {
|
||||
result, err = ensureStringAffix(result, path, op.Value, false)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
case "trim_space":
|
||||
result, err = transformStringValue(result, opPath, strings.TrimSpace)
|
||||
for _, path := range opPaths {
|
||||
result, err = transformStringValue(result, path, strings.TrimSpace)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
case "to_lower":
|
||||
result, err = transformStringValue(result, opPath, strings.ToLower)
|
||||
for _, path := range opPaths {
|
||||
result, err = transformStringValue(result, path, strings.ToLower)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
case "to_upper":
|
||||
result, err = transformStringValue(result, opPath, strings.ToUpper)
|
||||
for _, path := range opPaths {
|
||||
result, err = transformStringValue(result, path, strings.ToUpper)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
case "replace":
|
||||
result, err = replaceStringValue(result, opPath, op.From, op.To)
|
||||
for _, path := range opPaths {
|
||||
result, err = replaceStringValue(result, path, op.From, op.To)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
case "regex_replace":
|
||||
result, err = regexReplaceStringValue(result, opPath, op.From, op.To)
|
||||
for _, path := range opPaths {
|
||||
result, err = regexReplaceStringValue(result, path, op.From, op.To)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
case "return_error":
|
||||
returnErr, parseErr := parseParamOverrideReturnError(op.Value)
|
||||
if parseErr != nil {
|
||||
@@ -536,7 +612,12 @@ func applyOperations(jsonStr string, operations []ParamOperation, conditionConte
|
||||
}
|
||||
return "", returnErr
|
||||
case "prune_objects":
|
||||
result, err = pruneObjects(result, opPath, contextJSON, op.Value)
|
||||
for _, path := range opPaths {
|
||||
result, err = pruneObjects(result, path, contextJSON, op.Value)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
case "set_header":
|
||||
err = setHeaderOverrideInContext(context, op.Path, op.Value, op.KeepOrigin)
|
||||
if err == nil {
|
||||
@@ -1174,6 +1255,92 @@ func copyValue(jsonStr, fromPath, toPath string) (string, error) {
|
||||
return sjson.Set(jsonStr, toPath, sourceValue.Value())
|
||||
}
|
||||
|
||||
func isPathBasedOperation(mode string) bool {
|
||||
switch mode {
|
||||
case "delete", "set", "prepend", "append", "trim_prefix", "trim_suffix", "ensure_prefix", "ensure_suffix", "trim_space", "to_lower", "to_upper", "replace", "regex_replace", "prune_objects":
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func resolveOperationPaths(jsonStr, path string) ([]string, error) {
|
||||
if !strings.Contains(path, "*") {
|
||||
return []string{path}, nil
|
||||
}
|
||||
return expandWildcardPaths(jsonStr, path)
|
||||
}
|
||||
|
||||
func expandWildcardPaths(jsonStr, path string) ([]string, error) {
|
||||
var root interface{}
|
||||
if err := common.Unmarshal([]byte(jsonStr), &root); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
segments := strings.Split(path, ".")
|
||||
paths := collectWildcardPaths(root, segments, nil)
|
||||
return lo.Uniq(paths), nil
|
||||
}
|
||||
|
||||
func collectWildcardPaths(node interface{}, segments []string, prefix []string) []string {
|
||||
if len(segments) == 0 {
|
||||
return []string{strings.Join(prefix, ".")}
|
||||
}
|
||||
|
||||
segment := strings.TrimSpace(segments[0])
|
||||
if segment == "" {
|
||||
return nil
|
||||
}
|
||||
isLast := len(segments) == 1
|
||||
|
||||
if segment == "*" {
|
||||
switch typed := node.(type) {
|
||||
case map[string]interface{}:
|
||||
keys := lo.Keys(typed)
|
||||
sort.Strings(keys)
|
||||
return lo.FlatMap(keys, func(key string, _ int) []string {
|
||||
return collectWildcardPaths(typed[key], segments[1:], append(prefix, key))
|
||||
})
|
||||
case []interface{}:
|
||||
return lo.FlatMap(lo.Range(len(typed)), func(index int, _ int) []string {
|
||||
return collectWildcardPaths(typed[index], segments[1:], append(prefix, strconv.Itoa(index)))
|
||||
})
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
switch typed := node.(type) {
|
||||
case map[string]interface{}:
|
||||
if isLast {
|
||||
return []string{strings.Join(append(prefix, segment), ".")}
|
||||
}
|
||||
next, exists := typed[segment]
|
||||
if !exists {
|
||||
return nil
|
||||
}
|
||||
return collectWildcardPaths(next, segments[1:], append(prefix, segment))
|
||||
case []interface{}:
|
||||
index, err := strconv.Atoi(segment)
|
||||
if err != nil || index < 0 || index >= len(typed) {
|
||||
return nil
|
||||
}
|
||||
if isLast {
|
||||
return []string{strings.Join(append(prefix, segment), ".")}
|
||||
}
|
||||
return collectWildcardPaths(typed[index], segments[1:], append(prefix, segment))
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func deleteValue(jsonStr, path string) (string, error) {
|
||||
if strings.TrimSpace(path) == "" {
|
||||
return jsonStr, nil
|
||||
}
|
||||
return sjson.Delete(jsonStr, path)
|
||||
}
|
||||
|
||||
func modifyValue(jsonStr, path string, value interface{}, keepOrigin, isPrepend bool) (string, error) {
|
||||
current := gjson.Get(jsonStr, path)
|
||||
switch {
|
||||
|
||||
@@ -2,6 +2,7 @@ package common
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
@@ -9,6 +10,7 @@ import (
|
||||
|
||||
"github.com/QuantumNous/new-api/dto"
|
||||
"github.com/QuantumNous/new-api/setting/model_setting"
|
||||
"github.com/samber/lo"
|
||||
)
|
||||
|
||||
func TestApplyParamOverrideTrimPrefix(t *testing.T) {
|
||||
@@ -242,6 +244,224 @@ func TestApplyParamOverrideDelete(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplyParamOverrideDeleteWildcardPath(t *testing.T) {
|
||||
input := []byte(`{"tools":[{"type":"bash","custom":{"input_examples":["a"],"other":1}},{"type":"code","custom":{"input_examples":["b"]}},{"type":"noop","custom":{"other":2}}]}`)
|
||||
override := map[string]interface{}{
|
||||
"operations": []interface{}{
|
||||
map[string]interface{}{
|
||||
"path": "tools.*.custom.input_examples",
|
||||
"mode": "delete",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
out, err := ApplyParamOverride(input, override, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("ApplyParamOverride returned error: %v", err)
|
||||
}
|
||||
assertJSONEqual(t, `{"tools":[{"type":"bash","custom":{"other":1}},{"type":"code","custom":{}},{"type":"noop","custom":{"other":2}}]}`, string(out))
|
||||
}
|
||||
|
||||
func TestApplyParamOverrideSetWildcardPath(t *testing.T) {
|
||||
input := []byte(`{"tools":[{"custom":{"tag":"A"}},{"custom":{"tag":"B"}},{"custom":{"tag":"C"}}]}`)
|
||||
override := map[string]interface{}{
|
||||
"operations": []interface{}{
|
||||
map[string]interface{}{
|
||||
"path": "tools.*.custom.enabled",
|
||||
"mode": "set",
|
||||
"value": true,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
out, err := ApplyParamOverride(input, override, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("ApplyParamOverride returned error: %v", err)
|
||||
}
|
||||
|
||||
var got struct {
|
||||
Tools []struct {
|
||||
Custom struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
} `json:"custom"`
|
||||
} `json:"tools"`
|
||||
}
|
||||
if err := json.Unmarshal(out, &got); err != nil {
|
||||
t.Fatalf("failed to unmarshal output JSON: %v", err)
|
||||
}
|
||||
|
||||
if !lo.EveryBy(got.Tools, func(item struct {
|
||||
Custom struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
} `json:"custom"`
|
||||
}) bool {
|
||||
return item.Custom.Enabled
|
||||
}) {
|
||||
t.Fatalf("expected wildcard set to enable all tools, got: %s", string(out))
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplyParamOverrideTrimSpaceWildcardPath(t *testing.T) {
|
||||
input := []byte(`{"tools":[{"custom":{"name":" alpha "}},{"custom":{"name":" beta"}},{"custom":{"name":"gamma "}}]}`)
|
||||
override := map[string]interface{}{
|
||||
"operations": []interface{}{
|
||||
map[string]interface{}{
|
||||
"path": "tools.*.custom.name",
|
||||
"mode": "trim_space",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
out, err := ApplyParamOverride(input, override, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("ApplyParamOverride returned error: %v", err)
|
||||
}
|
||||
|
||||
var got struct {
|
||||
Tools []struct {
|
||||
Custom struct {
|
||||
Name string `json:"name"`
|
||||
} `json:"custom"`
|
||||
} `json:"tools"`
|
||||
}
|
||||
if err := json.Unmarshal(out, &got); err != nil {
|
||||
t.Fatalf("failed to unmarshal output JSON: %v", err)
|
||||
}
|
||||
|
||||
names := lo.Map(got.Tools, func(item struct {
|
||||
Custom struct {
|
||||
Name string `json:"name"`
|
||||
} `json:"custom"`
|
||||
}, _ int) string {
|
||||
return item.Custom.Name
|
||||
})
|
||||
if !reflect.DeepEqual(names, []string{"alpha", "beta", "gamma"}) {
|
||||
t.Fatalf("unexpected names after wildcard trim_space: %v", names)
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplyParamOverrideDeleteWildcardEqualsIndexedPaths(t *testing.T) {
|
||||
input := []byte(`{"tools":[{"custom":{"input_examples":["a"],"other":1}},{"custom":{"input_examples":["b"],"other":2}},{"custom":{"input_examples":["c"],"other":3}}]}`)
|
||||
|
||||
wildcardOverride := map[string]interface{}{
|
||||
"operations": []interface{}{
|
||||
map[string]interface{}{
|
||||
"path": "tools.*.custom.input_examples",
|
||||
"mode": "delete",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
indexedOverride := map[string]interface{}{
|
||||
"operations": lo.Map(lo.Range(3), func(index int, _ int) interface{} {
|
||||
return map[string]interface{}{
|
||||
"path": fmt.Sprintf("tools.%d.custom.input_examples", index),
|
||||
"mode": "delete",
|
||||
}
|
||||
}),
|
||||
}
|
||||
|
||||
wildcardOut, err := ApplyParamOverride(input, wildcardOverride, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("wildcard ApplyParamOverride returned error: %v", err)
|
||||
}
|
||||
|
||||
indexedOut, err := ApplyParamOverride(input, indexedOverride, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("indexed ApplyParamOverride returned error: %v", err)
|
||||
}
|
||||
|
||||
assertJSONEqual(t, string(indexedOut), string(wildcardOut))
|
||||
}
|
||||
|
||||
func TestApplyParamOverrideSetWildcardKeepOrigin(t *testing.T) {
|
||||
input := []byte(`{"tools":[{"custom":{"tag":"A"}},{"custom":{"tag":"B","enabled":false}},{"custom":{"tag":"C"}}]}`)
|
||||
override := map[string]interface{}{
|
||||
"operations": []interface{}{
|
||||
map[string]interface{}{
|
||||
"path": "tools.*.custom.enabled",
|
||||
"mode": "set",
|
||||
"value": true,
|
||||
"keep_origin": true,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
out, err := ApplyParamOverride(input, override, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("ApplyParamOverride returned error: %v", err)
|
||||
}
|
||||
|
||||
var got struct {
|
||||
Tools []struct {
|
||||
Custom struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
} `json:"custom"`
|
||||
} `json:"tools"`
|
||||
}
|
||||
if err := json.Unmarshal(out, &got); err != nil {
|
||||
t.Fatalf("failed to unmarshal output JSON: %v", err)
|
||||
}
|
||||
|
||||
enabledValues := lo.Map(got.Tools, func(item struct {
|
||||
Custom struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
} `json:"custom"`
|
||||
}, _ int) bool {
|
||||
return item.Custom.Enabled
|
||||
})
|
||||
if !reflect.DeepEqual(enabledValues, []bool{true, false, true}) {
|
||||
t.Fatalf("unexpected enabled values after wildcard keep_origin set: %v", enabledValues)
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplyParamOverrideTrimSpaceMultiWildcardPath(t *testing.T) {
|
||||
input := []byte(`{"tools":[{"custom":{"items":[{"name":" alpha "},{"name":" beta "}]}},{"custom":{"items":[{"name":" gamma"}]}}]}`)
|
||||
override := map[string]interface{}{
|
||||
"operations": []interface{}{
|
||||
map[string]interface{}{
|
||||
"path": "tools.*.custom.items.*.name",
|
||||
"mode": "trim_space",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
out, err := ApplyParamOverride(input, override, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("ApplyParamOverride returned error: %v", err)
|
||||
}
|
||||
|
||||
var got struct {
|
||||
Tools []struct {
|
||||
Custom struct {
|
||||
Items []struct {
|
||||
Name string `json:"name"`
|
||||
} `json:"items"`
|
||||
} `json:"custom"`
|
||||
} `json:"tools"`
|
||||
}
|
||||
if err := json.Unmarshal(out, &got); err != nil {
|
||||
t.Fatalf("failed to unmarshal output JSON: %v", err)
|
||||
}
|
||||
|
||||
names := lo.FlatMap(got.Tools, func(tool struct {
|
||||
Custom struct {
|
||||
Items []struct {
|
||||
Name string `json:"name"`
|
||||
} `json:"items"`
|
||||
} `json:"custom"`
|
||||
}, _ int) []string {
|
||||
return lo.Map(tool.Custom.Items, func(item struct {
|
||||
Name string `json:"name"`
|
||||
}, _ int) string {
|
||||
return item.Name
|
||||
})
|
||||
})
|
||||
if !reflect.DeepEqual(names, []string{"alpha", "beta", "gamma"}) {
|
||||
t.Fatalf("unexpected names after multi wildcard trim_space: %v", names)
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplyParamOverrideSet(t *testing.T) {
|
||||
input := []byte(`{"model":"gpt-4","temperature":0.7}`)
|
||||
override := map[string]interface{}{
|
||||
@@ -261,6 +481,42 @@ func TestApplyParamOverrideSet(t *testing.T) {
|
||||
assertJSONEqual(t, `{"model":"gpt-4","temperature":0.1}`, string(out))
|
||||
}
|
||||
|
||||
func TestApplyParamOverrideSetWithDescriptionKeepsCompatibility(t *testing.T) {
|
||||
input := []byte(`{"model":"gpt-4","temperature":0.7}`)
|
||||
overrideWithoutDesc := map[string]interface{}{
|
||||
"operations": []interface{}{
|
||||
map[string]interface{}{
|
||||
"path": "temperature",
|
||||
"mode": "set",
|
||||
"value": 0.1,
|
||||
},
|
||||
},
|
||||
}
|
||||
overrideWithDesc := map[string]interface{}{
|
||||
"operations": []interface{}{
|
||||
map[string]interface{}{
|
||||
"description": "set temperature for deterministic output",
|
||||
"path": "temperature",
|
||||
"mode": "set",
|
||||
"value": 0.1,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
outWithoutDesc, err := ApplyParamOverride(input, overrideWithoutDesc, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("ApplyParamOverride without description returned error: %v", err)
|
||||
}
|
||||
|
||||
outWithDesc, err := ApplyParamOverride(input, overrideWithDesc, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("ApplyParamOverride with description returned error: %v", err)
|
||||
}
|
||||
|
||||
assertJSONEqual(t, string(outWithoutDesc), string(outWithDesc))
|
||||
assertJSONEqual(t, `{"model":"gpt-4","temperature":0.1}`, string(outWithDesc))
|
||||
}
|
||||
|
||||
func TestApplyParamOverrideSetKeepOrigin(t *testing.T) {
|
||||
input := []byte(`{"model":"gpt-4","temperature":0.7}`)
|
||||
override := map[string]interface{}{
|
||||
|
||||
Reference in New Issue
Block a user