diff --git a/README.md b/README.md
index 8f43a74..341733e 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,11 @@
+
# New API
+

+
+
+
> [!NOTE]
> 本项目为开源项目,在[One API](https://github.com/songquanpeng/one-api)的基础上进行二次开发
@@ -115,24 +120,18 @@ docker run --name new-api -d --restart always -p 3000:3000 -e SQL_DSN="root:1234
## Suno接口设置文档
[对接文档](Suno.md)
-## 交流群
-
-
## 界面截图

-
-
-
-
+

-
夜间模式

-
-

+## 交流群
+
+
## 相关项目
- [One API](https://github.com/songquanpeng/one-api):原版项目
- [Midjourney-Proxy](https://github.com/novicezk/midjourney-proxy):Midjourney接口支持
diff --git a/common/utils.go b/common/utils.go
index 3d95508..3d0cb6a 100644
--- a/common/utils.go
+++ b/common/utils.go
@@ -128,6 +128,11 @@ func IntMax(a int, b int) int {
}
}
+func IsIP(s string) bool {
+ ip := net.ParseIP(s)
+ return ip != nil
+}
+
func GetUUID() string {
code := uuid.New().String()
code = strings.Replace(code, "-", "", -1)
diff --git a/controller/model.go b/controller/model.go
index 6b4a878..36beb2d 100644
--- a/controller/model.go
+++ b/controller/model.go
@@ -146,22 +146,49 @@ func ListModels(c *gin.Context) {
})
return
}
- models := model.GetGroupModels(user.Group)
userOpenAiModels := make([]dto.OpenAIModels, 0)
permission := getPermission()
- for _, s := range models {
- if _, ok := openAIModelsMap[s]; ok {
- userOpenAiModels = append(userOpenAiModels, openAIModelsMap[s])
+
+ modelLimitEnable := c.GetBool("token_model_limit_enabled")
+ if modelLimitEnable {
+ s, ok := c.Get("token_model_limit")
+ var tokenModelLimit map[string]bool
+ if ok {
+ tokenModelLimit = s.(map[string]bool)
} else {
- userOpenAiModels = append(userOpenAiModels, dto.OpenAIModels{
- Id: s,
- Object: "model",
- Created: 1626777600,
- OwnedBy: "custom",
- Permission: permission,
- Root: s,
- Parent: nil,
- })
+ tokenModelLimit = map[string]bool{}
+ }
+ for allowModel, _ := range tokenModelLimit {
+ if _, ok := openAIModelsMap[allowModel]; ok {
+ userOpenAiModels = append(userOpenAiModels, openAIModelsMap[allowModel])
+ } else {
+ userOpenAiModels = append(userOpenAiModels, dto.OpenAIModels{
+ Id: allowModel,
+ Object: "model",
+ Created: 1626777600,
+ OwnedBy: "custom",
+ Permission: permission,
+ Root: allowModel,
+ Parent: nil,
+ })
+ }
+ }
+ } else {
+ models := model.GetGroupModels(user.Group)
+ for _, s := range models {
+ if _, ok := openAIModelsMap[s]; ok {
+ userOpenAiModels = append(userOpenAiModels, openAIModelsMap[s])
+ } else {
+ userOpenAiModels = append(userOpenAiModels, dto.OpenAIModels{
+ Id: s,
+ Object: "model",
+ Created: 1626777600,
+ OwnedBy: "custom",
+ Permission: permission,
+ Root: s,
+ Parent: nil,
+ })
+ }
}
}
c.JSON(200, gin.H{
diff --git a/controller/token.go b/controller/token.go
index 39e6024..50a368f 100644
--- a/controller/token.go
+++ b/controller/token.go
@@ -134,6 +134,7 @@ func AddToken(c *gin.Context) {
UnlimitedQuota: token.UnlimitedQuota,
ModelLimitsEnabled: token.ModelLimitsEnabled,
ModelLimits: token.ModelLimits,
+ AllowIps: token.AllowIps,
}
err = cleanToken.Insert()
if err != nil {
@@ -221,6 +222,7 @@ func UpdateToken(c *gin.Context) {
cleanToken.UnlimitedQuota = token.UnlimitedQuota
cleanToken.ModelLimitsEnabled = token.ModelLimitsEnabled
cleanToken.ModelLimits = token.ModelLimits
+ cleanToken.AllowIps = token.AllowIps
}
err = cleanToken.Update()
if err != nil {
diff --git a/middleware/auth.go b/middleware/auth.go
index f9a5900..481960e 100644
--- a/middleware/auth.go
+++ b/middleware/auth.go
@@ -175,6 +175,7 @@ func TokenAuth() func(c *gin.Context) {
} else {
c.Set("token_model_limit_enabled", false)
}
+ c.Set("allow_ips", token.GetIpLimitsMap())
if len(parts) > 1 {
if model.IsAdmin(token.UserId) {
c.Set("specific_channel_id", parts[1])
diff --git a/middleware/distributor.go b/middleware/distributor.go
index 3ca5b8f..9b55cc2 100644
--- a/middleware/distributor.go
+++ b/middleware/distributor.go
@@ -22,6 +22,14 @@ type ModelRequest struct {
func Distribute() func(c *gin.Context) {
return func(c *gin.Context) {
+ allowIpsMap := c.GetStringMap("allow_ips")
+ if len(allowIpsMap) != 0 {
+ clientIp := c.ClientIP()
+ if _, ok := allowIpsMap[clientIp]; !ok {
+ abortWithOpenAiMessage(c, http.StatusForbidden, "您的 IP 不在令牌允许访问的列表中")
+ return
+ }
+ }
userId := c.GetInt("id")
var channel *model.Channel
channelId, ok := c.Get("specific_channel_id")
diff --git a/model/token.go b/model/token.go
index 272c573..18aa297 100644
--- a/model/token.go
+++ b/model/token.go
@@ -23,10 +23,33 @@ type Token struct {
UnlimitedQuota bool `json:"unlimited_quota" gorm:"default:false"`
ModelLimitsEnabled bool `json:"model_limits_enabled" gorm:"default:false"`
ModelLimits string `json:"model_limits" gorm:"type:varchar(1024);default:''"`
+ AllowIps *string `json:"allow_ips" gorm:"default:''"`
UsedQuota int `json:"used_quota" gorm:"default:0"` // used quota
DeletedAt gorm.DeletedAt `gorm:"index"`
}
+func (token *Token) GetIpLimitsMap() map[string]any {
+ // delete empty spaces
+ //split with \n
+ ipLimitsMap := make(map[string]any)
+ if token.AllowIps == nil {
+ return ipLimitsMap
+ }
+ cleanIps := strings.ReplaceAll(*token.AllowIps, " ", "")
+ if cleanIps == "" {
+ return ipLimitsMap
+ }
+ ips := strings.Split(cleanIps, "\n")
+ for _, ip := range ips {
+ ip = strings.TrimSpace(ip)
+ ip = strings.ReplaceAll(ip, ",", "")
+ if common.IsIP(ip) {
+ ipLimitsMap[ip] = true
+ }
+ }
+ return ipLimitsMap
+}
+
func GetAllUserTokens(userId int, startIdx int, num int) ([]*Token, error) {
var tokens []*Token
var err error
@@ -130,7 +153,7 @@ func (token *Token) Insert() error {
// Update Make sure your token's fields is completed, because this will update non-zero values
func (token *Token) Update() error {
var err error
- err = DB.Model(token).Select("name", "status", "expired_time", "remain_quota", "unlimited_quota", "model_limits_enabled", "model_limits").Updates(token).Error
+ err = DB.Model(token).Select("name", "status", "expired_time", "remain_quota", "unlimited_quota", "model_limits_enabled", "model_limits", "allow_ips").Updates(token).Error
return err
}
diff --git a/web/src/App.js b/web/src/App.js
index 18cfdd0..c56dd09 100644
--- a/web/src/App.js
+++ b/web/src/App.js
@@ -20,12 +20,11 @@ import Redemption from './pages/Redemption';
import TopUp from './pages/TopUp';
import Log from './pages/Log';
import Chat from './pages/Chat';
-import Chat2Link from './pages/Chat2Link';
+import Chat2Link from './pages/Chat2Link';
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 Detail from './pages/Detail';
const Home = lazy(() => import('./pages/Home'));
const Detail = lazy(() => import('./pages/Detail'));
@@ -59,204 +58,203 @@ function App() {
}, []);
return (
-
-
-
-
+
+ }>
+
+
+ }
+ />
+
+
+
+ }
+ />
+ }>
+
+
+ }
+ />
+ }>
+
+
+ }
+ />
+
+
+
+ }
+ />
+
+
+
+ }
+ />
+
+
+
+ }
+ />
+ }>
+
+
+ }
+ />
+ }>
+
+
+ }
+ />
+ }>
+
+
+ }
+ />
+ }>
+
+
+ }
+ />
+ }>
+
+
+ }
+ />
+ }>
+
+
+ }
+ />
+ }>
+
+
+ }
+ />
+
}>
-
+
- }
- />
-
-
-
- }
- />
-
+ }
+ />
+
}>
-
+
- }
- />
-
+ }
+ />
+
+
+
+ }
+ />
+
}>
-
+
- }
- />
-
-
-
- }
- />
-
-
-
- }
- />
-
-
-
- }
- />
-
+ }
+ />
+
}>
-
+
- }
- />
-
+ }
+ />
+
}>
-
+
- }
- />
- }>
-
-
- }
- />
- }>
-
-
- }
- />
- }>
-
-
- }
- />
- }>
-
-
- }
- />
- }>
-
-
- }
- />
-
- }>
-
-
-
- }
- />
-
- }>
-
-
-
- }
- />
-
-
-
- }
- />
-
- }>
-
-
-
- }
- />
-
- }>
-
-
-
- }
- />
-
- }>
-
-
-
- }
- />
- }>
-
-
- }
- />
- }>
-
-
- }
- />
- }>
-
-
- }
- />
- {/* 方便使用chat2link直接跳转聊天... */}
+
+ }
+ />
+ }>
+
+
+ }
+ />
+ }>
+
+
+ }
+ />
+ }>
+
+
+ }
+ />
+ {/* 方便使用chat2link直接跳转聊天... */}
} />
-
-
+ >
);
}
diff --git a/web/src/components/Footer.js b/web/src/components/Footer.js
index 7b80ac7..891a1aa 100644
--- a/web/src/components/Footer.js
+++ b/web/src/components/Footer.js
@@ -3,7 +3,7 @@ import React, { useEffect, useState } from 'react';
import { getFooterHTML, getSystemName } from '../helpers';
import { Layout, Tooltip } from '@douyinfe/semi-ui';
-const Footer = () => {
+const FooterBar = () => {
const systemName = getSystemName();
const [footer, setFooter] = useState(getFooterHTML());
let remainCheckTimes = 5;
@@ -56,19 +56,17 @@ const Footer = () => {
}, []);
return (
-
-
- {footer ? (
-
- ) : (
- defaultFooter
- )}
-
-
+
+ {footer ? (
+
+ ) : (
+ defaultFooter
+ )}
+
);
};
-export default Footer;
+export default FooterBar;
diff --git a/web/src/components/HeaderBar.js b/web/src/components/HeaderBar.js
index 5510d42..b73bb0e 100644
--- a/web/src/components/HeaderBar.js
+++ b/web/src/components/HeaderBar.js
@@ -3,14 +3,23 @@ import { Link, useNavigate } from 'react-router-dom';
import { UserContext } from '../context/User';
import { useSetTheme, useTheme } from '../context/Theme';
-import { API, getLogo, getSystemName, showSuccess } from '../helpers';
+import { API, getLogo, getSystemName, isMobile, showSuccess } from '../helpers';
import '../index.css';
import fireworks from 'react-fireworks';
-import { IconHelpCircle, IconKey, IconUser } from '@douyinfe/semi-icons';
+import {
+ IconHelpCircle,
+ IconHome,
+ IconHomeStroked,
+ IconKey,
+ IconNoteMoneyStroked,
+ IconPriceTag,
+ IconUser
+} from '@douyinfe/semi-icons';
import { Avatar, Dropdown, Layout, Nav, Switch } from '@douyinfe/semi-ui';
import { stringToColor } from '../helpers/render';
+import Text from '@douyinfe/semi-ui/lib/es/typography/text';
// HeaderBar Buttons
let headerButtons = [
@@ -22,6 +31,21 @@ let headerButtons = [
},
];
+let buttons = [
+ {
+ text: '首页',
+ itemKey: 'home',
+ to: '/',
+ icon: ,
+ },
+ // {
+ // text: '模型价格',
+ // itemKey: 'pricing',
+ // to: '/pricing',
+ // icon: ,
+ // },
+];
+
if (localStorage.getItem('chat_link')) {
headerButtons.splice(1, 0, {
name: '聊天',
@@ -90,6 +114,7 @@ const HeaderBar = () => {
about: '/about',
login: '/login',
register: '/register',
+ home: '/',
};
return (
{
selectedKeys={[]}
// items={headerButtons}
onSelect={(key) => {}}
+ header={isMobile()?{
+ logo: (
+
+ ),
+ }:{
+ logo: (
+
+ ),
+ text: systemName,
+
+ }}
+ items={buttons}
footer={
<>
{isNewYear && (
@@ -121,15 +158,19 @@ const HeaderBar = () => {
)}
} />
- {
- setTheme(checked);
- }}
- />
+ <>
+ {!isMobile() && (
+ {
+ setTheme(checked);
+ }}
+ />
+ )}
+ >
{userState.user ? (
<>
{
}
+ // icon={}
/>
{
const systemName = getSystemName();
const logo = getLogo();
const [isCollapsed, setIsCollapsed] = useState(defaultIsCollapsed);
+ const theme = useTheme();
+ const setTheme = useSetTheme();
const routerMap = {
home: '/',
@@ -63,11 +67,17 @@ const SiderBar = () => {
const headerButtons = useMemo(
() => [
+ // {
+ // text: '首页',
+ // itemKey: 'home',
+ // to: '/',
+ // icon: ,
+ // },
{
- text: '首页',
- itemKey: 'home',
- to: '/',
- icon: ,
+ text: '模型价格',
+ itemKey: 'pricing',
+ to: '/pricing',
+ icon: ,
},
{
text: '渠道',
@@ -104,12 +114,6 @@ const SiderBar = () => {
to: '/topup',
icon: ,
},
- {
- text: '模型价格',
- itemKey: 'pricing',
- to: '/pricing',
- icon: ,
- },
{
text: '用户管理',
itemKey: 'user',
@@ -205,48 +209,58 @@ const SiderBar = () => {
return (
<>
-
-
-
-
-
+
>
);
};
diff --git a/web/src/index.css b/web/src/index.css
index 9c77d18..d373e98 100644
--- a/web/src/index.css
+++ b/web/src/index.css
@@ -9,11 +9,12 @@ body {
scrollbar-width: none;
color: var(--semi-color-text-0) !important;
background-color: var(--semi-color-bg-0) !important;
- height: 100%;
+ height: 100vh;
}
#root {
- height: 100%;
+ height: 100vh;
+ flex-direction: column;
}
@media only screen and (max-width: 767px) {
@@ -50,9 +51,9 @@ body {
}
}
-.semi-layout {
- height: 100%;
-}
+/*.semi-layout {*/
+/* height: 100%;*/
+/*}*/
.tableShow {
display: revert;
diff --git a/web/src/index.js b/web/src/index.js
index 94b2286..3def4a9 100644
--- a/web/src/index.js
+++ b/web/src/index.js
@@ -3,7 +3,6 @@ import ReactDOM from 'react-dom/client';
import { BrowserRouter } from 'react-router-dom';
import App from './App';
import HeaderBar from './components/HeaderBar';
-import Footer from './components/Footer';
import 'semantic-ui-offline/semantic.min.css';
import './index.css';
import { UserProvider } from './context/User';
@@ -13,35 +12,36 @@ import { StatusProvider } from './context/Status';
import { Layout } from '@douyinfe/semi-ui';
import SiderBar from './components/SiderBar';
import { ThemeProvider } from './context/Theme';
+import FooterBar from './components/Footer';
// initialization
const root = ReactDOM.createRoot(document.getElementById('root'));
-const { Sider, Content, Header } = Layout;
+const { Sider, Content, Header, Footer } = Layout;
root.render(
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/web/src/pages/Token/EditToken.js b/web/src/pages/Token/EditToken.js
index 2af406f..64aa719 100644
--- a/web/src/pages/Token/EditToken.js
+++ b/web/src/pages/Token/EditToken.js
@@ -18,8 +18,8 @@ import {
Select,
SideSheet,
Space,
- Spin,
- Typography,
+ Spin, TextArea,
+ Typography
} from '@douyinfe/semi-ui';
import Title from '@douyinfe/semi-ui/lib/es/typography/title';
import { Divider } from 'semantic-ui-react';
@@ -34,6 +34,7 @@ const EditToken = (props) => {
unlimited_quota: false,
model_limits_enabled: false,
model_limits: [],
+ allow_ips: '',
};
const [inputs, setInputs] = useState(originInputs);
const {
@@ -43,6 +44,7 @@ const EditToken = (props) => {
unlimited_quota,
model_limits_enabled,
model_limits,
+ allow_ips
} = inputs;
// const [visible, setVisible] = useState(false);
const [models, setModels] = useState({});
@@ -374,6 +376,19 @@ const EditToken = (props) => {
+
+ IP白名单(请勿过度信任此功能)
+
+