Compare commits

...

25 Commits

Author SHA1 Message Date
CaIon
3b63d9bba6 优化渠道余额显示 2023-12-11 20:42:51 +08:00
CaIon
b99b24f271 令牌界面添加聊天按钮 2023-12-11 20:11:26 +08:00
CaIon
ac11f4bc0e 优化前端代码 2023-12-11 19:52:41 +08:00
CaIon
44465a398b support gpts 2023-12-11 19:50:43 +08:00
CaIon
4f0419c7bc update ISSUE_TEMPLATE 2023-12-11 00:02:41 +08:00
CaIon
07b4a8f5d2 修复日志充值显示问题 2023-12-10 18:49:17 +08:00
CaIon
348e5a0df3 渠道余额改为保留两位小数 2023-12-10 18:34:44 +08:00
CaIon
79d7758617 充值改为保留两位小数 2023-12-09 22:56:24 +08:00
CaIon
5240971b4a update docker-compose.yml 2023-12-09 17:37:43 +08:00
CaIon
00af47566f 修复渠道管理无法点击分页的bug 2023-12-08 19:39:33 +08:00
CaIon
9cd3cd3caa 适配渠道管理移动端 2023-12-08 19:28:04 +08:00
CaIon
b301be7fc0 修复渠道余额显示问题 2023-12-08 18:36:06 +08:00
CaIon
aa07ada0f4 fix pgsql 2023-12-08 13:15:59 +08:00
CaIon
1a7a10c3d4 fix gpt-4-1106-preview count image token 2023-12-08 12:54:29 +08:00
CaIon
821645e5da update README.md 2023-12-07 19:20:04 +08:00
Calcium-Ion
e095900d88 Merge pull request #20 from Calcium-Ion/optimize/hign--cpu
fix: 修复客户端中断请求,计算补全阻塞问题
2023-12-07 17:11:57 +08:00
CaIon
143e279214 Merge remote-tracking branch 'public/main' into latest 2023-12-07 16:58:37 +08:00
CaIon
3ee4136256 消费日志添加用户调用前余额 2023-12-07 16:58:24 +08:00
Calcium-Ion
2ced102a57 Merge pull request #22 from Happy-clo/pr
docs: update
2023-12-06 12:50:45 +08:00
Happy
2c187fde6e docs: update 2023-12-06 00:27:08 +08:00
CaIon
0d469804e3 修复兑换码复制bug 2023-12-06 00:19:00 +08:00
CaIon
655722641b 修复渠道管理没有测试按钮的bug 2023-12-05 21:26:27 +08:00
Xyfacai
f712b73c18 fix: 如果还有数据,等待一会 2023-11-30 20:30:29 +08:00
Xyfacai
ed22a202f7 fix: 如果还有数据,等待一会 2023-11-30 20:28:57 +08:00
Xyfacai
6680f6d83a fix: 修复客户端中断请求,计算补全阻塞问题 2023-11-28 22:02:09 +08:00
23 changed files with 210 additions and 102 deletions

View File

@@ -1,8 +1,5 @@
blank_issues_enabled: false
contact_links:
- name: 项目群聊
url: https://openai.justsong.cn/
about: QQ 群:828520184自动审核备注 One API
- name: 赞赏支持
url: https://iamazing.cn/page/reward
about: 请作者喝杯咖啡,以激励作者持续开发
url: https://private-user-images.githubusercontent.com/61247483/283011625-de536a8a-0161-47a7-a0a2-66ef6de81266.jpeg?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTEiLCJleHAiOjE3MDIyMjQzOTAsIm5iZiI6MTcwMjIyNDA5MCwicGF0aCI6Ii82MTI0NzQ4My8yODMwMTE2MjUtZGU1MzZhOGEtMDE2MS00N2E3LWEwYTItNjZlZjZkZTgxMjY2LmpwZWc_WC1BbXotQWxnb3JpdGhtPUFXUzQtSE1BQy1TSEEyNTYmWC1BbXotQ3JlZGVudGlhbD1BS0lBSVdOSllBWDRDU1ZFSDUzQSUyRjIwMjMxMjEwJTJGdXMtZWFzdC0xJTJGczMlMkZhd3M0X3JlcXVlc3QmWC1BbXotRGF0ZT0yMDIzMTIxMFQxNjAxMzBaJlgtQW16LUV4cGlyZXM9MzAwJlgtQW16LVNpZ25hdHVyZT02MGIxYmM3ZDQyYzBkOTA2ZTYyYmVmMzQ1NjY4NjM1YjY0NTUzNTM5NjE1NDZkYTIzODdhYTk4ZjZjODJmYzY2JlgtQW16LVNpZ25lZEhlYWRlcnM9aG9zdCZhY3Rvcl9pZD0wJmtleV9pZD0wJnJlcG9faWQ9MCJ9.TJ8CTfOSwR0-CHS1KLfomqgL0e4YH1luy8lSLrkv5Zg
about: QQ 群:629454374

