mirror of
https://github.com/router-for-me/CLIProxyAPI.git
synced 2026-05-31 03:41:59 +08:00
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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user