merge upstream

Signed-off-by: wozulong <>
This commit is contained in:
wozulong
2024-10-14 16:31:22 +08:00
52 changed files with 6145 additions and 2260 deletions

View File

@@ -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

File diff suppressed because it is too large Load Diff

View File

@@ -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 />

View File

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

View File

@@ -39,10 +39,10 @@ let buttons = [
// icon: <IconHomeStroked />,
},
// {
// text: '模型价格',
// itemKey: 'pricing',
// to: '/pricing',
// icon: <IconNoteMoneyStroked />,
// text: 'Playground',
// itemKey: 'playground',
// to: '/playground',
// // icon: <IconNoteMoneyStroked />,
// },
];

View File

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

View File

@@ -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} />

View 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;

View 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 SSL465端口强制开启'
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;

View File

@@ -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() && (

View File

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

View File

@@ -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;

View File

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

View File

@@ -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,

View File

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

View File

@@ -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>

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

View File

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

View File

@@ -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) => {

View File

@@ -55,6 +55,10 @@ export default defineConfig({
target: 'http://localhost:3000',
changeOrigin: true,
},
'/pg': {
target: 'http://localhost:3000',
changeOrigin: true,
},
},
},
});

File diff suppressed because it is too large Load Diff