Commit 7399e472 by CaIon

feat: add slide-in animations and update translations for new UI elements

# Conflicts:
#	web/src/components/table/channels/modals/EditChannelModal.jsx
parent a5e20269
...@@ -47,6 +47,8 @@ import { ...@@ -47,6 +47,8 @@ import {
Highlight, Highlight,
Input, Input,
Tooltip, Tooltip,
Collapse,
Dropdown,
} from '@douyinfe/semi-ui'; } from '@douyinfe/semi-ui';
import { import {
getChannelModels, getChannelModels,
...@@ -80,7 +82,6 @@ import { ...@@ -80,7 +82,6 @@ import {
IconGlobe, IconGlobe,
IconBolt, IconBolt,
IconSearch, IconSearch,
IconChevronUp,
IconChevronDown, IconChevronDown,
} from '@douyinfe/semi-icons'; } from '@douyinfe/semi-icons';
...@@ -124,8 +125,6 @@ const PARAM_OVERRIDE_OPERATIONS_TEMPLATE = { ...@@ -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([ 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,
...@@ -397,39 +396,13 @@ const EditChannelModal = (props) => { ...@@ -397,39 +396,13 @@ const EditChannelModal = (props) => {
[], [],
); );
// 表单块导航相关状态 // 高级设置折叠状态
const formSectionRefs = useRef({ const [advancedSettingsOpen, setAdvancedSettingsOpen] = useState(false);
basicInfo: null,
apiConfig: null,
modelConfig: null,
advancedSettings: null,
channelExtraSettings: null,
});
const [currentSectionIndex, setCurrentSectionIndex] = useState(0);
const formSections = [
'basicInfo',
'apiConfig',
'modelConfig',
'advancedSettings',
'channelExtraSettings',
];
const formContainerRef = useRef(null); const formContainerRef = useRef(null);
const doubaoApiClickCountRef = useRef(0); const doubaoApiClickCountRef = useRef(0);
const initialBaseUrlRef = useRef('');
const initialModelsRef = useRef([]); const initialModelsRef = useRef([]);
const initialModelMappingRef = useRef(''); const initialModelMappingRef = useRef('');
const initialStatusCodeMappingRef = 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 = (
<Tooltip content={doubaoCodingPlanDeprecationMessage} position='left'>
<span className='inline-flex items-center gap-2'>
<span>Doubao Coding Plan</span>
</span>
</Tooltip>
);
// 2FA状态更新辅助函数 // 2FA状态更新辅助函数
const updateTwoFAState = (updates) => { const updateTwoFAState = (updates) => {
...@@ -481,43 +454,6 @@ const EditChannelModal = (props) => { ...@@ -481,43 +454,6 @@ const EditChannelModal = (props) => {
setVerifyLoading(false); 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 = () => { const handleApiConfigSecretClick = () => {
if (inputs.type !== 45) return; if (inputs.type !== 45) return;
const next = doubaoApiClickCountRef.current + 1; const next = doubaoApiClickCountRef.current + 1;
...@@ -960,7 +896,6 @@ const EditChannelModal = (props) => { ...@@ -960,7 +896,6 @@ const EditChannelModal = (props) => {
data.base_url = 'https://ark.cn-beijing.volces.com'; data.base_url = 'https://ark.cn-beijing.volces.com';
} }
initialBaseUrlRef.current = data.base_url || '';
setInputs(data); setInputs(data);
if (formApiRef.current) { if (formApiRef.current) {
formApiRef.current.setValues(data); formApiRef.current.setValues(data);
...@@ -1006,7 +941,27 @@ const EditChannelModal = (props) => { ...@@ -1006,7 +941,27 @@ const EditChannelModal = (props) => {
const managedByIonet = !!parsedIonet; const managedByIonet = !!parsedIonet;
setIsIonetChannel(managedByIonet); setIsIonetChannel(managedByIonet);
setIonetMetadata(parsedIonet); 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 { } else {
showError(message); showError(message);
} }
...@@ -1275,7 +1230,6 @@ const EditChannelModal = (props) => { ...@@ -1275,7 +1230,6 @@ const EditChannelModal = (props) => {
fetchModels().then(); fetchModels().then();
fetchGroups().then(); fetchGroups().then();
if (!isEdit) { if (!isEdit) {
initialBaseUrlRef.current = '';
setInputs(originInputs); setInputs(originInputs);
if (formApiRef.current) { if (formApiRef.current) {
formApiRef.current.setValues(originInputs); formApiRef.current.setValues(originInputs);
...@@ -1303,8 +1257,8 @@ const EditChannelModal = (props) => { ...@@ -1303,8 +1257,8 @@ const EditChannelModal = (props) => {
fetchModelGroups(); fetchModelGroups();
// 重置手动输入模式状态 // 重置手动输入模式状态
setUseManualInput(false); setUseManualInput(false);
// 重置导航状态 // 重置高级设置折叠状态
setCurrentSectionIndex(0); setAdvancedSettingsOpen(false);
} else { } else {
// 统一的模态框关闭重置逻辑 // 统一的模态框关闭重置逻辑
resetModalState(); resetModalState();
...@@ -1349,6 +1303,8 @@ const EditChannelModal = (props) => { ...@@ -1349,6 +1303,8 @@ const EditChannelModal = (props) => {
setDoubaoApiEditUnlocked(false); setDoubaoApiEditUnlocked(false);
doubaoApiClickCountRef.current = 0; doubaoApiClickCountRef.current = 0;
setModelSearchValue(''); setModelSearchValue('');
// 重置高级设置折叠状态
setAdvancedSettingsOpen(false);
// 清空表单中的key_mode字段 // 清空表单中的key_mode字段
if (formApiRef.current) { if (formApiRef.current) {
formApiRef.current.setValue('key_mode', undefined); formApiRef.current.setValue('key_mode', undefined);
...@@ -2118,58 +2074,22 @@ const EditChannelModal = (props) => { ...@@ -2118,58 +2074,22 @@ const EditChannelModal = (props) => {
visible={props.visible} visible={props.visible}
width={isMobile ? '100%' : 600} width={isMobile ? '100%' : 600}
footer={ footer={
<div className='flex justify-between items-center bg-white'> <div className='flex justify-end items-center gap-2'>
<div className='flex gap-2'> <Button
<Button theme='solid'
size='small' onClick={() => formApiRef.current?.submitForm()}
type='tertiary' icon={<IconSave />}
icon={<IconChevronUp />} >
onClick={() => navigateToSection('up')} {t('提交')}
style={{ </Button>
borderRadius: '50%', <Button
width: '32px', theme='light'
height: '32px', type='primary'
padding: 0, onClick={handleCancel}
display: 'flex', icon={<IconClose />}
alignItems: 'center', >
justifyContent: 'center', {t('取消')}
}} </Button>
title={t('上一个表单块')}
/>
<Button
size='small'
type='tertiary'
icon={<IconChevronDown />}
onClick={() => navigateToSection('down')}
style={{
borderRadius: '50%',
width: '32px',
height: '32px',
padding: 0,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
title={t('下一个表单块')}
/>
</div>
<Space>
<Button
theme='solid'
onClick={() => formApiRef.current?.submitForm()}
icon={<IconSave />}
>
{t('提交')}
</Button>
<Button
theme='light'
type='primary'
onClick={handleCancel}
icon={<IconClose />}
>
{t('取消')}
</Button>
</Space>
</div> </div>
} }
closeIcon={null} closeIcon={null}
...@@ -2181,29 +2101,350 @@ const EditChannelModal = (props) => { ...@@ -2181,29 +2101,350 @@ const EditChannelModal = (props) => {
getFormApi={(api) => (formApiRef.current = api)} getFormApi={(api) => (formApiRef.current = api)}
onSubmit={submit} onSubmit={submit}
> >
{() => ( {() => {
const advancedSettingsContent = (
<div className='space-y-4'>
{/* Upstream Model Management Section */}
{MODEL_FETCHABLE_CHANNEL_TYPES.has(inputs.type) && (
<div className='pb-3 border-b border-gray-100'>
<Text className='text-sm font-medium text-gray-500 mb-3 block'>
{t('上游模型管理')}
</Text>
<Form.Switch
field='upstream_model_update_check_enabled'
label={t('是否检测上游模型更新')}
checkedText={t('开')}
uncheckedText={t('关')}
onChange={(value) =>
handleChannelOtherSettingsChange(
'upstream_model_update_check_enabled',
value,
)
}
extraText={t(
'开启后由后端定时任务检测该渠道上游模型变化',
)}
/>
<Form.Switch
field='upstream_model_update_auto_sync_enabled'
label={t('是否自动同步上游模型更新')}
checkedText={t('开')}
uncheckedText={t('关')}
disabled={!inputs.upstream_model_update_check_enabled}
onChange={(value) =>
handleChannelOtherSettingsChange('upstream_model_update_auto_sync_enabled', value)
}
extraText={t('开启后检测到新增模型会自动加入当前渠道模型列表')}
/>
<Form.Input
field='upstream_model_update_ignored_models'
label={t('已忽略模型')}
placeholder={t('例如:gpt-4.1-nano,gpt-4o-mini')}
onChange={(value) =>
handleInputChange(
'upstream_model_update_ignored_models',
value,
)
}
showClear
/>
<div className='text-xs text-gray-500 mb-2'>
{t('上次检测时间')}:&nbsp;
{formatUnixTime(
inputs.upstream_model_update_last_check_time,
)}
</div>
<div className='text-xs text-gray-500 mb-3'>
{t('上次检测到可加入模型')}:&nbsp;
{upstreamDetectedModels.length === 0 ? (
t('暂无')
) : (
<>
<Tooltip
position='topLeft'
content={
<div className='max-w-[640px] break-all text-xs leading-5'>
{upstreamDetectedModels.join(', ')}
</div>
}
>
<span className='cursor-help break-all'>
{upstreamDetectedModelsPreview.join(', ')}
</span>
</Tooltip>
<span className='ml-1 text-gray-400'>
{upstreamDetectedModelsOmittedCount > 0
? t('(共 {{total}} 个,省略 {{omit}} 个)', {
total: upstreamDetectedModels.length,
omit: upstreamDetectedModelsOmittedCount,
})
: t('(共 {{total}} 个)', {
total: upstreamDetectedModels.length,
})}
</span>
</>
)}
</div>
</div>
)}
{/* Request Config Section */}
<div className='py-3 border-b border-gray-100'>
<Text className='text-sm font-medium text-gray-500 mb-3 block'>
{t('请求配置')}
</Text>
<div className='mb-4'>
<div className='flex items-center justify-between gap-2 mb-1'>
<Text className='text-sm font-medium'>{t('参数覆盖')}</Text>
<Space>
<Button
size='small'
type='primary'
icon={<IconCode size={14} />}
onClick={() => setParamOverrideEditorVisible(true)}
>
{t('可视化编辑')}
</Button>
<Dropdown
trigger='click'
position='bottomRight'
menu={[
{ node: 'item', name: t('填充新模板'), onClick: () => applyParamOverrideTemplate('operations', 'fill') },
{ node: 'item', name: t('填充旧模板'), onClick: () => applyParamOverrideTemplate('legacy', 'fill') },
{ node: 'item', name: t('清空'), onClick: clearParamOverride },
]}
>
<Button size='small' type='tertiary'>
{t('更多')} <IconChevronDown size={12} />
</Button>
</Dropdown>
</Space>
</div>
<Text type='tertiary' size='small'>
{t('此项可选,用于覆盖请求参数。不支持覆盖 stream 参数')}
</Text>
<div
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'
icon={<IconCopy />}
type='tertiary'
onClick={copyParamOverrideJson}
>
{t('复制')}
</Button>
</div>
<pre className='mb-0 text-xs leading-5 whitespace-pre-wrap break-all max-h-56 overflow-auto'>
{paramOverrideMeta.preview}
</pre>
</div>
</div>
<Form.TextArea
field='header_override'
label={t('请求头覆盖')}
placeholder={
t('此项可选,用于覆盖请求头参数') +
'\n' +
t('格式示例:') +
'\n{\n "User-Agent": "Mozilla/5.0 ...",\n "Authorization": "Bearer {api_key}"\n}'
}
autosize
onChange={(value) =>
handleInputChange('header_override', value)
}
extraText={
<div className='flex flex-col gap-1'>
<div className='flex gap-2 flex-wrap items-center'>
<Text
className='!text-semi-color-primary cursor-pointer'
onClick={() =>
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('填入模板')}
</Text>
<Text
className='!text-semi-color-primary cursor-pointer'
onClick={() =>
handleInputChange('header_override', JSON.stringify({ '*': true }, null, 2))
}
>
{t('填入透传模版')}
</Text>
<Text
className='!text-semi-color-primary cursor-pointer'
onClick={() => formatJsonField('header_override')}
>
{t('格式化')}
</Text>
</div>
<div>
<Text type='tertiary' size='small'>
{t('支持变量:')}
</Text>
<div className='text-xs text-tertiary ml-2'>
<div>
{t('渠道密钥')}: {'{api_key}'}
</div>
</div>
</div>
</div>
}
showClear
/>
<JSONEditor
key={`status_code_mapping-${isEdit ? channelId : 'new'}`}
field='status_code_mapping'
label={t('状态码复写')}
placeholder={
t('此项可选,用于复写返回的状态码,仅影响本地判断,不修改返回到上游的状态码,比如将claude渠道的400错误复写为500(用于重试),请勿滥用该功能,例如:') +
'\n' +
JSON.stringify(STATUS_CODE_MAPPING_EXAMPLE, null, 2)
}
value={inputs.status_code_mapping || ''}
onChange={(value) =>
handleInputChange('status_code_mapping', value)
}
template={STATUS_CODE_MAPPING_EXAMPLE}
templateLabel={t('填入模板')}
editorType='keyValue'
formApi={formApiRef.current}
extraText={t('键为原状态码,值为要复写的状态码,仅影响本地判断')}
/>
</div>
{/* Channel Behavior Section */}
<div className='py-3 border-b border-gray-100'>
<Text className='text-sm font-medium text-gray-500 mb-3 block'>
{t('渠道行为')}
</Text>
<Form.Input
field='tag'
label={t('渠道标签')}
placeholder={t('渠道标签')}
showClear
onChange={(value) => handleInputChange('tag', value)}
/>
<Form.TextArea
field='remark'
label={t('备注')}
placeholder={t('请输入备注(仅管理员可见)')}
maxLength={255}
showClear
onChange={(value) => handleInputChange('remark', value)}
/>
<Row gutter={12}>
<Col span={12}>
<Form.InputNumber
field='priority'
label={t('渠道优先级')}
placeholder={t('渠道优先级')}
min={0}
onNumberChange={(value) => handleInputChange('priority', value)}
style={{ width: '100%' }}
/>
</Col>
<Col span={12}>
<Form.InputNumber
field='weight'
label={t('渠道权重')}
placeholder={t('渠道权重')}
min={0}
onNumberChange={(value) => handleInputChange('weight', value)}
style={{ width: '100%' }}
/>
</Col>
</Row>
{inputs.type === 1 && (
<>
<div className='mt-4 mb-2 text-sm font-medium text-gray-700'>
{t('字段透传控制')}
</div>
<Form.Switch field='allow_service_tier' label={t('允许 service_tier 透传')} checkedText={t('开')} uncheckedText={t('关')} onChange={(value) => handleChannelOtherSettingsChange('allow_service_tier', value)} extraText={t('service_tier 字段用于指定服务层级,允许透传可能导致实际计费高于预期。默认关闭以避免额外费用')} />
<Form.Switch field='disable_store' label={t('禁用 store 透传')} checkedText={t('开')} uncheckedText={t('关')} onChange={(value) => handleChannelOtherSettingsChange('disable_store', value)} extraText={t('store 字段用于授权 OpenAI 存储请求数据以评估和优化产品。默认关闭,开启后可能导致 Codex 无法正常使用')} />
<Form.Switch field='allow_safety_identifier' label={t('允许 safety_identifier 透传')} checkedText={t('开')} uncheckedText={t('关')} onChange={(value) => handleChannelOtherSettingsChange('allow_safety_identifier', value)} extraText={t('safety_identifier 字段用于帮助 OpenAI 识别可能违反使用政策的应用程序用户。默认关闭以保护用户隐私')} />
<Form.Switch field='allow_include_obfuscation' label={t('允许 stream_options.include_obfuscation 透传')} checkedText={t('开')} uncheckedText={t('关')} onChange={(value) => handleChannelOtherSettingsChange('allow_include_obfuscation', value)} extraText={t('include_obfuscation 用于控制 Responses 流混淆字段。默认关闭以避免客户端关闭该安全保护')} />
</>
)}
{inputs.type === 14 && (
<>
<div className='mt-4 mb-2 text-sm font-medium text-gray-700'>
{t('字段透传控制')}
</div>
<Form.Switch field='allow_service_tier' label={t('允许 service_tier 透传')} checkedText={t('开')} uncheckedText={t('关')} onChange={(value) => handleChannelOtherSettingsChange('allow_service_tier', value)} extraText={t('service_tier 字段用于指定服务层级,允许透传可能导致实际计费高于预期。默认关闭以避免额外费用')} />
<Form.Switch field='allow_inference_geo' label={t('允许 inference_geo 透传')} checkedText={t('开')} uncheckedText={t('关')} onChange={(value) => handleChannelOtherSettingsChange('allow_inference_geo', value)} extraText={t('inference_geo 字段用于控制 Claude 数据驻留推理区域。默认关闭以避免未经授权透传地域信息')} />
</>
)}
</div>
{/* Extra Settings Section */}
<div className='pt-3'>
<Text className='text-sm font-medium text-gray-500 mb-3 block'>
{t('额外设置')}
</Text>
{inputs.type === 14 && (
<Form.Switch field='claude_beta_query' label={t('Claude 强制 beta=true')} checkedText={t('开')} uncheckedText={t('关')} onChange={(value) => handleChannelOtherSettingsChange('claude_beta_query', value)} extraText={t('开启后,该渠道请求 Claude 时将强制追加 ?beta=true(无需客户端手动传参)')} />
)}
{inputs.type === 1 && (
<Form.Switch field='force_format' label={t('强制格式化')} checkedText={t('开')} uncheckedText={t('关')} onChange={(value) => handleChannelSettingsChange('force_format', value)} extraText={t('强制将响应格式化为 OpenAI 标准格式(只适用于OpenAI渠道类型)')} />
)}
<Form.Switch field='thinking_to_content' label={t('思考内容转换')} checkedText={t('开')} uncheckedText={t('关')} onChange={(value) => handleChannelSettingsChange('thinking_to_content', value)} extraText={t('将 reasoning_content 转换为 <think> 标签拼接到内容中')} />
<Form.Switch field='pass_through_body_enabled' label={t('透传请求体')} checkedText={t('开')} uncheckedText={t('关')} onChange={(value) => handleChannelSettingsChange('pass_through_body_enabled', value)} extraText={t('启用请求体透传功能')} />
<Form.Input field='proxy' label={t('代理地址')} placeholder={t('例如: socks5://user:pass@host:port')} onChange={(value) => handleChannelSettingsChange('proxy', value)} showClear extraText={t('用于配置网络代理,支持 socks5 协议')} />
<Form.TextArea field='system_prompt' label={t('系统提示词')} placeholder={t('输入系统提示词,用户的系统提示词将优先于此设置')} onChange={(value) => handleChannelSettingsChange('system_prompt', value)} autosize showClear extraText={t('用户优先:如果用户在请求中指定了系统提示词,将优先使用用户的设置')} />
<Form.Switch field='system_prompt_override' label={t('系统提示词拼接')} checkedText={t('开')} uncheckedText={t('关')} onChange={(value) => handleChannelSettingsChange('system_prompt_override', value)} extraText={t('如果用户请求中包含系统提示词,则使用此设置拼接到用户的系统提示词前面')} />
</div>
</div>
);
return (
<>
<Spin spinning={loading}> <Spin spinning={loading}>
<div className='p-2 space-y-3' ref={formContainerRef}> <div className='p-2 space-y-3' ref={formContainerRef}>
<div ref={(el) => (formSectionRefs.current.basicInfo = el)}> {/* Core Configuration Card - Always Visible */}
<Card className='!rounded-2xl shadow-sm border-0 mb-6'> <Card className='!rounded-2xl shadow-sm border-0'>
{/* Header: Basic Info */} {/* Header */}
<div className='flex items-center mb-2'> <div className='flex items-center mb-4'>
<Avatar <Avatar
size='small' size='small'
color='blue' color='blue'
className='mr-2 shadow-md' className='mr-2 shadow-md'
> >
<IconServer size={16} /> <IconServer size={16} />
</Avatar> </Avatar>
<div> <div>
<Text className='text-lg font-medium'> <Text className='text-lg font-medium'>
{t('基本信息')} {t('核心配置')}
</Text> </Text>
<div className='text-xs text-gray-600'> <div className='text-xs text-gray-600'>
{t('渠道的基本配置信息')} {t('创建渠道所需的基本信息')}
</div>
</div> </div>
</div> </div>
</div>
{isIonetChannel && ( {isIonetChannel && (
<Banner <Banner
...@@ -2853,34 +3094,10 @@ const EditChannelModal = (props) => { ...@@ -2853,34 +3094,10 @@ const EditChannelModal = (props) => {
} }
/> />
)} )}
</Card>
</div>
{/* API Configuration Card */} {/* API Configuration Section */}
{showApiConfigCard && ( {showApiConfigCard && (
<div ref={(el) => (formSectionRefs.current.apiConfig = el)}> <div onClick={handleApiConfigSecretClick}>
<Card className='!rounded-2xl shadow-sm border-0 mb-6'>
{/* Header: API Config */}
<div
className='flex items-center mb-2'
onClick={handleApiConfigSecretClick}
>
<Avatar
size='small'
color='green'
className='mr-2 shadow-md'
>
<IconGlobe size={16} />
</Avatar>
<div>
<Text className='text-lg font-medium'>
{t('API 配置')}
</Text>
<div className='text-xs text-gray-600'>
{t('API 地址和相关配置')}
</div>
</div>
</div>
{inputs.type === 40 && ( {inputs.type === 40 && (
<Banner <Banner
...@@ -3078,9 +3295,8 @@ const EditChannelModal = (props) => { ...@@ -3078,9 +3295,8 @@ const EditChannelModal = (props) => {
'https://ark.ap-southeast.bytepluses.com', 'https://ark.ap-southeast.bytepluses.com',
}, },
{ {
value: DEPRECATED_DOUBAO_CODING_PLAN_BASE_URL, value: 'doubao-coding-plan',
label: doubaoCodingPlanOptionLabel, label: 'Doubao Coding Plan',
disabled: !canKeepDeprecatedDoubaoCodingPlan,
}, },
]} ]}
defaultValue='https://ark.cn-beijing.volces.com' defaultValue='https://ark.cn-beijing.volces.com'
...@@ -3088,33 +3304,11 @@ const EditChannelModal = (props) => { ...@@ -3088,33 +3304,11 @@ const EditChannelModal = (props) => {
/> />
</div> </div>
)} )}
</Card>
</div>
)}
{/* Model Configuration Card */}
<div ref={(el) => (formSectionRefs.current.modelConfig = el)}>
<Card className='!rounded-2xl shadow-sm border-0 mb-6'>
{/* Header: Model Config */}
<div className='flex items-center mb-2'>
<Avatar
size='small'
color='purple'
className='mr-2 shadow-md'
>
<IconCode size={16} />
</Avatar>
<div>
<Text className='text-lg font-medium'>
{t('模型配置')}
</Text>
<div className='text-xs text-gray-600'>
{t('模型选择和映射设置')}
</div>
</div>
</div> </div>
)}
<Form.Select {/* Model Selection - Part of Core Config */}
<Form.Select
field='models' field='models'
label={t('模型')} label={t('模型')}
placeholder={t('请选择该渠道所支持的模型')} placeholder={t('请选择该渠道所支持的模型')}
...@@ -3163,7 +3357,7 @@ const EditChannelModal = (props) => { ...@@ -3163,7 +3357,7 @@ const EditChannelModal = (props) => {
}; };
}} }}
extraText={ extraText={
<Space wrap> <Space>
<Button <Button
size='small' size='small'
type='primary' type='primary'
...@@ -3173,15 +3367,6 @@ const EditChannelModal = (props) => { ...@@ -3173,15 +3367,6 @@ const EditChannelModal = (props) => {
> >
{t('填入相关模型')} {t('填入相关模型')}
</Button> </Button>
<Button
size='small'
type='secondary'
onClick={() =>
handleInputChange('models', fullModels)
}
>
{t('填入所有模型')}
</Button>
{MODEL_FETCHABLE_CHANNEL_TYPES.has(inputs.type) && ( {MODEL_FETCHABLE_CHANNEL_TYPES.has(inputs.type) && (
<Button <Button
size='small' size='small'
...@@ -3191,767 +3376,267 @@ const EditChannelModal = (props) => { ...@@ -3191,767 +3376,267 @@ const EditChannelModal = (props) => {
{t('获取模型列表')} {t('获取模型列表')}
</Button> </Button>
)} )}
{inputs.type === 4 && isEdit && ( <Dropdown
<Button trigger='click'
size='small' position='bottomRight'
type='primary' menu={[
theme='light' { node: 'item', name: t('填入所有模型'), onClick: () => handleInputChange('models', fullModels) },
onClick={() => setOllamaModalVisible(true)} ...(inputs.type === 4 && isEdit ? [{ node: 'item', name: t('Ollama 模型管理'), onClick: () => setOllamaModalVisible(true) }] : []),
> { node: 'divider' },
{t('Ollama 模型管理')} { node: 'item', name: t('复制所有模型'), onClick: () => {
</Button> if (inputs.models.length === 0) { showInfo(t('没有模型可以复制')); return; }
)} try { copy(inputs.models.join(',')); showSuccess(t('模型列表已复制到剪贴板')); } catch (error) { showError(t('复制失败')); }
<Button }},
size='small' { node: 'item', name: t('清除所有模型'), type: 'danger', onClick: () => handleInputChange('models', []) },
type='warning' ...((modelGroups && modelGroups.length > 0) ? [
onClick={() => handleInputChange('models', [])} { node: 'divider' },
> ...modelGroups.map((group) => ({
{t('清除所有模型')} node: 'item',
</Button> name: group.name,
<Button onClick: () => {
size='small' let items = [];
type='tertiary' try {
onClick={() => { if (Array.isArray(group.items)) { items = group.items; }
if (inputs.models.length === 0) { else if (typeof group.items === 'string') {
showInfo(t('没有模型可以复制')); const parsed = JSON.parse(group.items || '[]');
return; if (Array.isArray(parsed)) items = parsed;
} }
try { } catch {}
copy(inputs.models.join(',')); const current = formApiRef.current?.getValue('models') || inputs.models || [];
showSuccess(t('模型列表已复制到剪贴板')); const merged = Array.from(new Set([...current, ...items].map((m) => (m || '').trim()).filter(Boolean)));
} catch (error) { handleInputChange('models', merged);
showError(t('复制失败')); },
} })),
}} ] : []),
]}
> >
{t('复制所有模型')} <Button size='small' type='tertiary'>
</Button> {t('更多')} <IconChevronDown size={12} />
{modelGroups && </Button>
modelGroups.length > 0 && </Dropdown>
modelGroups.map((group) => (
<Button
key={group.id}
size='small'
type='primary'
onClick={() => {
let items = [];
try {
if (Array.isArray(group.items)) {
items = group.items;
} else if (
typeof group.items === 'string'
) {
const parsed = JSON.parse(
group.items || '[]',
);
if (Array.isArray(parsed)) items = parsed;
}
} catch {}
const current =
formApiRef.current?.getValue('models') ||
inputs.models ||
[];
const merged = Array.from(
new Set(
[...current, ...items]
.map((m) => (m || '').trim())
.filter(Boolean),
),
);
handleInputChange('models', merged);
}}
>
{group.name}
</Button>
))}
</Space> </Space>
} }
/> />
<Form.Input {/* Custom Model Name - Core Config */}
field='custom_model' <Form.Input
label={t('自定义模型名称')} field='custom_model'
placeholder={t('输入自定义模型名称')} label={t('自定义模型名称')}
onChange={(value) => setCustomModel(value.trim())} placeholder={t('输入自定义模型名称')}
value={customModel} onChange={(value) => setCustomModel(value.trim())}
suffix={ value={customModel}
<Button suffix={
size='small' <Button
type='primary' size='small'
onClick={addCustomModels} type='primary'
> onClick={addCustomModels}
{t('填入')} >
</Button> {t('填入')}
} </Button>
/> }
/>
{MODEL_FETCHABLE_CHANNEL_TYPES.has(inputs.type) && (
<> {/* Groups - Core Config */}
<Form.Switch <Form.Select
field='upstream_model_update_check_enabled' field='groups'
label={t('是否检测上游模型更新')} label={t('分组')}
checkedText={t('开')} placeholder={t('请选择可以使用该渠道的分组')}
uncheckedText={t('关')} multiple
onChange={(value) => allowAdditions
handleChannelOtherSettingsChange( additionLabel={t(
'upstream_model_update_check_enabled', '请在系统设置页面编辑分组倍率以添加新的分组:',
value,
)
}
extraText={t(
'开启后由后端定时任务检测该渠道上游模型变化',
)}
/>
<div className='text-xs text-gray-500 mb-2'>
{t('上次检测时间')}:&nbsp;
{formatUnixTime(
inputs.upstream_model_update_last_check_time,
)}
</div>
<Form.Input
field='upstream_model_update_ignored_models'
label={t('已忽略模型')}
placeholder={t(
'例如:gpt-4.1-nano,regex:^claude-.*$,regex:^sora-.*$',
)}
extraText={t(
'支持精确匹配;使用 regex: 开头可按正则匹配。',
)}
onChange={(value) =>
handleInputChange(
'upstream_model_update_ignored_models',
value,
)
}
showClear
/>
</>
)} )}
optionList={groupOptions}
<Form.Input style={{ width: '100%' }}
field='test_model' position='top'
label={t('默认测试模型')} onChange={(value) => handleInputChange('groups', value)}
placeholder={t('不填则为模型列表第一个')} />
onChange={(value) =>
handleInputChange('test_model', value) {/* Model Mapping - Core Config */}
<JSONEditor
key={`model_mapping-${isEdit ? channelId : 'new'}`}
field='model_mapping'
label={t('模型重定向')}
placeholder={
t(
'此项可选,用于修改请求体中的模型名称,为一个 JSON 字符串,键为请求中模型名称,值为要替换的模型名称,例如:',
) +
`\n${JSON.stringify(MODEL_MAPPING_EXAMPLE, null, 2)}`
}
value={inputs.model_mapping || ''}
onChange={(value) =>
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;
} }
showClear const disabled = !String(pairKey ?? '').trim();
/> return (
<Tooltip content={t('选择模型')}>
<JSONEditor <Button
key={`model_mapping-${isEdit ? channelId : 'new'}`} type='tertiary'
field='model_mapping' theme='borderless'
label={t('模型重定向')} size='small'
placeholder={ icon={<IconSearch size={14} />}
t( disabled={disabled}
'此项可选,用于修改请求体中的模型名称,为一个 JSON 字符串,键为请求中模型名称,值为要替换的模型名称,例如:', onClick={(e) => {
) + e.stopPropagation();
`\n${JSON.stringify(MODEL_MAPPING_EXAMPLE, null, 2)}` openModelMappingValueModal({ pairKey, value });
} }}
value={inputs.model_mapping || ''} />
onChange={(value) => </Tooltip>
handleInputChange('model_mapping', value) );
} }}
template={MODEL_MAPPING_EXAMPLE} extraText={t(
templateLabel={t('填入模板')} '键为请求中的模型名称,值为要替换的模型名称',
editorType='keyValue' )}
formApi={formApiRef.current} />
renderStringValueSuffix={({ pairKey, value }) => {
if (!MODEL_FETCHABLE_CHANNEL_TYPES.has(inputs.type)) { {/* Auto Ban - Core Config */}
return null; <Form.Switch
} field='auto_ban'
const disabled = !String(pairKey ?? '').trim(); label={t('是否自动禁用')}
return ( checkedText={t('开')}
<Tooltip content={t('选择模型')}> uncheckedText={t('关')}
<Button onChange={(value) => setAutoBan(value)}
type='tertiary' extraText={t(
theme='borderless' '仅当自动禁用开启时有效,关闭后不会自动禁用该渠道',
size='small' )}
icon={<IconSearch size={14} />} initValue={autoBan}
disabled={disabled} />
onClick={(e) => {
e.stopPropagation(); {/* Test Model - Core Config */}
openModelMappingValueModal({ pairKey, value }); <Form.Input
}} field='test_model'
/> label={t('默认测试模型')}
</Tooltip> placeholder={t('不填则为模型列表第一个')}
); onChange={(value) =>
}} handleInputChange('test_model', value)
extraText={t( }
'键为请求中的模型名称,值为要替换的模型名称', showClear
)} />
/> </Card>
</Card>
</div> {/* Advanced Settings Toggle / Collapse */}
{isMobile ? (
{/* Advanced Settings Card */} <Collapse
<div activeKey={advancedSettingsOpen ? ['advanced'] : []}
ref={(el) => (formSectionRefs.current.advancedSettings = el)} onChange={(keys) => setAdvancedSettingsOpen(keys.includes('advanced'))}
> >
<Card className='!rounded-2xl shadow-sm border-0 mb-6'> <Collapse.Panel
{/* Header: Advanced Settings */} header={
<div className='flex items-center mb-2'> <div className='flex items-center gap-2'>
<Avatar
size='small'
color='orange'
className='mr-2 shadow-md'
>
<IconSetting size={16} /> <IconSetting size={16} />
</Avatar> <Text className='font-medium'>{t('高级设置')}</Text>
<div>
<Text className='text-lg font-medium'>
{t('高级设置')}
</Text>
<div className='text-xs text-gray-600'>
{t('渠道的高级配置选项')}
</div>
</div> </div>
}
itemKey='advanced'
>
{advancedSettingsContent}
</Collapse.Panel>
</Collapse>
) : (
/* Desktop: toggle button to open side panel */
<div
className='flex items-center justify-between p-3 rounded-xl cursor-pointer transition-colors hover:bg-gray-50'
style={{
backgroundColor: advancedSettingsOpen ? 'var(--semi-color-primary-light-default)' : 'var(--semi-color-fill-0)',
border: '1px solid var(--semi-color-fill-2)',
}}
onClick={() => setAdvancedSettingsOpen(!advancedSettingsOpen)}
>
<div className='flex items-center gap-2'>
<IconSetting size={16} />
<Text className='font-medium'>{t('高级设置')}</Text>
</div> </div>
<div className='flex items-center gap-1 text-sm' style={{ color: 'var(--semi-color-primary)' }}>
<Form.Select <Text size='small' style={{ color: 'var(--semi-color-primary)' }}>
field='groups' {advancedSettingsOpen ? t('收起') : isEdit ? t('向左展开') : t('向右展开')}
label={t('分组')}
placeholder={t('请选择可以使用该渠道的分组')}
multiple
allowAdditions
additionLabel={t(
'请在系统设置页面编辑分组倍率以添加新的分组:',
)}
optionList={groupOptions}
style={{ width: '100%' }}
onChange={(value) => handleInputChange('groups', value)}
/>
<Form.Input
field='tag'
label={t('渠道标签')}
placeholder={t('渠道标签')}
showClear
onChange={(value) => handleInputChange('tag', value)}
/>
<Form.TextArea
field='remark'
label={t('备注')}
placeholder={t('请输入备注(仅管理员可见)')}
maxLength={255}
showClear
onChange={(value) => handleInputChange('remark', value)}
/>
<Row gutter={12}>
<Col span={12}>
<Form.InputNumber
field='priority'
label={t('渠道优先级')}
placeholder={t('渠道优先级')}
min={0}
onNumberChange={(value) =>
handleInputChange('priority', value)
}
style={{ width: '100%' }}
/>
</Col>
<Col span={12}>
<Form.InputNumber
field='weight'
label={t('渠道权重')}
placeholder={t('渠道权重')}
min={0}
onNumberChange={(value) =>
handleInputChange('weight', value)
}
style={{ width: '100%' }}
/>
</Col>
</Row>
<Form.Switch
field='auto_ban'
label={t('是否自动禁用')}
checkedText={t('开')}
uncheckedText={t('关')}
onChange={(value) => setAutoBan(value)}
extraText={t(
'仅当自动禁用开启时有效,关闭后不会自动禁用该渠道',
)}
initValue={autoBan}
/>
<Form.Switch
field='upstream_model_update_auto_sync_enabled'
label={t('是否自动同步上游模型更新')}
checkedText={t('开')}
uncheckedText={t('关')}
disabled={!inputs.upstream_model_update_check_enabled}
onChange={(value) =>
handleChannelOtherSettingsChange(
'upstream_model_update_auto_sync_enabled',
value,
)
}
extraText={t(
'开启后检测到新增模型会自动加入当前渠道模型列表',
)}
/>
<div className='text-xs text-gray-500 mb-3'>
{t('上次检测到可加入模型')}:&nbsp;
{upstreamDetectedModels.length === 0 ? (
t('暂无')
) : (
<>
<Tooltip
position='topLeft'
content={
<div className='max-w-[640px] break-all text-xs leading-5'>
{upstreamDetectedModels.join(', ')}
</div>
}
>
<span className='cursor-help break-all'>
{upstreamDetectedModelsPreview.join(', ')}
</span>
</Tooltip>
<span className='ml-1 text-gray-400'>
{upstreamDetectedModelsOmittedCount > 0
? t('(共 {{total}} 个,省略 {{omit}} 个)', {
total: upstreamDetectedModels.length,
omit: upstreamDetectedModelsOmittedCount,
})
: t('(共 {{total}} 个)', {
total: upstreamDetectedModels.length,
})}
</span>
</>
)}
</div>
<div className='mb-4'>
<div className='flex items-center justify-between gap-2 mb-1'>
<Text className='text-sm font-medium'>{t('参数覆盖')}</Text>
<Space wrap>
<Button
size='small'
type='primary'
icon={<IconCode size={14} />}
onClick={() => setParamOverrideEditorVisible(true)}
>
{t('可视化编辑')}
</Button>
<Button
size='small'
onClick={() =>
applyParamOverrideTemplate('operations', 'fill')
}
>
{t('填充新模板')}
</Button>
<Button
size='small'
onClick={() =>
applyParamOverrideTemplate('legacy', 'fill')
}
>
{t('填充旧模板')}
</Button>
<Button
size='small'
type='tertiary'
onClick={clearParamOverride}
>
{t('清空')}
</Button>
</Space>
</div>
<Text type='tertiary' size='small'>
{t('此项可选,用于覆盖请求参数。不支持覆盖 stream 参数')}
</Text> </Text>
<div <IconChevronDown
className='mt-2 rounded-xl p-3' size={14}
style={{ style={{
backgroundColor: 'var(--semi-color-fill-0)', transform: advancedSettingsOpen
border: '1px solid var(--semi-color-fill-2)', ? 'rotate(180deg)'
}} : isEdit ? 'rotate(90deg)' : 'rotate(-90deg)',
> transition: 'transform 0.2s',
<div className='flex items-center justify-between mb-2'> }}
<Tag color={paramOverrideMeta.tagColor}> />
{paramOverrideMeta.tagLabel}
</Tag>
<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}
</pre>
</div>
</div> </div>
</div>
)}
</div>
</Spin>
<Form.TextArea {/* Desktop: Advanced Settings Side Panel - rendered inside Form tree */}
field='header_override' {!isMobile && advancedSettingsOpen && (
label={t('请求头覆盖')} <div
placeholder={ className='fixed top-0 h-full overflow-y-auto z-[999] semi-sidesheet-inner'
t('此项可选,用于覆盖请求头参数') + style={{
'\n' + width: 600,
t('格式示例:') + [isEdit ? 'right' : 'left']: 600,
'\n{\n "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",\n "Authorization": "Bearer {api_key}"\n}' backgroundColor: 'var(--semi-color-bg-0)',
} borderLeft: isEdit ? 'none' : '1px solid var(--semi-color-border)',
autosize borderRight: isEdit ? '1px solid var(--semi-color-border)' : 'none',
onChange={(value) => animation: `slideIn${isEdit ? 'Left' : 'Right'} 0.3s ease-out`,
handleInputChange('header_override', value) }}
} >
extraText={ <div className='semi-sidesheet-header'>
<div className='flex flex-col gap-1'> <div className='semi-sidesheet-title'>
<div className='flex gap-2 flex-wrap items-center'> <Space>
<Text <Tag color='cyan' shape='circle'>
className='!text-semi-color-primary cursor-pointer' {t('高级')}
onClick={() => </Tag>
handleInputChange( <Title heading={4} className='m-0'>
'header_override', {t('高级设置')}
JSON.stringify( </Title>
{ </Space>
'*': true, </div>
're:^X-Trace-.*$': true, <Button
'X-Foo': '{client_header:X-Foo}', className='semi-sidesheet-close'
Authorization: 'Bearer {api_key}', type='tertiary'
'User-Agent': theme='borderless'
'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', icon={<IconClose />}
}, size='small'
null, onClick={() => setAdvancedSettingsOpen(false)}
2, />
), </div>
) <div className='semi-sidesheet-body' style={{ padding: 0 }}>
} <div className='p-2 space-y-3'>
> <Card className='!rounded-2xl shadow-sm border-0'>
{t('填入模板')} <div className='flex items-center mb-4'>
</Text> <Avatar
<Text size='small'
className='!text-semi-color-primary cursor-pointer' color='orange'
onClick={() => className='mr-2 shadow-md'
handleInputChange( >
'header_override', <IconSetting size={16} />
JSON.stringify( </Avatar>
{ <div>
'*': true, <Text className='text-lg font-medium'>
}, {t('高级设置')}
null, </Text>
2, <div className='text-xs text-gray-600'>
), {t('渠道的高级配置选项')}
)
}
>
{t('填入透传模版')}
</Text>
<Text
className='!text-semi-color-primary cursor-pointer'
onClick={() => formatJsonField('header_override')}
>
{t('格式化')}
</Text>
</div>
<div>
<Text type='tertiary' size='small'>
{t('支持变量:')}
</Text>
<div className='text-xs text-tertiary ml-2'>
<div>
{t('渠道密钥')}: {'{api_key}'}
</div>
</div>
</div>
</div> </div>
}
showClear
/>
<JSONEditor
key={`status_code_mapping-${isEdit ? channelId : 'new'}`}
field='status_code_mapping'
label={t('状态码复写')}
placeholder={
t(
'此项可选,用于复写返回的状态码,仅影响本地判断,不修改返回到上游的状态码,比如将claude渠道的400错误复写为500(用于重试),请勿滥用该功能,例如:',
) +
'\n' +
JSON.stringify(STATUS_CODE_MAPPING_EXAMPLE, null, 2)
}
value={inputs.status_code_mapping || ''}
onChange={(value) =>
handleInputChange('status_code_mapping', value)
}
template={STATUS_CODE_MAPPING_EXAMPLE}
templateLabel={t('填入模板')}
editorType='keyValue'
formApi={formApiRef.current}
extraText={t(
'键为原状态码,值为要复写的状态码,仅影响本地判断',
)}
/>
{/* 字段透传控制 - OpenAI 渠道 */}
{inputs.type === 1 && (
<>
<div className='mt-4 mb-2 text-sm font-medium text-gray-700'>
{t('字段透传控制')}
</div> </div>
<Form.Switch
field='allow_service_tier'
label={t('允许 service_tier 透传')}
checkedText={t('开')}
uncheckedText={t('关')}
onChange={(value) =>
handleChannelOtherSettingsChange(
'allow_service_tier',
value,
)
}
extraText={t(
'service_tier 字段用于指定服务层级,允许透传可能导致实际计费高于预期。默认关闭以避免额外费用',
)}
/>
<Form.Switch
field='disable_store'
label={t('禁用 store 透传')}
checkedText={t('开')}
uncheckedText={t('关')}
onChange={(value) =>
handleChannelOtherSettingsChange(
'disable_store',
value,
)
}
extraText={t(
'store 字段用于授权 OpenAI 存储请求数据以评估和优化产品。默认关闭,开启后可能导致 Codex 无法正常使用',
)}
/>
<Form.Switch
field='allow_safety_identifier'
label={t('允许 safety_identifier 透传')}
checkedText={t('开')}
uncheckedText={t('关')}
onChange={(value) =>
handleChannelOtherSettingsChange(
'allow_safety_identifier',
value,
)
}
extraText={t(
'safety_identifier 字段用于帮助 OpenAI 识别可能违反使用政策的应用程序用户。默认关闭以保护用户隐私',
)}
/>
<Form.Switch
field='allow_include_obfuscation'
label={t(
'允许 stream_options.include_obfuscation 透传',
)}
checkedText={t('开')}
uncheckedText={t('关')}
onChange={(value) =>
handleChannelOtherSettingsChange(
'allow_include_obfuscation',
value,
)
}
extraText={t(
'include_obfuscation 用于控制 Responses 流混淆字段。默认关闭以避免客户端关闭该安全保护',
)}
/>
</>
)}
{/* 字段透传控制 - Claude 渠道 */}
{inputs.type === 14 && (
<>
<div className='mt-4 mb-2 text-sm font-medium text-gray-700'>
{t('字段透传控制')}
</div>
<Form.Switch
field='allow_service_tier'
label={t('允许 service_tier 透传')}
checkedText={t('开')}
uncheckedText={t('关')}
onChange={(value) =>
handleChannelOtherSettingsChange(
'allow_service_tier',
value,
)
}
extraText={t(
'service_tier 字段用于指定服务层级,允许透传可能导致实际计费高于预期。默认关闭以避免额外费用',
)}
/>
<Form.Switch
field='allow_inference_geo'
label={t('允许 inference_geo 透传')}
checkedText={t('开')}
uncheckedText={t('关')}
onChange={(value) =>
handleChannelOtherSettingsChange(
'allow_inference_geo',
value,
)
}
extraText={t(
'inference_geo 字段用于控制 Claude 数据驻留推理区域。默认关闭以避免未经授权透传地域信息',
)}
/>
</>
)}
</Card>
</div>
{/* Channel Extra Settings Card */}
<div
ref={(el) =>
(formSectionRefs.current.channelExtraSettings = el)
}
>
<Card className='!rounded-2xl shadow-sm border-0 mb-6'>
{/* Header: Channel Extra Settings */}
<div className='flex items-center mb-2'>
<Avatar
size='small'
color='violet'
className='mr-2 shadow-md'
>
<IconBolt size={16} />
</Avatar>
<div>
<Text className='text-lg font-medium'>
{t('渠道额外设置')}
</Text>
</div> </div>
</div> {advancedSettingsContent}
</Card>
{inputs.type === 14 && ( </div>
<Form.Switch
field='claude_beta_query'
label={t('Claude 强制 beta=true')}
checkedText={t('开')}
uncheckedText={t('关')}
onChange={(value) =>
handleChannelOtherSettingsChange(
'claude_beta_query',
value,
)
}
extraText={t(
'开启后,该渠道请求 Claude 时将强制追加 ?beta=true(无需客户端手动传参)',
)}
/>
)}
{inputs.type === 1 && (
<Form.Switch
field='force_format'
label={t('强制格式化')}
checkedText={t('开')}
uncheckedText={t('关')}
onChange={(value) =>
handleChannelSettingsChange('force_format', value)
}
extraText={t(
'强制将响应格式化为 OpenAI 标准格式(只适用于OpenAI渠道类型)',
)}
/>
)}
<Form.Switch
field='thinking_to_content'
label={t('思考内容转换')}
checkedText={t('开')}
uncheckedText={t('关')}
onChange={(value) =>
handleChannelSettingsChange(
'thinking_to_content',
value,
)
}
extraText={t(
'将 reasoning_content 转换为 <think> 标签拼接到内容中',
)}
/>
<Form.Switch
field='pass_through_body_enabled'
label={t('透传请求体')}
checkedText={t('开')}
uncheckedText={t('关')}
onChange={(value) =>
handleChannelSettingsChange(
'pass_through_body_enabled',
value,
)
}
extraText={t('启用请求体透传功能')}
/>
<Form.Input
field='proxy'
label={t('代理地址')}
placeholder={t('例如: socks5://user:pass@host:port')}
onChange={(value) =>
handleChannelSettingsChange('proxy', value)
}
showClear
extraText={t('用于配置网络代理,支持 socks5 协议')}
/>
<Form.TextArea
field='system_prompt'
label={t('系统提示词')}
placeholder={t(
'输入系统提示词,用户的系统提示词将优先于此设置',
)}
onChange={(value) =>
handleChannelSettingsChange('system_prompt', value)
}
autosize
showClear
extraText={t(
'用户优先:如果用户在请求中指定了系统提示词,将优先使用用户的设置',
)}
/>
<Form.Switch
field='system_prompt_override'
label={t('系统提示词拼接')}
checkedText={t('开')}
uncheckedText={t('关')}
onChange={(value) =>
handleChannelSettingsChange(
'system_prompt_override',
value,
)
}
extraText={t(
'如果用户请求中包含系统提示词,则使用此设置拼接到用户的系统提示词前面',
)}
/>
</Card>
</div> </div>
</div> </div>
</Spin> )}
)} </>
);
}}
</Form> </Form>
<ImagePreview <ImagePreview
src={modalImageUrl} src={modalImageUrl}
visible={isModalOpenurl} visible={isModalOpenurl}
......
...@@ -1507,6 +1507,8 @@ ...@@ -1507,6 +1507,8 @@
"收益统计": "Income statistics", "收益统计": "Income statistics",
"收起": "Collapse", "收起": "Collapse",
"收起侧边栏": "Collapse sidebar", "收起侧边栏": "Collapse sidebar",
"向左展开": "Expand left",
"向右展开": "Expand right",
"收起内容": "Collapse content", "收起内容": "Collapse content",
"放大": "Upscalers", "放大": "Upscalers",
"放大编辑": "Expand editor", "放大编辑": "Expand editor",
...@@ -3220,6 +3222,12 @@ ...@@ -3220,6 +3222,12 @@
"高级设置": "Advanced Settings", "高级设置": "Advanced Settings",
"高级选项": "Advanced Options", "高级选项": "Advanced Options",
"高级配置": "Advanced Configuration", "高级配置": "Advanced Configuration",
"核心配置": "Core Configuration",
"创建渠道所需的基本信息": "Basic information needed to create a channel",
"请求配置": "Request Configuration",
"渠道行为": "Channel Behavior",
"额外设置": "Extra Settings",
"上游模型管理": "Upstream Model Management",
"黑名单": "Blacklist", "黑名单": "Blacklist",
"默认": "Default", "默认": "Default",
"默认 API 版本": "Default API Version", "默认 API 版本": "Default API Version",
......
...@@ -1495,6 +1495,8 @@ ...@@ -1495,6 +1495,8 @@
"收益统计": "Statistiques sur les revenus", "收益统计": "Statistiques sur les revenus",
"收起": "Réduire", "收起": "Réduire",
"收起侧边栏": "Réduire la barre latérale", "收起侧边栏": "Réduire la barre latérale",
"向左展开": "Développer à gauche",
"向右展开": "Développer à droite",
"收起内容": "Réduire le contenu", "收起内容": "Réduire le contenu",
"放大": "Upscalers", "放大": "Upscalers",
"放大编辑": "Développer l'éditeur", "放大编辑": "Développer l'éditeur",
...@@ -3164,6 +3166,12 @@ ...@@ -3164,6 +3166,12 @@
"高级设置": "Paramètres avancés", "高级设置": "Paramètres avancés",
"高级选项": "Options avancées", "高级选项": "Options avancées",
"高级配置": "Advanced Configuration", "高级配置": "Advanced Configuration",
"核心配置": "Configuration principale",
"创建渠道所需的基本信息": "Informations de base pour créer un canal",
"请求配置": "Configuration des requêtes",
"渠道行为": "Comportement du canal",
"额外设置": "Paramètres supplémentaires",
"上游模型管理": "Gestion des modèles amont",
"黑名单": "Liste noire", "黑名单": "Liste noire",
"默认": "Par défaut", "默认": "Par défaut",
"默认 API 版本": "Version de l'API par défaut", "默认 API 版本": "Version de l'API par défaut",
......
...@@ -1478,6 +1478,8 @@ ...@@ -1478,6 +1478,8 @@
"收益统计": "収益統計", "收益统计": "収益統計",
"收起": "折りたたみ", "收起": "折りたたみ",
"收起侧边栏": "サイドバー折りたたみ", "收起侧边栏": "サイドバー折りたたみ",
"向左展开": "左に展開",
"向右展开": "右に展開",
"收起内容": "コンテンツ折りたたみ", "收起内容": "コンテンツ折りたたみ",
"放大": "アップスケール", "放大": "アップスケール",
"放大编辑": "エディタで開く", "放大编辑": "エディタで開く",
...@@ -3145,6 +3147,12 @@ ...@@ -3145,6 +3147,12 @@
"高级设置": "詳細設定", "高级设置": "詳細設定",
"高级选项": "高度なオプション", "高级选项": "高度なオプション",
"高级配置": "Advanced Configuration", "高级配置": "Advanced Configuration",
"核心配置": "基本設定",
"创建渠道所需的基本信息": "チャネル作成に必要な基本情報",
"请求配置": "リクエスト設定",
"渠道行为": "チャネル動作",
"额外设置": "追加設定",
"上游模型管理": "上流モデル管理",
"黑名单": "ブラックリスト", "黑名单": "ブラックリスト",
"默认": "デフォルト", "默认": "デフォルト",
"默认 API 版本": "デフォルトAPIバージョン", "默认 API 版本": "デフォルトAPIバージョン",
......
...@@ -1507,6 +1507,8 @@ ...@@ -1507,6 +1507,8 @@
"收益统计": "Статистика доходов", "收益统计": "Статистика доходов",
"收起": "Свернуть", "收起": "Свернуть",
"收起侧边栏": "Свернуть боковую панель", "收起侧边栏": "Свернуть боковую панель",
"向左展开": "Развернуть влево",
"向右展开": "Развернуть вправо",
"收起内容": "Свернуть содержимое", "收起内容": "Свернуть содержимое",
"放大": "Увеличить", "放大": "Увеличить",
"放大编辑": "Увеличить и редактировать", "放大编辑": "Увеличить и редактировать",
...@@ -3178,6 +3180,12 @@ ...@@ -3178,6 +3180,12 @@
"高级设置": "Расширенные настройки", "高级设置": "Расширенные настройки",
"高级选项": "Расширенные параметры", "高级选项": "Расширенные параметры",
"高级配置": "Advanced Configuration", "高级配置": "Advanced Configuration",
"核心配置": "Основные настройки",
"创建渠道所需的基本信息": "Основная информация для создания канала",
"请求配置": "Настройки запросов",
"渠道行为": "Поведение канала",
"额外设置": "Дополнительные настройки",
"上游模型管理": "Управление моделями апстрима",
"黑名单": "Черный список", "黑名单": "Черный список",
"默认": "По умолчанию", "默认": "По умолчанию",
"默认 API 版本": "Версия API по умолчанию", "默认 API 版本": "Версия API по умолчанию",
......
...@@ -1479,6 +1479,8 @@ ...@@ -1479,6 +1479,8 @@
"收益统计": "Thống kê thu nhập", "收益统计": "Thống kê thu nhập",
"收起": "Thu gọn", "收起": "Thu gọn",
"收起侧边栏": "Thu gọn thanh bên", "收起侧边栏": "Thu gọn thanh bên",
"向左展开": "Mở rộng sang trái",
"向右展开": "Mở rộng sang phải",
"收起内容": "Thu gọn nội dung", "收起内容": "Thu gọn nội dung",
"放大": "Upscalers", "放大": "Upscalers",
"放大编辑": "Mở rộng trình chỉnh sửa", "放大编辑": "Mở rộng trình chỉnh sửa",
...@@ -3716,6 +3718,12 @@ ...@@ -3716,6 +3718,12 @@
"高级设置": "Cài đặt nâng cao", "高级设置": "Cài đặt nâng cao",
"高级选项": "Tùy chọn nâng cao", "高级选项": "Tùy chọn nâng cao",
"高级配置": "Advanced Configuration", "高级配置": "Advanced Configuration",
"核心配置": "Cấu hình cốt lõi",
"创建渠道所需的基本信息": "Thông tin cơ bản cần thiết để tạo kênh",
"请求配置": "Cấu hình yêu cầu",
"渠道行为": "Hành vi kênh",
"额外设置": "Cài đặt bổ sung",
"上游模型管理": "Quản lý mô hình thượng nguồn",
"黑名单": "Danh sách đen", "黑名单": "Danh sách đen",
"默认": "Mặc định", "默认": "Mặc định",
"默认 API 版本": "Phiên bản API mặc định", "默认 API 版本": "Phiên bản API mặc định",
......
...@@ -1190,6 +1190,8 @@ ...@@ -1190,6 +1190,8 @@
"收益统计": "收益統計", "收益统计": "收益統計",
"收起": "收起", "收起": "收起",
"收起侧边栏": "收起側邊欄", "收起侧边栏": "收起側邊欄",
"向左展开": "向左展開",
"向右展开": "向右展開",
"收起内容": "收起內容", "收起内容": "收起內容",
"放大": "放大", "放大": "放大",
"放大编辑": "放大編輯", "放大编辑": "放大編輯",
...@@ -2653,6 +2655,12 @@ ...@@ -2653,6 +2655,12 @@
"验证配置错误": "驗證設定錯誤", "验证配置错误": "驗證設定錯誤",
"高级设置": "進階設定", "高级设置": "進階設定",
"高级配置": "進階設定", "高级配置": "進階設定",
"核心配置": "核心設定",
"创建渠道所需的基本信息": "建立頻道所需的基本資訊",
"请求配置": "請求設定",
"渠道行为": "頻道行為",
"额外设置": "額外設定",
"上游模型管理": "上游模型管理",
"黑名单": "黑名單", "黑名单": "黑名單",
"默认": "預設", "默认": "預設",
"默认 API 版本": "預設 API 版本", "默认 API 版本": "預設 API 版本",
......
...@@ -971,3 +971,26 @@ html.dark .with-pastel-balls::before { ...@@ -971,3 +971,26 @@ html.dark .with-pastel-balls::before {
.semi-datepicker-range-input { .semi-datepicker-range-input {
border-radius: 10px !important; border-radius: 10px !important;
} }
@keyframes slideInLeft {
from {
transform: translateX(-100%);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
/* Channel advanced settings side panel animations */
@keyframes slideInRight {
from {
transform: translateX(100%);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment