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:
CaIon
2026-03-31 19:34:18 +08:00
parent d22f889e5d
commit 8bb9a42f68
13 changed files with 251 additions and 28 deletions
@@ -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 */}