feat(channel-ui): support multi-JSON batch creation for Vertex AI & multi-to-single mode

WHY
• Backend (0089157) now accepts structured request `{ mode, channel }`, including new `multi_to_single`.
• Need front-end to upload multiple service-account JSON files and generate correct `channel.key`.
• Improve UX: avoid red “uploadFail” state and offer drag-and-drop UI.

WHAT
1. EditChannel.js
   • Added Upload drag-area with IconBolt; `uploadTrigger="custom"`.
   • `handleJsonFileUpload` reads file, pushes content to `jsonFiles`, returns `{ shouldUpload:false, status:'success' }`.
   • New states: `batch`, `mergeToSingle`, `jsonFiles`.
   • Dynamic mode resolver: `single` | `batch` | `multi_to_single`.
   • Builds `channel.key` as JSON-object whose keys are the raw credential texts.
   • UI:
     – “Batch create” checkbox (new build only).
     – Nested “Merge to single channel (multi-key mode)” checkbox enabled when batch=true.
     – Real-time file count display.

2. Upload UX
   • Drag-and-drop, accepts `.json,application/json`.
   • Custom texts: “Click or drop files here” / “JSON credentials only”.
   • Eliminated mandatory `action` warning (`action="#"`).

3. Misc
   • Included IconBolt import.
   • Safeguard toggles reset logic to prevent stale state.

RESULT
Front-end now fully aligns with enhanced AddChannel API:
• Supports Vertex AI multi JSON batch creation.
• Supports new `multi_to_single` flow.
• Clean user feedback with successful file status.
This commit is contained in:
Apple\Apple
2025-06-16 01:47:41 +08:00
parent aa793088ed
commit 617c8e8f4f

View File