View File

@@ -1,17 +1,17 @@
# Neko API
> **Note**
> [!NOTE]
> 本项目为开源项目,在[One API](https://github.com/songquanpeng/one-api)的基础上进行二次开发,感谢原作者的无私奉献。
> 使用者必须在遵循 OpenAI 的[使用条款](https://openai.com/policies/terms-of-use)以及**法律法规**的情况下使用,不得用于非法用途。
> **Warning**
> [!WARNING]
> 本项目为个人学习使用,不保证稳定性,且不提供任何技术支持,使用者必须在遵循 OpenAI 的使用条款以及法律法规的情况下使用,不得用于非法用途。
> 根据[《生成式人工智能服务管理暂行办法》](http://www.cac.gov.cn/2023-07/13/c_1690898327029107.htm)的要求,请勿对中国地区公众提供一切未经备案的生成式人工智能服务。
> **Note**
> 最新版Docker镜像 calciumion/neko-api:latest
> [!NOTE]
> 最新版Docker镜像 calciumion/new-api:latest
> 更新指令 docker run --rm -v /var/run/docker.sock:/var/run/docker.sock containrrr/watchtower -cR
## 此分叉版本的主要变更

View File

@@ -39,7 +39,12 @@ func DecodeUrlImageData(imageUrl string) (image.Config, error) {
}
// 限制读取的字节数,防止下载整个图片
limitReader := io.LimitReader(response.Body, 8192)
limitReader := io.LimitReader(response.Body, 1024*20)
//data, err := io.ReadAll(limitReader)
//if err != nil {
// log.Fatal(err)
//}
//log.Printf("%x", data)
config, err := getImageConfig(limitReader)
response.Body.Close()
return config, err

View File

@@ -162,7 +162,7 @@ func relayAudioHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode
if quota != 0 {
tokenName := c.GetString("token_name")
logContent := fmt.Sprintf("模型倍率 %.2f,分组倍率 %.2f", modelRatio, groupRatio)
model.RecordConsumeLog(ctx, userId, channelId, promptTokens, 0, audioRequest.Model, tokenName, quota, logContent, tokenId)
model.RecordConsumeLog(ctx, userId, channelId, promptTokens, 0, audioRequest.Model, tokenName, quota, logContent, tokenId, userQuota)
model.UpdateUserUsedQuotaAndRequestCount(userId, quota)
channelId := c.GetInt("channel_id")
model.UpdateChannelUsedQuota(channelId, quota)

View File

@@ -169,7 +169,7 @@ func relayImageHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode
if quota != 0 {
tokenName := c.GetString("token_name")
logContent := fmt.Sprintf("模型倍率 %.2f,分组倍率 %.2f", modelRatio, groupRatio)
model.RecordConsumeLog(ctx, userId, channelId, 0, 0, imageRequest.Model, tokenName, quota, logContent, tokenId)
model.RecordConsumeLog(ctx, userId, channelId, 0, 0, imageRequest.Model, tokenName, quota, logContent, tokenId, userQuota)
model.UpdateUserUsedQuotaAndRequestCount(userId, quota)
channelId := c.GetInt("channel_id")
model.UpdateChannelUsedQuota(channelId, quota)

View File

@@ -370,7 +370,7 @@ func relayMidjourneySubmit(c *gin.Context, relayMode int) *MidjourneyResponse {
if quota != 0 {
tokenName := c.GetString("token_name")
logContent := fmt.Sprintf("模型倍率 %.2f,分组倍率 %.2f", modelRatio, groupRatio)
model.RecordConsumeLog(ctx, userId, channelId, 0, 0, imageModel, tokenName, quota, logContent, tokenId)
model.RecordConsumeLog(ctx, userId, channelId, 0, 0, imageModel, tokenName, quota, logContent, tokenId, userQuota)
model.UpdateUserUsedQuotaAndRequestCount(userId, quota)
channelId := c.GetInt("channel_id")
model.UpdateChannelUsedQuota(channelId, quota)

View File

@@ -9,10 +9,12 @@ import (
"net/http"
"one-api/common"
"strings"
"sync"
"time"
)
func openaiStreamHandler(c *gin.Context, resp *http.Response, relayMode int) (*OpenAIErrorWithStatusCode, string) {
responseText := ""
var responseTextBuilder strings.Builder
scanner := bufio.NewScanner(resp.Body)
scanner.Split(func(data []byte, atEOF bool) (advance int, token []byte, err error) {
if atEOF && len(data) == 0 {
@@ -26,9 +28,16 @@ func openaiStreamHandler(c *gin.Context, resp *http.Response, relayMode int) (*O
}
return 0, nil, nil
})
dataChan := make(chan string)
stopChan := make(chan bool)
dataChan := make(chan string, 5)
stopChan := make(chan bool, 2)
defer close(stopChan)
defer close(dataChan)
var wg sync.WaitGroup
go func() {
wg.Add(1)
defer wg.Done()
var streamItems []string
for scanner.Scan() {
data := scanner.Text()
if len(data) < 6 { // ignore blank line or wrong format
@@ -40,29 +49,39 @@ func openaiStreamHandler(c *gin.Context, resp *http.Response, relayMode int) (*O
dataChan <- data
data = data[6:]
if !strings.HasPrefix(data, "[DONE]") {
switch relayMode {
case RelayModeChatCompletions:
var streamResponse ChatCompletionsStreamResponseSimple
err := json.Unmarshal(common.StringToByteSlice(data), &streamResponse)
if err != nil {
common.SysError("error unmarshalling stream response: " + err.Error())
continue // just ignore the error
}
for _, choice := range streamResponse.Choices {
responseText += choice.Delta.Content
}
case RelayModeCompletions:
var streamResponse CompletionsStreamResponse
err := json.Unmarshal(common.StringToByteSlice(data), &streamResponse)
if err != nil {
common.SysError("error unmarshalling stream response: " + err.Error())
continue
}
for _, choice := range streamResponse.Choices {
responseText += choice.Text
}
streamItems = append(streamItems, data)
}
}
streamResp := "[" + strings.Join(streamItems, ",") + "]"
switch relayMode {
case RelayModeChatCompletions:
var streamResponses []ChatCompletionsStreamResponseSimple
err := json.Unmarshal(common.StringToByteSlice(streamResp), &streamResponses)
if err != nil {
common.SysError("error unmarshalling stream response: " + err.Error())
return // just ignore the error
}
for _, streamResponse := range streamResponses {
for _, choice := range streamResponse.Choices {
responseTextBuilder.WriteString(choice.Delta.Content)
}
}
case RelayModeCompletions:
var streamResponses []CompletionsStreamResponse
err := json.Unmarshal(common.StringToByteSlice(streamResp), &streamResponses)
if err != nil {
common.SysError("error unmarshalling stream response: " + err.Error())
return // just ignore the error
}
for _, streamResponse := range streamResponses {
for _, choice := range streamResponse.Choices {
responseTextBuilder.WriteString(choice.Text)
}
}
}
if len(dataChan) > 0 {
// wait data out
time.Sleep(2 * time.Second)
}
stopChan <- true
}()
@@ -85,7 +104,8 @@ func openaiStreamHandler(c *gin.Context, resp *http.Response, relayMode int) (*O
if err != nil {
return errorWrapper(err, "close_response_body_failed", http.StatusInternalServerError), ""
}
return nil, responseText
wg.Wait()
return nil, responseTextBuilder.String()
}
func openaiHandler(c *gin.Context, resp *http.Response, promptTokens int, model string) (*OpenAIErrorWithStatusCode, *Usage) {

View File

@@ -446,7 +446,7 @@ func relayTextHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode {
// record all the consume log even if quota is 0
useTimeSeconds := time.Now().Unix() - startTime.Unix()
logContent := fmt.Sprintf("模型倍率 %.2f,分组倍率 %.2f,用时 %d秒", modelRatio, groupRatio, useTimeSeconds)
model.RecordConsumeLog(ctx, userId, channelId, promptTokens, completionTokens, textRequest.Model, tokenName, quota, logContent, tokenId)
model.RecordConsumeLog(ctx, userId, channelId, promptTokens, completionTokens, textRequest.Model, tokenName, quota, logContent, tokenId, userQuota)
model.UpdateUserUsedQuotaAndRequestCount(userId, quota)
model.UpdateChannelUsedQuota(channelId, quota)
//if quota != 0 {

View File

@@ -98,7 +98,7 @@ func RequestEpay(c *gin.Context) {
topUp := &model.TopUp{
UserId: id,
Amount: req.Amount,
Money: int(amount),
Money: payMoney,
TradeNo: "A" + tradeNo,
CreateTime: time.Now().Unix(),
Status: "pending",
@@ -155,7 +155,7 @@ func EpayNotify(c *gin.Context) {
return
}
log.Printf("易支付回调更新用户成功 %v", topUp)
model.RecordLog(topUp.UserId, model.LogTypeTopup, fmt.Sprintf("使用在线充值成功,充值金额: %v支付金额%d", common.LogQuota(topUp.Amount*500000), topUp.Money))
model.RecordLog(topUp.UserId, model.LogTypeTopup, fmt.Sprintf("使用在线充值成功,充值金额: %v支付金额%f", common.LogQuota(topUp.Amount*500000), topUp.Money))
}
} else {
log.Printf("易支付异常回调: %v", verifyInfo)
@@ -175,5 +175,6 @@ func RequestAmount(c *gin.Context) {
}
id := c.GetInt("id")
user, _ := model.GetUserById(id, false)
c.JSON(200, gin.H{"message": "success", "data": GetAmount(float64(req.Amount), *user)})
payMoney := GetAmount(float64(req.Amount), *user)
c.JSON(200, gin.H{"message": "success", "data": strconv.FormatFloat(payMoney, 'f', 2, 64)})
}

View File

@@ -2,7 +2,7 @@ version: '3.4'
services:
one-api:
image: calciumion/neko-api:main
image: calciumion/new-api:latest
container_name: one-api
restart: always
command: --log-dir /app/logs

View File

@@ -14,10 +14,14 @@ type Ability struct {
}
func GetGroupModels(group string) []string {
var models []string
// Find distinct models
DB.Table("abilities").Where("`group` = ? and enabled = ?", group, true).Distinct("model").Pluck("model", &models)
return models
var models []string
// Find distinct models
groupCol := "`group`"
if common.UsingPostgreSQL {
groupCol = `"group"`
}
DB.Table("abilities").Where(groupCol+" = ? and enabled = ?", group, true).Distinct("model").Pluck("model", &models)
return models
}
func GetRandomSatisfiedChannel(group string, model string) (*Channel, error) {

View File

@@ -190,6 +190,10 @@ func SyncChannelCache(frequency int) {
}
func CacheGetRandomSatisfiedChannel(group string, model string) (*Channel, error) {
if strings.HasPrefix(model, "gpt-4-gizmo") {
model = "gpt-4-gizmo-*"
}
if !common.MemoryCacheEnabled {
return GetRandomSatisfiedChannel(group, model)
}

View File

@@ -54,8 +54,8 @@ func RecordLog(userId int, logType int, content string) {
}
}
func RecordConsumeLog(ctx context.Context, userId int, channelId int, promptTokens int, completionTokens int, modelName string, tokenName string, quota int, content string, tokenId int) {
common.LogInfo(ctx, fmt.Sprintf("record consume log: userId=%d, channelId=%d, promptTokens=%d, completionTokens=%d, modelName=%s, tokenName=%s, quota=%d, content=%s", userId, channelId, promptTokens, completionTokens, modelName, tokenName, quota, content))
func RecordConsumeLog(ctx context.Context, userId int, channelId int, promptTokens int, completionTokens int, modelName string, tokenName string, quota int, content string, tokenId int, userQuota int) {
common.LogInfo(ctx, fmt.Sprintf("record consume log: userId=%d, 用户调用前余额=%d, channelId=%d, promptTokens=%d, completionTokens=%d, modelName=%s, tokenName=%s, quota=%d, content=%s", userId, userQuota, channelId, promptTokens, completionTokens, modelName, tokenName, quota, content))
if !common.LogConsumeEnabled {
return
}

View File

@@ -219,6 +219,17 @@ func PostConsumeTokenQuota(tokenId int, userQuota int, quota int, preConsumedQuo
return err
}
if !token.UnlimitedQuota {
if quota > 0 {
err = DecreaseTokenQuota(tokenId, quota)
} else {
err = IncreaseTokenQuota(tokenId, -quota)
}
if err != nil {
return err
}
}
if sendEmail {
if (quota + preConsumedQuota) != 0 {
quotaTooLow := userQuota >= common.QuotaRemindThreshold && userQuota-(quota+preConsumedQuota) < common.QuotaRemindThreshold
@@ -247,15 +258,5 @@ func PostConsumeTokenQuota(tokenId int, userQuota int, quota int, preConsumedQuo
}
}
if !token.UnlimitedQuota {
if quota > 0 {
err = DecreaseTokenQuota(tokenId, quota)
} else {
err = IncreaseTokenQuota(tokenId, -quota)
}
if err != nil {
return err
}
}
return nil
}

View File

@@ -1,13 +1,13 @@
package model
type TopUp struct {
Id int `json:"id"`
UserId int `json:"user_id" gorm:"index"`
Amount int `json:"amount"`
Money int `json:"money"`
TradeNo string `json:"trade_no"`
CreateTime int64 `json:"create_time"`
Status string `json:"status"`
Id int `json:"id"`
UserId int `json:"user_id" gorm:"index"`
Amount int `json:"amount"`
Money float64 `json:"money"`
TradeNo string `json:"trade_no"`
CreateTime int64 `json:"create_time"`
Status string `json:"status"`
}
func (topUp *TopUp) Insert() error {

View File

@@ -1,10 +1,19 @@
import React, {useEffect, useState} from 'react';
import {Input, Label, Message, Popup} from 'semantic-ui-react';
import {Link} from 'react-router-dom';
import {API, setPromptShown, shouldShowPrompt, showError, showInfo, showSuccess, timestamp2string} from '../helpers';
import {
API,
isMobile,
setPromptShown,
shouldShowPrompt,
showError,
showInfo,
showSuccess,
timestamp2string
} from '../helpers';
import {CHANNEL_OPTIONS, ITEMS_PER_PAGE} from '../constants';
import {renderGroup, renderNumber, renderQuota, renderQuotaWithPrompt} from '../helpers/render';
import {renderGroup, renderNumber, renderNumberWithPoint, renderQuota, renderQuotaWithPrompt} from '../helpers/render';
import {
Avatar,
Tag,
@@ -133,8 +142,8 @@ const ChannelsTable = () => {
<Tooltip content={'已用额度'}>
<Tag color='white' type='ghost' size='large'>{renderQuota(record.used_quota)}</Tag>
</Tooltip>
<Tooltip content={'剩余额度,点击更新'}>
<Tag color='white' type='ghost' size='large' onClick={() => {updateChannelBalance(record)}}>{renderQuota(record.balance)}</Tag>
<Tooltip content={'剩余额度' + record.balance + ',点击更新'}>
<Tag color='white' type='ghost' size='large' onClick={() => {updateChannelBalance(record)}}>${renderNumberWithPoint(record.balance)}</Tag>
</Tooltip>
</Space>
</div>
@@ -165,6 +174,7 @@ const ChannelsTable = () => {
dataIndex: 'operate',
render: (text, record, index) => (
<div>
<Button theme='light' type='primary' style={{marginRight: 1}} onClick={()=>testChannel(record)}>测试</Button>
<Popconfirm
title="确定是否要删除此渠道?"
content="此修改将不可逆"
@@ -415,16 +425,15 @@ const ChannelsTable = () => {
setSearching(false);
};
const testChannel = async (id, name, idx) => {
const res = await API.get(`/api/channel/test/${id}/`);
const testChannel = async (record) => {
const res = await API.get(`/api/channel/test/${record.id}/`);
const {success, message, time} = res.data;
if (success) {
let newChannels = [...channels];
let realIdx = (activePage - 1) * pageSize + idx;
newChannels[realIdx].response_time = time * 1000;
newChannels[realIdx].test_time = Date.now() / 1000;
record.response_time = time * 1000;
record.test_time = Date.now() / 1000;
setChannels(newChannels);
showInfo(`通道 ${name} 测试成功,耗时 ${time.toFixed(2)} 秒。`);
showInfo(`通道 ${record.name} 测试成功,耗时 ${time.toFixed(2)} 秒。`);
} else {
showError(message);
}
@@ -593,13 +602,14 @@ const ChannelsTable = () => {
total: channelCount,
pageSizeOpts: [10, 20, 50, 100],
showSizeChanger: true,
formatPageText:(page) => '',
onPageSizeChange: (size) => {
handlePageSizeChange(size).then()
},
onPageChange: handlePageChange,
}} loading={loading} onRow={handleRow}/>
<div style={{display: 'flex'}}>
<Space>
<div style={{display: isMobile()?'':'flex', marginTop: isMobile()?0:-45, zIndex: 999, position: 'relative', pointerEvents: 'none'}}>
<Space style={{pointerEvents: 'auto'}}>
<Button theme='light' type='primary' style={{marginRight: 8}} onClick={
() => {
setEditingChannel({
@@ -612,6 +622,7 @@ const ChannelsTable = () => {
title="确定?"
okType={'warning'}
onConfirm={testAllChannels}
position={isMobile()?'top':''}
>
<Button theme='light' type='warning' style={{marginRight: 8}}>测试所有已启用通道</Button>
</Popconfirm>
@@ -633,6 +644,9 @@ const ChannelsTable = () => {
<Button theme='light' type='primary' style={{marginRight: 8}} onClick={refresh}>刷新</Button>
</Space>
{/*<div style={{width: '100%', pointerEvents: 'none', position: 'absolute'}}>*/}
{/*</div>*/}
</div>
</>
);

View File

@@ -1,5 +1,4 @@
import React, {useContext, useEffect, useState} from 'react';
import {Form, Image, Message} from 'semantic-ui-react';
import {Link, useNavigate} from 'react-router-dom';
import {API, copy, isRoot, showError, showInfo, showNotice, showSuccess} from '../helpers';
import Turnstile from 'react-turnstile';
@@ -10,7 +9,7 @@ import {
Button,
Card,
Descriptions,
Divider,
Divider, Image,
Input, InputNumber,
Layout,
Modal,
@@ -267,7 +266,7 @@ const PersonalSetting = () => {
}
return (
<div style={{lineHeight: '40px'}}>
<div>
<Layout>
<Layout.Content>
<Modal
@@ -426,8 +425,7 @@ const PersonalSetting = () => {
</Space>
{systemToken && (
<Form.Input
fluid
<Input
readOnly
value={systemToken}
onClick={handleSystemTokenClick}
@@ -451,24 +449,21 @@ const PersonalSetting = () => {
visible={showWeChatBindModal}
size={'mini'}
>
<Image src={status.wechat_qrcode} fluid/>
<Image src={status.wechat_qrcode}/>
<div style={{textAlign: 'center'}}>
<p>
微信扫码关注公众号输入验证码获取验证码三分钟内有效
</p>
</div>
<Form size='large'>
<Form.Input
fluid
placeholder='验证码'
name='wechat_verification_code'
value={inputs.wechat_verification_code}
onChange={handleInputChange}
/>
<Button color='' fluid size='large' onClick={bindWeChat}>
绑定
</Button>
</Form>
<Input
placeholder='验证码'
name='wechat_verification_code'
value={inputs.wechat_verification_code}
onChange={(v)=>handleInputChange('wechat_verification_code', v)}
/>
<Button color='' fluid size='large' onClick={bindWeChat}>
绑定
</Button>
</Modal>
</div>
</Card>

View File

@@ -173,9 +173,9 @@ const RedemptionsTable = () => {
// }
const setRedemptionFormat = (redeptions) => {
for (let i = 0; i < redeptions.length; i++) {
redeptions[i].key = '' + redeptions[i].id;
}
// for (let i = 0; i < redeptions.length; i++) {
// redeptions[i].key = '' + redeptions[i].id;
// }
// data.key = '' + data.id
setRedemptions(redeptions);
if (redeptions.length >= (activePage) * ITEMS_PER_PAGE) {

View File

@@ -2,7 +2,6 @@ import React, {useContext, useState} from 'react';
import {Link, useNavigate} from 'react-router-dom';
import {UserContext} from '../context/User';
import {Button, Container, Icon, Menu, Segment} from 'semantic-ui-react';
import {API, getLogo, getSystemName, isAdmin, isMobile, showSuccess} from '../helpers';
import '../index.css';

View File

@@ -4,7 +4,22 @@ import {API, copy, isAdmin, showError, showSuccess, showWarning, timestamp2strin
import {ITEMS_PER_PAGE} from '../constants';
import {renderQuota, stringToColor} from '../helpers/render';
import {Avatar, Tag, Table, Button, Popover, Form, Modal, Popconfirm} from "@douyinfe/semi-ui";
import {
Avatar,
Tag,
Table,
Button,
Popover,
Form,
Modal,
Popconfirm,
SplitButtonGroup,
Dropdown
} from "@douyinfe/semi-ui";
import {
IconTreeTriangleDown,
} from '@douyinfe/semi-icons';
import EditToken from "../pages/Token/EditToken";
const {Column} = Table;
@@ -44,6 +59,13 @@ function renderStatus(status) {
}
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: 'opencat', name: 'OpenCat', value: 'opencat'},
];
const columns = [
{
title: '名称',
@@ -124,6 +146,19 @@ const TokensTable = () => {
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)}}>聊天</Button>
<Dropdown trigger="click" position="bottomRight" menu={
[
{node: 'item', key: 'next', name: 'ChatGPT Next Web', onClick: () => {onOpenLink('next', record.key)}},
{node: 'item', key: 'ama', name: 'AMA 问天BotGrem', 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="此修改将不可逆"
@@ -301,7 +336,8 @@ const TokensTable = () => {
if (chatLink) {
defaultUrl = chatLink + `/#/?settings={"key":"sk-${key}","url":"${serverAddress}"}`;
} else {
defaultUrl = `https://chat.oneapi.pro/#/?settings={"key":"sk-${key}","url":"${serverAddress}"}`;
showError('管理员未设置聊天链接')
return
}
let url;
switch (type) {

View File

@@ -42,6 +42,34 @@ export function renderNumber(num) {
}
}
export function renderNumberWithPoint(num) {
num = num.toFixed(2);
if (num >= 100000) {
// Convert number to string to manipulate it
let numStr = num.toString();
// Find the position of the decimal point
let decimalPointIndex = numStr.indexOf('.');
let wholePart = numStr;
let decimalPart = '';
// If there is a decimal point, split the number into whole and decimal parts
if (decimalPointIndex !== -1) {
wholePart = numStr.slice(0, decimalPointIndex);
decimalPart = numStr.slice(decimalPointIndex);
}
// Take the first two and last two digits of the whole number part
let shortenedWholePart = wholePart.slice(0, 2) + '..' + wholePart.slice(-2);
// Return the formatted number
return shortenedWholePart + decimalPart;
}
// If the number is less than 100,000, return it unmodified
return num;
}
export function getQuotaPerUnit() {
let quotaPerUnit = localStorage.getItem('quota_per_unit');
quotaPerUnit = parseFloat(quotaPerUnit);

View File

@@ -27,6 +27,9 @@ body {
.semi-table-tbody>.semi-table-row {
border-bottom: 1px solid rgba(0,0,0,.1);
}
.semi-space {
display: block!important;
}
}
.semi-layout {

View File

@@ -253,6 +253,7 @@ const EditChannel = (props) => {
return (
<>
<SideSheet
maskClosable={false}
placement={isEdit ? 'right' : 'left'}
title={<Title level={3}>{isEdit ? '更新渠道信息' : '创建新的渠道'}</Title>}
headerStyle={{borderBottom: '1px solid var(--semi-color-border)'}}