Compare commits

..

3 Commits

Author SHA1 Message Date
JustSong
4a0e81fe83 fix: fix quota not consumed 2023-05-14 20:36:28 +08:00
JustSong
976c29ea9f docs: update README 2023-05-14 19:37:15 +08:00
JustSong
926951ee03 feat: able to customize system name & logo now 2023-05-14 19:29:02 +08:00
12 changed files with 103 additions and 50 deletions

View File

@@ -43,28 +43,28 @@ _✨ All in one 的 OpenAI 接口,整合各种 API 访问方式,开箱即用
## 功能 ## 功能
1. 支持多种 API 访问渠道,欢迎 PR 或提 issue 添加更多渠道: 1. 支持多种 API 访问渠道,欢迎 PR 或提 issue 添加更多渠道:
+ [x] OpenAI 官方通道 + [x] OpenAI 官方通道
+ [x] Azure OpenAI API + [x] **Azure OpenAI API**
+ [x] [API2D](https://api2d.com/r/197971) + [x] [API2D](https://api2d.com/r/197971)
+ [x] [CloseAI](https://console.openai-asia.com) + [x] [CloseAI](https://console.openai-asia.com)
+ [x] [OpenAI-SB](https://openai-sb.com) + [x] [OpenAI-SB](https://openai-sb.com)
+ [x] [OpenAI Max](https://openaimax.com) + [x] [OpenAI Max](https://openaimax.com)
+ [x] [OhMyGPT](https://www.ohmygpt.com) + [x] [OhMyGPT](https://www.ohmygpt.com)
+ [x] 自定义渠道:例如使用自行搭建的 OpenAI 代理 + [x] 自定义渠道:例如使用自行搭建的 OpenAI 代理
2. 支持通过负载均衡的方式访问多个渠道。 2. 支持通过**负载均衡**的方式访问多个渠道。
3. 支持单个访问渠道设置多个 API Key利用起来你的多个 API Key 3. 支持 **stream 模式**,可以通过流式传输实现打字机效果
4. 支持 HTTP SSE可以通过流式传输实现打字机效果 4. 支持**令牌管理**,设置令牌的过期时间和使用次数
5. 支持设置令牌的过期时间和使用次数 5. 支持**兑换码管理**,支持批量生成和导出兑换码,可使用兑换码为令牌进行充值
6. 支持批量生成和导出兑换码,可使用兑换码为令牌进行充值 6. 支持**通道管理**,批量创建通道
7. 支持批量创建通道 7. 支持发布公告,设置充值链接,设置新用户初始额度
8. 支持发布公告,自定义关于页面,设置充值链接,自定义页脚。 8. 支持丰富的**自定义**设置,
9. 支持自定义首页,可以选择使用 HTML & Markdown 代码进行自定义,或者使用一个单独的网页通过 iframe 嵌入 1. 支持自定义系统名称logo 以及页脚
10. 支持通过系统访问令牌访问管理 API 2. 支持自定义首页和关于页面,可以选择使用 HTML & Markdown 代码进行自定义,或者使用一个单独的网页通过 iframe 嵌入
11. 多种用户登录注册方式: 9. 支持通过系统访问令牌访问管理 API。
10. 支持用户管理,支持**多种用户登录注册方式**
+ 邮箱登录注册以及通过邮箱进行密码重置。 + 邮箱登录注册以及通过邮箱进行密码重置。
+ [GitHub 开放授权](https://github.com/settings/applications/new)。 + [GitHub 开放授权](https://github.com/settings/applications/new)。
+ 微信公众号授权(需要额外部署 [WeChat Server](https://github.com/songquanpeng/wechat-server))。 + 微信公众号授权(需要额外部署 [WeChat Server](https://github.com/songquanpeng/wechat-server))。
12. 支持用户管理,支持为新用户设置初始配额。 11. 未来其他大模型开放 API 后,将第一时间支持,并将其封装成同样的 API 访问方式
13. 未来其他大模型开放 API 后,将第一时间支持,并将其封装成同样的 API 访问方式。
## 部署 ## 部署
### 基于 Docker 进行部署 ### 基于 Docker 进行部署

View File

@@ -11,6 +11,7 @@ var Version = "v0.0.0" // this hard coding will be replaced automatic
var SystemName = "One API" var SystemName = "One API"
var ServerAddress = "http://localhost:3000" var ServerAddress = "http://localhost:3000"
var Footer = "" var Footer = ""
var Logo = ""
var TopUpLink = "" var TopUpLink = ""
var UsingSQLite = false var UsingSQLite = false

View File

@@ -20,6 +20,7 @@ func GetStatus(c *gin.Context) {
"github_oauth": common.GitHubOAuthEnabled, "github_oauth": common.GitHubOAuthEnabled,
"github_client_id": common.GitHubClientId, "github_client_id": common.GitHubClientId,
"system_name": common.SystemName, "system_name": common.SystemName,
"logo": common.Logo,
"footer_html": common.Footer, "footer_html": common.Footer,
"wechat_qrcode": common.WeChatAccountQRCodeImageURL, "wechat_qrcode": common.WeChatAccountQRCodeImageURL,
"wechat_login": common.WeChatAuthEnabled, "wechat_login": common.WeChatAuthEnabled,

View File

@@ -111,14 +111,9 @@ func TokenAuth() func(c *gin.Context) {
c.Set("id", token.UserId) c.Set("id", token.UserId)
c.Set("token_id", token.Id) c.Set("token_id", token.Id)
requestURL := c.Request.URL.String() requestURL := c.Request.URL.String()
consumeQuota := false consumeQuota := !token.UnlimitedQuota
switch requestURL { if strings.HasPrefix(requestURL, "/models") {
case "/v1/chat/completions": consumeQuota = false
consumeQuota = !token.UnlimitedQuota
case "/v1/completions":
consumeQuota = !token.UnlimitedQuota
case "/v1/edits":
consumeQuota = !token.UnlimitedQuota
} }
c.Set("consume_quota", consumeQuota) c.Set("consume_quota", consumeQuota)
if len(parts) > 1 { if len(parts) > 1 {

View File

@@ -41,6 +41,8 @@ func InitOptionMap() {
common.OptionMap["About"] = "" common.OptionMap["About"] = ""
common.OptionMap["HomePageContent"] = "" common.OptionMap["HomePageContent"] = ""
common.OptionMap["Footer"] = common.Footer common.OptionMap["Footer"] = common.Footer
common.OptionMap["SystemName"] = common.SystemName
common.OptionMap["Logo"] = common.Logo
common.OptionMap["ServerAddress"] = "" common.OptionMap["ServerAddress"] = ""
common.OptionMap["GitHubClientId"] = "" common.OptionMap["GitHubClientId"] = ""
common.OptionMap["GitHubClientSecret"] = "" common.OptionMap["GitHubClientSecret"] = ""
@@ -134,6 +136,10 @@ func updateOptionMap(key string, value string) (err error) {
common.GitHubClientSecret = value common.GitHubClientSecret = value
case "Footer": case "Footer":
common.Footer = value common.Footer = value
case "SystemName":
common.SystemName = value
case "Logo":
common.Logo = value
case "WeChatServerAddress": case "WeChatServerAddress":
common.WeChatServerAddress = value common.WeChatServerAddress = value
case "WeChatServerToken": case "WeChatServerToken":

View File

@@ -42,6 +42,8 @@ function App() {
if (success) { if (success) {
localStorage.setItem('status', JSON.stringify(data)); localStorage.setItem('status', JSON.stringify(data));
statusDispatch({ type: 'set', payload: data }); statusDispatch({ type: 'set', payload: data });
localStorage.setItem('system_name', data.system_name);
localStorage.setItem('logo', data.logo);
localStorage.setItem('footer_html', data.footer_html); localStorage.setItem('footer_html', data.footer_html);
if ( if (
data.version !== process.env.REACT_APP_VERSION && data.version !== process.env.REACT_APP_VERSION &&

View File

@@ -1,40 +1,37 @@
import React, { useEffect, useState } from 'react'; import React from 'react';
import { Container, Segment } from 'semantic-ui-react'; import { Container, Segment } from 'semantic-ui-react';
import { getFooterHTML, getSystemName } from '../helpers';
const Footer = () => { const Footer = () => {
const [Footer, setFooter] = useState(''); const systemName = getSystemName();
useEffect(() => { const footer = getFooterHTML();
let savedFooter = localStorage.getItem('footer_html');
if (!savedFooter) savedFooter = '';
setFooter(savedFooter);
});
return ( return (
<Segment vertical> <Segment vertical>
<Container textAlign="center"> <Container textAlign='center'>
{Footer === '' ? ( {footer ? (
<div className="custom-footer"> <div
className='custom-footer'
dangerouslySetInnerHTML={{ __html: footer }}
></div>
) : (
<div className='custom-footer'>
<a <a
href="https://github.com/songquanpeng/one-api" href='https://github.com/songquanpeng/one-api'
target="_blank" target='_blank'
> >
One API {process.env.REACT_APP_VERSION}{' '} {systemName} {process.env.REACT_APP_VERSION}{' '}
</a> </a>
{' '} {' '}
<a href="https://github.com/songquanpeng" target="_blank"> <a href='https://github.com/songquanpeng' target='_blank'>
JustSong JustSong
</a>{' '} </a>{' '}
构建源代码遵循{' '} 构建源代码遵循{' '}
<a href="https://opensource.org/licenses/mit-license.php"> <a href='https://opensource.org/licenses/mit-license.php'>
MIT 协议 MIT 协议
</a> </a>
</div> </div>
) : (
<div
className="custom-footer"
dangerouslySetInnerHTML={{ __html: Footer }}
></div>
)} )}
</Container> </Container>
</Segment> </Segment>

View File

@@ -3,7 +3,7 @@ import { Link, useNavigate } from 'react-router-dom';
import { UserContext } from '../context/User'; import { UserContext } from '../context/User';
import { Button, Container, Dropdown, Icon, Menu, Segment } from 'semantic-ui-react'; import { Button, Container, Dropdown, Icon, Menu, Segment } from 'semantic-ui-react';
import { API, isAdmin, isMobile, showSuccess } from '../helpers'; import { API, getLogo, getSystemName, isAdmin, isMobile, showSuccess } from '../helpers';
import '../index.css'; import '../index.css';
// Header Buttons // Header Buttons
@@ -53,6 +53,8 @@ const Header = () => {
let navigate = useNavigate(); let navigate = useNavigate();
const [showSidebar, setShowSidebar] = useState(false); const [showSidebar, setShowSidebar] = useState(false);
const systemName = getSystemName();
const logo = getLogo();
async function logout() { async function logout() {
setShowSidebar(false); setShowSidebar(false);
@@ -111,12 +113,12 @@ const Header = () => {
<Container> <Container>
<Menu.Item as={Link} to='/'> <Menu.Item as={Link} to='/'>
<img <img
src='/logo.png' src={logo}
alt='logo' alt='logo'
style={{ marginRight: '0.75em' }} style={{ marginRight: '0.75em' }}
/> />
<div style={{ fontSize: '20px' }}> <div style={{ fontSize: '20px' }}>
<b>One API</b> <b>{systemName}</b>
</div> </div>
</Menu.Item> </Menu.Item>
<Menu.Menu position='right'> <Menu.Menu position='right'>
@@ -168,9 +170,9 @@ const Header = () => {
<Menu borderless style={{ borderTop: 'none' }}> <Menu borderless style={{ borderTop: 'none' }}>
<Container> <Container>
<Menu.Item as={Link} to='/' className={'hide-on-mobile'}> <Menu.Item as={Link} to='/' className={'hide-on-mobile'}>
<img src='/logo.png' alt='logo' style={{ marginRight: '0.75em' }} /> <img src={logo} alt='logo' style={{ marginRight: '0.75em' }} />
<div style={{ fontSize: '20px' }}> <div style={{ fontSize: '20px' }}>
<b>One API</b> <b>{systemName}</b>
</div> </div>
</Menu.Item> </Menu.Item>
{renderButtons(false)} {renderButtons(false)}

View File

@@ -12,7 +12,7 @@ import {
} from 'semantic-ui-react'; } from 'semantic-ui-react';
import { Link, useNavigate, useSearchParams } from 'react-router-dom'; import { Link, useNavigate, useSearchParams } from 'react-router-dom';
import { UserContext } from '../context/User'; import { UserContext } from '../context/User';
import { API, showError, showSuccess } from '../helpers'; import { API, getLogo, showError, showSuccess } from '../helpers';
const LoginForm = () => { const LoginForm = () => {
const [inputs, setInputs] = useState({ const [inputs, setInputs] = useState({
@@ -27,6 +27,7 @@ const LoginForm = () => {
let navigate = useNavigate(); let navigate = useNavigate();
const [status, setStatus] = useState({}); const [status, setStatus] = useState({});
const logo = getLogo();
useEffect(() => { useEffect(() => {
if (searchParams.get("expired")) { if (searchParams.get("expired")) {
@@ -95,7 +96,7 @@ const LoginForm = () => {
<Grid textAlign="center" style={{ marginTop: '48px' }}> <Grid textAlign="center" style={{ marginTop: '48px' }}>
<Grid.Column style={{ maxWidth: 450 }}> <Grid.Column style={{ maxWidth: 450 }}>
<Header as="h2" color="" textAlign="center"> <Header as="h2" color="" textAlign="center">
<Image src="/logo.png" /> 用户登录 <Image src={logo} /> 用户登录
</Header> </Header>
<Form size="large"> <Form size="large">
<Segment> <Segment>

View File

@@ -8,6 +8,8 @@ const OtherSetting = () => {
Footer: '', Footer: '',
Notice: '', Notice: '',
About: '', About: '',
SystemName: '',
Logo: '',
HomePageContent: '', HomePageContent: '',
}); });
let originInputs = {}; let originInputs = {};
@@ -66,6 +68,14 @@ const OtherSetting = () => {
await updateOption('Footer', inputs.Footer); await updateOption('Footer', inputs.Footer);
}; };
const submitSystemName = async () => {
await updateOption('SystemName', inputs.SystemName);
};
const submitLogo = async () => {
await updateOption('Logo', inputs.Logo);
};
const submitAbout = async () => { const submitAbout = async () => {
await updateOption('About', inputs.About); await updateOption('About', inputs.About);
}; };
@@ -114,6 +124,27 @@ const OtherSetting = () => {
<Form.Button onClick={submitNotice}>保存公告</Form.Button> <Form.Button onClick={submitNotice}>保存公告</Form.Button>
<Divider /> <Divider />
<Header as='h3'>个性化设置</Header> <Header as='h3'>个性化设置</Header>
<Form.Group widths='equal'>
<Form.Input
label='系统名称'
placeholder='在此输入系统名称'
value={inputs.SystemName}
name='SystemName'
onChange={handleInputChange}
/>
</Form.Group>
<Form.Button onClick={submitSystemName}>设置系统名称</Form.Button>
<Form.Group widths='equal'>
<Form.Input
label='Logo 图片地址'
placeholder='在此输入 Logo 图片地址'
value={inputs.Logo}
name='Logo'
type='url'
onChange={handleInputChange}
/>
</Form.Group>
<Form.Button onClick={submitLogo}>设置 Logo</Form.Button>
<Form.Group widths='equal'> <Form.Group widths='equal'>
<Form.TextArea <Form.TextArea
label='首页内容' label='首页内容'

View File

@@ -9,7 +9,7 @@ import {
Segment, Segment,
} from 'semantic-ui-react'; } from 'semantic-ui-react';
import { Link, useNavigate } from 'react-router-dom'; import { Link, useNavigate } from 'react-router-dom';
import { API, showError, showInfo, showSuccess } from '../helpers'; import { API, getLogo, showError, showInfo, showSuccess } from '../helpers';
import Turnstile from 'react-turnstile'; import Turnstile from 'react-turnstile';
const RegisterForm = () => { const RegisterForm = () => {
@@ -26,6 +26,7 @@ const RegisterForm = () => {
const [turnstileSiteKey, setTurnstileSiteKey] = useState(''); const [turnstileSiteKey, setTurnstileSiteKey] = useState('');
const [turnstileToken, setTurnstileToken] = useState(''); const [turnstileToken, setTurnstileToken] = useState('');
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const logo = getLogo();
useEffect(() => { useEffect(() => {
let status = localStorage.getItem('status'); let status = localStorage.getItem('status');
@@ -100,7 +101,7 @@ const RegisterForm = () => {
<Grid textAlign='center' style={{ marginTop: '48px' }}> <Grid textAlign='center' style={{ marginTop: '48px' }}>
<Grid.Column style={{ maxWidth: 450 }}> <Grid.Column style={{ maxWidth: 450 }}>
<Header as='h2' color='' textAlign='center'> <Header as='h2' color='' textAlign='center'>
<Image src='/logo.png' /> 新用户注册 <Image src={logo} /> 新用户注册
</Header> </Header>
<Form size='large'> <Form size='large'>
<Segment> <Segment>

View File

@@ -15,6 +15,22 @@ export function isRoot() {
return user.role >= 100; return user.role >= 100;
} }
export function getSystemName() {
let system_name = localStorage.getItem('system_name');
if (!system_name) return 'One API';
return system_name;
}
export function getLogo() {
let logo = localStorage.getItem('logo');
if (!logo) return '/logo.png';
return logo
}
export function getFooterHTML() {
return localStorage.getItem('footer_html');
}
export async function copy(text) { export async function copy(text) {
let okay = true; let okay = true;
try { try {