diff --git a/web/src/components/table/channels/modals/EditChannelModal.jsx b/web/src/components/table/channels/modals/EditChannelModal.jsx index 2f240447..c2c72a0f 100644 --- a/web/src/components/table/channels/modals/EditChannelModal.jsx +++ b/web/src/components/table/channels/modals/EditChannelModal.jsx @@ -47,6 +47,8 @@ import { Highlight, Input, Tooltip, + Collapse, + Dropdown, } from '@douyinfe/semi-ui'; import { getChannelModels, @@ -80,7 +82,6 @@ import { IconGlobe, IconBolt, IconSearch, - IconChevronUp, IconChevronDown, } from '@douyinfe/semi-icons'; @@ -124,8 +125,6 @@ const PARAM_OVERRIDE_OPERATIONS_TEMPLATE = { ], }; -const DEPRECATED_DOUBAO_CODING_PLAN_BASE_URL = 'doubao-coding-plan'; - // 支持并且已适配通过接口获取模型列表的渠道类型 const MODEL_FETCHABLE_TYPES = new Set([ 1, 4, 14, 34, 17, 26, 27, 24, 47, 25, 20, 23, 31, 40, 42, 48, 43, @@ -397,39 +396,13 @@ const EditChannelModal = (props) => { [], ); - // 表单块导航相关状态 - const formSectionRefs = useRef({ - basicInfo: null, - apiConfig: null, - modelConfig: null, - advancedSettings: null, - channelExtraSettings: null, - }); - const [currentSectionIndex, setCurrentSectionIndex] = useState(0); - const formSections = [ - 'basicInfo', - 'apiConfig', - 'modelConfig', - 'advancedSettings', - 'channelExtraSettings', - ]; + // 高级设置折叠状态 + const [advancedSettingsOpen, setAdvancedSettingsOpen] = useState(false); const formContainerRef = useRef(null); const doubaoApiClickCountRef = useRef(0); - const initialBaseUrlRef = useRef(''); const initialModelsRef = useRef([]); const initialModelMappingRef = useRef(''); const initialStatusCodeMappingRef = useRef(''); - const doubaoCodingPlanDeprecationMessage = - 'Doubao Coding Plan 不再允许新增。根据火山方舟文档,Coding 套餐额度仅适用于 AI Coding 产品内调用,不适用于单独 API 调用;在非 AI Coding 产品中使用对应的 Base URL 和 API Key 可能被视为违规,并可能导致订阅停用或账号封禁。'; - const canKeepDeprecatedDoubaoCodingPlan = - initialBaseUrlRef.current === DEPRECATED_DOUBAO_CODING_PLAN_BASE_URL; - const doubaoCodingPlanOptionLabel = ( - - - Doubao Coding Plan - - - ); // 2FA状态更新辅助函数 const updateTwoFAState = (updates) => { @@ -481,43 +454,6 @@ const EditChannelModal = (props) => { setVerifyLoading(false); }; - // 表单导航功能 - const scrollToSection = (sectionKey) => { - const sectionElement = formSectionRefs.current[sectionKey]; - if (sectionElement) { - sectionElement.scrollIntoView({ - behavior: 'smooth', - block: 'start', - inline: 'nearest', - }); - } - }; - - const navigateToSection = (direction) => { - const availableSections = formSections.filter((section) => { - if (section === 'apiConfig') { - return showApiConfigCard; - } - return true; - }); - - let newIndex; - if (direction === 'up') { - newIndex = - currentSectionIndex > 0 - ? currentSectionIndex - 1 - : availableSections.length - 1; - } else { - newIndex = - currentSectionIndex < availableSections.length - 1 - ? currentSectionIndex + 1 - : 0; - } - - setCurrentSectionIndex(newIndex); - scrollToSection(availableSections[newIndex]); - }; - const handleApiConfigSecretClick = () => { if (inputs.type !== 45) return; const next = doubaoApiClickCountRef.current + 1; @@ -960,7 +896,6 @@ const EditChannelModal = (props) => { data.base_url = 'https://ark.cn-beijing.volces.com'; } - initialBaseUrlRef.current = data.base_url || ''; setInputs(data); if (formApiRef.current) { formApiRef.current.setValues(data); @@ -1006,7 +941,27 @@ const EditChannelModal = (props) => { const managedByIonet = !!parsedIonet; setIsIonetChannel(managedByIonet); setIonetMetadata(parsedIonet); - // console.log(data); + + // Smart expand: auto-open advanced settings if any advanced field has a value + const hasAdvancedValues = + (data.model_mapping && data.model_mapping.trim()) || + (data.param_override && data.param_override.trim()) || + (data.status_code_mapping && data.status_code_mapping.trim()) || + (data.header_override && data.header_override.trim()) || + (data.tag && data.tag.trim()) || + (data.remark && data.remark.trim()) || + (data.priority && data.priority !== 0) || + (data.weight && data.weight !== 0) || + (data.proxy && data.proxy.trim()) || + (data.system_prompt && data.system_prompt.trim()) || + data.thinking_to_content || + data.pass_through_body_enabled || + data.force_format || + data.claude_beta_query || + data.system_prompt_override; + if (hasAdvancedValues) { + setAdvancedSettingsOpen(true); + } } else { showError(message); } @@ -1275,7 +1230,6 @@ const EditChannelModal = (props) => { fetchModels().then(); fetchGroups().then(); if (!isEdit) { - initialBaseUrlRef.current = ''; setInputs(originInputs); if (formApiRef.current) { formApiRef.current.setValues(originInputs); @@ -1303,8 +1257,8 @@ const EditChannelModal = (props) => { fetchModelGroups(); // 重置手动输入模式状态 setUseManualInput(false); - // 重置导航状态 - setCurrentSectionIndex(0); + // 重置高级设置折叠状态 + setAdvancedSettingsOpen(false); } else { // 统一的模态框关闭重置逻辑 resetModalState(); @@ -1349,6 +1303,8 @@ const EditChannelModal = (props) => { setDoubaoApiEditUnlocked(false); doubaoApiClickCountRef.current = 0; setModelSearchValue(''); + // 重置高级设置折叠状态 + setAdvancedSettingsOpen(false); // 清空表单中的key_mode字段 if (formApiRef.current) { formApiRef.current.setValue('key_mode', undefined); @@ -2118,58 +2074,22 @@ const EditChannelModal = (props) => { visible={props.visible} width={isMobile ? '100%' : 600} footer={ -
-
-
- - - - +
+ +
} closeIcon={null} @@ -2181,29 +2101,350 @@ const EditChannelModal = (props) => { getFormApi={(api) => (formApiRef.current = api)} onSubmit={submit} > - {() => ( - -
-
(formSectionRefs.current.basicInfo = el)}> - - {/* Header: Basic Info */} -
- - - -
- - {t('基本信息')} - -
- {t('渠道的基本配置信息')} + {() => { + const advancedSettingsContent = ( +
+ {/* Upstream Model Management Section */} + {MODEL_FETCHABLE_CHANNEL_TYPES.has(inputs.type) && ( +
+ + {t('上游模型管理')} + + + + handleChannelOtherSettingsChange( + 'upstream_model_update_check_enabled', + value, + ) + } + extraText={t( + '开启后由后端定时任务检测该渠道上游模型变化', + )} + /> + + handleChannelOtherSettingsChange('upstream_model_update_auto_sync_enabled', value) + } + extraText={t('开启后检测到新增模型会自动加入当前渠道模型列表')} + /> + + handleInputChange( + 'upstream_model_update_ignored_models', + value, + ) + } + showClear + /> +
+ {t('上次检测时间')}:  + {formatUnixTime( + inputs.upstream_model_update_last_check_time, + )} +
+
+ {t('上次检测到可加入模型')}:  + {upstreamDetectedModels.length === 0 ? ( + t('暂无') + ) : ( + <> + + {upstreamDetectedModels.join(', ')} +
+ } + > + + {upstreamDetectedModelsPreview.join(', ')} + + + + {upstreamDetectedModelsOmittedCount > 0 + ? t('(共 {{total}} 个,省略 {{omit}} 个)', { + total: upstreamDetectedModels.length, + omit: upstreamDetectedModelsOmittedCount, + }) + : t('(共 {{total}} 个)', { + total: upstreamDetectedModels.length, + })} + + + )} +
+
+ )} + + {/* Request Config Section */} +
+ + {t('请求配置')} + + +
+
+ {t('参数覆盖')} + + + applyParamOverrideTemplate('operations', 'fill') }, + { node: 'item', name: t('填充旧模板'), onClick: () => applyParamOverrideTemplate('legacy', 'fill') }, + { node: 'item', name: t('清空'), onClick: clearParamOverride }, + ]} + > + + + +
+ + {t('此项可选,用于覆盖请求参数。不支持覆盖 stream 参数')} + +
+
+ + {paramOverrideMeta.tagLabel} + + +
+
+                        {paramOverrideMeta.preview}
+                      
+
+
+ + + handleInputChange('header_override', value) + } + extraText={ +
+
+ + handleInputChange( + 'header_override', + JSON.stringify({ '*': true, 're:^X-Trace-.*$': true, 'X-Foo': '{client_header:X-Foo}', Authorization: 'Bearer {api_key}', 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36 Edg/139.0.0.0' }, null, 2), + ) + } + > + {t('填入模板')} + + + handleInputChange('header_override', JSON.stringify({ '*': true }, null, 2)) + } + > + {t('填入透传模版')} + + formatJsonField('header_override')} + > + {t('格式化')} + +
+
+ + {t('支持变量:')} + +
+
+ {t('渠道密钥')}: {'{api_key}'} +
+
+ } + showClear + /> + + handleInputChange('status_code_mapping', value) + } + template={STATUS_CODE_MAPPING_EXAMPLE} + templateLabel={t('填入模板')} + editorType='keyValue' + formApi={formApiRef.current} + extraText={t('键为原状态码,值为要复写的状态码,仅影响本地判断')} + /> +
+ + {/* Channel Behavior Section */} +
+ + {t('渠道行为')} + + + handleInputChange('tag', value)} + /> + handleInputChange('remark', value)} + /> + + + + handleInputChange('priority', value)} + style={{ width: '100%' }} + /> + + + handleInputChange('weight', value)} + style={{ width: '100%' }} + /> + + + + {inputs.type === 1 && ( + <> +
+ {t('字段透传控制')} +
+ handleChannelOtherSettingsChange('allow_service_tier', value)} extraText={t('service_tier 字段用于指定服务层级,允许透传可能导致实际计费高于预期。默认关闭以避免额外费用')} /> + handleChannelOtherSettingsChange('disable_store', value)} extraText={t('store 字段用于授权 OpenAI 存储请求数据以评估和优化产品。默认关闭,开启后可能导致 Codex 无法正常使用')} /> + handleChannelOtherSettingsChange('allow_safety_identifier', value)} extraText={t('safety_identifier 字段用于帮助 OpenAI 识别可能违反使用政策的应用程序用户。默认关闭以保护用户隐私')} /> + handleChannelOtherSettingsChange('allow_include_obfuscation', value)} extraText={t('include_obfuscation 用于控制 Responses 流混淆字段。默认关闭以避免客户端关闭该安全保护')} /> + + )} + + {inputs.type === 14 && ( + <> +
+ {t('字段透传控制')} +
+ handleChannelOtherSettingsChange('allow_service_tier', value)} extraText={t('service_tier 字段用于指定服务层级,允许透传可能导致实际计费高于预期。默认关闭以避免额外费用')} /> + handleChannelOtherSettingsChange('allow_inference_geo', value)} extraText={t('inference_geo 字段用于控制 Claude 数据驻留推理区域。默认关闭以避免未经授权透传地域信息')} /> + + )} +
+ + {/* Extra Settings Section */} +
+ + {t('额外设置')} + + + {inputs.type === 14 && ( + handleChannelOtherSettingsChange('claude_beta_query', value)} extraText={t('开启后,该渠道请求 Claude 时将强制追加 ?beta=true(无需客户端手动传参)')} /> + )} + + {inputs.type === 1 && ( + handleChannelSettingsChange('force_format', value)} extraText={t('强制将响应格式化为 OpenAI 标准格式(只适用于OpenAI渠道类型)')} /> + )} + + handleChannelSettingsChange('thinking_to_content', value)} extraText={t('将 reasoning_content 转换为 标签拼接到内容中')} /> + handleChannelSettingsChange('pass_through_body_enabled', value)} extraText={t('启用请求体透传功能')} /> + + handleChannelSettingsChange('proxy', value)} showClear extraText={t('用于配置网络代理,支持 socks5 协议')} /> + + handleChannelSettingsChange('system_prompt', value)} autosize showClear extraText={t('用户优先:如果用户在请求中指定了系统提示词,将优先使用用户的设置')} /> + handleChannelSettingsChange('system_prompt_override', value)} extraText={t('如果用户请求中包含系统提示词,则使用此设置拼接到用户的系统提示词前面')} /> +
+
+ ); + + return ( + <> + +
+ {/* Core Configuration Card - Always Visible */} + + {/* Header */} +
+ + + +
+ + {t('核心配置')} + +
+ {t('创建渠道所需的基本信息')} +
+
{isIonetChannel && ( { } /> )} -
-
- {/* API Configuration Card */} - {showApiConfigCard && ( -
(formSectionRefs.current.apiConfig = el)}> - - {/* Header: API Config */} -
- - - -
- - {t('API 配置')} - -
- {t('API 地址和相关配置')} -
-
-
+ {/* API Configuration Section */} + {showApiConfigCard && ( +
{inputs.type === 40 && ( { 'https://ark.ap-southeast.bytepluses.com', }, { - value: DEPRECATED_DOUBAO_CODING_PLAN_BASE_URL, - label: doubaoCodingPlanOptionLabel, - disabled: !canKeepDeprecatedDoubaoCodingPlan, + value: 'doubao-coding-plan', + label: 'Doubao Coding Plan', }, ]} defaultValue='https://ark.cn-beijing.volces.com' @@ -3088,33 +3304,11 @@ const EditChannelModal = (props) => { />
)} -
-
- )} - - {/* Model Configuration Card */} -
(formSectionRefs.current.modelConfig = el)}> - - {/* Header: Model Config */} -
- - - -
- - {t('模型配置')} - -
- {t('模型选择和映射设置')} -
-
+ )} - { }; }} extraText={ - + - {MODEL_FETCHABLE_CHANNEL_TYPES.has(inputs.type) && ( )} - {inputs.type === 4 && isEdit && ( - - )} - + + + + } + /> + + {/* Custom Model Name - Core Config */} + setCustomModel(value.trim())} + value={customModel} + suffix={ + + } + /> + + {/* Groups - Core Config */} + handleInputChange('groups', value)} + /> + + {/* Model Mapping - Core Config */} + + handleInputChange('model_mapping', value) + } + template={MODEL_MAPPING_EXAMPLE} + templateLabel={t('填入模板')} + editorType='keyValue' + formApi={formApiRef.current} + renderStringValueSuffix={({ pairKey, value }) => { + if (!MODEL_FETCHABLE_CHANNEL_TYPES.has(inputs.type)) { + return null; + } + const disabled = !String(pairKey ?? '').trim(); + return ( + - {modelGroups && - modelGroups.length > 0 && - modelGroups.map((group) => ( - - ))} - - } - /> - - setCustomModel(value.trim())} - value={customModel} - suffix={ - - } - /> - - {MODEL_FETCHABLE_CHANNEL_TYPES.has(inputs.type) && ( - <> - - handleChannelOtherSettingsChange( - 'upstream_model_update_check_enabled', - value, - ) - } - extraText={t( - '开启后由后端定时任务检测该渠道上游模型变化', - )} - /> -
- {t('上次检测时间')}:  - {formatUnixTime( - inputs.upstream_model_update_last_check_time, - )} -
- - handleInputChange( - 'upstream_model_update_ignored_models', - value, - ) - } - showClear - /> - + /> + + ); + }} + extraText={t( + '键为请求中的模型名称,值为要替换的模型名称', )} + /> - - handleInputChange('test_model', value) - } - showClear - /> + {/* Auto Ban - Core Config */} + setAutoBan(value)} + extraText={t( + '仅当自动禁用开启时有效,关闭后不会自动禁用该渠道', + )} + initValue={autoBan} + /> - - handleInputChange('model_mapping', value) - } - template={MODEL_MAPPING_EXAMPLE} - templateLabel={t('填入模板')} - editorType='keyValue' - formApi={formApiRef.current} - renderStringValueSuffix={({ pairKey, value }) => { - if (!MODEL_FETCHABLE_CHANNEL_TYPES.has(inputs.type)) { - return null; - } - const disabled = !String(pairKey ?? '').trim(); - return ( - -
+ {/* Test Model - Core Config */} + + handleInputChange('test_model', value) + } + showClear + /> + - {/* Advanced Settings Card */} -
(formSectionRefs.current.advancedSettings = el)} + {/* Advanced Settings Toggle / Collapse */} + {isMobile ? ( + setAdvancedSettingsOpen(keys.includes('advanced'))} > - - {/* Header: Advanced Settings */} -
- + - -
- - {t('高级设置')} - -
- {t('渠道的高级配置选项')} -
+ {t('高级设置')}
+ } + itemKey='advanced' + > + {advancedSettingsContent} + + + ) : ( + /* Desktop: toggle button to open side panel */ +
setAdvancedSettingsOpen(!advancedSettingsOpen)} + > +
+ + {t('高级设置')}
- - handleInputChange('groups', value)} - /> - - handleInputChange('tag', value)} - /> - handleInputChange('remark', value)} - /> - - - - - handleInputChange('priority', value) - } - style={{ width: '100%' }} - /> - - - - handleInputChange('weight', value) - } - style={{ width: '100%' }} - /> - - - - setAutoBan(value)} - extraText={t( - '仅当自动禁用开启时有效,关闭后不会自动禁用该渠道', - )} - initValue={autoBan} - /> - - - handleChannelOtherSettingsChange( - 'upstream_model_update_auto_sync_enabled', - value, - ) - } - extraText={t( - '开启后检测到新增模型会自动加入当前渠道模型列表', - )} - /> - -
- {t('上次检测到可加入模型')}:  - {upstreamDetectedModels.length === 0 ? ( - t('暂无') - ) : ( - <> - - {upstreamDetectedModels.join(', ')} -
- } - > - - {upstreamDetectedModelsPreview.join(', ')} - - - - {upstreamDetectedModelsOmittedCount > 0 - ? t('(共 {{total}} 个,省略 {{omit}} 个)', { - total: upstreamDetectedModels.length, - omit: upstreamDetectedModelsOmittedCount, - }) - : t('(共 {{total}} 个)', { - total: upstreamDetectedModels.length, - })} - - - )} -
- -
-
- {t('参数覆盖')} - - - - - - -
- - {t('此项可选,用于覆盖请求参数。不支持覆盖 stream 参数')} +
+ + {advancedSettingsOpen ? t('收起') : isEdit ? t('向左展开') : t('向右展开')} -
-
- - {paramOverrideMeta.tagLabel} - - - - - -
-
-                          {paramOverrideMeta.preview}
-                        
-
-
- - - handleInputChange('header_override', value) - } - extraText={ -
-
- - handleInputChange( - 'header_override', - JSON.stringify( - { - '*': true, - 're:^X-Trace-.*$': true, - 'X-Foo': '{client_header:X-Foo}', - Authorization: 'Bearer {api_key}', - 'User-Agent': - 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36 Edg/139.0.0.0', - }, - null, - 2, - ), - ) - } - > - {t('填入模板')} - - - handleInputChange( - 'header_override', - JSON.stringify( - { - '*': true, - }, - null, - 2, - ), - ) - } - > - {t('填入透传模版')} - - formatJsonField('header_override')} - > - {t('格式化')} - -
-
- - {t('支持变量:')} - -
-
- {t('渠道密钥')}: {'{api_key}'} -
-
-
-
- } - showClear - /> - - handleInputChange('status_code_mapping', value) - } - template={STATUS_CODE_MAPPING_EXAMPLE} - templateLabel={t('填入模板')} - editorType='keyValue' - formApi={formApiRef.current} - extraText={t( - '键为原状态码,值为要复写的状态码,仅影响本地判断', - )} - /> - - {/* 字段透传控制 - OpenAI 渠道 */} - {inputs.type === 1 && ( - <> -
- {t('字段透传控制')} -
- - - handleChannelOtherSettingsChange( - 'allow_service_tier', - value, - ) - } - extraText={t( - 'service_tier 字段用于指定服务层级,允许透传可能导致实际计费高于预期。默认关闭以避免额外费用', - )} - /> - - - handleChannelOtherSettingsChange( - 'disable_store', - value, - ) - } - extraText={t( - 'store 字段用于授权 OpenAI 存储请求数据以评估和优化产品。默认关闭,开启后可能导致 Codex 无法正常使用', - )} - /> - - - handleChannelOtherSettingsChange( - 'allow_safety_identifier', - value, - ) - } - extraText={t( - 'safety_identifier 字段用于帮助 OpenAI 识别可能违反使用政策的应用程序用户。默认关闭以保护用户隐私', - )} - /> - - - handleChannelOtherSettingsChange( - 'allow_include_obfuscation', - value, - ) - } - extraText={t( - 'include_obfuscation 用于控制 Responses 流混淆字段。默认关闭以避免客户端关闭该安全保护', - )} - /> - - )} - - {/* 字段透传控制 - Claude 渠道 */} - {inputs.type === 14 && ( - <> -
- {t('字段透传控制')} -
- - - handleChannelOtherSettingsChange( - 'allow_service_tier', - value, - ) - } - extraText={t( - 'service_tier 字段用于指定服务层级,允许透传可能导致实际计费高于预期。默认关闭以避免额外费用', - )} - /> - - - handleChannelOtherSettingsChange( - 'allow_inference_geo', - value, - ) - } - extraText={t( - 'inference_geo 字段用于控制 Claude 数据驻留推理区域。默认关闭以避免未经授权透传地域信息', - )} - /> - - )} - -
- - {/* Channel Extra Settings Card */} -
- (formSectionRefs.current.channelExtraSettings = el) - } - > - - {/* Header: Channel Extra Settings */} -
- - - -
- - {t('渠道额外设置')} - -
-
- - {inputs.type === 14 && ( - - handleChannelOtherSettingsChange( - 'claude_beta_query', - value, - ) - } - extraText={t( - '开启后,该渠道请求 Claude 时将强制追加 ?beta=true(无需客户端手动传参)', - )} + - )} - - {inputs.type === 1 && ( - - handleChannelSettingsChange('force_format', value) - } - extraText={t( - '强制将响应格式化为 OpenAI 标准格式(只适用于OpenAI渠道类型)', - )} - /> - )} - - - handleChannelSettingsChange( - 'thinking_to_content', - value, - ) - } - extraText={t( - '将 reasoning_content 转换为 标签拼接到内容中', - )} - /> - - - handleChannelSettingsChange( - 'pass_through_body_enabled', - value, - ) - } - extraText={t('启用请求体透传功能')} - /> - - - handleChannelSettingsChange('proxy', value) - } - showClear - extraText={t('用于配置网络代理,支持 socks5 协议')} - /> - - - handleChannelSettingsChange('system_prompt', value) - } - autosize - showClear - extraText={t( - '用户优先:如果用户在请求中指定了系统提示词,将优先使用用户的设置', - )} - /> - - handleChannelSettingsChange( - 'system_prompt_override', - value, - ) - } - extraText={t( - '如果用户请求中包含系统提示词,则使用此设置拼接到用户的系统提示词前面', - )} - /> -
-
+
+
+ )}
- )} + + {/* Desktop: Advanced Settings Side Panel - rendered inside Form tree */} + {!isMobile && advancedSettingsOpen && ( +
+
+
+ + + {t('高级')} + + + {t('高级设置')} + + +
+
+
+
+ +
+ + + +
+ + {t('高级设置')} + +
+ {t('渠道的高级配置选项')} +
+
+
+ {advancedSettingsContent} +
+
+
+
+ )} + + ); + }} +