Merge pull request #3182 from seefs001/feature/params-override-beta-header-append

feat:support $keep_only_declared and deduped $append for header override
This commit is contained in:
Calcium-Ion
2026-03-10 02:03:02 +08:00
committed by GitHub
3 changed files with 239 additions and 32 deletions

View File

@@ -847,24 +847,30 @@ func resolveHeaderOverrideValueByMapping(context map[string]interface{}, headerN
return "", false, fmt.Errorf("header value mapping cannot be empty") return "", false, fmt.Errorf("header value mapping cannot be empty")
} }
sourceValue, exists := getHeaderValueFromContext(context, headerName) appendTokens, err := parseHeaderAppendTokens(mapping)
if !exists { if err != nil {
return "", false, nil return "", false, err
} }
sourceTokens := splitHeaderListValue(sourceValue) keepOnlyDeclared := parseHeaderKeepOnlyDeclared(mapping)
if len(sourceTokens) == 0 {
return "", false, nil sourceValue, exists := getHeaderValueFromContext(context, headerName)
sourceTokens := make([]string, 0)
if exists {
sourceTokens = splitHeaderListValue(sourceValue)
} }
wildcardValue, hasWildcard := mapping["*"] wildcardValue, hasWildcard := mapping["*"]
resultTokens := make([]string, 0, len(sourceTokens)) resultTokens := make([]string, 0, len(sourceTokens)+len(appendTokens))
for _, token := range sourceTokens { for _, token := range sourceTokens {
replacementRaw, hasReplacement := mapping[token] replacementRaw, hasReplacement := mapping[token]
if !hasReplacement && hasWildcard { if !hasReplacement && hasWildcard && !keepOnlyDeclared {
replacementRaw = wildcardValue replacementRaw = wildcardValue
hasReplacement = true hasReplacement = true
} }
if !hasReplacement { if !hasReplacement {
if keepOnlyDeclared {
continue
}
resultTokens = append(resultTokens, token) resultTokens = append(resultTokens, token)
continue continue
} }
@@ -875,6 +881,7 @@ func resolveHeaderOverrideValueByMapping(context map[string]interface{}, headerN
resultTokens = append(resultTokens, replacementTokens...) resultTokens = append(resultTokens, replacementTokens...)
} }
resultTokens = append(resultTokens, appendTokens...)
resultTokens = lo.Uniq(resultTokens) resultTokens = lo.Uniq(resultTokens)
if len(resultTokens) == 0 { if len(resultTokens) == 0 {
return "", false, nil return "", false, nil
@@ -882,6 +889,26 @@ func resolveHeaderOverrideValueByMapping(context map[string]interface{}, headerN
return strings.Join(resultTokens, ","), true, nil return strings.Join(resultTokens, ","), true, nil
} }
func parseHeaderAppendTokens(mapping map[string]interface{}) ([]string, error) {
appendRaw, ok := mapping["$append"]
if !ok {
return nil, nil
}
return parseHeaderReplacementTokens(appendRaw)
}
func parseHeaderKeepOnlyDeclared(mapping map[string]interface{}) bool {
keepOnlyDeclaredRaw, ok := mapping["$keep_only_declared"]
if !ok {
return false
}
keepOnlyDeclared, ok := keepOnlyDeclaredRaw.(bool)
if !ok {
return false
}
return keepOnlyDeclared
}
func parseHeaderReplacementTokens(value interface{}) ([]string, error) { func parseHeaderReplacementTokens(value interface{}) ([]string, error) {
switch raw := value.(type) { switch raw := value.(type) {
case nil: case nil:

View File

@@ -1653,6 +1653,141 @@ func TestApplyParamOverrideSetHeaderMapDeleteWholeHeaderWhenAllTokensCleared(t *
} }
} }
func TestApplyParamOverrideSetHeaderMapAppendsTokens(t *testing.T) {
input := []byte(`{"temperature":0.7}`)
override := map[string]interface{}{
"operations": []interface{}{
map[string]interface{}{
"mode": "set_header",
"path": "anthropic-beta",
"value": map[string]interface{}{
"$append": []interface{}{"context-1m-2025-08-07", "computer-use-2025-01-24"},
},
},
},
}
ctx := map[string]interface{}{
"header_override": map[string]interface{}{
"anthropic-beta": "computer-use-2025-01-24",
},
}
out, err := ApplyParamOverride(input, override, ctx)
if err != nil {
t.Fatalf("ApplyParamOverride returned error: %v", err)
}
assertJSONEqual(t, `{"temperature":0.7}`, string(out))
headers, ok := ctx["header_override"].(map[string]interface{})
if !ok {
t.Fatalf("expected header_override context map")
}
if headers["anthropic-beta"] != "computer-use-2025-01-24,context-1m-2025-08-07" {
t.Fatalf("expected anthropic-beta to append new token without duplicates, got: %v", headers["anthropic-beta"])
}
}
func TestApplyParamOverrideSetHeaderMapAppendsTokensWhenHeaderMissing(t *testing.T) {
input := []byte(`{"temperature":0.7}`)
override := map[string]interface{}{
"operations": []interface{}{
map[string]interface{}{
"mode": "set_header",
"path": "anthropic-beta",
"value": map[string]interface{}{
"$append": []interface{}{"context-1m-2025-08-07", "computer-use-2025-01-24"},
},
},
},
}
ctx := map[string]interface{}{}
out, err := ApplyParamOverride(input, override, ctx)
if err != nil {
t.Fatalf("ApplyParamOverride returned error: %v", err)
}
assertJSONEqual(t, `{"temperature":0.7}`, string(out))
headers, ok := ctx["header_override"].(map[string]interface{})
if !ok {
t.Fatalf("expected header_override context map")
}
if headers["anthropic-beta"] != "context-1m-2025-08-07,computer-use-2025-01-24" {
t.Fatalf("expected anthropic-beta to be created from appended tokens, got: %v", headers["anthropic-beta"])
}
}
func TestApplyParamOverrideSetHeaderMapKeepOnlyDeclaredDropsUndeclaredTokens(t *testing.T) {
input := []byte(`{"temperature":0.7}`)
override := map[string]interface{}{
"operations": []interface{}{
map[string]interface{}{
"mode": "set_header",
"path": "anthropic-beta",
"value": map[string]interface{}{
"computer-use-2025-01-24": "computer-use-2025-01-24",
"$append": []interface{}{"context-1m-2025-08-07"},
"$keep_only_declared": true,
},
},
},
}
ctx := map[string]interface{}{
"header_override": map[string]interface{}{
"anthropic-beta": "advanced-tool-use-2025-11-20,computer-use-2025-01-24",
},
}
out, err := ApplyParamOverride(input, override, ctx)
if err != nil {
t.Fatalf("ApplyParamOverride returned error: %v", err)
}
assertJSONEqual(t, `{"temperature":0.7}`, string(out))
headers, ok := ctx["header_override"].(map[string]interface{})
if !ok {
t.Fatalf("expected header_override context map")
}
if headers["anthropic-beta"] != "computer-use-2025-01-24,context-1m-2025-08-07" {
t.Fatalf("expected anthropic-beta to keep only declared tokens, got: %v", headers["anthropic-beta"])
}
}
func TestApplyParamOverrideSetHeaderMapKeepOnlyDeclaredDeletesHeaderWhenNothingDeclaredMatches(t *testing.T) {
input := []byte(`{"temperature":0.7}`)
override := map[string]interface{}{
"operations": []interface{}{
map[string]interface{}{
"mode": "set_header",
"path": "anthropic-beta",
"value": map[string]interface{}{
"computer-use-2025-01-24": "computer-use-2025-01-24",
"$keep_only_declared": true,
},
},
},
}
ctx := map[string]interface{}{
"header_override": map[string]interface{}{
"anthropic-beta": "advanced-tool-use-2025-11-20",
},
}
out, err := ApplyParamOverride(input, override, ctx)
if err != nil {
t.Fatalf("ApplyParamOverride returned error: %v", err)
}
assertJSONEqual(t, `{"temperature":0.7}`, string(out))
headers, ok := ctx["header_override"].(map[string]interface{})
if !ok {
t.Fatalf("expected header_override context map")
}
if _, exists := headers["anthropic-beta"]; exists {
t.Fatalf("expected anthropic-beta to be deleted when no declared tokens remain, got: %v", headers["anthropic-beta"])
}
}
func TestApplyParamOverrideConditionsObjectShorthand(t *testing.T) { func TestApplyParamOverrideConditionsObjectShorthand(t *testing.T) {
input := []byte(`{"temperature":0.7}`) input := []byte(`{"temperature":0.7}`)
override := map[string]interface{}{ override := map[string]interface{}{

View File

@@ -163,7 +163,7 @@ const MODE_DESCRIPTIONS = {
prune_objects: '按条件清理对象中的子项', prune_objects: '按条件清理对象中的子项',
pass_headers: '把指定请求头透传到上游请求', pass_headers: '把指定请求头透传到上游请求',
sync_fields: '在一个字段有值、另一个缺失时自动补齐', sync_fields: '在一个字段有值、另一个缺失时自动补齐',
set_header: '设置运行期请求头(支持整值覆盖,或用 JSON 映射按逗号 token 替换/删除)', set_header: '设置运行期请求头:可直接覆盖整条值,也可对逗号分隔的 token 做删除、替换、追加或白名单保留',
delete_header: '删除运行期请求头', delete_header: '删除运行期请求头',
copy_header: '复制请求头', copy_header: '复制请求头',
move_header: '移动请求头', move_header: '移动请求头',
@@ -230,17 +230,29 @@ const getModeValueLabel = (mode) => {
return '值(支持 JSON 或普通文本)'; return '值(支持 JSON 或普通文本)';
}; };
const HEADER_VALUE_JSONC_EXAMPLE = `{
// 置空:删除 Bedrock 不支持的 beta特性
"files-api-2025-04-14": null,
// 替换:把旧特性改成兼容特性
"advanced-tool-use-2025-11-20": "tool-search-tool-2025-10-19",
// 追加:在末尾补一个需要的特性
"$append": ["context-1m-2025-08-07"]
}`;
const getModeValuePlaceholder = (mode) => { const getModeValuePlaceholder = (mode) => {
if (mode === 'set_header') { if (mode === 'set_header') {
return [ return [
'String example:', '纯字符串(整条覆盖):',
'Bearer sk-xxx', 'Bearer sk-xxx',
'', '',
'JSON map example:', '或使用 JSON 规则:',
'{"advanced-tool-use-2025-11-20": null, "computer-use-2025-01-24": "computer-use-2025-01-24"}', '{',
'', ' "files-api-2025-04-14": null,',
'JSON map wildcard:', ' "advanced-tool-use-2025-11-20": "tool-search-tool-2025-10-19",',
'{"*": null, "computer-use-2025-11-24": "computer-use-2025-11-24"}', ' "$append": ["context-1m-2025-08-07"]',
'}',
].join('\n'); ].join('\n');
} }
if (mode === 'pass_headers') return 'Authorization, X-Request-Id'; if (mode === 'pass_headers') return 'Authorization, X-Request-Id';
@@ -258,11 +270,6 @@ const getModeValuePlaceholder = (mode) => {
return '0.7'; return '0.7';
}; };
const getModeValueHelp = (mode) => {
if (mode !== 'set_header') return '';
return '字符串整条请求头直接覆盖。JSON 映射:按逗号分隔 token 逐项处理null 表示删除string/array 表示替换,* 表示兜底规则。';
};
const SYNC_TARGET_TYPE_OPTIONS = [ const SYNC_TARGET_TYPE_OPTIONS = [
{ label: '请求体字段', value: 'json' }, { label: '请求体字段', value: 'json' },
{ label: '请求头字段', value: 'header' }, { label: '请求头字段', value: 'header' },
@@ -1075,6 +1082,7 @@ const ParamOverrideEditorModal = ({ visible, value, onSave, onCancel }) => {
const [dragOverPosition, setDragOverPosition] = useState('before'); const [dragOverPosition, setDragOverPosition] = useState('before');
const [templateGroupKey, setTemplateGroupKey] = useState('basic'); const [templateGroupKey, setTemplateGroupKey] = useState('basic');
const [templatePresetKey, setTemplatePresetKey] = useState('operations_default'); const [templatePresetKey, setTemplatePresetKey] = useState('operations_default');
const [headerValueExampleVisible, setHeaderValueExampleVisible] = useState(false);
const [fieldGuideVisible, setFieldGuideVisible] = useState(false); const [fieldGuideVisible, setFieldGuideVisible] = useState(false);
const [fieldGuideTarget, setFieldGuideTarget] = useState('path'); const [fieldGuideTarget, setFieldGuideTarget] = useState('path');
const [fieldGuideKeyword, setFieldGuideKeyword] = useState(''); const [fieldGuideKeyword, setFieldGuideKeyword] = useState('');
@@ -1101,6 +1109,7 @@ const ParamOverrideEditorModal = ({ visible, value, onSave, onCancel }) => {
setTemplateGroupKey('basic'); setTemplateGroupKey('basic');
setTemplatePresetKey('operations_default'); setTemplatePresetKey('operations_default');
} }
setHeaderValueExampleVisible(false);
setFieldGuideVisible(false); setFieldGuideVisible(false);
setFieldGuideTarget('path'); setFieldGuideTarget('path');
setFieldGuideKeyword(''); setFieldGuideKeyword('');
@@ -2823,15 +2832,35 @@ const ParamOverrideEditorModal = ({ visible, value, onSave, onCancel }) => {
{t(getModeValueLabel(mode))} {t(getModeValueLabel(mode))}
</Text> </Text>
{mode === 'set_header' ? ( {mode === 'set_header' ? (
<Button <Space spacing={6}>
size='small' <Button
type='tertiary' size='small'
onClick={formatSelectedOperationValueAsJson} type='tertiary'
> onClick={() =>
{t('格式化 JSON')} setHeaderValueExampleVisible(true)
</Button> }
>
{t('查看 JSON 示例')}
</Button>
<Button
size='small'
type='tertiary'
onClick={formatSelectedOperationValueAsJson}
>
{t('格式化 JSON')}
</Button>
</Space>
) : null} ) : null}
</div> </div>
{mode === 'set_header' ? (
<Text
type='tertiary'
size='small'
className='mt-1 mb-2 block'
>
{t('纯字符串会直接覆盖整条请求头,或者点击“查看 JSON 示例”按 token 规则处理。')}
</Text>
) : null}
<TextArea <TextArea
value={selectedOperation.value_text} value={selectedOperation.value_text}
autosize={{ minRows: 1, maxRows: 4 }} autosize={{ minRows: 1, maxRows: 4 }}
@@ -2842,11 +2871,6 @@ const ParamOverrideEditorModal = ({ visible, value, onSave, onCancel }) => {
}) })
} }
/> />
{getModeValueHelp(mode) ? (
<Text type='tertiary' size='small'>
{t(getModeValueHelp(mode))}
</Text>
) : null}
</div> </div>
) )
) : null} ) : null}
@@ -3302,6 +3326,27 @@ const ParamOverrideEditorModal = ({ visible, value, onSave, onCancel }) => {
</Space> </Space>
</Modal> </Modal>
<Modal
title={t('anthropic-beta JSON 示例')}
visible={headerValueExampleVisible}
width={760}
footer={null}
onCancel={() => setHeaderValueExampleVisible(false)}
bodyStyle={{ padding: 16, paddingBottom: 24 }}
>
<Space vertical align='start' spacing={12} style={{ width: '100%' }}>
<Text type='tertiary' size='small'>
{t('下面是带注释的示例,仅用于参考;实际保存时请删除注释。')}
</Text>
<TextArea
value={HEADER_VALUE_JSONC_EXAMPLE}
readOnly
autosize={{ minRows: 16, maxRows: 20 }}
style={{ marginBottom: 8 }}
/>
</Space>
</Modal>
<Modal <Modal
title={null} title={null}
visible={fieldGuideVisible} visible={fieldGuideVisible}