mirror of
https://github.com/QuantumNous/new-api.git
synced 2026-04-21 05:48:38 +00:00
195 lines
5.2 KiB
JavaScript
195 lines
5.2 KiB
JavaScript
/*
|
|
Copyright (C) 2025 QuantumNous
|
|
|
|
This program is free software: you can redistribute it and/or modify
|
|
it under the terms of the GNU Affero General Public License as
|
|
published by the Free Software Foundation, either version 3 of the
|
|
License, or (at your option) any later version.
|
|
|
|
This program is distributed in the hope that it will be useful,
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
GNU Affero General Public License for more details.
|
|
|
|
You should have received a copy of the GNU Affero General Public License
|
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
|
|
For commercial licensing, please contact support@quantumnous.com
|
|
*/
|
|
import React, { useState, useEffect, useMemo } from 'react';
|
|
import {
|
|
Modal,
|
|
RadioGroup,
|
|
Radio,
|
|
Select,
|
|
Input,
|
|
Toast,
|
|
Typography,
|
|
} from '@douyinfe/semi-ui';
|
|
import { useTranslation } from 'react-i18next';
|
|
import { selectFilter } from '../../../../helpers';
|
|
|
|
const APP_CONFIGS = {
|
|
claude: {
|
|
label: 'Claude',
|
|
defaultName: 'My Claude',
|
|
modelFields: [
|
|
{ key: 'model', label: '主模型' },
|
|
{ key: 'haikuModel', label: 'Haiku 模型' },
|
|
{ key: 'sonnetModel', label: 'Sonnet 模型' },
|
|
{ key: 'opusModel', label: 'Opus 模型' },
|
|
],
|
|
},
|
|
codex: {
|
|
label: 'Codex',
|
|
defaultName: 'My Codex',
|
|
modelFields: [{ key: 'model', label: '主模型' }],
|
|
},
|
|
gemini: {
|
|
label: 'Gemini',
|
|
defaultName: 'My Gemini',
|
|
modelFields: [{ key: 'model', label: '主模型' }],
|
|
},
|
|
};
|
|
|
|
function getServerAddress() {
|
|
try {
|
|
const raw = localStorage.getItem('status');
|
|
if (raw) {
|
|
const status = JSON.parse(raw);
|
|
if (status.server_address) return status.server_address;
|
|
}
|
|
} catch (_) {}
|
|
return window.location.origin;
|
|
}
|
|
|
|
function buildCCSwitchURL(app, name, models, apiKey) {
|
|
const serverAddress = getServerAddress();
|
|
const endpoint = app === 'codex' ? serverAddress + '/v1' : serverAddress;
|
|
const params = new URLSearchParams();
|
|
params.set('resource', 'provider');
|
|
params.set('app', app);
|
|
params.set('name', name);
|
|
params.set('endpoint', endpoint);
|
|
params.set('apiKey', apiKey);
|
|
for (const [k, v] of Object.entries(models)) {
|
|
if (v) params.set(k, v);
|
|
}
|
|
params.set('homepage', serverAddress);
|
|
params.set('enabled', 'true');
|
|
return `ccswitch://v1/import?${params.toString()}`;
|
|
}
|
|
|
|
export default function CCSwitchModal({
|
|
visible,
|
|
onClose,
|
|
tokenKey,
|
|
modelOptions,
|
|
}) {
|
|
const { t } = useTranslation();
|
|
const [app, setApp] = useState('claude');
|
|
const [name, setName] = useState(APP_CONFIGS.claude.defaultName);
|
|
const [models, setModels] = useState({});
|
|
|
|
const currentConfig = APP_CONFIGS[app];
|
|
|
|
useEffect(() => {
|
|
if (visible) {
|
|
setModels({});
|
|
setApp('claude');
|
|
setName(APP_CONFIGS.claude.defaultName);
|
|
}
|
|
}, [visible]);
|
|
|
|
const handleAppChange = (val) => {
|
|
setApp(val);
|
|
setName(APP_CONFIGS[val].defaultName);
|
|
setModels({});
|
|
};
|
|
|
|
const handleModelChange = (field, value) => {
|
|
setModels((prev) => ({ ...prev, [field]: value }));
|
|
};
|
|
|
|
const handleSubmit = () => {
|
|
if (!models.model) {
|
|
Toast.warning(t('请选择主模型'));
|
|
return;
|
|
}
|
|
const url = buildCCSwitchURL(app, name, models, 'sk-' + tokenKey);
|
|
window.open(url, '_blank');
|
|
onClose();
|
|
};
|
|
|
|
const fieldLabelStyle = useMemo(
|
|
() => ({
|
|
marginBottom: 4,
|
|
fontSize: 13,
|
|
color: 'var(--semi-color-text-1)',
|
|
}),
|
|
[],
|
|
);
|
|
|
|
return (
|
|
<Modal
|
|
title={t('填入 CC Switch')}
|
|
visible={visible}
|
|
onCancel={onClose}
|
|
onOk={handleSubmit}
|
|
okText={t('打开 CC Switch')}
|
|
cancelText={t('取消')}
|
|
maskClosable={false}
|
|
width={480}
|
|
>
|
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
|
|
<div>
|
|
<div style={fieldLabelStyle}>{t('应用')}</div>
|
|
<RadioGroup
|
|
type='button'
|
|
value={app}
|
|
onChange={(e) => handleAppChange(e.target.value)}
|
|
style={{ width: '100%' }}
|
|
>
|
|
{Object.entries(APP_CONFIGS).map(([key, cfg]) => (
|
|
<Radio key={key} value={key}>
|
|
{cfg.label}
|
|
</Radio>
|
|
))}
|
|
</RadioGroup>
|
|
</div>
|
|
|
|
<div>
|
|
<div style={fieldLabelStyle}>{t('名称')}</div>
|
|
<Input
|
|
value={name}
|
|
onChange={setName}
|
|
placeholder={currentConfig.defaultName}
|
|
/>
|
|
</div>
|
|
|
|
{currentConfig.modelFields.map((field) => (
|
|
<div key={field.key}>
|
|
<div style={fieldLabelStyle}>
|
|
{t(field.label)}
|
|
{field.key === 'model' && (
|
|
<Typography.Text type='danger'> *</Typography.Text>
|
|
)}
|
|
</div>
|
|
<Select
|
|
placeholder={t('请选择模型')}
|
|
optionList={modelOptions}
|
|
value={models[field.key] || undefined}
|
|
onChange={(val) => handleModelChange(field.key, val)}
|
|
filter={selectFilter}
|
|
style={{ width: '100%' }}
|
|
showClear
|
|
searchable
|
|
emptyContent={t('暂无数据')}
|
|
/>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</Modal>
|
|
);
|
|
}
|