✨ feat(channel): Robust Vertex AI batch-key upload & stable multi-key settings
Summary
-------
1. Vertex AI JSON key upload
• Accept multiple `.json` files (drag & drop / click).
• Parse each `fileInstance`; build valid key array.
• Malformed files are skipped and collected into **one** toast message.
• Upload list now仅displays valid files; form state (`vertex_files`) 同步保持.
• On *Submit* keys are re-parsed to prevent async timing loss.
2. Multi-key mode stability
• Added `multi_key_mode: "random"` to initial form values.
• `Form.Select` becomes fully controlled (`value`/`onChange`).
• Toggling the “multi-key mode” checkbox writes default / removes field, so
`setValues(inputs)` no longer clears the user’s choice.
3. UX / compatibility tweaks
• Preserve uploaded files when editing any other field.
• Use `fileInstance` only – compatible with legacy Semi Upload API.
• Removed redundant `limit` prop on `Form.Upload`.
• Aggregated error handling avoids toast spam.
Result
------
Channel creation/update now supports:
• Reliable batch import of Vertex AI service-account keys.
• Consistent retention of multi-key strategy (`random` / `polling`).
• Cleaner, user-friendly error feedback.
This commit is contained in:
@@ -1142,7 +1142,7 @@
|
|||||||
"鉴权json": "Authentication JSON",
|
"鉴权json": "Authentication JSON",
|
||||||
"请输入鉴权json": "Please enter authentication JSON",
|
"请输入鉴权json": "Please enter authentication JSON",
|
||||||
"组织": "Organization",
|
"组织": "Organization",
|
||||||
"组织,可选,不填则为默认组织": "Organization (optional), default if empty",
|
"组织,不填则为默认组织": "Organization, default if empty",
|
||||||
"请输入组织org-xxx": "Please enter organization org-xxx",
|
"请输入组织org-xxx": "Please enter organization org-xxx",
|
||||||
"默认测试模型": "Default Test Model",
|
"默认测试模型": "Default Test Model",
|
||||||
"不填则为模型列表第一个": "First model in list if empty",
|
"不填则为模型列表第一个": "First model in list if empty",
|
||||||
@@ -1756,5 +1756,14 @@
|
|||||||
"生成数量必须大于0": "Generation quantity must be greater than 0",
|
"生成数量必须大于0": "Generation quantity must be greater than 0",
|
||||||
"创建后可在编辑渠道时获取上游模型列表": "After creation, you can get the upstream model list when editing the channel",
|
"创建后可在编辑渠道时获取上游模型列表": "After creation, you can get the upstream model list when editing the channel",
|
||||||
"可用端点类型": "Supported endpoint types",
|
"可用端点类型": "Supported endpoint types",
|
||||||
"未登录,使用默认分组倍率:": "Not logged in, using default group ratio: "
|
"未登录,使用默认分组倍率:": "Not logged in, using default group ratio: ",
|
||||||
|
"密钥聚合模式": "Key aggregation mode",
|
||||||
|
"随机": "Random",
|
||||||
|
"轮询": "Polling",
|
||||||
|
"密钥文件 (.json)": "Key file (.json)",
|
||||||
|
"点击上传文件或拖拽文件到这里": "Click to upload file or drag and drop file here",
|
||||||
|
"仅支持 JSON 文件,支持多文件": "Only JSON files are supported, multiple files are supported",
|
||||||
|
"请上传密钥文件": "Please upload the key file",
|
||||||
|
"请填写部署地区": "Please fill in the deployment region",
|
||||||
|
"请输入部署地区,例如:us-central1\n支持使用模型映射格式\n{\n \"default\": \"us-central1\",\n \"claude-3-5-sonnet-20240620\": \"europe-west1\"\n}": "Please enter the deployment region, for example: us-central1\nSupports using model mapping format\n{\n \"default\": \"us-central1\",\n \"claude-3-5-sonnet-20240620\": \"europe-west1\"\n}"
|
||||||
}
|
}
|
||||||
@@ -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
|
inputs.type === 41 ? (
|
||||||
field='key'
|
<Form.Upload
|
||||||
label={t('密钥')}
|
field='vertex_files'
|
||||||
placeholder={t('请输入密钥,一行一个')}
|
label={t('密钥文件 (.json)')}
|
||||||
rules={isEdit ? [] : [{ required: true, message: t('请输入密钥') }]}
|
accept='.json'
|
||||||
autosize={{ minRows: 6, maxRows: 6 }}
|
multiple
|
||||||
autoComplete='new-password'
|
draggable
|
||||||
onChange={(value) => handleInputChange('key', value)}
|
dragIcon={<IconBolt />}
|
||||||
extraText={batchExtra}
|
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('渠道标签')}
|
||||||
|
|||||||
Reference in New Issue
Block a user