Compare commits

...

20 Commits

Author SHA1 Message Date
CaIon
42469cb782 修复无法指定渠道id的问题 2023-12-14 16:43:20 +08:00
CaIon
e1da1e31d5 添加批量删除渠道功能 2023-12-14 16:35:03 +08:00
CaIon
0fdd4fc6e3 美化绘画IU 2023-12-14 15:16:51 +08:00
CaIon
261dc43c4e 修复 测试所有渠道按钮 失效的问题 2023-12-14 15:16:27 +08:00
CaIon
6463e0539f Warning when SESSION_SECRET is 'random_string' 2023-12-14 11:10:14 +08:00
CaIon
c5f08a757d Merge remote-tracking branch 'public/main' into latest 2023-12-14 11:02:08 +08:00
CaIon
8a9bd08d66 update README.md 2023-12-14 11:01:53 +08:00
Calcium-Ion
751c33a6c0 Merge pull request #30 from YOMIkio/email
fix: add Date header for email
2023-12-14 10:53:39 +08:00
CaIon
57f664d0fa 修复Safari无法拉起支付的问题 2023-12-13 17:02:33 +08:00
liyujie
cdcbebce6d fix: add Date header for email 2023-12-13 16:51:19 +08:00
CaIon
a29e3765f4 修复gpts无法设置倍率的问题 2023-12-13 00:29:53 +08:00
CaIon
766e20719d update README.md 2023-12-12 16:41:51 +08:00
CaIon
4b93f185bb 添加gpt-4-1106-vision-preview模型 2023-12-12 16:41:37 +08:00
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
22 changed files with 310 additions and 98 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

