feat(body_rules): 支持通配符路径、范围索引、name_style action 和 $item 条件引用

- 路径语法新增 [*] 通配符(遍历所有元素)和 [N-M] 范围索引
- 新增 name_style action,支持 snake_case/camelCase/PascalCase/kebab-case/capitalize 风格转换
- condition 支持 $item.xxx 引用通配符当前元素,实现逐元素条件过滤
- 前端同步更新类型定义、表单 UI 和帮助文档
- 提取 isBodyRuleEffective 函数消除重复的规则有效性判断逻辑
This commit is contained in:
fawney19
2026-02-21 14:59:37 +08:00
parent 05177dffb3
commit bb7d393128
5 changed files with 883 additions and 53 deletions

View File

@@ -296,7 +296,7 @@
v-if="getEndpointEditBodyRules(endpoint.id).length > 0"
class="flex items-center gap-1 text-xs text-muted-foreground px-2"
>
<span><code class="bg-muted px-1 rounded">.</code> 嵌套字段 / <code class="bg-muted px-1 rounded">[N]</code> 数组索引;值为 JSON 格式</span>
<span><code class="bg-muted px-1 rounded">.</code> 嵌套字段 / <code class="bg-muted px-1 rounded">[N]</code> 数组索引 / <code class="bg-muted px-1 rounded">[*]</code> 通配符;值为 JSON 格式</span>
<div class="flex-1" />
<Popover
:open="bodyRuleHelpOpenEndpointId === endpoint.id"
@@ -326,6 +326,8 @@
<div class="text-muted-foreground">
<code>metadata.user_id</code> 嵌套字段<br>
<code>messages[0].content</code> 数组索引<br>
<code>tools[*].name</code> 通配符(遍历所有元素)<br>
<code>tools[0-4].name</code> 范围(遍历索引 0~4<br>
<code>config\.v1.key</code> 转义点号
</div>
</div>
@@ -339,6 +341,14 @@
<code v-pre>{{$original}}</code> 引用原值
</div>
</div>
<div>
<div class="font-medium mb-0.5">
命名风格
</div>
<div class="text-muted-foreground">
批量转换字段命名capitalize / snake_case / camelCase / PascalCase / kebab-case
</div>
</div>
<div>
<div class="font-medium mb-0.5">
条件运算符
@@ -350,12 +360,16 @@
<code>matches</code> 正则匹配<br>
<code>exists</code> <code>not_exists</code> 字段存在性<br>
<code>in</code> 在列表中(值填 <code>["a","b"]</code><br>
<code>type_is</code> 类型判断string/number/boolean/array/object/null
<code>type_is</code> 类型判断string/number/boolean/array/object/null<br>
条件路径支持 <code>$item.xxx</code> 引用通配符当前元素
</div>
</div>
<div class="text-muted-foreground">
规则按顺序执行,前面的修改对后续规则可见。
</div>
<div class="text-muted-foreground">
规则在格式转换之后执行,路径需按目标提供商的请求体结构填写。
</div>
</div>
</PopoverContent>
</Popover>
@@ -398,6 +412,9 @@
<SelectItem value="regex_replace">
正则替换
</SelectItem>
<SelectItem value="name_style">
命名风格
</SelectItem>
</SelectContent>
</Select>
<Button
@@ -524,6 +541,41 @@
:title="getRegexPatternValidationTip(rule)"
/>
</template>
<template v-else-if="rule.action === 'name_style'">
<Input
:model-value="rule.path"
placeholder="字段路径 tools[*].name"
size="sm"
class="flex-[2] min-w-0 h-7 text-xs"
@update:model-value="(v) => updateEndpointBodyRuleField(endpoint.id, index, 'path', v)"
/>
<span class="text-muted-foreground text-xs">→</span>
<Select
:model-value="rule.style || 'capitalize'"
@update:model-value="(v: string) => updateEndpointBodyRuleField(endpoint.id, index, 'style', v)"
>
<SelectTrigger class="w-[120px] h-7 text-xs shrink-0">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="capitalize">
Capitalize
</SelectItem>
<SelectItem value="snake_case">
snake_case
</SelectItem>
<SelectItem value="camelCase">
camelCase
</SelectItem>
<SelectItem value="PascalCase">
PascalCase
</SelectItem>
<SelectItem value="kebab-case">
kebab-case
</SelectItem>
</SelectContent>
</Select>
</template>
<Button
variant="ghost"
size="icon"
@@ -541,7 +593,7 @@
<span class="text-[10px] font-semibold text-muted-foreground shrink-0">IF</span>
<Input
:model-value="rule.conditionPath"
placeholder="字段路径"
:placeholder="rule.path?.includes('[*]') || rule.path?.match(/\[\d+-\d+\]/) ? '$item.字段名' : '字段路径'"
size="sm"
class="flex-1 min-w-0 h-7 text-xs"
@update:model-value="(v) => updateEndpointBodyRuleField(endpoint.id, index, 'conditionPath', v)"
@@ -742,6 +794,7 @@ import {
type HeaderRule,
type BodyRule,
type BodyRuleRegexReplace,
type BodyRuleNameStyle,
type BodyRuleCondition,
type BodyRuleConditionOp,
} from '@/api/endpoints'
@@ -758,7 +811,7 @@ interface EditableRule {
}
// 编辑用的请求体规则类型
type BodyRuleAction = 'set' | 'drop' | 'rename' | 'append' | 'insert' | 'regex_replace'
type BodyRuleAction = 'set' | 'drop' | 'rename' | 'append' | 'insert' | 'regex_replace' | 'name_style'
interface EditableBodyRule {
action: BodyRuleAction
@@ -770,6 +823,7 @@ interface EditableBodyRule {
pattern: string // regex_replace 用
replacement: string // regex_replace 用
flags: string // regex_replace 用i/m/s
style: string // name_style 用snake_case/camelCase/PascalCase/kebab-case/capitalize
conditionEnabled: boolean // 是否启用条件
conditionPath: string
conditionOp: string
@@ -1082,7 +1136,7 @@ function initEndpointEditState(endpoint: ProviderEndpoint): EndpointEditState {
}
const emptyBodyRule = (): Omit<EditableBodyRule, 'action'> => ({
path: '', value: '', from: '', to: '', index: '', pattern: '', replacement: '', flags: '',
path: '', value: '', from: '', to: '', index: '', pattern: '', replacement: '', flags: '', style: '',
conditionEnabled: false, conditionPath: '', conditionOp: 'eq', conditionValue: '',
})
@@ -1115,6 +1169,8 @@ function initEndpointEditState(endpoint: ProviderEndpoint): EndpointEditState {
bodyRules.push({ ...emptyBodyRule(), action: 'insert', path: rule.path || '', value, index: String(rule.index ?? ''), ...conditionFields })
} else if (rule.action === 'regex_replace') {
bodyRules.push({ ...emptyBodyRule(), action: 'regex_replace', path: rule.path || '', pattern: rule.pattern || '', replacement: rule.replacement || '', flags: rule.flags || '', ...conditionFields })
} else if (rule.action === 'name_style') {
bodyRules.push({ ...emptyBodyRule(), action: 'name_style', path: rule.path || '', style: rule.style || 'capitalize', ...conditionFields })
}
}
}
@@ -1303,7 +1359,7 @@ function getEndpointEditBodyRules(endpointId: string): EditableBodyRule[] {
// 添加请求体规则(同时自动展开折叠)
function handleAddEndpointBodyRule(endpointId: string) {
const rules = getEndpointEditBodyRules(endpointId)
rules.push({ action: 'set', path: '', value: '', from: '', to: '', index: '', pattern: '', replacement: '', flags: '', conditionEnabled: false, conditionPath: '', conditionOp: 'eq', conditionValue: '' })
rules.push({ action: 'set', path: '', value: '', from: '', to: '', index: '', pattern: '', replacement: '', flags: '', style: '', conditionEnabled: false, conditionPath: '', conditionOp: 'eq', conditionValue: '' })
// 自动展开折叠
endpointRulesExpanded.value[endpointId] = true
}
@@ -1327,11 +1383,12 @@ function updateEndpointBodyRuleAction(endpointId: string, index: number, action:
rules[index].pattern = ''
rules[index].replacement = ''
rules[index].flags = ''
rules[index].style = ''
}
}
// 更新请求体规则字段
function updateEndpointBodyRuleField(endpointId: string, index: number, field: 'path' | 'value' | 'from' | 'to' | 'index' | 'pattern' | 'replacement' | 'flags' | 'conditionPath' | 'conditionOp' | 'conditionValue', value: string) {
function updateEndpointBodyRuleField(endpointId: string, index: number, field: 'path' | 'value' | 'from' | 'to' | 'index' | 'pattern' | 'replacement' | 'flags' | 'style' | 'conditionPath' | 'conditionOp' | 'conditionValue', value: string) {
const rules = getEndpointEditBodyRules(endpointId)
if (rules[index]) {
rules[index][field] = value
@@ -1520,17 +1577,31 @@ function getBodySetValueValidationTip(rule: EditableBodyRule): string {
}
}
// 判断请求体规则是否有效(必填字段已填写)
function isBodyRuleEffective(r: EditableBodyRule): boolean {
switch (r.action) {
case 'set':
case 'drop':
return !!r.path.trim()
case 'rename':
return !!(r.from.trim() && r.to.trim())
case 'insert':
case 'append':
return !!r.path.trim()
case 'regex_replace':
return !!(r.path.trim() && r.pattern.trim())
case 'name_style':
return !!(r.path.trim() && r.style.trim())
default:
return false
}
}
// 获取端点的请求体规则数量(有效的规则)
function getEndpointBodyRulesCount(endpoint: ProviderEndpoint): number {
const state = endpointEditStates.value[endpoint.id]
if (state) {
return state.bodyRules.filter(r => {
if (r.action === 'set' || r.action === 'drop') return r.path.trim()
if (r.action === 'rename') return r.from.trim() && r.to.trim()
if (r.action === 'insert' || r.action === 'append') return r.path.trim()
if (r.action === 'regex_replace') return r.path.trim() && r.pattern.trim()
return false
}).length
return state.bodyRules.filter(isBodyRuleEffective).length
}
return endpoint.body_rules?.length || 0
}
@@ -1592,13 +1663,7 @@ function hasBodyRulesChanges(endpoint: ProviderEndpoint): boolean {
if (!state) return false
const originalRules = endpoint.body_rules || []
const editedRules = state.bodyRules.filter(r => {
if (r.action === 'set' || r.action === 'drop') return r.path.trim()
if (r.action === 'rename') return r.from.trim() && r.to.trim()
if (r.action === 'insert' || r.action === 'append') return r.path.trim()
if (r.action === 'regex_replace') return r.path.trim() && r.pattern.trim()
return false
})
const editedRules = state.bodyRules.filter(isBodyRuleEffective)
if (editedRules.length !== originalRules.length) return true
for (let i = 0; i < editedRules.length; i++) {
const edited = editedRules[i]
@@ -1629,6 +1694,9 @@ function hasBodyRulesChanges(endpoint: ProviderEndpoint): boolean {
if (edited.pattern !== (original.pattern ?? '')) return true
if (edited.replacement !== (original.replacement ?? '')) return true
if (edited.flags !== (original.flags ?? '')) return true
} else if (edited.action === 'name_style' && original.action === 'name_style') {
if (edited.path !== (original.path ?? '')) return true
if (edited.style !== (original.style ?? '')) return true
}
// 条件变更检测
const origCond = original.condition
@@ -1693,6 +1761,8 @@ function rulesToBodyRules(rules: EditableBodyRule[]): BodyRule[] | null {
...(rule.flags.trim() ? { flags: rule.flags.trim() } : {}),
}
result.push({ ...entry, ...(condition ? { condition } : {}) })
} else if (rule.action === 'name_style' && rule.path.trim() && rule.style.trim()) {
result.push({ action: 'name_style', path: rule.path.trim(), style: rule.style.trim() as BodyRuleNameStyle['style'], ...(condition ? { condition } : {}) })
}
}
@@ -1740,6 +1810,10 @@ function getBodyValidationErrorForEndpoint(endpointId: string): string | null {
if (!validFlags.has(f)) return `${prefix}flags 仅允许 i/m/s非法字符: ${f}`
}
}
} else if (rule.action === 'name_style') {
if (!rule.path.trim()) return `${prefix}路径不能为空`
const validStyles = new Set(['snake_case', 'camelCase', 'PascalCase', 'kebab-case', 'capitalize'])
if (!rule.style.trim() || !validStyles.has(rule.style.trim())) return `${prefix}请选择有效的命名风格`
}
}
return null