feat: add clipboard magic string for quick channel creation from token copy
When copying a token, users can now choose "Copy Connection String" which encodes both the API key and server URL as a JSON clipboard payload (type: newapi_channel_conn). When opening the channel creation form, the clipboard is auto-detected and a banner offers to fill key + base_url, eliminating repeated tab-switching when connecting to another new-api instance.
This commit is contained in:
@@ -67,6 +67,7 @@ import SecureVerificationModal from '../../../common/modals/SecureVerificationMo
|
||||
import StatusCodeRiskGuardModal from './StatusCodeRiskGuardModal';
|
||||
import ChannelKeyDisplay from '../../../common/ui/ChannelKeyDisplay';
|
||||
import { useSecureVerification } from '../../../../hooks/common/useSecureVerification';
|
||||
import { parseChannelConnectionString } from '../../../../helpers/token';
|
||||
import { createApiCalls } from '../../../../services/secureVerification';
|
||||
import {
|
||||
collectInvalidStatusCodeEntries,
|
||||
@@ -398,6 +399,9 @@ const EditChannelModal = (props) => {
|
||||
[],
|
||||
);
|
||||
|
||||
// 剪贴板连接信息自动检测
|
||||
const [clipboardConfig, setClipboardConfig] = useState(null);
|
||||
|
||||
// 高级设置折叠状态
|
||||
const [advancedSettingsOpen, setAdvancedSettingsOpen] = useState(false);
|
||||
const formContainerRef = useRef(null);
|
||||
@@ -538,6 +542,35 @@ const EditChannelModal = (props) => {
|
||||
handleInputChange('settings', settingsJson);
|
||||
};
|
||||
|
||||
const applyClipboardConfig = (config) => {
|
||||
if (!config) return;
|
||||
setInputs((prev) => ({
|
||||
...prev,
|
||||
key: config.key,
|
||||
base_url: config.url,
|
||||
}));
|
||||
if (formApiRef.current) {
|
||||
formApiRef.current.setValue('key', config.key);
|
||||
formApiRef.current.setValue('base_url', config.url);
|
||||
}
|
||||
setClipboardConfig(null);
|
||||
showSuccess(t('连接信息已填入'));
|
||||
};
|
||||
|
||||
const pasteFromClipboard = async () => {
|
||||
try {
|
||||
const text = await navigator.clipboard.readText();
|
||||
const parsed = parseChannelConnectionString(text);
|
||||
if (parsed) {
|
||||
applyClipboardConfig(parsed);
|
||||
} else {
|
||||
showInfo(t('剪贴板中未检测到连接信息'));
|
||||
}
|
||||
} catch {
|
||||
showError(t('无法读取剪贴板'));
|
||||
}
|
||||
};
|
||||
|
||||
const isIonetLocked = isIonetChannel && isEdit;
|
||||
|
||||
const handleInputChange = (name, value) => {
|
||||
@@ -1269,6 +1302,13 @@ const EditChannelModal = (props) => {
|
||||
loadChannel();
|
||||
} else {
|
||||
formApiRef.current?.setValues(getInitValues());
|
||||
// best-effort clipboard auto-detect for new channels
|
||||
navigator.clipboard.readText().then((text) => {
|
||||
const parsed = parseChannelConnectionString(text);
|
||||
if (parsed) {
|
||||
setClipboardConfig(parsed);
|
||||
}
|
||||
}).catch(() => {});
|
||||
}
|
||||
fetchModelGroups();
|
||||
// 重置手动输入模式状态
|
||||
@@ -1329,6 +1369,8 @@ const EditChannelModal = (props) => {
|
||||
setInputs(getInitValues());
|
||||
// 重置密钥显示状态
|
||||
resetKeyDisplayState();
|
||||
// 重置剪贴板检测状态
|
||||
setClipboardConfig(null);
|
||||
};
|
||||
|
||||
const handleVertexUploadChange = ({ fileList }) => {
|
||||
@@ -2077,14 +2119,27 @@ const EditChannelModal = (props) => {
|
||||
<SideSheet
|
||||
placement={isEdit ? 'right' : 'left'}
|
||||
title={
|
||||
<Space>
|
||||
<Tag color='blue' shape='circle'>
|
||||
{isEdit ? t('编辑') : t('新建')}
|
||||
</Tag>
|
||||
<Title heading={4} className='m-0'>
|
||||
{isEdit ? t('更新渠道信息') : t('创建新的渠道')}
|
||||
</Title>
|
||||
</Space>
|
||||
<div className='flex items-center justify-between w-full'>
|
||||
<Space>
|
||||
<Tag color='blue' shape='circle'>
|
||||
{isEdit ? t('编辑') : t('新建')}
|
||||
</Tag>
|
||||
<Title heading={4} className='m-0'>
|
||||
{isEdit ? t('更新渠道信息') : t('创建新的渠道')}
|
||||
</Title>
|
||||
</Space>
|
||||
{!isEdit && (
|
||||
<Button
|
||||
size='small'
|
||||
type='tertiary'
|
||||
className='ec-dbcd0a3c01b55203 shrink-0'
|
||||
icon={<IconBolt />}
|
||||
onClick={pasteFromClipboard}
|
||||
>
|
||||
{t('从剪贴板粘贴配置')}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
bodyStyle={{ padding: '0' }}
|
||||
visible={props.visible}
|
||||
@@ -2446,6 +2501,34 @@ const EditChannelModal = (props) => {
|
||||
<>
|
||||
<Spin spinning={loading}>
|
||||
<div className='p-2 space-y-3' ref={formContainerRef}>
|
||||
{!isEdit && clipboardConfig && (
|
||||
<Banner
|
||||
type='info'
|
||||
className='ec-dbcd0a3c01b55203'
|
||||
description={
|
||||
<div className='flex items-center justify-between gap-2'>
|
||||
<span>{t('检测到剪贴板中的连接信息')}</span>
|
||||
<div className='flex gap-1'>
|
||||
<Button
|
||||
size='small'
|
||||
theme='solid'
|
||||
type='primary'
|
||||
onClick={() => applyClipboardConfig(clipboardConfig)}
|
||||
>
|
||||
{t('自动填入')}
|
||||
</Button>
|
||||
<Button
|
||||
size='small'
|
||||
type='tertiary'
|
||||
onClick={() => setClipboardConfig(null)}
|
||||
>
|
||||
{t('忽略')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{/* Core Configuration Card - Always Visible */}
|
||||
<Card className='!rounded-2xl shadow-sm border-0'>
|
||||
{/* Header */}
|
||||
|
||||
Reference in New Issue
Block a user