@@ -29,7 +29,19 @@
+ 配合项目[neko-api-key-tool](https://github.com/Calcium-Ion/neko-api-key-tool)可实现用key查询使用情况方便二次分销
5. 渠道显示已使用额度,支持指定组织访问
6. 分页支持选择每页显示数量
7. 支持gpt-4-1106-vision-previewdall-e-3tts-1
7. 支持 gpt-4-1106-vision-previewdall-e-3tts-1
8. 支持第三方模型 **gps** gpt-4-gizmo-*在渠道中添加自定义模型gpt-4-gizmo-*即可
9. 兼容原版One API的数据库可直接使用原版数据库one-api.db
## 部署
### 基于 Docker 进行部署
```shell
# 使用 SQLite 的部署命令:
docker run --name new-api -d --restart always -p 3000:3000 -e TZ=Asia/Shanghai -v /home/ubuntu/data/new-api:/data calciumion/new-api:latest
# 使用 MySQL 的部署命令,在上面的基础上添加 `-e SQL_DSN="root:123456@tcp(localhost:3306)/oneapi"`,请自行修改数据库连接参数。
# 例如:
docker run --name new-api -d --restart always -p 3000:3000 -e SQL_DSN="root:123456@tcp(localhost:3306)/oneapi" -e TZ=Asia/Shanghai -v /home/ubuntu/data/new-api:/data calciumion/new-api:latest
```
## 交流群
<img src="https://github.com/Calcium-Ion/new-api/assets/61247483/de536a8a-0161-47a7-a0a2-66ef6de81266" width="500">

View File

@@ -11,7 +11,7 @@ import (
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 = "One API"
var SystemName = "New API"
var ServerAddress = "http://localhost:3000"
var PayAddress = ""
var EpayId = ""

View File

@@ -6,6 +6,7 @@ import (
"fmt"
"net/smtp"
"strings"
"time"
)
func SendEmail(subject string, receiver string, content string) error {
@@ -16,8 +17,9 @@ func SendEmail(subject string, receiver string, content string) error {
mail := []byte(fmt.Sprintf("To: %s\r\n"+
"From: %s<%s>\r\n"+
"Subject: %s\r\n"+
"Date: %s\r\n"+
"Content-Type: text/html; charset=UTF-8\r\n\r\n%s\r\n",
receiver, SystemName, SMTPFrom, encodedSubject, content))
receiver, SystemName, SMTPFrom, encodedSubject, time.Now().Format(time.RFC1123Z), content))
auth := smtp.PlainAuth("", SMTPAccount, SMTPToken, SMTPServer)
addr := fmt.Sprintf("%s:%d", SMTPServer, SMTPPort)
to := strings.Split(receiver, ";")

View File

@@ -36,7 +36,14 @@ func init() {
}
if os.Getenv("SESSION_SECRET") != "" {
SessionSecret = os.Getenv("SESSION_SECRET")
ss := os.Getenv("SESSION_SECRET")
if ss == "random_string" {
log.Println("WARNING: SESSION_SECRET is set to the default value 'random_string', please change it to a random string.")
log.Println("警告SESSION_SECRET被设置为默认值'random_string',请修改为随机字符串。")
log.Fatal("Please set SESSION_SECRET to a random string.")
} else {
SessionSecret = ss
}
}
if os.Getenv("SQLITE_PATH") != "" {
SQLitePath = os.Getenv("SQLITE_PATH")

View File

@@ -15,6 +15,7 @@ import (
// 1 === ¥0.014 / 1k tokens
var ModelRatio = map[string]float64{
"midjourney": 50,
"gpt-4-gizmo-*": 15,
"gpt-4": 15,
"gpt-4-0314": 15,
"gpt-4-0613": 15,
@@ -23,6 +24,7 @@ var ModelRatio = map[string]float64{
"gpt-4-32k-0613": 30,
"gpt-4-1106-preview": 5, // $0.01 / 1K tokens
"gpt-4-vision-preview": 5, // $0.01 / 1K tokens
"gpt-4-1106-vision-preview": 5, // $0.01 / 1K tokens
"gpt-3.5-turbo": 0.75, // $0.0015 / 1K tokens
"gpt-3.5-turbo-0301": 0.75,
"gpt-3.5-turbo-0613": 0.75,
@@ -88,6 +90,9 @@ func UpdateModelRatioByJSONString(jsonStr string) error {
}
func GetModelRatio(name string) float64 {
if strings.HasPrefix(name, "gpt-4-gizmo") {
name = "gpt-4-gizmo-*"
}
ratio, ok := ModelRatio[name]
if !ok {
SysError("model ratio not found: " + name)

View File

@@ -151,6 +151,36 @@ func DeleteDisabledChannel(c *gin.Context) {
return
}
type ChannelBatch struct {
Ids []int `json:"ids"`
}
func DeleteChannelBatch(c *gin.Context) {
channelBatch := ChannelBatch{}
err := c.ShouldBindJSON(&channelBatch)
if err != nil || len(channelBatch.Ids) == 0 {
c.JSON(http.StatusOK, gin.H{
"success": false,
"message": "参数错误",
})
return
}
err = model.BatchDeleteChannels(channelBatch.Ids)
if err != nil {
c.JSON(http.StatusOK, gin.H{
"success": false,
"message": err.Error(),
})
return
}
c.JSON(http.StatusOK, gin.H{
"success": true,
"message": "",
"data": len(channelBatch.Ids),
})
return
}
func UpdateChannel(c *gin.Context) {
channel := model.Channel{}
err := c.ShouldBindJSON(&channel)

View File

@@ -187,6 +187,12 @@ func GetUserMidjourney(c *gin.Context) {
if logs == nil {
logs = make([]*model.Midjourney, 0)
}
if !strings.Contains(common.ServerAddress, "localhost") {
for i, midjourney := range logs {
midjourney.ImageUrl = common.ServerAddress + "/mj/image/" + midjourney.MjId
logs[i] = midjourney
}
}
c.JSON(200, gin.H{
"success": true,
"message": "",

View File

@@ -261,6 +261,15 @@ func init() {
Root: "gpt-4-vision-preview",
Parent: nil,
},
{
Id: "gpt-4-1106-vision-preview",
Object: "model",
Created: 1699593571,
OwnedBy: "openai",
Permission: permission,
Root: "gpt-4-1106-vision-preview",
Parent: nil,
},
{
Id: "text-embedding-ada-002",
Object: "model",

View File

@@ -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)

View File

@@ -91,11 +91,11 @@ func TokenAuth() func(c *gin.Context) {
key = c.Request.Header.Get("mj-api-secret")
key = strings.TrimPrefix(key, "Bearer ")
key = strings.TrimPrefix(key, "sk-")
parts := strings.Split(key, "-")
parts = strings.Split(key, "-")
key = parts[0]
} else {
key = strings.TrimPrefix(key, "sk-")
parts := strings.Split(key, "-")
parts = strings.Split(key, "-")
key = parts[0]
}
token, err := model.ValidateUserToken(key)

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

@@ -86,6 +86,25 @@ func BatchInsertChannels(channels []Channel) error {
return nil
}
func BatchDeleteChannels(ids []int) error {
//使用事务 删除channel表和channel_ability表
tx := DB.Begin()
err := tx.Where("id in (?)", ids).Delete(&Channel{}).Error
if err != nil {
// 回滚事务
tx.Rollback()
return err
}
err = tx.Where("channel_id in (?)", ids).Delete(&Ability{}).Error
if err != nil {
// 回滚事务
tx.Rollback()
}
// 提交事务
tx.Commit()
return err
}
func (channel *Channel) GetPriority() int64 {
if channel.Priority == nil {
return 0

View File

@@ -83,6 +83,7 @@ func SetApiRouter(router *gin.Engine) {
channelRoute.PUT("/", controller.UpdateChannel)
channelRoute.DELETE("/disabled", controller.DeleteDisabledChannel)
channelRoute.DELETE("/:id", controller.DeleteChannel)
channelRoute.POST("/batch", controller.DeleteChannelBatch)
}
tokenRoute := apiRouter.Group("/token")
tokenRoute.Use(middleware.UserAuth())

View File

@@ -13,7 +13,7 @@ import {
} 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,
@@ -74,6 +74,11 @@ function renderBalance(type, balance) {
const ChannelsTable = () => {
const columns = [
// {
// title: '',
// dataIndex: 'checkbox',
// className: 'checkbox',
// },
{
title: 'ID',
dataIndex: 'id',
@@ -142,8 +147,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)}}>${record.balance}</Tag>
<Tooltip content={'剩余额度' + record.balance + ',点击更新'}>
<Tag color='white' type='ghost' size='large' onClick={() => {updateChannelBalance(record)}}>${renderNumberWithPoint(record.balance)}</Tag>
</Tooltip>
</Space>
</div>
@@ -235,9 +240,11 @@ const ChannelsTable = () => {
const [channelCount, setChannelCount] = useState(pageSize);
const [groupOptions, setGroupOptions] = useState([]);
const [showEdit, setShowEdit] = useState(false);
const [enableBatchDelete, setEnableBatchDelete] = useState(false);
const [editingChannel, setEditingChannel] = useState({
id: undefined,
});
const [selectedChannels, setSelectedChannels] = useState([]);
const removeRecord = id => {
let newDataSource = [...channels];
@@ -484,6 +491,27 @@ const ChannelsTable = () => {
setUpdatingBalance(false);
};
const batchDeleteChannels = async () => {
if (selectedChannels.length === 0) {
showError('请先选择要删除的通道!');
return;
}
setLoading(true);
let ids = [];
selectedChannels.forEach((channel) => {
ids.push(channel.id);
});
const res = await API.post(`/api/channel/batch`, {ids: ids});
const {success, message, data} = res.data;
if (success) {
showSuccess(`已删除 ${data} 个通道!`);
await refresh();
} else {
showError(message);
}
setLoading(false);
}
const sortChannel = (key) => {
if (channels.length === 0) return;
setLoading(true);
@@ -557,6 +585,7 @@ const ChannelsTable = () => {
}
};
return (
<>
<EditChannel refresh={refresh} visible={showEdit} handleClose={closeEdit} editingChannel={editingChannel}/>
@@ -583,16 +612,18 @@ const ChannelsTable = () => {
</Form>
<div style={{marginTop: 10, display: 'flex'}}>
<Space>
<Typography.Text strong>使用ID排序</Typography.Text>
<Switch checked={idSort} label='使用ID排序' uncheckedText="关" aria-label="是否用ID排序" onChange={(v) => {
localStorage.setItem('id-sort', v + '')
setIdSort(v)
loadChannels(0, pageSize, v)
.then()
.catch((reason) => {
showError(reason);
})
}}></Switch>
<Space>
<Typography.Text strong>使用ID排序</Typography.Text>
<Switch checked={idSort} label='使用ID排序' uncheckedText="关" aria-label="是否用ID排序" onChange={(v) => {
localStorage.setItem('id-sort', v + '')
setIdSort(v)
loadChannels(0, pageSize, v)
.then()
.catch((reason) => {
showError(reason);
})
}}></Switch>
</Space>
</Space>
</div>
@@ -607,7 +638,15 @@ const ChannelsTable = () => {
handlePageSizeChange(size).then()
},
onPageChange: handlePageChange,
}} loading={loading} onRow={handleRow}/>
}} loading={loading} onRow={handleRow} rowSelection={
enableBatchDelete ?
{
onChange: (selectedRowKeys, selectedRows) => {
// console.log(`selectedRowKeys: ${selectedRowKeys}`, 'selectedRows: ', selectedRows);
setSelectedChannels(selectedRows);
},
} : null
}/>
<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={
@@ -622,7 +661,7 @@ const ChannelsTable = () => {
title="确定?"
okType={'warning'}
onConfirm={testAllChannels}
position={isMobile()?'top':''}
position={isMobile()?'top':'top'}
>
<Button theme='light' type='warning' style={{marginRight: 8}}>测试所有已启用通道</Button>
</Popconfirm>
@@ -648,6 +687,24 @@ const ChannelsTable = () => {
{/*</div>*/}
</div>
<div style={{marginTop: 20}}>
<Space>
<Typography.Text strong>开启批量删除</Typography.Text>
<Switch label='开启批量删除' uncheckedText="关" aria-label="是否开启批量删除" onChange={(v) => {
setEnableBatchDelete(v)
}}></Switch>
<Popconfirm
title="确定是否要删除所选通道?"
content="此修改将不可逆"
okType={'danger'}
onConfirm={batchDeleteChannels}
disabled={!enableBatchDelete}
position={'top'}
>
<Button disabled={!enableBatchDelete} theme='light' type='danger' style={{marginRight: 8}}>删除所选通道</Button>
</Popconfirm>
</Space>
</div>
</>
);
};

View File

@@ -2,7 +2,19 @@ import React, {useEffect, useState} from 'react';
import {Label} from 'semantic-ui-react';
import {API, copy, isAdmin, showError, showSuccess, timestamp2string} from '../helpers';
import {Table, Avatar, Tag, Form, Button, Layout, Select, Popover, Modal } from '@douyinfe/semi-ui';
import {
Table,
Avatar,
Tag,
Form,
Button,
Layout,
Select,
Popover,
Modal,
ImagePreview,
Typography
} from '@douyinfe/semi-ui';
import {ITEMS_PER_PAGE} from '../constants';
import {renderNumber, renderQuota, stringToColor} from '../helpers/render';
@@ -194,19 +206,16 @@ const LogsTable = () => {
}
return (
text.length > 10 ?
<>
{text.slice(0, 10)}
<Button
onClick={() => {
setModalContent(text);
setIsModalOpen(true);
}}
>
查看全部
</Button>
</>
: text
<Typography.Text
ellipsis={{ showTooltip: true }}
style={{ width: 100 }}
onClick={() => {
setModalContent(text);
setIsModalOpen(true);
}}
>
{text}
</Typography.Text>
);
}
},
@@ -220,19 +229,16 @@ const LogsTable = () => {
}
return (
text.length > 10 ?
<>
{text.slice(0, 10)}
<Button
onClick={() => {
setModalContent(text);
setIsModalOpen(true);
}}
>
查看全部
</Button>
</>
: text
<Typography.Text
ellipsis={{ showTooltip: true }}
style={{ width: 100 }}
onClick={() => {
setModalContent(text);
setIsModalOpen(true);
}}
>
{text}
</Typography.Text>
);
}
},
@@ -246,19 +252,16 @@ const LogsTable = () => {
}
return (
text.length > 10 ?
<>
{text.slice(0, 10)}
<Button
onClick={() => {
setModalContent(text);
setIsModalOpen(true);
}}
>
查看全部
</Button>
</>
: text
<Typography.Text
ellipsis={{ showTooltip: true }}
style={{ width: 100 }}
onClick={() => {
setModalContent(text);
setIsModalOpen(true);
}}
>
{text}
</Typography.Text>
);
}
}
@@ -414,15 +417,11 @@ const LogsTable = () => {
>
<p style={{ whiteSpace: 'pre-line' }}>{modalContent}</p>
</Modal>
{/* 模态框组件,用于展示图片 */}
<Modal
title="图片预览"
visible={isModalOpenurl}
onCancel={() => setIsModalOpenurl(false)}
footer={null} // 模态框不显示底部按钮
>
<img src={modalImageUrl} style={{ width: '100%' }} alt="结果图片" />
</Modal>
<ImagePreview
src={modalImageUrl}
visible={isModalOpenurl}
onVisibleChange={(visible) => setIsModalOpenurl(visible)}
/>
</Layout>
</>

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

@@ -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

@@ -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)'}}

View File

@@ -74,12 +74,17 @@ const TopUp = () => {
const {message, data} = res.data;
// showInfo(message);
if (message === 'success') {
let params = data
let url = res.data.url
let form = document.createElement('form')
form.action = url
form.method = 'POST'
form.target = '_blank'
// 判断是否为safari浏览器
let isSafari = navigator.userAgent.indexOf("Safari") > -1 && navigator.userAgent.indexOf("Chrome") < 1;
if (!isSafari) {
form.target = '_blank'
}
for (let key in params) {
let input = document.createElement('input')
input.type = 'hidden'