From 96264d2f8f2dde68fce9fcc3f2e58730063bec59 Mon Sep 17 00:00:00 2001 From: CaIon Date: Sun, 1 Mar 2026 23:23:20 +0800 Subject: [PATCH] feat: add cc-switch integration and modal for token management - Introduced a new CCSwitchModal component for managing CCSwitch configurations. - Updated the TokensPage to include functionality for opening the CCSwitch modal. - Enhanced the useTokensData hook to handle CCSwitch URLs and trigger the modal. - Modified chat settings to include a new "CC Switch" entry. - Updated sidebar logic to skip certain links based on the new configuration. --- setting/chat.go | 6 + web/src/components/layout/SiderBar.jsx | 4 +- web/src/components/table/tokens/index.jsx | 25 +- .../table/tokens/modals/CCSwitchModal.jsx | 195 ++ web/src/hooks/tokens/useTokensData.jsx | 16 +- web/src/i18n/locales/en.json | 903 +++-- web/src/i18n/locales/fr.json | 752 +++- web/src/i18n/locales/ja.json | 748 +++- web/src/i18n/locales/ru.json | 751 +++- web/src/i18n/locales/vi.json | 736 +++- web/src/i18n/locales/zh.json | 3083 +++++++++++++++++ web/src/pages/Setting/Chat/SettingsChats.jsx | 134 +- 12 files changed, 6361 insertions(+), 992 deletions(-) create mode 100644 web/src/components/table/tokens/modals/CCSwitchModal.jsx create mode 100644 web/src/i18n/locales/zh.json diff --git a/setting/chat.go b/setting/chat.go index 9be08268a..ecd741cd2 100644 --- a/setting/chat.go +++ b/setting/chat.go @@ -13,9 +13,15 @@ var Chats = []map[string]string{ { "Cherry Studio": "cherrystudio://providers/api-keys?v=1&data={cherryConfig}", }, + //{ + // "AionUI": "aionui://provider/add?v=1&data={aionuiConfig}", + //}, { "流畅阅读": "fluentread", }, + { + "CC Switch": "ccswitch", + }, { "Lobe Chat 官方示例": "https://chat-preview.lobehub.com/?settings={\"keyVaults\":{\"openai\":{\"apiKey\":\"{key}\",\"baseURL\":\"{address}/v1\"}}}", }, diff --git a/web/src/components/layout/SiderBar.jsx b/web/src/components/layout/SiderBar.jsx index 34971c397..bcbe41237 100644 --- a/web/src/components/layout/SiderBar.jsx +++ b/web/src/components/layout/SiderBar.jsx @@ -251,9 +251,9 @@ const SiderBar = ({ onNavigate = () => {} }) => { for (let key in chats[i]) { let link = chats[i][key]; if (typeof link !== 'string') continue; // 确保链接是字符串 - if (link.startsWith('fluent')) { + if (link.startsWith('fluent') || link.startsWith('ccswitch')) { shouldSkip = true; - break; // 跳过 Fluent Read + break; } chat.text = key; chat.itemKey = 'chat' + i; diff --git a/web/src/components/table/tokens/index.jsx b/web/src/components/table/tokens/index.jsx index f6194a35a..c78d8b82d 100644 --- a/web/src/components/table/tokens/index.jsx +++ b/web/src/components/table/tokens/index.jsx @@ -38,6 +38,7 @@ import TokensActions from './TokensActions'; import TokensFilters from './TokensFilters'; import TokensDescription from './TokensDescription'; import EditTokenModal from './modals/EditTokenModal'; +import CCSwitchModal from './modals/CCSwitchModal'; import { useTokensData } from '../../../hooks/tokens/useTokensData'; import { useIsMobile } from '../../../hooks/common/useIsMobile'; import { createCardProPagination } from '../../../helpers/utils'; @@ -45,8 +46,10 @@ import { createCardProPagination } from '../../../helpers/utils'; function TokensPage() { // Define the function first, then pass it into the hook to avoid TDZ errors const openFluentNotificationRef = useRef(null); - const tokensData = useTokensData((key) => - openFluentNotificationRef.current?.(key), + const openCCSwitchModalRef = useRef(null); + const tokensData = useTokensData( + (key) => openFluentNotificationRef.current?.(key), + (key) => openCCSwitchModalRef.current?.(key), ); const isMobile = useIsMobile(); const latestRef = useRef({ @@ -60,6 +63,8 @@ function TokensPage() { const [selectedModel, setSelectedModel] = useState(''); const [fluentNoticeOpen, setFluentNoticeOpen] = useState(false); const [prefillKey, setPrefillKey] = useState(''); + const [ccSwitchVisible, setCCSwitchVisible] = useState(false); + const [ccSwitchKey, setCCSwitchKey] = useState(''); // Keep latest data for handlers inside notifications useEffect(() => { @@ -183,6 +188,15 @@ function TokensPage() { // assign after definition so hook callback can call it safely openFluentNotificationRef.current = openFluentNotification; + function openCCSwitchModal(key) { + if (modelOptions.length === 0) { + loadModels(); + } + setCCSwitchKey(key || ''); + setCCSwitchVisible(true); + } + openCCSwitchModalRef.current = openCCSwitchModal; + // Prefill to Fluent handler const handlePrefillToFluent = () => { const { @@ -363,6 +377,13 @@ function TokensPage() { handleClose={closeEdit} /> + setCCSwitchVisible(false)} + tokenKey={ccSwitchKey} + modelOptions={modelOptions} + /> + . + +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 apiKey = 'sk-' + tokenKey; + const url = buildCCSwitchURL(app, name, models, apiKey); + window.open(url, '_blank'); + onClose(); + }; + + const fieldLabelStyle = useMemo( + () => ({ + marginBottom: 4, + fontSize: 13, + color: 'var(--semi-color-text-1)', + }), + [], + ); + + return ( + +
+
+
{t('应用')}
+ handleAppChange(e.target.value)} + style={{ width: '100%' }} + > + {Object.entries(APP_CONFIGS).map(([key, cfg]) => ( + + {cfg.label} + + ))} + +
+ +
+
{t('名称')}
+ +
+ + {currentConfig.modelFields.map((field) => ( +
+
+ {t(field.label)} + {field.key === 'model' && ( + * + )} +
+