Merge branch 'main' into telegram-login

This commit is contained in:
Ehco 2024-03-03 19:42:06 +08:00 committed by GitHub
commit 02d5a5f16d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
27 changed files with 233 additions and 116 deletions

View File

@ -9,14 +9,19 @@ import (
"github.com/google/uuid"
)
// Pay Settings
var PayAddress = ""
var CustomCallbackAddress = ""
var EpayId = ""
var EpayKey = ""
var Price = 7.3
var MinTopUp = 1
var StartTime = time.Now().Unix() // unit: second
var Version = "v0.0.0" // this hard coding will be replaced automatically when building, no need to manually change
var SystemName = "New API"
var ServerAddress = "http://localhost:3000"
var PayAddress = ""
var EpayId = ""
var EpayKey = ""
var Price = 7.3
var Footer = ""
var Logo = ""
var TopUpLink = ""
@ -29,6 +34,7 @@ var DrawingEnabled = true
var DataExportEnabled = true
var DataExportInterval = 5 // unit: minute
var DataExportDefaultTime = "hour" // unit: minute
var DefaultCollapseSidebar = false // default value of collapse sidebar
// Any options with "Secret", "Token" in its key won't be return by GetOptions

View File

@ -80,7 +80,10 @@ var ModelRatio = map[string]float64{
"qwen-turbo": 0.8572, // ¥0.012 / 1k tokens
"qwen-plus": 10, // ¥0.14 / 1k tokens
"text-embedding-v1": 0.05, // ¥0.0007 / 1k tokens
"SparkDesk": 1.2858, // ¥0.018 / 1k tokens
"SparkDesk-v1.1": 1.2858, // ¥0.018 / 1k tokens
"SparkDesk-v2.1": 1.2858, // ¥0.018 / 1k tokens
"SparkDesk-v3.1": 1.2858, // ¥0.018 / 1k tokens
"SparkDesk-v3.5": 1.2858, // ¥0.018 / 1k tokens
"360GPT_S2_V9": 0.8572, // ¥0.012 / 1k tokens
"embedding-bert-512-v1": 0.0715, // ¥0.001 / 1k tokens
"embedding_s1_v1": 0.0715, // ¥0.001 / 1k tokens

View File

@ -54,8 +54,9 @@ func FixChannelsAbilities(c *gin.Context) {
func SearchChannels(c *gin.Context) {
keyword := c.Query("keyword")
group := c.Query("group")
modelKeyword := c.Query("model")
//idSort, _ := strconv.ParseBool(c.Query("id_sort"))
channels, err := model.SearchChannels(keyword, group)
channels, err := model.SearchChannels(keyword, group, modelKeyword)
if err != nil {
c.JSON(http.StatusOK, gin.H{
"success": false,

View File

@ -29,6 +29,7 @@ func GetStatus(c *gin.Context) {
"wechat_login": common.WeChatAuthEnabled,
"server_address": common.ServerAddress,
"price": common.Price,
"min_topup": common.MinTopUp,
"turnstile_check": common.TurnstileCheckEnabled,
"turnstile_site_key": common.TurnstileSiteKey,
"top_up_link": common.TopUpLink,
@ -40,6 +41,7 @@ func GetStatus(c *gin.Context) {
"enable_drawing": common.DrawingEnabled,
"enable_data_export": common.DataExportEnabled,
"data_export_default_time": common.DataExportDefaultTime,
"default_collapse_sidebar": common.DefaultCollapseSidebar,
"enable_online_topup": common.PayAddress != "" && common.EpayId != "" && common.EpayKey != "",
},
})

View File

@ -129,6 +129,13 @@ func ListModels(c *gin.Context) {
})
}
func ChannelListModels(c *gin.Context) {
c.JSON(200, gin.H{
"object": "list",
"data": openAIModels,
})
}
func RetrieveModel(c *gin.Context) {
modelId := c.Param("model")
if model, ok := openAIModelsMap[modelId]; ok {

View File

@ -9,6 +9,7 @@ import (
"net/url"
"one-api/common"
"one-api/model"
"one-api/service"
"strconv"
"time"
)
@ -55,14 +56,14 @@ func RequestEpay(c *gin.Context) {
c.JSON(200, gin.H{"message": err.Error(), "data": 10})
return
}
if req.Amount < 1 {
c.JSON(200, gin.H{"message": "充值金额不能小于1", "data": 10})
if req.Amount < common.MinTopUp {
c.JSON(200, gin.H{"message": fmt.Sprintf("充值数量不能小于 %d", common.MinTopUp), "data": 10})
return
}
id := c.GetInt("id")
user, _ := model.GetUserById(id, false)
amount := GetAmount(float64(req.Amount), *user)
payMoney := GetAmount(float64(req.Amount), *user)
var payType epay.PurchaseType
if req.PaymentMethod == "zfb" {
@ -72,11 +73,10 @@ func RequestEpay(c *gin.Context) {
req.PaymentMethod = "wxpay"
payType = epay.WechatPay
}
callBackAddress := service.GetCallbackAddress()
returnUrl, _ := url.Parse(common.ServerAddress + "/log")
notifyUrl, _ := url.Parse(common.ServerAddress + "/api/user/epay/notify")
notifyUrl, _ := url.Parse(callBackAddress + "/api/user/epay/notify")
tradeNo := strconv.FormatInt(time.Now().Unix(), 10)
payMoney := amount
client := GetEpayClient()
if client == nil {
c.JSON(200, gin.H{"message": "error", "data": "当前管理员未配置支付信息"})
@ -169,8 +169,8 @@ func RequestAmount(c *gin.Context) {
c.JSON(200, gin.H{"message": "error", "data": "参数错误"})
return
}
if req.Amount < 1 {
c.JSON(200, gin.H{"message": "error", "data": "充值金额不能小于1"})
if req.Amount < common.MinTopUp {
c.JSON(200, gin.H{"message": "error", "data": fmt.Sprintf("充值数量不能小于 %d", common.MinTopUp)})
return
}
id := c.GetInt("id")

View File

@ -291,24 +291,27 @@ func CacheGetRandomSatisfiedChannel(group string, model string) (*Channel, error
}
}
}
// 平滑系数
smoothingFactor := 10
// Calculate the total weight of all channels up to endIdx
totalWeight := 0
for _, channel := range channels[:endIdx] {
totalWeight += channel.GetWeight()
totalWeight += channel.GetWeight() + smoothingFactor
}
if totalWeight == 0 {
// If all weights are 0, select a channel randomly
return channels[rand.Intn(endIdx)], nil
}
//if totalWeight == 0 {
// // If all weights are 0, select a channel randomly
// return channels[rand.Intn(endIdx)], nil
//}
// Generate a random value in the range [0, totalWeight)
randomWeight := rand.Intn(totalWeight)
// Find a channel based on its weight
for _, channel := range channels[:endIdx] {
randomWeight -= channel.GetWeight()
if randomWeight <= 0 {
randomWeight -= channel.GetWeight() + smoothingFactor
if randomWeight < 0 {
return channel, nil
}
}

View File

@ -43,21 +43,39 @@ func GetAllChannels(startIdx int, num int, selectAll bool, idSort bool) ([]*Chan
return channels, err
}
func SearchChannels(keyword string, group string) (channels []*Channel, err error) {
func SearchChannels(keyword string, group string, model string) ([]*Channel, error) {
var channels []*Channel
keyCol := "`key`"
groupCol := "`group`"
modelsCol := "`models`"
// 如果是 PostgreSQL使用双引号
if common.UsingPostgreSQL {
keyCol = `"key"`
}
if group != "" {
groupCol := "`group`"
if common.UsingPostgreSQL {
groupCol = `"group"`
modelsCol = `"models"`
}
err = DB.Omit("key").Where("(id = ? or name LIKE ? or "+keyCol+" = ?) and "+groupCol+" LIKE ?", common.String2Int(keyword), keyword+"%", keyword, "%"+group+"%").Find(&channels).Error
// 构造基础查询
baseQuery := DB.Model(&Channel{}).Omit(keyCol)
// 构造WHERE子句
var whereClause string
var args []interface{}
if group != "" {
whereClause = "(id = ? OR name LIKE ? OR " + keyCol + " = ?) AND " + groupCol + " LIKE ? AND " + modelsCol + " LIKE ?"
args = append(args, common.String2Int(keyword), "%"+keyword+"%", keyword, "%"+group+"%", "%"+model+"%")
} else {
err = DB.Omit("key").Where("id = ? or name LIKE ? or "+keyCol+" = ?", common.String2Int(keyword), keyword+"%", keyword).Find(&channels).Error
whereClause = "(id = ? OR name LIKE ? OR " + keyCol + " = ?) AND " + modelsCol + " LIKE ?"
args = append(args, common.String2Int(keyword), "%"+keyword+"%", keyword, "%"+model+"%")
}
return channels, err
// 执行查询
err := baseQuery.Where(whereClause, args...).Find(&channels).Error
if err != nil {
return nil, err
}
return channels, nil
}
func GetChannelById(id int, selectAll bool) (*Channel, error) {

View File

@ -57,9 +57,11 @@ func InitOptionMap() {
common.OptionMap["Logo"] = common.Logo
common.OptionMap["ServerAddress"] = ""
common.OptionMap["PayAddress"] = ""
common.OptionMap["CustomCallbackAddress"] = ""
common.OptionMap["EpayId"] = ""
common.OptionMap["EpayKey"] = ""
common.OptionMap["Price"] = strconv.FormatFloat(common.Price, 'f', -1, 64)
common.OptionMap["MinTopUp"] = strconv.Itoa(common.MinTopUp)
common.OptionMap["TopupGroupRatio"] = common.TopupGroupRatio2JSONString()
common.OptionMap["GitHubClientId"] = ""
common.OptionMap["GitHubClientSecret"] = ""
@ -85,6 +87,7 @@ func InitOptionMap() {
common.OptionMap["RetryTimes"] = strconv.Itoa(common.RetryTimes)
common.OptionMap["DataExportInterval"] = strconv.Itoa(common.DataExportInterval)
common.OptionMap["DataExportDefaultTime"] = common.DataExportDefaultTime
common.OptionMap["DefaultCollapseSidebar"] = strconv.FormatBool(common.DefaultCollapseSidebar)
common.OptionMapRWMutex.Unlock()
loadOptionsFromDatabase()
@ -141,7 +144,7 @@ func updateOptionMap(key string, value string) (err error) {
common.ImageDownloadPermission = intValue
}
}
if strings.HasSuffix(key, "Enabled") {
if strings.HasSuffix(key, "Enabled") || key == "DefaultCollapseSidebar" {
boolValue := value == "true"
switch key {
case "PasswordRegisterEnabled":
@ -176,6 +179,8 @@ func updateOptionMap(key string, value string) (err error) {
common.DrawingEnabled = boolValue
case "DataExportEnabled":
common.DataExportEnabled = boolValue
case "DefaultCollapseSidebar":
common.DefaultCollapseSidebar = boolValue
}
}
switch key {
@ -196,12 +201,16 @@ func updateOptionMap(key string, value string) (err error) {
common.ServerAddress = value
case "PayAddress":
common.PayAddress = value
case "CustomCallbackAddress":
common.CustomCallbackAddress = value
case "EpayId":
common.EpayId = value
case "EpayKey":
common.EpayKey = value
case "Price":
common.Price, _ = strconv.ParseFloat(value, 64)
case "MinTopUp":
common.MinTopUp, _ = strconv.Atoi(value)
case "TopupGroupRatio":
err = common.UpdateTopupGroupRatioByJSONString(value)
case "GitHubClientId":

View File

@ -71,10 +71,10 @@ func (a *Adaptor) DoRequest(c *gin.Context, info *relaycommon.RelayInfo, request
func (a *Adaptor) DoResponse(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo) (usage *dto.Usage, err *dto.OpenAIErrorWithStatusCode) {
if info.IsStream {
var responseText string
err, responseText = openaiStreamHandler(c, resp, info.RelayMode)
err, responseText = OpenaiStreamHandler(c, resp, info.RelayMode)
usage = service.ResponseText2Usage(responseText, info.UpstreamModelName, info.PromptTokens)
} else {
err, usage = openaiHandler(c, resp, info.PromptTokens, info.UpstreamModelName)
err, usage = OpenaiHandler(c, resp, info.PromptTokens, info.UpstreamModelName)
}
return
}

View File

@ -16,7 +16,7 @@ import (
"time"
)
func openaiStreamHandler(c *gin.Context, resp *http.Response, relayMode int) (*dto.OpenAIErrorWithStatusCode, string) {
func OpenaiStreamHandler(c *gin.Context, resp *http.Response, relayMode int) (*dto.OpenAIErrorWithStatusCode, string) {
var responseTextBuilder strings.Builder
scanner := bufio.NewScanner(resp.Body)
scanner.Split(func(data []byte, atEOF bool) (advance int, token []byte, err error) {
@ -111,7 +111,7 @@ func openaiStreamHandler(c *gin.Context, resp *http.Response, relayMode int) (*d
return nil, responseTextBuilder.String()
}
func openaiHandler(c *gin.Context, resp *http.Response, promptTokens int, model string) (*dto.OpenAIErrorWithStatusCode, *dto.Usage) {
func OpenaiHandler(c *gin.Context, resp *http.Response, promptTokens int, model string) (*dto.OpenAIErrorWithStatusCode, *dto.Usage) {
var textResponse dto.TextResponse
responseBody, err := io.ReadAll(resp.Body)
if err != nil {

View File

@ -1,4 +1,4 @@
package zhipu_v4
package zhipu_4v
import (
"errors"
@ -8,7 +8,9 @@ import (
"net/http"
"one-api/dto"
"one-api/relay/channel"
"one-api/relay/channel/openai"
relaycommon "one-api/relay/common"
"one-api/service"
)
type Adaptor struct {
@ -41,9 +43,11 @@ func (a *Adaptor) DoRequest(c *gin.Context, info *relaycommon.RelayInfo, request
func (a *Adaptor) DoResponse(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo) (usage *dto.Usage, err *dto.OpenAIErrorWithStatusCode) {
if info.IsStream {
err, usage = zhipuStreamHandler(c, resp)
var responseText string
err, responseText = openai.OpenaiStreamHandler(c, resp, info.RelayMode)
usage = service.ResponseText2Usage(responseText, info.UpstreamModelName, info.PromptTokens)
} else {
err, usage = zhipuHandler(c, resp)
err, usage = openai.OpenaiHandler(c, resp, info.PromptTokens, info.UpstreamModelName)
}
return
}

View File

@ -1,7 +1,7 @@
package zhipu_v4
package zhipu_4v
var ModelList = []string{
"glm-4", "glm-4v", "glm-3-turbo",
}
var ChannelName = "zhipu_v4"
var ChannelName = "zhipu_4v"

View File

@ -1,4 +1,4 @@
package zhipu_v4
package zhipu_4v
import (
"one-api/dto"

View File

@ -1,4 +1,4 @@
package zhipu_v4
package zhipu_4v
import (
"bufio"

View File

@ -59,6 +59,7 @@ func getAndValidateTextRequest(c *gin.Context, relayInfo *relaycommon.RelayInfo)
}
}
relayInfo.IsStream = textRequest.Stream
relayInfo.UpstreamModelName = textRequest.Model
return textRequest, nil
}

View File

@ -11,7 +11,7 @@ import (
"one-api/relay/channel/tencent"
"one-api/relay/channel/xunfei"
"one-api/relay/channel/zhipu"
"one-api/relay/channel/zhipu_v4"
"one-api/relay/channel/zhipu_4v"
"one-api/relay/constant"
)
@ -38,7 +38,7 @@ func GetAdaptor(apiType int) channel.Adaptor {
case constant.APITypeZhipu:
return &zhipu.Adaptor{}
case constant.APITypeZhipu_v4:
return &zhipu_v4.Adaptor{}
return &zhipu_4v.Adaptor{}
}
return nil
}

View File

@ -75,7 +75,7 @@ func SetApiRouter(router *gin.Engine) {
{
channelRoute.GET("/", controller.GetAllChannels)
channelRoute.GET("/search", controller.SearchChannels)
channelRoute.GET("/models", controller.ListModels)
channelRoute.GET("/models", controller.ChannelListModels)
channelRoute.GET("/:id", controller.GetChannel)
channelRoute.GET("/test", controller.TestAllChannels)
channelRoute.GET("/test/:id", controller.TestChannel)

10
service/epay.go Normal file
View File

@ -0,0 +1,10 @@
package service
import "one-api/common"
func GetCallbackAddress() string {
if common.CustomCallbackAddress == "" {
return common.ServerAddress
}
return common.CustomCallbackAddress
}

View File

@ -29,7 +29,7 @@ const Home = lazy(() => import('./pages/Home'));
const About = lazy(() => import('./pages/About'));
function App() {
const [userState, userDispatch] = useContext(UserContext);
const [statusState, statusDispatch] = useContext(StatusContext);
// const [statusState, statusDispatch] = useContext(StatusContext);
const loadUser = () => {
let user = localStorage.getItem('user');
@ -38,47 +38,9 @@ function App() {
userDispatch({ type: 'login', payload: data });
}
};
const loadStatus = async () => {
const res = await API.get('/api/status');
const { success, data } = res.data;
if (success) {
localStorage.setItem('status', JSON.stringify(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('quota_per_unit', data.quota_per_unit);
localStorage.setItem('display_in_currency', data.display_in_currency);
localStorage.setItem('enable_drawing', data.enable_drawing);
localStorage.setItem('enable_data_export', data.enable_data_export);
localStorage.setItem('data_export_default_time', data.data_export_default_time);
if (data.chat_link) {
localStorage.setItem('chat_link', data.chat_link);
} else {
localStorage.removeItem('chat_link');
}
if (data.chat_link2) {
localStorage.setItem('chat_link2', data.chat_link2);
} else {
localStorage.removeItem('chat_link2');
}
// if (
// data.version !== process.env.REACT_APP_VERSION &&
// data.version !== 'v0.0.0' &&
// process.env.REACT_APP_VERSION !== ''
// ) {
// showNotice(
// `新版本可用:${data.version},请使用快捷键 Shift + F5 刷新页面`
// );
// }
} else {
showError('无法正常连接至服务器!');
}
};
useEffect(() => {
loadUser();
loadStatus().then();
let systemName = getSystemName();
if (systemName) {
document.title = systemName;

View File

@ -257,6 +257,7 @@ const ChannelsTable = () => {
const [idSort, setIdSort] = useState(false);
const [searchKeyword, setSearchKeyword] = useState('');
const [searchGroup, setSearchGroup] = useState('');
const [searchModel, setSearchModel] = useState('');
const [searching, setSearching] = useState(false);
const [updatingBalance, setUpdatingBalance] = useState(false);
const [pageSize, setPageSize] = useState(ITEMS_PER_PAGE);
@ -440,15 +441,15 @@ const ChannelsTable = () => {
}
};
const searchChannels = async (searchKeyword, searchGroup) => {
if (searchKeyword === '' && searchGroup === '') {
const searchChannels = async (searchKeyword, searchGroup, searchModel) => {
if (searchKeyword === '' && searchGroup === '' && searchModel === '') {
// if keyword is blank, load files instead.
await loadChannels(0, pageSize, idSort);
setActivePage(1);
return;
}
setSearching(true);
const res = await API.get(`/api/channel/search?keyword=${searchKeyword}&group=${searchGroup}`);
const res = await API.get(`/api/channel/search?keyword=${searchKeyword}&group=${searchGroup}&model=${searchModel}`);
const {success, message, data} = res.data;
if (success) {
setChannels(data);
@ -625,13 +626,12 @@ const ChannelsTable = () => {
return (
<>
<EditChannel refresh={refresh} visible={showEdit} handleClose={closeEdit} editingChannel={editingChannel}/>
<Form onSubmit={() => {searchChannels(searchKeyword, searchGroup)}} labelPosition='left'>
<Form onSubmit={() => {searchChannels(searchKeyword, searchGroup, searchModel)}} labelPosition='left'>
<div style={{display: 'flex'}}>
<Space>
<Form.Input
field='search'
label='关键词'
field='search_keyword'
label='搜索渠道关键词'
placeholder='ID名称和密钥 ...'
value={searchKeyword}
loading={searching}
@ -639,10 +639,22 @@ const ChannelsTable = () => {
setSearchKeyword(v.trim())
}}
/>
<Form.Input
field='search_model'
label='模型'
placeholder='模型关键字'
value={searchModel}
loading={searching}
onChange={(v)=>{
setSearchModel(v.trim())
}}
/>
<Form.Select field="group" label='分组' optionList={groupOptions} onChange={(v) => {
setSearchGroup(v)
searchChannels(searchKeyword, v)
searchChannels(searchKeyword, v, searchModel)
}}/>
<Button label='查询' type="primary" htmlType="submit" className="btn-margin-right"
style={{marginRight: 8}}>查询</Button>
</Space>
</div>
</Form>

View File

@ -27,6 +27,7 @@ const OperationSetting = () => {
DataExportEnabled: '',
DataExportDefaultTime: 'hour',
DataExportInterval: 5,
DefaultCollapseSidebar: '', // 默认折叠侧边栏
RetryTimes: 0
});
const [originInputs, setOriginInputs] = useState({});
@ -65,6 +66,10 @@ const OperationSetting = () => {
if (key.endsWith('Enabled')) {
value = inputs[key] === 'true' ? 'false' : 'true';
}
if (key === 'DefaultCollapseSidebar') {
value = inputs[key] === 'true' ? 'false' : 'true';
}
console.log(key, value)
const res = await API.put('/api/option/', {
key,
value
@ -79,7 +84,7 @@ const OperationSetting = () => {
};
const handleInputChange = async (e, {name, value}) => {
if (name.endsWith('Enabled') || name === 'DataExportInterval' || name === 'DataExportDefaultTime') {
if (name.endsWith('Enabled') || name === 'DataExportInterval' || name === 'DataExportDefaultTime' || name === 'DefaultCollapseSidebar') {
if (name === 'DataExportDefaultTime') {
localStorage.setItem('data_export_default_time', value);
}
@ -243,6 +248,12 @@ const OperationSetting = () => {
name='DrawingEnabled'
onChange={handleInputChange}
/>
<Form.Checkbox
checked={inputs.DefaultCollapseSidebar === 'true'}
label='默认折叠侧边栏'
name='DefaultCollapseSidebar'
onChange={handleInputChange}
/>
</Form.Group>
<Form.Button onClick={() => {
submitConfig('general').then();

View File

@ -1,8 +1,8 @@
import React, {useContext, useMemo, useState} from 'react';
import React, { useContext, useEffect, useLayoutEffect, useMemo, useState } from 'react';
import {Link, useNavigate} from 'react-router-dom';
import {UserContext} from '../context/User';
import {API, getLogo, getSystemName, isAdmin, isMobile, showSuccess} from '../helpers';
import { API, getLogo, getSystemName, isAdmin, isMobile, showError, showSuccess } from '../helpers';
import '../index.css';
import {
@ -24,11 +24,14 @@ import {Nav, Avatar, Dropdown, Layout} from '@douyinfe/semi-ui';
const SiderBar = () => {
const [userState, userDispatch] = useContext(UserContext);
const defaultIsCollapsed = isMobile() || localStorage.getItem('default_collapse_sidebar') === 'true';
let navigate = useNavigate();
const [selectedKeys, setSelectedKeys] = useState(['home']);
const [showSidebar, setShowSidebar] = useState(false);
const systemName = getSystemName();
const logo = getLogo();
const [isCollapsed, setIsCollapsed] = useState(defaultIsCollapsed);
const headerButtons = useMemo(() => [
{
text: '首页',
@ -110,15 +113,41 @@ const SiderBar = () => {
// }
], [localStorage.getItem('enable_data_export'), localStorage.getItem('enable_drawing'), localStorage.getItem('chat_link'), isAdmin()]);
async function logout() {
setShowSidebar(false);
await API.get('/api/user/logout');
showSuccess('注销成功!');
userDispatch({type: 'logout'});
localStorage.removeItem('user');
navigate('/login');
const loadStatus = async () => {
const res = await API.get('/api/status');
const { success, data } = res.data;
if (success) {
localStorage.setItem('status', JSON.stringify(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('quota_per_unit', data.quota_per_unit);
localStorage.setItem('display_in_currency', data.display_in_currency);
localStorage.setItem('enable_drawing', data.enable_drawing);
localStorage.setItem('enable_data_export', data.enable_data_export);
localStorage.setItem('data_export_default_time', data.data_export_default_time);
localStorage.setItem('default_collapse_sidebar', data.default_collapse_sidebar);
if (data.chat_link) {
localStorage.setItem('chat_link', data.chat_link);
} else {
localStorage.removeItem('chat_link');
}
if (data.chat_link2) {
localStorage.setItem('chat_link2', data.chat_link2);
} else {
localStorage.removeItem('chat_link2');
}
} else {
showError('无法正常连接至服务器!');
}
};
useEffect(() => {
loadStatus().then(() => {
setIsCollapsed(isMobile() || localStorage.getItem('default_collapse_sidebar') === 'true');
});
},[])
return (
<>
@ -127,7 +156,12 @@ const SiderBar = () => {
<Nav
// mode={'horizontal'}
// bodyStyle={{ height: 100 }}
defaultIsCollapsed={isMobile()}
defaultIsCollapsed={isMobile() || localStorage.getItem('default_collapse_sidebar') === 'true'}
isCollapsed={isCollapsed}
onCollapseChange={collapsed => {
console.log(collapsed);
setIsCollapsed(collapsed);
}}
selectedKeys={selectedKeys}
renderWrapper={({itemElement, isSubNav, isInSubNav, props}) => {
const routerMap = {

View File

@ -20,8 +20,10 @@ const SystemSetting = () => {
EpayId: '',
EpayKey: '',
Price: 7.3,
MinTopUp: 1,
TopupGroupRatio: '',
PayAddress: '',
CustomCallbackAddress: '',
Footer: '',
WeChatAuthEnabled: '',
WeChatServerAddress: '',
@ -293,7 +295,7 @@ const SystemSetting = () => {
更新服务器地址
</Form.Button>
<Divider/>
<Header as='h3'>支付设置当前仅支持易支付接口使用上方服务器地址作为回调地址</Header>
<Header as='h3'>支付设置当前仅支持易支付接口默认使用上方服务器地址作为回调地址</Header>
<Form.Group widths='equal'>
<Form.Input
label='支付地址,不填写则不启用在线支付'
@ -316,15 +318,32 @@ const SystemSetting = () => {
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

View File

@ -153,7 +153,7 @@ const TokensTable = () => {
[
{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: 'ama', name: 'AMA 问天BotGrem', onClick: () => {onOpenLink('ama', 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)}},
]
}

View File

@ -132,6 +132,8 @@ export const modelColorMap = {
'gpt-4-0314': 'rgb(70,130,180)', // 钢蓝色
'gpt-4-0613': 'rgb(100,149,237)', // 矢车菊蓝
'gpt-4-1106-preview': 'rgb(30,144,255)', // 道奇蓝
'gpt-4-0125-preview': 'rgb(2,177,236)', // 深天蓝
'gpt-4-turbo-preview': 'rgb(2,177,255)', // 深天蓝
'gpt-4-32k': 'rgb(104,111,238)', // 中紫色
'gpt-4-32k-0314': 'rgb(90,105,205)', // 暗灰蓝色
'gpt-4-32k-0613': 'rgb(61,71,139)', // 暗蓝灰色

View File

@ -10,6 +10,7 @@ const TopUp = () => {
const [topUpCount, setTopUpCount] = useState(10);
const [minTopupCount, setMinTopUpCount] = useState(1);
const [amount, setAmount] = useState(0.0);
const [minTopUp, setMinTopUp] = useState(1);
const [topUpLink, setTopUpLink] = useState('');
const [enableOnlineTopUp, setEnableOnlineTopUp] = useState(false);
const [userQuota, setUserQuota] = useState(0);
@ -61,6 +62,10 @@ const TopUp = () => {
if (amount === 0) {
await getAmount();
}
if (topUpCount < minTopUp) {
showInfo('充值数量不能小于' + minTopUp);
return;
}
setPayWay(payment)
setOpen(true);
}
@ -69,6 +74,10 @@ const TopUp = () => {
if (amount === 0) {
await getAmount();
}
if (topUpCount < minTopUp) {
showInfo('充值数量不能小于' + minTopUp);
return;
}
setOpen(false);
try {
const res = await API.post('/api/user/pay', {
@ -132,6 +141,9 @@ const TopUp = () => {
if (status.top_up_link) {
setTopUpLink(status.top_up_link);
}
if (status.min_topup) {
setMinTopUp(status.min_topup);
}
if (status.enable_online_topup) {
setEnableOnlineTopUp(status.enable_online_topup);
}
@ -239,12 +251,13 @@ const TopUp = () => {
disabled={!enableOnlineTopUp}
field={'redemptionCount'}
label={'实付金额:' + renderAmount()}
placeholder='充值数量'
placeholder={'充值数量,最低' + minTopUp + '$'}
name='redemptionCount'
type={'number'}
value={topUpCount}
suffix={'$'}
min={1}
min={minTopUp}
defaultValue={minTopUp}
max={100000}
onChange={async (value) => {
if (value < 1) {