@@ -25,6 +25,7 @@ import {
ImagePreview,
Card,
Tag,
Upload,
} from '@douyinfe/semi-ui';
import { getChannelModels } from '../../helpers';
import {
@@ -34,6 +35,7 @@ import {
IconSetting,
IconCode,
IconGlobe,
IconBolt,
} from '@douyinfe/semi-icons';
const { Text, Title } = Typography;
@@ -97,8 +99,9 @@ const EditChannel = (props) => {
tag: '',
};
const [batch, setBatch] = useState(false);
const [mergeToSingle, setMergeToSingle] = useState(false);
const [autoBan, setAutoBan] = useState(true);
// const [autoBan, setAutoBan] = useState(true);
const [jsonFiles, setJsonFiles] = useState([]);
const [inputs, setInputs] = useState(originInputs);
const [originModelOptions, setOriginModelOptions] = useState([]);
const [modelOptions, setModelOptions] = useState([]);
@@ -325,9 +328,20 @@ const EditChannel = (props) => {
}, [props.editingChannel.id]);
const submit = async () => {
if (!isEdit && (inputs.name === '' || inputs.key === '')) {
showInfo(t('请填写渠道名称和渠道密钥!'));
return;
if (!isEdit) {
if (inputs.name === '') {
showInfo(t('请填写渠道名称!'));
return;
}
if (inputs.type === 41 && batch) {
if (jsonFiles.length === 0) {
showInfo(t('请至少选择一个 JSON 凭证文件!'));
return;
}
} else if (inputs.key === '') {
showInfo(t('请填写渠道密钥!'));
return;
}
}
if (inputs.models.length === 0) {
showInfo(t('请至少选择一个模型!'));
@@ -356,13 +370,32 @@ const EditChannel = (props) => {
localInputs.auto_ban = autoBan ? 1 : 0;
localInputs.models = localInputs.models.join(',');
localInputs.group = localInputs.groups.join(',');
if (inputs.type === 41 && batch) {
const keyObj = {};
jsonFiles.forEach((content, idx) => {
keyObj[content] = idx;
});
localInputs.key = JSON.stringify(keyObj);
}
let mode = 'single';
if (batch) {
mode = mergeToSingle ? 'multi_to_single' : 'batch';
}
const payload = {
mode,
channel: localInputs,
};
if (isEdit) {
res = await API.put(`/api/channel/`, {
...localInputs,
id: parseInt(channelId),
});
} else {
res = await API.post(`/api/channel/`, localInputs);
res = await API.post(`/api/channel/`, payload);
}
const { success, message } = res.data;
if (success) {
@@ -415,6 +448,18 @@ const EditChannel = (props) => {
}
};
const handleJsonFileUpload = (file) => {
return new Promise((resolve) => {
const reader = new FileReader();
reader.onload = (e) => {
const content = e.target.result;
setJsonFiles((prev) => [...prev, content]);
resolve({ shouldUpload: false, status: 'success' });
};
reader.readAsText(file);
});
};
return (
<>
<SideSheet
@@ -522,72 +567,107 @@ const EditChannel = (props) => {
<div>
<Text strong className="block mb-2">{t('密钥')}</Text>
{batch ? (
<TextArea
name='key'
required
placeholder={t('请输入密钥,一行一个')}
onChange={(value) => {
handleInputChange('key', value);
}}
value={inputs.key}
style={{ minHeight: 150, fontFamily: 'JetBrains Mono, Consolas' }}
autoComplete='new-password'
className="!rounded-lg"
/>
inputs.type === 41 ? (
<Upload
uploadTrigger="custom"
action="#"
accept=".json,application/json"
multiple
beforeUpload={handleJsonFileUpload}
showUploadList
draggable={true}
dragIcon={<IconBolt />}
dragMainText={t('点击上传文件或拖拽文件到这里')}
dragSubText={t('仅支持 JSON 格式的凭证文件')}
style={{ marginTop: 10 }}
/>
) : (
<TextArea
name="key"
required
placeholder={t('请输入密钥,一行一个')}
onChange={(value) => {
handleInputChange('key', value);
}}
value={inputs.key}
style={{ minHeight: 150, fontFamily: 'JetBrains Mono, Consolas' }}
autoComplete="new-password"
className="!rounded-lg"
/>
)
) : (
<>
{inputs.type === 41 ? (
<TextArea
name='key'
required
placeholder={
'{\n' +
' "type": "service_account",\n' +
' "project_id": "abc-bcd-123-456",\n' +
' "private_key_id": "123xxxxx456",\n' +
' "private_key": "-----BEGIN PRIVATE KEY-----xxxx\n' +
' "client_email": "xxx@developer.gserviceaccount.com",\n' +
' "client_id": "111222333",\n' +
' "auth_uri": "https://accounts.google.com/o/oauth2/auth",\n' +
' "token_uri": "https://oauth2.googleapis.com/token",\n' +
' "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",\n' +
' "client_x509_cert_url": "https://xxxxx.gserviceaccount.com",\n' +
' "universe_domain": "googleapis.com"\n' +
'}'
}
onChange={(value) => {
handleInputChange('key', value);
}}
autosize={{ minRows: 10 }}
value={inputs.key}
autoComplete='new-password'
className="!rounded-lg font-mono"
/>
) : (
<Input
name='key'
required
placeholder={t(type2secretPrompt(inputs.type))}
onChange={(value) => {
handleInputChange('key', value);
}}
value={inputs.key}
autoComplete='new-password'
size="large"
className="!rounded-lg"
/>
)}
</>
inputs.type === 41 ? (
<TextArea
name="key"
required
placeholder={
'{\n' +
' "type": "service_account",\n' +
' "project_id": "abc-bcd-123-456",\n' +
' "private_key_id": "123xxxxx456",\n' +
' "private_key": "-----BEGIN PRIVATE KEY-----xxxx\n' +
' "client_email": "xxx@developer.gserviceaccount.com",\n' +
' "client_id": "111222333",\n' +
' "auth_uri": "https://accounts.google.com/o/oauth2/auth",\n' +
' "token_uri": "https://oauth2.googleapis.com/token",\n' +
' "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",\n' +
' "client_x509_cert_url": "https://xxxxx.gserviceaccount.com",\n' +
' "universe_domain": "googleapis.com"\n' +
'}'
}
onChange={(value) => {
handleInputChange('key', value);
}}
autosize={{ minRows: 10 }}
value={inputs.key}
autoComplete="new-password"
className="!rounded-lg font-mono"
/>
) : (
<Input
name="key"
required
placeholder={t(type2secretPrompt(inputs.type))}
onChange={(value) => {
handleInputChange('key', value);
}}
value={inputs.key}
autoComplete="new-password"
size="large"
className="!rounded-lg"
/>
)
)}
</div>
{!isEdit && (
<div className="flex items-center">
<Checkbox
checked={batch}
onChange={() => setBatch(!batch)}
/>
<Text strong className="ml-2">{t('批量创建')}</Text>
<div className="flex flex-col mt-2 gap-2">
<div className="flex items-center">
<Checkbox
checked={batch}
onChange={() => {
setBatch(!batch);
if (!batch === false) setMergeToSingle(false);
}}
/>
<Text strong className="ml-2">{t('批量创建')}</Text>
{inputs.type === 41 && batch && (
<Text type='tertiary' className='ml-2'>
{t('已选择 {{count}} 个文件', { count: jsonFiles.length })}
</Text>
)}
</div>
{batch && (
<div className="flex items-center pl-6">
<Checkbox
checked={mergeToSingle}
onChange={() => setMergeToSingle(!mergeToSingle)}
/>
<Text style={{ fontSize: 12 }} className="ml-2 text-gray-600">
{t('合并为单通道(多 Key 模式)')}
</Text>
</div>
)}
</div>
)}
</div>