diff --git a/README.md b/README.md index 8f43a74eb..341733e90 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,11 @@ +
# New API +Calcium-Ion%2Fnew-api | Trendshift + +
+ > [!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) -## 交流群 - - ## 界面截图 ![image](https://github.com/Calcium-Ion/new-api/assets/61247483/ad0e7aae-0203-471c-9716-2d83768927d4) -![image](https://github.com/Calcium-Ion/new-api/assets/61247483/d1ac216e-0804-4105-9fdc-66b35022d861) - -![image](https://github.com/Calcium-Ion/new-api/assets/61247483/3ca0b282-00ff-4c96-bf9d-e29ef615c605) -![image](https://github.com/Calcium-Ion/new-api/assets/61247483/f4f40ed4-8ccb-43d7-a580-90677827646d) +![image](https://github.com/Calcium-Ion/new-api/assets/61247483/3ca0b282-00ff-4c96-bf9d-e29ef615c605) ![image](https://github.com/Calcium-Ion/new-api/assets/61247483/90d7d763-6a77-4b36-9f76-2bb30f18583d) -![image](https://github.com/Calcium-Ion/new-api/assets/61247483/e414228a-3c35-429a-b298-6451d76d9032) 夜间模式 ![image](https://github.com/Calcium-Ion/new-api/assets/61247483/1c66b593-bb9e-4757-9720-ff2759539242) - -![image](https://github.com/Calcium-Ion/new-api/assets/61247483/5b3228e8-2556-44f7-97d6-4f8d8ee6effa) ![image](https://github.com/Calcium-Ion/new-api/assets/61247483/af9a07ee-5101-4b3d-8bd9-ae21a4fd7e9e) +## 交流群 + + ## 相关项目 - [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 3d95508cb..3d0cb6a00 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 6b4a878b9..36beb2d18 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 39e602463..50a368f6f 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 f9a590017..481960efa 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 3ca5b8f7f..9b55cc2d2 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 272c5734f..18aa2979e 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 18cfdd05f..c56dd095c 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 7b80ac70f..891a1aa8a 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 5510d4251..b73bb0eea 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 + ), + }:{ + 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 9c77d1890..d373e98e0 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 94b22862b..3def4a939 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 2af406f8a..64aa71982 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白名单(请勿过度信任此功能) +
+