mirror of
https://github.com/QuantumNous/new-api.git
synced 2026-03-30 02:25:00 +00:00
feat: add pass_headers op, grouped presets (incl. Gemini 4K), and robust JSON fallback
This commit is contained in:
@@ -34,7 +34,7 @@ type ConditionOperation struct {
|
|||||||
|
|
||||||
type ParamOperation struct {
|
type ParamOperation struct {
|
||||||
Path string `json:"path"`
|
Path string `json:"path"`
|
||||||
Mode string `json:"mode"` // delete, set, move, copy, prepend, append, trim_prefix, trim_suffix, ensure_prefix, ensure_suffix, trim_space, to_lower, to_upper, replace, regex_replace, return_error, prune_objects, set_header, delete_header, copy_header, move_header, sync_fields
|
Mode string `json:"mode"` // delete, set, move, copy, prepend, append, trim_prefix, trim_suffix, ensure_prefix, ensure_suffix, trim_space, to_lower, to_upper, replace, regex_replace, return_error, prune_objects, set_header, delete_header, copy_header, move_header, pass_headers, sync_fields
|
||||||
Value interface{} `json:"value"`
|
Value interface{} `json:"value"`
|
||||||
KeepOrigin bool `json:"keep_origin"`
|
KeepOrigin bool `json:"keep_origin"`
|
||||||
From string `json:"from,omitempty"`
|
From string `json:"from,omitempty"`
|
||||||
@@ -494,6 +494,19 @@ func applyOperations(jsonStr string, operations []ParamOperation, conditionConte
|
|||||||
if err == nil {
|
if err == nil {
|
||||||
contextJSON, err = marshalContextJSON(context)
|
contextJSON, err = marshalContextJSON(context)
|
||||||
}
|
}
|
||||||
|
case "pass_headers":
|
||||||
|
headerNames, parseErr := parseHeaderPassThroughNames(op.Value)
|
||||||
|
if parseErr != nil {
|
||||||
|
return "", parseErr
|
||||||
|
}
|
||||||
|
for _, headerName := range headerNames {
|
||||||
|
if err = copyHeaderInContext(context, headerName, headerName, op.KeepOrigin); err != nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err == nil {
|
||||||
|
contextJSON, err = marshalContextJSON(context)
|
||||||
|
}
|
||||||
case "sync_fields":
|
case "sync_fields":
|
||||||
result, err = syncFieldsBetweenTargets(result, context, op.From, op.To)
|
result, err = syncFieldsBetweenTargets(result, context, op.From, op.To)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
@@ -678,6 +691,80 @@ func deleteHeaderOverrideInContext(context map[string]interface{}, headerName st
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func parseHeaderPassThroughNames(value interface{}) ([]string, error) {
|
||||||
|
normalizeNames := func(values []string) []string {
|
||||||
|
names := lo.FilterMap(values, func(item string, _ int) (string, bool) {
|
||||||
|
headerName := strings.TrimSpace(item)
|
||||||
|
if headerName == "" {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
return headerName, true
|
||||||
|
})
|
||||||
|
return lo.Uniq(names)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch raw := value.(type) {
|
||||||
|
case nil:
|
||||||
|
return nil, fmt.Errorf("pass_headers value is required")
|
||||||
|
case string:
|
||||||
|
trimmed := strings.TrimSpace(raw)
|
||||||
|
if trimmed == "" {
|
||||||
|
return nil, fmt.Errorf("pass_headers value is required")
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(trimmed, "[") || strings.HasPrefix(trimmed, "{") {
|
||||||
|
var parsed interface{}
|
||||||
|
if err := common.UnmarshalJsonStr(trimmed, &parsed); err == nil {
|
||||||
|
return parseHeaderPassThroughNames(parsed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
names := normalizeNames(strings.Split(trimmed, ","))
|
||||||
|
if len(names) == 0 {
|
||||||
|
return nil, fmt.Errorf("pass_headers value is invalid")
|
||||||
|
}
|
||||||
|
return names, nil
|
||||||
|
case []interface{}:
|
||||||
|
names := lo.FilterMap(raw, func(item interface{}, _ int) (string, bool) {
|
||||||
|
headerName := strings.TrimSpace(fmt.Sprintf("%v", item))
|
||||||
|
if headerName == "" {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
return headerName, true
|
||||||
|
})
|
||||||
|
names = lo.Uniq(names)
|
||||||
|
if len(names) == 0 {
|
||||||
|
return nil, fmt.Errorf("pass_headers value is invalid")
|
||||||
|
}
|
||||||
|
return names, nil
|
||||||
|
case map[string]interface{}:
|
||||||
|
candidates := make([]string, 0, 8)
|
||||||
|
if headersRaw, ok := raw["headers"]; ok {
|
||||||
|
names, err := parseHeaderPassThroughNames(headersRaw)
|
||||||
|
if err == nil {
|
||||||
|
candidates = append(candidates, names...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if namesRaw, ok := raw["names"]; ok {
|
||||||
|
names, err := parseHeaderPassThroughNames(namesRaw)
|
||||||
|
if err == nil {
|
||||||
|
candidates = append(candidates, names...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if headerRaw, ok := raw["header"]; ok {
|
||||||
|
names, err := parseHeaderPassThroughNames(headerRaw)
|
||||||
|
if err == nil {
|
||||||
|
candidates = append(candidates, names...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
names := normalizeNames(candidates)
|
||||||
|
if len(names) == 0 {
|
||||||
|
return nil, fmt.Errorf("pass_headers value is invalid")
|
||||||
|
}
|
||||||
|
return names, nil
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("pass_headers value must be string, array or object")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
type syncTarget struct {
|
type syncTarget struct {
|
||||||
kind string
|
kind string
|
||||||
key string
|
key string
|
||||||
|
|||||||
@@ -71,6 +71,7 @@ import {
|
|||||||
IconServer,
|
IconServer,
|
||||||
IconSetting,
|
IconSetting,
|
||||||
IconCode,
|
IconCode,
|
||||||
|
IconCopy,
|
||||||
IconGlobe,
|
IconGlobe,
|
||||||
IconBolt,
|
IconBolt,
|
||||||
IconSearch,
|
IconSearch,
|
||||||
@@ -95,6 +96,28 @@ const REGION_EXAMPLE = {
|
|||||||
'claude-3-5-sonnet-20240620': 'europe-west1',
|
'claude-3-5-sonnet-20240620': 'europe-west1',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const PARAM_OVERRIDE_LEGACY_TEMPLATE = {
|
||||||
|
temperature: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
const PARAM_OVERRIDE_OPERATIONS_TEMPLATE = {
|
||||||
|
operations: [
|
||||||
|
{
|
||||||
|
path: 'temperature',
|
||||||
|
mode: 'set',
|
||||||
|
value: 0.7,
|
||||||
|
conditions: [
|
||||||
|
{
|
||||||
|
path: 'model',
|
||||||
|
mode: 'prefix',
|
||||||
|
value: 'openai/',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
logic: 'AND',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
// 支持并且已适配通过接口获取模型列表的渠道类型
|
// 支持并且已适配通过接口获取模型列表的渠道类型
|
||||||
const MODEL_FETCHABLE_TYPES = new Set([
|
const MODEL_FETCHABLE_TYPES = new Set([
|
||||||
1, 4, 14, 34, 17, 26, 27, 24, 47, 25, 20, 23, 31, 40, 42, 48, 43,
|
1, 4, 14, 34, 17, 26, 27, 24, 47, 25, 20, 23, 31, 40, 42, 48, 43,
|
||||||
@@ -270,7 +293,7 @@ const EditChannelModal = (props) => {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
tagLabel: 'Custom JSON',
|
tagLabel: t('自定义 JSON'),
|
||||||
tagColor: 'orange',
|
tagColor: 'orange',
|
||||||
preview: pretty,
|
preview: pretty,
|
||||||
};
|
};
|
||||||
@@ -608,6 +631,100 @@ const EditChannelModal = (props) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const copyParamOverrideJson = async () => {
|
||||||
|
const raw =
|
||||||
|
typeof inputs.param_override === 'string'
|
||||||
|
? inputs.param_override.trim()
|
||||||
|
: '';
|
||||||
|
if (!raw) {
|
||||||
|
showInfo(t('暂无可复制 JSON'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let content = raw;
|
||||||
|
if (verifyJSON(raw)) {
|
||||||
|
try {
|
||||||
|
content = JSON.stringify(JSON.parse(raw), null, 2);
|
||||||
|
} catch (error) {
|
||||||
|
content = raw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const ok = await copy(content);
|
||||||
|
if (ok) {
|
||||||
|
showSuccess(t('参数覆盖 JSON 已复制'));
|
||||||
|
} else {
|
||||||
|
showError(t('复制失败'));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const parseParamOverrideInput = () => {
|
||||||
|
const raw =
|
||||||
|
typeof inputs.param_override === 'string'
|
||||||
|
? inputs.param_override.trim()
|
||||||
|
: '';
|
||||||
|
if (!raw) return null;
|
||||||
|
if (!verifyJSON(raw)) {
|
||||||
|
throw new Error(t('当前参数覆盖不是合法的 JSON'));
|
||||||
|
}
|
||||||
|
return JSON.parse(raw);
|
||||||
|
};
|
||||||
|
|
||||||
|
const applyParamOverrideTemplate = (
|
||||||
|
templateType = 'operations',
|
||||||
|
applyMode = 'fill',
|
||||||
|
) => {
|
||||||
|
try {
|
||||||
|
const parsedCurrent = parseParamOverrideInput();
|
||||||
|
if (templateType === 'legacy') {
|
||||||
|
if (applyMode === 'fill') {
|
||||||
|
handleInputChange(
|
||||||
|
'param_override',
|
||||||
|
JSON.stringify(PARAM_OVERRIDE_LEGACY_TEMPLATE, null, 2),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const currentLegacy =
|
||||||
|
parsedCurrent &&
|
||||||
|
typeof parsedCurrent === 'object' &&
|
||||||
|
!Array.isArray(parsedCurrent) &&
|
||||||
|
!Array.isArray(parsedCurrent.operations)
|
||||||
|
? parsedCurrent
|
||||||
|
: {};
|
||||||
|
const merged = {
|
||||||
|
...PARAM_OVERRIDE_LEGACY_TEMPLATE,
|
||||||
|
...currentLegacy,
|
||||||
|
};
|
||||||
|
handleInputChange('param_override', JSON.stringify(merged, null, 2));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (applyMode === 'fill') {
|
||||||
|
handleInputChange(
|
||||||
|
'param_override',
|
||||||
|
JSON.stringify(PARAM_OVERRIDE_OPERATIONS_TEMPLATE, null, 2),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const currentOperations =
|
||||||
|
parsedCurrent &&
|
||||||
|
typeof parsedCurrent === 'object' &&
|
||||||
|
!Array.isArray(parsedCurrent) &&
|
||||||
|
Array.isArray(parsedCurrent.operations)
|
||||||
|
? parsedCurrent.operations
|
||||||
|
: [];
|
||||||
|
const merged = {
|
||||||
|
operations: [
|
||||||
|
...currentOperations,
|
||||||
|
...PARAM_OVERRIDE_OPERATIONS_TEMPLATE.operations,
|
||||||
|
],
|
||||||
|
};
|
||||||
|
handleInputChange('param_override', JSON.stringify(merged, null, 2));
|
||||||
|
} catch (error) {
|
||||||
|
showError(error.message || t('模板应用失败'));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const loadChannel = async () => {
|
const loadChannel = async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
let res = await API.get(`/api/channel/${channelId}`);
|
let res = await API.get(`/api/channel/${channelId}`);
|
||||||
@@ -3119,51 +3236,18 @@ const EditChannelModal = (props) => {
|
|||||||
<Button
|
<Button
|
||||||
size='small'
|
size='small'
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
handleInputChange(
|
applyParamOverrideTemplate('operations', 'fill')
|
||||||
'param_override',
|
|
||||||
JSON.stringify({ temperature: 0 }, null, 2),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{t('旧格式模板')}
|
{t('填充新模板')}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
size='small'
|
size='small'
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
handleInputChange(
|
applyParamOverrideTemplate('legacy', 'fill')
|
||||||
'param_override',
|
|
||||||
JSON.stringify(
|
|
||||||
{
|
|
||||||
operations: [
|
|
||||||
{
|
|
||||||
path: 'temperature',
|
|
||||||
mode: 'set',
|
|
||||||
value: 0.7,
|
|
||||||
conditions: [
|
|
||||||
{
|
|
||||||
path: 'model',
|
|
||||||
mode: 'prefix',
|
|
||||||
value: 'gpt',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
logic: 'AND',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
null,
|
|
||||||
2,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{t('新格式模板')}
|
{t('填充旧模板')}
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
size='small'
|
|
||||||
type='tertiary'
|
|
||||||
onClick={() => handleInputChange('param_override', '')}
|
|
||||||
>
|
|
||||||
{t('不更改')}
|
|
||||||
</Button>
|
</Button>
|
||||||
</Space>
|
</Space>
|
||||||
</div>
|
</div>
|
||||||
@@ -3171,20 +3255,33 @@ const EditChannelModal = (props) => {
|
|||||||
{t('此项可选,用于覆盖请求参数。不支持覆盖 stream 参数')}
|
{t('此项可选,用于覆盖请求参数。不支持覆盖 stream 参数')}
|
||||||
</Text>
|
</Text>
|
||||||
<div
|
<div
|
||||||
className='mt-2 rounded-lg border p-3'
|
className='mt-2 rounded-xl p-3'
|
||||||
style={{ backgroundColor: 'var(--semi-color-fill-0)' }}
|
style={{
|
||||||
|
backgroundColor: 'var(--semi-color-fill-0)',
|
||||||
|
border: '1px solid var(--semi-color-fill-2)',
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<div className='flex items-center justify-between mb-2'>
|
<div className='flex items-center justify-between mb-2'>
|
||||||
<Tag color={paramOverrideMeta.tagColor}>
|
<Tag color={paramOverrideMeta.tagColor}>
|
||||||
{paramOverrideMeta.tagLabel}
|
{paramOverrideMeta.tagLabel}
|
||||||
</Tag>
|
</Tag>
|
||||||
<Button
|
<Space spacing={8}>
|
||||||
size='small'
|
<Button
|
||||||
type='tertiary'
|
size='small'
|
||||||
onClick={() => setParamOverrideEditorVisible(true)}
|
icon={<IconCopy />}
|
||||||
>
|
type='tertiary'
|
||||||
{t('编辑')}
|
onClick={copyParamOverrideJson}
|
||||||
</Button>
|
>
|
||||||
|
{t('复制')}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size='small'
|
||||||
|
type='tertiary'
|
||||||
|
onClick={() => setParamOverrideEditorVisible(true)}
|
||||||
|
>
|
||||||
|
{t('编辑')}
|
||||||
|
</Button>
|
||||||
|
</Space>
|
||||||
</div>
|
</div>
|
||||||
<pre className='mb-0 text-xs leading-5 whitespace-pre-wrap break-all max-h-56 overflow-auto'>
|
<pre className='mb-0 text-xs leading-5 whitespace-pre-wrap break-all max-h-56 overflow-auto'>
|
||||||
{paramOverrideMeta.preview}
|
{paramOverrideMeta.preview}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user