mirror of
https://github.com/QuantumNous/new-api.git
synced 2026-03-30 04:40:59 +00:00
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.
This commit is contained in:
@@ -13,9 +13,15 @@ var Chats = []map[string]string{
|
|||||||
{
|
{
|
||||||
"Cherry Studio": "cherrystudio://providers/api-keys?v=1&data={cherryConfig}",
|
"Cherry Studio": "cherrystudio://providers/api-keys?v=1&data={cherryConfig}",
|
||||||
},
|
},
|
||||||
|
//{
|
||||||
|
// "AionUI": "aionui://provider/add?v=1&data={aionuiConfig}",
|
||||||
|
//},
|
||||||
{
|
{
|
||||||
"流畅阅读": "fluentread",
|
"流畅阅读": "fluentread",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"CC Switch": "ccswitch",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"Lobe Chat 官方示例": "https://chat-preview.lobehub.com/?settings={\"keyVaults\":{\"openai\":{\"apiKey\":\"{key}\",\"baseURL\":\"{address}/v1\"}}}",
|
"Lobe Chat 官方示例": "https://chat-preview.lobehub.com/?settings={\"keyVaults\":{\"openai\":{\"apiKey\":\"{key}\",\"baseURL\":\"{address}/v1\"}}}",
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -251,9 +251,9 @@ const SiderBar = ({ onNavigate = () => {} }) => {
|
|||||||
for (let key in chats[i]) {
|
for (let key in chats[i]) {
|
||||||
let link = chats[i][key];
|
let link = chats[i][key];
|
||||||
if (typeof link !== 'string') continue; // 确保链接是字符串
|
if (typeof link !== 'string') continue; // 确保链接是字符串
|
||||||
if (link.startsWith('fluent')) {
|
if (link.startsWith('fluent') || link.startsWith('ccswitch')) {
|
||||||
shouldSkip = true;
|
shouldSkip = true;
|
||||||
break; // 跳过 Fluent Read
|
break;
|
||||||
}
|
}
|
||||||
chat.text = key;
|
chat.text = key;
|
||||||
chat.itemKey = 'chat' + i;
|
chat.itemKey = 'chat' + i;
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ import TokensActions from './TokensActions';
|
|||||||
import TokensFilters from './TokensFilters';
|
import TokensFilters from './TokensFilters';
|
||||||
import TokensDescription from './TokensDescription';
|
import TokensDescription from './TokensDescription';
|
||||||
import EditTokenModal from './modals/EditTokenModal';
|
import EditTokenModal from './modals/EditTokenModal';
|
||||||
|
import CCSwitchModal from './modals/CCSwitchModal';
|
||||||
import { useTokensData } from '../../../hooks/tokens/useTokensData';
|
import { useTokensData } from '../../../hooks/tokens/useTokensData';
|
||||||
import { useIsMobile } from '../../../hooks/common/useIsMobile';
|
import { useIsMobile } from '../../../hooks/common/useIsMobile';
|
||||||
import { createCardProPagination } from '../../../helpers/utils';
|
import { createCardProPagination } from '../../../helpers/utils';
|
||||||
@@ -45,8 +46,10 @@ import { createCardProPagination } from '../../../helpers/utils';
|
|||||||
function TokensPage() {
|
function TokensPage() {
|
||||||
// Define the function first, then pass it into the hook to avoid TDZ errors
|
// Define the function first, then pass it into the hook to avoid TDZ errors
|
||||||
const openFluentNotificationRef = useRef(null);
|
const openFluentNotificationRef = useRef(null);
|
||||||
const tokensData = useTokensData((key) =>
|
const openCCSwitchModalRef = useRef(null);
|
||||||
openFluentNotificationRef.current?.(key),
|
const tokensData = useTokensData(
|
||||||
|
(key) => openFluentNotificationRef.current?.(key),
|
||||||
|
(key) => openCCSwitchModalRef.current?.(key),
|
||||||
);
|
);
|
||||||
const isMobile = useIsMobile();
|
const isMobile = useIsMobile();
|
||||||
const latestRef = useRef({
|
const latestRef = useRef({
|
||||||
@@ -60,6 +63,8 @@ function TokensPage() {
|
|||||||
const [selectedModel, setSelectedModel] = useState('');
|
const [selectedModel, setSelectedModel] = useState('');
|
||||||
const [fluentNoticeOpen, setFluentNoticeOpen] = useState(false);
|
const [fluentNoticeOpen, setFluentNoticeOpen] = useState(false);
|
||||||
const [prefillKey, setPrefillKey] = useState('');
|
const [prefillKey, setPrefillKey] = useState('');
|
||||||
|
const [ccSwitchVisible, setCCSwitchVisible] = useState(false);
|
||||||
|
const [ccSwitchKey, setCCSwitchKey] = useState('');
|
||||||
|
|
||||||
// Keep latest data for handlers inside notifications
|
// Keep latest data for handlers inside notifications
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -183,6 +188,15 @@ function TokensPage() {
|
|||||||
// assign after definition so hook callback can call it safely
|
// assign after definition so hook callback can call it safely
|
||||||
openFluentNotificationRef.current = openFluentNotification;
|
openFluentNotificationRef.current = openFluentNotification;
|
||||||
|
|
||||||
|
function openCCSwitchModal(key) {
|
||||||
|
if (modelOptions.length === 0) {
|
||||||
|
loadModels();
|
||||||
|
}
|
||||||
|
setCCSwitchKey(key || '');
|
||||||
|
setCCSwitchVisible(true);
|
||||||
|
}
|
||||||
|
openCCSwitchModalRef.current = openCCSwitchModal;
|
||||||
|
|
||||||
// Prefill to Fluent handler
|
// Prefill to Fluent handler
|
||||||
const handlePrefillToFluent = () => {
|
const handlePrefillToFluent = () => {
|
||||||
const {
|
const {
|
||||||
@@ -363,6 +377,13 @@ function TokensPage() {
|
|||||||
handleClose={closeEdit}
|
handleClose={closeEdit}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<CCSwitchModal
|
||||||
|
visible={ccSwitchVisible}
|
||||||
|
onClose={() => setCCSwitchVisible(false)}
|
||||||
|
tokenKey={ccSwitchKey}
|
||||||
|
modelOptions={modelOptions}
|
||||||
|
/>
|
||||||
|
|
||||||
<CardPro
|
<CardPro
|
||||||
type='type1'
|
type='type1'
|
||||||
descriptionArea={
|
descriptionArea={
|
||||||
|
|||||||
195
web/src/components/table/tokens/modals/CCSwitchModal.jsx
Normal file
195
web/src/components/table/tokens/modals/CCSwitchModal.jsx
Normal file
@@ -0,0 +1,195 @@
|
|||||||
|
/*
|
||||||
|
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 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 (
|
||||||
|
<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>
|
||||||
|
);
|
||||||
|
}
|
||||||
16
web/src/hooks/tokens/useTokensData.jsx
vendored
16
web/src/hooks/tokens/useTokensData.jsx
vendored
@@ -30,7 +30,7 @@ import {
|
|||||||
import { ITEMS_PER_PAGE } from '../../constants';
|
import { ITEMS_PER_PAGE } from '../../constants';
|
||||||
import { useTableCompactMode } from '../common/useTableCompactMode';
|
import { useTableCompactMode } from '../common/useTableCompactMode';
|
||||||
|
|
||||||
export const useTokensData = (openFluentNotification) => {
|
export const useTokensData = (openFluentNotification, openCCSwitchModal) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
// Basic state
|
// Basic state
|
||||||
@@ -124,6 +124,10 @@ export const useTokensData = (openFluentNotification) => {
|
|||||||
|
|
||||||
// Open link function for chat integrations
|
// Open link function for chat integrations
|
||||||
const onOpenLink = async (type, url, record) => {
|
const onOpenLink = async (type, url, record) => {
|
||||||
|
if (url && url.startsWith('ccswitch')) {
|
||||||
|
openCCSwitchModal(record.key);
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (url && url.startsWith('fluent')) {
|
if (url && url.startsWith('fluent')) {
|
||||||
openFluentNotification(record.key);
|
openFluentNotification(record.key);
|
||||||
return;
|
return;
|
||||||
@@ -147,6 +151,16 @@ export const useTokensData = (openFluentNotification) => {
|
|||||||
encodeToBase64(JSON.stringify(cherryConfig)),
|
encodeToBase64(JSON.stringify(cherryConfig)),
|
||||||
);
|
);
|
||||||
url = url.replaceAll('{cherryConfig}', encodedConfig);
|
url = url.replaceAll('{cherryConfig}', encodedConfig);
|
||||||
|
} else if (url.includes('{aionuiConfig}') === true) {
|
||||||
|
let aionuiConfig = {
|
||||||
|
platform: 'new-api',
|
||||||
|
baseUrl: serverAddress,
|
||||||
|
apiKey: 'sk-' + record.key,
|
||||||
|
};
|
||||||
|
let encodedConfig = encodeURIComponent(
|
||||||
|
encodeToBase64(JSON.stringify(aionuiConfig)),
|
||||||
|
);
|
||||||
|
url = url.replaceAll('{aionuiConfig}', encodedConfig);
|
||||||
} else {
|
} else {
|
||||||
let encodedServerAddress = encodeURIComponent(serverAddress);
|
let encodedServerAddress = encodeURIComponent(serverAddress);
|
||||||
url = url.replaceAll('{address}', encodedServerAddress);
|
url = url.replaceAll('{address}', encodedServerAddress);
|
||||||
|
|||||||
903
web/src/i18n/locales/en.json
vendored
903
web/src/i18n/locales/en.json
vendored
File diff suppressed because it is too large
Load Diff
752
web/src/i18n/locales/fr.json
vendored
752
web/src/i18n/locales/fr.json
vendored
File diff suppressed because it is too large
Load Diff
748
web/src/i18n/locales/ja.json
vendored
748
web/src/i18n/locales/ja.json
vendored
File diff suppressed because it is too large
Load Diff
751
web/src/i18n/locales/ru.json
vendored
751
web/src/i18n/locales/ru.json
vendored
File diff suppressed because it is too large
Load Diff
736
web/src/i18n/locales/vi.json
vendored
736
web/src/i18n/locales/vi.json
vendored
File diff suppressed because it is too large
Load Diff
3083
web/src/i18n/locales/zh.json
vendored
Normal file
3083
web/src/i18n/locales/zh.json
vendored
Normal file
File diff suppressed because it is too large
Load Diff
@@ -21,6 +21,7 @@ import React, { useEffect, useState, useRef } from 'react';
|
|||||||
import {
|
import {
|
||||||
Banner,
|
Banner,
|
||||||
Button,
|
Button,
|
||||||
|
Dropdown,
|
||||||
Form,
|
Form,
|
||||||
Space,
|
Space,
|
||||||
Spin,
|
Spin,
|
||||||
@@ -37,6 +38,7 @@ import {
|
|||||||
IconDelete,
|
IconDelete,
|
||||||
IconSearch,
|
IconSearch,
|
||||||
IconSaveStroked,
|
IconSaveStroked,
|
||||||
|
IconBolt,
|
||||||
} from '@douyinfe/semi-icons';
|
} from '@douyinfe/semi-icons';
|
||||||
import {
|
import {
|
||||||
compareObjects,
|
compareObjects,
|
||||||
@@ -64,6 +66,37 @@ export default function SettingsChats(props) {
|
|||||||
const [searchText, setSearchText] = useState('');
|
const [searchText, setSearchText] = useState('');
|
||||||
const modalFormRef = useRef();
|
const modalFormRef = useRef();
|
||||||
|
|
||||||
|
const BUILTIN_TEMPLATES = [
|
||||||
|
{ name: 'Cherry Studio', url: 'cherrystudio://providers/api-keys?v=1&data={cherryConfig}' },
|
||||||
|
{ name: '流畅阅读', url: 'fluentread' },
|
||||||
|
{ name: 'CC Switch', url: 'ccswitch' },
|
||||||
|
{ name: 'Lobe Chat', url: 'https://chat-preview.lobehub.com/?settings={"keyVaults":{"openai":{"apiKey":"{key}","baseURL":"{address}/v1"}}}' },
|
||||||
|
{ name: 'AI as Workspace', url: 'https://aiaw.app/set-provider?provider={"type":"openai","settings":{"apiKey":"{key}","baseURL":"{address}/v1","compatibility":"strict"}}' },
|
||||||
|
{ name: 'AMA 问天', url: 'ama://set-api-key?server={address}&key={key}' },
|
||||||
|
{ name: 'OpenCat', url: 'opencat://team/join?domain={address}&token={key}' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const addTemplates = (templates) => {
|
||||||
|
const existingNames = new Set(chatConfigs.map((c) => c.name));
|
||||||
|
const toAdd = templates.filter((tpl) => !existingNames.has(tpl.name));
|
||||||
|
if (toAdd.length === 0) {
|
||||||
|
showWarning(t('所选模板已存在'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let maxId = chatConfigs.length > 0
|
||||||
|
? Math.max(...chatConfigs.map((c) => c.id))
|
||||||
|
: -1;
|
||||||
|
const newItems = toAdd.map((tpl) => ({
|
||||||
|
id: ++maxId,
|
||||||
|
name: tpl.name,
|
||||||
|
url: tpl.url,
|
||||||
|
}));
|
||||||
|
const newConfigs = [...chatConfigs, ...newItems];
|
||||||
|
setChatConfigs(newConfigs);
|
||||||
|
syncConfigsToJson(newConfigs);
|
||||||
|
showSuccess(t('已添加 {{count}} 个模板', { count: toAdd.length }));
|
||||||
|
};
|
||||||
|
|
||||||
const jsonToConfigs = (jsonString) => {
|
const jsonToConfigs = (jsonString) => {
|
||||||
try {
|
try {
|
||||||
const configs = JSON.parse(jsonString);
|
const configs = JSON.parse(jsonString);
|
||||||
@@ -105,49 +138,47 @@ export default function SettingsChats(props) {
|
|||||||
|
|
||||||
async function onSubmit() {
|
async function onSubmit() {
|
||||||
try {
|
try {
|
||||||
console.log('Starting validation...');
|
if (editMode === 'json' && refForm.current) {
|
||||||
await refForm.current
|
try {
|
||||||
.validate()
|
await refForm.current.validate();
|
||||||
.then(() => {
|
} catch (error) {
|
||||||
console.log('Validation passed');
|
|
||||||
const updateArray = compareObjects(inputs, inputsRow);
|
|
||||||
if (!updateArray.length)
|
|
||||||
return showWarning(t('你似乎并没有修改什么'));
|
|
||||||
const requestQueue = updateArray.map((item) => {
|
|
||||||
let value = '';
|
|
||||||
if (typeof inputs[item.key] === 'boolean') {
|
|
||||||
value = String(inputs[item.key]);
|
|
||||||
} else {
|
|
||||||
value = inputs[item.key];
|
|
||||||
}
|
|
||||||
return API.put('/api/option/', {
|
|
||||||
key: item.key,
|
|
||||||
value,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
setLoading(true);
|
|
||||||
Promise.all(requestQueue)
|
|
||||||
.then((res) => {
|
|
||||||
if (requestQueue.length === 1) {
|
|
||||||
if (res.includes(undefined)) return;
|
|
||||||
} else if (requestQueue.length > 1) {
|
|
||||||
if (res.includes(undefined))
|
|
||||||
return showError(t('部分保存失败,请重试'));
|
|
||||||
}
|
|
||||||
showSuccess(t('保存成功'));
|
|
||||||
props.refresh();
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
showError(t('保存失败,请重试'));
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
setLoading(false);
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
console.error('Validation failed:', error);
|
console.error('Validation failed:', error);
|
||||||
showError(t('请检查输入'));
|
showError(t('请检查输入'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateArray = compareObjects(inputs, inputsRow);
|
||||||
|
if (!updateArray.length)
|
||||||
|
return showWarning(t('你似乎并没有修改什么'));
|
||||||
|
const requestQueue = updateArray.map((item) => {
|
||||||
|
let value = '';
|
||||||
|
if (typeof inputs[item.key] === 'boolean') {
|
||||||
|
value = String(inputs[item.key]);
|
||||||
|
} else {
|
||||||
|
value = inputs[item.key];
|
||||||
|
}
|
||||||
|
return API.put('/api/option/', {
|
||||||
|
key: item.key,
|
||||||
|
value,
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
const res = await Promise.all(requestQueue);
|
||||||
|
if (res.includes(undefined)) {
|
||||||
|
if (requestQueue.length > 1) {
|
||||||
|
showError(t('部分保存失败,请重试'));
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
showSuccess(t('保存成功'));
|
||||||
|
props.refresh();
|
||||||
|
} catch {
|
||||||
|
showError(t('保存失败,请重试'));
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
showError(t('请检查输入'));
|
showError(t('请检查输入'));
|
||||||
console.error(error);
|
console.error(error);
|
||||||
@@ -390,6 +421,29 @@ export default function SettingsChats(props) {
|
|||||||
>
|
>
|
||||||
{t('添加聊天配置')}
|
{t('添加聊天配置')}
|
||||||
</Button>
|
</Button>
|
||||||
|
<Dropdown
|
||||||
|
trigger='click'
|
||||||
|
position='bottomLeft'
|
||||||
|
menu={[
|
||||||
|
...BUILTIN_TEMPLATES.map((tpl, idx) => ({
|
||||||
|
node: 'item',
|
||||||
|
key: String(idx),
|
||||||
|
name: tpl.name,
|
||||||
|
onClick: () => addTemplates([tpl]),
|
||||||
|
})),
|
||||||
|
{ node: 'divider', key: 'divider' },
|
||||||
|
{
|
||||||
|
node: 'item',
|
||||||
|
key: 'all',
|
||||||
|
name: t('全部填入'),
|
||||||
|
onClick: () => addTemplates(BUILTIN_TEMPLATES),
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Button icon={<IconBolt />}>
|
||||||
|
{t('填入模板')}
|
||||||
|
</Button>
|
||||||
|
</Dropdown>
|
||||||
<Button
|
<Button
|
||||||
type='primary'
|
type='primary'
|
||||||
theme='solid'
|
theme='solid'
|
||||||
|
|||||||
Reference in New Issue
Block a user