feat(runtime): enhance payload rule resolution with dynamic path support

- Introduced `resolvePayloadRulePaths` function to dynamically resolve rule paths supporting array queries and complex logic.
- Updated payload processing logic (`apply defaults`, `overrides`, `filters`) to handle resolved paths for better flexibility.
- Added helper functions for path parsing, query matching, and logical resolution to improve modularity and reusability.
This commit is contained in:
Luis Pater
2026-05-17 16:42:35 +08:00
parent be841b88ee
commit 26d13af28f

View File

@@ -2,6 +2,7 @@ package helps
import (
"encoding/json"
"strconv"
"strings"
"github.com/router-for-me/CLIProxyAPI/v7/internal/config"
@@ -55,18 +56,20 @@ func ApplyPayloadConfigWithRoot(cfg *config.Config, model, protocol, root string
if fullPath == "" {
continue
}
if gjson.GetBytes(source, fullPath).Exists() {
continue
for _, resolvedPath := range resolvePayloadRulePaths(out, fullPath) {
if gjson.GetBytes(source, resolvedPath).Exists() {
continue
}
if _, ok := appliedDefaults[resolvedPath]; ok {
continue
}
updated, errSet := sjson.SetBytes(out, resolvedPath, value)
if errSet != nil {
continue
}
out = updated
appliedDefaults[resolvedPath] = struct{}{}
}
if _, ok := appliedDefaults[fullPath]; ok {
continue
}
updated, errSet := sjson.SetBytes(out, fullPath, value)
if errSet != nil {
continue
}
out = updated
appliedDefaults[fullPath] = struct{}{}
}
}
// Apply default raw rules: first write wins per field across all matching rules.
@@ -80,22 +83,24 @@ func ApplyPayloadConfigWithRoot(cfg *config.Config, model, protocol, root string
if fullPath == "" {
continue
}
if gjson.GetBytes(source, fullPath).Exists() {
continue
for _, resolvedPath := range resolvePayloadRulePaths(out, fullPath) {
if gjson.GetBytes(source, resolvedPath).Exists() {
continue
}
if _, ok := appliedDefaults[resolvedPath]; ok {
continue
}
rawValue, ok := payloadRawValue(value)
if !ok {
continue
}
updated, errSet := sjson.SetRawBytes(out, resolvedPath, rawValue)
if errSet != nil {
continue
}
out = updated
appliedDefaults[resolvedPath] = struct{}{}
}
if _, ok := appliedDefaults[fullPath]; ok {
continue
}
rawValue, ok := payloadRawValue(value)
if !ok {
continue
}
updated, errSet := sjson.SetRawBytes(out, fullPath, rawValue)
if errSet != nil {
continue
}
out = updated
appliedDefaults[fullPath] = struct{}{}
}
}
// Apply override rules: last write wins per field across all matching rules.
@@ -109,11 +114,13 @@ func ApplyPayloadConfigWithRoot(cfg *config.Config, model, protocol, root string
if fullPath == "" {
continue
}
updated, errSet := sjson.SetBytes(out, fullPath, value)
if errSet != nil {
continue
for _, resolvedPath := range resolvePayloadRulePaths(out, fullPath) {
updated, errSet := sjson.SetBytes(out, resolvedPath, value)
if errSet != nil {
continue
}
out = updated
}
out = updated
}
}
// Apply override raw rules: last write wins per field across all matching rules.
@@ -131,11 +138,13 @@ func ApplyPayloadConfigWithRoot(cfg *config.Config, model, protocol, root string
if !ok {
continue
}
updated, errSet := sjson.SetRawBytes(out, fullPath, rawValue)
if errSet != nil {
continue
for _, resolvedPath := range resolvePayloadRulePaths(out, fullPath) {
updated, errSet := sjson.SetRawBytes(out, resolvedPath, rawValue)
if errSet != nil {
continue
}
out = updated
}
out = updated
}
}
// Apply filter rules: remove matching paths from payload.
@@ -149,11 +158,15 @@ func ApplyPayloadConfigWithRoot(cfg *config.Config, model, protocol, root string
if fullPath == "" {
continue
}
updated, errDel := sjson.DeleteBytes(out, fullPath)
if errDel != nil {
continue
resolvedPaths := resolvePayloadRulePaths(out, fullPath)
for i := len(resolvedPaths) - 1; i >= 0; i-- {
resolvedPath := resolvedPaths[i]
updated, errDel := sjson.DeleteBytes(out, resolvedPath)
if errDel != nil {
continue
}
out = updated
}
out = updated
}
}
}
@@ -254,6 +267,235 @@ func buildPayloadPath(root, path string) string {
return r + "." + p
}
func resolvePayloadRulePaths(payload []byte, path string) []string {
path = strings.TrimSpace(path)
if path == "" {
return nil
}
if !strings.Contains(path, "#(") {
return []string{path}
}
parts := splitPayloadRulePath(path)
if len(parts) == 0 {
return nil
}
paths := []string{""}
for _, part := range parts {
query, allMatches, ok := parsePayloadQueryPathPart(part)
if !ok {
for i := range paths {
paths[i] = appendPayloadPathPart(paths[i], part)
}
continue
}
nextPaths := make([]string, 0, len(paths))
for _, basePath := range paths {
array := payloadValueAtPath(payload, basePath)
if !array.Exists() || !array.IsArray() {
continue
}
for index, item := range array.Array() {
if !payloadQueryMatches(item, query) {
continue
}
nextPaths = append(nextPaths, appendPayloadPathPart(basePath, strconv.Itoa(index)))
if !allMatches {
break
}
}
}
paths = nextPaths
if len(paths) == 0 {
return nil
}
}
return paths
}
func splitPayloadRulePath(path string) []string {
var parts []string
start := 0
depth := 0
var quote byte
escaped := false
for i := 0; i < len(path); i++ {
ch := path[i]
if escaped {
escaped = false
continue
}
if ch == '\\' {
escaped = true
continue
}
if quote != 0 {
if ch == quote {
quote = 0
}
continue
}
if ch == '"' || ch == '\'' {
quote = ch
continue
}
if ch == '(' {
depth++
continue
}
if ch == ')' {
if depth > 0 {
depth--
}
continue
}
if ch == '.' && depth == 0 {
parts = append(parts, path[start:i])
start = i + 1
}
}
parts = append(parts, path[start:])
return parts
}
func parsePayloadQueryPathPart(part string) (string, bool, bool) {
if !strings.HasPrefix(part, "#(") {
return "", false, false
}
closeIndex := findPayloadQueryClose(part)
if closeIndex < 0 {
return "", false, false
}
suffix := part[closeIndex+1:]
if suffix != "" && suffix != "#" {
return "", false, false
}
return strings.TrimSpace(part[2:closeIndex]), suffix == "#", true
}
func findPayloadQueryClose(part string) int {
var quote byte
escaped := false
depth := 1
for i := 2; i < len(part); i++ {
ch := part[i]
if escaped {
escaped = false
continue
}
if ch == '\\' {
escaped = true
continue
}
if quote != 0 {
if ch == quote {
quote = 0
}
continue
}
if ch == '"' || ch == '\'' {
quote = ch
continue
}
if ch == '(' {
depth++
continue
}
if ch == ')' {
depth--
if depth == 0 {
return i
}
}
}
return -1
}
func appendPayloadPathPart(path, part string) string {
if path == "" {
return part
}
if part == "" {
return path
}
return path + "." + part
}
func payloadValueAtPath(payload []byte, path string) gjson.Result {
if path == "" {
return gjson.ParseBytes(payload)
}
return gjson.GetBytes(payload, path)
}
func payloadQueryMatches(item gjson.Result, query string) bool {
for _, orPart := range splitPayloadLogical(query, "||") {
if payloadQueryAndMatches(item, orPart) {
return true
}
}
return false
}
func payloadQueryAndMatches(item gjson.Result, query string) bool {
parts := splitPayloadLogical(query, "&&")
if len(parts) == 0 {
return false
}
for _, part := range parts {
if !payloadQueryTermMatches(item, part) {
return false
}
}
return true
}
func splitPayloadLogical(query, operator string) []string {
var parts []string
start := 0
var quote byte
escaped := false
for i := 0; i < len(query); i++ {
ch := query[i]
if escaped {
escaped = false
continue
}
if ch == '\\' {
escaped = true
continue
}
if quote != 0 {
if ch == quote {
quote = 0
}
continue
}
if ch == '"' || ch == '\'' {
quote = ch
continue
}
if strings.HasPrefix(query[i:], operator) {
parts = append(parts, strings.TrimSpace(query[start:i]))
i += len(operator) - 1
start = i + 1
}
}
parts = append(parts, strings.TrimSpace(query[start:]))
return parts
}
func payloadQueryTermMatches(item gjson.Result, term string) bool {
term = strings.TrimSpace(term)
if term == "" || item.Raw == "" {
return false
}
wrapped := make([]byte, 0, len(item.Raw)+2)
wrapped = append(wrapped, '[')
wrapped = append(wrapped, item.Raw...)
wrapped = append(wrapped, ']')
return gjson.GetBytes(wrapped, "#("+term+")").Exists()
}
func removeToolTypeFromPayloadWithRoot(payload []byte, root string, toolType string) []byte {
if len(payload) == 0 {
return payload