Compare commits

..

10 Commits

Author SHA1 Message Date
JustSong
241ade2fae fix: delete a token with a negative quota will now update the account's quota correctly (close #51) 2023-05-11 20:05:49 +08:00
chunzhi
80065de8a3 feat: add Docker Compose support (#55)
* docker-compose.yml

* Create one-api.service

配置systemd守护进程

* Update docker-compose.yml

* Update one-api.service

* Update docker-compose.yml

* Update docker-compose.yml
2023-05-11 16:56:14 +08:00
JustSong
16f53b5afb feat: double check before deleting a user 2023-05-10 10:13:39 +08:00
JustSong
3071300c0c feat: support API /dashboard/billing/credit_grants now (#45) 2023-05-10 09:28:41 +08:00
JustSong
8b056bf408 docs: update README (#47) 2023-05-05 21:56:48 +08:00
JustSong
e5640857b1 docs: add funding link 2023-05-05 15:26:54 +08:00
JustSong
331177d97e fix: return quota to user when delete token (close #37) 2023-05-04 10:20:39 +08:00
JustSong
4fed003f1a chore: update placeholder text (#36) 2023-04-29 18:42:05 +08:00
JustSong
a1ea1bf696 chore: update placeholder text (#36) 2023-04-29 18:41:19 +08:00
JustSong
7c66fc6c21 fix: shouldn't close c.Request.Body too soon (close #35) 2023-04-29 14:49:10 +08:00
15 changed files with 129 additions and 30 deletions

1
.github/FUNDING.yml vendored Normal file
View File

@@ -0,0 +1 @@
custom: ['https://iamazing.cn/page/reward']

View File

@@ -49,7 +49,7 @@ _✨ All in one 的 OpenAI 接口,整合各种 API 访问方式,开箱即用
+ [x] [OpenAI-SB](https://openai-sb.com)
+ [x] [OpenAI Max](https://openaimax.com)
+ [x] [OhMyGPT](https://www.ohmygpt.com)
+ [x] 自定义渠道
+ [x] 自定义渠道:例如使用自行搭建的 OpenAI 代理
2. 支持通过负载均衡的方式访问多个渠道。
3. 支持单个访问渠道设置多个 API Key利用起来你的多个 API Key。
4. 支持 HTTP SSE可以通过流式传输实现打字机效果。

View File

@@ -94,10 +94,6 @@ func relayHelper(c *gin.Context) error {
if err != nil {
return err
}
err = c.Request.Body.Close()
if err != nil {
return err
}
req.Header.Set("Authorization", c.Request.Header.Get("Authorization"))
req.Header.Set("Content-Type", c.Request.Header.Get("Content-Type"))
req.Header.Set("Accept", c.Request.Header.Get("Accept"))
@@ -111,7 +107,10 @@ func relayHelper(c *gin.Context) error {
if err != nil {
return err
}
err = c.Request.Body.Close()
if err != nil {
return err
}
var textResponse TextResponse
isStream := resp.Header.Get("Content-Type") == "text/event-stream"
var streamResponseText string
@@ -138,7 +137,7 @@ func relayHelper(c *gin.Context) error {
ratio = common.RatioGPT3dot5
}
quota = int(float64(quota) * ratio)
err := model.ConsumeTokenQuota(tokenId, quota)
err := model.DecreaseTokenQuota(tokenId, quota)
if err != nil {
common.SysError("Error consuming token remain quota: " + err.Error())
}

View File

@@ -75,6 +75,30 @@ func GetToken(c *gin.Context) {
return
}
func GetTokenStatus(c *gin.Context) {
tokenId := c.GetInt("token_id")
userId := c.GetInt("id")
token, err := model.GetTokenByIds(tokenId, userId)
if err != nil {
c.JSON(http.StatusOK, gin.H{
"success": false,
"message": err.Error(),
})
return
}
expiredAt := token.ExpiredTime
if expiredAt == -1 {
expiredAt = 0
}
c.JSON(http.StatusOK, gin.H{
"object": "credit_summary",
"total_granted": token.RemainQuota,
"total_used": 0, // not supported currently
"total_available": token.RemainQuota,
"expires_at": expiredAt * 1000,
})
}
func AddToken(c *gin.Context) {
isAdmin := c.GetInt("role") >= common.RoleAdminUser
token := model.Token{}

22
docker-compose.yml Normal file
View File

@@ -0,0 +1,22 @@
version: '3.4'
services:
one-api:
image: ghcr.io/songquanpeng/one-api:latest
container_name: one-api
restart: always
command: --log-dir /app/logs
ports:
- "3000:3000"
volumes:
- /home/ubuntu/data/one-api:/data
- /home/ubuntu/data/one-api/logs:/app/logs
# environment:
# REDIS_CONN_STRING: redis://default:redispw@localhost:49153
# SESSION_SECRET: random_string
# SQL_DSN: root:123456@tcp(localhost:3306)/one-api
healthcheck:
test: ["CMD-SHELL", "curl -s http://localhost:3000/api/status | grep -o '\"success\":\\s*true' | awk '{print $2}' | grep 'true'"]
interval: 30s
timeout: 10s
retries: 3

View File

@@ -55,7 +55,7 @@ func Redeem(key string, tokenId int) (quota int, err error) {
if redemption.Status != common.RedemptionCodeStatusEnabled {
return 0, errors.New("该兑换码已被使用")
}
err = TopUpTokenQuota(tokenId, redemption.Quota)
err = IncreaseTokenQuota(tokenId, redemption.Quota)
if err != nil {
return 0, err
}

View File

@@ -116,15 +116,26 @@ func DeleteTokenById(id int, userId int) (err error) {
if err != nil {
return err
}
quota := token.RemainQuota
if quota != 0 {
if quota > 0 {
err = IncreaseUserQuota(userId, quota)
} else {
err = DecreaseUserQuota(userId, -quota)
}
}
if err != nil {
return err
}
return token.Delete()
}
func ConsumeTokenQuota(id int, quota int) (err error) {
err = DB.Model(&Token{}).Where("id = ?", id).Update("remain_quota", gorm.Expr("remain_quota - ?", quota)).Error
return err
}
func TopUpTokenQuota(id int, quota int) (err error) {
func IncreaseTokenQuota(id int, quota int) (err error) {
err = DB.Model(&Token{}).Where("id = ?", id).Update("remain_quota", gorm.Expr("remain_quota + ?", quota)).Error
return err
}
func DecreaseTokenQuota(id int, quota int) (err error) {
err = DB.Model(&Token{}).Where("id = ?", id).Update("remain_quota", gorm.Expr("remain_quota - ?", quota)).Error
return err
}

View File

@@ -225,6 +225,11 @@ func GetUserQuota(id int) (quota int, err error) {
return quota, err
}
func IncreaseUserQuota(id int, quota int) (err error) {
err = DB.Model(&User{}).Where("id = ?", id).Update("quota", gorm.Expr("quota + ?", quota)).Error
return err
}
func DecreaseUserQuota(id int, quota int) (err error) {
err = DB.Model(&User{}).Where("id = ?", id).Update("quota", gorm.Expr("quota - ?", quota)).Error
return err

13
one-api.service Normal file
View File

@@ -0,0 +1,13 @@
[Unit]
Description=One API Service
After=network.target
[Service]
User=yourusername # 守护进程用户名
WorkingDirectory=/path/to/One-API # One API运行路径
ExecStart=/path/to/One-API/one-api --port 3000 --log-dir /path/to/One-API/logs # 端口
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.target

18
router/dashboard.go Normal file
View File

@@ -0,0 +1,18 @@
package router
import (
"github.com/gin-contrib/gzip"
"github.com/gin-gonic/gin"
"one-api/controller"
"one-api/middleware"
)
func SetDashboardRouter(router *gin.Engine) {
apiRouter := router.Group("/dashboard")
apiRouter.Use(gzip.Gzip(gzip.DefaultCompression))
apiRouter.Use(middleware.GlobalAPIRateLimit())
apiRouter.Use(middleware.TokenAuth())
{
apiRouter.GET("/billing/credit_grants", controller.GetTokenStatus)
}
}

View File

@@ -7,6 +7,7 @@ import (
func SetRouter(router *gin.Engine, buildFS embed.FS, indexPage []byte) {
SetApiRouter(router)
SetDashboardRouter(router)
SetRelayRouter(router)
setWebRouter(router, buildFS, indexPage)
}

View File

@@ -35,9 +35,4 @@ func SetRelayRouter(router *gin.Engine) {
relayV1Router.DELETE("/models/:model", controller.RelayNotImplemented)
relayV1Router.POST("/moderations", controller.RelayNotImplemented)
}
relayDashboardRouter := router.Group("/dashboard") // TODO: return system's own token info
relayDashboardRouter.Use(middleware.TokenAuth(), middleware.Distribute())
{
relayDashboardRouter.Any("/*path", controller.Relay)
}
}

View File

@@ -1,5 +1,5 @@
import React, { useEffect, useState } from 'react';
import { Button, Form, Label, Pagination, Table } from 'semantic-ui-react';
import { Button, Form, Label, Pagination, Popup, Table } from 'semantic-ui-react';
import { Link } from 'react-router-dom';
import { API, showError, showSuccess } from '../helpers';
@@ -237,15 +237,25 @@ const UsersTable = () => {
>
降级
</Button>
<Button
size={'small'}
negative
onClick={() => {
manageUser(user.username, 'delete', idx);
}}
<Popup
trigger={
<Button size='small' negative>
删除
</Button>
}
on='click'
flowing
hoverable
>
删除
</Button>
<Button
negative
onClick={() => {
manageUser(user.username, 'delete', idx);
}}
>
删除用户 {user.username}
</Button>
</Popup>
<Button
size={'small'}
onClick={() => {

View File

@@ -52,7 +52,7 @@ const AddChannel = () => {
<Form.Input
label='Base URL'
name='base_url'
placeholder={'请输入自定义渠道的 Base URL'}
placeholder={'请输入自定义渠道的 Base URL例如https://openai.justsong.cn'}
onChange={handleInputChange}
value={inputs.base_url}
autoComplete='off'

View File

@@ -66,7 +66,7 @@ const EditChannel = () => {
<Form.Input
label='Base URL'
name='base_url'
placeholder={'请输入新的自定义渠道的 Base URL'}
placeholder={'请输入新的自定义渠道的 Base URL例如https://openai.justsong.cn'}
onChange={handleInputChange}
value={inputs.base_url}
autoComplete='off'