mirror of
https://github.com/linux-do/new-api.git
synced 2025-11-10 16:13:42 +08:00
merge upstream
Signed-off-by: wozulong <>
This commit is contained in:
@@ -4,8 +4,8 @@
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@douyinfe/semi-icons": "^2.46.1",
|
||||
"@douyinfe/semi-ui": "^2.55.3",
|
||||
"@douyinfe/semi-icons": "^2.63.1",
|
||||
"@douyinfe/semi-ui": "^2.63.1",
|
||||
"@visactor/react-vchart": "~1.8.8",
|
||||
"@visactor/vchart": "~1.8.8",
|
||||
"@visactor/vchart-semi-theme": "~1.8.8",
|
||||
@@ -22,7 +22,8 @@
|
||||
"react-toastify": "^9.0.8",
|
||||
"react-turnstile": "^1.0.5",
|
||||
"semantic-ui-offline": "^2.5.0",
|
||||
"semantic-ui-react": "^2.1.3"
|
||||
"semantic-ui-react": "^2.1.3",
|
||||
"sse": "github:mpetazzoni/sse.js"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
4752
web/pnpm-lock.yaml
generated
4752
web/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -26,6 +26,7 @@ import { Layout } from '@douyinfe/semi-ui';
|
||||
import Midjourney from './pages/Midjourney';
|
||||
import Pricing from './pages/Pricing/index.js';
|
||||
import Task from './pages/Task/index.js';
|
||||
import Playground from './components/Playground.js';
|
||||
|
||||
const Home = lazy(() => import('./pages/Home'));
|
||||
const Detail = lazy(() => import('./pages/Detail'));
|
||||
@@ -101,6 +102,14 @@ function App() {
|
||||
</PrivateRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path='/playground'
|
||||
element={
|
||||
<PrivateRoute>
|
||||
<Playground />
|
||||
</PrivateRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path='/redemption'
|
||||
element={
|
||||
@@ -256,7 +265,7 @@ function App() {
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path='/chat'
|
||||
path='/chat/:id?'
|
||||
element={
|
||||
<Suspense fallback={<Loading></Loading>}>
|
||||
<Chat />
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import React, { useContext, useEffect, useState } from 'react';
|
||||
import { Dimmer, Loader, Segment } from 'semantic-ui-react';
|
||||
import { useNavigate, useSearchParams } from 'react-router-dom';
|
||||
import { API, showError, showSuccess } from '../helpers';
|
||||
import { API, showError, showSuccess, updateAPI } from '../helpers';
|
||||
import { UserContext } from '../context/User';
|
||||
import { setUserData } from '../helpers/data.js';
|
||||
|
||||
const GitHubOAuth = () => {
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
@@ -28,8 +29,10 @@ const GitHubOAuth = () => {
|
||||
} else {
|
||||
userDispatch({ type: 'login', payload: data });
|
||||
localStorage.setItem('user', JSON.stringify(data));
|
||||
setUserData(data);
|
||||
updateAPI();
|
||||
showSuccess('登录成功!');
|
||||
navigate('/');
|
||||
navigate('/token');
|
||||
}
|
||||
} else {
|
||||
showError(message);
|
||||
|
||||
@@ -39,10 +39,10 @@ let buttons = [
|
||||
// icon: <IconHomeStroked />,
|
||||
},
|
||||
// {
|
||||
// text: '模型价格',
|
||||
// itemKey: 'pricing',
|
||||
// to: '/pricing',
|
||||
// icon: <IconNoteMoneyStroked />,
|
||||
// text: 'Playground',
|
||||
// itemKey: 'playground',
|
||||
// to: '/playground',
|
||||
// // icon: <IconNoteMoneyStroked />,
|
||||
// },
|
||||
];
|
||||
|
||||
|
||||
@@ -79,6 +79,8 @@ const LoginForm = () => {
|
||||
if (success) {
|
||||
userDispatch({ type: 'login', payload: data });
|
||||
localStorage.setItem('user', JSON.stringify(data));
|
||||
setUserData(data);
|
||||
updateAPI();
|
||||
navigate('/');
|
||||
showSuccess('登录成功!');
|
||||
setShowWeChatLoginModal(false);
|
||||
@@ -151,6 +153,8 @@ const LoginForm = () => {
|
||||
userDispatch({ type: 'login', payload: data });
|
||||
localStorage.setItem('user', JSON.stringify(data));
|
||||
showSuccess('登录成功!');
|
||||
setUserData(data);
|
||||
updateAPI();
|
||||
navigate('/');
|
||||
} else {
|
||||
showError(message);
|
||||
|
||||
@@ -10,6 +10,7 @@ import SettingsCreditLimit from '../pages/Setting/Operation/SettingsCreditLimit.
|
||||
import SettingsMagnification from '../pages/Setting/Operation/SettingsMagnification.js';
|
||||
|
||||
import { API, showError, showSuccess } from '../helpers';
|
||||
import SettingsChats from '../pages/Setting/Operation/SettingsChats.js';
|
||||
|
||||
const OperationSetting = () => {
|
||||
let [inputs, setInputs] = useState({
|
||||
@@ -50,6 +51,7 @@ const OperationSetting = () => {
|
||||
DataExportInterval: 5,
|
||||
DefaultCollapseSidebar: false, // 默认折叠侧边栏
|
||||
RetryTimes: 0,
|
||||
Chats: '[]',
|
||||
});
|
||||
|
||||
let [loading, setLoading] = useState(false);
|
||||
@@ -131,6 +133,10 @@ const OperationSetting = () => {
|
||||
<Card style={{ marginTop: '10px' }}>
|
||||
<SettingsCreditLimit options={inputs} refresh={onRefresh} />
|
||||
</Card>
|
||||
{/* 聊天设置 */}
|
||||
<Card style={{ marginTop: '10px' }}>
|
||||
<SettingsChats options={inputs} refresh={onRefresh} />
|
||||
</Card>
|
||||
{/* 倍率设置 */}
|
||||
<Card style={{ marginTop: '10px' }}>
|
||||
<SettingsMagnification options={inputs} refresh={onRefresh} />
|
||||
|
||||
357
web/src/components/Playground.js
Normal file
357
web/src/components/Playground.js
Normal file
@@ -0,0 +1,357 @@
|
||||
import React, { useCallback, useContext, useEffect, useState } from 'react';
|
||||
import { useNavigate, useSearchParams } from 'react-router-dom';
|
||||
import { UserContext } from '../context/User';
|
||||
import { API, getUserIdFromLocalStorage, showError } from '../helpers';
|
||||
import {
|
||||
Card,
|
||||
Chat,
|
||||
Input,
|
||||
Layout,
|
||||
Select,
|
||||
Slider,
|
||||
TextArea,
|
||||
Typography,
|
||||
} from '@douyinfe/semi-ui';
|
||||
import { SSE } from 'sse';
|
||||
|
||||
const defaultMessage = [
|
||||
{
|
||||
role: 'user',
|
||||
id: '2',
|
||||
createAt: 1715676751919,
|
||||
content: '你好',
|
||||
},
|
||||
{
|
||||
role: 'assistant',
|
||||
id: '3',
|
||||
createAt: 1715676751919,
|
||||
content: '你好,请问有什么可以帮助您的吗?',
|
||||
},
|
||||
];
|
||||
|
||||
let id = 4;
|
||||
function getId() {
|
||||
return `${id++}`;
|
||||
}
|
||||
|
||||
const Playground = () => {
|
||||
const [inputs, setInputs] = useState({
|
||||
model: 'gpt-4o-mini',
|
||||
group: '',
|
||||
max_tokens: 0,
|
||||
temperature: 0,
|
||||
});
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
const [userState, userDispatch] = useContext(UserContext);
|
||||
const [status, setStatus] = useState({});
|
||||
const [systemPrompt, setSystemPrompt] = useState(
|
||||
'You are a helpful assistant. You can help me by answering my questions. You can also ask me questions.',
|
||||
);
|
||||
const [message, setMessage] = useState(defaultMessage);
|
||||
const [models, setModels] = useState([]);
|
||||
const [groups, setGroups] = useState([]);
|
||||
|
||||
const handleInputChange = (name, value) => {
|
||||
setInputs((inputs) => ({ ...inputs, [name]: value }));
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (searchParams.get('expired')) {
|
||||
showError('未登录或登录已过期,请重新登录!');
|
||||
}
|
||||
let status = localStorage.getItem('status');
|
||||
if (status) {
|
||||
status = JSON.parse(status);
|
||||
setStatus(status);
|
||||
}
|
||||
loadModels();
|
||||
loadGroups();
|
||||
}, []);
|
||||
|
||||
const loadModels = async () => {
|
||||
let res = await API.get(`/api/user/models`);
|
||||
const { success, message, data } = res.data;
|
||||
if (success) {
|
||||
let localModelOptions = data.map((model) => ({
|
||||
label: model,
|
||||
value: model,
|
||||
}));
|
||||
setModels(localModelOptions);
|
||||
} else {
|
||||
showError(message);
|
||||
}
|
||||
};
|
||||
|
||||
const loadGroups = async () => {
|
||||
let res = await API.get(`/api/user/groups`);
|
||||
const { success, message, data } = res.data;
|
||||
if (success) {
|
||||
// return data is a map, key is group name, value is group description
|
||||
// label is group description, value is group name
|
||||
let localGroupOptions = Object.keys(data).map((group) => ({
|
||||
label: data[group],
|
||||
value: group,
|
||||
}));
|
||||
// handleInputChange('group', localGroupOptions[0].value);
|
||||
|
||||
if (localGroupOptions.length > 0) {
|
||||
} else {
|
||||
localGroupOptions = [
|
||||
{
|
||||
label: '用户分组',
|
||||
value: '',
|
||||
},
|
||||
];
|
||||
setGroups(localGroupOptions);
|
||||
}
|
||||
setGroups(localGroupOptions);
|
||||
handleInputChange('group', localGroupOptions[0].value);
|
||||
} else {
|
||||
showError(message);
|
||||
}
|
||||
};
|
||||
|
||||
const commonOuterStyle = {
|
||||
border: '1px solid var(--semi-color-border)',
|
||||
borderRadius: '16px',
|
||||
margin: '0px 8px',
|
||||
};
|
||||
|
||||
const getSystemMessage = () => {
|
||||
if (systemPrompt !== '') {
|
||||
return {
|
||||
role: 'system',
|
||||
id: '1',
|
||||
createAt: 1715676751919,
|
||||
content: systemPrompt,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
let handleSSE = (payload) => {
|
||||
let source = new SSE('/pg/chat/completions', {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'New-Api-User': getUserIdFromLocalStorage(),
|
||||
},
|
||||
method: 'POST',
|
||||
payload: JSON.stringify(payload),
|
||||
});
|
||||
source.addEventListener('message', (e) => {
|
||||
if (e.data !== '[DONE]') {
|
||||
let payload = JSON.parse(e.data);
|
||||
// console.log("Payload: ", payload);
|
||||
if (payload.choices.length === 0) {
|
||||
source.close();
|
||||
completeMessage();
|
||||
} else {
|
||||
let text = payload.choices[0].delta.content;
|
||||
if (text) {
|
||||
generateMockResponse(text);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
completeMessage();
|
||||
}
|
||||
});
|
||||
|
||||
source.addEventListener('error', (e) => {
|
||||
generateMockResponse(e.data);
|
||||
completeMessage('error');
|
||||
});
|
||||
|
||||
source.addEventListener('readystatechange', (e) => {
|
||||
if (e.readyState >= 2) {
|
||||
if (source.status === undefined) {
|
||||
source.close();
|
||||
completeMessage();
|
||||
}
|
||||
}
|
||||
});
|
||||
source.stream();
|
||||
};
|
||||
|
||||
const onMessageSend = useCallback(
|
||||
(content, attachment) => {
|
||||
console.log('attachment: ', attachment);
|
||||
setMessage((prevMessage) => {
|
||||
const newMessage = [
|
||||
...prevMessage,
|
||||
{
|
||||
role: 'user',
|
||||
content: content,
|
||||
createAt: Date.now(),
|
||||
id: getId(),
|
||||
},
|
||||
];
|
||||
|
||||
// 将 getPayload 移到这里
|
||||
const getPayload = () => {
|
||||
let systemMessage = getSystemMessage();
|
||||
let messages = newMessage.map((item) => {
|
||||
return {
|
||||
role: item.role,
|
||||
content: item.content,
|
||||
};
|
||||
});
|
||||
if (systemMessage) {
|
||||
messages.unshift(systemMessage);
|
||||
}
|
||||
return {
|
||||
messages: messages,
|
||||
stream: true,
|
||||
model: inputs.model,
|
||||
group: inputs.group,
|
||||
max_tokens: parseInt(inputs.max_tokens),
|
||||
temperature: inputs.temperature,
|
||||
};
|
||||
};
|
||||
|
||||
// 使用更新后的消息状态调用 handleSSE
|
||||
handleSSE(getPayload());
|
||||
newMessage.push({
|
||||
role: 'assistant',
|
||||
content: '',
|
||||
createAt: Date.now(),
|
||||
id: getId(),
|
||||
status: 'loading',
|
||||
});
|
||||
return newMessage;
|
||||
});
|
||||
},
|
||||
[getSystemMessage],
|
||||
);
|
||||
|
||||
const completeMessage = useCallback((status = 'complete') => {
|
||||
// console.log("Complete Message: ", status)
|
||||
setMessage((prevMessage) => {
|
||||
const lastMessage = prevMessage[prevMessage.length - 1];
|
||||
// only change the status if the last message is not complete and not error
|
||||
if (lastMessage.status === 'complete' || lastMessage.status === 'error') {
|
||||
return prevMessage;
|
||||
}
|
||||
return [...prevMessage.slice(0, -1), { ...lastMessage, status: status }];
|
||||
});
|
||||
}, []);
|
||||
|
||||
const generateMockResponse = useCallback((content) => {
|
||||
// console.log("Generate Mock Response: ", content);
|
||||
setMessage((message) => {
|
||||
const lastMessage = message[message.length - 1];
|
||||
let newMessage = { ...lastMessage };
|
||||
if (
|
||||
lastMessage.status === 'loading' ||
|
||||
lastMessage.status === 'incomplete'
|
||||
) {
|
||||
newMessage = {
|
||||
...newMessage,
|
||||
content: (lastMessage.content || '') + content,
|
||||
status: 'incomplete',
|
||||
};
|
||||
}
|
||||
return [...message.slice(0, -1), newMessage];
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Layout style={{ height: '100%' }}>
|
||||
<Layout.Sider>
|
||||
<Card style={commonOuterStyle}>
|
||||
<div style={{ marginTop: 10 }}>
|
||||
<Typography.Text strong>分组:</Typography.Text>
|
||||
</div>
|
||||
<Select
|
||||
placeholder={'请选择分组'}
|
||||
name='group'
|
||||
required
|
||||
selection
|
||||
onChange={(value) => {
|
||||
handleInputChange('group', value);
|
||||
}}
|
||||
value={inputs.group}
|
||||
autoComplete='new-password'
|
||||
optionList={groups}
|
||||
/>
|
||||
<div style={{ marginTop: 10 }}>
|
||||
<Typography.Text strong>模型:</Typography.Text>
|
||||
</div>
|
||||
<Select
|
||||
placeholder={'请选择模型'}
|
||||
name='model'
|
||||
required
|
||||
selection
|
||||
filter
|
||||
onChange={(value) => {
|
||||
handleInputChange('model', value);
|
||||
}}
|
||||
value={inputs.model}
|
||||
autoComplete='new-password'
|
||||
optionList={models}
|
||||
/>
|
||||
<div style={{ marginTop: 10 }}>
|
||||
<Typography.Text strong>Temperature:</Typography.Text>
|
||||
</div>
|
||||
<Slider
|
||||
step={0.1}
|
||||
min={0.1}
|
||||
max={1}
|
||||
value={inputs.temperature}
|
||||
onChange={(value) => {
|
||||
handleInputChange('temperature', value);
|
||||
}}
|
||||
/>
|
||||
<div style={{ marginTop: 10 }}>
|
||||
<Typography.Text strong>MaxTokens:</Typography.Text>
|
||||
</div>
|
||||
<Input
|
||||
placeholder='MaxTokens'
|
||||
name='max_tokens'
|
||||
required
|
||||
autoComplete='new-password'
|
||||
defaultValue={0}
|
||||
value={inputs.max_tokens}
|
||||
onChange={(value) => {
|
||||
handleInputChange('max_tokens', value);
|
||||
}}
|
||||
/>
|
||||
|
||||
<div style={{ marginTop: 10 }}>
|
||||
<Typography.Text strong>System:</Typography.Text>
|
||||
</div>
|
||||
<TextArea
|
||||
placeholder='System Prompt'
|
||||
name='system'
|
||||
required
|
||||
autoComplete='new-password'
|
||||
autosize
|
||||
defaultValue={systemPrompt}
|
||||
// value={systemPrompt}
|
||||
onChange={(value) => {
|
||||
setSystemPrompt(value);
|
||||
}}
|
||||
/>
|
||||
</Card>
|
||||
</Layout.Sider>
|
||||
<Layout.Content>
|
||||
<div style={{ height: '100%' }}>
|
||||
<Chat
|
||||
chatBoxRenderConfig={{
|
||||
renderChatBoxAction: () => {
|
||||
return <div></div>;
|
||||
},
|
||||
}}
|
||||
style={commonOuterStyle}
|
||||
chats={message}
|
||||
onMessageSend={onMessageSend}
|
||||
showClearContext
|
||||
onClear={() => {
|
||||
setMessage([]);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</Layout.Content>
|
||||
</Layout>
|
||||
);
|
||||
};
|
||||
|
||||
export default Playground;
|
||||
796
web/src/components/SafetySetting.js
Normal file
796
web/src/components/SafetySetting.js
Normal file
@@ -0,0 +1,796 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import {
|
||||
Button,
|
||||
Divider,
|
||||
Form,
|
||||
Grid,
|
||||
Header,
|
||||
Message,
|
||||
Modal,
|
||||
} from 'semantic-ui-react';
|
||||
import { API, removeTrailingSlash, showError, verifyJSON } from '../helpers';
|
||||
|
||||
import { useTheme } from '../context/Theme';
|
||||
|
||||
const SafetySetting = () => {
|
||||
let [inputs, setInputs] = useState({
|
||||
PasswordLoginEnabled: '',
|
||||
PasswordRegisterEnabled: '',
|
||||
EmailVerificationEnabled: '',
|
||||
GitHubOAuthEnabled: '',
|
||||
GitHubClientId: '',
|
||||
GitHubClientSecret: '',
|
||||
Notice: '',
|
||||
SMTPServer: '',
|
||||
SMTPPort: '',
|
||||
SMTPAccount: '',
|
||||
SMTPFrom: '',
|
||||
SMTPToken: '',
|
||||
ServerAddress: '',
|
||||
WorkerUrl: '',
|
||||
WorkerValidKey: '',
|
||||
EpayId: '',
|
||||
EpayKey: '',
|
||||
Price: 7.3,
|
||||
MinTopUp: 1,
|
||||
TopupGroupRatio: '',
|
||||
PayAddress: '',
|
||||
CustomCallbackAddress: '',
|
||||
Footer: '',
|
||||
WeChatAuthEnabled: '',
|
||||
WeChatServerAddress: '',
|
||||
WeChatServerToken: '',
|
||||
WeChatAccountQRCodeImageURL: '',
|
||||
TurnstileCheckEnabled: '',
|
||||
TurnstileSiteKey: '',
|
||||
TurnstileSecretKey: '',
|
||||
RegisterEnabled: '',
|
||||
EmailDomainRestrictionEnabled: '',
|
||||
EmailAliasRestrictionEnabled: '',
|
||||
SMTPSSLEnabled: '',
|
||||
EmailDomainWhitelist: [],
|
||||
// telegram login
|
||||
TelegramOAuthEnabled: '',
|
||||
TelegramBotToken: '',
|
||||
TelegramBotName: '',
|
||||
});
|
||||
const [originInputs, setOriginInputs] = useState({});
|
||||
let [loading, setLoading] = useState(false);
|
||||
const [EmailDomainWhitelist, setEmailDomainWhitelist] = useState([]);
|
||||
const [restrictedDomainInput, setRestrictedDomainInput] = useState('');
|
||||
const [showPasswordWarningModal, setShowPasswordWarningModal] =
|
||||
useState(false);
|
||||
|
||||
const theme = useTheme();
|
||||
const isDark = theme === 'dark';
|
||||
|
||||
const getOptions = async () => {
|
||||
const res = await API.get('/api/option/');
|
||||
const { success, message, data } = res.data;
|
||||
if (success) {
|
||||
let newInputs = {};
|
||||
data.forEach((item) => {
|
||||
if (item.key === 'TopupGroupRatio') {
|
||||
item.value = JSON.stringify(JSON.parse(item.value), null, 2);
|
||||
}
|
||||
newInputs[item.key] = item.value;
|
||||
});
|
||||
setInputs({
|
||||
...newInputs,
|
||||
EmailDomainWhitelist: newInputs.EmailDomainWhitelist.split(','),
|
||||
});
|
||||
setOriginInputs(newInputs);
|
||||
|
||||
setEmailDomainWhitelist(
|
||||
newInputs.EmailDomainWhitelist.split(',').map((item) => {
|
||||
return { key: item, text: item, value: item };
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
showError(message);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
getOptions().then();
|
||||
}, []);
|
||||
useEffect(() => {}, [inputs.EmailDomainWhitelist]);
|
||||
|
||||
const updateOption = async (key, value) => {
|
||||
setLoading(true);
|
||||
switch (key) {
|
||||
case 'PasswordLoginEnabled':
|
||||
case 'PasswordRegisterEnabled':
|
||||
case 'EmailVerificationEnabled':
|
||||
case 'GitHubOAuthEnabled':
|
||||
case 'WeChatAuthEnabled':
|
||||
case 'TelegramOAuthEnabled':
|
||||
case 'TurnstileCheckEnabled':
|
||||
case 'EmailDomainRestrictionEnabled':
|
||||
case 'EmailAliasRestrictionEnabled':
|
||||
case 'SMTPSSLEnabled':
|
||||
case 'RegisterEnabled':
|
||||
value = inputs[key] === 'true' ? 'false' : 'true';
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
const res = await API.put('/api/option/', {
|
||||
key,
|
||||
value,
|
||||
});
|
||||
const { success, message } = res.data;
|
||||
if (success) {
|
||||
if (key === 'EmailDomainWhitelist') {
|
||||
value = value.split(',');
|
||||
}
|
||||
if (key === 'Price') {
|
||||
value = parseFloat(value);
|
||||
}
|
||||
setInputs((inputs) => ({
|
||||
...inputs,
|
||||
[key]: value,
|
||||
}));
|
||||
} else {
|
||||
showError(message);
|
||||
}
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
const handleInputChange = async (e, { name, value }) => {
|
||||
if (name === 'PasswordLoginEnabled' && inputs[name] === 'true') {
|
||||
// block disabling password login
|
||||
setShowPasswordWarningModal(true);
|
||||
return;
|
||||
}
|
||||
if (
|
||||
name === 'Notice' ||
|
||||
(name.startsWith('SMTP') && name !== 'SMTPSSLEnabled') ||
|
||||
name === 'ServerAddress' ||
|
||||
name === 'WorkerUrl' ||
|
||||
name === 'WorkerValidKey' ||
|
||||
name === 'EpayId' ||
|
||||
name === 'EpayKey' ||
|
||||
name === 'Price' ||
|
||||
name === 'PayAddress' ||
|
||||
name === 'GitHubClientId' ||
|
||||
name === 'GitHubClientSecret' ||
|
||||
name === 'WeChatServerAddress' ||
|
||||
name === 'WeChatServerToken' ||
|
||||
name === 'WeChatAccountQRCodeImageURL' ||
|
||||
name === 'TurnstileSiteKey' ||
|
||||
name === 'TurnstileSecretKey' ||
|
||||
name === 'EmailDomainWhitelist' ||
|
||||
name === 'TopupGroupRatio' ||
|
||||
name === 'TelegramBotToken' ||
|
||||
name === 'TelegramBotName'
|
||||
) {
|
||||
setInputs((inputs) => ({ ...inputs, [name]: value }));
|
||||
} else {
|
||||
await updateOption(name, value);
|
||||
}
|
||||
};
|
||||
|
||||
const submitServerAddress = async () => {
|
||||
let ServerAddress = removeTrailingSlash(inputs.ServerAddress);
|
||||
await updateOption('ServerAddress', ServerAddress);
|
||||
};
|
||||
|
||||
const submitWorker = async () => {
|
||||
let WorkerUrl = removeTrailingSlash(inputs.WorkerUrl);
|
||||
await updateOption('WorkerUrl', WorkerUrl);
|
||||
if (inputs.WorkerValidKey !== '') {
|
||||
await updateOption('WorkerValidKey', inputs.WorkerValidKey);
|
||||
}
|
||||
};
|
||||
|
||||
const submitPayAddress = async () => {
|
||||
if (inputs.ServerAddress === '') {
|
||||
showError('请先填写服务器地址');
|
||||
return;
|
||||
}
|
||||
if (originInputs['TopupGroupRatio'] !== inputs.TopupGroupRatio) {
|
||||
if (!verifyJSON(inputs.TopupGroupRatio)) {
|
||||
showError('充值分组倍率不是合法的 JSON 字符串');
|
||||
return;
|
||||
}
|
||||
await updateOption('TopupGroupRatio', inputs.TopupGroupRatio);
|
||||
}
|
||||
let PayAddress = removeTrailingSlash(inputs.PayAddress);
|
||||
await updateOption('PayAddress', PayAddress);
|
||||
if (inputs.EpayId !== '') {
|
||||
await updateOption('EpayId', inputs.EpayId);
|
||||
}
|
||||
if (inputs.EpayKey !== undefined && inputs.EpayKey !== '') {
|
||||
await updateOption('EpayKey', inputs.EpayKey);
|
||||
}
|
||||
await updateOption('Price', '' + inputs.Price);
|
||||
};
|
||||
|
||||
const submitSMTP = async () => {
|
||||
if (originInputs['SMTPServer'] !== inputs.SMTPServer) {
|
||||
await updateOption('SMTPServer', inputs.SMTPServer);
|
||||
}
|
||||
if (originInputs['SMTPAccount'] !== inputs.SMTPAccount) {
|
||||
await updateOption('SMTPAccount', inputs.SMTPAccount);
|
||||
}
|
||||
if (originInputs['SMTPFrom'] !== inputs.SMTPFrom) {
|
||||
await updateOption('SMTPFrom', inputs.SMTPFrom);
|
||||
}
|
||||
if (
|
||||
originInputs['SMTPPort'] !== inputs.SMTPPort &&
|
||||
inputs.SMTPPort !== ''
|
||||
) {
|
||||
await updateOption('SMTPPort', inputs.SMTPPort);
|
||||
}
|
||||
if (
|
||||
originInputs['SMTPToken'] !== inputs.SMTPToken &&
|
||||
inputs.SMTPToken !== ''
|
||||
) {
|
||||
await updateOption('SMTPToken', inputs.SMTPToken);
|
||||
}
|
||||
};
|
||||
|
||||
const submitEmailDomainWhitelist = async () => {
|
||||
if (
|
||||
originInputs['EmailDomainWhitelist'] !==
|
||||
inputs.EmailDomainWhitelist.join(',') &&
|
||||
inputs.SMTPToken !== ''
|
||||
) {
|
||||
await updateOption(
|
||||
'EmailDomainWhitelist',
|
||||
inputs.EmailDomainWhitelist.join(','),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const submitWeChat = async () => {
|
||||
if (originInputs['WeChatServerAddress'] !== inputs.WeChatServerAddress) {
|
||||
await updateOption(
|
||||
'WeChatServerAddress',
|
||||
removeTrailingSlash(inputs.WeChatServerAddress),
|
||||
);
|
||||
}
|
||||
if (
|
||||
originInputs['WeChatAccountQRCodeImageURL'] !==
|
||||
inputs.WeChatAccountQRCodeImageURL
|
||||
) {
|
||||
await updateOption(
|
||||
'WeChatAccountQRCodeImageURL',
|
||||
inputs.WeChatAccountQRCodeImageURL,
|
||||
);
|
||||
}
|
||||
if (
|
||||
originInputs['WeChatServerToken'] !== inputs.WeChatServerToken &&
|
||||
inputs.WeChatServerToken !== ''
|
||||
) {
|
||||
await updateOption('WeChatServerToken', inputs.WeChatServerToken);
|
||||
}
|
||||
};
|
||||
|
||||
const submitGitHubOAuth = async () => {
|
||||
if (originInputs['GitHubClientId'] !== inputs.GitHubClientId) {
|
||||
await updateOption('GitHubClientId', inputs.GitHubClientId);
|
||||
}
|
||||
if (
|
||||
originInputs['GitHubClientSecret'] !== inputs.GitHubClientSecret &&
|
||||
inputs.GitHubClientSecret !== ''
|
||||
) {
|
||||
await updateOption('GitHubClientSecret', inputs.GitHubClientSecret);
|
||||
}
|
||||
};
|
||||
|
||||
const submitTelegramSettings = async () => {
|
||||
// await updateOption('TelegramOAuthEnabled', inputs.TelegramOAuthEnabled);
|
||||
await updateOption('TelegramBotToken', inputs.TelegramBotToken);
|
||||
await updateOption('TelegramBotName', inputs.TelegramBotName);
|
||||
};
|
||||
|
||||
const submitTurnstile = async () => {
|
||||
if (originInputs['TurnstileSiteKey'] !== inputs.TurnstileSiteKey) {
|
||||
await updateOption('TurnstileSiteKey', inputs.TurnstileSiteKey);
|
||||
}
|
||||
if (
|
||||
originInputs['TurnstileSecretKey'] !== inputs.TurnstileSecretKey &&
|
||||
inputs.TurnstileSecretKey !== ''
|
||||
) {
|
||||
await updateOption('TurnstileSecretKey', inputs.TurnstileSecretKey);
|
||||
}
|
||||
};
|
||||
|
||||
const submitNewRestrictedDomain = () => {
|
||||
const localDomainList = inputs.EmailDomainWhitelist;
|
||||
if (
|
||||
restrictedDomainInput !== '' &&
|
||||
!localDomainList.includes(restrictedDomainInput)
|
||||
) {
|
||||
setRestrictedDomainInput('');
|
||||
setInputs({
|
||||
...inputs,
|
||||
EmailDomainWhitelist: [...localDomainList, restrictedDomainInput],
|
||||
});
|
||||
setEmailDomainWhitelist([
|
||||
...EmailDomainWhitelist,
|
||||
{
|
||||
key: restrictedDomainInput,
|
||||
text: restrictedDomainInput,
|
||||
value: restrictedDomainInput,
|
||||
},
|
||||
]);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Grid columns={1}>
|
||||
<Grid.Column>
|
||||
<Form loading={loading} inverted={isDark}>
|
||||
<Header as='h3' inverted={isDark}>
|
||||
通用设置
|
||||
</Header>
|
||||
<Form.Group widths='equal'>
|
||||
<Form.Input
|
||||
label='服务器地址'
|
||||
placeholder='例如:https://yourdomain.com'
|
||||
value={inputs.ServerAddress}
|
||||
name='ServerAddress'
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
</Form.Group>
|
||||
<Form.Button onClick={submitServerAddress}>
|
||||
更新服务器地址
|
||||
</Form.Button>
|
||||
<Header as='h3' inverted={isDark}>
|
||||
代理设置(支持{' '}
|
||||
<a
|
||||
href='https://github.com/Calcium-Ion/new-api-worker'
|
||||
target='_blank'
|
||||
rel='noreferrer'
|
||||
>
|
||||
new-api-worker
|
||||
</a>
|
||||
)
|
||||
</Header>
|
||||
<Form.Group widths='equal'>
|
||||
<Form.Input
|
||||
label='Worker地址,不填写则不启用代理'
|
||||
placeholder='例如:https://workername.yourdomain.workers.dev'
|
||||
value={inputs.WorkerUrl}
|
||||
name='WorkerUrl'
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
<Form.Input
|
||||
label='Worker密钥,根据你部署的 Worker 填写'
|
||||
placeholder='例如:your_secret_key'
|
||||
value={inputs.WorkerValidKey}
|
||||
name='WorkerValidKey'
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
</Form.Group>
|
||||
<Form.Button onClick={submitWorker}>更新Worker设置</Form.Button>
|
||||
<Divider />
|
||||
<Header as='h3' inverted={isDark}>
|
||||
支付设置(当前仅支持易支付接口,默认使用上方服务器地址作为回调地址!)
|
||||
</Header>
|
||||
<Form.Group widths='equal'>
|
||||
<Form.Input
|
||||
label='支付地址,不填写则不启用在线支付'
|
||||
placeholder='例如:https://yourdomain.com'
|
||||
value={inputs.PayAddress}
|
||||
name='PayAddress'
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
<Form.Input
|
||||
label='易支付商户ID'
|
||||
placeholder='例如:0001'
|
||||
value={inputs.EpayId}
|
||||
name='EpayId'
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
<Form.Input
|
||||
label='易支付商户密钥'
|
||||
placeholder='敏感信息不会发送到前端显示'
|
||||
value={inputs.EpayKey}
|
||||
name='EpayKey'
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
</Form.Group>
|
||||
<Form.Group widths='equal'>
|
||||
<Form.Input
|
||||
label='回调地址,不填写则使用上方服务器地址作为回调地址'
|
||||
placeholder='例如:https://yourdomain.com'
|
||||
value={inputs.CustomCallbackAddress}
|
||||
name='CustomCallbackAddress'
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
<Form.Input
|
||||
label='充值价格(x元/美金)'
|
||||
placeholder='例如:7,就是7元/美金'
|
||||
value={inputs.Price}
|
||||
name='Price'
|
||||
min={0}
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
<Form.Input
|
||||
label='最低充值美元数量(以美金为单位,如果使用额度请自行换算!)'
|
||||
placeholder='例如:2,就是最低充值2$'
|
||||
value={inputs.MinTopUp}
|
||||
name='MinTopUp'
|
||||
min={1}
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
</Form.Group>
|
||||
<Form.Group widths='equal'>
|
||||
<Form.TextArea
|
||||
label='充值分组倍率'
|
||||
name='TopupGroupRatio'
|
||||
onChange={handleInputChange}
|
||||
style={{ minHeight: 250, fontFamily: 'JetBrains Mono, Consolas' }}
|
||||
autoComplete='new-password'
|
||||
value={inputs.TopupGroupRatio}
|
||||
placeholder='为一个 JSON 文本,键为组名称,值为倍率'
|
||||
/>
|
||||
</Form.Group>
|
||||
<Form.Button onClick={submitPayAddress}>更新支付设置</Form.Button>
|
||||
<Divider />
|
||||
<Header as='h3' inverted={isDark}>
|
||||
配置登录注册
|
||||
</Header>
|
||||
<Form.Group inline>
|
||||
<Form.Checkbox
|
||||
checked={inputs.PasswordLoginEnabled === 'true'}
|
||||
label='允许通过密码进行登录'
|
||||
name='PasswordLoginEnabled'
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
{showPasswordWarningModal && (
|
||||
<Modal
|
||||
open={showPasswordWarningModal}
|
||||
onClose={() => setShowPasswordWarningModal(false)}
|
||||
size={'tiny'}
|
||||
style={{ maxWidth: '450px' }}
|
||||
>
|
||||
<Modal.Header>警告</Modal.Header>
|
||||
<Modal.Content>
|
||||
<p>
|
||||
取消密码登录将导致所有未绑定其他登录方式的用户(包括管理员)无法通过密码登录,确认取消?
|
||||
</p>
|
||||
</Modal.Content>
|
||||
<Modal.Actions>
|
||||
<Button onClick={() => setShowPasswordWarningModal(false)}>
|
||||
取消
|
||||
</Button>
|
||||
<Button
|
||||
color='yellow'
|
||||
onClick={async () => {
|
||||
setShowPasswordWarningModal(false);
|
||||
await updateOption('PasswordLoginEnabled', 'false');
|
||||
}}
|
||||
>
|
||||
确定
|
||||
</Button>
|
||||
</Modal.Actions>
|
||||
</Modal>
|
||||
)}
|
||||
<Form.Checkbox
|
||||
checked={inputs.PasswordRegisterEnabled === 'true'}
|
||||
label='允许通过密码进行注册'
|
||||
name='PasswordRegisterEnabled'
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
<Form.Checkbox
|
||||
checked={inputs.EmailVerificationEnabled === 'true'}
|
||||
label='通过密码注册时需要进行邮箱验证'
|
||||
name='EmailVerificationEnabled'
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
<Form.Checkbox
|
||||
checked={inputs.GitHubOAuthEnabled === 'true'}
|
||||
label='允许通过 GitHub 账户登录 & 注册'
|
||||
name='GitHubOAuthEnabled'
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
<Form.Checkbox
|
||||
checked={inputs.WeChatAuthEnabled === 'true'}
|
||||
label='允许通过微信登录 & 注册'
|
||||
name='WeChatAuthEnabled'
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
<Form.Checkbox
|
||||
checked={inputs.TelegramOAuthEnabled === 'true'}
|
||||
label='允许通过 Telegram 进行登录'
|
||||
name='TelegramOAuthEnabled'
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
</Form.Group>
|
||||
<Form.Group inline>
|
||||
<Form.Checkbox
|
||||
checked={inputs.RegisterEnabled === 'true'}
|
||||
label='允许新用户注册(此项为否时,新用户将无法以任何方式进行注册)'
|
||||
name='RegisterEnabled'
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
<Form.Checkbox
|
||||
checked={inputs.TurnstileCheckEnabled === 'true'}
|
||||
label='启用 Turnstile 用户校验'
|
||||
name='TurnstileCheckEnabled'
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
</Form.Group>
|
||||
<Divider />
|
||||
<Header as='h3' inverted={isDark}>
|
||||
配置邮箱域名白名单
|
||||
<Header.Subheader>
|
||||
用以防止恶意用户利用临时邮箱批量注册
|
||||
</Header.Subheader>
|
||||
</Header>
|
||||
<Form.Group widths={3}>
|
||||
<Form.Checkbox
|
||||
label='启用邮箱域名白名单'
|
||||
name='EmailDomainRestrictionEnabled'
|
||||
onChange={handleInputChange}
|
||||
checked={inputs.EmailDomainRestrictionEnabled === 'true'}
|
||||
/>
|
||||
</Form.Group>
|
||||
<Form.Group widths={3}>
|
||||
<Form.Checkbox
|
||||
label='启用邮箱别名限制(例如:ab.cd@gmail.com)'
|
||||
name='EmailAliasRestrictionEnabled'
|
||||
onChange={handleInputChange}
|
||||
checked={inputs.EmailAliasRestrictionEnabled === 'true'}
|
||||
/>
|
||||
</Form.Group>
|
||||
<Form.Group widths={2}>
|
||||
<Form.Dropdown
|
||||
label='允许的邮箱域名'
|
||||
placeholder='允许的邮箱域名'
|
||||
name='EmailDomainWhitelist'
|
||||
required
|
||||
fluid
|
||||
multiple
|
||||
selection
|
||||
onChange={handleInputChange}
|
||||
value={inputs.EmailDomainWhitelist}
|
||||
autoComplete='new-password'
|
||||
options={EmailDomainWhitelist}
|
||||
/>
|
||||
<Form.Input
|
||||
label='添加新的允许的邮箱域名'
|
||||
action={
|
||||
<Button
|
||||
type='button'
|
||||
onClick={() => {
|
||||
submitNewRestrictedDomain();
|
||||
}}
|
||||
>
|
||||
填入
|
||||
</Button>
|
||||
}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
submitNewRestrictedDomain();
|
||||
}
|
||||
}}
|
||||
autoComplete='new-password'
|
||||
placeholder='输入新的允许的邮箱域名'
|
||||
value={restrictedDomainInput}
|
||||
onChange={(e, { value }) => {
|
||||
setRestrictedDomainInput(value);
|
||||
}}
|
||||
/>
|
||||
</Form.Group>
|
||||
<Form.Button onClick={submitEmailDomainWhitelist}>
|
||||
保存邮箱域名白名单设置
|
||||
</Form.Button>
|
||||
<Divider />
|
||||
<Header as='h3' inverted={isDark}>
|
||||
配置 SMTP
|
||||
<Header.Subheader>用以支持系统的邮件发送</Header.Subheader>
|
||||
</Header>
|
||||
<Form.Group widths={3}>
|
||||
<Form.Input
|
||||
label='SMTP 服务器地址'
|
||||
name='SMTPServer'
|
||||
onChange={handleInputChange}
|
||||
autoComplete='new-password'
|
||||
value={inputs.SMTPServer}
|
||||
placeholder='例如:smtp.qq.com'
|
||||
/>
|
||||
<Form.Input
|
||||
label='SMTP 端口'
|
||||
name='SMTPPort'
|
||||
onChange={handleInputChange}
|
||||
autoComplete='new-password'
|
||||
value={inputs.SMTPPort}
|
||||
placeholder='默认: 587'
|
||||
/>
|
||||
<Form.Input
|
||||
label='SMTP 账户'
|
||||
name='SMTPAccount'
|
||||
onChange={handleInputChange}
|
||||
autoComplete='new-password'
|
||||
value={inputs.SMTPAccount}
|
||||
placeholder='通常是邮箱地址'
|
||||
/>
|
||||
</Form.Group>
|
||||
<Form.Group widths={3}>
|
||||
<Form.Input
|
||||
label='SMTP 发送者邮箱'
|
||||
name='SMTPFrom'
|
||||
onChange={handleInputChange}
|
||||
autoComplete='new-password'
|
||||
value={inputs.SMTPFrom}
|
||||
placeholder='通常和邮箱地址保持一致'
|
||||
/>
|
||||
<Form.Input
|
||||
label='SMTP 访问凭证'
|
||||
name='SMTPToken'
|
||||
onChange={handleInputChange}
|
||||
type='password'
|
||||
autoComplete='new-password'
|
||||
checked={inputs.RegisterEnabled === 'true'}
|
||||
placeholder='敏感信息不会发送到前端显示'
|
||||
/>
|
||||
</Form.Group>
|
||||
<Form.Group widths={3}>
|
||||
<Form.Checkbox
|
||||
label='启用SMTP SSL(465端口强制开启)'
|
||||
name='SMTPSSLEnabled'
|
||||
onChange={handleInputChange}
|
||||
checked={inputs.SMTPSSLEnabled === 'true'}
|
||||
/>
|
||||
</Form.Group>
|
||||
<Form.Button onClick={submitSMTP}>保存 SMTP 设置</Form.Button>
|
||||
<Divider />
|
||||
<Header as='h3' inverted={isDark}>
|
||||
配置 GitHub OAuth App
|
||||
<Header.Subheader>
|
||||
用以支持通过 GitHub 进行登录注册,
|
||||
<a
|
||||
href='https://github.com/settings/developers'
|
||||
target='_blank'
|
||||
rel='noreferrer'
|
||||
>
|
||||
点击此处
|
||||
</a>
|
||||
管理你的 GitHub OAuth App
|
||||
</Header.Subheader>
|
||||
</Header>
|
||||
<Message>
|
||||
Homepage URL 填 <code>{inputs.ServerAddress}</code>
|
||||
,Authorization callback URL 填{' '}
|
||||
<code>{`${inputs.ServerAddress}/oauth/github`}</code>
|
||||
</Message>
|
||||
<Form.Group widths={3}>
|
||||
<Form.Input
|
||||
label='GitHub Client ID'
|
||||
name='GitHubClientId'
|
||||
onChange={handleInputChange}
|
||||
autoComplete='new-password'
|
||||
value={inputs.GitHubClientId}
|
||||
placeholder='输入你注册的 GitHub OAuth APP 的 ID'
|
||||
/>
|
||||
<Form.Input
|
||||
label='GitHub Client Secret'
|
||||
name='GitHubClientSecret'
|
||||
onChange={handleInputChange}
|
||||
type='password'
|
||||
autoComplete='new-password'
|
||||
value={inputs.GitHubClientSecret}
|
||||
placeholder='敏感信息不会发送到前端显示'
|
||||
/>
|
||||
</Form.Group>
|
||||
<Form.Button onClick={submitGitHubOAuth}>
|
||||
保存 GitHub OAuth 设置
|
||||
</Form.Button>
|
||||
<Divider />
|
||||
<Header as='h3' inverted={isDark}>
|
||||
配置 WeChat Server
|
||||
<Header.Subheader>
|
||||
用以支持通过微信进行登录注册,
|
||||
<a
|
||||
href='https://github.com/songquanpeng/wechat-server'
|
||||
target='_blank'
|
||||
rel='noreferrer'
|
||||
>
|
||||
点击此处
|
||||
</a>
|
||||
了解 WeChat Server
|
||||
</Header.Subheader>
|
||||
</Header>
|
||||
<Form.Group widths={3}>
|
||||
<Form.Input
|
||||
label='WeChat Server 服务器地址'
|
||||
name='WeChatServerAddress'
|
||||
placeholder='例如:https://yourdomain.com'
|
||||
onChange={handleInputChange}
|
||||
autoComplete='new-password'
|
||||
value={inputs.WeChatServerAddress}
|
||||
/>
|
||||
<Form.Input
|
||||
label='WeChat Server 访问凭证'
|
||||
name='WeChatServerToken'
|
||||
type='password'
|
||||
onChange={handleInputChange}
|
||||
autoComplete='new-password'
|
||||
value={inputs.WeChatServerToken}
|
||||
placeholder='敏感信息不会发送到前端显示'
|
||||
/>
|
||||
<Form.Input
|
||||
label='微信公众号二维码图片链接'
|
||||
name='WeChatAccountQRCodeImageURL'
|
||||
onChange={handleInputChange}
|
||||
autoComplete='new-password'
|
||||
value={inputs.WeChatAccountQRCodeImageURL}
|
||||
placeholder='输入一个图片链接'
|
||||
/>
|
||||
</Form.Group>
|
||||
<Form.Button onClick={submitWeChat}>
|
||||
保存 WeChat Server 设置
|
||||
</Form.Button>
|
||||
<Divider />
|
||||
<Header as='h3' inverted={isDark}>
|
||||
配置 Telegram 登录
|
||||
</Header>
|
||||
<Form.Group inline>
|
||||
<Form.Input
|
||||
label='Telegram Bot Token'
|
||||
name='TelegramBotToken'
|
||||
onChange={handleInputChange}
|
||||
value={inputs.TelegramBotToken}
|
||||
placeholder='输入你的 Telegram Bot Token'
|
||||
/>
|
||||
<Form.Input
|
||||
label='Telegram Bot 名称'
|
||||
name='TelegramBotName'
|
||||
onChange={handleInputChange}
|
||||
value={inputs.TelegramBotName}
|
||||
placeholder='输入你的 Telegram Bot 名称'
|
||||
/>
|
||||
</Form.Group>
|
||||
<Form.Button onClick={submitTelegramSettings}>
|
||||
保存 Telegram 登录设置
|
||||
</Form.Button>
|
||||
<Divider />
|
||||
<Header as='h3' inverted={isDark}>
|
||||
配置 Turnstile
|
||||
<Header.Subheader>
|
||||
用以支持用户校验,
|
||||
<a
|
||||
href='https://dash.cloudflare.com/'
|
||||
target='_blank'
|
||||
rel='noreferrer'
|
||||
>
|
||||
点击此处
|
||||
</a>
|
||||
管理你的 Turnstile Sites,推荐选择 Invisible Widget Type
|
||||
</Header.Subheader>
|
||||
</Header>
|
||||
<Form.Group widths={3}>
|
||||
<Form.Input
|
||||
label='Turnstile Site Key'
|
||||
name='TurnstileSiteKey'
|
||||
onChange={handleInputChange}
|
||||
autoComplete='new-password'
|
||||
value={inputs.TurnstileSiteKey}
|
||||
placeholder='输入你注册的 Turnstile Site Key'
|
||||
/>
|
||||
<Form.Input
|
||||
label='Turnstile Secret Key'
|
||||
name='TurnstileSecretKey'
|
||||
onChange={handleInputChange}
|
||||
type='password'
|
||||
autoComplete='new-password'
|
||||
value={inputs.TurnstileSecretKey}
|
||||
placeholder='敏感信息不会发送到前端显示'
|
||||
/>
|
||||
</Form.Group>
|
||||
<Form.Button onClick={submitTurnstile}>
|
||||
保存 Turnstile 设置
|
||||
</Form.Button>
|
||||
</Form>
|
||||
</Grid.Column>
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
|
||||
export default SystemSetting;
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
IconCalendarClock,
|
||||
IconChecklistStroked,
|
||||
IconComment,
|
||||
IconCommentStroked,
|
||||
IconCreditCard,
|
||||
IconGift,
|
||||
IconHelpCircle,
|
||||
@@ -42,11 +43,9 @@ const SiderBar = () => {
|
||||
const defaultIsCollapsed =
|
||||
isMobile() || localStorage.getItem('default_collapse_sidebar') === 'true';
|
||||
|
||||
let navigate = useNavigate();
|
||||
const [selectedKeys, setSelectedKeys] = useState(['home']);
|
||||
const systemName = getSystemName();
|
||||
const logo = getLogo();
|
||||
const [isCollapsed, setIsCollapsed] = useState(defaultIsCollapsed);
|
||||
const [chatItems, setChatItems] = useState([]);
|
||||
const theme = useTheme();
|
||||
const setTheme = useSetTheme();
|
||||
|
||||
@@ -65,16 +64,17 @@ const SiderBar = () => {
|
||||
detail: '/detail',
|
||||
pricing: '/pricing',
|
||||
task: '/task',
|
||||
playground: '/playground',
|
||||
};
|
||||
|
||||
const headerButtons = useMemo(
|
||||
() => [
|
||||
// {
|
||||
// text: '首页',
|
||||
// itemKey: 'home',
|
||||
// to: '/',
|
||||
// icon: <IconHome />,
|
||||
// },
|
||||
{
|
||||
text: 'Playground',
|
||||
itemKey: 'playground',
|
||||
to: '/playground',
|
||||
icon: <IconCommentStroked />,
|
||||
},
|
||||
{
|
||||
text: '模型价格',
|
||||
itemKey: 'pricing',
|
||||
@@ -91,11 +91,12 @@ const SiderBar = () => {
|
||||
{
|
||||
text: '聊天',
|
||||
itemKey: 'chat',
|
||||
to: '/chat',
|
||||
// to: '/chat',
|
||||
items: chatItems,
|
||||
icon: <IconComment />,
|
||||
className: localStorage.getItem('chat_link')
|
||||
? 'semi-navigation-item-normal'
|
||||
: 'tableHiddle',
|
||||
// className: localStorage.getItem('chat_link')
|
||||
// ? 'semi-navigation-item-normal'
|
||||
// : 'tableHiddle',
|
||||
},
|
||||
{
|
||||
text: '令牌',
|
||||
@@ -177,6 +178,7 @@ const SiderBar = () => {
|
||||
localStorage.getItem('enable_drawing'),
|
||||
localStorage.getItem('enable_task'),
|
||||
localStorage.getItem('chat_link'),
|
||||
chatItems,
|
||||
isAdmin(),
|
||||
],
|
||||
);
|
||||
@@ -207,6 +209,33 @@ const SiderBar = () => {
|
||||
localKey = 'home';
|
||||
}
|
||||
setSelectedKeys([localKey]);
|
||||
let chatLink = localStorage.getItem('chat_link');
|
||||
if (!chatLink) {
|
||||
let chats = localStorage.getItem('chats');
|
||||
if (chats) {
|
||||
// console.log(chats);
|
||||
try {
|
||||
chats = JSON.parse(chats);
|
||||
if (Array.isArray(chats)) {
|
||||
let chatItems = [];
|
||||
for (let i = 0; i < chats.length; i++) {
|
||||
let chat = {};
|
||||
for (let key in chats[i]) {
|
||||
chat.text = key;
|
||||
chat.itemKey = 'chat' + i;
|
||||
chat.to = '/chat/' + i;
|
||||
}
|
||||
// setRouterMap({ ...routerMap, chat: '/chat/' + i })
|
||||
chatItems.push(chat);
|
||||
}
|
||||
setChatItems(chatItems);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showError('聊天数据解析失败');
|
||||
}
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
@@ -223,6 +252,27 @@ const SiderBar = () => {
|
||||
}}
|
||||
selectedKeys={selectedKeys}
|
||||
renderWrapper={({ itemElement, isSubNav, isInSubNav, props }) => {
|
||||
let chatLink = localStorage.getItem('chat_link');
|
||||
if (!chatLink) {
|
||||
let chats = localStorage.getItem('chats');
|
||||
if (chats) {
|
||||
chats = JSON.parse(chats);
|
||||
if (Array.isArray(chats) && chats.length > 0) {
|
||||
for (let i = 0; i < chats.length; i++) {
|
||||
routerMap['chat' + i] = '/chat/' + i;
|
||||
}
|
||||
if (chats.length > 1) {
|
||||
// delete /chat
|
||||
if (routerMap['chat']) {
|
||||
delete routerMap['chat'];
|
||||
}
|
||||
} else {
|
||||
// rename /chat to /chat/0
|
||||
routerMap['chat'] = '/chat/0';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return (
|
||||
<Link
|
||||
style={{ textDecoration: 'none' }}
|
||||
@@ -236,15 +286,6 @@ const SiderBar = () => {
|
||||
onSelect={(key) => {
|
||||
setSelectedKeys([key.itemKey]);
|
||||
}}
|
||||
// header={{
|
||||
// logo: (
|
||||
// <img src={logo} alt='logo' style={{ marginRight: '0.75em' }} />
|
||||
// ),
|
||||
// text: systemName,
|
||||
// }}
|
||||
// footer={{
|
||||
// text: '© 2021 NekoAPI',
|
||||
// }}
|
||||
footer={
|
||||
<>
|
||||
{isMobile() && (
|
||||
|
||||
@@ -25,17 +25,6 @@ import {
|
||||
import { IconTreeTriangleDown } from '@douyinfe/semi-icons';
|
||||
import EditToken from '../pages/Token/EditToken';
|
||||
|
||||
const COPY_OPTIONS = [
|
||||
{ key: 'next', text: 'ChatGPT Next Web', value: 'next' },
|
||||
{ key: 'ama', text: 'ChatGPT Web & Midjourney', value: 'ama' },
|
||||
{ key: 'opencat', text: 'OpenCat', value: 'opencat' },
|
||||
];
|
||||
|
||||
const OPEN_LINK_OPTIONS = [
|
||||
{ key: 'ama', text: 'ChatGPT Web & Midjourney', value: 'ama' },
|
||||
{ key: 'opencat', text: 'OpenCat', value: 'opencat' },
|
||||
];
|
||||
|
||||
function renderTimestamp(timestamp) {
|
||||
return <>{timestamp2string(timestamp)}</>;
|
||||
}
|
||||
@@ -88,28 +77,6 @@ function renderStatus(status, model_limits_enabled = false) {
|
||||
}
|
||||
|
||||
const TokensTable = () => {
|
||||
const link_menu = [
|
||||
{
|
||||
node: 'item',
|
||||
key: 'next',
|
||||
name: 'ChatGPT Next Web',
|
||||
onClick: () => {
|
||||
onOpenLink('next');
|
||||
},
|
||||
},
|
||||
{ node: 'item', key: 'ama', name: 'AMA 问天', value: 'ama' },
|
||||
{
|
||||
node: 'item',
|
||||
key: 'next-mj',
|
||||
name: 'ChatGPT Web & Midjourney',
|
||||
value: 'next-mj',
|
||||
onClick: () => {
|
||||
onOpenLink('next-mj');
|
||||
},
|
||||
},
|
||||
{ node: 'item', key: 'opencat', name: 'OpenCat', value: 'opencat' },
|
||||
];
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: '名称',
|
||||
@@ -177,149 +144,174 @@ const TokensTable = () => {
|
||||
{
|
||||
title: '',
|
||||
dataIndex: 'operate',
|
||||
render: (text, record, index) => (
|
||||
<div>
|
||||
<Popover
|
||||
content={'sk-' + record.key}
|
||||
style={{ padding: 20 }}
|
||||
position='top'
|
||||
>
|
||||
<Button theme='light' type='tertiary' style={{ marginRight: 1 }}>
|
||||
查看
|
||||
</Button>
|
||||
</Popover>
|
||||
<Button
|
||||
theme='light'
|
||||
type='secondary'
|
||||
style={{ marginRight: 1 }}
|
||||
onClick={async (text) => {
|
||||
await copyText('sk-' + record.key);
|
||||
}}
|
||||
>
|
||||
复制
|
||||
</Button>
|
||||
<SplitButtonGroup
|
||||
style={{ marginRight: 1 }}
|
||||
aria-label='项目操作按钮组'
|
||||
>
|
||||
<Button
|
||||
theme='light'
|
||||
style={{ color: 'rgba(var(--semi-teal-7), 1)' }}
|
||||
onClick={() => {
|
||||
onOpenLink('next', record.key);
|
||||
}}
|
||||
render: (text, record, index) => {
|
||||
let chats = localStorage.getItem('chats');
|
||||
let chatsArray = [];
|
||||
let chatLink = localStorage.getItem('chat_link');
|
||||
let mjLink = localStorage.getItem('chat_link2');
|
||||
let shouldUseCustom = true;
|
||||
if (chatLink) {
|
||||
shouldUseCustom = false;
|
||||
chatLink += `/#/?settings={"key":"{key}","url":"{address}"}`;
|
||||
chatsArray.push({
|
||||
node: 'item',
|
||||
key: 'default',
|
||||
name: 'ChatGPT Next Web',
|
||||
onClick: () => {
|
||||
onOpenLink('default', chatLink, record);
|
||||
},
|
||||
});
|
||||
}
|
||||
if (mjLink) {
|
||||
shouldUseCustom = false;
|
||||
mjLink += `/#/?settings={"key":"{key}","url":"{address}"}`;
|
||||
chatsArray.push({
|
||||
node: 'item',
|
||||
key: 'mj',
|
||||
name: 'ChatGPT Next Midjourney',
|
||||
onClick: () => {
|
||||
onOpenLink('mj', mjLink, record);
|
||||
},
|
||||
});
|
||||
}
|
||||
if (shouldUseCustom) {
|
||||
try {
|
||||
// console.log(chats);
|
||||
chats = JSON.parse(chats);
|
||||
// check chats is array
|
||||
if (Array.isArray(chats)) {
|
||||
for (let i = 0; i < chats.length; i++) {
|
||||
let chat = {};
|
||||
chat.node = 'item';
|
||||
// c is a map
|
||||
// chat.key = chats[i].name;
|
||||
// console.log(chats[i])
|
||||
for (let key in chats[i]) {
|
||||
if (chats[i].hasOwnProperty(key)) {
|
||||
chat.key = i;
|
||||
chat.name = key;
|
||||
chat.onClick = () => {
|
||||
onOpenLink(key, chats[i][key], record);
|
||||
};
|
||||
}
|
||||
}
|
||||
chatsArray.push(chat);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
showError('聊天链接配置错误,请联系管理员');
|
||||
}
|
||||
}
|
||||
return (
|
||||
<div>
|
||||
<Popover
|
||||
content={'sk-' + record.key}
|
||||
style={{ padding: 20 }}
|
||||
position='top'
|
||||
>
|
||||
聊天
|
||||
</Button>
|
||||
<Dropdown
|
||||
trigger='click'
|
||||
position='bottomRight'
|
||||
menu={[
|
||||
{
|
||||
node: 'item',
|
||||
key: 'next',
|
||||
disabled: !localStorage.getItem('chat_link'),
|
||||
name: 'ChatGPT Next Web',
|
||||
onClick: () => {
|
||||
onOpenLink('next', record.key);
|
||||
},
|
||||
},
|
||||
{
|
||||
node: 'item',
|
||||
key: 'next-mj',
|
||||
disabled: !localStorage.getItem('chat_link2'),
|
||||
name: 'ChatGPT Web & Midjourney',
|
||||
onClick: () => {
|
||||
onOpenLink('next-mj', record.key);
|
||||
},
|
||||
},
|
||||
// {
|
||||
// node: 'item',
|
||||
// key: 'lobe',
|
||||
// name: 'Lobe Chat',
|
||||
// onClick: () => {
|
||||
// onOpenLink('lobe', record.key);
|
||||
// },
|
||||
// },
|
||||
{
|
||||
node: 'item',
|
||||
key: 'ama',
|
||||
name: 'AMA 问天(BotGem)',
|
||||
onClick: () => {
|
||||
onOpenLink('ama', record.key);
|
||||
},
|
||||
},
|
||||
{
|
||||
node: 'item',
|
||||
key: 'opencat',
|
||||
name: 'OpenCat',
|
||||
onClick: () => {
|
||||
onOpenLink('opencat', record.key);
|
||||
},
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Button
|
||||
style={{
|
||||
padding: '8px 4px',
|
||||
color: 'rgba(var(--semi-teal-7), 1)',
|
||||
}}
|
||||
type='primary'
|
||||
icon={<IconTreeTriangleDown />}
|
||||
></Button>
|
||||
</Dropdown>
|
||||
</SplitButtonGroup>
|
||||
<Popconfirm
|
||||
title='确定是否要删除此令牌?'
|
||||
content='此修改将不可逆'
|
||||
okType={'danger'}
|
||||
position={'left'}
|
||||
onConfirm={() => {
|
||||
manageToken(record.id, 'delete', record).then(() => {
|
||||
removeRecord(record.key);
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Button theme='light' type='danger' style={{ marginRight: 1 }}>
|
||||
删除
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
{record.status === 1 ? (
|
||||
<Button
|
||||
theme='light'
|
||||
type='warning'
|
||||
style={{ marginRight: 1 }}
|
||||
onClick={async () => {
|
||||
manageToken(record.id, 'disable', record);
|
||||
}}
|
||||
>
|
||||
禁用
|
||||
</Button>
|
||||
) : (
|
||||
<Button theme='light' type='tertiary' style={{ marginRight: 1 }}>
|
||||
查看
|
||||
</Button>
|
||||
</Popover>
|
||||
<Button
|
||||
theme='light'
|
||||
type='secondary'
|
||||
style={{ marginRight: 1 }}
|
||||
onClick={async () => {
|
||||
manageToken(record.id, 'enable', record);
|
||||
onClick={async (text) => {
|
||||
await copyText('sk-' + record.key);
|
||||
}}
|
||||
>
|
||||
启用
|
||||
复制
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
theme='light'
|
||||
type='tertiary'
|
||||
style={{ marginRight: 1 }}
|
||||
onClick={() => {
|
||||
setEditingToken(record);
|
||||
setShowEdit(true);
|
||||
}}
|
||||
>
|
||||
编辑
|
||||
</Button>
|
||||
</div>
|
||||
),
|
||||
<SplitButtonGroup
|
||||
style={{ marginRight: 1 }}
|
||||
aria-label='项目操作按钮组'
|
||||
>
|
||||
<Button
|
||||
theme='light'
|
||||
style={{ color: 'rgba(var(--semi-teal-7), 1)' }}
|
||||
onClick={() => {
|
||||
if (chatsArray.length === 0) {
|
||||
showError('请联系管理员配置聊天链接');
|
||||
} else {
|
||||
onOpenLink(
|
||||
'default',
|
||||
chats[0][Object.keys(chats[0])[0]],
|
||||
record,
|
||||
);
|
||||
}
|
||||
}}
|
||||
>
|
||||
聊天
|
||||
</Button>
|
||||
<Dropdown
|
||||
trigger='click'
|
||||
position='bottomRight'
|
||||
menu={chatsArray}
|
||||
>
|
||||
<Button
|
||||
style={{
|
||||
padding: '8px 4px',
|
||||
color: 'rgba(var(--semi-teal-7), 1)',
|
||||
}}
|
||||
type='primary'
|
||||
icon={<IconTreeTriangleDown />}
|
||||
></Button>
|
||||
</Dropdown>
|
||||
</SplitButtonGroup>
|
||||
<Popconfirm
|
||||
title='确定是否要删除此令牌?'
|
||||
content='此修改将不可逆'
|
||||
okType={'danger'}
|
||||
position={'left'}
|
||||
onConfirm={() => {
|
||||
manageToken(record.id, 'delete', record).then(() => {
|
||||
removeRecord(record.key);
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Button theme='light' type='danger' style={{ marginRight: 1 }}>
|
||||
删除
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
{record.status === 1 ? (
|
||||
<Button
|
||||
theme='light'
|
||||
type='warning'
|
||||
style={{ marginRight: 1 }}
|
||||
onClick={async () => {
|
||||
manageToken(record.id, 'disable', record);
|
||||
}}
|
||||
>
|
||||
禁用
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
theme='light'
|
||||
type='secondary'
|
||||
style={{ marginRight: 1 }}
|
||||
onClick={async () => {
|
||||
manageToken(record.id, 'enable', record);
|
||||
}}
|
||||
>
|
||||
启用
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
theme='light'
|
||||
type='tertiary'
|
||||
style={{ marginRight: 1 }}
|
||||
onClick={() => {
|
||||
setEditingToken(record);
|
||||
setShowEdit(true);
|
||||
}}
|
||||
>
|
||||
编辑
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
@@ -333,8 +325,7 @@ const TokensTable = () => {
|
||||
const [searchKeyword, setSearchKeyword] = useState('');
|
||||
const [searchToken, setSearchToken] = useState('');
|
||||
const [searching, setSearching] = useState(false);
|
||||
const [showTopUpModal, setShowTopUpModal] = useState(false);
|
||||
const [targetTokenIdx, setTargetTokenIdx] = useState(0);
|
||||
const [chats, setChats] = useState([]);
|
||||
const [editingToken, setEditingToken] = useState({
|
||||
id: undefined,
|
||||
});
|
||||
@@ -379,16 +370,6 @@ const TokensTable = () => {
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
const onPaginationChange = (e, { activePage }) => {
|
||||
(async () => {
|
||||
if (activePage === Math.ceil(tokens.length / pageSize) + 1) {
|
||||
// In this case we have to load more data and then append them.
|
||||
await loadTokens(activePage - 1);
|
||||
}
|
||||
setActivePage(activePage);
|
||||
})();
|
||||
};
|
||||
|
||||
const refresh = async () => {
|
||||
await loadTokens(activePage - 1);
|
||||
};
|
||||
@@ -405,7 +386,8 @@ const TokensTable = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const onOpenLink = async (type, key) => {
|
||||
const onOpenLink = async (type, url, record) => {
|
||||
// console.log(type, url, key);
|
||||
let status = localStorage.getItem('status');
|
||||
let serverAddress = '';
|
||||
if (status) {
|
||||
@@ -416,36 +398,39 @@ const TokensTable = () => {
|
||||
serverAddress = window.location.origin;
|
||||
}
|
||||
let encodedServerAddress = encodeURIComponent(serverAddress);
|
||||
const chatLink = localStorage.getItem('chat_link');
|
||||
const mjLink = localStorage.getItem('chat_link2');
|
||||
let defaultUrl;
|
||||
|
||||
if (chatLink) {
|
||||
defaultUrl =
|
||||
chatLink + `/#/?settings={"key":"sk-${key}","url":"${serverAddress}"}`;
|
||||
}
|
||||
let url;
|
||||
switch (type) {
|
||||
case 'ama':
|
||||
url = `ama://set-api-key?server=${encodedServerAddress}&key=sk-${key}`;
|
||||
break;
|
||||
case 'opencat':
|
||||
url = `opencat://team/join?domain=${encodedServerAddress}&token=sk-${key}`;
|
||||
break;
|
||||
case 'lobe':
|
||||
url = `https://chat-preview.lobehub.com/?settings={"keyVaults":{"openai":{"apiKey":"sk-${key}","baseURL":"${encodedServerAddress}/v1"}}}`;
|
||||
break;
|
||||
case 'next-mj':
|
||||
url =
|
||||
mjLink + `/#/?settings={"key":"sk-${key}","url":"${serverAddress}"}`;
|
||||
break;
|
||||
default:
|
||||
if (!chatLink) {
|
||||
showError('管理员未设置聊天链接');
|
||||
return;
|
||||
}
|
||||
url = defaultUrl;
|
||||
}
|
||||
url = url.replace('{address}', encodedServerAddress);
|
||||
url = url.replace('{key}', 'sk-' + record.key);
|
||||
// console.log(url);
|
||||
// const chatLink = localStorage.getItem('chat_link');
|
||||
// const mjLink = localStorage.getItem('chat_link2');
|
||||
// let defaultUrl;
|
||||
//
|
||||
// if (chatLink) {
|
||||
// defaultUrl =
|
||||
// chatLink + `/#/?settings={"key":"sk-${key}","url":"${serverAddress}"}`;
|
||||
// }
|
||||
// let url;
|
||||
// switch (type) {
|
||||
// case 'ama':
|
||||
// url = `ama://set-api-key?server=${encodedServerAddress}&key=sk-${key}`;
|
||||
// break;
|
||||
// case 'opencat':
|
||||
// url = `opencat://team/join?domain=${encodedServerAddress}&token=sk-${key}`;
|
||||
// break;
|
||||
// case 'lobe':
|
||||
// url = `https://chat-preview.lobehub.com/?settings={"keyVaults":{"openai":{"apiKey":"sk-${key}","baseURL":"${encodedServerAddress}/v1"}}}`;
|
||||
// break;
|
||||
// case 'next-mj':
|
||||
// url =
|
||||
// mjLink + `/#/?settings={"key":"sk-${key}","url":"${serverAddress}"}`;
|
||||
// break;
|
||||
// default:
|
||||
// if (!chatLink) {
|
||||
// showError('管理员未设置聊天链接');
|
||||
// return;
|
||||
// }
|
||||
// url = defaultUrl;
|
||||
// }
|
||||
|
||||
window.open(url, '_blank');
|
||||
};
|
||||
|
||||
@@ -151,7 +151,7 @@ const UsersTable = () => {
|
||||
title='确定?'
|
||||
okType={'warning'}
|
||||
onConfirm={() => {
|
||||
manageUser(record.username, 'promote', record);
|
||||
manageUser(record.id, 'promote', record);
|
||||
}}
|
||||
>
|
||||
<Button theme='light' type='warning' style={{ marginRight: 1 }}>
|
||||
@@ -162,7 +162,7 @@ const UsersTable = () => {
|
||||
title='确定?'
|
||||
okType={'warning'}
|
||||
onConfirm={() => {
|
||||
manageUser(record.username, 'demote', record);
|
||||
manageUser(record.id, 'demote', record);
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
@@ -179,7 +179,7 @@ const UsersTable = () => {
|
||||
type='warning'
|
||||
style={{ marginRight: 1 }}
|
||||
onClick={async () => {
|
||||
manageUser(record.username, 'disable', record);
|
||||
manageUser(record.id, 'disable', record);
|
||||
}}
|
||||
>
|
||||
禁用
|
||||
@@ -190,7 +190,7 @@ const UsersTable = () => {
|
||||
type='secondary'
|
||||
style={{ marginRight: 1 }}
|
||||
onClick={async () => {
|
||||
manageUser(record.username, 'enable', record);
|
||||
manageUser(record.id, 'enable', record);
|
||||
}}
|
||||
disabled={record.status === 3}
|
||||
>
|
||||
@@ -214,7 +214,7 @@ const UsersTable = () => {
|
||||
okType={'danger'}
|
||||
position={'left'}
|
||||
onConfirm={() => {
|
||||
manageUser(record.username, 'delete', record).then(() => {
|
||||
manageUser(record.id, 'delete', record).then(() => {
|
||||
removeRecord(record.id);
|
||||
});
|
||||
}}
|
||||
@@ -303,9 +303,9 @@ const UsersTable = () => {
|
||||
fetchGroups().then();
|
||||
}, []);
|
||||
|
||||
const manageUser = async (username, action, record) => {
|
||||
const manageUser = async (userId, action, record) => {
|
||||
const res = await API.post('/api/user/manage', {
|
||||
username,
|
||||
id: userId,
|
||||
action,
|
||||
});
|
||||
const { success, message } = res.data;
|
||||
|
||||
@@ -4,7 +4,7 @@ import { API, showError } from '../helpers';
|
||||
|
||||
async function fetchTokenKeys() {
|
||||
try {
|
||||
const response = await API.get('/api/token/?p=0&size=999');
|
||||
const response = await API.get('/api/token/?p=0&size=100');
|
||||
const { success, data } = response.data;
|
||||
if (success) {
|
||||
const activeTokens = data.filter((token) => token.status === 1);
|
||||
@@ -38,9 +38,9 @@ function getServerAddress() {
|
||||
return serverAddress;
|
||||
}
|
||||
|
||||
export function useTokenKeys() {
|
||||
export function useTokenKeys(id) {
|
||||
const [keys, setKeys] = useState([]);
|
||||
const [chatLink, setChatLink] = useState('');
|
||||
// const [chatLink, setChatLink] = useState('');
|
||||
const [serverAddress, setServerAddress] = useState('');
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
@@ -55,9 +55,7 @@ export function useTokenKeys() {
|
||||
}
|
||||
setKeys(fetchedKeys);
|
||||
setIsLoading(false);
|
||||
|
||||
const link = localStorage.getItem('chat_link');
|
||||
setChatLink(link);
|
||||
// setChatLink(link);
|
||||
|
||||
const address = getServerAddress();
|
||||
setServerAddress(address);
|
||||
@@ -66,5 +64,5 @@ export function useTokenKeys() {
|
||||
loadAllData();
|
||||
}, []);
|
||||
|
||||
return { keys, chatLink, serverAddress, isLoading };
|
||||
return { keys, serverAddress, isLoading };
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ export function setStatusData(data) {
|
||||
localStorage.setItem('enable_drawing', data.enable_drawing);
|
||||
localStorage.setItem('enable_task', data.enable_task);
|
||||
localStorage.setItem('enable_data_export', data.enable_data_export);
|
||||
localStorage.setItem('chats', JSON.stringify(data.chats));
|
||||
localStorage.setItem(
|
||||
'data_export_default_time',
|
||||
data.data_export_default_time,
|
||||
|
||||
@@ -59,6 +59,17 @@ body {
|
||||
display: revert;
|
||||
}
|
||||
|
||||
.semi-chat {
|
||||
padding-top: 0 !important;
|
||||
padding-bottom: 0 !important;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.semi-chat-chatBox-content {
|
||||
min-width: auto;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.tableHiddle {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
@@ -1,13 +1,32 @@
|
||||
import React from 'react';
|
||||
import React, { useEffect } from 'react';
|
||||
import { useTokenKeys } from '../../components/fetchTokenKeys';
|
||||
import { Layout } from '@douyinfe/semi-ui';
|
||||
import { Banner, Layout } from '@douyinfe/semi-ui';
|
||||
import { useParams } from 'react-router-dom';
|
||||
|
||||
const ChatPage = () => {
|
||||
const { keys, chatLink, serverAddress, isLoading } = useTokenKeys();
|
||||
const { id } = useParams();
|
||||
const { keys, serverAddress, isLoading } = useTokenKeys(id);
|
||||
|
||||
const comLink = (key) => {
|
||||
if (!chatLink || !serverAddress || !key) return '';
|
||||
return `${chatLink}/#/?settings={"key":"sk-${key}","url":"${encodeURIComponent(serverAddress)}"}`;
|
||||
// console.log('chatLink:', chatLink);
|
||||
if (!serverAddress || !key) return '';
|
||||
let link = localStorage.getItem('chat_link');
|
||||
if (link) {
|
||||
link = `${link}/#/?settings={"key":"sk-${key}","url":"${encodeURIComponent(serverAddress)}"}`;
|
||||
} else if (id) {
|
||||
let chats = localStorage.getItem('chats');
|
||||
if (chats) {
|
||||
chats = JSON.parse(chats);
|
||||
if (Array.isArray(chats) && chats.length > 0) {
|
||||
for (let k in chats[id]) {
|
||||
link = chats[id][k];
|
||||
link = link.replace('{address}', encodeURIComponent(serverAddress));
|
||||
link = link.replace('{key}', 'sk-' + key);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return link;
|
||||
};
|
||||
|
||||
const iframeSrc = keys.length > 0 ? comLink(keys[0]) : '';
|
||||
@@ -22,11 +41,7 @@ const ChatPage = () => {
|
||||
<div>
|
||||
<Layout>
|
||||
<Layout.Header>
|
||||
<h3 style={{ color: 'red' }}>
|
||||
当前没有可用的已启用令牌,请确认是否有令牌处于启用状态!
|
||||
<br />
|
||||
正在跳转......
|
||||
</h3>
|
||||
<Banner description={'正在跳转......'} type={'warning'} />
|
||||
</Layout.Header>
|
||||
</Layout>
|
||||
</div>
|
||||
|
||||
162
web/src/pages/Setting/Operation/SettingsChats.js
Normal file
162
web/src/pages/Setting/Operation/SettingsChats.js
Normal file
@@ -0,0 +1,162 @@
|
||||
import React, { useEffect, useState, useRef } from 'react';
|
||||
import {
|
||||
Banner,
|
||||
Button,
|
||||
Col,
|
||||
Form,
|
||||
Popconfirm,
|
||||
Row,
|
||||
Space,
|
||||
Spin,
|
||||
} from '@douyinfe/semi-ui';
|
||||
import {
|
||||
compareObjects,
|
||||
API,
|
||||
showError,
|
||||
showSuccess,
|
||||
showWarning,
|
||||
verifyJSON,
|
||||
verifyJSONPromise,
|
||||
} from '../../../helpers';
|
||||
|
||||
export default function SettingsChats(props) {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [inputs, setInputs] = useState({
|
||||
Chats: '[]',
|
||||
});
|
||||
const refForm = useRef();
|
||||
const [inputsRow, setInputsRow] = useState(inputs);
|
||||
|
||||
async function onSubmit() {
|
||||
try {
|
||||
console.log('Starting validation...');
|
||||
await refForm.current
|
||||
.validate()
|
||||
.then(() => {
|
||||
console.log('Validation passed');
|
||||
const updateArray = compareObjects(inputs, inputsRow);
|
||||
if (!updateArray.length) return showWarning('你似乎并没有修改什么');
|
||||
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('部分保存失败,请重试');
|
||||
}
|
||||
showSuccess('保存成功');
|
||||
props.refresh();
|
||||
})
|
||||
.catch(() => {
|
||||
showError('保存失败,请重试');
|
||||
})
|
||||
.finally(() => {
|
||||
setLoading(false);
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Validation failed:', error);
|
||||
showError('请检查输入');
|
||||
});
|
||||
} catch (error) {
|
||||
showError('请检查输入');
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
async function resetModelRatio() {
|
||||
try {
|
||||
let res = await API.post(`/api/option/rest_model_ratio`);
|
||||
// return {success, message}
|
||||
if (res.data.success) {
|
||||
showSuccess(res.data.message);
|
||||
props.refresh();
|
||||
} else {
|
||||
showError(res.data.message);
|
||||
}
|
||||
} catch (error) {
|
||||
showError(error);
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const currentInputs = {};
|
||||
for (let key in props.options) {
|
||||
if (Object.keys(inputs).includes(key)) {
|
||||
if (key === 'Chats') {
|
||||
const obj = JSON.parse(props.options[key]);
|
||||
currentInputs[key] = JSON.stringify(obj, null, 2);
|
||||
} else {
|
||||
currentInputs[key] = props.options[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
setInputs(currentInputs);
|
||||
setInputsRow(structuredClone(currentInputs));
|
||||
refForm.current.setValues(currentInputs);
|
||||
}, [props.options]);
|
||||
|
||||
return (
|
||||
<Spin spinning={loading}>
|
||||
<Form
|
||||
values={inputs}
|
||||
getFormApi={(formAPI) => (refForm.current = formAPI)}
|
||||
style={{ marginBottom: 15 }}
|
||||
>
|
||||
<Form.Section text={'令牌聊天设置'}>
|
||||
<Banner
|
||||
type='warning'
|
||||
description={
|
||||
'必须将上方聊天链接全部设置为空,才能使用下方聊天设置功能'
|
||||
}
|
||||
/>
|
||||
<Banner
|
||||
type='info'
|
||||
description={
|
||||
'链接中的{key}将自动替换为sk-xxxx,{address}将自动替换为系统设置的服务器地址,末尾不带/和/v1'
|
||||
}
|
||||
/>
|
||||
<Form.TextArea
|
||||
label={'聊天配置'}
|
||||
extraText={''}
|
||||
placeholder={'为一个 JSON 文本'}
|
||||
field={'Chats'}
|
||||
autosize={{ minRows: 6, maxRows: 12 }}
|
||||
trigger='blur'
|
||||
stopValidateWithError
|
||||
rules={[
|
||||
{
|
||||
validator: (rule, value) => {
|
||||
return verifyJSON(value);
|
||||
},
|
||||
message: '不是合法的 JSON 字符串',
|
||||
},
|
||||
]}
|
||||
onChange={(value) =>
|
||||
setInputs({
|
||||
...inputs,
|
||||
Chats: value,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</Form.Section>
|
||||
</Form>
|
||||
<Space>
|
||||
<Button onClick={onSubmit}>保存聊天设置</Button>
|
||||
</Space>
|
||||
</Spin>
|
||||
);
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useEffect, useState, useRef } from 'react';
|
||||
import { Button, Col, Form, Row, Spin } from '@douyinfe/semi-ui';
|
||||
import { Banner, Button, Col, Form, Row, Spin } from '@douyinfe/semi-ui';
|
||||
import {
|
||||
compareObjects,
|
||||
API,
|
||||
@@ -74,6 +74,10 @@ export default function GeneralSettings(props) {
|
||||
return (
|
||||
<>
|
||||
<Spin spinning={loading}>
|
||||
<Banner
|
||||
type='warning'
|
||||
description={'聊天链接功能已经弃用,请使用下方聊天设置功能'}
|
||||
/>
|
||||
<Form
|
||||
values={inputs}
|
||||
getFormApi={(formAPI) => (refForm.current = formAPI)}
|
||||
|
||||
@@ -50,7 +50,7 @@ const EditToken = (props) => {
|
||||
group,
|
||||
} = inputs;
|
||||
// const [visible, setVisible] = useState(false);
|
||||
const [models, setModels] = useState({});
|
||||
const [models, setModels] = useState([]);
|
||||
const [groups, setGroups] = useState([]);
|
||||
const navigate = useNavigate();
|
||||
const handleInputChange = (name, value) => {
|
||||
|
||||
@@ -55,6 +55,10 @@ export default defineConfig({
|
||||
target: 'http://localhost:3000',
|
||||
changeOrigin: true,
|
||||
},
|
||||
'/pg': {
|
||||
target: 'http://localhost:3000',
|
||||
changeOrigin: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
1261
web/yarn.lock
1261
web/yarn.lock
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user