|
@@ -26,6 +26,7 @@ import {
|
|
|
Form,
|
|
Form,
|
|
|
Row,
|
|
Row,
|
|
|
Col,
|
|
Col,
|
|
|
|
|
+ Upload,
|
|
|
} from '@douyinfe/semi-ui';
|
|
} from '@douyinfe/semi-ui';
|
|
|
import { getChannelModels, copy } from '../../helpers';
|
|
import { getChannelModels, copy } from '../../helpers';
|
|
|
import {
|
|
import {
|
|
@@ -35,6 +36,7 @@ import {
|
|
|
IconSetting,
|
|
IconSetting,
|
|
|
IconCode,
|
|
IconCode,
|
|
|
IconGlobe,
|
|
IconGlobe,
|
|
|
|
|
+ IconBolt,
|
|
|
} from '@douyinfe/semi-icons';
|
|
} from '@douyinfe/semi-icons';
|
|
|
|
|
|
|
|
const { Text, Title } = Typography;
|
|
const { Text, Title } = Typography;
|
|
@@ -100,8 +102,11 @@ const EditChannel = (props) => {
|
|
|
priority: 0,
|
|
priority: 0,
|
|
|
weight: 0,
|
|
weight: 0,
|
|
|
tag: '',
|
|
tag: '',
|
|
|
|
|
+ multi_key_mode: 'random',
|
|
|
};
|
|
};
|
|
|
const [batch, setBatch] = useState(false);
|
|
const [batch, setBatch] = useState(false);
|
|
|
|
|
+ const [multiToSingle, setMultiToSingle] = useState(false);
|
|
|
|
|
+ const [multiKeyMode, setMultiKeyMode] = useState('random');
|
|
|
const [autoBan, setAutoBan] = useState(true);
|
|
const [autoBan, setAutoBan] = useState(true);
|
|
|
// const [autoBan, setAutoBan] = useState(true);
|
|
// const [autoBan, setAutoBan] = useState(true);
|
|
|
const [inputs, setInputs] = useState(originInputs);
|
|
const [inputs, setInputs] = useState(originInputs);
|
|
@@ -114,6 +119,9 @@ const EditChannel = (props) => {
|
|
|
const [modalImageUrl, setModalImageUrl] = useState('');
|
|
const [modalImageUrl, setModalImageUrl] = useState('');
|
|
|
const [isModalOpenurl, setIsModalOpenurl] = useState(false);
|
|
const [isModalOpenurl, setIsModalOpenurl] = useState(false);
|
|
|
const formApiRef = useRef(null);
|
|
const formApiRef = useRef(null);
|
|
|
|
|
+ const [vertexKeys, setVertexKeys] = useState([]);
|
|
|
|
|
+ const [vertexFileList, setVertexFileList] = useState([]);
|
|
|
|
|
+ const vertexErroredNames = useRef(new Set()); // 避免重复报错
|
|
|
const getInitValues = () => ({ ...originInputs });
|
|
const getInitValues = () => ({ ...originInputs });
|
|
|
const handleInputChange = (name, value) => {
|
|
const handleInputChange = (name, value) => {
|
|
|
if (formApiRef.current) {
|
|
if (formApiRef.current) {
|
|
@@ -377,10 +385,72 @@ const EditChannel = (props) => {
|
|
|
}
|
|
}
|
|
|
}, [props.visible, channelId]);
|
|
}, [props.visible, channelId]);
|
|
|
|
|
|
|
|
|
|
+ const handleVertexUploadChange = ({ fileList }) => {
|
|
|
|
|
+ (async () => {
|
|
|
|
|
+ const validFiles = [];
|
|
|
|
|
+ const keys = [];
|
|
|
|
|
+ const errorNames = [];
|
|
|
|
|
+ for (const item of fileList) {
|
|
|
|
|
+ const fileObj = item.fileInstance;
|
|
|
|
|
+ if (!fileObj) continue;
|
|
|
|
|
+ try {
|
|
|
|
|
+ const txt = await fileObj.text();
|
|
|
|
|
+ keys.push(JSON.parse(txt));
|
|
|
|
|
+ validFiles.push(item); // 仅合法文件加入列表
|
|
|
|
|
+ } catch (err) {
|
|
|
|
|
+ if (!vertexErroredNames.current.has(item.name)) {
|
|
|
|
|
+ errorNames.push(item.name);
|
|
|
|
|
+ vertexErroredNames.current.add(item.name);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ setVertexKeys(keys);
|
|
|
|
|
+ setVertexFileList(validFiles);
|
|
|
|
|
+ if (formApiRef.current) {
|
|
|
|
|
+ formApiRef.current.setValue('vertex_files', validFiles);
|
|
|
|
|
+ }
|
|
|
|
|
+ setInputs((prev) => ({ ...prev, vertex_files: validFiles }));
|
|
|
|
|
+
|
|
|
|
|
+ if (errorNames.length > 0) {
|
|
|
|
|
+ showError(t('以下文件解析失败,已忽略:{{list}}', { list: errorNames.join(', ') }));
|
|
|
|
|
+ }
|
|
|
|
|
+ })();
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
const submit = async () => {
|
|
const submit = async () => {
|
|
|
const formValues = formApiRef.current ? formApiRef.current.getValues() : {};
|
|
const formValues = formApiRef.current ? formApiRef.current.getValues() : {};
|
|
|
let localInputs = { ...formValues };
|
|
let localInputs = { ...formValues };
|
|
|
|
|
|
|
|
|
|
+ if (localInputs.type === 41 && batch) {
|
|
|
|
|
+ let keys = vertexKeys;
|
|
|
|
|
+ if (keys.length === 0) {
|
|
|
|
|
+ // 确保提交时也能解析,避免因异步延迟导致 keys 为空
|
|
|
|
|
+ try {
|
|
|
|
|
+ const parsed = await Promise.all(
|
|
|
|
|
+ vertexFileList.map(async (item) => {
|
|
|
|
|
+ const fileObj = item.fileInstance;
|
|
|
|
|
+ if (!fileObj) return null;
|
|
|
|
|
+ const txt = await fileObj.text();
|
|
|
|
|
+ return JSON.parse(txt);
|
|
|
|
|
+ })
|
|
|
|
|
+ );
|
|
|
|
|
+ keys = parsed.filter(Boolean);
|
|
|
|
|
+ } catch (err) {
|
|
|
|
|
+ showError(t('解析密钥文件失败: {{msg}}', { msg: err.message }));
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (keys.length === 0) {
|
|
|
|
|
+ showInfo(t('请上传密钥文件!'));
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ localInputs.key = JSON.stringify(keys);
|
|
|
|
|
+ }
|
|
|
|
|
+ delete localInputs.vertex_files;
|
|
|
|
|
+
|
|
|
if (!isEdit && (!localInputs.name || !localInputs.key)) {
|
|
if (!isEdit && (!localInputs.name || !localInputs.key)) {
|
|
|
showInfo(t('请填写渠道名称和渠道密钥!'));
|
|
showInfo(t('请填写渠道名称和渠道密钥!'));
|
|
|
return;
|
|
return;
|
|
@@ -406,13 +476,23 @@ const EditChannel = (props) => {
|
|
|
localInputs.auto_ban = localInputs.auto_ban ? 1 : 0;
|
|
localInputs.auto_ban = localInputs.auto_ban ? 1 : 0;
|
|
|
localInputs.models = localInputs.models.join(',');
|
|
localInputs.models = localInputs.models.join(',');
|
|
|
localInputs.group = (localInputs.groups || []).join(',');
|
|
localInputs.group = (localInputs.groups || []).join(',');
|
|
|
|
|
+
|
|
|
|
|
+ let mode = 'single';
|
|
|
|
|
+ if (batch) {
|
|
|
|
|
+ mode = multiToSingle ? 'multi_to_single' : 'batch';
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
if (isEdit) {
|
|
if (isEdit) {
|
|
|
res = await API.put(`/api/channel/`, {
|
|
res = await API.put(`/api/channel/`, {
|
|
|
...localInputs,
|
|
...localInputs,
|
|
|
id: parseInt(channelId),
|
|
id: parseInt(channelId),
|
|
|
});
|
|
});
|
|
|
} else {
|
|
} else {
|
|
|
- res = await API.post(`/api/channel/`, localInputs);
|
|
|
|
|
|
|
+ res = await API.post(`/api/channel/`, {
|
|
|
|
|
+ mode: mode,
|
|
|
|
|
+ multi_key_mode: mode === 'multi_to_single' ? multiKeyMode : undefined,
|
|
|
|
|
+ channel: localInputs,
|
|
|
|
|
+ });
|
|
|
}
|
|
}
|
|
|
const { success, message } = res.data;
|
|
const { success, message } = res.data;
|
|
|
if (success) {
|
|
if (success) {
|
|
@@ -465,9 +545,31 @@ const EditChannel = (props) => {
|
|
|
}
|
|
}
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
- const batchAllowed = !isEdit && inputs.type !== 41;
|
|
|
|
|
|
|
+ const batchAllowed = !isEdit;
|
|
|
const batchExtra = batchAllowed ? (
|
|
const batchExtra = batchAllowed ? (
|
|
|
- <Checkbox checked={batch} onChange={() => setBatch(!batch)}>{t('批量创建')}</Checkbox>
|
|
|
|
|
|
|
+ <Space>
|
|
|
|
|
+ <Checkbox checked={batch} onChange={() => {
|
|
|
|
|
+ setBatch(!batch);
|
|
|
|
|
+ if (batch) {
|
|
|
|
|
+ setMultiToSingle(false);
|
|
|
|
|
+ setMultiKeyMode('random');
|
|
|
|
|
+ }
|
|
|
|
|
+ }}>{t('批量创建')}</Checkbox>
|
|
|
|
|
+ {batch && (
|
|
|
|
|
+ <Checkbox checked={multiToSingle} onChange={() => {
|
|
|
|
|
+ setMultiToSingle(prev => !prev);
|
|
|
|
|
+ setInputs(prev => {
|
|
|
|
|
+ const newInputs = { ...prev };
|
|
|
|
|
+ if (!multiToSingle) {
|
|
|
|
|
+ newInputs.multi_key_mode = multiKeyMode;
|
|
|
|
|
+ } else {
|
|
|
|
|
+ delete newInputs.multi_key_mode;
|
|
|
|
|
+ }
|
|
|
|
|
+ return newInputs;
|
|
|
|
|
+ });
|
|
|
|
|
+ }}>{t('密钥聚合模式')}</Checkbox>
|
|
|
|
|
+ )}
|
|
|
|
|
+ </Space>
|
|
|
) : null;
|
|
) : null;
|
|
|
|
|
|
|
|
return (
|
|
return (
|
|
@@ -553,16 +655,37 @@ const EditChannel = (props) => {
|
|
|
/>
|
|
/>
|
|
|
|
|
|
|
|
{batch ? (
|
|
{batch ? (
|
|
|
- <Form.TextArea
|
|
|
|
|
- field='key'
|
|
|
|
|
- label={t('密钥')}
|
|
|
|
|
- placeholder={t('请输入密钥,一行一个')}
|
|
|
|
|
- rules={isEdit ? [] : [{ required: true, message: t('请输入密钥') }]}
|
|
|
|
|
- autosize={{ minRows: 6, maxRows: 6 }}
|
|
|
|
|
- autoComplete='new-password'
|
|
|
|
|
- onChange={(value) => handleInputChange('key', value)}
|
|
|
|
|
- extraText={batchExtra}
|
|
|
|
|
- />
|
|
|
|
|
|
|
+ inputs.type === 41 ? (
|
|
|
|
|
+ <Form.Upload
|
|
|
|
|
+ field='vertex_files'
|
|
|
|
|
+ label={t('密钥文件 (.json)')}
|
|
|
|
|
+ accept='.json'
|
|
|
|
|
+ multiple
|
|
|
|
|
+ draggable
|
|
|
|
|
+ dragIcon={<IconBolt />}
|
|
|
|
|
+ dragMainText={t('点击上传文件或拖拽文件到这里')}
|
|
|
|
|
+ dragSubText={t('仅支持 JSON 文件,支持多文件')}
|
|
|
|
|
+ style={{ marginTop: 10 }}
|
|
|
|
|
+ uploadTrigger='custom'
|
|
|
|
|
+ beforeUpload={() => false}
|
|
|
|
|
+ onChange={handleVertexUploadChange}
|
|
|
|
|
+ fileList={vertexFileList}
|
|
|
|
|
+ rules={isEdit ? [] : [{ required: true, message: t('请上传密钥文件') }]}
|
|
|
|
|
+ extraText={batchExtra}
|
|
|
|
|
+ />
|
|
|
|
|
+ ) : (
|
|
|
|
|
+ <Form.TextArea
|
|
|
|
|
+ field='key'
|
|
|
|
|
+ label={t('密钥')}
|
|
|
|
|
+ placeholder={t('请输入密钥,一行一个')}
|
|
|
|
|
+ rules={isEdit ? [] : [{ required: true, message: t('请输入密钥') }]}
|
|
|
|
|
+ autosize
|
|
|
|
|
+ autoComplete='new-password'
|
|
|
|
|
+ onChange={(value) => handleInputChange('key', value)}
|
|
|
|
|
+ extraText={batchExtra}
|
|
|
|
|
+ showClear
|
|
|
|
|
+ />
|
|
|
|
|
+ )
|
|
|
) : (
|
|
) : (
|
|
|
<>
|
|
<>
|
|
|
{inputs.type === 41 ? (
|
|
{inputs.type === 41 ? (
|
|
@@ -585,10 +708,11 @@ const EditChannel = (props) => {
|
|
|
'}'
|
|
'}'
|
|
|
}
|
|
}
|
|
|
rules={isEdit ? [] : [{ required: true, message: t('请输入密钥') }]}
|
|
rules={isEdit ? [] : [{ required: true, message: t('请输入密钥') }]}
|
|
|
- autosize={{ minRows: 10 }}
|
|
|
|
|
|
|
+ autosize
|
|
|
autoComplete='new-password'
|
|
autoComplete='new-password'
|
|
|
onChange={(value) => handleInputChange('key', value)}
|
|
onChange={(value) => handleInputChange('key', value)}
|
|
|
extraText={batchExtra}
|
|
extraText={batchExtra}
|
|
|
|
|
+ showClear
|
|
|
/>
|
|
/>
|
|
|
) : (
|
|
) : (
|
|
|
<Form.Input
|
|
<Form.Input
|
|
@@ -599,10 +723,102 @@ const EditChannel = (props) => {
|
|
|
autoComplete='new-password'
|
|
autoComplete='new-password'
|
|
|
onChange={(value) => handleInputChange('key', value)}
|
|
onChange={(value) => handleInputChange('key', value)}
|
|
|
extraText={batchExtra}
|
|
extraText={batchExtra}
|
|
|
|
|
+ showClear
|
|
|
/>
|
|
/>
|
|
|
)}
|
|
)}
|
|
|
</>
|
|
</>
|
|
|
)}
|
|
)}
|
|
|
|
|
+
|
|
|
|
|
+ {batch && multiToSingle && (
|
|
|
|
|
+ <Form.Select
|
|
|
|
|
+ field='multi_key_mode'
|
|
|
|
|
+ label={t('密钥聚合模式')}
|
|
|
|
|
+ placeholder={t('请选择多密钥使用策略')}
|
|
|
|
|
+ optionList={[
|
|
|
|
|
+ { label: t('随机'), value: 'random' },
|
|
|
|
|
+ { label: t('轮询'), value: 'polling' },
|
|
|
|
|
+ ]}
|
|
|
|
|
+ style={{ width: '100%' }}
|
|
|
|
|
+ value={inputs.multi_key_mode || 'random'}
|
|
|
|
|
+ onChange={(value) => {
|
|
|
|
|
+ setMultiKeyMode(value);
|
|
|
|
|
+ handleInputChange('multi_key_mode', value);
|
|
|
|
|
+ }}
|
|
|
|
|
+ />
|
|
|
|
|
+ )}
|
|
|
|
|
+
|
|
|
|
|
+ {inputs.type === 18 && (
|
|
|
|
|
+ <Form.Input
|
|
|
|
|
+ field='other'
|
|
|
|
|
+ label={t('模型版本')}
|
|
|
|
|
+ placeholder={'请输入星火大模型版本,注意是接口地址中的版本号,例如:v2.1'}
|
|
|
|
|
+ onChange={(value) => handleInputChange('other', value)}
|
|
|
|
|
+ showClear
|
|
|
|
|
+ />
|
|
|
|
|
+ )}
|
|
|
|
|
+
|
|
|
|
|
+ {inputs.type === 41 && (
|
|
|
|
|
+ <Form.TextArea
|
|
|
|
|
+ field='other'
|
|
|
|
|
+ label={t('部署地区')}
|
|
|
|
|
+ placeholder={t(
|
|
|
|
|
+ '请输入部署地区,例如:us-central1\n支持使用模型映射格式\n{\n "default": "us-central1",\n "claude-3-5-sonnet-20240620": "europe-west1"\n}'
|
|
|
|
|
+ )}
|
|
|
|
|
+ autosize
|
|
|
|
|
+ onChange={(value) => handleInputChange('other', value)}
|
|
|
|
|
+ rules={[{ required: true, message: t('请填写部署地区') }]}
|
|
|
|
|
+ extraText={
|
|
|
|
|
+ <Text
|
|
|
|
|
+ className="!text-semi-color-primary cursor-pointer"
|
|
|
|
|
+ onClick={() => handleInputChange('other', JSON.stringify(REGION_EXAMPLE, null, 2))}
|
|
|
|
|
+ >
|
|
|
|
|
+ {t('填入模板')}
|
|
|
|
|
+ </Text>
|
|
|
|
|
+ }
|
|
|
|
|
+ showClear
|
|
|
|
|
+ />
|
|
|
|
|
+ )}
|
|
|
|
|
+
|
|
|
|
|
+ {inputs.type === 21 && (
|
|
|
|
|
+ <Form.Input
|
|
|
|
|
+ field='other'
|
|
|
|
|
+ label={t('知识库 ID')}
|
|
|
|
|
+ placeholder={'请输入知识库 ID,例如:123456'}
|
|
|
|
|
+ onChange={(value) => handleInputChange('other', value)}
|
|
|
|
|
+ showClear
|
|
|
|
|
+ />
|
|
|
|
|
+ )}
|
|
|
|
|
+
|
|
|
|
|
+ {inputs.type === 39 && (
|
|
|
|
|
+ <Form.Input
|
|
|
|
|
+ field='other'
|
|
|
|
|
+ label='Account ID'
|
|
|
|
|
+ placeholder={'请输入Account ID,例如:d6b5da8hk1awo8nap34ube6gh'}
|
|
|
|
|
+ onChange={(value) => handleInputChange('other', value)}
|
|
|
|
|
+ showClear
|
|
|
|
|
+ />
|
|
|
|
|
+ )}
|
|
|
|
|
+
|
|
|
|
|
+ {inputs.type === 49 && (
|
|
|
|
|
+ <Form.Input
|
|
|
|
|
+ field='other'
|
|
|
|
|
+ label={t('智能体ID')}
|
|
|
|
|
+ placeholder={'请输入智能体ID,例如:7342866812345'}
|
|
|
|
|
+ onChange={(value) => handleInputChange('other', value)}
|
|
|
|
|
+ showClear
|
|
|
|
|
+ />
|
|
|
|
|
+ )}
|
|
|
|
|
+
|
|
|
|
|
+ {inputs.type === 1 && (
|
|
|
|
|
+ <Form.Input
|
|
|
|
|
+ field='openai_organization'
|
|
|
|
|
+ label={t('组织')}
|
|
|
|
|
+ placeholder={t('请输入组织org-xxx')}
|
|
|
|
|
+ showClear
|
|
|
|
|
+ helpText={t('组织,不填则为默认组织')}
|
|
|
|
|
+ onChange={(value) => handleInputChange('openai_organization', value)}
|
|
|
|
|
+ />
|
|
|
|
|
+ )}
|
|
|
</Card>
|
|
</Card>
|
|
|
|
|
|
|
|
{/* API Configuration Card */}
|
|
{/* API Configuration Card */}
|
|
@@ -860,77 +1076,6 @@ const EditChannel = (props) => {
|
|
|
onChange={(value) => handleInputChange('groups', value)}
|
|
onChange={(value) => handleInputChange('groups', value)}
|
|
|
/>
|
|
/>
|
|
|
|
|
|
|
|
- {inputs.type === 18 && (
|
|
|
|
|
- <Form.Input
|
|
|
|
|
- field='other'
|
|
|
|
|
- label={t('模型版本')}
|
|
|
|
|
- placeholder={'请输入星火大模型版本,注意是接口地址中的版本号,例如:v2.1'}
|
|
|
|
|
- onChange={(value) => handleInputChange('other', value)}
|
|
|
|
|
- showClear
|
|
|
|
|
- />
|
|
|
|
|
- )}
|
|
|
|
|
-
|
|
|
|
|
- {inputs.type === 41 && (
|
|
|
|
|
- <Form.TextArea
|
|
|
|
|
- field='other'
|
|
|
|
|
- label={t('部署地区')}
|
|
|
|
|
- placeholder={t(
|
|
|
|
|
- '请输入部署地区,例如:us-central1\n支持使用模型映射格式\n{\n "default": "us-central1",\n "claude-3-5-sonnet-20240620": "europe-west1"\n}'
|
|
|
|
|
- )}
|
|
|
|
|
- autosize={{ minRows: 2 }}
|
|
|
|
|
- onChange={(value) => handleInputChange('other', value)}
|
|
|
|
|
- extraText={
|
|
|
|
|
- <Text
|
|
|
|
|
- className="!text-semi-color-primary cursor-pointer"
|
|
|
|
|
- onClick={() => handleInputChange('other', JSON.stringify(REGION_EXAMPLE, null, 2))}
|
|
|
|
|
- >
|
|
|
|
|
- {t('填入模板')}
|
|
|
|
|
- </Text>
|
|
|
|
|
- }
|
|
|
|
|
- />
|
|
|
|
|
- )}
|
|
|
|
|
-
|
|
|
|
|
- {inputs.type === 21 && (
|
|
|
|
|
- <Form.Input
|
|
|
|
|
- field='other'
|
|
|
|
|
- label={t('知识库 ID')}
|
|
|
|
|
- placeholder={'请输入知识库 ID,例如:123456'}
|
|
|
|
|
- onChange={(value) => handleInputChange('other', value)}
|
|
|
|
|
- showClear
|
|
|
|
|
- />
|
|
|
|
|
- )}
|
|
|
|
|
-
|
|
|
|
|
- {inputs.type === 39 && (
|
|
|
|
|
- <Form.Input
|
|
|
|
|
- field='other'
|
|
|
|
|
- label='Account ID'
|
|
|
|
|
- placeholder={'请输入Account ID,例如:d6b5da8hk1awo8nap34ube6gh'}
|
|
|
|
|
- onChange={(value) => handleInputChange('other', value)}
|
|
|
|
|
- showClear
|
|
|
|
|
- />
|
|
|
|
|
- )}
|
|
|
|
|
-
|
|
|
|
|
- {inputs.type === 49 && (
|
|
|
|
|
- <Form.Input
|
|
|
|
|
- field='other'
|
|
|
|
|
- label={t('智能体ID')}
|
|
|
|
|
- placeholder={'请输入智能体ID,例如:7342866812345'}
|
|
|
|
|
- onChange={(value) => handleInputChange('other', value)}
|
|
|
|
|
- showClear
|
|
|
|
|
- />
|
|
|
|
|
- )}
|
|
|
|
|
-
|
|
|
|
|
- {inputs.type === 1 && (
|
|
|
|
|
- <Form.Input
|
|
|
|
|
- field='openai_organization'
|
|
|
|
|
- label={t('组织')}
|
|
|
|
|
- placeholder={t('请输入组织org-xxx')}
|
|
|
|
|
- showClear
|
|
|
|
|
- helpText={t('组织,可选,不填则为默认组织')}
|
|
|
|
|
- onChange={(value) => handleInputChange('openai_organization', value)}
|
|
|
|
|
- />
|
|
|
|
|
- )}
|
|
|
|
|
-
|
|
|
|
|
<Form.Input
|
|
<Form.Input
|
|
|
field='tag'
|
|
field='tag'
|
|
|
label={t('渠道标签')}
|
|
label={t('渠道标签')}
|