mirror of
https://github.com/QuantumNous/new-api.git
synced 2026-03-30 20:59:07 +00:00
Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
08f3562e53 | ||
|
|
88b0e6a768 | ||
|
|
6d4edc1f5b | ||
|
|
2d1b2676f7 | ||
|
|
1035a8e0df | ||
|
|
ea433b2ed6 | ||
|
|
bb0c504709 |
11
README.md
11
README.md
@@ -88,6 +88,17 @@
|
||||
[图文教程](BT.md)
|
||||
|
||||
### 基于 Docker 进行部署
|
||||
### 使用 Docker Compose 部署(推荐)
|
||||
```shell
|
||||
# 下载项目
|
||||
git clone https://github.com/Calcium-Ion/new-api.git
|
||||
cd new-api
|
||||
# 按需编辑 docker-compose.yml
|
||||
# 启动
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
### 直接使用 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
|
||||
|
||||
@@ -3,12 +3,13 @@ package controller
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/gin-gonic/gin"
|
||||
"net/http"
|
||||
"one-api/common"
|
||||
"one-api/model"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type OpenAIModel struct {
|
||||
@@ -48,41 +49,36 @@ func GetAllChannels(c *gin.Context) {
|
||||
if pageSize < 0 {
|
||||
pageSize = common.ItemsPerPage
|
||||
}
|
||||
channelData := make([]*model.Channel, 0)
|
||||
idSort, _ := strconv.ParseBool(c.Query("id_sort"))
|
||||
channels, err := model.GetAllChannels(p*pageSize, pageSize, false, idSort)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
"message": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
tags := make(map[string]bool)
|
||||
channelData := make([]*model.Channel, 0, len(channels))
|
||||
tagChannels := make([]*model.Channel, 0)
|
||||
for _, channel := range channels {
|
||||
channelTag := channel.GetTag()
|
||||
if channelTag != "" && !tags[channelTag] {
|
||||
tags[channelTag] = true
|
||||
tagChannel, err := model.GetChannelsByTag(channelTag)
|
||||
if err == nil {
|
||||
tagChannels = append(tagChannels, tagChannel...)
|
||||
}
|
||||
} else {
|
||||
channelData = append(channelData, channel)
|
||||
enableTagMode, _ := strconv.ParseBool(c.Query("tag_mode"))
|
||||
if enableTagMode {
|
||||
tags, err := model.GetPaginatedTags(p*pageSize, pageSize)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
"message": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
for i, channel := range tagChannels {
|
||||
find := false
|
||||
for _, can := range channelData {
|
||||
if channel.Id == can.Id {
|
||||
find = true
|
||||
break
|
||||
for _, tag := range tags {
|
||||
if tag != nil && *tag != "" {
|
||||
tagChannel, err := model.GetChannelsByTag(*tag)
|
||||
if err == nil {
|
||||
channelData = append(channelData, tagChannel...)
|
||||
}
|
||||
}
|
||||
}
|
||||
if !find {
|
||||
channelData = append(channelData, tagChannels[i])
|
||||
} else {
|
||||
channels, err := model.GetAllChannels(p*pageSize, pageSize, false, idSort)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
"message": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
channelData = channels
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
|
||||
@@ -43,8 +43,8 @@ services:
|
||||
MYSQL_DATABASE: new-api
|
||||
volumes:
|
||||
- mysql_data:/var/lib/mysql
|
||||
ports:
|
||||
- "3306:3306" # If you want to access MySQL from outside Docker, uncomment
|
||||
# ports:
|
||||
# - "3306:3306" # If you want to access MySQL from outside Docker, uncomment
|
||||
|
||||
volumes:
|
||||
mysql_data:
|
||||
|
||||
@@ -212,6 +212,7 @@ func TokenAuth() func(c *gin.Context) {
|
||||
}
|
||||
c.Set("id", token.UserId)
|
||||
c.Set("token_id", token.Id)
|
||||
c.Set("token_key", token.Key)
|
||||
c.Set("token_name", token.Name)
|
||||
c.Set("token_unlimited_quota", token.UnlimitedQuota)
|
||||
if !token.UnlimitedQuota {
|
||||
|
||||
@@ -2,9 +2,10 @@ package model
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"gorm.io/gorm"
|
||||
"one-api/common"
|
||||
"strings"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type Channel struct {
|
||||
@@ -403,3 +404,9 @@ func DeleteDisabledChannel() (int64, error) {
|
||||
result := DB.Where("status = ? or status = ?", common.ChannelStatusAutoDisabled, common.ChannelStatusManuallyDisabled).Delete(&Channel{})
|
||||
return result.RowsAffected, result.Error
|
||||
}
|
||||
|
||||
func GetPaginatedTags(offset int, limit int) ([]*string, error) {
|
||||
var tags []*string
|
||||
err := DB.Model(&Channel{}).Select("DISTINCT tag").Where("tag != ''").Offset(offset).Limit(limit).Find(&tags).Error
|
||||
return tags, err
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ type RelayInfo struct {
|
||||
ChannelType int
|
||||
ChannelId int
|
||||
TokenId int
|
||||
TokenKey string
|
||||
UserId int
|
||||
Group string
|
||||
TokenUnlimited bool
|
||||
@@ -58,6 +59,7 @@ func GenRelayInfo(c *gin.Context) *RelayInfo {
|
||||
channelId := c.GetInt("channel_id")
|
||||
|
||||
tokenId := c.GetInt("token_id")
|
||||
tokenKey := c.GetString("token_key")
|
||||
userId := c.GetInt("id")
|
||||
group := c.GetString("group")
|
||||
tokenUnlimited := c.GetBool("token_unlimited_quota")
|
||||
@@ -73,6 +75,7 @@ func GenRelayInfo(c *gin.Context) *RelayInfo {
|
||||
ChannelType: channelType,
|
||||
ChannelId: channelId,
|
||||
TokenId: tokenId,
|
||||
TokenKey: tokenKey,
|
||||
UserId: userId,
|
||||
Group: group,
|
||||
TokenUnlimited: tokenUnlimited,
|
||||
|
||||
@@ -22,7 +22,7 @@ func PreWssConsumeQuota(ctx *gin.Context, relayInfo *relaycommon.RelayInfo, usag
|
||||
return err
|
||||
}
|
||||
|
||||
token, err := model.CacheGetTokenByKey(strings.TrimLeft(relayInfo.ApiKey, "sk-"))
|
||||
token, err := model.CacheGetTokenByKey(strings.TrimLeft(relayInfo.TokenKey, "sk-"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -317,7 +317,7 @@ const ChannelsTable = () => {
|
||||
position={'left'}
|
||||
onConfirm={() => {
|
||||
manageChannel(record.id, 'delete', record).then(() => {
|
||||
removeRecord(record.id);
|
||||
removeRecord(record);
|
||||
});
|
||||
}}
|
||||
>
|
||||
@@ -365,7 +365,7 @@ const ChannelsTable = () => {
|
||||
okType={'danger'}
|
||||
position={'left'}
|
||||
onConfirm={async () => {
|
||||
copySelectedChannel(record.id);
|
||||
copySelectedChannel(record);
|
||||
}}
|
||||
>
|
||||
<Button theme="light" type="primary" style={{ marginRight: 1 }}>
|
||||
@@ -439,12 +439,24 @@ const ChannelsTable = () => {
|
||||
const [editingTag, setEditingTag] = useState('');
|
||||
const [selectedChannels, setSelectedChannels] = useState([]);
|
||||
const [showEditPriority, setShowEditPriority] = useState(false);
|
||||
const [enableTagMode, setEnableTagMode] = useState(false);
|
||||
|
||||
|
||||
const removeRecord = (id) => {
|
||||
const removeRecord = (record) => {
|
||||
let newDataSource = [...channels];
|
||||
if (id != null) {
|
||||
let idx = newDataSource.findIndex((data) => data.id === id);
|
||||
if (record.id != null) {
|
||||
let idx = newDataSource.findIndex((data) => {
|
||||
if (data.children !== undefined) {
|
||||
for (let i = 0; i < data.children.length; i++) {
|
||||
if (data.children[i].id === record.id) {
|
||||
data.children.splice(i, 1);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return data.id === record.id
|
||||
}
|
||||
});
|
||||
|
||||
if (idx > -1) {
|
||||
newDataSource.splice(idx, 1);
|
||||
@@ -453,13 +465,12 @@ const ChannelsTable = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const setChannelFormat = (channels) => {
|
||||
const setChannelFormat = (channels, enableTagMode) => {
|
||||
let channelDates = [];
|
||||
let channelTags = {};
|
||||
for (let i = 0; i < channels.length; i++) {
|
||||
channels[i].key = '' + channels[i].id;
|
||||
|
||||
if (channels[i].tag === '' || channels[i].tag === null) {
|
||||
if (!enableTagMode) {
|
||||
let test_models = [];
|
||||
channels[i].models.split(',').forEach((item, index) => {
|
||||
test_models.push({
|
||||
@@ -543,10 +554,10 @@ const ChannelsTable = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const loadChannels = async (startIdx, pageSize, idSort) => {
|
||||
const loadChannels = async (startIdx, pageSize, idSort, enableTagMode) => {
|
||||
setLoading(true);
|
||||
const res = await API.get(
|
||||
`/api/channel/?p=${startIdx}&page_size=${pageSize}&id_sort=${idSort}`
|
||||
`/api/channel/?p=${startIdx}&page_size=${pageSize}&id_sort=${idSort}&tag_mode=${enableTagMode}`
|
||||
);
|
||||
if (res === undefined) {
|
||||
return;
|
||||
@@ -554,11 +565,11 @@ const ChannelsTable = () => {
|
||||
const { success, message, data } = res.data;
|
||||
if (success) {
|
||||
if (startIdx === 0) {
|
||||
setChannelFormat(data);
|
||||
setChannelFormat(data, enableTagMode);
|
||||
} else {
|
||||
let newChannels = [...channels];
|
||||
newChannels.splice(startIdx * pageSize, data.length, ...data);
|
||||
setChannelFormat(newChannels);
|
||||
setChannelFormat(newChannels, enableTagMode);
|
||||
}
|
||||
} else {
|
||||
showError(message);
|
||||
@@ -566,11 +577,8 @@ const ChannelsTable = () => {
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
const copySelectedChannel = async (id) => {
|
||||
const channelToCopy = channels.find(
|
||||
(channel) => String(channel.id) === String(id)
|
||||
);
|
||||
console.log(channelToCopy);
|
||||
const copySelectedChannel = async (record) => {
|
||||
const channelToCopy = record
|
||||
channelToCopy.name += '_复制';
|
||||
channelToCopy.created_time = null;
|
||||
channelToCopy.balance = 0;
|
||||
@@ -594,7 +602,7 @@ const ChannelsTable = () => {
|
||||
};
|
||||
|
||||
const refresh = async () => {
|
||||
await loadChannels(activePage - 1, pageSize, idSort);
|
||||
await loadChannels(activePage - 1, pageSize, idSort, enableTagMode);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
@@ -604,7 +612,7 @@ const ChannelsTable = () => {
|
||||
parseInt(localStorage.getItem('page-size')) || ITEMS_PER_PAGE;
|
||||
setIdSort(localIdSort);
|
||||
setPageSize(localPageSize);
|
||||
loadChannels(0, localPageSize, localIdSort)
|
||||
loadChannels(0, localPageSize, localIdSort, enableTagMode)
|
||||
.then()
|
||||
.catch((reason) => {
|
||||
showError(reason);
|
||||
@@ -762,18 +770,22 @@ const ChannelsTable = () => {
|
||||
|
||||
const searchChannels = async (searchKeyword, searchGroup, searchModel) => {
|
||||
if (searchKeyword === '' && searchGroup === '' && searchModel === '') {
|
||||
// if keyword is blank, load files instead.
|
||||
await loadChannels(0, pageSize, idSort);
|
||||
await loadChannels(0, pageSize, idSort, enableTagMode);
|
||||
setActivePage(1);
|
||||
return;
|
||||
}
|
||||
setSearching(true);
|
||||
const res = await API.get(
|
||||
`/api/channel/search?keyword=${searchKeyword}&group=${searchGroup}&model=${searchModel}&id_sort=${idSort}`
|
||||
`/api/channel/search?keyword=${searchKeyword}&group=${searchGroup}&model=${searchModel}&id_sort=${idSort}&tag_mode=${enableTagMode}`
|
||||
);
|
||||
const { success, message, data } = res.data;
|
||||
if (success) {
|
||||
setChannelFormat(data);
|
||||
if (enableTagMode) {
|
||||
setChannelFormat(data, enableTagMode);
|
||||
} else {
|
||||
setChannels(data.map(channel => ({...channel, key: '' + channel.id})));
|
||||
setChannelCount(data.length);
|
||||
}
|
||||
setActivePage(1);
|
||||
} else {
|
||||
showError(message);
|
||||
@@ -879,7 +891,7 @@ const ChannelsTable = () => {
|
||||
setActivePage(page);
|
||||
if (page === Math.ceil(channels.length / pageSize) + 1) {
|
||||
// In this case we have to load more data and then append them.
|
||||
loadChannels(page - 1, pageSize, idSort).then((r) => {
|
||||
loadChannels(page - 1, pageSize, idSort, enableTagMode).then((r) => {
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -888,7 +900,7 @@ const ChannelsTable = () => {
|
||||
localStorage.setItem('page-size', size + '');
|
||||
setPageSize(size);
|
||||
setActivePage(1);
|
||||
loadChannels(0, size, idSort)
|
||||
loadChannels(0, size, idSort, enableTagMode)
|
||||
.then()
|
||||
.catch((reason) => {
|
||||
showError(reason);
|
||||
@@ -1044,7 +1056,7 @@ const ChannelsTable = () => {
|
||||
onChange={(v) => {
|
||||
localStorage.setItem('id-sort', v + '');
|
||||
setIdSort(v);
|
||||
loadChannels(0, pageSize, v)
|
||||
loadChannels(0, pageSize, v, enableTagMode)
|
||||
.then()
|
||||
.catch((reason) => {
|
||||
showError(reason);
|
||||
@@ -1145,6 +1157,22 @@ const ChannelsTable = () => {
|
||||
</Popconfirm>
|
||||
</Space>
|
||||
</div>
|
||||
<div style={{ marginTop: 20 }}>
|
||||
<Space>
|
||||
<Typography.Text strong>标签聚合模式</Typography.Text>
|
||||
<Switch
|
||||
checked={enableTagMode}
|
||||
label="标签聚合模式"
|
||||
uncheckedText="关"
|
||||
aria-label="是否启用标签聚合"
|
||||
onChange={(v) => {
|
||||
setEnableTagMode(v);
|
||||
// 切换模式时重新加载数据
|
||||
loadChannels(0, pageSize, idSort, v);
|
||||
}}
|
||||
/>
|
||||
</Space>
|
||||
</div>
|
||||
|
||||
|
||||
<Table
|
||||
|
||||
Reference in New Issue
Block a user