mirror of
https://github.com/linux-do/new-api.git
synced 2025-11-17 19:13:42 +08:00
Compare commits
20 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
42469cb782 | ||
|
|
e1da1e31d5 | ||
|
|
0fdd4fc6e3 | ||
|
|
261dc43c4e | ||
|
|
6463e0539f | ||
|
|
c5f08a757d | ||
|
|
8a9bd08d66 | ||
|
|
751c33a6c0 | ||
|
|
57f664d0fa | ||
|
|
cdcbebce6d | ||
|
|
a29e3765f4 | ||
|
|
766e20719d | ||
|
|
4b93f185bb | ||
|
|
3b63d9bba6 | ||
|
|
b99b24f271 | ||
|
|
ac11f4bc0e | ||
|
|
44465a398b | ||
|
|
4f0419c7bc | ||
|
|
07b4a8f5d2 | ||
|
|
348e5a0df3 |
7
.github/ISSUE_TEMPLATE/config.yml
vendored
7
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -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
|
||||
|
||||
14
README.md
14
README.md
@@ -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-preview,dall-e-3,tts-1
|
||||
7. 支持 gpt-4-1106-vision-preview,dall-e-3,tts-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">
|
||||
|
||||
@@ -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 = ""
|
||||
|
||||
@@ -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, ";")
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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": "",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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)'}}
|
||||
|
||||
@@ -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'
|
||||
|
||||
Reference in New Issue
Block a user