mirror of
https://github.com/QuantumNous/new-api.git
synced 2026-04-18 04:27:28 +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 {
|
||||
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"`
|
||||
KeepOrigin bool `json:"keep_origin"`
|
||||
From string `json:"from,omitempty"`
|
||||
@@ -494,6 +494,19 @@ func applyOperations(jsonStr string, operations []ParamOperation, conditionConte
|
||||
if err == nil {
|
||||
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":
|
||||
result, err = syncFieldsBetweenTargets(result, context, op.From, op.To)
|
||||
if err == nil {
|
||||
@@ -678,6 +691,80 @@ func deleteHeaderOverrideInContext(context map[string]interface{}, headerName st
|
||||
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 {
|
||||
kind string
|
||||
key string
|
||||
|
||||
@@ -71,6 +71,7 @@ import {
|
||||
IconServer,
|
||||
IconSetting,
|
||||
IconCode,
|
||||
IconCopy,
|
||||
IconGlobe,
|
||||
IconBolt,
|
||||
IconSearch,
|
||||
@@ -95,6 +96,28 @@ const REGION_EXAMPLE = {
|
||||
'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([
|
||||
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 {
|
||||
tagLabel: 'Custom JSON',
|
||||
tagLabel: t('自定义 JSON'),
|
||||
tagColor: 'orange',
|
||||
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 () => {
|
||||
setLoading(true);
|
||||
let res = await API.get(`/api/channel/${channelId}`);
|
||||
@@ -3119,51 +3236,18 @@ const EditChannelModal = (props) => {
|
||||
<Button
|
||||
size='small'
|
||||
onClick={() =>
|
||||
handleInputChange(
|
||||
'param_override',
|
||||
JSON.stringify({ temperature: 0 }, null, 2),
|
||||
)
|
||||
applyParamOverrideTemplate('operations', 'fill')
|
||||
}
|
||||
>
|
||||
{t('旧格式模板')}
|
||||
{t('填充新模板')}
|
||||
</Button>
|
||||
<Button
|
||||
size='small'
|
||||
onClick={() =>
|
||||
handleInputChange(
|
||||
'param_override',
|
||||
JSON.stringify(
|
||||
{
|
||||
operations: [
|
||||
{
|
||||
path: 'temperature',
|
||||
mode: 'set',
|
||||
value: 0.7,
|
||||
conditions: [
|
||||
{
|
||||
path: 'model',
|
||||
mode: 'prefix',
|
||||
value: 'gpt',
|
||||
},
|
||||
],
|
||||
logic: 'AND',
|
||||
},
|
||||
],
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
)
|
||||
applyParamOverrideTemplate('legacy', 'fill')
|
||||
}
|
||||
>
|
||||
{t('新格式模板')}
|
||||
</Button>
|
||||
<Button
|
||||
size='small'
|
||||
type='tertiary'
|
||||
onClick={() => handleInputChange('param_override', '')}
|
||||
>
|
||||
{t('不更改')}
|
||||
{t('填充旧模板')}
|
||||
</Button>
|
||||
</Space>
|
||||
</div>
|
||||
@@ -3171,20 +3255,33 @@ const EditChannelModal = (props) => {
|
||||
{t('此项可选,用于覆盖请求参数。不支持覆盖 stream 参数')}
|
||||
</Text>
|
||||
<div
|
||||
className='mt-2 rounded-lg border p-3'
|
||||
style={{ backgroundColor: 'var(--semi-color-fill-0)' }}
|
||||
className='mt-2 rounded-xl p-3'
|
||||
style={{
|
||||
backgroundColor: 'var(--semi-color-fill-0)',
|
||||
border: '1px solid var(--semi-color-fill-2)',
|
||||
}}
|
||||
>
|
||||
<div className='flex items-center justify-between mb-2'>
|
||||
<Tag color={paramOverrideMeta.tagColor}>
|
||||
{paramOverrideMeta.tagLabel}
|
||||
</Tag>
|
||||
<Button
|
||||
size='small'
|
||||
type='tertiary'
|
||||
onClick={() => setParamOverrideEditorVisible(true)}
|
||||
>
|
||||
{t('编辑')}
|
||||
</Button>
|
||||
<Space spacing={8}>
|
||||
<Button
|
||||
size='small'
|
||||
icon={<IconCopy />}
|
||||
type='tertiary'
|
||||
onClick={copyParamOverrideJson}
|
||||
>
|
||||
{t('复制')}
|
||||
</Button>
|
||||
<Button
|
||||
size='small'
|
||||
type='tertiary'
|
||||
onClick={() => setParamOverrideEditorVisible(true)}
|
||||
>
|
||||
{t('编辑')}
|
||||
</Button>
|
||||
</Space>
|
||||
</div>
|
||||
<pre className='mb-0 text-xs leading-5 whitespace-pre-wrap break-all max-h-56 overflow-auto'>
|
||||
{paramOverrideMeta.preview}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user