Remove body name style rule support

This commit is contained in:
fawney19
2026-05-06 22:12:21 +08:00
parent 9525a68719
commit e2bffbbaca
3 changed files with 8 additions and 225 deletions

View File

@@ -11,13 +11,6 @@ const ITEM_PREFIX: &str = "$item.";
const ITEM_EXACT: &str = "$item";
const CONDITION_SOURCES: &[&str] = &["current", "original"];
const CONDITION_TYPE_VALUES: &[&str] = &["string", "number", "boolean", "array", "object", "null"];
const NAME_STYLE_VALUES: &[&str] = &[
"snake_case",
"camelCase",
"PascalCase",
"kebab-case",
"capitalize",
];
static RANGE_RE: OnceLock<Regex> = OnceLock::new();
@@ -160,8 +153,7 @@ pub fn body_rules_handle_path(rules: Option<&Value>, path: &str) -> bool {
| Some("drop")
| Some("append")
| Some("insert")
| Some("regex_replace")
| Some("name_style") => rule
| Some("regex_replace") => rule
.get("path")
.and_then(Value::as_str)
.and_then(parse_body_path)
@@ -379,37 +371,6 @@ pub fn apply_local_body_rules(
}
}
}
Some("name_style") => {
let Some(path) = rule
.get("path")
.and_then(Value::as_str)
.and_then(parse_body_path)
else {
continue;
};
let Some(style) = rule.get("style").and_then(Value::as_str) else {
continue;
};
if !valid_name_style(style) {
continue;
}
for target_path in iter_wildcard_targets(
body,
&path,
condition,
item_condition,
original_body,
true,
false,
) {
if let Some(target) = get_nested_value_mut(body, &target_path) {
let Some(current) = target.as_str().map(str::to_owned) else {
continue;
};
*target = Value::String(convert_name_style(&current, style));
}
}
}
_ => continue,
}
}
@@ -1068,97 +1029,6 @@ fn parse_non_negative_count(value: &Value) -> Option<usize> {
})
}
fn valid_name_style(style: &str) -> bool {
NAME_STYLE_VALUES.contains(&style)
}
fn convert_name_style(name: &str, style: &str) -> String {
let words = split_identifier(name);
if words.is_empty() {
return name.to_string();
}
match style {
"snake_case" => words.join("_"),
"camelCase" => {
let mut result = words[0].clone();
for word in words.iter().skip(1) {
result.push_str(&capitalize_ascii(word));
}
result
}
"PascalCase" => words
.iter()
.map(|word| capitalize_ascii(word))
.collect::<String>(),
"kebab-case" => words.join("-"),
"capitalize" => name
.chars()
.next()
.map(|first| {
let mut result = first.to_uppercase().collect::<String>();
result.push_str(name.chars().skip(1).collect::<String>().as_str());
result
})
.unwrap_or_default(),
_ => name.to_string(),
}
}
fn split_identifier(name: &str) -> Vec<String> {
let normalized = name.replace(['_', '-'], " ");
let chars: Vec<char> = normalized.chars().collect();
let mut words = Vec::new();
let mut current = String::new();
for (index, ch) in chars.iter().copied().enumerate() {
if ch.is_whitespace() {
if !current.is_empty() {
words.push(std::mem::take(&mut current));
}
continue;
}
let boundary = if current.is_empty() {
false
} else {
let prev = chars[index - 1];
let next = chars.get(index + 1).copied();
(prev.is_ascii_lowercase() && ch.is_ascii_uppercase())
|| (prev.is_ascii_alphabetic() && ch.is_ascii_digit())
|| (prev.is_ascii_digit() && ch.is_ascii_alphabetic())
|| (prev.is_ascii_uppercase()
&& ch.is_ascii_uppercase()
&& next.is_some_and(|next| next.is_ascii_lowercase()))
};
if boundary {
words.push(std::mem::take(&mut current));
}
current.push(ch);
}
if !current.is_empty() {
words.push(current);
}
words
.into_iter()
.filter(|word| !word.is_empty())
.map(|word| word.to_ascii_lowercase())
.collect()
}
fn capitalize_ascii(word: &str) -> String {
let mut chars = word.chars();
let Some(first) = chars.next() else {
return String::new();
};
let mut result = first.to_uppercase().collect::<String>();
result.push_str(chars.as_str());
result
}
fn get_nested_value(value: &Value, path: &[BodyPathSegment]) -> Option<Value> {
let mut current = value;
for segment in path {
@@ -1390,7 +1260,6 @@ mod tests {
{"action":"append","path":"messages","value":{"role":"assistant","content":"done"}},
{"action":"insert","path":"messages","index":-1,"value":{"role":"system","content":"before-last"}},
{"action":"regex_replace","path":"tools[*].name","pattern":"tool","replacement":"utility","flags":"i","count":1},
{"action":"name_style","path":"tools[*].kind","style":"camelCase","condition":{"path":"$item.name","op":"starts_with","value":"Writer"}},
{"action":"drop","path":"tools[1-2].deprecated"}
]);
assert!(body_rules_are_locally_supported(Some(&rules)));
@@ -1434,9 +1303,6 @@ mod tests {
assert_eq!(body["tools"][0]["name"], "Writerutility");
assert_eq!(body["tools"][1]["name"], "Readutility");
assert_eq!(body["tools"][2]["name"], "Otherutility");
assert_eq!(body["tools"][0]["kind"], "snakeCaseName");
assert_eq!(body["tools"][1]["kind"], "Pascal Case");
assert_eq!(body["tools"][2]["kind"], "kebab-case-value");
assert_eq!(body["tools"][0]["deprecated"], true);
assert!(body["tools"][1].get("deprecated").is_none());
assert!(body["tools"][2].get("deprecated").is_none());
@@ -1601,15 +1467,14 @@ mod tests {
let rules = serde_json::json!([
{"action":"append","path":"messages","value":{}},
{"action":"regex_replace","path":"tools[*].name","pattern":"foo","replacement":"bar"},
{"action":"name_style","path":"tools[0-2].kind","style":"snake_case"},
{"action":"rename","from":"metadata.old","to":"metadata.new"}
]);
assert!(body_rules_handle_path(Some(&rules), "messages"));
assert!(body_rules_handle_path(Some(&rules), "tools[1].name"));
assert!(body_rules_handle_path(Some(&rules), "tools[2].kind"));
assert!(body_rules_handle_path(Some(&rules), "metadata.old"));
assert!(body_rules_handle_path(Some(&rules), "metadata.new"));
assert!(!body_rules_handle_path(Some(&rules), "tools[2].kind"));
assert!(!body_rules_handle_path(Some(&rules), "tools[3].kind"));
assert!(!body_rules_handle_path(Some(&rules), "instructions"));
}

View File

@@ -160,19 +160,7 @@ export type HeaderRule = (HeaderRuleSet | HeaderRuleDrop | HeaderRuleRename) & {
condition?: BodyRuleCondition
}
/**
* 请求体规则 - 转换命名风格
*
* - path 指向目标字符串字段,支持通配符如 "tools[*].name"
* - style 为目标命名风格
*/
export interface BodyRuleNameStyle {
action: 'name_style'
path: string
style: 'snake_case' | 'camelCase' | 'PascalCase' | 'kebab-case' | 'capitalize'
}
export type BodyRule = (BodyRuleSet | BodyRuleDrop | BodyRuleRename | BodyRuleAppend | BodyRuleInsert | BodyRuleRegexReplace | BodyRuleNameStyle) & {
export type BodyRule = (BodyRuleSet | BodyRuleDrop | BodyRuleRename | BodyRuleAppend | BodyRuleInsert | BodyRuleRegexReplace) & {
condition?: BodyRuleCondition
}

View File

@@ -622,14 +622,6 @@
<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">
条件运算符
@@ -692,7 +684,7 @@
@update:model-value="(v: string) => updateEndpointBodyRuleAction(endpoint.id, index, v as BodyRuleAction)"
@update:open="(v) => handleBodyRuleSelectOpen(endpoint.id, index, v)"
>
<SelectTrigger class="w-[96px] h-7 text-xs shrink-0">
<SelectTrigger class="w-[88px] h-7 text-xs shrink-0">
<SelectValue />
</SelectTrigger>
<SelectContent>
@@ -714,9 +706,6 @@
<SelectItem value="regex_replace">
正则替换
</SelectItem>
<SelectItem value="name_style">
命名风格
</SelectItem>
</SelectContent>
</Select>
<Button
@@ -873,41 +862,6 @@
: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"
@@ -1066,7 +1020,6 @@ import {
type HeaderRule,
type BodyRule,
type BodyRuleRegexReplace,
type BodyRuleNameStyle,
} from '@/api/endpoints'
import { adminApi } from '@/api/admin'
import { formatApiFormat } from '@/api/endpoints/types/api-format'
@@ -1091,7 +1044,7 @@ interface EditableRule {
}
// 编辑用的请求体规则类型
type BodyRuleAction = 'set' | 'drop' | 'rename' | 'append' | 'insert' | 'regex_replace' | 'name_style'
type BodyRuleAction = 'set' | 'drop' | 'rename' | 'append' | 'insert' | 'regex_replace'
interface EditableBodyRule {
action: BodyRuleAction
@@ -1104,7 +1057,6 @@ interface EditableBodyRule {
replacement: string // regex_replace 用
flags: string // regex_replace 用i/m/s
count: string // regex_replace 用(空=默认全部0=全部)
style: string // name_style 用snake_case/camelCase/PascalCase/kebab-case/capitalize
condition: EditableConditionNode | null
}
@@ -1481,8 +1433,7 @@ const RESERVED_BODY_FIELDS = new Set([
'stream',
])
const BODY_RULE_JSON_ACTIONS = new Set(['set', 'drop', 'rename', 'append', 'insert', 'regex_replace', 'name_style'])
const BODY_RULE_JSON_STYLES = new Set(['snake_case', 'camelCase', 'PascalCase', 'kebab-case', 'capitalize'])
const BODY_RULE_JSON_ACTIONS = new Set(['set', 'drop', 'rename', 'append', 'insert', 'regex_replace'])
const CONDITION_JSON_OPS = new Set(['eq', 'neq', 'gt', 'lt', 'gte', 'lte', 'starts_with', 'ends_with', 'contains', 'matches', 'exists', 'not_exists', 'in', 'type_is'])
function isJsonObject(value: unknown): value is Record<string, unknown> {
@@ -1598,11 +1549,7 @@ function validateBodyRuleJson(rule: unknown, label: string, index: number): stri
if (rule.count !== undefined && !Number.isInteger(rule.count)) return `${label}第 ${index + 1} 条count 必须是整数`
return validateJsonCondition(rule, label, index)
}
if (requireJsonString(rule, 'path', label, index)) return requireJsonString(rule, 'path', label, index)
if (typeof rule.style !== 'string' || !BODY_RULE_JSON_STYLES.has(rule.style)) {
return `${label}第 ${index + 1} 条style 无效`
}
return validateJsonCondition(rule, label, index)
return `${label}第 ${index + 1} 条action 无效`
}
function parseEndpointRulesJsonDraft(draft: string): { value: EndpointRulesJsonPayload | null; error: string | null } {
@@ -2023,7 +1970,6 @@ function emptyBodyRule(action: BodyRuleAction = 'set'): EditableBodyRule {
replacement: '',
flags: '',
count: '',
style: '',
condition: null,
}
}
@@ -2056,8 +2002,6 @@ function editableBodyRulesFromRules(rules: BodyRule[] | null | undefined): Edita
count: rule.count === undefined || rule.count === null ? '' : String(rule.count),
condition: conditionToEditable(rule.condition),
})
} else if (rule.action === 'name_style') {
bodyRules.push({ ...emptyBodyRule('name_style'), path: rule.path || '', style: rule.style || 'capitalize', condition: conditionToEditable(rule.condition) })
}
}
return bodyRules
@@ -2457,7 +2401,7 @@ function updateEndpointBodyRuleAction(endpointId: string, index: number, action:
}
// 更新请求体规则字段
function updateEndpointBodyRuleField(endpointId: string, index: number, field: 'path' | 'value' | 'from' | 'to' | 'index' | 'pattern' | 'replacement' | 'flags' | 'count' | 'style', value: string) {
function updateEndpointBodyRuleField(endpointId: string, index: number, field: 'path' | 'value' | 'from' | 'to' | 'index' | 'pattern' | 'replacement' | 'flags' | 'count', value: string) {
const rules = getEndpointEditBodyRules(endpointId)
if (rules[index]) {
rules[index][field] = value
@@ -2685,8 +2629,6 @@ function isBodyRuleEffective(r: EditableBodyRule): boolean {
return !!(r.path.trim() && r.index.trim())
case 'regex_replace':
return !!(r.path.trim() && r.pattern.trim())
case 'name_style':
return !!(r.path.trim() && r.style.trim())
default:
return false
}
@@ -2753,9 +2695,6 @@ function _formatBodyRuleLabel(rule: EditableBodyRule): string {
const flags = rule.flags.trim()
const count = rule.count.trim()
return `${rule.path}: s/${rule.pattern}/${rule.replacement || ''}/${flags}${count ? ` ×${count}` : ''}`
} else if (rule.action === 'name_style') {
if (!rule.path || !rule.style) return '(未设置)'
return `${rule.path}→${rule.style}`
}
return '(未知)'
}
@@ -2796,9 +2735,6 @@ function hasBodyRulesChanges(endpoint: ProviderEndpoint): boolean {
if (edited.replacement !== (original.replacement ?? '')) return true
if (edited.flags !== (original.flags ?? '')) return true
if (edited.count !== (original.count === undefined || original.count === null ? '' : String(original.count))) 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
}
if (!conditionEquals(edited.condition, conditionToEditable(original.condition))) return true
}
@@ -2839,8 +2775,6 @@ function rulesToBodyRules(rules: EditableBodyRule[]): BodyRule[] | null {
...(isStrictNonNegativeIntegerString(rule.count) ? { count: parseInt(rule.count.trim(), 10) } : {}),
}
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 } : {}) })
}
}
@@ -2898,10 +2832,6 @@ function getBodyValidationErrorForEndpoint(endpointId: string): string | null {
if (count) {
if (!isStrictNonNegativeIntegerString(count)) return `${prefix}替换次数必须是大于等于 0 的整数`
}
} 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}请选择有效的命名风格`
}
const conditionErr = validateEditableCondition(rule.condition)