|
@@ -26,6 +26,7 @@ import {
|
|
|
Form,
|
|
Form,
|
|
|
Row,
|
|
Row,
|
|
|
Col,
|
|
Col,
|
|
|
|
|
+ Highlight,
|
|
|
} from '@douyinfe/semi-ui';
|
|
} from '@douyinfe/semi-ui';
|
|
|
import { getChannelModels, copy, getChannelIcon, getModelCategories } from '../../helpers';
|
|
import { getChannelModels, copy, getChannelIcon, getModelCategories } from '../../helpers';
|
|
|
import {
|
|
import {
|
|
@@ -122,6 +123,8 @@ const EditChannel = (props) => {
|
|
|
const [vertexFileList, setVertexFileList] = useState([]);
|
|
const [vertexFileList, setVertexFileList] = useState([]);
|
|
|
const vertexErroredNames = useRef(new Set()); // 避免重复报错
|
|
const vertexErroredNames = useRef(new Set()); // 避免重复报错
|
|
|
const [isMultiKeyChannel, setIsMultiKeyChannel] = useState(false);
|
|
const [isMultiKeyChannel, setIsMultiKeyChannel] = useState(false);
|
|
|
|
|
+ const [channelSearchValue, setChannelSearchValue] = useState('');
|
|
|
|
|
+ const [useManualInput, setUseManualInput] = useState(false); // 是否使用手动输入模式
|
|
|
const getInitValues = () => ({ ...originInputs });
|
|
const getInitValues = () => ({ ...originInputs });
|
|
|
const handleInputChange = (name, value) => {
|
|
const handleInputChange = (name, value) => {
|
|
|
if (formApiRef.current) {
|
|
if (formApiRef.current) {
|
|
@@ -190,6 +193,9 @@ const EditChannel = (props) => {
|
|
|
setInputs((inputs) => ({ ...inputs, models: localModels }));
|
|
setInputs((inputs) => ({ ...inputs, models: localModels }));
|
|
|
}
|
|
}
|
|
|
setBasicModels(localModels);
|
|
setBasicModels(localModels);
|
|
|
|
|
+
|
|
|
|
|
+ // 重置手动输入模式状态
|
|
|
|
|
+ setUseManualInput(false);
|
|
|
}
|
|
}
|
|
|
//setAutoBan
|
|
//setAutoBan
|
|
|
};
|
|
};
|
|
@@ -418,6 +424,8 @@ const EditChannel = (props) => {
|
|
|
} else {
|
|
} else {
|
|
|
formApiRef.current?.setValues(getInitValues());
|
|
formApiRef.current?.setValues(getInitValues());
|
|
|
}
|
|
}
|
|
|
|
|
+ // 重置手动输入模式状态
|
|
|
|
|
+ setUseManualInput(false);
|
|
|
} else {
|
|
} else {
|
|
|
formApiRef.current?.reset();
|
|
formApiRef.current?.reset();
|
|
|
}
|
|
}
|
|
@@ -468,41 +476,60 @@ const EditChannel = (props) => {
|
|
|
let localInputs = { ...formValues };
|
|
let localInputs = { ...formValues };
|
|
|
|
|
|
|
|
if (localInputs.type === 41) {
|
|
if (localInputs.type === 41) {
|
|
|
- let keys = vertexKeys;
|
|
|
|
|
-
|
|
|
|
|
- // 若当前未选择文件,尝试从已上传文件列表解析(异步读取)
|
|
|
|
|
- if (keys.length === 0 && vertexFileList.length > 0) {
|
|
|
|
|
- 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) {
|
|
|
|
|
- if (!isEdit) {
|
|
|
|
|
- showInfo(t('请上传密钥文件!'));
|
|
|
|
|
|
|
+ if (useManualInput) {
|
|
|
|
|
+ // 手动输入模式
|
|
|
|
|
+ if (localInputs.key && localInputs.key.trim() !== '') {
|
|
|
|
|
+ try {
|
|
|
|
|
+ // 验证 JSON 格式
|
|
|
|
|
+ const parsedKey = JSON.parse(localInputs.key);
|
|
|
|
|
+ // 确保是有效的密钥格式
|
|
|
|
|
+ localInputs.key = JSON.stringify(parsedKey);
|
|
|
|
|
+ } catch (err) {
|
|
|
|
|
+ showError(t('密钥格式无效,请输入有效的 JSON 格式密钥'));
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ } else if (!isEdit) {
|
|
|
|
|
+ showInfo(t('请输入密钥!'));
|
|
|
return;
|
|
return;
|
|
|
- } else {
|
|
|
|
|
- // 编辑模式且未上传新密钥,不修改 key
|
|
|
|
|
- delete localInputs.key;
|
|
|
|
|
}
|
|
}
|
|
|
} else {
|
|
} else {
|
|
|
- // 有新密钥,则覆盖
|
|
|
|
|
- if (batch) {
|
|
|
|
|
- localInputs.key = JSON.stringify(keys);
|
|
|
|
|
|
|
+ // 文件上传模式
|
|
|
|
|
+ let keys = vertexKeys;
|
|
|
|
|
+
|
|
|
|
|
+ // 若当前未选择文件,尝试从已上传文件列表解析(异步读取)
|
|
|
|
|
+ if (keys.length === 0 && vertexFileList.length > 0) {
|
|
|
|
|
+ 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) {
|
|
|
|
|
+ if (!isEdit) {
|
|
|
|
|
+ showInfo(t('请上传密钥文件!'));
|
|
|
|
|
+ return;
|
|
|
|
|
+ } else {
|
|
|
|
|
+ // 编辑模式且未上传新密钥,不修改 key
|
|
|
|
|
+ delete localInputs.key;
|
|
|
|
|
+ }
|
|
|
} else {
|
|
} else {
|
|
|
- localInputs.key = JSON.stringify(keys[0]);
|
|
|
|
|
|
|
+ // 有新密钥,则覆盖
|
|
|
|
|
+ if (batch) {
|
|
|
|
|
+ localInputs.key = JSON.stringify(keys);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ localInputs.key = JSON.stringify(keys[0]);
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
@@ -646,23 +673,33 @@ const EditChannel = (props) => {
|
|
|
if (!checked) {
|
|
if (!checked) {
|
|
|
setMultiToSingle(false);
|
|
setMultiToSingle(false);
|
|
|
setMultiKeyMode('random');
|
|
setMultiKeyMode('random');
|
|
|
|
|
+ } else {
|
|
|
|
|
+ // 批量模式下禁用手动输入,并清空手动输入的内容
|
|
|
|
|
+ setUseManualInput(false);
|
|
|
|
|
+ if (inputs.type === 41) {
|
|
|
|
|
+ // 清空手动输入的密钥内容
|
|
|
|
|
+ if (formApiRef.current) {
|
|
|
|
|
+ formApiRef.current.setValue('key', '');
|
|
|
|
|
+ }
|
|
|
|
|
+ handleInputChange('key', '');
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
}}
|
|
}}
|
|
|
>{t('批量创建')}</Checkbox>
|
|
>{t('批量创建')}</Checkbox>
|
|
|
- {batch && (
|
|
|
|
|
- <Checkbox disabled={isEdit} 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>
|
|
|
|
|
- )}
|
|
|
|
|
|
|
+ {/*{batch && (*/}
|
|
|
|
|
+ {/* <Checkbox disabled={isEdit} 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>
|
|
</Space>
|
|
|
) : null;
|
|
) : null;
|
|
|
|
|
|
|
@@ -670,16 +707,68 @@ const EditChannel = (props) => {
|
|
|
() =>
|
|
() =>
|
|
|
CHANNEL_OPTIONS.map((opt) => ({
|
|
CHANNEL_OPTIONS.map((opt) => ({
|
|
|
...opt,
|
|
...opt,
|
|
|
- label: (
|
|
|
|
|
- <span className="flex items-center gap-2">
|
|
|
|
|
- {getChannelIcon(opt.value)}
|
|
|
|
|
- {opt.label}
|
|
|
|
|
- </span>
|
|
|
|
|
- ),
|
|
|
|
|
|
|
+ // 保持 label 为纯文本以支持搜索
|
|
|
|
|
+ label: opt.label,
|
|
|
})),
|
|
})),
|
|
|
[],
|
|
[],
|
|
|
);
|
|
);
|
|
|
|
|
|
|
|
|
|
+ const renderChannelOption = (renderProps) => {
|
|
|
|
|
+ const {
|
|
|
|
|
+ disabled,
|
|
|
|
|
+ selected,
|
|
|
|
|
+ label,
|
|
|
|
|
+ value,
|
|
|
|
|
+ focused,
|
|
|
|
|
+ className,
|
|
|
|
|
+ style,
|
|
|
|
|
+ onMouseEnter,
|
|
|
|
|
+ onClick,
|
|
|
|
|
+ ...rest
|
|
|
|
|
+ } = renderProps;
|
|
|
|
|
+
|
|
|
|
|
+ const searchWords = channelSearchValue ? [channelSearchValue] : [];
|
|
|
|
|
+
|
|
|
|
|
+ // 构建样式类名
|
|
|
|
|
+ const optionClassName = [
|
|
|
|
|
+ 'flex items-center gap-3 px-3 py-2 transition-all duration-200 rounded-lg mx-2 my-1',
|
|
|
|
|
+ focused && 'bg-blue-50 shadow-sm',
|
|
|
|
|
+ selected && 'bg-blue-100 text-blue-700 shadow-lg ring-2 ring-blue-200 ring-opacity-50',
|
|
|
|
|
+ disabled && 'opacity-50 cursor-not-allowed',
|
|
|
|
|
+ !disabled && 'hover:bg-gray-50 hover:shadow-md cursor-pointer',
|
|
|
|
|
+ className
|
|
|
|
|
+ ].filter(Boolean).join(' ');
|
|
|
|
|
+
|
|
|
|
|
+ return (
|
|
|
|
|
+ <div
|
|
|
|
|
+ style={style}
|
|
|
|
|
+ className={optionClassName}
|
|
|
|
|
+ onClick={() => !disabled && onClick()}
|
|
|
|
|
+ onMouseEnter={e => onMouseEnter()}
|
|
|
|
|
+ >
|
|
|
|
|
+ <div className="flex items-center gap-3 w-full">
|
|
|
|
|
+ <div className="flex-shrink-0 w-5 h-5 flex items-center justify-center">
|
|
|
|
|
+ {getChannelIcon(value)}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div className="flex-1 min-w-0">
|
|
|
|
|
+ <Highlight
|
|
|
|
|
+ sourceString={label}
|
|
|
|
|
+ searchWords={searchWords}
|
|
|
|
|
+ className="text-sm font-medium truncate"
|
|
|
|
|
+ />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ {selected && (
|
|
|
|
|
+ <div className="flex-shrink-0 text-blue-600">
|
|
|
|
|
+ <svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
|
|
|
|
|
+ <path d="M13.78 4.22a.75.75 0 010 1.06l-7.25 7.25a.75.75 0 01-1.06 0L2.22 9.28a.75.75 0 011.06-1.06L6 10.94l6.72-6.72a.75.75 0 011.06 0z"/>
|
|
|
|
|
+ </svg>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ )}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ );
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
return (
|
|
return (
|
|
|
<>
|
|
<>
|
|
|
<SideSheet
|
|
<SideSheet
|
|
@@ -749,6 +838,8 @@ const EditChannel = (props) => {
|
|
|
style={{ width: '100%' }}
|
|
style={{ width: '100%' }}
|
|
|
filter
|
|
filter
|
|
|
searchPosition='dropdown'
|
|
searchPosition='dropdown'
|
|
|
|
|
+ onSearch={(value) => setChannelSearchValue(value)}
|
|
|
|
|
+ renderOptionItem={renderChannelOption}
|
|
|
onChange={(value) => handleInputChange('type', value)}
|
|
onChange={(value) => handleInputChange('type', value)}
|
|
|
/>
|
|
/>
|
|
|
|
|
|
|
@@ -797,22 +888,91 @@ const EditChannel = (props) => {
|
|
|
) : (
|
|
) : (
|
|
|
<>
|
|
<>
|
|
|
{inputs.type === 41 ? (
|
|
{inputs.type === 41 ? (
|
|
|
- <Form.Upload
|
|
|
|
|
- field='vertex_files'
|
|
|
|
|
- label={t('密钥文件 (.json)')}
|
|
|
|
|
- accept='.json'
|
|
|
|
|
- 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}
|
|
|
|
|
- />
|
|
|
|
|
|
|
+ <>
|
|
|
|
|
+ {!batch && (
|
|
|
|
|
+ <div className="flex items-center justify-between mb-3">
|
|
|
|
|
+ <Text className="text-sm font-medium">{t('密钥输入方式')}</Text>
|
|
|
|
|
+ <Space>
|
|
|
|
|
+ <Button
|
|
|
|
|
+ size="small"
|
|
|
|
|
+ type={!useManualInput ? 'primary' : 'tertiary'}
|
|
|
|
|
+ onClick={() => {
|
|
|
|
|
+ setUseManualInput(false);
|
|
|
|
|
+ // 切换到文件上传模式时清空手动输入的密钥
|
|
|
|
|
+ if (formApiRef.current) {
|
|
|
|
|
+ formApiRef.current.setValue('key', '');
|
|
|
|
|
+ }
|
|
|
|
|
+ handleInputChange('key', '');
|
|
|
|
|
+ }}
|
|
|
|
|
+ >
|
|
|
|
|
+ {t('文件上传')}
|
|
|
|
|
+ </Button>
|
|
|
|
|
+ <Button
|
|
|
|
|
+ size="small"
|
|
|
|
|
+ type={useManualInput ? 'primary' : 'tertiary'}
|
|
|
|
|
+ onClick={() => {
|
|
|
|
|
+ setUseManualInput(true);
|
|
|
|
|
+ // 切换到手动输入模式时清空文件上传相关状态
|
|
|
|
|
+ setVertexKeys([]);
|
|
|
|
|
+ setVertexFileList([]);
|
|
|
|
|
+ if (formApiRef.current) {
|
|
|
|
|
+ formApiRef.current.setValue('vertex_files', []);
|
|
|
|
|
+ }
|
|
|
|
|
+ setInputs((prev) => ({ ...prev, vertex_files: [] }));
|
|
|
|
|
+ }}
|
|
|
|
|
+ >
|
|
|
|
|
+ {t('手动输入')}
|
|
|
|
|
+ </Button>
|
|
|
|
|
+ </Space>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ )}
|
|
|
|
|
+
|
|
|
|
|
+ {batch && (
|
|
|
|
|
+ <Banner
|
|
|
|
|
+ type='info'
|
|
|
|
|
+ description={t('批量创建模式下仅支持文件上传,不支持手动输入')}
|
|
|
|
|
+ className='!rounded-lg mb-3'
|
|
|
|
|
+ />
|
|
|
|
|
+ )}
|
|
|
|
|
+
|
|
|
|
|
+ {useManualInput && !batch ? (
|
|
|
|
|
+ <Form.TextArea
|
|
|
|
|
+ field='key'
|
|
|
|
|
+ label={isEdit ? t('密钥(编辑模式下,保存的密钥不会显示)') : t('密钥')}
|
|
|
|
|
+ placeholder={t('请输入 JSON 格式的密钥内容,例如:\n{\n "type": "service_account",\n "project_id": "your-project-id",\n "private_key_id": "...",\n "private_key": "...",\n "client_email": "...",\n "client_id": "...",\n "auth_uri": "...",\n "token_uri": "...",\n "auth_provider_x509_cert_url": "...",\n "client_x509_cert_url": "..."\n}')}
|
|
|
|
|
+ rules={isEdit ? [] : [{ required: true, message: t('请输入密钥') }]}
|
|
|
|
|
+ autoComplete='new-password'
|
|
|
|
|
+ onChange={(value) => handleInputChange('key', value)}
|
|
|
|
|
+ extraText={
|
|
|
|
|
+ <div className="flex items-center gap-2">
|
|
|
|
|
+ <Text type="tertiary" size="small">
|
|
|
|
|
+ {t('请输入完整的 JSON 格式密钥内容')}
|
|
|
|
|
+ </Text>
|
|
|
|
|
+ {batchExtra}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ }
|
|
|
|
|
+ autosize
|
|
|
|
|
+ showClear
|
|
|
|
|
+ />
|
|
|
|
|
+ ) : (
|
|
|
|
|
+ <Form.Upload
|
|
|
|
|
+ field='vertex_files'
|
|
|
|
|
+ label={t('密钥文件 (.json)')}
|
|
|
|
|
+ accept='.json'
|
|
|
|
|
+ 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.Input
|
|
<Form.Input
|
|
|
field='key'
|
|
field='key'
|