| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| 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> |
| ); |
| } |
|
|